mirror of
https://github.com/fosrl/pangolin.git
synced 2026-08-08 13:38:33 +02:00
add mobile overflow menu
This commit is contained in:
@@ -68,6 +68,8 @@ export async function Layout({
|
|||||||
navItems={navItems}
|
navItems={navItems}
|
||||||
showSidebar={showSidebar}
|
showSidebar={showSidebar}
|
||||||
showTopBar={showTopBar}
|
showTopBar={showTopBar}
|
||||||
|
launcherMode={launcherMode}
|
||||||
|
showViewAsAdmin={showViewAsAdmin}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user