style(admin): reduce header height and simplify layout spacing and menu item focus styles
This commit is contained in:
@@ -52,7 +52,7 @@ const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, ap
|
||||
const userInitials = getUserInitials(user?.name);
|
||||
|
||||
return (
|
||||
<header className="bg-white dark:bg-black border-b border-neutral-200 dark:border-neutral-800/70 sticky top-0 z-30 h-14 flex items-center w-full">
|
||||
<header className="bg-white dark:bg-black border-b border-neutral-200 dark:border-neutral-800/70 sticky top-0 z-30 h-12 flex items-center w-full">
|
||||
<div className="flex items-center justify-between lg:justify-end px-4 lg:px-6 py-2 w-full">
|
||||
{/* Left section — Mobile menu button + Logo */}
|
||||
<div className="flex items-center space-x-3 lg:hidden">
|
||||
@@ -164,7 +164,7 @@ const AdminHeader = ({ isMobileMenuOpen, setIsMobileMenuOpen, user, onLogout, ap
|
||||
<MenuItem>
|
||||
<a
|
||||
href="/admin/profile"
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-neutral-500 dark:text-neutral-400 transition-colors duration-150 data-focus:bg-violet-50 dark:data-focus:bg-violet-500/10 data-focus:text-violet-500 dark:data-focus:text-violet-400"
|
||||
className="flex items-center gap-3 px-3 py-2.5 rounded-lg text-sm text-neutral-500 dark:text-neutral-400 transition-colors duration-[120ms] ease-out data-focus:bg-neutral-100 dark:data-focus:bg-white/5 data-focus:text-neutral-900 dark:data-focus:text-white"
|
||||
>
|
||||
<svg className="w-4 h-4 shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.75} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function AdminPagesLayout({ children, user, onLogout, appName, en
|
||||
<div className="flex-1 flex flex-col min-w-0">
|
||||
<AdminHeader isMobileMenuOpen={isMobileMenuOpen} setIsMobileMenuOpen={setIsMobileMenuOpen} user={user} onLogout={onLogout} appName={appName} />
|
||||
<main className="flex-1 overflow-y-auto bg-neutral-50 dark:bg-black">
|
||||
<div className="px-4 sm:px-6 lg:px-8 xl:px-12 pt-4 sm:pt-6 lg:pt-8 pb-32 max-w-[1400px] mx-auto">
|
||||
<div className="px-8 py-7 pb-32 max-w-[1400px] mx-auto">
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -11,35 +11,28 @@ import { ChevronDownIcon } from '@zen/core/shared/icons';
|
||||
* Icons are passed as strings from server to avoid serialization issues
|
||||
*/
|
||||
function resolveIcon(iconNameOrComponent) {
|
||||
// If it's already a component (function), return it
|
||||
if (typeof iconNameOrComponent === 'function') {
|
||||
return iconNameOrComponent;
|
||||
}
|
||||
// If it's a string, look up in Icons
|
||||
if (typeof iconNameOrComponent === 'string') {
|
||||
return Icons[iconNameOrComponent] || Icons.DashboardSquare03Icon;
|
||||
}
|
||||
// Default fallback
|
||||
return Icons.DashboardSquare03Icon;
|
||||
}
|
||||
|
||||
const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledModules, navigationSections: serverNavigationSections }) => {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
||||
// State to manage collapsed sections (all open by default)
|
||||
|
||||
const [collapsedSections, setCollapsedSections] = useState(new Set());
|
||||
|
||||
// Function to toggle a section's state
|
||||
const toggleSection = (sectionId) => {
|
||||
// Find the section to check if it has active items
|
||||
const section = navigationSections.find(s => s.id === sectionId);
|
||||
|
||||
// Don't allow collapsing sections with active items
|
||||
|
||||
if (section && isSectionActive(section)) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
setCollapsedSections(prev => {
|
||||
const newCollapsed = new Set(prev);
|
||||
if (newCollapsed.has(sectionId)) {
|
||||
@@ -51,15 +44,13 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
});
|
||||
};
|
||||
|
||||
// Handle mobile menu closure when clicking on a link
|
||||
const handleMobileLinkClick = () => {
|
||||
setIsMobileMenuOpen(false);
|
||||
};
|
||||
|
||||
// Close mobile menu on screen size change
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
if (window.innerWidth >= 1024) { // lg breakpoint
|
||||
if (window.innerWidth >= 1024) {
|
||||
setIsMobileMenuOpen(false);
|
||||
}
|
||||
};
|
||||
@@ -68,23 +59,18 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
return () => window.removeEventListener('resize', handleResize);
|
||||
}, [setIsMobileMenuOpen]);
|
||||
|
||||
// Function to check if any item in a section is currently active
|
||||
const isSectionActive = (section) => {
|
||||
return section.items.some(item => item.current);
|
||||
};
|
||||
|
||||
// Function to check if a section should be rendered as a direct link
|
||||
const shouldRenderAsDirectLink = (section) => {
|
||||
// Check if there's only one item and it has the same name as the section
|
||||
return section.items.length === 1 &&
|
||||
return section.items.length === 1 &&
|
||||
section.items[0].name.toLowerCase() === section.title.toLowerCase();
|
||||
};
|
||||
|
||||
// Update collapsed sections when pathname changes to ensure active sections are open
|
||||
useEffect(() => {
|
||||
setCollapsedSections(prev => {
|
||||
const newSet = new Set(prev);
|
||||
// Add any sections that have active items to ensure they stay open
|
||||
navigationSections.forEach(section => {
|
||||
if (isSectionActive(section)) {
|
||||
newSet.add(section.id);
|
||||
@@ -95,9 +81,6 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [pathname]);
|
||||
|
||||
// Use server-provided navigation sections if available, otherwise use core-only fallback
|
||||
// Server navigation includes module navigation, fallback only has core pages
|
||||
// Update the 'current' property based on the actual pathname (client-side)
|
||||
const navigationSections = serverNavigationSections.map(section => ({
|
||||
...section,
|
||||
items: section.items.map(item => ({
|
||||
@@ -106,11 +89,15 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
}))
|
||||
}));
|
||||
|
||||
// Function to render a complete navigation section
|
||||
const itemBase = 'w-full flex items-center justify-between px-[10px] py-[7px] rounded-lg text-[13px] transition-colors duration-[120ms] ease-out';
|
||||
const itemActive = 'bg-neutral-200 dark:bg-neutral-800 text-neutral-900 dark:text-white font-semibold';
|
||||
const itemInactive = 'text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-900 hover:text-neutral-900 dark:hover:text-white';
|
||||
|
||||
const subItemBase = 'w-full flex items-center justify-between px-[10px] py-[7px] rounded-lg text-[12px] transition-colors duration-[120ms] ease-out';
|
||||
|
||||
const renderNavSection = (section) => {
|
||||
const Icon = resolveIcon(section.icon);
|
||||
|
||||
// If section should be rendered as a direct link
|
||||
|
||||
if (shouldRenderAsDirectLink(section)) {
|
||||
const item = section.items[0];
|
||||
return (
|
||||
@@ -118,18 +105,14 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={handleMobileLinkClick}
|
||||
className={`${
|
||||
item.current
|
||||
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-900 dark:text-white hover:text-neutral-500 dark:hover:text-neutral-300'
|
||||
} w-full flex items-center justify-between border-y border-neutral-200 dark:border-neutral-800/70 px-4 py-2.5 -mb-[1px] text-[13px] tracking-wide transition-colorsduration-0`}
|
||||
className={`${itemBase} ${item.current ? itemActive : itemInactive}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon className="mr-3 h-4 w-4 flex-shrink-0" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
|
||||
<span>{section.title}</span>
|
||||
</div>
|
||||
{item.badge && (
|
||||
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full font-medium">
|
||||
<span className="bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-lg font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
@@ -138,9 +121,9 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
);
|
||||
}
|
||||
|
||||
// Regular section with expandable sub-items
|
||||
const isCollapsed = !collapsedSections.has(section.id);
|
||||
|
||||
const isActive = isSectionActive(section);
|
||||
|
||||
return (
|
||||
<div key={section.id}>
|
||||
<button
|
||||
@@ -150,26 +133,26 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
}
|
||||
toggleSection(section.id);
|
||||
}}
|
||||
className="cursor-pointer w-full flex items-center justify-between border-y border-neutral-200 dark:border-neutral-800/70 px-4 py-2.5 -mb-[1px] text-[13px] text-neutral-900 dark:text-white tracking-wide hover:text-neutral-500 dark:hover:text-neutral-300 transition-colorsduration-0"
|
||||
className={`cursor-pointer ${itemBase} ${isActive ? itemActive : itemInactive}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon className="mr-3 h-4 w-4 flex-shrink-0" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
|
||||
<span>{section.title}</span>
|
||||
</div>
|
||||
<ChevronDownIcon
|
||||
className={`h-3 w-3 ${
|
||||
<ChevronDownIcon
|
||||
className={`h-3 w-3 transition-transform duration-[120ms] ease-out ${
|
||||
isCollapsed ? '-rotate-90' : 'rotate-0'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
className={`overflow-hidden ${
|
||||
isCollapsed
|
||||
? 'max-h-0 opacity-0'
|
||||
<div
|
||||
className={`overflow-hidden transition-all duration-[120ms] ease-out ${
|
||||
isCollapsed
|
||||
? 'max-h-0 opacity-0'
|
||||
: 'max-h-[1000px] opacity-100'
|
||||
}`}
|
||||
>
|
||||
<ul className="flex flex-col gap-0">
|
||||
<ul className="flex flex-col gap-0.5 pt-0.5 pl-3">
|
||||
{section.items.map(renderNavItem)}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -177,7 +160,6 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
);
|
||||
};
|
||||
|
||||
// Function to render a navigation item
|
||||
const renderNavItem = (item) => {
|
||||
const Icon = resolveIcon(item.icon);
|
||||
return (
|
||||
@@ -185,18 +167,14 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
<Link
|
||||
href={item.href}
|
||||
onClick={handleMobileLinkClick}
|
||||
className={`${
|
||||
item.current
|
||||
? 'bg-neutral-100 dark:bg-neutral-800 text-neutral-900 dark:text-white'
|
||||
: 'text-neutral-500 dark:text-neutral-400 hover:bg-neutral-100 dark:hover:bg-neutral-800/50 hover:text-neutral-900 dark:hover:text-white'
|
||||
} group flex items-center justify-between px-4 py-1.5 text-[12px] font-medium transition-allduration-0`}
|
||||
className={`${subItemBase} ${item.current ? itemActive : itemInactive}`}
|
||||
>
|
||||
<div className="flex items-center">
|
||||
<Icon className="mr-3 h-3.5 w-3.5 flex-shrink-0" />
|
||||
<div className="flex items-center gap-3">
|
||||
<Icon className="h-[15px] w-[15px] flex-shrink-0" />
|
||||
{item.name}
|
||||
</div>
|
||||
{item.badge && (
|
||||
<span className="bg-red-500 text-white text-xs px-1.5 py-0.5 rounded-full font-medium">
|
||||
<span className="bg-red-500 text-white text-[10px] px-1.5 py-0.5 rounded-lg font-medium">
|
||||
{item.badge}
|
||||
</span>
|
||||
)}
|
||||
@@ -209,8 +187,8 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
<>
|
||||
{/* Mobile overlay */}
|
||||
{isMobileMenuOpen && (
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 bg-black/50 backdrop-blur-sm z-40"
|
||||
<div
|
||||
className="lg:hidden fixed inset-0 bg-black/50 z-40"
|
||||
onClick={() => setIsMobileMenuOpen(false)}
|
||||
/>
|
||||
)}
|
||||
@@ -218,18 +196,18 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
{/* Sidebar */}
|
||||
<div className={`
|
||||
${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full lg:translate-x-0'}
|
||||
fixed lg:static inset-y-0 left-0 z-40 w-64 bg-white dark:bg-black border-r border-neutral-200 dark:border-neutral-800/70 flex flex-col h-screen transition-transform duration-300 ease-in-out
|
||||
fixed lg:static inset-y-0 left-0 z-40 w-[220px] bg-white dark:bg-black border-r border-neutral-200 dark:border-neutral-800/70 flex flex-col h-screen transition-transform duration-[120ms] ease-out
|
||||
`}>
|
||||
{/* Logo Section */}
|
||||
<Link href="/admin" className="px-4 h-14 flex items-center justify-start gap-2 border-b border-neutral-200 dark:border-neutral-800/70">
|
||||
{/* Logo */}
|
||||
<Link href="/admin" className="px-4 h-12 flex items-center justify-start gap-2 border-b border-neutral-200 dark:border-neutral-800/70">
|
||||
<h1 className="text-neutral-900 dark:text-white font-semibold">{appName}</h1>
|
||||
<span className="bg-red-700/10 border border-red-600/20 text-red-600 uppercase text-[10px] leading-none px-2 py-1 rounded-full font-semibold">
|
||||
<span className="bg-red-700/10 border border-red-600/20 text-red-600 uppercase text-[10px] leading-none px-2 py-1 rounded-lg font-semibold">
|
||||
Admin
|
||||
</span>
|
||||
</Link>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 px-0 overflow-y-auto flex flex-col gap-0 pb-12 -mt-[1px]">
|
||||
<nav className="flex-1 px-2 py-2 overflow-y-auto flex flex-col gap-0.5 pb-12">
|
||||
{navigationSections.map(renderNavSection)}
|
||||
</nav>
|
||||
</div>
|
||||
@@ -237,4 +215,4 @@ const AdminSidebar = ({ isMobileMenuOpen, setIsMobileMenuOpen, appName, enabledM
|
||||
);
|
||||
};
|
||||
|
||||
export default AdminSidebar;
|
||||
export default AdminSidebar;
|
||||
|
||||
Reference in New Issue
Block a user