mirror of
https://github.com/supabase/supabase.git
synced 2026-10-14 21:59:16 +03:00
collapsible sidebar
This commit is contained in:
12 files changed
+399
-315
No files matched your search
@@ -30,15 +30,18 @@ import {
|
||||
import { Route } from '../ui/ui.types'
|
||||
import {
|
||||
useIsAPIDocsSidePanelEnabled,
|
||||
useIsNavigationV2Enabled,
|
||||
useIsPlatformWebhooksEnabled,
|
||||
useUnifiedLogsPreview,
|
||||
} from './App/FeaturePreview/FeaturePreviewContext'
|
||||
import { ConnectSheet } from './ConnectSheet/ConnectSheet'
|
||||
import {
|
||||
generateOtherRoutes,
|
||||
generateProductRoutes,
|
||||
generateSettingsRoutes,
|
||||
generateToolRoutes,
|
||||
} from '@/components/layouts/Navigation/NavigationBar/NavigationBar.utils'
|
||||
import { SidebarHeader } from '@/components/layouts/NavigationV2/SidebarHeader'
|
||||
import { ProjectIndexPageLink } from '@/data/prefetchers/project.$ref'
|
||||
import { useSendEventMutation } from '@/data/telemetry/send-event-mutation'
|
||||
import { useHideSidebar } from '@/hooks/misc/useHideSidebar'
|
||||
@@ -65,6 +68,7 @@ export interface SidebarProps extends ComponentPropsWithoutRef<typeof SidebarPri
|
||||
export const Sidebar = ({ className, ...props }: SidebarProps) => {
|
||||
const { setOpen } = useSidebar()
|
||||
const hideSideBar = useHideSidebar()
|
||||
const isNavigationV2 = useIsNavigationV2Enabled()
|
||||
|
||||
const [sidebarBehaviour, setSidebarBehaviour] = useLocalStorageQuery(
|
||||
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
|
||||
@@ -78,52 +82,69 @@ export const Sidebar = ({ className, ...props }: SidebarProps) => {
|
||||
}, [sidebarBehaviour, setOpen])
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{!hideSideBar && (
|
||||
<SidebarMotion
|
||||
{...props}
|
||||
className={cn('z-50', className)}
|
||||
transition={{ delay: 0.4, duration: 0.4 }}
|
||||
overflowing={sidebarBehaviour === 'expandable'}
|
||||
collapsible="icon"
|
||||
variant="sidebar"
|
||||
onMouseEnter={() => {
|
||||
if (sidebarBehaviour === 'expandable') setOpen(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (sidebarBehaviour === 'expandable') setOpen(false)
|
||||
}}
|
||||
>
|
||||
<SidebarContent
|
||||
footer={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className={`w-min px-1.5 mx-0.5 ${sidebarBehaviour === 'open' ? '!px-2' : ''}`}
|
||||
icon={<PanelLeftDashed size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start" className="w-40">
|
||||
<DropdownMenuRadioGroup
|
||||
value={sidebarBehaviour}
|
||||
onValueChange={(value) => setSidebarBehaviour(value as SidebarBehaviourType)}
|
||||
>
|
||||
<DropdownMenuLabel>Sidebar control</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioItem value="open">Expanded</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="closed">Collapsed</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="expandable">
|
||||
Expand on hover
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
</SidebarMotion>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
<>
|
||||
<AnimatePresence>
|
||||
{!hideSideBar && (
|
||||
<SidebarMotion
|
||||
{...props}
|
||||
className={cn('z-50', className)}
|
||||
transition={{ delay: 0.4, duration: 0.4 }}
|
||||
overflowing={sidebarBehaviour === 'expandable'}
|
||||
collapsible="icon"
|
||||
variant="sidebar"
|
||||
onMouseEnter={() => {
|
||||
if (sidebarBehaviour === 'expandable') setOpen(true)
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (sidebarBehaviour === 'expandable') setOpen(false)
|
||||
}}
|
||||
>
|
||||
{isNavigationV2 && <SidebarHeader variant="desktop" />}
|
||||
<SidebarContent
|
||||
footer={
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
type="text"
|
||||
className={cn('w-min px-1.5 mx-0.5', sidebarBehaviour === 'open' && '!px-2')}
|
||||
icon={<PanelLeftDashed size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent side="top" align="start" className="w-40">
|
||||
<DropdownMenuRadioGroup
|
||||
value={sidebarBehaviour}
|
||||
onValueChange={(value) => setSidebarBehaviour(value as SidebarBehaviourType)}
|
||||
>
|
||||
<DropdownMenuLabel>Sidebar control</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuRadioItem value="open">Expanded</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="closed">Collapsed</DropdownMenuRadioItem>
|
||||
<DropdownMenuRadioItem value="expandable">
|
||||
Expand on hover
|
||||
</DropdownMenuRadioItem>
|
||||
</DropdownMenuRadioGroup>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
}
|
||||
/>
|
||||
</SidebarMotion>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
{isNavigationV2 && <ConnectSheet />}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export function SidebarMobileSheetMenu() {
|
||||
const router = useRouter()
|
||||
const sheetScope = router.pathname.startsWith('/project') ? 'project' : 'organization'
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-sidebar text-foreground">
|
||||
<SidebarHeader variant="mobile-sheet" scope={sheetScope} />
|
||||
<SidebarContent />
|
||||
<ConnectSheet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -131,30 +152,33 @@ export const SidebarContent = ({ footer }: { footer?: ReactNode }) => {
|
||||
const { ref: projectRef } = useParams()
|
||||
|
||||
return (
|
||||
<>
|
||||
<AnimatePresence mode="wait">
|
||||
<SidebarContentPrimitive>
|
||||
{projectRef ? (
|
||||
<motion.div key="project-links">
|
||||
<ProjectLinks />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="org-links"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
<OrganizationLinks />
|
||||
</motion.div>
|
||||
)}
|
||||
</SidebarContentPrimitive>
|
||||
</AnimatePresence>
|
||||
<SidebarFooter>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<AnimatePresence mode="wait">
|
||||
<SidebarContentPrimitive>
|
||||
{projectRef ? (
|
||||
<motion.div key="project-links" className="flex min-h-0 min-w-0 flex-1 flex-col">
|
||||
<ProjectLinks />
|
||||
</motion.div>
|
||||
) : (
|
||||
<motion.div
|
||||
key="org-links"
|
||||
className="flex min-h-0 min-w-0 flex-1 flex-col"
|
||||
initial={{ opacity: 0, y: -20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: -20 }}
|
||||
transition={{ duration: 0.2, ease: 'easeOut' }}
|
||||
>
|
||||
<OrganizationLinks />
|
||||
</motion.div>
|
||||
)}
|
||||
</SidebarContentPrimitive>
|
||||
</AnimatePresence>
|
||||
</div>
|
||||
<SidebarFooter className="shrink-0">
|
||||
<SidebarGroup className="p-0">{footer}</SidebarGroup>
|
||||
</SidebarFooter>
|
||||
</>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -208,7 +232,7 @@ const ActiveDot = ({ hasErrors, hasWarnings }: { hasErrors: boolean; hasWarnings
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'absolute pointer-events-none flex h-2 w-2 left-[18px] group-data-[state=expanded]:left-[20px] top-2 z-10 rounded-full',
|
||||
'absolute pointer-events-none flex h-2 w-2 left-[18px] top-2 z-10 rounded-full',
|
||||
hasErrors ? 'bg-destructive-600' : hasWarnings ? 'bg-warning-600' : 'bg-transparent'
|
||||
)}
|
||||
/>
|
||||
@@ -226,7 +250,6 @@ const ProjectLinks = () => {
|
||||
const { mutate: sendEvent } = useSendEventMutation()
|
||||
|
||||
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
|
||||
const platformWebhooksEnabled = useIsPlatformWebhooksEnabled()
|
||||
const { isEnabled: isUnifiedLogsEnabled } = useUnifiedLogsPreview()
|
||||
|
||||
const activeRoute = router.pathname.split('/')[3]
|
||||
|
||||
@@ -30,7 +30,7 @@ import { useOrgProjectsInfiniteQuery } from '@/data/projects/org-projects-infini
|
||||
import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
|
||||
export function OrgSelector() {
|
||||
export function OrgSelector({ isCollapsed = false }: { isCollapsed?: boolean }) {
|
||||
const router = useRouter()
|
||||
const { slug: routeSlug } = useParams()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
@@ -57,31 +57,78 @@ export function OrgSelector() {
|
||||
if (isLoadingOrganizations)
|
||||
return (
|
||||
<ShimmeringLoader
|
||||
className={cn('ml-1 w-[120px]', isNavigationV2 && 'ml-0 w-full h-10 rounded-md')}
|
||||
className={cn(
|
||||
'ml-1 w-[120px]',
|
||||
isNavigationV2 && 'ml-0 h-10 w-full rounded-md',
|
||||
isCollapsed && 'min-w-0 max-w-full'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
const collapsedTooltipContent = isCollapsed ? (
|
||||
<div className="max-w-[280px] text-left text-sm">
|
||||
<div className="flex min-w-0 flex-1 flex-col pl-0.5 pr-1 text-left">
|
||||
<div className="truncate font-medium leading-tight text-foreground-light group-hover:text-foreground">
|
||||
{selectedOrganization?.name ?? 'Select organization'}
|
||||
</div>
|
||||
{selectedOrganization && (
|
||||
<div className="flex items-center gap-1 truncate text-xs leading-tight text-foreground-lighter group-hover:text-foreground-light">
|
||||
<Boxes className="size-3 shrink-0" strokeWidth={1.5} />
|
||||
<span className="min-w-0 truncate">{projectsLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
const triggerButton = (
|
||||
<SidebarMenuButton
|
||||
size="lg"
|
||||
className="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground gap-2 h-10 text-left group px-1.5 py-1 touch-manipulation"
|
||||
hasIcon={!isCollapsed}
|
||||
tooltip={
|
||||
isCollapsed
|
||||
? {
|
||||
children: collapsedTooltipContent,
|
||||
className: 'p-1',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'group/org-selector touch-manipulation text-left data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground hover:bg-transparent',
|
||||
isCollapsed
|
||||
? 'h-9 gap-1 !bg-transparent !px-0.5 !py-0.5 overflow-visible'
|
||||
: 'h-9 gap-2 px-1.5 py-1'
|
||||
)}
|
||||
onClick={isMobile ? () => setOpen(true) : undefined}
|
||||
>
|
||||
<div className="flex min-w-0 flex-1 flex-col text-left pl-0.5">
|
||||
<div className="truncate text-foreground font-medium leading-tight min-w-[100px] max-w-[250px]">
|
||||
{selectedOrganization?.name ?? 'Select organization'}
|
||||
</div>
|
||||
{selectedOrganization && (
|
||||
<div className="flex items-center gap-1 truncate text-foreground-light leading-tight text-xs">
|
||||
<Boxes className="shrink-0 size-3" strokeWidth={1.5} />
|
||||
<span>{projectsLabel}</span>
|
||||
{!isCollapsed ? (
|
||||
<>
|
||||
<div className="flex min-w-0 flex-1 flex-col pl-0.5 text-left">
|
||||
<div className="min-w-[100px] max-w-[250px] truncate font-medium leading-tight text-foreground-light group-hover/org-selector:text-foreground">
|
||||
{selectedOrganization?.name ?? 'Select organization'}
|
||||
</div>
|
||||
{selectedOrganization && (
|
||||
<div className="flex items-center gap-1 truncate text-xs leading-tight text-foreground-lighter group-hover/org-selector:text-foreground-light">
|
||||
<Boxes className="size-3 shrink-0" strokeWidth={1.5} />
|
||||
<span className="min-w-0 truncate">{projectsLabel}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<ChevronsUpDown
|
||||
strokeWidth={1}
|
||||
className="ml-auto text-foreground-light md:hidden md:group-hover:block !w-4 !h-4"
|
||||
/>
|
||||
<div className="p-1 rounded-md group-hover/org-selector:bg-surface-200 hover:!bg-selection group-hover/org-selector:text-foreground">
|
||||
<ChevronsUpDown
|
||||
strokeWidth={1.5}
|
||||
className={cn(
|
||||
'ml-auto !h-4 !w-4 text-foreground-lighter group-hover:text-foreground-light',
|
||||
isCollapsed && 'hidden'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative flex h-8 aspect-square shrink-0 items-center bg-background-muted group-hover:border-stronger justify-center rounded border border-strong text-xs">
|
||||
<ChevronsUpDown strokeWidth={1.5} className="!h-4 !w-4 text-foreground-lighter" />
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenuButton>
|
||||
)
|
||||
|
||||
@@ -108,7 +155,11 @@ export function OrgSelector() {
|
||||
<SidebarMenuItem>
|
||||
<Popover_Shadcn_ open={open} onOpenChange={setOpen} modal={false}>
|
||||
<PopoverTrigger_Shadcn_ asChild>{triggerButton}</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="p-0" side="bottom" align="start">
|
||||
<PopoverContent_Shadcn_
|
||||
className="p-0"
|
||||
side="bottom"
|
||||
align={isCollapsed ? 'center' : 'start'}
|
||||
>
|
||||
<Command_Shadcn_>
|
||||
<CommandInput_Shadcn_ placeholder="Find organization..." />
|
||||
<CommandList_Shadcn_>
|
||||
|
||||
@@ -23,7 +23,7 @@ import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganizati
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM } from '@/lib/constants'
|
||||
|
||||
export function ProjectBranchSelector() {
|
||||
export function ProjectBranchSelector({ isCollapsed = false }: { isCollapsed?: boolean }) {
|
||||
const router = useRouter()
|
||||
const { ref } = useParams()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
@@ -63,7 +63,11 @@ export function ProjectBranchSelector() {
|
||||
if (isLoadingProject || !displayProject)
|
||||
return (
|
||||
<ShimmeringLoader
|
||||
className={cn('w-[120px] ml-1 md:py-3', isNavigationV2 && 'ml-0 w-full h-10 rounded-md')}
|
||||
className={cn(
|
||||
'ml-1 w-[120px] md:py-3',
|
||||
isNavigationV2 && 'ml-0 h-10 w-full rounded-md',
|
||||
isCollapsed && 'min-w-0 max-w-full'
|
||||
)}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -86,6 +90,7 @@ export function ProjectBranchSelector() {
|
||||
isProductionBranch,
|
||||
branchDisplayName,
|
||||
onGoToOrganization: goToOrganization,
|
||||
isCollapsed,
|
||||
}
|
||||
|
||||
if (isMobile) {
|
||||
@@ -113,7 +118,7 @@ export function ProjectBranchSelector() {
|
||||
<PopoverTrigger_Shadcn_ asChild>
|
||||
<ProjectBranchSelectorTrigger {...triggerProps} />
|
||||
</PopoverTrigger_Shadcn_>
|
||||
<PopoverContent_Shadcn_ className="p-0 w-[780px]" side="bottom" align="start">
|
||||
<PopoverContent_Shadcn_ className="w-[780px] p-0" side="bottom" align="start">
|
||||
<ProjectBranchSelectorPopover onClose={() => setOpen(false)} />
|
||||
</PopoverContent_Shadcn_>
|
||||
</Popover_Shadcn_>
|
||||
|
||||
+108
-37
@@ -18,6 +18,7 @@ export interface ProjectBranchSelectorTriggerProps {
|
||||
isProductionBranch: boolean
|
||||
branchDisplayName: string
|
||||
onGoToOrganization: () => void
|
||||
isCollapsed?: boolean
|
||||
}
|
||||
|
||||
export const ProjectBranchSelectorTrigger = React.forwardRef<
|
||||
@@ -27,46 +28,116 @@ export const ProjectBranchSelectorTrigger = React.forwardRef<
|
||||
React.ComponentPropsWithoutRef<typeof SidebarMenuButtonComponent>,
|
||||
keyof ProjectBranchSelectorTriggerProps
|
||||
>
|
||||
>(({ displayProjectName, selectedOrg, isBranch, branchDisplayName, ...buttonProps }, ref) => {
|
||||
const selectedOrgInitial = getSelectedOrgInitial(selectedOrg?.name ?? 'O')
|
||||
>(
|
||||
(
|
||||
{
|
||||
displayProjectName,
|
||||
selectedOrg,
|
||||
isBranch,
|
||||
branchDisplayName,
|
||||
isCollapsed,
|
||||
className,
|
||||
...buttonProps
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const selectedOrgInitial = getSelectedOrgInitial(selectedOrg?.name ?? 'O')
|
||||
|
||||
return (
|
||||
<SidebarMenuButtonComponent
|
||||
ref={ref}
|
||||
size="lg"
|
||||
className="group py-1 gap-1.5 w-full flex h-auto text-left data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground touch-manipulation"
|
||||
{...buttonProps}
|
||||
>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="relative flex h-8 aspect-square shrink-0 items-center bg-background-muted group-hover:border-stronger justify-center rounded border border-strong text-xs">
|
||||
{selectedOrgInitial}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{selectedOrg?.name}</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="text-left flex-grow min-w-0">
|
||||
<div className="text-left flex-grow min-w-0">
|
||||
<div className="w-full truncate text-foreground leading-tight max-w-[250px]">
|
||||
{displayProjectName}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-0.5',
|
||||
isBranch ? 'text-foreground-lighter' : 'text-warning'
|
||||
)}
|
||||
>
|
||||
<GitBranch className="shrink-0 size-3" strokeWidth={1.5} />
|
||||
<span className="truncate min-w-0 leading-tight text-xs">{branchDisplayName}</span>
|
||||
const collapsedTooltipContent = isCollapsed ? (
|
||||
<div className="flex min-w-[120px] max-w-[250px] items-start gap-1.5 text-left text-sm pr-1 text-foreground-light">
|
||||
<div className="relative flex aspect-square h-8 shrink-0 items-center justify-center rounded border border-strong bg-background-muted text-xs">
|
||||
{selectedOrgInitial}
|
||||
</div>
|
||||
<div className="min-w-0 flex-1 text-left">
|
||||
<div className="min-w-0 flex-grow text-left">
|
||||
<div className="w-full truncate leading-tight">{displayProjectName}</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-0.5',
|
||||
isBranch ? 'text-foreground-lighter' : 'text-warning'
|
||||
)}
|
||||
>
|
||||
<GitBranch className="size-3 shrink-0" strokeWidth={1.5} />
|
||||
<span className="min-w-0 truncate text-xs leading-tight">{branchDisplayName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
|
||||
<ChevronsUpDown
|
||||
strokeWidth={1.5}
|
||||
className="ml-auto text-foreground-lighter !w-4 !h-4 md:hidden md:group-hover:flex"
|
||||
/>
|
||||
</SidebarMenuButtonComponent>
|
||||
)
|
||||
})
|
||||
return (
|
||||
<SidebarMenuButtonComponent
|
||||
ref={ref}
|
||||
size="lg"
|
||||
hasIcon={!isCollapsed}
|
||||
tooltip={
|
||||
isCollapsed
|
||||
? {
|
||||
children: collapsedTooltipContent,
|
||||
className: 'p-1 bg-sidebar',
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
className={cn(
|
||||
'group/project-branch-selector !bg-transparent flex h-auto w-full touch-manipulation text-left data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground hover:bg-transparent',
|
||||
isCollapsed ? 'min-h-9 gap-1 !pl-0 !py-0.5 overflow-visible' : 'gap-1.5 pl-0 py-0.5',
|
||||
className
|
||||
)}
|
||||
{...buttonProps}
|
||||
>
|
||||
{!isCollapsed ? (
|
||||
<>
|
||||
<Tooltip>
|
||||
<TooltipTrigger>
|
||||
<div className="relative flex h-8 aspect-square shrink-0 items-center bg-background-muted group-hover/project-branch-selector:border-stronger justify-center rounded border border-strong text-xs">
|
||||
{selectedOrgInitial}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{selectedOrg?.name}</TooltipContent>
|
||||
</Tooltip>
|
||||
<div className="text-left flex-grow min-w-0">
|
||||
<div className="text-left flex-grow min-w-0">
|
||||
<div
|
||||
className={cn(
|
||||
'w-full truncate leading-tight transition-colors text-foreground-light group-hover/project-branch-selector:text-foreground',
|
||||
isCollapsed ? 'max-w-full min-w-0' : 'max-w-[250px]'
|
||||
)}
|
||||
>
|
||||
{displayProjectName}
|
||||
</div>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-0.5',
|
||||
isBranch
|
||||
? 'text-foreground-lighter group-hover/project-branch-selector:text-foreground-light'
|
||||
: 'text-warning'
|
||||
)}
|
||||
>
|
||||
<GitBranch className="shrink-0 size-3" strokeWidth={1.5} />
|
||||
<span className="truncate min-w-0 leading-tight text-xs">
|
||||
{branchDisplayName}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-1 rounded-md group-hover:bg-surface-200 hover:!bg-selection group-hover:text-foreground">
|
||||
<ChevronsUpDown
|
||||
strokeWidth={1.5}
|
||||
className={cn(
|
||||
'ml-auto !h-4 !w-4 text-foreground-lighter group-hover:text-foreground-light',
|
||||
isCollapsed && 'hidden'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="relative flex h-8 aspect-square shrink-0 items-center bg-background-muted group-hover:border-stronger justify-center rounded border border-strong text-xs">
|
||||
<ChevronsUpDown strokeWidth={1.5} className="!h-4 !w-4 text-foreground-lighter" />
|
||||
</div>
|
||||
)}
|
||||
</SidebarMenuButtonComponent>
|
||||
)
|
||||
}
|
||||
)
|
||||
ProjectBranchSelectorTrigger.displayName = 'ProjectBranchSelectorTrigger'
|
||||
@@ -1,147 +0,0 @@
|
||||
import { useParams } from 'common'
|
||||
import { Plug } from 'lucide-react'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { Button, cn, Sidebar, SidebarHeader } from 'ui'
|
||||
import { ShimmeringLoader } from 'ui-patterns'
|
||||
|
||||
import type { AppSidebarScope } from './app-sidebar.types'
|
||||
import { ConnectSheet } from '@/components/interfaces/ConnectSheet/ConnectSheet'
|
||||
import { SidebarContent as StudioSidebarNav } from '@/components/interfaces/Sidebar'
|
||||
import { OrgSelector } from '@/components/layouts/Navigation/NavigationBar/OrgSelector'
|
||||
import { ProjectBranchSelector } from '@/components/layouts/Navigation/NavigationBar/ProjectBranchSelector'
|
||||
import { useOrganizationsQuery } from '@/data/organizations/organizations-query'
|
||||
import { useProjectDetailQuery } from '@/data/projects/project-detail-query'
|
||||
import { useHideSidebar } from '@/hooks/misc/useHideSidebar'
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
|
||||
|
||||
export type { AppSidebarScope } from './app-sidebar.types'
|
||||
|
||||
type AppSidebarNavBodyVariant = 'desktop' | 'mobile-sheet'
|
||||
|
||||
interface AppSidebarNavBodyProps {
|
||||
scope?: AppSidebarScope
|
||||
variant?: AppSidebarNavBodyVariant
|
||||
}
|
||||
|
||||
/**
|
||||
* Shared nav chrome (selectors, Connect) + V1 `SidebarContent` link tree for desktop and mobile sheet.
|
||||
*/
|
||||
function AppSidebarNavBody({ scope, variant = 'desktop' }: AppSidebarNavBodyProps) {
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
const router = useRouter()
|
||||
const { slug: orgRouteSlug } = useParams()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const { isPending: isLoadingOrganizations } = useOrganizationsQuery()
|
||||
const { data: project, isPending: isLoadingProject } = useSelectedProjectQuery()
|
||||
|
||||
const resolvedScope: AppSidebarScope =
|
||||
scope ?? (router.pathname.startsWith('/project') ? 'project' : 'organization')
|
||||
const isProjectScope = resolvedScope === 'project'
|
||||
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
||||
|
||||
const isBranch = project != null && project.parentRef !== project.ref
|
||||
const { data: parentProject, isPending: isLoadingParentProject } = useProjectDetailQuery(
|
||||
{ ref: project?.parent_project_ref },
|
||||
{ enabled: Boolean(isProjectScope && isBranch && project?.parent_project_ref) }
|
||||
)
|
||||
const selectedProject = parentProject ?? project
|
||||
|
||||
const isOrgSelectorLoading = isLoadingOrganizations
|
||||
const isProjectSelectorLoading =
|
||||
isLoadingProject || (isBranch && isLoadingParentProject) || !selectedProject
|
||||
|
||||
const shouldShowOrgSelector =
|
||||
!isProjectScope && IS_PLATFORM && (Boolean(orgRouteSlug) || Boolean(selectedOrganization))
|
||||
|
||||
const isHeaderLoading =
|
||||
(shouldShowOrgSelector && isOrgSelectorLoading) || (isProjectScope && isProjectSelectorLoading)
|
||||
|
||||
const isMobileSheet = variant === 'mobile-sheet'
|
||||
const selectorHeaderClass = cn('flex-col gap-2', isMobileSheet ? 'flex' : 'hidden md:flex')
|
||||
|
||||
return (
|
||||
<>
|
||||
<SidebarHeader className={selectorHeaderClass}>
|
||||
{isHeaderLoading ? (
|
||||
<ShimmeringLoader
|
||||
className={
|
||||
isProjectScope
|
||||
? 'p-2 md:mr-2 md:w-[90px]'
|
||||
: cn('p-2 md:mr-2 w-[90px]', isMobileSheet && 'w-full max-w-none')
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
{shouldShowOrgSelector && <OrgSelector />}
|
||||
{isProjectScope && (
|
||||
<>
|
||||
<ProjectBranchSelector />
|
||||
<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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</SidebarHeader>
|
||||
<div className="relative flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">
|
||||
<div className="min-h-0 min-w-0 flex-1 overflow-y-auto">
|
||||
<StudioSidebarNav />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
interface AppSidebarV2Props {
|
||||
scope?: AppSidebarScope
|
||||
}
|
||||
|
||||
export function AppSidebarV2({ scope }: AppSidebarV2Props = {}) {
|
||||
const hideSidebar = useHideSidebar()
|
||||
|
||||
return (
|
||||
<>
|
||||
{!hideSidebar && (
|
||||
<Sidebar
|
||||
collapsible="none"
|
||||
className="hidden md:flex h-full w-full border-r border-default group"
|
||||
>
|
||||
<AppSidebarNavBody scope={scope} variant="desktop" />
|
||||
</Sidebar>
|
||||
)}
|
||||
|
||||
<ConnectSheet />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Full-height V2 navigation for `StudioMobileSheetNav`, opened via `registerOpenMenu` from
|
||||
* {@link ProjectLayoutV2}. Scope follows the current route like {@link DefaultLayoutV2}.
|
||||
*/
|
||||
export function AppSidebarMobileSheetMenu() {
|
||||
const router = useRouter()
|
||||
const sheetScope: AppSidebarScope = router.pathname.startsWith('/project')
|
||||
? 'project'
|
||||
: 'organization'
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col bg-sidebar text-foreground">
|
||||
<AppSidebarNavBody scope={sheetScope} variant="mobile-sheet" />
|
||||
<ConnectSheet />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -5,12 +5,12 @@ import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
|
||||
import { useRouter } from 'next/router'
|
||||
import type { PropsWithChildren } from 'react'
|
||||
import { useAppStateSnapshot } from 'state/app-state'
|
||||
import { ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
|
||||
import { cn } from 'ui'
|
||||
|
||||
import { DefaultLayoutProviders } from '../DefaultLayoutProviders'
|
||||
import { AppSidebarV2 } from './AppSidebar'
|
||||
import { LayoutHeader } from './LayoutHeader'
|
||||
import { RightPanelToolbarLayout } from './RightPanelToolbar'
|
||||
import { Sidebar } from '@/components/interfaces/Sidebar'
|
||||
import { useHideSidebar } from '@/hooks/misc/useHideSidebar'
|
||||
|
||||
export interface DefaultLayoutV2Props {
|
||||
@@ -18,14 +18,10 @@ export interface DefaultLayoutV2Props {
|
||||
hideMobileMenu?: boolean
|
||||
}
|
||||
|
||||
const leftSidebarMinSize = 160
|
||||
const leftSidebarMaxSize = 300
|
||||
const leftSidebarDefaultSize = '230px'
|
||||
|
||||
const mainContentClass = 'flex h-full w-full min-h-0 min-w-0 flex-col overflow-hidden'
|
||||
|
||||
/**
|
||||
* V2 navigation shell: mobile bar, then AppSidebar (when shown) + main column. Main column has
|
||||
* V2 navigation shell: mobile bar, then primary `Sidebar` (when shown) + main column. Main column has
|
||||
* LayoutHeader above the row of page content and right panel toolbar.
|
||||
*/
|
||||
export const DefaultLayoutV2 = ({
|
||||
@@ -60,7 +56,7 @@ export const DefaultLayoutV2 = ({
|
||||
const contentRow = <RightPanelToolbarLayout>{children}</RightPanelToolbarLayout>
|
||||
|
||||
const mainColumn = (
|
||||
<div className={mainContentClass}>
|
||||
<div className={cn(mainContentClass, 'min-w-0 flex-1')}>
|
||||
<div className="shrink-0">{header}</div>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 flex-col overflow-hidden">{contentRow}</div>
|
||||
</div>
|
||||
@@ -77,29 +73,8 @@ export const DefaultLayoutV2 = ({
|
||||
/>
|
||||
</div>
|
||||
<div className="flex min-h-0 min-w-0 flex-1 overflow-hidden">
|
||||
{showLeftSidebar ? (
|
||||
<ResizablePanelGroup
|
||||
orientation="horizontal"
|
||||
autoSaveId="default-layout-v2-left-sidebar"
|
||||
className="h-full w-full overflow-hidden"
|
||||
>
|
||||
<ResizablePanel
|
||||
id="panel-v2-left-sidebar"
|
||||
minSize={leftSidebarMinSize}
|
||||
maxSize={leftSidebarMaxSize}
|
||||
defaultSize={leftSidebarDefaultSize}
|
||||
className="h-full min-h-0 overflow-hidden"
|
||||
>
|
||||
<AppSidebarV2 scope={scope} />
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle className="hidden md:flex bg-background" />
|
||||
<ResizablePanel id="panel-v2-main-column" className={mainContentClass}>
|
||||
{mainColumn}
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
) : (
|
||||
mainColumn
|
||||
)}
|
||||
{showLeftSidebar && <Sidebar className="h-full min-h-0 border-r border-default" />}
|
||||
{mainColumn}
|
||||
</div>
|
||||
</div>
|
||||
</DefaultLayoutProviders>
|
||||
|
||||
@@ -15,7 +15,7 @@ import { useSetMainScrollContainer } from '../MainScrollContainerContext'
|
||||
import { useMobileSheet } from '../Navigation/NavigationBar/MobileSheetContext'
|
||||
import { ContentWrapper } from '../ProjectLayout'
|
||||
import { ProjectPausedState } from '../ProjectLayout/PausedState/ProjectPausedState'
|
||||
import { AppSidebarMobileSheetMenu } from './AppSidebar'
|
||||
import { SidebarMobileSheetMenu } from '@/components/interfaces/Sidebar'
|
||||
|
||||
export interface ProjectLayoutV2Props {
|
||||
title?: string
|
||||
@@ -27,7 +27,7 @@ export interface ProjectLayoutV2Props {
|
||||
|
||||
/**
|
||||
* V2 Project layout - renders content directly without a secondary product menu sidebar.
|
||||
* In V2, all navigation is in the primary sidebar (AppSidebar), so this layout
|
||||
* In V2, all navigation is in the primary `Sidebar`, so this layout
|
||||
* only handles title, loading states, and content wrapping.
|
||||
*/
|
||||
export const ProjectLayoutV2 = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayoutV2Props>>(
|
||||
@@ -57,7 +57,7 @@ export const ProjectLayoutV2 = forwardRef<HTMLDivElement, PropsWithChildren<Proj
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const unregister = registerOpenMenu(() => {
|
||||
setMobileSheetContent(<AppSidebarMobileSheetMenu />)
|
||||
setMobileSheetContent(<SidebarMobileSheetMenu />)
|
||||
})
|
||||
return unregister
|
||||
}, [registerOpenMenu, setMobileSheetContent])
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import { Plug } from 'lucide-react'
|
||||
import {
|
||||
Button,
|
||||
cn,
|
||||
SidebarHeader as SidebarHeaderPrimitive,
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from 'ui'
|
||||
|
||||
import { useSidebarHeader, type SidebarHeaderProps } from './useSidebarHeader'
|
||||
import { OrgSelector } from '@/components/layouts/Navigation/NavigationBar/OrgSelector'
|
||||
import { ProjectBranchSelector } from '@/components/layouts/Navigation/NavigationBar/ProjectBranchSelector'
|
||||
|
||||
export type { SidebarHeaderProps }
|
||||
|
||||
export function SidebarHeader(props: SidebarHeaderProps) {
|
||||
const {
|
||||
setShowConnect,
|
||||
isProjectScope,
|
||||
isActiveHealthy,
|
||||
shouldShowOrgSelector,
|
||||
selectorHeaderClass,
|
||||
isCollapsedRail,
|
||||
} = useSidebarHeader(props)
|
||||
|
||||
return (
|
||||
<SidebarHeaderPrimitive className={cn(selectorHeaderClass, 'shrink-0 pb-0')}>
|
||||
{shouldShowOrgSelector && <OrgSelector isCollapsed={isCollapsedRail} />}
|
||||
{isProjectScope && (
|
||||
<>
|
||||
<ProjectBranchSelector isCollapsed={isCollapsedRail} />
|
||||
<div className="flex items-center px-0">
|
||||
{isCollapsedRail ? (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
type="default"
|
||||
size="tiny"
|
||||
disabled={!isActiveHealthy}
|
||||
onClick={() => setShowConnect(true)}
|
||||
className="h-8 w-8 shrink-0 p-0"
|
||||
icon={<Plug size={16} className="rotate-90 !size-4" strokeWidth={1.5} />}
|
||||
/>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right">Connect</TooltipContent>
|
||||
</Tooltip>
|
||||
) : (
|
||||
<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} />}
|
||||
>
|
||||
Connect
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</SidebarHeaderPrimitive>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
export type { AppSidebarScope } from './app-sidebar.types'
|
||||
export { AppSidebarV2 } from './AppSidebar'
|
||||
export { DefaultLayoutV2 } from './DefaultLayout'
|
||||
export { ProjectLayoutV2, ProjectLayoutV2WithAuth } from './ProjectLayout'
|
||||
export { RightPanelToolbarLayout } from './RightPanelToolbar'
|
||||
@@ -0,0 +1,42 @@
|
||||
import { useParams } from 'common'
|
||||
import { useRouter } from 'next/router'
|
||||
import { parseAsBoolean, useQueryState } from 'nuqs'
|
||||
import { useSidebar } from 'ui'
|
||||
|
||||
import { useSelectedOrganizationQuery } from '@/hooks/misc/useSelectedOrganization'
|
||||
import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject'
|
||||
import { IS_PLATFORM, PROJECT_STATUS } from '@/lib/constants'
|
||||
|
||||
export type SidebarHeaderProps = {
|
||||
variant: 'desktop' | 'mobile-sheet'
|
||||
scope?: 'project' | 'organization'
|
||||
}
|
||||
|
||||
export function useSidebarHeader({ variant, scope: scopeProp }: SidebarHeaderProps) {
|
||||
const { state, isMobile } = useSidebar()
|
||||
const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false))
|
||||
const router = useRouter()
|
||||
const { slug: orgRouteSlug } = useParams()
|
||||
const { data: selectedOrganization } = useSelectedOrganizationQuery()
|
||||
const { data: project } = useSelectedProjectQuery()
|
||||
|
||||
const resolvedScope =
|
||||
scopeProp ?? (router.pathname.startsWith('/project') ? 'project' : 'organization')
|
||||
const isProjectScope = resolvedScope === 'project'
|
||||
const isActiveHealthy = project?.status === PROJECT_STATUS.ACTIVE_HEALTHY
|
||||
|
||||
const shouldShowOrgSelector =
|
||||
!isProjectScope && IS_PLATFORM && (Boolean(orgRouteSlug) || Boolean(selectedOrganization))
|
||||
|
||||
const selectorHeaderClass = 'flex-col gap-2 hidden md:flex'
|
||||
const isCollapsedRail = variant === 'desktop' && state === 'collapsed' && !isMobile
|
||||
|
||||
return {
|
||||
setShowConnect,
|
||||
isProjectScope,
|
||||
isActiveHealthy,
|
||||
shouldShowOrgSelector,
|
||||
selectorHeaderClass,
|
||||
isCollapsedRail,
|
||||
}
|
||||
}
|
||||
@@ -25,7 +25,6 @@ import { useEditorType } from '../editors/EditorsLayout.hooks'
|
||||
import { useSetMainScrollContainer } from '../MainScrollContainerContext'
|
||||
import { useMobileSheet } from '../Navigation/NavigationBar/MobileSheetContext'
|
||||
import ProductMenuBar from '../Navigation/ProductMenuBar'
|
||||
import { AppSidebarMobileSheetMenu } from '../NavigationV2/AppSidebar'
|
||||
import BuildingState from './BuildingState'
|
||||
import ConnectingState from './ConnectingState'
|
||||
import { getSectionKeyFromPathname, MobileMenuContent } from './LayoutHeader/MobileMenuContent'
|
||||
@@ -41,6 +40,7 @@ import { UpgradingState } from './UpgradingState'
|
||||
import { useIsNavigationV2Enabled } from '@/components/interfaces/App/FeaturePreview/FeaturePreviewContext'
|
||||
import { CreateBranchModal } from '@/components/interfaces/BranchManagement/CreateBranchModal'
|
||||
import { ProjectAPIDocs } from '@/components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
|
||||
import { SidebarMobileSheetMenu } from '@/components/interfaces/Sidebar'
|
||||
import { BannerFreeMicroUpgrade } from '@/components/ui/BannerStack/Banners/BannerFreeMicroUpgrade'
|
||||
import { BANNER_ID, useBannerStack } from '@/components/ui/BannerStack/BannerStackProvider'
|
||||
import { ResourceExhaustionWarningBanner } from '@/components/ui/ResourceExhaustionWarningBanner/ResourceExhaustionWarningBanner'
|
||||
@@ -203,7 +203,7 @@ export const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<Projec
|
||||
useLayoutEffect(() => {
|
||||
const unregister = registerOpenMenu(() => {
|
||||
if (isNavigationV2) {
|
||||
setMobileSheetContent(<AppSidebarMobileSheetMenu />)
|
||||
setMobileSheetContent(<SidebarMobileSheetMenu />)
|
||||
} else {
|
||||
setMobileSheetContent(
|
||||
<MobileMenuContent
|
||||
|
||||
@@ -191,15 +191,15 @@ function CustomApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
<MonacoThemeProvider />
|
||||
</CommandProvider>
|
||||
</AiAssistantStateContextProvider>
|
||||
<DevToolbar />
|
||||
{/* <DevToolbar /> */}
|
||||
</DevToolbarProvider>
|
||||
</ThemeProvider>
|
||||
</RouteValidationWrapper>
|
||||
</TooltipProvider>
|
||||
<Telemetry />
|
||||
{!isTestEnv && (
|
||||
{/* {!isTestEnv && (
|
||||
<ReactQueryDevtools initialIsOpen={false} buttonPosition="bottom-left" />
|
||||
)}
|
||||
)} */}
|
||||
</ProfileProvider>
|
||||
</FeatureFlagProviderWithOrgContext>
|
||||
</AuthProvider>
|
||||
|
||||
Reference in new issue
Block a user