feat(ui): usable on phones without changing the desktop layout
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:
twotalesanimation
2026-09-18 06:56:38 +02:00
parent 33bc6a935c
commit 40b99f3add
21 changed files with 148 additions and 67 deletions
+13 -2
View File
@@ -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) => (
+20
View File
@@ -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);
+70 -26
View File
@@ -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>
</>
);
}