feat(ui): usable on phones without changing the desktop layout
Deploy / deploy (push) Successful in 3m19s
Deploy / deploy (push) Successful in 3m19s
At phone width the sidebar stayed 240px wide, leaving pages a ~135px strip. Below md it is now hidden behind a menu button in the header and opens as a drawer (closes on navigation, backdrop, X or Escape); from md up the sidebar is unchanged. Page wrappers use p-4 on phones and keep p-6/p-8 from md. Shot detail: on phones the thumbnail takes the full width with the details below it instead of pushing them off screen, the action buttons wrap, and the tab bar scrolls sideways. Desktop sizing is kept as it was. Every change is behind a md: breakpoint, so the desktop layout is the same. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -12,8 +12,9 @@ import {
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import { NotificationBell } from "@/components/notifications/NotificationBell";
|
||||
import { getInitials } from "@/lib/utils";
|
||||
import { LogOut, User, Settings } from "lucide-react";
|
||||
import { LogOut, User, Settings, Menu } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { useMobileNav } from "@/components/layout/MobileNav";
|
||||
|
||||
interface HeaderProps {
|
||||
title?: string;
|
||||
@@ -23,11 +24,21 @@ interface HeaderProps {
|
||||
export function Header({ title, breadcrumbs }: HeaderProps) {
|
||||
const { data: session } = useSession();
|
||||
const user = session?.user;
|
||||
const { setOpen: setMobileNavOpen } = useMobileNav();
|
||||
|
||||
return (
|
||||
<header className="flex h-14 items-center justify-between border-b border-zinc-800 bg-zinc-950 px-6">
|
||||
<header className="flex h-14 items-center justify-between border-b border-zinc-800 bg-zinc-950 px-4 md:px-6">
|
||||
{/* Left: Title / Breadcrumbs */}
|
||||
<div className="flex items-center gap-2 min-w-0">
|
||||
{/* Phones only: the sidebar is hidden below md and opens as a drawer. */}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileNavOpen(true)}
|
||||
className="md:hidden -ml-2 rounded-md p-2 text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
||||
aria-label="Open menu"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
</button>
|
||||
{breadcrumbs ? (
|
||||
<nav className="flex items-center gap-1.5 text-sm">
|
||||
{breadcrumbs.map((crumb, i) => (
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
'use client';
|
||||
|
||||
import { createContext, useContext, useState } from 'react';
|
||||
|
||||
/**
|
||||
* Open/closed state of the phone-width navigation drawer, shared between the
|
||||
* Header (which has the menu button) and the Sidebar (which is the drawer).
|
||||
* At md and up the sidebar is always visible and this state is unused.
|
||||
*/
|
||||
const MobileNavContext = createContext<{ open: boolean; setOpen: (open: boolean) => void }>({
|
||||
open: false,
|
||||
setOpen: () => {},
|
||||
});
|
||||
|
||||
export function MobileNavProvider({ children }: { children: React.ReactNode }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
return <MobileNavContext.Provider value={{ open, setOpen }}>{children}</MobileNavContext.Provider>;
|
||||
}
|
||||
|
||||
export const useMobileNav = () => useContext(MobileNavContext);
|
||||
@@ -22,10 +22,12 @@ import {
|
||||
Clapperboard,
|
||||
LayoutGrid,
|
||||
Server,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useState } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useSession } from 'next-auth/react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { useMobileNav } from '@/components/layout/MobileNav';
|
||||
|
||||
const navItems = [
|
||||
{ href: '/dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||
@@ -54,17 +56,76 @@ const montserrat = Montserrat({
|
||||
|
||||
export function Sidebar() {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const [collapsed, setCollapsed] = useState(false);
|
||||
const { open: mobileOpen, setOpen: setMobileOpen } = useMobileNav();
|
||||
|
||||
// Picking a page closes the phone drawer; so does Escape.
|
||||
useEffect(() => {
|
||||
setMobileOpen(false);
|
||||
}, [pathname, setMobileOpen]);
|
||||
useEffect(() => {
|
||||
if (!mobileOpen) return;
|
||||
const onKey = (e: KeyboardEvent) => e.key === 'Escape' && setMobileOpen(false);
|
||||
window.addEventListener('keydown', onKey);
|
||||
return () => window.removeEventListener('keydown', onKey);
|
||||
}, [mobileOpen, setMobileOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* md and up: the sidebar exactly as before. */}
|
||||
<aside
|
||||
className={cn(
|
||||
'hidden md:flex flex-col border-r border-zinc-800 bg-zinc-900 transition-all duration-200',
|
||||
collapsed ? 'w-16' : 'w-60',
|
||||
)}
|
||||
>
|
||||
<SidebarContent collapsed={collapsed} />
|
||||
{/* Collapse toggle */}
|
||||
<div className="border-t border-zinc-800 p-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="w-full"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
{/* Phones: the same navigation as a drawer, opened from the header. */}
|
||||
{mobileOpen && (
|
||||
<div className="fixed inset-0 z-50 md:hidden">
|
||||
<div className="absolute inset-0 bg-black/60" onClick={() => setMobileOpen(false)} />
|
||||
<aside className="absolute inset-y-0 left-0 flex w-64 max-w-[80vw] flex-col overflow-y-auto border-r border-zinc-800 bg-zinc-900">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setMobileOpen(false)}
|
||||
className="absolute right-2 top-2 rounded-md p-2 text-zinc-400 hover:bg-zinc-800 hover:text-white"
|
||||
aria-label="Close menu"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
<SidebarContent collapsed={false} />
|
||||
</aside>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function SidebarContent({ collapsed }: { collapsed: boolean }) {
|
||||
const pathname = usePathname();
|
||||
const { data: session } = useSession();
|
||||
const isAdmin = ['ADMIN', 'PRODUCER'].includes(session?.user?.role ?? '');
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'flex flex-col border-r border-zinc-800 bg-zinc-900 transition-all duration-200',
|
||||
collapsed ? 'w-16' : 'w-60',
|
||||
)}
|
||||
>
|
||||
<>
|
||||
{/* Logo */}
|
||||
<div
|
||||
className={cn(
|
||||
@@ -119,23 +180,6 @@ export function Sidebar() {
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Collapse toggle */}
|
||||
<div className="border-t border-zinc-800 p-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
className="w-full"
|
||||
onClick={() => setCollapsed(!collapsed)}
|
||||
title={collapsed ? 'Expand sidebar' : 'Collapse sidebar'}
|
||||
>
|
||||
{collapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user