Feat/sidebar studio (#33612)

* add sidebar

* sidebar docs added

* add sidebar

* fixed issues

* fix mobile

* move settings icon back down

* remove old stuff

* Delete SideBar.tsx

* use ProfileImage component

* spelling

* Update ThemeSettings.tsx

* Update ThemeSettings.tsx

* Update Sidebar.tsx

* layout udpate

* Update Sidebar.tsx

* fixes

* Minor lints

* Update Sidebar.tsx

* create a alt user dropdown for self hosting and local

* add fragment back in

* Adds Theme Selector for local/self hosting

* Update UserDropdown.tsx

* fix theme selector for local self host

* Update index.ts

* move dropdown and fix

* Update LayoutHeader.tsx

* update const

---------

Co-authored-by: Joshen Lim <joshenlimek@gmail.com>
This commit is contained in:
Jonathan Summers-MuirandJoshen Lim authored and Joshen Lim committed 2025-03-05 15:39:01 +01:00
1 parent fcffe66af1
commit 51beca756d
16 files changed
+724 -531

No files matched your search

@@ -9,22 +9,26 @@ import {
Label_Shadcn_,
RadioGroup_Shadcn_,
RadioGroupLargeItem_Shadcn_,
Select_Shadcn_,
SelectContent_Shadcn_,
SelectItem_Shadcn_,
SelectTrigger_Shadcn_,
SelectValue_Shadcn_,
Separator,
singleThemes,
Switch,
Theme,
} from 'ui'
import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout'
import { DEFAULT_SIDEBAR_BEHAVIOR } from 'components/interfaces/Sidebar'
export const ThemeSettings = () => {
const [mounted, setMounted] = useState(false)
const { theme, setTheme } = useTheme()
const [allowNavPanelToExpand, setAllowNavPanelToExpand] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
true
const [sidebarBehaviour, setSidebarBehaviour] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
DEFAULT_SIDEBAR_BEHAVIOR
)
/**
* Avoid Hydration Mismatch
* https://github.com/pacocoursey/next-themes?tab=readme-ov-file#avoid-hydration-mismatch
@@ -83,15 +87,24 @@ export const ThemeSettings = () => {
<Panel.Content>
<FormItemLayout
isReactForm={false}
label="Expand Navigation menu"
label="Sidebar behavior"
layout="flex-row-reverse"
description="Allow the Navigation panel to expand on hover"
description="Choose your preferred sidebar behavior: open, closed, or expand on hover."
>
<Switch
size="large"
checked={allowNavPanelToExpand}
onCheckedChange={setAllowNavPanelToExpand}
/>
<Select_Shadcn_
value={sidebarBehaviour}
onValueChange={setSidebarBehaviour}
aria-label="Select an option"
>
<SelectTrigger_Shadcn_>
<SelectValue_Shadcn_ placeholder="Choose an option" />
</SelectTrigger_Shadcn_>
<SelectContent_Shadcn_>
<SelectItem_Shadcn_ value="open">Expanded</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="closed">Collapsed</SelectItem_Shadcn_>
<SelectItem_Shadcn_ value="expandable">Expand on hover</SelectItem_Shadcn_>
</SelectContent_Shadcn_>
</Select_Shadcn_>
</FormItemLayout>
</Panel.Content>
</Panel>
@@ -0,0 +1,384 @@
import { AnimatePresence, motion, MotionProps } from 'framer-motion'
import { isUndefined } from 'lodash'
import { Blocks, Boxes, ChartArea, Command, PanelLeftDashed, Settings, Users } from 'lucide-react'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { ComponentProps, ComponentPropsWithoutRef, FC, useEffect } from 'react'
import { useParams } from 'common'
import {
generateOtherRoutes,
generateProductRoutes,
generateSettingsRoutes,
generateToolRoutes,
} from 'components/layouts/ProjectLayout/NavigationBar/NavigationBar.utils'
import { useProjectContext } from 'components/layouts/ProjectLayout/ProjectContext'
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
import { useHideSidebar } from 'hooks/misc/useHideSidebar'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { Home } from 'icons'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
Separator,
SidebarContent as SidebarContentPrimitive,
SidebarFooter,
SidebarGroup,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
Sidebar as SidebarPrimitive,
useSidebar,
} from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns'
import { ThemeDropdown } from './ThemeDropdown'
import { UserDropdown } from './UserDropdown'
export const ICON_SIZE = 32
export const ICON_STROKE_WIDTH = 1.5
export type SidebarBehaviourType = 'expandable' | 'open' | 'closed'
export const DEFAULT_SIDEBAR_BEHAVIOR = 'expandable'
const SidebarMotion = motion(SidebarPrimitive) as FC<
ComponentProps<typeof SidebarPrimitive> & {
transition?: MotionProps['transition']
}
>
export interface SidebarProps extends ComponentPropsWithoutRef<typeof SidebarPrimitive> {}
export const Sidebar = ({ className, ...props }: SidebarProps) => {
const { setOpen } = useSidebar()
const hideSideBar = useHideSidebar()
const [sidebarBehaviour, setSidebarBehaviour] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
DEFAULT_SIDEBAR_BEHAVIOR
)
useEffect(() => {
// logic to toggle sidebar open based on sidebarBehaviour state
if (sidebarBehaviour === 'open') setOpen(true)
if (sidebarBehaviour === 'closed') setOpen(false)
}, [sidebarBehaviour, setOpen])
return (
<>
<AnimatePresence>
{!hideSideBar && (
<SidebarMotion
{...props}
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 group-data-[state=expanded]: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>
</>
)
}
export function SidebarContent({ footer }: { footer?: React.ReactNode }) {
const setCommandMenuOpen = useSetCommandMenuOpen()
// temporary logic to show settings route in sidebar footer
// this will be removed once we move to an updated org/project nav
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const settingsRoutes = generateSettingsRoutes(ref, project)
const activeRoute = router.pathname.split('/')[3]
return (
<>
<AnimatePresence mode="wait">
<SidebarContentPrimitive>
{/* Org sidebar to be added in with project/org nav */}
{/* {project ? ( */}
<motion.div key="project-links">
<ProjectLinks />
</motion.div>
{/* Org sidebar to be added in with project/org nav */}
{/* ) : (
<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>
<SidebarMenu className="group-data-[state=expanded]:p-0">
<SidebarGroup className="p-0 gap-0.5">
{settingsRoutes.map((route) => (
<SideBarNavLink
key={`settings-routes-${route.key}`}
route={route}
active={activeRoute === route.key}
/>
))}
</SidebarGroup>
<SidebarGroup className="p-0">
<SideBarNavLink
key="cmdk"
route={{
key: 'cmdk',
label: 'Command Menu',
icon: <Command size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
}}
onClick={() => setCommandMenuOpen(true)}
/>
</SidebarGroup>
</SidebarMenu>
<SidebarMenu className="group-data-[state=expanded]:p-0">
<SidebarGroup className="p-0">
{IS_PLATFORM ? <UserDropdown /> : <ThemeDropdown />}
</SidebarGroup>
</SidebarMenu>
<SidebarGroup className="p-0">{footer}</SidebarGroup>
</SidebarFooter>
</>
)
}
export function SideBarNavLink({
route,
active,
onClick,
...props
}: {
route: any
active?: boolean
onClick?: () => void
} & ComponentPropsWithoutRef<typeof SidebarMenuButton>) {
const [sidebarBehaviour] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
DEFAULT_SIDEBAR_BEHAVIOR
)
const buttonProps = {
tooltip: sidebarBehaviour === 'closed' ? route.label : '',
isActive: active,
className: 'text-sm',
size: 'default' as const,
onClick: onClick,
}
const content = props.children ? (
props.children
) : (
<>
{route.icon}
<span>{route.label}</span>
</>
)
return (
<SidebarMenuItem>
{route.link ? (
<SidebarMenuButton {...buttonProps} asChild>
<Link href={route.link}>{content}</Link>
</SidebarMenuButton>
) : (
<SidebarMenuButton {...buttonProps}>{content}</SidebarMenuButton>
)}
</SidebarMenuItem>
)
}
function ProjectLinks() {
const router = useRouter()
const { ref } = useParams()
const { project } = useProjectContext()
const activeRoute = router.pathname.split('/')[3]
const {
projectAuthAll: authEnabled,
projectEdgeFunctionAll: edgeFunctionsEnabled,
projectStorageAll: storageEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled([
'project_auth:all',
'project_edge_function:all',
'project_storage:all',
'realtime:all',
])
const toolRoutes = generateToolRoutes(ref, project)
const productRoutes = generateProductRoutes(ref, project, {
auth: authEnabled,
edgeFunctions: edgeFunctionsEnabled,
storage: storageEnabled,
realtime: realtimeEnabled,
})
const otherRoutes = generateOtherRoutes(ref, project)
/* Settings routes to be added in with project/org nav */
// const settingsRoutes = generateSettingsRoutes(ref, project)
return (
<SidebarMenu>
<SidebarGroup className="gap-0.5">
<SideBarNavLink
key="home"
active={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
route={{
key: 'HOME',
label: 'Project overview',
icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: `/project/${ref}`,
linkElement: <ProjectIndexPageLink projectRef={ref} />,
}}
/>
{toolRoutes.map((route, i) => (
<SideBarNavLink
key={`tools-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
))}
</SidebarGroup>
<Separator className="w-[calc(100%-1rem)] mx-auto" />
<SidebarGroup className="gap-0.5">
{productRoutes.map((route, i) => (
<SideBarNavLink
key={`product-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
))}
</SidebarGroup>
<Separator className="w-[calc(100%-1rem)] mx-auto" />
<SidebarGroup className="gap-0.5">
{otherRoutes.map((route, i) => (
<SideBarNavLink
key={`other-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
))}
</SidebarGroup>
{/* Settings routes to be added in with project/org nav */}
{/* <SidebarGroup className="gap-0.5">
{settingsRoutes.map((route, i) => (
<SideBarNavLink
key={`settings-routes-${i}`}
route={route}
active={activeRoute === route.key}
/>
))}
</SidebarGroup> */}
</SidebarMenu>
)
}
// Not currently used, will be part of org layout PR
const OrganizationLinks = () => {
const router = useRouter()
const { slug } = useParams()
const activeRoute = router.pathname.split('/')[3]
const navMenuItems = [
{
label: 'Projects',
href: `/org/${slug}`,
key: '',
icon: <Boxes size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
},
{
label: 'Team',
href: `/org/${slug}/team`,
key: 'team',
icon: <Users size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
},
{
label: 'Integrations',
href: `/org/${slug}/integrations`,
key: 'integrations',
icon: <Blocks size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
},
{
label: 'Usage',
href: `/org/${slug}/usage`,
key: 'usage',
icon: <ChartArea size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
},
{
label: 'Organization settings',
href: `/org/${slug}/settings/general`,
key: 'settings',
icon: <Settings size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
},
]
return (
<SidebarMenu className="flex flex-col gap-1 items-start">
<SidebarGroup className="gap-0.5">
{navMenuItems.map((item, i) => (
<SideBarNavLink
active={i === 0 ? activeRoute === undefined : activeRoute === item.key}
route={{
label: item.label,
link: item.href,
key: item.label,
icon: item.icon,
}}
/>
))}
</SidebarGroup>
</SidebarMenu>
)
}
@@ -0,0 +1,82 @@
import { Palette } from 'lucide-react'
import { useTheme } from 'next-themes'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import {
cn,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuTrigger,
sidebarMenuButtonVariants,
singleThemes,
Theme,
Tooltip,
TooltipContent,
TooltipTrigger,
} from 'ui'
import {
DEFAULT_SIDEBAR_BEHAVIOR,
ICON_SIZE,
ICON_STROKE_WIDTH,
SidebarBehaviourType,
} from './Sidebar'
export const ThemeDropdown = () => {
const { theme, setTheme } = useTheme()
const [sidebarBehaviour] = useLocalStorageQuery<SidebarBehaviourType>(
LOCAL_STORAGE_KEYS.SIDEBAR_BEHAVIOR,
DEFAULT_SIDEBAR_BEHAVIOR
)
const button = (
<button
data-sidebar="menu-button"
data-has-icon={true}
className={cn(
sidebarMenuButtonVariants({ variant: 'default', size: 'default', hasIcon: true })
)}
>
<Palette size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />
<span>Theme</span>
</button>
)
return (
<DropdownMenu>
{sidebarBehaviour === 'closed' ? (
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="right" align="center">
<span>Theme</span>
</TooltipContent>
</Tooltip>
) : (
<DropdownMenuTrigger asChild>{button}</DropdownMenuTrigger>
)}
<DropdownMenuContent side="top" align="start">
<DropdownMenuGroup>
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={theme}
onValueChange={(value) => {
setTheme(value)
}}
>
{singleThemes.map((theme: Theme) => (
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
{theme.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -0,0 +1,146 @@
import { Command, FlaskConical, Palette, Settings } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useSignOut } from 'lib/auth'
import { IS_PLATFORM } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { useAppStateSnapshot } from 'state/app-state'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
SidebarMenuButton,
Theme,
cn,
singleThemes,
} from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { ICON_SIZE, ICON_STROKE_WIDTH, SideBarNavLink } from './Sidebar'
export const UserDropdown = () => {
const { profile } = useProfile()
const appStateSnapshot = useAppStateSnapshot()
const { theme, setTheme } = useTheme()
const signOut = useSignOut()
const router = useRouter()
const setCommandMenuOpen = useSetCommandMenuOpen()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<SidebarMenuButton
className={cn(
'text-sm',
'group-data-[state=expanded]:h-10',
'p-0.5 group-data-[state=expanded]:pr-2 group-data-[state=expanded]:pl-1'
)}
size={'default'}
hasIcon={false}
asChild
isActive={false}
>
<button>
<div className="aspect-square h-7 w-7 rounded-md border flex-shrink-0">
<ProfileImage
alt={profile?.username}
src={profile?.profileImageUrl}
className="w-7 h-7 rounded-md"
/>
</div>
<span className="flex flex-col gap-0">
{profile?.username}
<span className="text-foreground-lighter text-xs">{profile?.primary_email}</span>
</span>
</button>
</SidebarMenuButton>
</DropdownMenuTrigger>
<DropdownMenuContent side="top" align="start">
{IS_PLATFORM && (
<>
<div className="px-2 py-1 flex flex-col gap-0 text-sm">
{profile && (
<>
<span
title={profile.username}
className="w-full text-left text-foreground truncate"
>
{profile.username}
</span>
{profile.primary_email !== profile.username && (
<span
title={profile.primary_email}
className="w-full text-left text-foreground-light text-xs truncate"
>
{profile.primary_email}
</span>
)}
</>
)}
</div>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="flex gap-2" asChild>
<Link href="/account/me">
<Settings size={14} strokeWidth={1.5} className="text-foreground-lighter" />
Account preferences
</Link>
</DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => appStateSnapshot.setShowFeaturePreviewModal(true)}
onSelect={() => appStateSnapshot.setShowFeaturePreviewModal(true)}
>
<FlaskConical size={14} strokeWidth={1.5} className="text-foreground-lighter" />
Feature previews
</DropdownMenuItem>
<DropdownMenuItem className="flex gap-2" onClick={() => setCommandMenuOpen(true)}>
<Command size={14} strokeWidth={1.5} className="text-foreground-lighter" />
Command menu
</DropdownMenuItem>
<DropdownMenuSeparator />
</DropdownMenuGroup>
</>
)}
<DropdownMenuGroup>
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={theme}
onValueChange={(value) => {
setTheme(value)
}}
>
{singleThemes.map((theme: Theme) => (
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
{theme.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
{IS_PLATFORM && (
<>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onSelect={async () => {
await signOut()
await router.push('/sign-in')
}}
>
Log out
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
)
}
@@ -3,9 +3,10 @@ import { PropsWithChildren } from 'react'
import { useParams } from 'common'
import { AppBannerWrapper } from 'components/interfaces/App'
import { AppBannerContextProvider } from 'components/interfaces/App/AppBannerWrapperContext'
import { Sidebar } from 'components/interfaces/Sidebar'
import { SidebarProvider } from 'ui'
import { LayoutHeader } from './ProjectLayout/LayoutHeader'
import MobileNavigationBar from './ProjectLayout/NavigationBar/MobileNavigationBar'
import NavigationBar from './ProjectLayout/NavigationBar/NavigationBar'
import { ProjectContextProvider } from './ProjectLayout/ProjectContext'
export interface DefaultLayoutProps {
@@ -26,25 +27,28 @@ export interface DefaultLayoutProps {
const DefaultLayout = ({ children, showProductMenu }: PropsWithChildren<DefaultLayoutProps>) => {
const { ref } = useParams()
return (
<ProjectContextProvider projectRef={ref}>
<AppBannerContextProvider>
<div className="flex flex-col h-screen w-screen">
{/* Top Banner */}
<AppBannerWrapper />
<div className="flex-shrink-0">
<MobileNavigationBar />
<LayoutHeader showProductMenu={showProductMenu} />
<SidebarProvider defaultOpen={false}>
<ProjectContextProvider projectRef={ref}>
<AppBannerContextProvider>
<div className="flex flex-col h-screen w-screen">
{/* Top Banner */}
<AppBannerWrapper />
<div className="flex-shrink-0">
<MobileNavigationBar />
<LayoutHeader showProductMenu={showProductMenu} />
</div>
{/* Main Content Area */}
<div className="flex flex-1 w-full overflow-y-hidden">
{/* Sidebar */}
<Sidebar />
{/* Main Content */}
<div className="flex-grow h-full overflow-y-auto">{children}</div>
</div>
</div>
{/* Main Content Area */}
<div className="flex flex-1 w-full overflow-y-hidden">
{/* Sidebar */}
<NavigationBar />
{/* Main Content */}
<div className="flex-grow h-full overflow-y-auto">{children}</div>
</div>
</div>
</AppBannerContextProvider>
</ProjectContextProvider>
</AppBannerContextProvider>
</ProjectContextProvider>
</SidebarProvider>
)
}
@@ -109,12 +109,12 @@ const LayoutHeader = ({
{projectRef && (
<Link
href={IS_PLATFORM ? '/projects' : `/project/default`}
className="mx-1 hidden md:flex items-center w-[40px] h-[40px]"
className="ml-1 mr-2 hidden md:flex items-center"
>
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
className="absolute h-[40px] w-6 cursor-pointer rounded"
className="w-5 cursor-pointer rounded"
/>
</Link>
)}
@@ -4,25 +4,16 @@ import { useRouter } from 'next/router'
import { useState } from 'react'
import { useParams } from 'common'
import { SidebarContent } from 'components/interfaces/Sidebar'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { buttonVariants, cn } from 'ui'
import { CommandMenuTrigger } from 'ui-patterns'
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
import { NavContent } from './NavigationBar'
const MobileNavigationBar = () => {
const router = useRouter()
const [isSheetOpen, setIsSheetOpen] = useState(false)
const { ref: projectRef } = useParams()
const snap = useAppStateSnapshot()
const onCloseNavigationIconLink = (event: any) => {
snap.setNavigationPanelOpen(
false,
event.target.id === 'icon-link' || ['svg', 'path'].includes(event.target.localName)
)
}
return (
<div className="h-14 w-full flex flex-row md:hidden">
@@ -37,7 +28,6 @@ const MobileNavigationBar = () => {
<Link
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
className="flex items-center h-[26px] w-[26px] min-w-[26px]"
onClick={onCloseNavigationIconLink}
>
<img
alt="Supabase"
@@ -76,8 +66,8 @@ const MobileNavigationBar = () => {
</button>
</div>
</nav>
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
<NavContent />
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen} data-state="expanded">
<SidebarContent />
</MobileSheetNav>
</div>
)
@@ -1,447 +0,0 @@
import { Home } from 'icons'
import { isUndefined } from 'lodash'
import { Command, FileText, FlaskConical, Search, Settings } from 'lucide-react'
import { useTheme } from 'next-themes'
import Link from 'next/link'
import { useRouter } from 'next/router'
import { useState } from 'react'
import { useParams } from 'common'
import { useIsAPIDocsSidePanelEnabled } from 'components/interfaces/App/FeaturePreview/FeaturePreviewContext'
import { ProfileImage } from 'components/ui/ProfileImage'
import { useProjectLintsQuery } from 'data/lint/lint-query'
import { ProjectIndexPageLink } from 'data/prefetchers/project.$ref'
import { useIsFeatureEnabled } from 'hooks/misc/useIsFeatureEnabled'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { useFlag } from 'hooks/ui/useFlag'
import { useSignOut } from 'lib/auth'
import { IS_PLATFORM, LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useProfile } from 'lib/profile'
import { useAppStateSnapshot } from 'state/app-state'
import {
AiIconAnimation,
Button,
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
HoverCardContent_Shadcn_,
HoverCardTrigger_Shadcn_,
HoverCard_Shadcn_,
Separator,
Theme,
Tooltip,
TooltipContent,
TooltipTrigger,
cn,
singleThemes,
} from 'ui'
import { useSetCommandMenuOpen } from 'ui-patterns/CommandMenu'
import { useProjectContext } from '../ProjectContext'
import { CommandOption } from './CommandOption'
import {
generateOtherRoutes,
generateProductRoutes,
generateSettingsRoutes,
generateToolRoutes,
} from './NavigationBar.utils'
import { NavigationIconButton } from './NavigationIconButton'
import NavigationIconLink from './NavigationIconLink'
export const ICON_SIZE = 20
export const ICON_STROKE_WIDTH = 1.5
const NavigationBar = () => {
const snap = useAppStateSnapshot()
const [storedAllowNavPanel] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
true
)
// Don't allow the nav panel to expand in playwright tests
const allowNavPanelToExpand = process.env.NEXT_PUBLIC_NODE_ENV !== 'test' && storedAllowNavPanel
return (
<div className="w-14 h-full hidden md:flex flex-col">
<nav
data-state={snap.navigationPanelOpen ? 'expanded' : 'collapsed'}
className={cn(
'group py-2 z-10 h-full w-[13rem] md:w-14 md:data-[state=expanded]:w-[13rem]',
'border-r bg-dash-sidebar border-default data-[state=expanded]:shadow-xl',
'transition-width duration-200',
'hide-scrollbar flex flex-col justify-between overflow-y-auto'
)}
onMouseEnter={() => allowNavPanelToExpand && snap.setNavigationPanelOpen(true)}
onMouseLeave={() => allowNavPanelToExpand && snap.setNavigationPanelOpen(false)}
>
<NavContent />
</nav>
</div>
)
}
export const NavContent = () => {
const router = useRouter()
const { profile } = useProfile()
const { project } = useProjectContext()
const { theme, setTheme } = useTheme()
const { ref: projectRef } = useParams()
const setCommandMenuOpen = useSetCommandMenuOpen()
const snap = useAppStateSnapshot()
const signOut = useSignOut()
const isNewAPIDocsEnabled = useIsAPIDocsSidePanelEnabled()
const [hasInvalidImg, setHasInvalidImg] = useState(false)
const [userDropdownOpen, setUserDropdownOpenState] = useState(false)
const [storedAllowNavPanel] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
true
)
// Don't allow the nav panel to expand in playwright tests
const allowNavPanelToExpand = process.env.NEXT_PUBLIC_NODE_ENV !== 'test' && storedAllowNavPanel
const {
projectAuthAll: authEnabled,
projectEdgeFunctionAll: edgeFunctionsEnabled,
projectStorageAll: storageEnabled,
realtimeAll: realtimeEnabled,
} = useIsFeatureEnabled([
'project_auth:all',
'project_edge_function:all',
'project_storage:all',
'realtime:all',
])
const { data } = useProjectLintsQuery({
projectRef: project?.ref,
})
const securityLints = (data ?? []).filter((lint) => lint.categories.includes('SECURITY'))
const errorLints = securityLints.filter((lint) => lint.level === 'ERROR')
const activeRoute = router.pathname.split('/')[3]
const toolRoutes = generateToolRoutes(projectRef, project)
const productRoutes = generateProductRoutes(projectRef, project, {
auth: authEnabled,
edgeFunctions: edgeFunctionsEnabled,
storage: storageEnabled,
realtime: realtimeEnabled,
})
const showWarehouse = useFlag('warehouse')
const otherRoutes = generateOtherRoutes(projectRef, project)
const settingsRoutes = generateSettingsRoutes(projectRef, project)
const onCloseNavigationIconLink = (event: any) => {
snap.setNavigationPanelOpen(
false,
event.target.id === 'icon-link' || ['svg', 'path'].includes(event.target.localName)
)
}
const CommandButton = (
<HoverCard_Shadcn_ openDelay={10}>
<HoverCardTrigger_Shadcn_ asChild>
<NavigationIconButton
size="tiny"
type="text"
icon={<Command size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
>
Commands
</NavigationIconButton>
</HoverCardTrigger_Shadcn_>
<HoverCardContent_Shadcn_ side="right" className="w-48 p-1 flex flex-col gap-y-1">
<CommandOption
icon={
<div className="px-1">
<Search size={16} />
</div>
}
label="Search"
shortcut="K"
onClick={() => {
setCommandMenuOpen(true)
snap.setNavigationPanelOpen(false)
}}
/>
<CommandOption
icon={
<AiIconAnimation className="scale-75 [&>div>div]:border-black dark:[&>div>div]:border-white" />
}
label="Assistant"
shortcut="I"
onClick={() => {
snap.setAiAssistantPanel({ open: !snap.aiAssistantPanel.open })
}}
/>
</HoverCardContent_Shadcn_>
</HoverCard_Shadcn_>
)
const UserAccountButton = (
<Button
block
type="text"
size="tiny"
className={cn(
'mt-3 h-10 [&>span]:relative [&>span]:flex [&>span]:w-full [&>span]:h-full p-0'
)}
>
<div className="relative w-full h-full flex items-center justify-center">
<ProfileImage
alt={profile?.username}
src={profile?.profileImageUrl}
className="absolute left-1.5 w-6 h-6"
/>
<span
className={cn(
'w-full md:w-[8rem] flex flex-col items-start text-sm truncate',
'absolute left-10 md:left-7 group-data-[state=expanded]:left-10',
'opacity-100 md:group-data-[state=collapsed]:opacity-0 md:group-data-[state=expanded]:opacity-100',
'transition-all'
)}
>
{profile && IS_PLATFORM && (
<>
<span title={profile.username} className="w-full text-left text-foreground truncate">
{profile.username}
</span>
{profile.primary_email !== profile.username && (
<span
title={profile.primary_email}
className="w-full text-left text-foreground-light text-xs truncate"
>
{profile.primary_email}
</span>
)}
</>
)}
</span>
</div>
</Button>
)
return (
<>
<ul className="flex flex-col gap-y-1 justify-start px-2 relative">
<NavigationIconLink
isActive={isUndefined(activeRoute) && !isUndefined(router.query.ref)}
route={{
key: 'HOME',
label: 'Home',
icon: <Home size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />,
link: `/project/${projectRef}`,
linkElement: <ProjectIndexPageLink projectRef={projectRef} />,
}}
onClick={onCloseNavigationIconLink}
/>
<Separator className="my-1 bg-border-muted" />
{toolRoutes.map((route) => (
<NavigationIconLink
key={route.key}
route={route}
isActive={activeRoute === route.key}
onClick={onCloseNavigationIconLink}
/>
))}
<Separator className="my-1 bg-border-muted" />
{productRoutes.map((route) => (
<NavigationIconLink
key={route.key}
route={route}
isActive={activeRoute === route.key}
onClick={onCloseNavigationIconLink}
/>
))}
<Separator className="my-1 bg-border-muted" />
{otherRoutes.map((route) => {
if (route.key === 'api' && isNewAPIDocsEnabled) {
return (
<NavigationIconButton
key={route.key}
onClick={() => {
snap.setShowProjectApiDocs(true)
snap.setNavigationPanelOpen(false)
}}
icon={<FileText size={ICON_SIZE} strokeWidth={ICON_STROKE_WIDTH} />}
>
Project API
</NavigationIconButton>
)
} else if (route.key === 'advisors') {
return (
<div className="relative" key={route.key}>
{securityLints.length > 0 && (
<div
className={cn(
'absolute flex h-2 w-2 left-6 top-2 z-10 rounded-full',
errorLints.length > 0 ? 'bg-destructive-600' : 'bg-warning-600'
)}
/>
)}
<NavigationIconLink
route={route}
isActive={activeRoute === route.key}
onClick={onCloseNavigationIconLink}
/>
</div>
)
} else if (route.key === 'logs') {
// TODO: Undo this when warehouse flag is removed
const label = showWarehouse ? 'Logs & Analytics' : route.label
const newRoute = { ...route, label }
return (
<NavigationIconLink
key={newRoute.key}
route={newRoute}
isActive={activeRoute === newRoute.key}
onClick={onCloseNavigationIconLink}
/>
)
} else {
return (
<NavigationIconLink
key={route.key}
route={route}
isActive={activeRoute === route.key}
onClick={onCloseNavigationIconLink}
/>
)
}
})}
</ul>
<ul className="flex flex-col px-2 pb-4 md:pb-0 gap-y-1">
{settingsRoutes.map((route) => (
<NavigationIconLink
key={route.key}
route={route}
isActive={activeRoute === route.key}
onClick={onCloseNavigationIconLink}
/>
))}
{IS_PLATFORM && (
<>
{!allowNavPanelToExpand && (
<Tooltip>
<TooltipTrigger asChild>{CommandButton}</TooltipTrigger>
<TooltipContent side="right">
<span>Commands</span>
</TooltipContent>
</Tooltip>
)}
{allowNavPanelToExpand && CommandButton}
</>
)}
<DropdownMenu
open={userDropdownOpen}
onOpenChange={(open: boolean) => {
setUserDropdownOpenState(open)
if (open === false) snap.setNavigationPanelOpen(false)
}}
>
{allowNavPanelToExpand ? (
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
) : (
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
</TooltipTrigger>
<TooltipContent side="right">
<span>Account settings</span>
</TooltipContent>
</Tooltip>
)}
<DropdownMenuContent side="top" align="start">
{IS_PLATFORM && (
<>
<div className="px-2 py-1 flex flex-col gap-0 text-sm">
{profile && (
<>
<span
title={profile.username}
className="w-full text-left text-foreground truncate"
>
{profile.username}
</span>
{profile.primary_email !== profile.username && (
<span
title={profile.primary_email}
className="w-full text-left text-foreground-light text-xs truncate"
>
{profile.primary_email}
</span>
)}
</>
)}
</div>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem className="flex gap-2" asChild>
<Link href="/account/me">
<Settings size={14} strokeWidth={1.5} className="text-foreground-lighter" />
Account preferences
</Link>
</DropdownMenuItem>
<DropdownMenuItem
className="flex gap-2"
onClick={() => snap.setShowFeaturePreviewModal(true)}
onSelect={() => snap.setShowFeaturePreviewModal(true)}
>
<FlaskConical size={14} strokeWidth={1.5} className="text-foreground-lighter" />
Feature previews
</DropdownMenuItem>
<DropdownMenuSeparator />
</DropdownMenuGroup>
</>
)}
<DropdownMenuGroup>
<DropdownMenuLabel>Theme</DropdownMenuLabel>
<DropdownMenuRadioGroup
value={theme}
onValueChange={(value) => {
setTheme(value)
}}
>
{singleThemes.map((theme: Theme) => (
<DropdownMenuRadioItem key={theme.value} value={theme.value}>
{theme.name}
</DropdownMenuRadioItem>
))}
</DropdownMenuRadioGroup>
</DropdownMenuGroup>
{IS_PLATFORM && (
<>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem
onSelect={async () => {
await signOut()
await router.push('/sign-in')
}}
>
Log out
</DropdownMenuItem>
</DropdownMenuGroup>
</>
)}
</DropdownMenuContent>
</DropdownMenu>
</ul>
</>
)
}
export default NavigationBar
@@ -14,7 +14,7 @@ import {
TableEditor,
} from 'icons'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { ICON_SIZE, ICON_STROKE_WIDTH } from './NavigationBar'
import { ICON_SIZE, ICON_STROKE_WIDTH } from 'components/interfaces/Sidebar'
export const generateToolRoutes = (ref?: string, project?: Project): Route[] => {
const isProjectBuilding = project?.status === PROJECT_STATUS.COMING_UP
@@ -12,7 +12,6 @@ import { cn, Tooltip, TooltipContent, TooltipTrigger } from 'ui'
import type { Route } from 'components/ui/ui.types'
import { useLocalStorageQuery } from 'hooks/misc/useLocalStorage'
import { LOCAL_STORAGE_KEYS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
interface NavigationIconButtonProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
route: Route
@@ -21,8 +20,6 @@ interface NavigationIconButtonProps extends AnchorHTMLAttributes<HTMLAnchorEleme
const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonProps>(
({ route, isActive = false, onClick = noop, ...props }, ref) => {
const snap = useAppStateSnapshot()
const [storedAllowNavPanel] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
true
@@ -78,7 +75,6 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
{route.icon}
</span>
<span
aria-hidden={snap.navigationPanelOpen || undefined}
className={cn(
'min-w-[128px] text-sm text-foreground-light',
'group-hover/item:text-foreground',
+8
View File
@@ -0,0 +1,8 @@
import { usePathname } from 'next/navigation'
export function useHideSidebar() {
const pathname = usePathname() ?? ''
return (
pathname.startsWith('/account') || pathname.startsWith('/new') || pathname === '/organizations'
)
}
+1
View File
@@ -42,6 +42,7 @@ export const USAGE_APPROACHING_THRESHOLD = 0.75
export const LOCAL_STORAGE_KEYS = {
AI_ASSISTANT_STATE: 'supabase-ai-assistant-state',
SIDEBAR_BEHAVIOR: 'supabase-sidebar-behavior',
EDITOR_PANEL_STATE: 'supabase-editor-panel-state',
UI_PREVIEW_NAVIGATION_LAYOUT: 'supabase-ui-preview-nav-layout',
-25
View File
@@ -98,8 +98,6 @@ const getInitialState = () => {
selectedFeaturePreview: '',
showAiSettingsModal: false,
showGenerateSqlModal: false,
navigationPanelOpen: false,
navigationPanelJustClosed: false,
ongoingQueriesPanelOpen: false,
mobileMenuOpen: false,
}
@@ -149,8 +147,6 @@ const getInitialState = () => {
selectedFeaturePreview: '',
showAiSettingsModal: false,
showGenerateSqlModal: false,
navigationPanelOpen: false,
navigationPanelJustClosed: false,
ongoingQueriesPanelOpen: false,
mobileMenuOpen: false,
}
@@ -215,27 +211,6 @@ export const appState = proxy({
appState.showGenerateSqlModal = value
},
navigationPanelOpen: false,
navigationPanelJustClosed: false,
setNavigationPanelOpen: (value: boolean, trackJustClosed: boolean = false) => {
if (value === false) {
if (trackJustClosed) {
appState.navigationPanelOpen = false
appState.navigationPanelJustClosed = true
} else {
appState.navigationPanelOpen = false
appState.navigationPanelJustClosed = false
}
} else {
if (appState.navigationPanelJustClosed === false) {
appState.navigationPanelOpen = true
}
}
},
setNavigationPanelJustClosed: (value: boolean) => {
appState.navigationPanelJustClosed = value
},
resetAiAssistantPanel: () => {
appState.aiAssistantPanel = {
...INITIAL_AI_ASSISTANT,
+16
View File
@@ -15,6 +15,14 @@
--chart-5: 27 87% 67%;
--chart-warning: hsl(var(--warning-default));
--chart-destructive: hsl(var(--destructive-default));
--sidebar-background: var(--background-dash-sidebar);
--sidebar-foreground: var(--foreground-default);
--sidebar-primary: var(--foreground-default);
--sidebar-primary-foreground: var(--warning-default);
--sidebar-accent: var(--background-selection);
--sidebar-accent-foreground: var(--foreground-default);
--sidebar-border: var(--border-default);
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
@@ -25,6 +33,14 @@
--chart-5: 340 75% 55%;
--chart-warning: hsl(var(--warning-default));
--chart-destructive: hsl(var(--destructive-default));
--sidebar-background: var(--background-dash-sidebar);
--sidebar-foreground: var(--foreground-default);
--sidebar-primary: var(--foreground-default);
--sidebar-primary-foreground: var(--warning-default);
--sidebar-accent: var(--background-selection);
--sidebar-accent-foreground: var(--foreground-default);
--sidebar-border: var(--border-default);
--sidebar-ring: 217.2 91.2% 59.8%;
}
@layer utilities {
+10
View File
@@ -112,6 +112,16 @@ const uiConfig = ui({
extend: {
colors: {
...kebabToNested(colorExtend),
sidebar: {
DEFAULT: 'hsl(var(--sidebar-background))',
foreground: 'hsl(var(--sidebar-foreground))',
primary: 'hsl(var(--sidebar-primary))',
'primary-foreground': 'hsl(var(--sidebar-primary-foreground))',
accent: 'hsl(var(--sidebar-accent))',
'accent-foreground': 'hsl(var(--sidebar-accent-foreground))',
border: 'hsl(var(--sidebar-border))',
ring: 'hsl(var(--sidebar-ring))',
},
},
typography: ({ theme }) => ({
@@ -149,6 +149,7 @@ SidebarProvider.displayName = 'SidebarProvider'
const Sidebar = React.forwardRef<
HTMLDivElement,
React.ComponentProps<'div'> & {
overflowing?: boolean
side?: 'left' | 'right'
variant?: 'sidebar' | 'floating' | 'inset'
collapsible?: 'offcanvas' | 'icon' | 'none'
@@ -156,6 +157,7 @@ const Sidebar = React.forwardRef<
>(
(
{
overflowing = false,
side = 'left',
variant = 'sidebar',
collapsible = 'offcanvas',
@@ -205,7 +207,10 @@ const Sidebar = React.forwardRef<
return (
<div
ref={ref}
className="relative group peer hidden md:block text-sidebar-foreground"
className={cn(
overflowing ? 'w-12' : '',
'relative group peer hidden md:block text-sidebar-foreground'
)}
data-state={state}
data-collapsible={state === 'collapsed' ? collapsible : ''}
data-variant={variant}
@@ -214,9 +219,8 @@ const Sidebar = React.forwardRef<
{/* This is what handles the sidebar gap on desktop */}
<div
className={cn(
'relative', // sidebar custom changes - to contain the absolute sidebar below
//
'duration-200 h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
overflowing ? 'absolute top-0' : 'relative', // sidebar custom changes - to contain the absolute sidebar below
'duration-100 h-svh w-[--sidebar-width] bg-transparent transition-[width] ease-linear',
'group-data-[collapsible=offcanvas]:w-0',
'group-data-[side=right]:rotate-180',
variant === 'floating' || variant === 'inset'
@@ -228,7 +232,7 @@ const Sidebar = React.forwardRef<
className={cn(
'absolute top-0 h-full', // sidebar custom changes - We have also removed 'fixed', and 'h-svh'
//
'duration-200 inset-y-0 z-10 hidden w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
'duration-100 inset-y-0 z-10 hidden w-[--sidebar-width] transition-[left,right,width] ease-linear md:flex',
side === 'left'
? 'left-0 group-data-[collapsible=offcanvas]:left-[calc(var(--sidebar-width)*-1)]'
: 'right-0 group-data-[collapsible=offcanvas]:right-[calc(var(--sidebar-width)*-1)]',
@@ -427,7 +431,7 @@ const SidebarGroupLabel = React.forwardRef<
ref={ref}
data-sidebar="group-label"
className={cn(
'duration-200 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-5 [&>svg]:shrink-0',
'duration-100 flex h-8 shrink-0 items-center rounded-md px-2 text-xs font-medium text-sidebar-foreground/70 outline-none ring-sidebar-ring transition-[margin,opa] ease-linear focus-visible:ring-2 [&>svg]:size-5 [&>svg]:shrink-0',
'group-data-[collapsible=icon]:-mt-8 group-data-[collapsible=icon]:opacity-0',
className
)}
@@ -497,7 +501,9 @@ const SidebarMenuItem = React.forwardRef<HTMLLIElement, React.ComponentProps<'li
SidebarMenuItem.displayName = 'SidebarMenuItem'
const sidebarMenuButtonVariants = cva(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-2 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 group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!pl-1.5 group-data-[collapsible=icon]:!pr-2 [&>span:last-child]:truncate [&>svg]:size-5 [&>svg]:shrink-0 text-foreground-lighter data-[active=true]:text-foreground',
cn(
'peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md py-2 px-2 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'
),
{
variants: {
variant: {
@@ -510,10 +516,15 @@ const sidebarMenuButtonVariants = cva(
sm: 'h-7 text-xs',
lg: 'h-12 text-sm group-data-[collapsible=icon]:!p-0',
},
hasIcon: {
true: 'group-data-[collapsible=icon]:!size-8 group-data-[collapsible=icon]:!pl-1.5 group-data-[collapsible=icon]:!pr-2',
false: '',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
hasIcon: true,
},
}
)
@@ -524,6 +535,7 @@ const SidebarMenuButton = React.forwardRef<
asChild?: boolean
isActive?: boolean
tooltip?: string | React.ComponentProps<typeof TooltipContent>
hasIcon?: boolean
} & VariantProps<typeof sidebarMenuButtonVariants>
>(
(
@@ -532,6 +544,7 @@ const SidebarMenuButton = React.forwardRef<
isActive = false,
variant = 'default',
size = 'default',
hasIcon = true,
tooltip,
className,
...props
@@ -547,7 +560,8 @@ const SidebarMenuButton = React.forwardRef<
data-sidebar="menu-button"
data-size={size}
data-active={isActive}
className={cn(sidebarMenuButtonVariants({ variant, size }), className)}
data-has-icon={hasIcon}
className={cn(sidebarMenuButtonVariants({ variant, size, hasIcon }), className)}
{...props}
/>
)
@@ -737,4 +751,5 @@ export {
SidebarSeparator,
SidebarTrigger,
useSidebar,
sidebarMenuButtonVariants,
}