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

View File

@@ -68,6 +68,8 @@ export async function Layout({
navItems={navItems}
showSidebar={showSidebar}
showTopBar={showTopBar}
launcherMode={launcherMode}
showViewAsAdmin={showViewAsAdmin}
/>
)}

View File

@@ -6,7 +6,7 @@ import { OrgSelector } from "@app/components/OrgSelector";
import { cn } from "@app/lib/cn";
import { ListUserOrgsResponse } from "@server/routers/org";
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 { usePathname } from "next/navigation";
import { useUserContext } from "@app/hooks/useUserContext";
@@ -29,6 +29,8 @@ interface LayoutMobileMenuProps {
navItems: SidebarNavSection[];
showSidebar: boolean;
showTopBar: boolean;
launcherMode?: boolean;
showViewAsAdmin?: boolean;
}
export function LayoutMobileMenu({
@@ -36,19 +38,33 @@ export function LayoutMobileMenu({
orgs,
navItems,
showSidebar,
showTopBar
showTopBar,
launcherMode = false,
showViewAsAdmin = false
}: LayoutMobileMenuProps) {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const pathname = usePathname();
const isAdminPage = pathname?.startsWith("/admin");
const { user } = useUserContext();
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 (
<div className="shrink-0 md:hidden sticky top-0 z-50">
<div className="h-16 flex items-center px-2">
<div className="flex items-center gap-4">
{showSidebar && (
{showMobileNav && (
<div>
<Sheet
open={isMobileMenuOpen}
@@ -69,24 +85,24 @@ export function LayoutMobileMenu({
<SheetDescription className="sr-only">
{t("navbarDescription")}
</SheetDescription>
<div className="w-full border-b border-border">
<div className="px-1 shrink-0">
<OrgSelector
orgId={orgId}
orgs={orgs}
/>
</div>
</div>
<div className="flex-1 overflow-y-auto relative">
<div className="px-3">
{!isAdminPage &&
user.serverAdmin && (
{launcherMode ? (
<>
<div className="w-full border-b border-border">
<div className="px-1 shrink-0">
<OrgSelector
orgId={orgId}
orgs={orgs}
/>
</div>
</div>
{showViewAsAdmin && orgId ? (
<div className="px-3">
<div className="mb-1">
<Link
href="/admin"
className={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"
)}
href={`/${orgId}/settings`}
className={
mobileNavLinkClassName
}
onClick={() =>
setIsMobileMenuOpen(
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">
<Server className="h-4 w-4" />
<Settings className="h-4 w-4" />
</span>
<span className="flex-1">
{t(
"serverAdmin"
"resourceLauncherViewAsAdmin"
)}
</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>
</div>
) : null}
</>
) : (
<>
<div className="w-full border-b border-border">
<div className="px-1 shrink-0">
<OrgSelector
orgId={orgId}
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>
</Sheet>
</div>

View File

@@ -371,8 +371,36 @@ export default function ResourceLauncher({
/>
<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 items-center gap-2 shrink-0 justify-start xl:justify-end order-1 xl:order-2">
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<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
isDefaultView={isDefaultView}
isAdmin={isAdmin}
@@ -420,34 +448,6 @@ export default function ResourceLauncher({
}}
/>
</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>