// ECO-PANNEAU.FR - _react/ui/_ui_components.jsx
// 1. - DESIGN SYSTEM : COMPOSANTS UI ATOMIQUES
// 1.1 - Bouton unifié (Gestion des variantes, icônes et formes)
window.pano_Button = ({ children, onClick, disabled, variant = 'primary', icon: Icon, className = '', type = 'button', title = '', shape, ...props }) => {
const hasPx = /(^|\s)(px-|p-)/.test(className);
const hasPy = /(^|\s)(py-|p-)/.test(className);
const hasH = /(^|\s)(h-|min-h-|max-h-)/.test(className);
const hasW = /(^|\s)(w-|min-w-|max-w-)/.test(className);
let baseClass = "text-xs font-bold transition flex items-center justify-center gap-2 shadow-sm border border-transparent select-none";
if (shape === 'square') {
baseClass += " rounded-xl shrink-0 aspect-square";
if (!hasPx && !hasPy && !hasW && !hasH) baseClass += " p-2.5";
} else if (shape === 'round') {
baseClass += " rounded-full shrink-0 aspect-square";
if (!hasPx && !hasPy && !hasW && !hasH) baseClass += " p-2.5";
} else {
baseClass += " rounded-xl";
if (!hasPx) baseClass += " px-4";
if (!hasPy && !hasH) baseClass += " py-2.5";
}
const variants = {
primary: "bg-emerald-600 text-white hover:bg-emerald-700 shadow-md",
success: "bg-emerald-50 text-emerald-600 hover:bg-emerald-100",
successSolid: "bg-emerald-600 text-white hover:bg-emerald-700 shadow-md",
danger: "bg-red-50 text-red-600 hover:bg-red-100",
dangerSolid: "bg-red-600 text-white hover:bg-red-700 shadow-md",
warning: "bg-orange-50 text-orange-600 hover:bg-orange-100",
warningSolid: "bg-orange-500 text-white hover:bg-orange-600 shadow-md",
info: "bg-blue-50 text-blue-700 hover:bg-blue-100",
infoSolid: "bg-blue-600 text-white hover:bg-blue-700 shadow-md",
secondary: "bg-slate-100 text-slate-700 hover:bg-slate-200 border-slate-200",
outline: "bg-white border-slate-200 text-slate-600 hover:bg-slate-50",
purple: "bg-purple-50 text-purple-700 hover:bg-purple-100",
dark: "bg-slate-900 text-white hover:bg-slate-800 shadow-md",
ghost: "bg-transparent text-slate-500 hover:text-slate-800 hover:bg-slate-100 shadow-none border-transparent"
};
return (
);
};
// 1.2 - Badge d'icône (Décoratif)
window.pano_IconBadge = ({ icon: Icon, variant = 'info', size = 'md', className = '' }) => {
const variants = {
primary: "bg-emerald-100 text-emerald-600",
success: "bg-emerald-100 text-emerald-600",
danger: "bg-red-100 text-red-600",
warning: "bg-orange-100 text-orange-600",
info: "bg-blue-100 text-blue-600",
secondary: "bg-slate-100 text-slate-500",
purple: "bg-purple-100 text-purple-600",
dark: "bg-slate-800 text-slate-300"
};
const sizes = {
sm: "w-8 h-8 rounded-lg",
md: "w-10 h-10 rounded-xl",
lg: "w-12 h-12 rounded-2xl",
xl: "w-14 h-14 rounded-2xl"
};
const iconSizes = { sm: 14, md: 18, lg: 24, xl: 28 };
return (
{Icon && }
);
};
// 1.3 - Badge de notification (Compteur)
window.pano_NotificationBadge = ({ count, className = '' }) => {
if (!count || count <= 0) return null;
const displayCount = count > 99 ? '99+' : count;
return (
{displayCount}
);
};
// 1.4 - Interrupteur (Toggle)
window.pano_Toggle = ({ checked, onChange, variant = 'success', disabled = false }) => {
const variants = {
success: 'bg-emerald-500',
info: 'bg-blue-500',
warning: 'bg-orange-500',
danger: 'bg-red-500',
purple: 'bg-purple-500'
};
const colorClass = variants[variant] || variants.success;
return (
{ if(onChange && !disabled) { e.stopPropagation(); onChange(!checked); } }}
>
);
};
// 1.5 - État vide (Empty State)
window.pano_EmptyState = ({ icon: Icon, text, subtext, className = "" }) => (
{Icon &&
}
{text}
{subtext &&
{subtext}
}
);
// 1.6 - Boîte d'alerte (Informations critiques)
window.pano_AlertBox = ({ type = 'info', icon: Icon, title, children, className = "" }) => {
const styles = {
info: 'bg-blue-50 text-blue-800 border-blue-200',
warning: 'bg-orange-50 text-orange-800 border-orange-200',
error: 'bg-red-50 text-red-800 border-red-200',
success: 'bg-emerald-50 text-emerald-800 border-emerald-200'
};
const iconColors = {
info: 'text-blue-500',
warning: 'text-orange-500',
error: 'text-red-500',
success: 'text-emerald-500'
};
return (
{Icon &&
}
{title &&
{title}
}
{children}
);
};
// 1.7 - Carte de statistique
window.pano_StatCard = ({ title, value, icon, variant = 'info', onClick }) => {
const variants = {
success: { bg: 'bg-emerald-100', text: 'text-emerald-600' },
info: { bg: 'bg-blue-100', text: 'text-blue-600' },
warning: { bg: 'bg-amber-100', text: 'text-amber-600' },
danger: { bg: 'bg-red-100', text: 'text-red-600' },
secondary: { bg: 'bg-slate-200', text: 'text-slate-600' },
purple: { bg: 'bg-purple-100', text: 'text-purple-600' },
indigo: { bg: 'bg-indigo-100', text: 'text-indigo-600' }
};
const v = variants[variant] || variants.info;
return (
);
};
// 1.8 - Saisie de prix (HT)
// CORRECTION UX : Implémentation du pattern "Controlled Input" avec état local pour ne pas bloquer les virgules (décimales)
window.pano_PriceInput = ({ label, value, onChange, ...props }) => {
const { useState, useEffect } = React;
// On conserve la saisie exacte de l'utilisateur sous forme de chaîne (permet de taper "10.")
const [localValue, setLocalValue] = useState(value === 0 ? '' : String(value));
// Si la valeur externe change "de force" (ex: reset du formulaire), on met à jour
useEffect(() => {
const parsedLocal = parseFloat(localValue);
if (parsedLocal !== value && !(localValue === '' && value === 0)) {
setLocalValue(value === 0 ? '' : String(value));
}
}, [value]);
const handleChange = (e) => {
const val = e.target.value;
setLocalValue(val);
const parsed = parseFloat(val);
onChange(isNaN(parsed) ? 0 : parsed);
};
return (
);
};
// 1.9 - Badge de statut (Coloration intelligente)
window.pano_StatusBadge = ({ status, variant, className = '' }) => {
let colorClass = 'bg-slate-100 text-slate-600 border-slate-200';
let displayStatus = status;
if (status === 'En attente de validation') displayStatus = 'Validation';
else if (status === 'En attente de commande au fournisseur') displayStatus = 'Commande';
else if (status === "En attente d'impression") displayStatus = 'Impression';
else if (status === 'Réception confirmée') displayStatus = 'Livré';
if (variant) {
const variants = {
success: 'bg-emerald-100 text-emerald-700 border-emerald-200',
danger: 'bg-red-100 text-red-700 border-red-200',
warning: 'bg-amber-100 text-amber-700 border-amber-200',
info: 'bg-blue-100 text-blue-700 border-blue-200',
secondary: 'bg-slate-100 text-slate-600 border-slate-200'
};
colorClass = variants[variant] || colorClass;
} else {
const s = (status || '').toLowerCase();
if (s.includes('actif') || s.includes('livré') || s.includes('activé') || s.includes('accept') || s === 'réception confirmée') {
colorClass = 'bg-emerald-100 text-emerald-700 border-emerald-200';
} else if (s.includes('suspendu') || s.includes('erreur') || s.includes('hors ligne') || s.includes('refus') || s.includes('désactivé')) {
colorClass = 'bg-red-100 text-red-700 border-red-200';
} else if (s.includes('brouillon') || s.includes('attente') || s.includes('validation') || s.includes('programmé') || s.includes('commande') || s.includes('impression')) {
colorClass = 'bg-amber-100 text-amber-700 border-amber-200';
} else if (s.includes('expédié')) {
colorClass = 'bg-blue-100 text-blue-700 border-blue-200';
}
}
return (
{displayStatus}
);
};
// 1.10 - Champ de saisie (FormInput)
window.pano_FormInput = ({ label, value, onChange, type = 'text', required = false, disabled = false, placeholder = '', className = '', inputClassName = '', hint = '', error = false, errorText = '', icon, ...props }) => {
const [showPwd, setShowPwd] = React.useState(false);
const { EyeIcon, EyeOffIcon } = window.pano_getIcons();
const isPassword = type === 'password';
const actualType = isPassword ? (showPwd ? 'text' : 'password') : type;
return (
{label && (
)}
{icon && (
{icon}
)}
{isPassword && (
{ e.preventDefault(); e.stopPropagation(); setShowPwd(!showPwd); }}
title={showPwd ? "Masquer" : "Afficher"}
>
{showPwd ? (EyeOffIcon && ) : (EyeIcon && )}
)}
{hint && !errorText &&
{hint}
}
{errorText &&
{errorText}
}
);
};
// 1.11 - Zone de texte (FormTextarea)
// CORRECTION CSS : Séparation propre de className (conteneur) et inputClassName (textarea)
window.pano_FormTextarea = ({ label, value, onChange, rows = 3, required = false, disabled = false, placeholder = '', className = '', inputClassName = '', hint = '', error = false, errorText = '', ...props }) => (
{hint && !errorText &&
{hint}
}
{errorText &&
{errorText}
}
);
// 2. - CONTAINERS ET GRILLES DE CARTES
// 2.1 - Grille responsive (CardGrid)
window.pano_CardGrid = ({ children, minWidth = "300px", className = "" }) => (
{children}
);
// 2.2 - Carte de données stylisée (DataCard)
window.pano_DataCard = ({ children, variant = 'default', className = "", onClick, ...props }) => {
const variants = {
default: 'bg-white border-slate-200',
warning: 'bg-amber-50/50 border-amber-300',
danger: 'bg-red-50/30 border-red-400',
success: 'bg-emerald-50 border-emerald-200',
info: 'bg-blue-50 border-blue-200',
slate: 'bg-slate-50 border-slate-200',
};
const vClass = variants[variant] || variants.default;
return (
{children}
);
};
// 3. - COMPOSANTS DE RECHERCHE ET PAGINATION
// 3.1 - Barre de recherche (SearchBar)
window.pano_SearchBar = ({ searchQuery, setSearchQuery, placeholder = "Rechercher..." }) => {
const { SearchIcon, XIcon } = window.pano_getIcons();
return (
{SearchIcon && }
setSearchQuery(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Escape') {
setSearchQuery('');
}
}}
placeholder={placeholder}
className="w-full pl-10 pr-10 py-3 bg-white border-2 border-slate-200 rounded-xl text-sm font-bold text-slate-700 outline-none focus:border-blue-500 transition shadow-sm"
/>
{searchQuery && (
)}
);
};
// 3.2 - Résultat de recherche vide (EmptySearch)
window.pano_EmptySearch = ({ searchQuery }) => {
const { SearchIcon } = window.pano_getIcons();
return (
Aucun résultat pour "{searchQuery}".
Essayez d'autres mots-clés ou videz la recherche.
);
};
// 3.3 - Pied de pagination
window.pano_PaginationFooter = ({ visibleCount, setVisibleCount, totalCount, step = 50 }) => {
const { PlusIcon } = window.pano_getIcons();
const { Button } = window.pano_getComponents();
if (totalCount <= visibleCount) return null;
const remaining = totalCount - visibleCount;
const nextCount = Math.min(step, remaining);
const labelText = nextCount > 1
? `Afficher ${window.pano_formatPlural(nextCount, "résultat suivant", "résultats suivants")}`
: `Afficher le résultat suivant`;
return (
{visibleCount} affichés sur {totalCount}
);
};
// 4. - LAYOUT PRINCIPAL (DASHBOARD SHELL)
window.pano_DashboardLayout = ({ role, user, theme = 'emerald', navItems, activeTab, setActiveTab, onLogout, customLogout, children, sidebarFooter }) => {
const { LogOutIcon, XIcon, MenuIcon } = window.pano_getIcons();
const { LogoSVG, NotificationBadge } = window.pano_getComponents();
const [isMobileMenuOpen, setIsMobileMenuOpen] = React.useState(false);
const themeConfig = {
emerald: { bg: 'bg-emerald-900', hover: 'hover:bg-emerald-800', active: 'bg-emerald-800 text-white border-emerald-400', text: 'text-emerald-100', border: 'border-emerald-800' },
purple: { bg: 'bg-slate-900', hover: 'hover:bg-slate-800', active: 'bg-purple-600 text-white border-purple-400', text: 'text-slate-300', border: 'border-slate-800' },
blue: { bg: 'bg-blue-900', hover: 'hover:bg-blue-800', active: 'bg-blue-800 text-white border-blue-400', text: 'text-blue-100', border: 'border-blue-800' }
}[theme];
return (
{/* 4.1 - Sidebar (Desktop) */}
{/* 4.2 - Contenu principal et Header Mobile */}
{isMobileMenuOpen && (
{sidebarFooter}
{customLogout ? (
{customLogout.icon} {customLogout.label}
) : (
{LogOutIcon && } Déconnexion
)}
)}
{children}
);
};
/* EOF ========== [_react/ui/_ui_components.jsx] */