Refact/cleanup #4
@@ -13,6 +13,7 @@
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"@zitadel/react-auth": "^1.2.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^1.27.0",
|
||||
"oidc-client-ts": "^3.5.0",
|
||||
"otpauth": "^9.5.1",
|
||||
"pocketbase": "^0.27.0",
|
||||
|
||||
Generated
+12
@@ -17,6 +17,9 @@ importers:
|
||||
clsx:
|
||||
specifier: ^2.1.1
|
||||
version: 2.1.1
|
||||
lucide-react:
|
||||
specifier: ^1.27.0
|
||||
version: 1.27.0(react@19.2.8)
|
||||
oidc-client-ts:
|
||||
specifier: ^3.5.0
|
||||
version: 3.5.0
|
||||
@@ -958,6 +961,11 @@ packages:
|
||||
lru-cache@5.1.1:
|
||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
||||
|
||||
lucide-react@1.27.0:
|
||||
resolution: {integrity: sha512-rJicGl/3Fly/E0rOH1YmPZ6e49JCnKknh1ox1vpHnkfjujAkKA6sqUZvH3MTAaXXjgexyUwgNwTJzTtYuAFYJw==}
|
||||
peerDependencies:
|
||||
react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0
|
||||
|
||||
magic-string@0.30.21:
|
||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||
|
||||
@@ -1990,6 +1998,10 @@ snapshots:
|
||||
dependencies:
|
||||
yallist: 3.1.1
|
||||
|
||||
lucide-react@1.27.0(react@19.2.8):
|
||||
dependencies:
|
||||
react: 19.2.8
|
||||
|
||||
magic-string@0.30.21:
|
||||
dependencies:
|
||||
'@jridgewell/sourcemap-codec': 1.5.5
|
||||
|
||||
@@ -1,56 +1,97 @@
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
import { Shield, ShieldCheck, HelpCircle, Lock, Settings, LogOut } from 'lucide-react';
|
||||
import { cn } from '@/utils/utils';
|
||||
import { Card } from '@/components/ui/Card';
|
||||
import { NEUMORPHISM } from '@/styles/Neumorphism';
|
||||
|
||||
interface SidebarProps {
|
||||
onLogout: () => void;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
// Liste de tes liens réels avec leurs icônes associées
|
||||
const NAVIGATION_ITEMS = [
|
||||
{ name: 'Trust Center', path: '/dashboard' },
|
||||
{ name: 'Service Desk', path: '/support' },
|
||||
{ name: 'Coffre-fort', path: '/vault' },
|
||||
{ name: 'Paramètres', path: '/account' },
|
||||
{ name: 'Trust Center', path: '/dashboard', icon: ShieldCheck },
|
||||
{ name: 'Service Desk', path: '/support', icon: HelpCircle },
|
||||
{ name: 'Coffre-fort', path: '/vault', icon: Lock },
|
||||
{ name: 'Paramètres', path: '/account', icon: Settings },
|
||||
];
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ onLogout }) => {
|
||||
|
||||
|
||||
export const Sidebar: React.FC<SidebarProps> = ({ onLogout, className }) => {
|
||||
return (
|
||||
<aside className="w-64 bg-slate-900 flex flex-col shrink-0">
|
||||
{/* En-tête / Logo */}
|
||||
<div className="h-16 flex items-center px-6 border-b border-slate-800">
|
||||
<span className="text-xl font-bold text-white tracking-widest">Aegis<span className="text-blue-500">.</span></span>
|
||||
<Card className={cn("w-60 flex flex-col p-6 shrink-0", className)}>
|
||||
{/* 1. EN-TÊTE / LOGO */}
|
||||
<div className="flex items-center gap-4 mb-10 px-2">
|
||||
{/* Petit badge logo avec effet creusé */}
|
||||
<div className={cn(
|
||||
"w-12 h-12 rounded-2xl flex items-center justify-center bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||
"shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff] dark:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]"
|
||||
)}>
|
||||
<Shield className="w-6 h-6 text-blue-600 dark:text-blue-500" />
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-black tracking-widest text-slate-800 dark:text-white leading-none">AEGIS</h1>
|
||||
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">by GISE</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Menu de navigation URL-based */}
|
||||
<nav className="flex-1 px-4 py-6 space-y-2">
|
||||
{NAVIGATION_ITEMS.map((item) => (
|
||||
{/* 2. NAVIGATION PRINCIPALE (Basée sur l'URL) */}
|
||||
<nav className="flex flex-col gap-4 flex-1">
|
||||
{NAVIGATION_ITEMS.map((item) => {
|
||||
const Icon = item.icon;
|
||||
|
||||
return (
|
||||
<NavLink
|
||||
key={item.path}
|
||||
to={item.path}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors duration-200",
|
||||
"w-full flex items-center gap-4 px-5 py-4 rounded-2xl transition-all duration-300 ease-in-out cursor-pointer",
|
||||
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||
isActive
|
||||
? "bg-blue-900/50 text-white border border-blue-800/50"
|
||||
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200 border border-transparent"
|
||||
? cn(NEUMORPHISM.insetLight, NEUMORPHISM.insetDark, "!text-blue-600 dark:!text-blue-400 font-black")
|
||||
: cn(NEUMORPHISM.lightShadow, NEUMORPHISM.darkShadow, "text-slate-500 dark:text-slate-400 font-bold hover:text-slate-700 dark:hover:text-slate-200")
|
||||
)
|
||||
}
|
||||
>
|
||||
{item.name}
|
||||
{({ isActive }) => (
|
||||
<>
|
||||
<Icon
|
||||
className={cn(
|
||||
"w-5 h-5 transition-transform duration-300 ease-in-out shrink-0",
|
||||
isActive ? "scale-110" : "scale-100"
|
||||
)}
|
||||
strokeWidth={isActive ? 2.5 : 2}
|
||||
/>
|
||||
<span className="text-sm">{item.name}</span>
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
|
||||
{/* Zone de déconnexion */}
|
||||
<div className="p-4 border-t border-slate-800">
|
||||
{/* 3. ZONE DE DÉCONNEXION */}
|
||||
<div className="pt-6 mt-6 border-t border-white/40 dark:border-slate-800/50">
|
||||
<button
|
||||
onClick={onLogout}
|
||||
className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-slate-400 hover:text-red-400 hover:bg-slate-800 rounded-lg transition-all cursor-pointer"
|
||||
className={cn(
|
||||
"w-full flex items-center justify-start gap-4 px-5 py-4 rounded-2xl cursor-pointer transition-all duration-300 ease-in-out",
|
||||
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||
NEUMORPHISM.lightShadow,
|
||||
NEUMORPHISM.darkShadow,
|
||||
"active:shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff] dark:active:shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]",
|
||||
"text-rose-500 dark:text-rose-400 font-bold"
|
||||
)}
|
||||
>
|
||||
Déconnexion
|
||||
<LogOut className="w-5 h-5" />
|
||||
<span className="text-sm font-bold">Déconnexion</span>
|
||||
</button>
|
||||
</div>
|
||||
</aside>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { Button, type ButtonProps } from '@/components/ui/Button';
|
||||
import { cn } from '@/utils/utils';
|
||||
|
||||
export interface BackButtonProps extends Omit<ButtonProps, 'onClick'> {
|
||||
/** Route explicite vers laquelle naviguer (ex: "/support"). Si non spécifié, fait un retour arrière navigateur (-1) */
|
||||
/** Route explicite (ex: "/support"). Si omis, fait un retour arrière dans l'historique (-1) */
|
||||
to?: string;
|
||||
/** Texte à afficher à côté de la flèche */
|
||||
/** Texte par défaut du bouton */
|
||||
label?: string;
|
||||
/** Callback optionnel si vous souhaitez intercepter le clic */
|
||||
/** Fonction personnalisée au clic */
|
||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||
}
|
||||
|
||||
@@ -15,7 +16,7 @@ export const BackButton: React.FC<BackButtonProps> = ({
|
||||
to,
|
||||
label = "Retour",
|
||||
onClick,
|
||||
variant = "ghost",
|
||||
variant = "ghost", // Style par défaut : ultra discret sur la surface Neumorphic
|
||||
size = "sm",
|
||||
children,
|
||||
className,
|
||||
@@ -23,13 +24,14 @@ export const BackButton: React.FC<BackButtonProps> = ({
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
// Logique du clic (priorité : onClick perso > destination 'to' > retour historique)
|
||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||
if (onClick) {
|
||||
onClick(e);
|
||||
} else if (to) {
|
||||
navigate(to);
|
||||
} else {
|
||||
navigate(-1); // Comportement natif "Précédent" dans l'historique
|
||||
navigate(-1);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -40,7 +42,7 @@ export const BackButton: React.FC<BackButtonProps> = ({
|
||||
onClick={handleClick}
|
||||
leftIcon={
|
||||
<svg
|
||||
className="w-4 h-4 transition-transform group-hover:-translate-x-0.5"
|
||||
className="w-4 h-4 transition-transform duration-200 group-hover:-translate-x-1"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -48,7 +50,7 @@ export const BackButton: React.FC<BackButtonProps> = ({
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
||||
</svg>
|
||||
}
|
||||
className={className}
|
||||
className={cn("group", className)}
|
||||
{...props}
|
||||
>
|
||||
{children || label}
|
||||
|
||||
@@ -1,27 +1,20 @@
|
||||
import React, { forwardRef } from 'react';
|
||||
import { cn } from '@/utils/utils';
|
||||
import { NEUMORPHISM } from '@/styles/Neumorphism';
|
||||
|
||||
// 1. Dictionnaire des variantes (Sémantique de couleurs GISE)
|
||||
// Couleurs de texte pour chaque variante
|
||||
const variantStyles = {
|
||||
primary: "bg-blue-900 text-white hover:bg-blue-800 focus:ring-blue-900 shadow-sm border border-transparent",
|
||||
secondary: "bg-slate-900 text-white hover:bg-slate-800 focus:ring-slate-900 shadow-sm border border-transparent",
|
||||
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-600 shadow-sm border border-transparent",
|
||||
success: "bg-emerald-600 text-white hover:bg-emerald-700 focus:ring-emerald-600 shadow-sm border border-transparent",
|
||||
outline: "bg-white text-slate-700 border border-slate-300 hover:bg-slate-50 focus:ring-slate-900",
|
||||
ghost: "bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus:ring-slate-900 border border-transparent",
|
||||
};
|
||||
|
||||
// 2. Dictionnaire des tailles
|
||||
const sizeStyles = {
|
||||
sm: "px-3 py-1.5 text-xs",
|
||||
md: "px-4 py-2.5 text-sm",
|
||||
lg: "px-6 py-3 text-base w-full sm:w-auto", // w-full sur mobile par défaut, auto sur desktop
|
||||
icon: "p-2", // Format carré optimisé pour les boutons sans texte
|
||||
primary: "text-[#090909] dark:text-white font-bold",
|
||||
secondary: "text-slate-500 dark:text-slate-400 font-bold",
|
||||
danger: "text-rose-600 dark:text-rose-400 font-bold",
|
||||
success: "text-emerald-600 dark:text-emerald-400 font-bold",
|
||||
outline: "text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700",
|
||||
ghost: "text-slate-600 dark:text-slate-400 border-transparent shadow-none dark:shadow-none",
|
||||
};
|
||||
|
||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: keyof typeof variantStyles;
|
||||
size?: keyof typeof sizeStyles;
|
||||
size?: keyof typeof NEUMORPHISM.sizeStyles;
|
||||
isLoading?: boolean;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
@@ -43,28 +36,34 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
},
|
||||
ref
|
||||
) => {
|
||||
const newLocal = "ml-2 flex-shrink-0";
|
||||
return (
|
||||
<button
|
||||
ref={ref} // Indispensable pour l'accessibilité ou des librairies externes
|
||||
ref={ref}
|
||||
type={type}
|
||||
disabled={disabled || isLoading}
|
||||
className={cn(
|
||||
// Styles de base structurels
|
||||
"inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200",
|
||||
"focus:outline-none active:scale-[0.98]",
|
||||
"disabled:opacity-60 disabled:pointer-events-none disabled:active:scale-100",
|
||||
// Application dynamique des dictionnaires
|
||||
// Style de base & Neumorphism
|
||||
"inline-flex items-center justify-center cursor-pointer outline-none transition-all duration-300 ease-in-out",
|
||||
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||
"border border-[#e8e8e8] dark:border-[#1e293b]",
|
||||
"hover:border-white dark:hover:border-[#2a3850]",
|
||||
NEUMORPHISM.lightShadow,
|
||||
NEUMORPHISM.lightActive,
|
||||
NEUMORPHISM.darkShadow,
|
||||
NEUMORPHISM.darkActive,
|
||||
"disabled:opacity-50 disabled:pointer-events-none",
|
||||
|
||||
// Variantes & Tailles
|
||||
variantStyles[variant],
|
||||
sizeStyles[size],
|
||||
NEUMORPHISM.sizeStyles[size],
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{/* Spinner SVG optimisé pour le chargement */}
|
||||
{/* Spinner SVG si en chargement */}
|
||||
{isLoading && (
|
||||
<svg
|
||||
className="animate-spin -ml-1 mr-2 h-4 w-4 text-current"
|
||||
className="animate-spin -ml-1 mr-2 h-5 w-5 text-current"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -81,7 +80,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
<span className="truncate">{children}</span>
|
||||
|
||||
{/* Icône de droite */}
|
||||
{rightIcon && <span className={newLocal}>{rightIcon}</span>}
|
||||
{rightIcon && <span className="ml-2 shrink-0">{rightIcon}</span>}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,11 +1,35 @@
|
||||
import React from 'react';
|
||||
import { cn } from '@/utils/utils';
|
||||
import { NEUMORPHISM } from '@/styles/Neumorphism';
|
||||
|
||||
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
|
||||
// IMPORTANT — contraste texte :
|
||||
// Le fond clair (#e8e8e8) est assez lumineux. Dans le contenu de la Card,
|
||||
// évite text-slate-400/500 en light mode (ratio WCAG insuffisant sur ce fond) :
|
||||
// utilise plutôt text-slate-600 (ou plus foncé) pour le texte secondaire en light mode.
|
||||
// Ex: className="text-slate-600 dark:text-slate-400"
|
||||
|
||||
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
// 'raised' : relief neumorphique complet — dashboards, widgets isolés, peu nombreux à l'écran.
|
||||
// 'flat' : bordure fine, sans ombre — listes/tables denses (perf + lisibilité).
|
||||
variant?: 'raised' | 'flat'|'digged';
|
||||
}
|
||||
|
||||
export const Card = React.forwardRef<HTMLDivElement, CardProps>(
|
||||
({ className, variant = 'raised', ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden", className)}
|
||||
className={cn(
|
||||
// Style de base
|
||||
"rounded-3xl border-none transition-[background-color,box-shadow] duration-300 ease-in-out flex flex-col overflow-hidden",
|
||||
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||
// Relief neumorphique (par défaut)
|
||||
variant === 'raised' && [NEUMORPHISM.lightShadow, NEUMORPHISM.darkShadow],
|
||||
variant === 'digged' && [NEUMORPHISM.insetDark, NEUMORPHISM.insetLight],
|
||||
// Variante plate : pas d'ombre, juste une bordure discrète — pour tables/listes denses
|
||||
variant === 'flat' && "border border-black/5 dark:border-white/10",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -16,7 +40,11 @@ export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<
|
||||
({ className, ...props }, ref) => (
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("px-6 py-5 border-b border-slate-100 flex flex-col space-y-1.5", className)}
|
||||
className={cn(
|
||||
// Séparation discrète (bordure fine plutôt qu'ombre inset, pour rester léger)
|
||||
"px-6 py-5 flex flex-col space-y-1.5 border-b border-black/5 dark:border-white/5",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
@@ -25,7 +53,11 @@ CardHeader.displayName = "CardHeader";
|
||||
|
||||
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||
({ className, ...props }, ref) => (
|
||||
<div ref={ref} className={cn("p-6 flex-1", className)} {...props} />
|
||||
<div
|
||||
ref={ref}
|
||||
className={cn("p-6 flex-1", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
);
|
||||
CardContent.displayName = "CardContent";
|
||||
+44
-21
@@ -1,17 +1,13 @@
|
||||
import React, { forwardRef, useId } from 'react';
|
||||
import { cn } from '@/utils/utils';
|
||||
import { NEUMORPHISM } from '@/styles/Neumorphism';
|
||||
|
||||
const sizeStyles = {
|
||||
sm: "px-3 py-1.5 text-xs",
|
||||
md: "px-4 py-2.5 text-sm",
|
||||
};
|
||||
|
||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
sizeVariant?: keyof typeof sizeStyles; // Gestion de la taille
|
||||
leftIcon?: React.ReactNode; // Icône optionnelle intégrée à gauche
|
||||
rightIcon?: React.ReactNode; // Icône optionnelle intégrée à droite
|
||||
size?: keyof typeof NEUMORPHISM.sizeStyles;
|
||||
leftIcon?: React.ReactNode;
|
||||
rightIcon?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
@@ -20,39 +16,63 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
label,
|
||||
error,
|
||||
id,
|
||||
sizeVariant = 'md',
|
||||
size = 'md',
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
required,
|
||||
disabled,
|
||||
...props
|
||||
}, ref) => {
|
||||
const autoId = useId();
|
||||
const inputId = id || autoId;
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
<div className="w-full flex flex-col gap-1.5">
|
||||
{/* Label */}
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="block text-sm font-medium text-slate-700 mb-1.5">
|
||||
{label} {props.required && <span className="text-blue-900 font-bold">*</span>}
|
||||
<label
|
||||
htmlFor={inputId}
|
||||
className="block text-sm font-bold text-slate-700 dark:text-slate-300"
|
||||
>
|
||||
{label} {required && <span className="text-rose-500 font-bold">*</span>}
|
||||
</label>
|
||||
)}
|
||||
|
||||
<div className="relative flex items-center w-full">
|
||||
{/* Icône à gauche */}
|
||||
{leftIcon && (
|
||||
<span className="absolute left-3 text-slate-400 pointer-events-none flex items-center">
|
||||
<span className="absolute left-4 text-slate-400 dark:text-slate-500 pointer-events-none flex items-center justify-center">
|
||||
{leftIcon}
|
||||
</span>
|
||||
)}
|
||||
|
||||
{/* Champ HTML avec les ombres creusées centralisées */}
|
||||
<input
|
||||
id={inputId}
|
||||
ref={ref}
|
||||
disabled={disabled}
|
||||
className={cn(
|
||||
"flex w-full rounded-lg border border-slate-300 bg-white text-slate-900 placeholder:text-slate-400 focus:outline-none focus:ring-1 focus:ring-blue-900 focus:border-blue-900 disabled:cursor-not-allowed disabled:bg-slate-50 disabled:text-slate-500 shadow-sm transition-colors",
|
||||
sizeStyles[sizeVariant],
|
||||
leftIcon && "pl-9", // Décale le texte à droite si icône gauche
|
||||
rightIcon && "pr-9", // Décale le texte à gauche si icône droite
|
||||
error && "border-red-500 focus:ring-red-500 focus:border-red-500",
|
||||
// Base & Couleurs de fond
|
||||
"w-full transition-all duration-300 ease-in-out outline-none border border-transparent",
|
||||
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||
"text-slate-800 dark:text-slate-100 font-medium placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||
|
||||
// 🎯 Utilisation directe de tes constantes Neumorphic creusées
|
||||
NEUMORPHISM.insetLight,
|
||||
NEUMORPHISM.insetDark,
|
||||
|
||||
// Focus & État désactivé
|
||||
"focus:border-blue-500/40 dark:focus:border-blue-400/40",
|
||||
"disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
|
||||
// Tailles & Marges d'icônes
|
||||
NEUMORPHISM.sizeStyles[size],
|
||||
leftIcon && "pl-11",
|
||||
rightIcon && "pr-11",
|
||||
|
||||
// Style en cas d'erreur
|
||||
error && "border-rose-500 focus:border-rose-500 text-rose-600 dark:text-rose-400",
|
||||
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
@@ -60,13 +80,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||
|
||||
{/* Icône à droite */}
|
||||
{rightIcon && (
|
||||
<span className="absolute right-3 text-slate-400 pointer-events-none flex items-center">
|
||||
<span className="absolute right-4 text-slate-400 dark:text-slate-500 pointer-events-none flex items-center justify-center">
|
||||
{rightIcon}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{error && <p className="mt-1.5 text-xs font-medium text-red-500">{error}</p>}
|
||||
{/* Message d'erreur */}
|
||||
{error && (
|
||||
<p className="text-xs font-semibold text-rose-500 dark:text-rose-400">{error}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ interface PageHeaderProps {
|
||||
|
||||
export const PageHeader: React.FC<PageHeaderProps> = ({ title, description, children }) => {
|
||||
return (
|
||||
<Card className="border-slate-200 shadow-sm">
|
||||
<Card>
|
||||
<CardContent className="p-6 sm:px-8 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import type { ManagedContract } from '@/types/ManagedContract';
|
||||
import { StatusBadge } from '@/components/ui/StatusBadge';
|
||||
import { Card } from './Card';
|
||||
|
||||
interface ServiceObjectProps {
|
||||
contract: ManagedContract;
|
||||
@@ -8,16 +9,15 @@ interface ServiceObjectProps {
|
||||
|
||||
export const ServiceObject: React.FC<ServiceObjectProps> = ({ contract }) => {
|
||||
return (
|
||||
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center">
|
||||
<div>
|
||||
<Card variant= 'digged' className="p-4 rounded-lg border border-slate-100 flex flex-col">
|
||||
<div className='flex flex-row justify-between'>
|
||||
<h3 className="text-sm font-medium text-slate-900">{contract.service_name}</h3>
|
||||
{contract.description && (
|
||||
<p className="text-xs text-slate-500 mt-1">{contract.description}</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Fonctionne instantanément avec 'Actif', 'En déploiement', 'Suspendu' */}
|
||||
<StatusBadge status={contract.status} />
|
||||
</div>
|
||||
{contract.description && (
|
||||
<p className="text-xs text-slate-500 mt-1">{contract.description}</p>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -55,7 +55,7 @@ export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, className }) =
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"px-2.5 py-1 rounded-full text-xs font-semibold inline-flex items-center border transition-colors",
|
||||
"px-2.5 py-1 rounded-full text-xs font-semibold inline-flex items-center border transition-colors min-w-fit max-h-fit",
|
||||
config.bg,
|
||||
className
|
||||
)}
|
||||
|
||||
@@ -42,7 +42,7 @@ export const EvolutionFilterBar: React.FC<EvolutionFilterBarProps> = ({
|
||||
<div className="relative min-w-60">
|
||||
<Input
|
||||
type="text"
|
||||
sizeVariant="sm" // Format compact pour la barre de recherche
|
||||
// Format compact pour la barre de recherche
|
||||
placeholder="Rechercher une initiative..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function ServicesWidget() {
|
||||
<p className="text-sm text-slate-500">Catalogue des contrats d'infogérance actifs</p>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto mb-6 space-y-3">
|
||||
<div className="flex-1 mb-6 space-y-3">
|
||||
{error && <EmptyState message={error} className="text-red-500" />}
|
||||
{isLoading && !error && <Loader />}
|
||||
{!isLoading && !error && contracts.length === 0 && (
|
||||
|
||||
@@ -7,15 +7,15 @@ export const SupportCtaWidget: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Card className="bg-blue-900 border-blue-800 text-white shadow-sm">
|
||||
<Card>
|
||||
<CardContent className="p-6">
|
||||
<h3 className="text-lg font-semibold mb-2">Centre de Support</h3>
|
||||
<p className="text-blue-200 text-sm mb-4 leading-relaxed">
|
||||
<p className="text-blue-900 text-sm mb-4 leading-relaxed">
|
||||
Déclarez un incident critique ou demandez une évolution de votre infrastructure.
|
||||
</p>
|
||||
<Button
|
||||
onClick={() => navigate('/support')}
|
||||
className="w-full bg-white text-blue-900 hover:bg-slate-50 font-medium py-2.5 px-4 rounded-lg transition-colors shadow-sm"
|
||||
className="w-full text-blue-900"
|
||||
>
|
||||
Ouvrir un ticket sécurisé
|
||||
</Button>
|
||||
|
||||
@@ -8,13 +8,13 @@ interface DashboardLayoutProps {
|
||||
|
||||
export default function DashboardLayout({ children, onLogout }: DashboardLayoutProps) {
|
||||
return (
|
||||
<div className="flex h-screen bg-slate-50 overflow-hidden">
|
||||
<div className="h-screen w-full flex bg-[#e8e8e8] dark:bg-[#1e293b] text-slate-800 dark:text-slate-100 font-sans transition-all duration-300 ease-in-out">
|
||||
{/* Intégration de la navbar extraite */}
|
||||
<Sidebar onLogout={onLogout} />
|
||||
<Sidebar onLogout={onLogout} className='my-4 ml-4'/>
|
||||
|
||||
{/* CONTENU PRINCIPAL */}
|
||||
<main className="flex-1 overflow-y-auto scrollbar-hide">
|
||||
<div className="p-8">
|
||||
<div>
|
||||
{children}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
@@ -10,8 +10,8 @@ export default function TrustCenter() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 font-sans text-slate-900 relative min-h-[calc(100vh-4rem)]">
|
||||
<main className="max-w-7xl mx-auto px-4 sm:px-8 py-8 space-y-6">
|
||||
<div className="font-sans relative min-h-[calc(100vh-4rem)]">
|
||||
<main className="max-w-7xl mx-auto px-8 sm:px-8 space-y-6 m-4">
|
||||
|
||||
{/* HEADER EXTRAIT */}
|
||||
<PageHeader
|
||||
@@ -31,8 +31,8 @@ export default function TrustCenter() {
|
||||
</PageHeader>
|
||||
|
||||
{/* GRILLE DES WIDGETS */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
<div className="lg:col-span-2 space-y-6">
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||
<div className="lg:col-span-3 space-y-6">
|
||||
<UptimeWidget />
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<BackupWidget onClick={() => navigate('/backups')} />
|
||||
@@ -40,7 +40,7 @@ export default function TrustCenter() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-6 lg:col-span-2">
|
||||
<ServicesWidget />
|
||||
<SupportCtaWidget />
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// Styles neumorphiques réutilisables.
|
||||
|
||||
export const NEUMORPHISM = {
|
||||
|
||||
// Reliefs au repos
|
||||
lightShadow: "shadow-[6px_6px_12px_#c5c5c5,-6px_-6px_12px_#ffffff]",
|
||||
darkShadow: "dark:shadow-[6px_6px_12px_#121926,-6px_-6px_12px_#2a3850]",
|
||||
|
||||
// Ombres Neumorphic quand on clique (enfoncé)
|
||||
lightActive: "active:shadow-[4px_4px_12px_#c5c5c5,-4px_-4px_12px_#ffffff]",
|
||||
darkActive: "dark:active:shadow-[4px_4px_12px_#121926,-4px_-4px_12px_#2a3850]",
|
||||
|
||||
sizeStyles: {
|
||||
sm: "px-5 py-2 text-sm rounded-md",
|
||||
md: "px-[1.7em] py-[0.7em] text-[18px] rounded-[0.5em]",
|
||||
lg: "px-10 py-4 text-xl rounded-xl",
|
||||
icon: "p-3 rounded-lg",
|
||||
},
|
||||
// Ombres du bouton ACTIF (effet enfoncé/creusé "inset")
|
||||
insetLight: "!shadow-[inset_4px_4px_8px_#c5c5c5,inset_-4px_-4px_8px_#ffffff]",
|
||||
insetDark: "dark:!shadow-[inset_4px_4px_8px_#121926,inset_-4px_-4px_8px_#2a3850]",
|
||||
|
||||
} as const;
|
||||
@@ -22,6 +22,7 @@
|
||||
"@assets/*": ["./src/assets/*"],
|
||||
"@types/*": ["./src/types/*"],
|
||||
"@widgets/*": ["./src/components/widgets/*"],
|
||||
"@styles/*": ["./src/styles/*"],
|
||||
},
|
||||
|
||||
/* Bundler mode */
|
||||
|
||||
@@ -24,6 +24,7 @@ export default defineConfig({
|
||||
'@assets': path.resolve(__dirname, './src/assets'),
|
||||
'@data': path.resolve(__dirname, './src/data'),
|
||||
'@widgets': path.resolve(__dirname, './src/components/widgets'),
|
||||
'@styles': path.resolve(__dirname, './src/styles'),
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user