feat: studio mobile nav (#31080)

* SheetProvider in studio

* mobile global nav

* view sub navigation

* fix ProjectLayout conditional

* Account mobile layout

* width

* hide

* layout

* overflow

* responsive button

* overflow

* fix mobile nav if IS_PLATFORM

* refactor

* hide if no content

* show command menu item on mobile

* ui-patterns sheet provider

* use client

* show if productMenu

* show if productMenu

* drag to close

* drag to close

* 16px input fontsize on inputs to avoid zoom on mobile

* offscreen test element

* offscreen test element

* offscreen test element

* update test

* update test

* update test

* pb

* test?

* test

* test

* test

* spec-click-target

* remove console

* remove unused import

* add dependency to sheet content

* close sheet on router.asPath change

* remove provider and use simple Sheet

* update sidebar mobile

* align labels

* avoid input zoom on mobile

* remove yo
This commit is contained in:
Francesco Sansalvadore
2024-12-16 10:49:17 -04:00
committed by GitHub
parent 70460772f1
commit 0035920bf1
22 changed files with 658 additions and 355 deletions

View File

@@ -43,7 +43,7 @@ const HomePageActions = ({
const { isSuccess: orgsLoaded } = useOrganizationsQuery()
return (
<div className="flex gap-x-3">
<div className="flex flex-col gap-2 md:gap-3 md:flex-row">
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button type="primary">
@@ -72,12 +72,12 @@ const HomePageActions = ({
</Button>
)}
<div className="flex items-center gap-x-2">
<div className="flex items-center gap-2">
<Input
size="tiny"
placeholder="Search for a project"
icon={<Search size={16} />}
className="w-64 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
className="w-full flex-1 md:w-64 [&>div>div>div>input]:!pl-7 [&>div>div>div>div]:!pl-2"
value={search}
onChange={(event) => setSearch(event.target.value)}
/>

View File

@@ -57,7 +57,7 @@ const SQLQuickstarts = () => {
}
return (
<div className="block h-full space-y-8 overflow-y-auto p-6">
<div className="block h-full space-y-8 overflow-y-auto p-4 md:p-6">
<div className="mb-8">
<div className="mb-4">
<h1 className="text-foreground mb-3 text-xl">Quickstarts</h1>

View File

@@ -57,7 +57,7 @@ const SQLTemplates = () => {
}
return (
<div className="block h-full space-y-8 overflow-y-auto p-6">
<div className="block h-full space-y-8 overflow-y-auto p-4 md:p-6">
<div>
<div className="mb-4">
<h1 className="text-foreground mb-3 text-xl">Scripts</h1>

View File

@@ -1,11 +1,12 @@
import { isUndefined } from 'lodash'
import { ArrowUpRight, LogOut } from 'lucide-react'
import Link from 'next/link'
import { ReactNode } from 'react'
import { PropsWithChildren, ReactNode, useState } from 'react'
import { Badge, cn, Menu } from 'ui'
import { Badge, cn, Menu, Sheet, SheetContent } from 'ui'
import { LayoutHeader } from '../ProjectLayout/LayoutHeader'
import type { SidebarLink, SidebarSection } from './AccountLayout.types'
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
interface WithSidebarProps {
title: string
@@ -16,7 +17,6 @@ interface WithSidebarProps {
subitemsParentKey?: number
hideSidebar?: boolean
customSidebarContent?: ReactNode
children: ReactNode
}
const WithSidebar = ({
@@ -29,63 +29,107 @@ const WithSidebar = ({
subitemsParentKey,
hideSidebar = false,
customSidebarContent,
}: WithSidebarProps) => {
}: PropsWithChildren<WithSidebarProps>) => {
const noContent = !sections && !customSidebarContent
const [isSheetOpen, setIsSheetOpen] = useState(false)
const handleMobileMenu = () => {
setIsSheetOpen(true)
}
return (
<div className="flex h-full">
<div className="flex flex-col md:flex-row h-full">
{!hideSidebar && !noContent && (
<div
id="with-sidebar"
className={[
'h-full bg-dash-sidebar',
'hide-scrollbar w-64 overflow-auto border-r border-default',
].join(' ')}
>
{title && (
<div className="mb-2">
<div className="flex h-12 max-h-12 items-center border-b px-6 border-default">
<h4 className="mb-0 text-lg truncate" title={title}>
{title}
</h4>
</div>
</div>
)}
{header && header}
<div className="-mt-1">
<Menu>
{customSidebarContent}
{sections.map((section) => {
return Boolean(section.heading) ? (
<SectionWithHeaders
key={section.key}
section={section}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
) : (
<div className="border-b py-5 px-6 border-default" key={section.key}>
<SidebarItem
links={section.links}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
</div>
)
})}
</Menu>
</div>
</div>
<SidebarContent
title={title}
header={header}
sections={sections}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
customSidebarContent={customSidebarContent}
className="hidden md:block"
/>
)}
<div className="flex flex-1 flex-col">
<LayoutHeader breadcrumbs={breadcrumbs} />
<LayoutHeader
breadcrumbs={breadcrumbs}
showProductMenu={!hideSidebar && !noContent}
handleMobileMenu={handleMobileMenu}
/>
<div className="flex-1 flex-grow overflow-y-auto">{children}</div>
</div>
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
<SidebarContent
title={title}
header={header}
sections={sections}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
customSidebarContent={customSidebarContent}
/>
</MobileSheetNav>
</div>
)
}
export default WithSidebar
export const SidebarContent = ({
title,
header,
sections,
subitems,
subitemsParentKey,
customSidebarContent,
className,
}: PropsWithChildren<Omit<WithSidebarProps, 'breadcrumbs'>> & { className?: string }) => {
return (
<>
<div
id="with-sidebar"
className={cn(
'h-full bg-dash-sidebar',
'hide-scrollbar w-full md:w-64 overflow-auto md:border-r border-default',
className
)}
>
{title && (
<div className="block mb-2">
<div className="flex h-12 max-h-12 items-center border-b px-6 border-default">
<h4 className="mb-0 text-lg truncate" title={title}>
{title}
</h4>
</div>
</div>
)}
{header && header}
<div className="-mt-1">
<Menu>
{customSidebarContent}
{sections.map((section) => {
return Boolean(section.heading) ? (
<SectionWithHeaders
key={section.key}
section={section}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
) : (
<div className="border-b py-5 px-6 border-default" key={section.key}>
<SidebarItem
links={section.links}
subitems={subitems}
subitemsParentKey={subitemsParentKey}
/>
</div>
)
})}
</Menu>
</div>
</div>
</>
)
}
interface SectionWithHeadersProps {
section: SidebarSection
subitems?: any[]

View File

@@ -77,7 +77,10 @@ const OrganizationLayout = ({ children }: PropsWithChildren<{}>) => {
<ScaffoldTitle className="pb-3">
{selectedOrganization?.name ?? 'Organization'} settings
</ScaffoldTitle>
<NavMenu className="border-none" aria-label="Organization menu navigation">
<NavMenu
className="border-none max-w-full overflow-hidden overflow-x-scroll"
aria-label="Organization menu navigation"
>
{filteredNavMenuItems.map((item) => (
<NavMenuItem key={item.label} active={currentPath === item.href}>
<Link href={item.href}>{item.label}</Link>

View File

@@ -1,5 +1,5 @@
import Link from 'next/link'
import { useMemo } from 'react'
import { ReactNode, useMemo } from 'react'
import Connect from 'components/interfaces/Connect/Connect'
import { useParams } from 'common'
@@ -39,7 +39,22 @@ const LayoutHeaderDivider = () => (
</span>
)
const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder = true }: any) => {
interface LayoutHeaderProps {
customHeaderComponents?: ReactNode
breadcrumbs?: any[]
headerBorder?: boolean
showProductMenu?: boolean
customSidebarContent?: ReactNode
handleMobileMenu: Function
}
const LayoutHeader = ({
customHeaderComponents,
breadcrumbs = [],
headerBorder = true,
showProductMenu,
handleMobileMenu,
}: LayoutHeaderProps) => {
const { ref: projectRef } = useParams()
const selectedProject = useSelectedProject()
const selectedOrganization = useSelectedOrganization()
@@ -72,51 +87,69 @@ const LayoutHeader = ({ customHeaderComponents, breadcrumbs = [], headerBorder =
headerBorder ? 'border-b border-default' : ''
)}
>
<div className="flex items-center justify-between py-2 px-3 flex-1">
<div className="flex items-center text-sm">
{projectRef && (
<>
<div className="flex items-center">
<OrganizationDropdown />
<LayoutHeaderDivider />
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
{selectedProject && isBranchingEnabled && (
<>
<LayoutHeaderDivider />
<BranchDropdown />
</>
)}
</div>
<div className="ml-3 flex items-center gap-x-3">
{connectDialogUpdate && <Connect />}
{!isBranchingEnabled && <EnableBranchingButton />}
</div>
</>
)}
{/* Additional breadcrumbs are supplied */}
<BreadcrumbsView defaultValue={breadcrumbs} />
{showProductMenu && (
<div className="flex items-center justify-center border-r flex-0 md:hidden h-full aspect-square">
<button
title="Menu dropdown button"
className={cn(
'group/view-toggle ml-4 flex justify-center flex-col border-none space-x-0 items-start gap-1 !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px]'
)}
onClick={() => handleMobileMenu()}
>
<div className="h-px inline-block left-0 w-4 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
<div className="h-px inline-block left-0 w-3 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
</button>
</div>
<div className="flex items-center gap-x-2">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM && (
<>
<FeedbackDropdown />
<NotificationsPopoverV2 />
<HelpPopover />
</>
)}
)}
<div className="relative flex flex-1 overflow-hidden">
<div className="flex w-full items-center justify-between py-2 pl-1 pr-3 md:px-3 flex-nowrap overflow-x-scroll">
<div className="flex items-center text-sm">
{projectRef && (
<>
<div className="flex items-center">
<OrganizationDropdown />
<LayoutHeaderDivider />
<ProjectDropdown />
{exceedingLimits && (
<div className="ml-2">
<Link href={`/org/${selectedOrganization?.slug}/usage`}>
<Badge variant="destructive">Exceeding usage limits</Badge>
</Link>
</div>
)}
{selectedProject && isBranchingEnabled && (
<>
<LayoutHeaderDivider />
<BranchDropdown />
</>
)}
</div>
<div className="ml-3 flex items-center gap-x-3">
{connectDialogUpdate && <Connect />}
{!isBranchingEnabled && <EnableBranchingButton />}
</div>
</>
)}
{/* Additional breadcrumbs are supplied */}
<BreadcrumbsView defaultValue={breadcrumbs} />
</div>
<div className="flex items-center gap-x-2">
{customHeaderComponents && customHeaderComponents}
{IS_PLATFORM && (
<>
<FeedbackDropdown />
<NotificationsPopoverV2 />
<HelpPopover />
</>
)}
</div>
</div>
<div className="absolute md:hidden left-0 h-full w-3 bg-gradient-to-r from-background-dash-sidebar to-transparent pointer-events-none" />
<div className="absolute md:hidden right-0 h-full w-3 bg-gradient-to-l from-background-dash-sidebar to-transparent pointer-events-none" />
</div>
{!!projectRef && (
<div className="border-l flex-0 h-full">

View File

@@ -0,0 +1,90 @@
import Link from 'next/link'
import { useRouter } from 'next/router'
import { Search, Menu } from 'lucide-react'
import { useParams } from 'common'
import { IS_PLATFORM } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { buttonVariants, cn, Sheet, SheetContent } from 'ui'
import { CommandMenuTrigger } from 'ui-patterns'
import { NavContent } from './NavigationBar'
import { useState } from 'react'
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
export const ICON_SIZE = 20
export const ICON_STROKE_WIDTH = 1.5
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">
<nav
className={cn(
'group px-4 z-10 w-full h-14',
'border-b bg-dash-sidebar border-default shadow-xl',
'transition-width duration-200',
'hide-scrollbar flex flex-row items-center justify-between overflow-x-auto'
)}
>
<Link
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
className="flex items-center h-[26px] w-[26px] min-w-[26px]"
onClick={onCloseNavigationIconLink}
>
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
className="absolute h-[26px] w-[26px] cursor-pointer rounded"
/>
</Link>
<div className="flex gap-2">
<CommandMenuTrigger>
<button
className={cn(
'group',
'flex-grow h-[30px] rounded-md',
'p-2',
'flex items-center justify-between',
'bg-transparent border-none text-foreground-lighter',
'hover:bg-opacity-100 hover:border-strong hover:text-foreground-light',
'focus-visible:!outline-4 focus-visible:outline-offset-1 focus-visible:outline-brand-600',
'transition'
)}
>
<div className="flex items-center space-x-2">
<Search size={18} strokeWidth={2} />
</div>
</button>
</CommandMenuTrigger>
<button
title="Menu dropdown button"
className={cn(
buttonVariants({ type: 'default' }),
'flex lg:hidden border-default bg-surface-100/75 text-foreground-light rounded-md min-w-[30px] w-[30px] h-[30px] data-[state=open]:bg-overlay-hover/30'
)}
onClick={() => setIsSheetOpen(true)}
>
<Menu size={18} strokeWidth={1} />
</button>
</div>
</nav>
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
<NavContent />
</MobileSheetNav>
</div>
)
}
export default MobileNavigationBar

View File

@@ -0,0 +1,39 @@
import { PropsWithChildren } from 'react'
import { cn } from 'ui'
export const ICON_SIZE = 20
export const ICON_STROKE_WIDTH = 1.5
interface Props {
title?: string
handleMobileMenu: Function
}
const MobileViewNav = ({ title, handleMobileMenu }: PropsWithChildren<Props>) => {
return (
<nav
className={cn(
'group px-4 z-10 w-full h-10',
'border-b border-default',
'transition-width duration-200',
'flex md:hidden flex-row items-center gap-1 overflow-x-auto'
)}
>
<button
title="Menu dropdown button"
className={cn(
'group/view-toggle flex justify-center flex-col border-none space-x-0 items-start gap-1 !bg-transparent rounded-md min-w-[30px] w-[30px] h-[30px]'
)}
onClick={() => handleMobileMenu()}
>
<div className="h-px inline-block left-0 w-4 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
<div className="h-px inline-block left-0 w-3 transition-all ease-out bg-foreground-lighter group-hover/view-toggle:bg-foreground p-0 m-0" />
</button>
<div className="flex items-center">
<h4 className="text-base">{title}</h4>
</div>
</nav>
)
}
export default MobileViewNav

View File

@@ -15,7 +15,6 @@ 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 { detectOS } from 'lib/helpers'
import { useProfile } from 'lib/profile'
import { useAppStateSnapshot } from 'state/app-state'
import {
@@ -57,7 +56,37 @@ export const ICON_SIZE = 20
export const ICON_STROKE_WIDTH = 1.5
const NavigationBar = () => {
const os = detectOS()
const snap = useAppStateSnapshot()
const [userDropdownOpen, setUserDropdownOpenState] = useState(false)
const [allowNavPanelToExpand] = useLocalStorageQuery(
LOCAL_STORAGE_KEYS.EXPAND_NAVIGATION_PANEL,
true
)
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={() => {
if (!userDropdownOpen && allowNavPanelToExpand) snap.setNavigationPanelOpen(false)
}}
>
<NavContent />
</nav>
</div>
)
}
export const NavContent = () => {
const router = useRouter()
const { profile } = useProfile()
const { project } = useProjectContext()
@@ -169,9 +198,9 @@ const NavigationBar = () => {
</figure>
<span
className={cn(
'w-[8rem] flex flex-col items-start text-sm truncate',
'absolute left-7 group-data-[state=expanded]:left-10',
'group-data-[state=collapsed]:opacity-0 group-data-[state=expanded]:opacity-100',
'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'
)}
>
@@ -196,247 +225,229 @@ const NavigationBar = () => {
)
return (
<div className="w-14 h-full flex flex-col">
<nav
data-state={snap.navigationPanelOpen ? 'expanded' : 'collapsed'}
className={cn(
'group py-2 z-10 h-full w-14 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={() => {
if (!userDropdownOpen && allowNavPanelToExpand) snap.setNavigationPanelOpen(false)
}}
>
<ul className="flex flex-col gap-y-1 justify-start px-2 relative">
<Link
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
className="mx-2 flex items-center h-[40px]"
onClick={onCloseNavigationIconLink}
>
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
className="absolute h-[40px] w-6 cursor-pointer rounded"
/>
</Link>
<>
<ul className="flex flex-col gap-y-1 justify-start px-2 relative">
<Link
href={IS_PLATFORM ? '/projects' : `/project/${projectRef}`}
className="mx-2 hidden md:flex items-center w-[40px] h-[40px]"
onClick={onCloseNavigationIconLink}
>
<img
alt="Supabase"
src={`${router.basePath}/img/supabase-logo.svg`}
className="absolute h-[40px] w-6 cursor-pointer rounded"
/>
</Link>
<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
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} />,
}}
key={route.key}
route={route}
isActive={activeRoute === route.key}
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" />
{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}
<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'
)}
/>
</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>
</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 gap-y-1">
{settingsRoutes.map((route) => (
<NavigationIconLink
key={route.key}
route={route}
isActive={activeRoute === route.key}
onClick={onCloseNavigationIconLink}
/>
))}
<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_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>{CommandButton}</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="right">
<span>Commands</span>
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
)}
{allowNavPanelToExpand && CommandButton}
</>
)}
<DropdownMenu
open={userDropdownOpen}
onOpenChange={(open: boolean) => {
setUserDropdownOpenState(open)
if (open === false) snap.setNavigationPanelOpen(false)
}}
>
{allowNavPanelToExpand ? (
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
) : (
{IS_PLATFORM && (
<>
{!allowNavPanelToExpand && (
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
</TooltipTrigger_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>{CommandButton}</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="right">
<span>Account settings</span>
<span>Commands</span>
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
)}
{allowNavPanelToExpand && CommandButton}
</>
)}
<DropdownMenuContent side="top" align="start">
{IS_PLATFORM && (
<>
<div className="px-2 py-1 flex flex-col gap-0 text-sm">
{profile && (
<>
<DropdownMenu
open={userDropdownOpen}
onOpenChange={(open: boolean) => {
setUserDropdownOpenState(open)
if (open === false) snap.setNavigationPanelOpen(false)
}}
>
{allowNavPanelToExpand ? (
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
) : (
<Tooltip_Shadcn_>
<TooltipTrigger_Shadcn_ asChild>
<DropdownMenuTrigger asChild>{UserAccountButton}</DropdownMenuTrigger>
</TooltipTrigger_Shadcn_>
<TooltipContent_Shadcn_ side="right">
<span>Account settings</span>
</TooltipContent_Shadcn_>
</Tooltip_Shadcn_>
)}
<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.username}
className="w-full text-left text-foreground truncate"
title={profile.primary_email}
className="w-full text-left text-foreground-light text-xs truncate"
>
{profile.username}
{profile.primary_email}
</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>
)}
</>
)}
</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>
<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>
</nav>
</div>
</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>
</>
)
}

View File

@@ -26,8 +26,8 @@ export const NavigationIconButton = forwardRef<
<div className="absolute left-2 text-foreground-lighter">{icon}</div>
<span
className={cn(
'absolute left-7 group-data-[state=expanded]:left-10',
'opacity-0 group-data-[state=expanded]:opacity-100',
'absolute left-10 md:left-7 md:group-data-[state=expanded]:left-10',
'opacity-100 md:opacity-0 md:group-data-[state=expanded]:opacity-100',
'w-[10rem] text-sm flex flex-col items-center',
'transition-all'
)}
@@ -38,8 +38,8 @@ export const NavigationIconButton = forwardRef<
<div
className={cn(
'absolute right-2 flex items-center',
'opacity-0 transition-all',
'group-data-[state=expanded]:opacity-100 '
'opacity-100 md:opacity-0 transition-all',
'md:group-data-[state=expanded]:opacity-100 '
)}
>
{rightText}

View File

@@ -36,7 +36,7 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
const classes = [
'relative',
'h-10 w-10 group-data-[state=expanded]:w-full',
'h-10 w-full md:w-10 md:group-data-[state=expanded]:w-full',
'transition-all duration-200',
'flex items-center rounded',
'group-data-[state=collapsed]:justify-center',
@@ -80,8 +80,8 @@ const NavigationIconLink = forwardRef<HTMLAnchorElement, NavigationIconButtonPro
'min-w-[128px] text-sm text-foreground-light',
'group-hover/item:text-foreground',
'group-aria-current/item:text-foreground',
'absolute left-7 group-data-[state=expanded]:left-12',
'opacity-0 group-data-[state=expanded]:opacity-100',
'absolute left-10 md:left-7 md:group-data-[state=expanded]:left-12',
'opacity-100 md:opacity-0 md:group-data-[state=expanded]:opacity-100',
`${isActive && 'text-foreground hover:text-foreground'}`,
'transition-all'
)}

View File

@@ -7,6 +7,10 @@ interface ProductMenuBarProps {
const ProductMenuBar = ({ title, children }: PropsWithChildren<ProductMenuBarProps>) => {
return (
<div
/**
* id used in playwright-tests/tests/snapshot/spec/table-editor.spec.ts
* */
id="spec-click-target"
className={[
'hide-scrollbar flex flex-col w-full h-full', // Layout
'bg-dash-sidebar',

View File

@@ -1,6 +1,14 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import { forwardRef, Fragment, PropsWithChildren, ReactNode, useEffect, useState } from 'react'
import {
forwardRef,
Fragment,
PropsWithChildren,
ReactNode,
useCallback,
useEffect,
useState,
} from 'react'
import { useParams } from 'common'
import ProjectAPIDocs from 'components/interfaces/ProjectAPIDocs/ProjectAPIDocs'
@@ -14,7 +22,7 @@ import { withAuth } from 'hooks/misc/withAuth'
import { IS_PLATFORM, PROJECT_STATUS } from 'lib/constants'
import { useAppStateSnapshot } from 'state/app-state'
import { useDatabaseSelectorStateSnapshot } from 'state/database-selector'
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup } from 'ui'
import { cn, ResizableHandle, ResizablePanel, ResizablePanelGroup, Sheet, SheetContent } from 'ui'
import AppLayout from '../AppLayout/AppLayout'
import EnableBranchingModal from '../AppLayout/EnableBranchingButton/EnableBranchingModal'
import BuildingState from './BuildingState'
@@ -22,6 +30,8 @@ import ConnectingState from './ConnectingState'
import { LayoutHeader } from './LayoutHeader'
import LoadingState from './LoadingState'
import NavigationBar from './NavigationBar/NavigationBar'
import MobileNavigationBar from './NavigationBar/MobileNavigationBar'
import MobileViewNav from './NavigationBar/MobileViewNav'
import { ProjectPausedState } from './PausedState/ProjectPausedState'
import PauseFailedState from './PauseFailedState'
import PausingState from './PausingState'
@@ -32,6 +42,7 @@ import RestartingState from './RestartingState'
import RestoreFailedState from './RestoreFailedState'
import RestoringState from './RestoringState'
import { UpgradingState } from './UpgradingState'
import MobileSheetNav from 'ui-patterns/MobileSheetNav/MobileSheetNav'
// [Joshen] This is temporary while we unblock users from managing their project
// if their project is not responding well for any reason. Eventually needs a bit of an overhaul
@@ -87,6 +98,7 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
ref
) => {
const router = useRouter()
const [isSheetOpen, setIsSheetOpen] = useState(false)
const { ref: projectRef } = useParams()
const selectedOrganization = useSelectedOrganization()
const selectedProject = useSelectedProject()
@@ -126,6 +138,10 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [open])
const handleMobileMenu = () => {
setIsSheetOpen(true)
}
return (
<AppLayout>
<ProjectContextProvider projectRef={projectRef}>
@@ -143,21 +159,32 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
</title>
<meta name="description" content="Supabase Studio" />
</Head>
<div className="flex h-full">
<div className="flex flex-col md:flex-row h-full">
{/* Left-most navigation side bar to access products */}
{!hideIconBar && <NavigationBar />}
{/* Top Nav to access products from mobile */}
{!hideIconBar && <MobileNavigationBar />}
{showProductMenu && productMenu && !(!hideHeader && IS_PLATFORM) && (
<MobileViewNav title={product} handleMobileMenu={handleMobileMenu} />
)}
{/* Product menu bar */}
<ResizablePanelGroup
className="flex h-full"
direction="horizontal"
autoSaveId="project-layout"
>
{/* Existing desktop menu */}
<ResizablePanel
id="panel-left"
className={cn(resizableSidebar ? 'min-w-64 max-w-[32rem]' : 'min-w-64 max-w-64', {
hidden: !showProductMenu || !productMenu,
})}
defaultSize={0} // forces panel to smallest width possible, at w-64
className={cn(
'hidden md:flex',
resizableSidebar ? 'min-w-64 max-w-[32rem]' : 'min-w-64 max-w-64',
{
'!hidden': !showProductMenu || !productMenu,
}
)}
defaultSize={0}
>
<MenuBarWrapper
isLoading={isLoading}
@@ -168,18 +195,23 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
</MenuBarWrapper>
</ResizablePanel>
<ResizableHandle
className={cn({ hidden: !showProductMenu || !productMenu })}
className={cn('hidden md:flex', { '!hidden': !showProductMenu || !productMenu })}
withHandle
disabled={resizableSidebar ? false : true}
/>
<ResizablePanel id="panel-right" className="h-full flex flex-col">
{!hideHeader && IS_PLATFORM && <LayoutHeader />}
{!hideHeader && IS_PLATFORM && (
<LayoutHeader
showProductMenu={!!(showProductMenu && productMenu)}
handleMobileMenu={handleMobileMenu}
/>
)}
<ResizablePanelGroup
className="h-full w-full overflow-x-hidden flex-1"
direction="horizontal"
autoSaveId="project-layout-content"
>
<ResizablePanel id="panel-content" className=" w-full min-w-[600px]">
<ResizablePanel id="panel-content" className="w-full md:min-w-[600px]">
<main
className="h-full flex flex-col flex-1 w-full overflow-y-auto overflow-x-hidden"
ref={ref}
@@ -222,6 +254,9 @@ const ProjectLayout = forwardRef<HTMLDivElement, PropsWithChildren<ProjectLayout
<AISettingsModal />
<ProjectAPIDocs />
</ProjectContextProvider>
<MobileSheetNav open={isSheetOpen} onOpenChange={setIsSheetOpen}>
{productMenu}
</MobileSheetNav>
</AppLayout>
)
}

View File

@@ -3,7 +3,7 @@ import { useAppStateSnapshot } from 'state/app-state'
import { cn } from 'ui'
export const MAX_WIDTH_CLASSES = 'mx-auto w-full max-w-[1200px]'
export const PADDING_CLASSES = 'px-6 lg:px-14 xl:px-24 2xl:px-32'
export const PADDING_CLASSES = 'px-4 md:px-6 lg:px-14 xl:px-24 2xl:px-32'
export const MAX_WIDTH_CLASSES_COLUMN = 'min-w-[420px]'
const ScaffoldHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(

View File

@@ -39,12 +39,14 @@ import {
} from 'ui-patterns/InnerSideMenu'
import { useProjectContext } from '../ProjectLayout/ProjectContext'
import EntityListItem from './EntityListItem'
import { useBreakpoint } from 'common/hooks/useBreakpoint'
const TableEditorMenu = () => {
const { id: _id } = useParams()
const id = _id ? Number(_id) : undefined
const snap = useTableEditorStateSnapshot()
const { selectedSchema, setSelectedSchema } = useQuerySchemaState()
const isMobile = useBreakpoint()
const [showModal, setShowModal] = useState(false)
const [searchText, setSearchText] = useState<string>('')
@@ -169,7 +171,7 @@ const TableEditorMenu = () => {
<div className="flex flex-auto flex-col gap-2 pb-4 px-2">
<InnerSideBarFilters>
<InnerSideBarFilterSearchInput
autoFocus
autoFocus={!isMobile}
name="search-tables"
aria-labelledby="Search tables"
onChange={(e) => {
@@ -201,7 +203,7 @@ const TableEditorMenu = () => {
<PopoverTrigger_Shadcn_ asChild>
<Button
type={visibleTypes.length !== 5 ? 'default' : 'dashed'}
className="h-[28px] px-1.5"
className="h-[32px] md:h-[28px] px-1.5"
icon={<Filter />}
/>
</PopoverTrigger_Shadcn_>

View File

@@ -24,7 +24,7 @@ const MigrationsPage: NextPageWithLayout = () => {
description="History of migrations that have been run on your database"
/>
</ScaffoldSectionContent>
<ScaffoldSectionDetail className="flex items-center justify-end gap-x-2">
<ScaffoldSectionDetail className="flex items-center md:justify-end gap-x-2">
<DocsButton
className="no-underline"
href="https://supabase.com/docs/guides/deployment/database-migrations"

View File

@@ -145,8 +145,8 @@ const InnerSideBarFilterSearchInput = forwardRef<
ref={ref}
type="text"
className={cn(
'h-[28px] w-full',
'text-xs',
'h-[32px] md:h-[28px] w-full',
'text-base md:text-xs',
'pl-7',
'pr-7',
'w-full',
@@ -179,7 +179,7 @@ const InnerSideBarFilterSortDropdown = forwardRef<
<DropdownMenuTrigger
asChild
className={cn(
'absolute right-1 top-[.3rem]',
'absolute right-1 top-[.4rem] md:top-[.3rem]',
'text-foreground-muted transition-colors hover:text-foreground data-[state=open]:text-foreground',
triggerClassName
)}

View File

@@ -0,0 +1,44 @@
'use client'
import { useRouter } from 'next/router'
import { useEffect } from 'react'
import { ErrorBoundary } from 'react-error-boundary'
import { useWindowSize } from 'react-use'
import { CommandEmpty_Shadcn_, Sheet, SheetContent } from 'ui'
import { cn } from 'ui/src/lib/utils'
const MobileSheetNav: React.FC<{
children: React.ReactNode
open?: boolean
onOpenChange(open: boolean): void
}> = ({ children, open = false, onOpenChange }) => {
const router = useRouter()
const { width } = useWindowSize()
useEffect(() => {
onOpenChange(false)
}, [router?.asPath])
useEffect(() => {
onOpenChange(false)
}, [width])
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
id="mobile-sheet-content"
showClose={false}
size="full"
side="bottom"
className={cn(
'rounded-t-lg overflow-hidden overflow-y-scroll',
'h-[85dvh] md:max-h-[500px] py-2'
)}
>
<ErrorBoundary FallbackComponent={() => <CommandEmpty_Shadcn_ />}>{children}</ErrorBoundary>
</SheetContent>
</Sheet>
)
}
export default MobileSheetNav

View File

@@ -0,0 +1 @@
export * from './MobileSheetNav'

View File

@@ -8,6 +8,7 @@ export * from './CountdownWidget'
export * from './ExpandableVideo'
export * from './GlassPanel'
export * from './IconPanel'
export * from './MobileSheetNav'
export * from './PrivacySettings'
export * from './ThemeToggle'
export * from './TweetCard'

View File

@@ -8,7 +8,7 @@ export interface InputProps
VariantProps<typeof InputVariants> {}
export const InputVariants = cva(
'aria-[] flex h-10 w-full rounded-md border border-control bg-foreground/[.026] px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:bg-destructive-200 aria-[invalid=true]:border-destructive-400 aria-[invalid=true]:focus:border-destructive aria-[invalid=true]:focus-visible:border-destructive',
'aria-[] flex h-10 w-full rounded-md border border-control bg-foreground/[.026] px-3 py-2 text-base md:text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-foreground-muted focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-background-control focus-visible:ring-offset-2 focus-visible:ring-offset-foreground-muted disabled:cursor-not-allowed disabled:opacity-50 aria-[invalid=true]:bg-destructive-200 aria-[invalid=true]:border-destructive-400 aria-[invalid=true]:focus:border-destructive aria-[invalid=true]:focus-visible:border-destructive',
{
variants: {
size: {

View File

@@ -77,11 +77,10 @@ test.describe('Table Editor page', () => {
await page.getByTestId('table-editor-pick-column-to-sort-button').click()
await page.getByLabel('Pick a column to sort by').getByText('defaultValueColumn').click()
await page.getByRole('button', { name: 'Apply sorting' }).click()
// click away to close the sorting dialog
await page
.locator('div')
.filter({ hasText: /^Table Editor$/ })
.click()
await page.locator('#spec-click-target').click()
// expect the row to be sorted by defaultValueColumn. They're inserted in the order 100, 2
await expect(page.locator('div.rdg-row:nth-child(2)')).toContainText('2')
await expect(page.locator('div.rdg-row:nth-child(3)')).toContainText('100')
@@ -98,10 +97,7 @@ test.describe('Table Editor page', () => {
await page.getByPlaceholder('Enter a value').fill('2')
await page.getByRole('button', { name: 'Apply filter' }).click()
// click away to close the filter dialog
await page
.locator('div')
.filter({ hasText: /^Table Editor$/ })
.click()
await page.locator('#spec-click-target').click()
await expect(page.getByRole('grid')).toContainText('2')
await expect(page.getByRole('grid')).not.toContainText('100')
})