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
+232
View File
@@ -0,0 +1,232 @@
# ZEN — Design Language
Document de référence visuelle. Décrit l'apparence, le ressenti et les règles visuelles du produit. Valable pour toute reproduction, quelle que soit la technologie.
---
## Philosophie
ZEN est un outil de travail. Pas un produit marketing. L'interface disparaît pour laisser place au contenu.
**Un seul principe directeur : ne jamais ajouter un élément sans raison.**
Chaque couleur, bordure, espace ou texte supplémentaire doit justifier sa présence. Le doute se résout toujours en faveur de la suppression.
---
## Palette
### Base
Le produit est **noir et blanc**. Pas de couleurs de marque, pas de teintes bleues ou violettes dans les fonds. Le fond est blanc pur. Le texte est presque noir.
| Rôle | Clair | Sombre |
|---|---|---|
| Fond de page | `#ffffff` | `#0c0c0b` |
| Fond hover | `#f5f5f4` | `#161614` |
| Fond actif / sélectionné | `#ebebea` | `#1f1f1d` |
| Bordure standard | `#e4e4e2` | `#272724` |
| Bordure forte | `#c8c8c4` | `#3a3a36` |
| Texte principal | `#0c0c0b` | `#f0f0ed` |
| Texte secondaire | `#6b6b68` | `#6b6b68` |
| Texte discret | `#a3a39e` | `#3d3d3a` |
### Accents sémantiques
Quatre couleurs uniquement. Utilisées **avec grande parcimonie** — pour les statuts, les alertes, les deltas. Jamais comme couleur décorative. Jamais sur un fond.
| Couleur | Usage |
|---|---|
| **Bleu** | Information, liens, action principale |
| **Vert** | Succès, actif, delta positif |
| **Ambre** | Avertissement, en attente, brouillon |
| **Rouge** | Erreur, danger, suppression, delta négatif |
Chaque couleur existe en trois déclinaisons : la couleur elle-même (pour le texte), un fond très pâle, et une bordure légère. Ces trois valeurs forment toujours un ensemble — on ne mélange pas les déclinaisons de couleurs différentes.
### Ce qu'on n'utilise pas
- Dégradés
- Couleurs de fond dans la sidebar ou les sections (sauf hover/actif)
- Couleurs de marque personnalisées
- Transparence et flou (sauf l'overlay des modales)
---
## Typographie
**Police unique : IBM Plex Sans.** Pour les codes, IDs, montants et dates : IBM Plex Mono.
### Hiérarchie
| Rôle | Taille | Graisse | Notes |
|---|---|---|---|
| Titre de page | 20px | 600 | Lettre-espacement -0.01em |
| Titre de section | 1620px | 600 | |
| Corps de texte | 14px | 400 | |
| Texte secondaire | 13px | 400 | Dans les tableaux, les listes |
| Labels / captions | 12px | 400500 | |
| Labels colonnes | 11px | 500 | Uppercase, lettre-espacement +0.04em |
| Codes / IDs / montants | 1113px | 400 | IBM Plex Mono |
**Taille minimale : 11px.** En dessous, on ne descend pas.
### Règles d'écriture
- **Sentence case partout.** "Tableau de bord", pas "TABLEAU DE BORD".
- Seule exception : les en-têtes de colonnes de tableau, en majuscules petits.
- Texte court et direct. Pas de phrase là où un mot suffit.
- Les montants financiers, les dates ISO et les identifiants sont toujours en monospace.
---
## Espace
Base : **4px**. Tout espacement est un multiple de 4.
| Valeur | Usage typique |
|---|---|
| 4px | Espacement interne minimal (gap entre icône et texte) |
| 8px | Padding compact, gap entre éléments proches |
| 12px | Padding interne des petits composants |
| 16px | Padding standard des cartes et panneaux |
| 24px | Espacement entre sections |
| 32px | Padding latéral du contenu principal |
| 4864px | Espacement entre blocs majeurs |
La densité est **moyenne**. Pas étouffant, pas aéré. Un admin est un outil qu'on utilise plusieurs heures par jour — la densité confortable prime sur l'esthétique aérée.
---
## Formes et bordures
### Rayon de bordure
| Contexte | Rayon |
|---|---|
| Badges, étiquettes, petits éléments | 8px |
| Boutons, champs de saisie, cartes | 12px |
| Modales, panneaux flottants importants | 16px |
| Avatars, indicateurs ronds | Cercle complet |
**12px est le rayon standard.** On ne mélange pas les rayons dans un même composant.
### Bordures
Toutes les séparations sont des **bordures 1px solid**. Pas d'ombres sur les cartes et surfaces statiques. Pas de lignes de 2px ou plus.
Les ombres existent uniquement pour les éléments qui **flottent au-dessus** de l'interface : menus déroulants, modales, toasts. Elles sont discrètes — juste assez pour indiquer la profondeur.
---
## Structure de la page
```
┌─────────────────────────────────────────────┐
│ Sidebar 220px │ Barre supérieure 48px │
│ ├─────────────────────────── │
│ Logo │ │
│ │ Zone de contenu │
│ Nav principale │ padding 28px / 32px │
│ │ │
│ ────────────── │ │
│ Paramètres │ │
└─────────────────────────────────────────────┘
```
- **Sidebar** : fixe, 220px. Fond blanc (`#ffffff`). Séparée par une bordure droite.
- **Barre supérieure** : fixe, 48px. Fond blanc. Fil d'Ariane à gauche, utilisateur à droite.
- **Contenu** : gris très pâle (`#fafafa` / `neutral-50`). Les cartes et tableaux ont un fond blanc, ce qui crée naturellement la séparation visuelle sans avoir besoin de bordures de section.
---
## Navigation sidebar
- Les items inactifs sont en texte secondaire (`#6b6b68`).
- Au survol : fond légèrement grisé, texte principal.
- L'item actif : fond légèrement grisé plus prononcé, texte principal gras.
- Les icônes sont toujours à gauche du texte. Taille 15×15px.
- Les sous-menus se révèlent par accordéon (chevron qui tourne).
- Rayon des items : 8px.
- Padding interne : 7px vertical, 10px horizontal.
---
## Boutons
Quatre variantes seulement :
| Variante | Apparence | Usage |
|---|---|---|
| **Primaire** | Fond noir, texte blanc | Action principale de la page |
| **Secondaire** | Fond transparent, bordure, texte sombre | Actions secondaires, annulation |
| **Fantôme** | Fond transparent, pas de bordure, texte sombre | Actions tertiaires |
| **Danger** | Fond rouge très pâle, texte rouge, bordure rouge pâle | Suppression uniquement |
**Taille standard : petite.** Padding 6px vertical / 12px horizontal, texte 12px.
Il ne doit jamais y avoir plus d'un bouton primaire par section ou en-tête de page.
---
## Cartes et panneaux
- Fond blanc.
- Bordure 1px.
- Rayon 12px.
- Padding interne 1620px.
- **Pas d'ombre.**
- En-tête interne séparé du contenu par une bordure horizontale.
- Pied de panneau (actions de formulaire) séparé par une bordure horizontale.
---
## Tableaux
- En-têtes de colonnes : texte 11px uppercase, couleur secondaire.
- Lignes : fond blanc. Hover : fond légèrement grisé.
- Bordure horizontale entre chaque ligne. Pas de bordure sur la dernière ligne.
- Données numériques, IDs, dates : IBM Plex Mono.
- Actions (modifier, supprimer) : à l'extrême droite, boutons icône discrets.
- Pas de fond alterné sur les lignes.
---
## Badges de statut
Petits éléments inline qui indiquent un état. Toujours composés de trois éléments : fond pâle de la couleur sémantique, bordure légère, texte de la couleur sémantique.
Les statuts "vivants" (actif, en attente, en retard) ont un point coloré à gauche du texte.
Les statuts terminaux (archivé, publié) n'ont pas de point.
Rayon : 8px. Taille du texte : 11px.
---
## États interactifs
| État | Apparence |
|---|---|
| Hover | Fond légèrement plus sombre (45% de gris) |
| Actif / focus | Bordure de 1px devient la couleur du texte principal |
| Désactivé | Opacité 50%, curseur interdit |
| Chargement | Pas défini — à traiter au cas par cas |
Toutes les transitions durent **120ms**, courbe `ease-out`. Aucune animation décorative.
---
## Ce qu'on n'utilise jamais
- Dégradés de couleur sur les fonds ou les boutons
- Cartes avec une bordure colorée uniquement sur le côté gauche
- Ombres internes
- Texte en gras uniquement pour l'emphase décorative
- Icônes sans fonction claire
- Statistiques ou chiffres inventés pour remplir l'espace
- Couleurs de fond dans les sections de contenu
---
## Thème sombre
Le thème sombre est une inversion calibrée : les fonds passent au quasi-noir, les textes s'éclaircissent, les accents gagnent légèrement en luminosité pour rester lisibles. La structure, les espaces et les proportions restent identiques. Détection automatique via la préférence système — pas de toggle manuel.
+2 -2
View File
@@ -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>
+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;
@@ -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">
+3 -3
View File
@@ -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 (
+2 -2
View File
@@ -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'
};
+3 -3
View File
@@ -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',
+1 -1
View File
@@ -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 -2
View File
@@ -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.
+123
View File
@@ -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;
}