mirror of
https://github.com/supabase/supabase.git
synced 2026-10-18 15:49:09 +03:00
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:
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',
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
export function useHideSidebar() {
|
||||
const pathname = usePathname() ?? ''
|
||||
return (
|
||||
pathname.startsWith('/account') || pathname.startsWith('/new') || pathname === '/organizations'
|
||||
)
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
Reference in new issue
Block a user