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;
|
||||
|
||||
@@ -85,10 +85,10 @@ export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, re
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = 'w-full px-3 py-2.5 rounded-lg text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
||||
const inputClasses = 'w-full px-3 py-2.5 rounded-xl text-sm focus:outline-none transition-all duration-[120ms] ease-out disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20';
|
||||
|
||||
return (
|
||||
<div className="bg-white/80 dark:bg-neutral-900/40 backdrop-blur-sm border border-neutral-200/80 dark:border-neutral-800/50 rounded-2xl px-4 py-6 md:px-6 md:py-8 hover:bg-neutral-50/90 hover:border-neutral-300/80 dark:hover:bg-neutral-900/50 dark:hover:border-neutral-700/50 transition-all duration-300 w-full max-w-md">
|
||||
<div className="bg-white dark:bg-neutral-900/40 border border-neutral-200 dark:border-neutral-800/50 rounded-xl px-4 py-6 md:px-6 md:py-8 w-full max-w-md">
|
||||
{/* Header */}
|
||||
<div className="text-center mb-6">
|
||||
<h1 className="text-2xl font-bold text-neutral-900 dark:text-white mb-2">
|
||||
@@ -190,7 +190,7 @@ export default function LoginPage({ onSubmit, onNavigate, onSetSessionCookie, re
|
||||
type="button"
|
||||
onClick={handleSubmit}
|
||||
disabled={isLoading || success || currentUser}
|
||||
className="cursor-pointer w-full bg-neutral-900 text-white mt-2 py-2.5 px-4 rounded-lg text-sm font-medium hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-200 focus:outline-none focus:ring-1 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20"
|
||||
className="cursor-pointer w-full bg-neutral-900 text-white mt-2 py-2.5 px-4 rounded-xl text-sm font-medium hover:bg-neutral-800 disabled:opacity-50 disabled:cursor-not-allowed transition-all duration-[120ms] ease-out focus:outline-none focus:ring-1 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20"
|
||||
>
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center space-x-2">
|
||||
|
||||
@@ -24,9 +24,9 @@ const Badge = ({
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-2 py-0.5 rounded-full text-xs',
|
||||
md: 'px-2.5 py-0.5 rounded-full text-xs',
|
||||
lg: 'px-3 py-1 rounded-full text-sm'
|
||||
sm: 'px-2 py-0.5 rounded-lg text-[11px]',
|
||||
md: 'px-2.5 py-0.5 rounded-lg text-[11px]',
|
||||
lg: 'px-3 py-1 rounded-lg text-xs'
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,7 +15,7 @@ const Button = ({
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const baseClassName = 'cursor-pointer inline-flex items-center justify-center font-medium rounded-xl transition-all duration-200 focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
const baseClassName = 'cursor-pointer inline-flex items-center justify-center font-medium rounded-xl transition-all duration-[120ms] ease-out focus:outline-none focus:ring-1 disabled:opacity-50 disabled:cursor-not-allowed';
|
||||
|
||||
const variants = {
|
||||
primary: 'bg-neutral-900 text-white hover:bg-neutral-800 focus:ring-neutral-900/20 dark:bg-white dark:text-black dark:hover:bg-neutral-100 dark:focus:ring-white/20',
|
||||
@@ -27,7 +27,7 @@ const Button = ({
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
sm: 'px-3 py-2 text-xs gap-1.5',
|
||||
sm: 'px-3 py-1.5 text-xs gap-1.5',
|
||||
md: 'px-4 py-2.5 text-sm gap-2',
|
||||
lg: 'px-6 py-3 text-base gap-2.5'
|
||||
};
|
||||
|
||||
@@ -10,12 +10,12 @@ const Card = ({
|
||||
footer,
|
||||
variant = 'default',
|
||||
padding = 'md',
|
||||
hover = true,
|
||||
hover = false,
|
||||
spacing = 'md',
|
||||
className = '',
|
||||
...props
|
||||
}) => {
|
||||
const baseClassName = 'border transition-all duration-300';
|
||||
const baseClassName = 'border transition-all duration-[120ms] ease-out';
|
||||
const isLightDark = variant === 'lightDark';
|
||||
|
||||
const variants = {
|
||||
@@ -23,7 +23,7 @@ const Card = ({
|
||||
elevated: 'rounded-xl bg-neutral-50/80 dark:bg-neutral-900/40 border-neutral-200 dark:border-neutral-800/50',
|
||||
outline: 'rounded-xl bg-transparent border-neutral-300 dark:border-neutral-700/50',
|
||||
solid: 'rounded-xl bg-neutral-100 dark:bg-neutral-800 border-neutral-200 dark:border-neutral-700',
|
||||
lightDark: 'rounded-2xl bg-white/80 dark:bg-neutral-900/40 border-neutral-200/80 dark:border-neutral-800/50',
|
||||
lightDark: 'rounded-xl bg-white dark:bg-neutral-900/40 border-neutral-200 dark:border-neutral-800/50',
|
||||
success: 'rounded-xl bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-900/50',
|
||||
info: 'rounded-xl bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-900/50',
|
||||
warning: 'rounded-xl bg-yellow-50 dark:bg-yellow-900/30 border-yellow-200 dark:border-yellow-900/50',
|
||||
|
||||
@@ -18,7 +18,7 @@ const Input = ({
|
||||
step,
|
||||
...props
|
||||
}) => {
|
||||
const baseInputClassName = `w-full px-3 py-2.5 rounded-xl text-sm focus:outline-none transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900/60 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20 dark:hover:bg-neutral-900/80 ${
|
||||
const baseInputClassName = `w-full px-3 py-2.5 rounded-xl text-sm focus:outline-none transition-all duration-[120ms] ease-out disabled:opacity-50 disabled:cursor-not-allowed bg-white border border-neutral-300 text-neutral-900 placeholder-neutral-400 focus:border-neutral-500 focus:ring-1 focus:ring-neutral-500/20 dark:bg-neutral-900/60 dark:border-neutral-700/50 dark:text-white dark:placeholder-neutral-500 dark:focus:border-neutral-600 dark:focus:ring-neutral-600/20 dark:hover:bg-neutral-900/80 ${
|
||||
error ? 'border-red-500/50 dark:border-red-500/50' : ''
|
||||
} ${className}`;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Skeleton } from './LoadingState';
|
||||
|
||||
const StatCard = ({
|
||||
@@ -31,7 +30,7 @@ const StatCard = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`group bg-white dark:bg-neutral-900/40 backdrop-blur-sm border border-neutral-200 dark:border-neutral-800/50 rounded-2xl p-4 sm:p-6 hover:bg-neutral-50 dark:hover:bg-neutral-900/50 hover:border-neutral-300 dark:hover:border-neutral-700/50 transition-all duration-300 ${className}`}
|
||||
className={`bg-white dark:bg-neutral-900/40 border border-neutral-200 dark:border-neutral-800/50 rounded-xl p-4 sm:p-6 transition-all duration-[120ms] ease-out ${className}`}
|
||||
{...props}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -1,2 +1,125 @@
|
||||
/* Tailwind v4: tells the consumer's Tailwind to scan this package's components */
|
||||
@source "../../**/*.js";
|
||||
|
||||
/* ── IBM Plex Sans (variable font) ──────────────────────────────────────── */
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
src: url("../fonts/IBM_Plex_Sans/IBMPlexSans-VariableFont_wdth,wght.ttf") format("truetype");
|
||||
font-weight: 100 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: "IBM Plex Sans";
|
||||
src: url("../fonts/IBM_Plex_Sans/IBMPlexSans-Italic-VariableFont_wdth,wght.ttf") format("truetype");
|
||||
font-weight: 100 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ── IBM Plex Mono (static fonts) ───────────────────────────────────────── */
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-Thin.ttf") format("truetype");
|
||||
font-weight: 100;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-ThinItalic.ttf") format("truetype");
|
||||
font-weight: 100;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLight.ttf") format("truetype");
|
||||
font-weight: 200;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-ExtraLightItalic.ttf") format("truetype");
|
||||
font-weight: 200;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-Light.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-LightItalic.ttf") format("truetype");
|
||||
font-weight: 300;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-Regular.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-Italic.ttf") format("truetype");
|
||||
font-weight: 400;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-Medium.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-MediumItalic.ttf") format("truetype");
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-SemiBold.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-SemiBoldItalic.ttf") format("truetype");
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-Bold.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
font-display: swap;
|
||||
}
|
||||
@font-face {
|
||||
font-family: "IBM Plex Mono";
|
||||
src: url("../fonts/IBM_Plex_Mono/IBMPlexMono-BoldItalic.ttf") format("truetype");
|
||||
font-weight: 700;
|
||||
font-style: italic;
|
||||
font-display: swap;
|
||||
}
|
||||
|
||||
/* ── Enregistrement dans le thème Tailwind v4 ───────────────────────────── */
|
||||
@theme {
|
||||
--font-ibm-plex-sans: "IBM Plex Sans", sans-serif;
|
||||
--font-ibm-plex-mono: "IBM Plex Mono", monospace;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user