This commit is contained in:
Francesco Sansalvadore committed 2026-04-02 17:08:36 +02:00
1 parent 5591c35f6b
commit 263eb2dae3
13 files changed
+389 -238

No files matched your search

@@ -39,7 +39,7 @@ export function UserDropdown({
<DropdownMenuTrigger asChild className={cn('border flex-shrink-0 px-3', triggerClassName)}>
<Button
type="default"
className="[&>span]:flex px-0 py-0 rounded-full overflow-hidden h-8 w-8"
className="[&>span]:flex px-0 py-0 rounded-full overflow-hidden w-8 h-8"
>
{isLoading ? (
<div className="w-full h-full flex items-center justify-center">
@@ -3,9 +3,7 @@ import Link from 'next/link'
import { PropsWithChildren, ReactNode } from 'react'
import { cn, Menu } from 'ui'
import { NavUser } from '../NavigationV2/NavUser'
import type { SidebarSection } from './AccountLayout.types'
import { useAppStateSnapshot } from '@/state/app-state'
interface WithSidebarProps {
title: string
@@ -132,9 +130,6 @@ export const SidebarContent = ({
</Menu>
</div>
</div>
<footer className="p-2">
<NavUser />
</footer>
</div>
</div>
)
@@ -5,8 +5,8 @@ import { AnimatePresence, motion } from 'framer-motion'
import { ChevronLeft } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ReactNode, useMemo } from 'react'
import { Badge, cn } from 'ui'
import { ReactNode } from 'react'
import { cn } from 'ui'
import { CommandMenuTriggerInput } from 'ui-patterns'
import { BreadcrumbsView } from './BreadcrumbsView'
@@ -14,6 +14,7 @@ import { FeedbackDropdown } from './FeedbackDropdown/FeedbackDropdown'
import { HomeIcon } from './HomeIcon'
import { LocalVersionPopover } from './LocalVersionPopover'
import { MergeRequestButton } from './MergeRequestButton'
import OrgProjectBranchNav from './OrgProjectBranchNav'
import {
useIsBranching2Enabled,
useIsFloatingMobileToolbarEnabled,
@@ -24,15 +25,9 @@ import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
import { UserDropdown } from '@/components/interfaces/UserDropdown'
import { AdvisorButton } from '@/components/layouts/AppLayout/AdvisorButton'
import { AssistantButton } from '@/components/layouts/AppLayout/AssistantButton'
import { BranchDropdown } from '@/components/layouts/AppLayout/BranchDropdown'
import { InlineEditorButton } from '@/components/layouts/AppLayout/InlineEditorButton'
import { OrganizationDropdown } from '@/components/layouts/AppLayout/OrganizationDropdown'
import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
import { HelpButton } from '@/components/ui/HelpPanel/HelpButton'
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
import { IS_PLATFORM } from '@/lib/constants'
@@ -69,9 +64,8 @@ export const LayoutHeader = ({
backToDashboardURL,
}: LayoutHeaderProps) => {
const router = useRouter()
const { ref: projectRef, slug } = useParams()
const { ref: projectRef } = useParams()
const { data: selectedProject } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const gitlessBranching = useIsBranching2Enabled()
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
@@ -79,29 +73,12 @@ export const LayoutHeader = ({
const isAccountPage = router.pathname.startsWith('/account')
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
const { data: orgUsage } = useOrgUsageQuery(
{ orgSlug: selectedOrganization?.slug },
{ enabled: selectedOrganization?.usage_billing_enabled === false }
)
const exceedingLimits = useMemo(() => {
if (orgUsage) {
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
} else {
return false
}
}, [orgUsage])
const isNewProject =
selectedProject?.inserted_at !== undefined &&
dayjs(selectedProject.inserted_at).isAfter(dayjs().subtract(5, 'day'))
const connectButtonType = isNewProject ? 'primary' : 'default'
// show org selection if we are on a project page or on a explicit org route
const showOrgSelection = slug || (selectedOrganization && projectRef)
return (
<>
<header
@@ -146,46 +123,7 @@ export const LayoutHeader = ({
</div>
<div className="hidden md:flex items-center text-sm">
<HomeIcon />
<div className="flex items-center md:pl-2">
{showOrgSelection && IS_PLATFORM ? (
<>
<LayoutHeaderDivider className="hidden md:block" />
<OrganizationDropdown />
</>
) : null}
<AnimatePresence>
{projectRef && (
<motion.div
className="flex items-center"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
{IS_PLATFORM && <LayoutHeaderDivider />}
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
{selectedProject && IS_PLATFORM && (
<>
<LayoutHeaderDivider />
<BranchDropdown />
</>
)}
</motion.div>
)}
</AnimatePresence>
</div>
<OrgProjectBranchNav className="ml-2" />
<AnimatePresence>
{headerTitle && (
<motion.div
@@ -0,0 +1,82 @@
import { IS_PLATFORM, useParams } from 'common'
import { AnimatePresence, motion } from 'framer-motion'
import { Link } from 'lucide-react'
import { useMemo } from 'react'
import { Badge, cn } from 'ui'
import { LayoutHeaderDivider } from './LayoutHeader'
import { BranchDropdown } from '@/components/layouts/AppLayout/BranchDropdown'
import { OrganizationDropdown } from '@/components/layouts/AppLayout/OrganizationDropdown'
import { ProjectDropdown } from '@/components/layouts/AppLayout/ProjectDropdown'
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
const OrgProjectBranchNav = ({ className }: { className?: string }) => {
const { ref: projectRef, slug } = useParams()
const { data: selectedProject } = useSelectedProjectQuery()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
const { data: orgUsage } = useOrgUsageQuery(
{ orgSlug: selectedOrganization?.slug },
{ enabled: selectedOrganization?.usage_billing_enabled === false }
)
const exceedingLimits = useMemo(() => {
if (orgUsage) {
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
} else {
return false
}
}, [orgUsage])
// show org selection if we are on a project page or on a explicit org route
const showOrgSelection = slug || (selectedOrganization && projectRef)
return (
<div className={cn('flex items-center', className)}>
{showOrgSelection && IS_PLATFORM ? (
<>
<LayoutHeaderDivider />
<OrganizationDropdown />
</>
) : null}
<AnimatePresence>
{projectRef && (
<motion.div
className="flex items-center"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
{IS_PLATFORM && <LayoutHeaderDivider />}
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
{selectedProject && IS_PLATFORM && (
<>
<LayoutHeaderDivider />
<BranchDropdown />
</>
)}
</motion.div>
)}
</AnimatePresence>
</div>
)
}
export default OrgProjectBranchNav
@@ -13,7 +13,7 @@ import type { Organization } from '@/types'
export interface ProjectBranchSelectorTriggerProps {
displayProjectName: string
selectedOrg: Organization
selectedOrg: Organization | undefined
isBranch: boolean
isProductionBranch: boolean
branchDisplayName: string
@@ -1,16 +1,14 @@
import { IS_PLATFORM } from 'lib/constants'
import { Plug, Search } from 'lucide-react'
import { Plug } from 'lucide-react'
import { parseAsBoolean, useQueryState } from 'nuqs'
import { Button, Sidebar, SidebarContent, SidebarFooter, SidebarHeader } from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { Button, Sidebar, SidebarContent, SidebarHeader } from 'ui'
import { OrgSelector } from '../Navigation/NavigationBar/OrgSelector'
import { ProjectBranchSelector } from '../Navigation/NavigationBar/ProjectBranchSelector'
import { NavGroup } from './NavGroup'
import { NavUser } from './NavUser'
import { useAppSidebarNavItems, type AppSidebarScope } from './useAppSidebarNavItems'
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
import { OrgSelector } from '@/components/layouts/Navigation/NavigationBar/OrgSelector'
import { ProjectBranchSelector } from '@/components/layouts/Navigation/NavigationBar/ProjectBranchSelector'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from '@/lib/constants'
export type { AppSidebarScope }
@@ -19,7 +17,6 @@ interface AppSidebarV2Props {
}
export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
const setCommandMenuOpen = useSetCommandMenuOpen()
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
const { data: selectedOrganization } = useSelectedOrganizationQuery()
@@ -34,40 +31,38 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
organizationItems,
} = useAppSidebarNavItems({ scope })
const showSelector = IS_PLATFORM && (isProjectScope || selectedOrganization)
return (
<>
<Sidebar
collapsible="none"
className="hidden md:flex h-full w-full border-r border-default group"
>
<SidebarHeader className="gap-2 pt-2">
<SidebarHeader className="gap-2">
<div className="space-y-2">
{isProjectScope ? <ProjectBranchSelector /> : IS_PLATFORM ? <OrgSelector /> : null}
{isProjectScope && (
<div className="flex items-center gap-2 px-1.5">
<Button
type="outline"
size="small"
onClick={() => setCommandMenuOpen(true)}
className="h-8 min-w-8 !w-8 px-0 justify-center text-foreground-lighter font-normal bg-transparent"
icon={<Search strokeWidth={1.5} />}
/>
<Button
type="default"
size="small"
disabled={!isActiveHealthy}
onClick={() => setShowConnect(true)}
className="h-8 flex-1 justify-center gap-0 pl-2"
icon={<Plug className="rotate-90" strokeWidth={1.5} />}
>
<span>Connect</span>
</Button>
</div>
)}
{showSelector &&
(isProjectScope ? (
<ProjectBranchSelector />
) : (
selectedOrganization && <OrgSelector />
))}
<div className="flex items-center px-0.5">
<Button
type="default"
size="small"
disabled={!isActiveHealthy}
onClick={() => setShowConnect(true)}
className="h-7 flex-1 justify-center gap-0 pl-2"
icon={<Plug className="rotate-90" strokeWidth={1.5} />}
>
Connect
</Button>
</div>
</div>
</SidebarHeader>
<div className="relative min-h-0 flex-1 overflow-hidden">
<SidebarContent className="h-full gap-0">
<SidebarContent className="h-full gap-0 pb-8">
{isProjectScope ? (
<>
<NavGroup id="project" label="Project" items={projectItems} />
@@ -75,26 +70,25 @@ export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
<NavGroup id="platform" label="Platform" items={platformItems} />
<NavGroup id="observability" label="Observability" items={observabilityItems} />
<NavGroup id="integrations" label="Integrations" items={integrationsItems} />
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-3 bg-gradient-to-b from-sidebar to-transparent"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-3 bg-gradient-to-t from-sidebar to-transparent"
/>
</>
) : selectedOrganization ? (
) : (
<NavGroup
id="organization"
label="Organization"
label=""
items={organizationItems}
isCollapsible={false}
/>
) : null}
)}
</SidebarContent>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 top-0 z-10 h-4 bg-gradient-to-b from-sidebar to-transparent"
/>
<div
aria-hidden
className="pointer-events-none absolute inset-x-0 bottom-0 z-10 h-4 bg-gradient-to-t from-sidebar to-transparent"
/>
</div>
<SidebarFooter>{IS_PLATFORM && <NavUser />}</SidebarFooter>
</Sidebar>
<ConnectSheet />
@@ -9,38 +9,37 @@ import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import { DefaultLayoutProviders } from '../DefaultLayoutProviders'
import { AppSidebarV2 } from './AppSidebar'
import { RightRailLayout } from './RightIconRail'
import { LayoutHeader } from './LayoutHeader'
import { RightPanelToolbarLayout } from './RightPanelToolbar'
export interface DefaultLayoutV2Props {
headerTitle?: string
hideMobileMenu?: boolean
}
const leftSidebarMinSize = 180
const leftSidebarMinSize = 160
const leftSidebarMaxSize = 450
const leftSidebarDefaultSize = '250px'
const leftSidebarDefaultSize = '230px'
const mainContentClass = 'flex h-full min-h-0 min-w-0 flex-col overflow-hidden'
/**
* New three-column layout for the dashboard (V2 navigation).
*
* Layout structure:
* 1. Left sidebar - navigation with groups (Database, Platform, Observability, Integrations)
* 2. Main content area - page content (no secondary nav bar)
* 3. Right icon rail - AI, SQL, Alerts, Help panels
*
* This replaces DefaultLayout + ProjectLayout + feature-specific layouts (AuthLayout, DatabaseLayout, etc.)
* when the navigation V2 feature flag is enabled. The key difference is that there is no longer a
* secondary product menu sidebar - all navigation is handled in the primary sidebar with collapsible groups.
* V2 navigation shell: mobile bar, then AppSidebar (when shown) + main column. Main column has
* LayoutHeader above the row of page content and right panel toolbar.
*/
export const DefaultLayoutV2 = ({
children,
headerTitle,
hideMobileMenu,
}: PropsWithChildren<DefaultLayoutV2Props>) => {
const router = useRouter()
const isMobile = useBreakpoint('md')
const appSnap = useAppStateSnapshot()
const scope = router.pathname.startsWith('/project') ? 'project' : 'organization'
const showLeftSidebar = !isMobile && !router.pathname.startsWith('/account')
const showLeftSidebar =
!isMobile &&
!router.pathname.startsWith('/account') &&
!router.pathname.startsWith('/organizations')
const [lastVisitedOrganization] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.LAST_VISITED_ORGANIZATION,
@@ -55,17 +54,30 @@ export const DefaultLayoutV2 = ({
: '/organizations'
: undefined
const header = (
<LayoutHeader headerTitle={headerTitle} backToDashboardURL={backToDashboardURL} scope={scope} />
)
const contentRow = <RightPanelToolbarLayout>{children}</RightPanelToolbarLayout>
const mainColumn = (
<div className={mainContentClass}>
<div className="shrink-0">{header}</div>
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">{contentRow}</div>
</div>
)
return (
<DefaultLayoutProviders>
<div className="flex flex-col h-screen w-screen">
<div className="flex h-screen w-screen flex-col">
<AppBannerWrapper />
<div className="flex-shrink-0">
<div className="shrink-0">
<MobileNavigationBar
hideMobileMenu={hideMobileMenu}
backToDashboardURL={backToDashboardURL}
/>
</div>
<RightRailLayout>
<div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
{showLeftSidebar ? (
<ResizablePanelGroup
orientation="horizontal"
@@ -82,17 +94,14 @@ export const DefaultLayoutV2 = ({
<AppSidebarV2 scope={scope} />
</ResizablePanel>
<ResizableHandle withHandle className="hidden md:flex bg-background" />
<ResizablePanel
id="panel-v2-main-content"
className="h-full min-h-0 min-w-0 overflow-hidden"
>
<div className="flex h-full min-h-0 flex-1 overflow-hidden">{children}</div>
<ResizablePanel id="panel-v2-main-column" className={mainContentClass}>
{mainColumn}
</ResizablePanel>
</ResizablePanelGroup>
) : (
<div className="flex h-full min-h-0 flex-1 overflow-hidden">{children}</div>
mainColumn
)}
</RightRailLayout>
</div>
</div>
</DefaultLayoutProviders>
)
@@ -0,0 +1,186 @@
import { LOCAL_STORAGE_KEYS } from 'common'
import { DevToolbarTrigger } from 'dev-tools'
import { AnimatePresence, motion } from 'framer-motion'
import { ChevronLeft } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useMemo, type ReactNode } from 'react'
import { Badge, cn } from 'ui'
import { CommandMenuTriggerInput } from 'ui-patterns'
import { useAppSidebarNavItems, type AppSidebarScope } from './useAppSidebarNavItems'
import { useIsFloatingMobileToolbarEnabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
import { LocalDropdown } from '@/components/interfaces/LocalDropdown'
import { UserDropdown } from '@/components/interfaces/UserDropdown'
import { BreadcrumbsView } from '@/components/layouts/Navigation/LayoutHeader/BreadcrumbsView'
import { FeedbackDropdown } from '@/components/layouts/Navigation/LayoutHeader/FeedbackDropdown/FeedbackDropdown'
import { LayoutHeaderDivider } from '@/components/layouts/Navigation/LayoutHeader/LayoutHeader'
import { LocalVersionPopover } from '@/components/layouts/Navigation/LayoutHeader/LocalVersionPopover'
import { getResourcesExceededLimitsOrg } from '@/components/ui/OveragesBanner/OveragesBanner.utils'
import { useOrgUsageQuery } from '@/data/usage/org-usage-query'
import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage'
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
import { IS_PLATFORM } from '@/lib/constants'
interface LayoutHeaderProps {
customHeaderComponents?: ReactNode
breadcrumbs?: unknown[]
headerTitle?: string
backToDashboardURL?: string
scope?: AppSidebarScope
}
export const LayoutHeader = ({
customHeaderComponents,
breadcrumbs = [],
headerTitle,
backToDashboardURL,
scope,
}: LayoutHeaderProps) => {
const router = useRouter()
const { data: selectedOrganization } = useSelectedOrganizationQuery()
const showFloatingMobileToolbar = useIsFloatingMobileToolbarEnabled()
const [commandMenuEnabled] = useLocalStorageQuery(LOCAL_STORAGE_KEYS.HOTKEY_COMMAND_MENU, true)
const isAccountPage = router.pathname.startsWith('/account')
// We only want to query the org usage and check for possible over-ages for plans without usage billing enabled (free or pro with spend cap)
const { data: orgUsage } = useOrgUsageQuery(
{ orgSlug: selectedOrganization?.slug },
{ enabled: selectedOrganization?.usage_billing_enabled === false }
)
const { isProjectScope } = useAppSidebarNavItems({ scope })
const exceedingLimits = useMemo(() => {
if (orgUsage) {
return getResourcesExceededLimitsOrg(orgUsage?.usages || []).length > 0
} else {
return false
}
}, [orgUsage])
return (
<>
<header
className={cn(
'flex h-11 md:h-12 items-center flex-shrink-0 border-b',
showFloatingMobileToolbar && 'hidden md:flex'
)}
>
{backToDashboardURL && isAccountPage && (
<div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
<Link
href={backToDashboardURL}
className="flex items-center justify-center border-none !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px] text-foreground-lighter hover:text-foreground transition-colors"
>
<ChevronLeft strokeWidth={1.5} size={16} />
</Link>
</div>
)}
<div
className={cn(
'flex items-center justify-between h-full px-1 pl-3 flex-1 overflow-x-auto gap-x-4'
)}
>
<div className="hidden md:flex items-center justify-start text-sm gap-x-2">
{/* <HomeIcon />
<OrgProjectBranchNav /> */}
{/* {IS_PLATFORM && (
<div>
{isProjectScope ? (
<ProjectBranchSelector />
) : (
selectedOrganization && <OrgSelector />
)}
</div>
)} */}
<AnimatePresence>
{headerTitle && (
<motion.div
className="flex items-center"
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
exit={{ opacity: 0, x: -20 }}
transition={{
duration: 0.15,
ease: 'easeOut',
}}
>
{/* <HomeIcon className="pr-2" /> */}
<LayoutHeaderDivider />
<span className="text-foreground">{headerTitle}</span>
</motion.div>
)}
</AnimatePresence>
<BreadcrumbsView defaultValue={breadcrumbs} />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
</div>
{/* <div className="flex items-center justify-center gap-x-2">
<CommandMenuTriggerInput
showShortcut={commandMenuEnabled}
placeholder="Search..."
className={cn(
'flex max-w-32 xl:max-w-40 rounded-full bg-transparent border-strong',
'[&_.command-shortcut>div]:border-none',
'[&_.command-shortcut>div]:pr-2',
'[&_.command-shortcut>div]:bg-transparent',
'[&_.command-shortcut>div]:text-foreground-lighter'
)}
/>
</div> */}
<div className="flex items-center justify-end gap-x-2">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM ? (
<>
<DevToolbarTrigger />
<FeedbackDropdown />
<CommandMenuTriggerInput
showShortcut={commandMenuEnabled}
placeholder="Search..."
className={cn(
'flex max-w-32 xl:max-w-12 w-auto rounded-full bg-transparent border-strong',
'[&_.command-shortcut>div]:border-none',
'[&_.command-shortcut>div]:pr-2',
'[&_.command-shortcut>div]:bg-transparent',
'[&_.command-shortcut>div]:text-foreground-lighter'
)}
/>
<UserDropdown triggerClassName="hidden md:flex" />
</>
) : (
<>
<LocalVersionPopover />
<CommandMenuTriggerInput
showShortcut={commandMenuEnabled}
placeholder="Search..."
className={cn(
'flex max-w-32 xl:max-w-32 rounded-full bg-transparent border-strong',
'[&_.command-shortcut>div]:border-none',
'[&_.command-shortcut>div]:pr-2',
'[&_.command-shortcut>div]:bg-transparent',
'[&_.command-shortcut>div]:text-foreground-lighter'
)}
/>
<LocalDropdown triggerClassName="hidden md:flex" />
</>
)}
</div>
</div>
</header>
<ConnectSheet />
</>
)
}
@@ -65,14 +65,13 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
<SidebarMenuItem>
<CollapsibleTrigger asChild>
<SidebarMenuButton
tooltip={item.title}
isActive={item.isActive}
className="gap-2 text-foreground-light !py-1.5 !h-7"
className="gap-2 text-foreground-lighter !h-6"
>
<NavItemIcon icon={item.icon} />
<span className="truncate">{item.title}</span>
{item.label && (
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none font-medium">
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none">
{item.label}
</Badge>
)}
@@ -100,16 +99,15 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
) : (
<SidebarMenuItem key={item.title}>
<SidebarMenuButton
tooltip={item.title}
isActive={item.isActive}
asChild
className="gap-2 text-foreground-light !py-1.5 !h-7"
className="gap-2 text-foreground-lighter !h-6"
>
<Link href={item.url}>
<NavItemIcon icon={item.icon} />
<span>{item.title}</span>
{item.label && (
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none font-medium">
<Badge className="ml-1 px-1.5 py-0.5 bg-transparent !border-stronger text-[10px] leading-none">
{item.label}
</Badge>
)}
@@ -131,7 +129,7 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
<SidebarGroup>
<CollapsibleTrigger asChild>
<SidebarGroupLabel
className={`group/label text-foreground-lighter flex items-center gap-1 ${
className={`group/label text-foreground-lighter/70 flex items-center gap-1 !h-6 ${
isCollapsible ? 'cursor-pointer hover:text-foreground-light' : 'pointer-events-none'
}`}
>
@@ -139,7 +137,7 @@ export function NavGroup({ id, label, items, isCollapsible = true }: NavGroupPro
{isCollapsible && (
<ChevronRight
strokeWidth={1.5}
className="!w-4 !h-4 text-foreground-muted group-hover/label:text-foreground-light transition-transform duration-200 group-data-[state=open]/group-collapsible:rotate-90 hidden group-hover:block"
className="!w-4 !h-4 text-foreground-muted group-hover/label:text-foreground-lighter transition-transform duration-200 group-data-[state=open]/group-collapsible:rotate-90 hidden group-hover:block"
/>
)}
</SidebarGroupLabel>
@@ -1,61 +0,0 @@
import { UserDropdownContent } from 'components/interfaces/UserDropdown'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useProfileNameAndPicture } from 'lib/profile'
import { ChevronsUpDown, Loader2 } from 'lucide-react'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuTrigger,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
useSidebar,
} from 'ui'
export function NavUser() {
const { isMobile } = useSidebar()
const profileShowEmailEnabled = useIsFeatureEnabled('profile:show_email')
const { username, primaryEmail, avatarUrl, isLoading } = useProfileNameAndPicture()
return (
<SidebarMenu>
<SidebarMenuItem>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
size="lg"
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground"
>
{isLoading ? (
<div className="flex aspect-square size-8 items-center justify-center">
<Loader2 className="animate-spin text-foreground-lighter" size={16} />
</div>
) : (
<ProfileImage alt={username} src={avatarUrl} className="size-8 rounded-full" />
)}
<div className="grid flex-1 text-left text-sm leading-tight">
<span className="truncate font-medium">{username}</span>
{primaryEmail !== username && profileShowEmailEnabled && (
<span className="truncate text-xs text-foreground-light">{primaryEmail}</span>
)}
</div>
<ChevronsUpDown
strokeWidth={1}
className="ml-auto text-foreground-light hidden group-hover:block !w-4 !h-4"
/>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent
className="w-[--radix-dropdown-menu-trigger-width] min-w-56 rounded-lg"
side={isMobile ? 'bottom' : 'right'}
align="end"
sideOffset={4}
>
<UserDropdownContent />
</DropdownMenuContent>
</DropdownMenu>
</SidebarMenuItem>
</SidebarMenu>
)
}
@@ -18,13 +18,13 @@ import { useMobileSheet } from '../Navigation/NavigationBar/MobileSheetContext'
import { isSidebarId } from '../ProjectLayout/LayoutSidebar'
import { SIDEBAR_KEYS } from '../ProjectLayout/LayoutSidebar/LayoutSidebarProvider'
interface RailItem {
interface ToolbarItem {
id: string
label: string
icon: (props: { isActive: boolean }) => ReactNode
}
const RAIL_ITEMS: RailItem[] = [
const TOOLBAR_ITEMS: ToolbarItem[] = [
{
id: SIDEBAR_KEYS.AI_ASSISTANT,
label: 'Assistant',
@@ -47,7 +47,6 @@ const RAIL_ITEMS: RailItem[] = [
icon: () => <Lightbulb size={16} strokeWidth={1.5} />,
},
{
// id: SIDEBAR_KEYS.SUPPORT_PANEL,
id: SIDEBAR_KEYS.HELP_PANEL,
label: 'Help & Support',
icon: () => <HelpCircle size={16} strokeWidth={1.5} />,
@@ -58,7 +57,7 @@ const RIGHT_SIDEBAR_MIN_SIZE_PERCENTAGE = 300
const RIGHT_SIDEBAR_DEFAULT_SIZE_PERCENTAGE = 340
const RIGHT_SIDEBAR_MAX_SIZE_PERCENTAGE = 750
function RightIconRail() {
function RightPanelToolbar() {
const { activeSidebar, toggleSidebar } = useSidebarManagerSnapshot()
const isMobile = useBreakpoint('md')
@@ -78,15 +77,14 @@ function RightIconRail() {
return (
<aside className="bg-dash-sidebar text-foreground-lighter border-default flex w-10 shrink-0 border-l">
<nav className="flex flex-1 flex-col items-center justify-center gap-2 py-2 pt-3">
{RAIL_ITEMS.map((item) => {
<nav className="flex flex-1 flex-col items-center justify-start gap-1 p-1">
{TOOLBAR_ITEMS.map((item) => {
const isActive = activeSidebar?.id === item.id
return (
<Tooltip>
<Tooltip key={item.id}>
<TooltipTrigger>
<button
key={item.id}
type="button"
aria-label={item.label}
aria-pressed={isActive}
@@ -116,48 +114,60 @@ function RightSidebarPanel() {
if (!activeSidebar?.component) return null
return (
<aside className="bg-background border-default flex h-full min-h-0 w-full flex-col overflow-hidden border-l">
<aside className="bg-background border-default flex h-full min-h-0 min-w-0 flex-1 flex-col overflow-hidden border-l">
{activeSidebar.component()}
</aside>
)
}
export function RightRailLayout({ children }: { children: ReactNode }) {
export function RightPanelToolbarLayout({ children }: { children: ReactNode }) {
const isMobile = useBreakpoint('md')
const { activeSidebar } = useSidebarManagerSnapshot()
const showRightSidebar = activeSidebar?.component !== undefined
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
{showRightSidebar ? (
const main = <div className="h-full min-h-0 min-w-0 overflow-hidden">{children}</div>
if (isMobile) {
return <div className="flex min-h-0 flex-1 flex-col overflow-hidden">{main}</div>
}
if (showRightSidebar) {
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
<ResizablePanelGroup
orientation="horizontal"
autoSaveId="default-layout-v2-right-sidebar"
className="hidden md:flex min-h-0 min-w-0 flex-1 overflow-hidden"
className="min-h-0 min-w-0 flex-1 overflow-hidden"
>
<ResizablePanel
id="panel-v2-right-main-content"
className="h-full min-h-0 min-w-0 overflow-hidden"
className="min-h-0 min-w-0 overflow-hidden"
>
<div className="min-h-0 min-w-0 h-full">{children}</div>
{main}
</ResizablePanel>
<ResizableHandle withHandle className="hidden md:flex bg-background" />
<ResizableHandle withHandle className="bg-background" />
<ResizablePanel
id="panel-v2-right-sidebar"
minSize={RIGHT_SIDEBAR_MIN_SIZE_PERCENTAGE}
maxSize={RIGHT_SIDEBAR_MAX_SIZE_PERCENTAGE}
defaultSize={RIGHT_SIDEBAR_DEFAULT_SIZE_PERCENTAGE}
className="h-full min-h-0 overflow-hidden bg-background"
className="flex min-h-0 overflow-hidden bg-background"
>
<RightSidebarPanel />
<div className="flex h-full min-h-0 w-full min-w-0">
<RightSidebarPanel />
<RightPanelToolbar />
</div>
</ResizablePanel>
</ResizablePanelGroup>
) : (
<div className="min-h-0 min-w-0 flex-1">{children}</div>
)}
</div>
)
}
<div className="hidden md:flex md:shrink-0">
<RightIconRail />
return (
<div className="flex min-h-0 flex-1 overflow-hidden">
<div className="min-h-0 min-w-0 flex-1">{main}</div>
<div className="hidden shrink-0 md:flex">
<RightPanelToolbar />
</div>
</div>
)
@@ -3,4 +3,4 @@ export { ProjectLayoutV2, ProjectLayoutV2WithAuth } from './ProjectLayout'
export { AppSidebarV2 } from './AppSidebar'
export { NavGroup } from './NavGroup'
export type { NavGroupItem, NavGroupProps } from './NavGroup'
export { RightRailLayout } from './RightIconRail'
export { RightPanelToolbarLayout } from './RightPanelToolbar'
@@ -413,7 +413,7 @@ const SidebarGroup = React.forwardRef<HTMLDivElement, React.ComponentProps<'div'
<div
ref={ref}
data-sidebar="group"
className={cn('relative flex w-full min-w-0 flex-col px-2 py-1', className)}
className={cn('relative flex w-full min-w-0 flex-col p-2', className)}
{...props}
/>
)
@@ -503,7 +503,7 @@ SidebarMenuItem.displayName = 'SidebarMenuItem'
const sidebarMenuButtonVariants = cva(
cn(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1.5 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground'
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-1.5 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 group-has-[[data-sidebar=menu-action]]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:text-sidebar-accent-foreground data-[state=open]:hover:bg-sidebar-accent data-[state=open]:hover:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground'
),
{
variants: {