add mobile overflow menu

This commit is contained in:
miloschwartz
2026-07-01 10:11:17 -04:00
parent 561f75b6b1
commit 3c37e10638
3 changed files with 149 additions and 61 deletions
+2
View File
@@ -68,6 +68,8 @@ export async function Layout({
navItems={navItems} navItems={navItems}
showSidebar={showSidebar} showSidebar={showSidebar}
showTopBar={showTopBar} showTopBar={showTopBar}
launcherMode={launcherMode}
showViewAsAdmin={showViewAsAdmin}
/> />
)} )}
+117 -31
View File
@@ -6,7 +6,7 @@ import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn"; import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org"; import { ListUserOrgsResponse } from "@server/routers/org";
import { Button } from "@app/components/ui/button"; import { Button } from "@app/components/ui/button";
import { ArrowRight, Menu, Server } from "lucide-react"; import { Menu, Server, Settings, SquareMousePointer } from "lucide-react";
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext"; import { useUserContext } from "@app/hooks/useUserContext";
@@ -29,6 +29,8 @@ interface LayoutMobileMenuProps {
navItems: SidebarNavSection[]; navItems: SidebarNavSection[];
showSidebar: boolean; showSidebar: boolean;
showTopBar: boolean; showTopBar: boolean;
launcherMode?: boolean;
showViewAsAdmin?: boolean;
} }
export function LayoutMobileMenu({ export function LayoutMobileMenu({
@@ -36,19 +38,33 @@ export function LayoutMobileMenu({
orgs, orgs,
navItems, navItems,
showSidebar, showSidebar,
showTopBar showTopBar,
launcherMode = false,
showViewAsAdmin = false
}: LayoutMobileMenuProps) { }: LayoutMobileMenuProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const pathname = usePathname(); const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin"); const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext(); const { user } = useUserContext();
const t = useTranslations(); const t = useTranslations();
const showMobileNav = showSidebar || launcherMode;
const currentOrg = orgs?.find((org) => org.orgId === orgId);
const isSettingsPage = Boolean(
orgId && pathname?.includes(`/${orgId}/settings`)
);
const canViewResourceLauncher = Boolean(
currentOrg?.isAdmin || currentOrg?.isOwner
);
const mobileNavLinkClassName = cn(
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md px-3 py-1.5"
);
return ( return (
<div className="shrink-0 md:hidden sticky top-0 z-50"> <div className="shrink-0 md:hidden sticky top-0 z-50">
<div className="h-16 flex items-center px-2"> <div className="h-16 flex items-center px-2">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{showSidebar && ( {showMobileNav && (
<div> <div>
<Sheet <Sheet
open={isMobileMenuOpen} open={isMobileMenuOpen}
@@ -69,24 +85,24 @@ export function LayoutMobileMenu({
<SheetDescription className="sr-only"> <SheetDescription className="sr-only">
{t("navbarDescription")} {t("navbarDescription")}
</SheetDescription> </SheetDescription>
<div className="w-full border-b border-border"> {launcherMode ? (
<div className="px-1 shrink-0"> <>
<OrgSelector <div className="w-full border-b border-border">
orgId={orgId} <div className="px-1 shrink-0">
orgs={orgs} <OrgSelector
/> orgId={orgId}
</div> orgs={orgs}
</div> />
<div className="flex-1 overflow-y-auto relative"> </div>
<div className="px-3"> </div>
{!isAdminPage && {showViewAsAdmin && orgId ? (
user.serverAdmin && ( <div className="px-3">
<div className="mb-1"> <div className="mb-1">
<Link <Link
href="/admin" href={`/${orgId}/settings`}
className={cn( className={
"flex items-center rounded transition-colors text-muted-foreground hover:text-foreground text-sm w-full hover:bg-secondary/50 dark:hover:bg-secondary/20 rounded-md px-3 py-1.5" mobileNavLinkClassName
)} }
onClick={() => onClick={() =>
setIsMobileMenuOpen( setIsMobileMenuOpen(
false false
@@ -94,25 +110,95 @@ export function LayoutMobileMenu({
} }
> >
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground mr-3"> <span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground mr-3">
<Server className="h-4 w-4" /> <Settings className="h-4 w-4" />
</span> </span>
<span className="flex-1"> <span className="flex-1">
{t( {t(
"serverAdmin" "resourceLauncherViewAsAdmin"
)} )}
</span> </span>
</Link> </Link>
</div> </div>
)} </div>
<SidebarNav ) : null}
sections={navItems} </>
onItemClick={() => ) : (
setIsMobileMenuOpen(false) <>
} <div className="w-full border-b border-border">
/> <div className="px-1 shrink-0">
</div> <OrgSelector
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" /> orgId={orgId}
</div> orgs={orgs}
/>
</div>
</div>
<div className="flex-1 overflow-y-auto relative">
<div className="px-3">
{!isAdminPage &&
isSettingsPage &&
canViewResourceLauncher &&
orgId && (
<div className="mb-1">
<Link
href={`/${orgId}`}
className={
mobileNavLinkClassName
}
onClick={() =>
setIsMobileMenuOpen(
false
)
}
>
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground mr-3">
<SquareMousePointer className="h-4 w-4" />
</span>
<span className="flex-1">
{t(
"resourceLauncherTitle"
)}
</span>
</Link>
</div>
)}
{!isAdminPage &&
user.serverAdmin && (
<div className="mb-1">
<Link
href="/admin"
className={
mobileNavLinkClassName
}
onClick={() =>
setIsMobileMenuOpen(
false
)
}
>
<span className="flex-shrink-0 w-5 h-5 flex items-center justify-center text-muted-foreground mr-3">
<Server className="h-4 w-4" />
</span>
<span className="flex-1">
{t(
"serverAdmin"
)}
</span>
</Link>
</div>
)}
<SidebarNav
sections={navItems}
onItemClick={() =>
setIsMobileMenuOpen(
false
)
}
/>
</div>
<div className="sticky bottom-0 left-0 right-0 h-8 pointer-events-none bg-gradient-to-t from-card to-transparent" />
</div>
</>
)}
</SheetContent> </SheetContent>
</Sheet> </Sheet>
</div> </div>
@@ -371,8 +371,36 @@ export default function ResourceLauncher({
/> />
<div className="flex flex-col gap-3 mb-6"> <div className="flex flex-col gap-3 mb-6">
<div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between"> <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-2 shrink-0 justify-start xl:justify-end order-1 xl:order-2"> <div className="flex flex-col sm:flex-row sm:items-center gap-3 min-w-0 flex-1 order-2 sm:order-none">
<div className="relative w-full sm:max-w-sm shrink-0">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
value={searchInput}
onChange={(event) => {
const value = event.target.value;
setSearchInput(value);
debouncedNavigateSearch(
activeViewIdRef.current,
value
);
}}
placeholder={t(
"resourceLauncherSearchPlaceholder"
)}
className="pl-8"
/>
</div>
<LauncherViewTabs
activeViewId={activeViewId}
savedViews={views.map((view) => ({
viewId: view.viewId,
name: view.name
}))}
onSelectView={selectView}
/>
</div>
<div className="flex items-center gap-2 shrink-0 justify-start sm:justify-end order-1 sm:order-none">
<LauncherSaveViewMenu <LauncherSaveViewMenu
isDefaultView={isDefaultView} isDefaultView={isDefaultView}
isAdmin={isAdmin} isAdmin={isAdmin}
@@ -420,34 +448,6 @@ export default function ResourceLauncher({
}} }}
/> />
</div> </div>
<div className="flex flex-col sm:flex-row sm:items-center gap-3 min-w-0 flex-1 order-2 xl:order-1">
<div className="relative w-full sm:max-w-sm shrink-0">
<Search className="absolute left-2.5 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input
value={searchInput}
onChange={(event) => {
const value = event.target.value;
setSearchInput(value);
debouncedNavigateSearch(
activeViewIdRef.current,
value
);
}}
placeholder={t(
"resourceLauncherSearchPlaceholder"
)}
className="pl-8"
/>
</div>
<LauncherViewTabs
activeViewId={activeViewId}
savedViews={views.map((view) => ({
viewId: view.viewId,
name: view.name
}))}
onSelectView={selectView}
/>
</div>
</div> </div>
</div> </div>