style(admin): reduce header height and simplify layout spacing and menu item focus styles

This commit is contained in:
2026-04-22 11:09:12 -04:00
parent ba3b6239b1
commit 345371d43c
27 changed files with 411 additions and 79 deletions
+40 -62
View File
@@ -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;