Merge pull request 'Feat/neumorphism/widgets' (#3) from feat/neumorphism/widgets into test
Reviewed-on: maximus/aegis#3
This commit was merged in pull request #3.
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"@tailwindcss/vite": "^4.3.3",
|
"@tailwindcss/vite": "^4.3.3",
|
||||||
"@zitadel/react-auth": "^1.2.1",
|
"@zitadel/react-auth": "^1.2.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"lucide-react": "^1.27.0",
|
||||||
"oidc-client-ts": "^3.5.0",
|
"oidc-client-ts": "^3.5.0",
|
||||||
"otpauth": "^9.5.1",
|
"otpauth": "^9.5.1",
|
||||||
"pocketbase": "^0.27.0",
|
"pocketbase": "^0.27.0",
|
||||||
|
|||||||
Generated
+12
@@ -17,6 +17,9 @@ importers:
|
|||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
|
lucide-react:
|
||||||
|
specifier: ^1.27.0
|
||||||
|
version: 1.27.0(react@19.2.8)
|
||||||
oidc-client-ts:
|
oidc-client-ts:
|
||||||
specifier: ^3.5.0
|
specifier: ^3.5.0
|
||||||
version: 3.5.0
|
version: 3.5.0
|
||||||
@@ -958,6 +961,11 @@ packages:
|
|||||||
lru-cache@5.1.1:
|
lru-cache@5.1.1:
|
||||||
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
|
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:
|
magic-string@0.30.21:
|
||||||
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==}
|
||||||
|
|
||||||
@@ -1990,6 +1998,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist: 3.1.1
|
yallist: 3.1.1
|
||||||
|
|
||||||
|
lucide-react@1.27.0(react@19.2.8):
|
||||||
|
dependencies:
|
||||||
|
react: 19.2.8
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@jridgewell/sourcemap-codec': 1.5.5
|
'@jridgewell/sourcemap-codec': 1.5.5
|
||||||
|
|||||||
@@ -1,56 +1,97 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { NavLink } from 'react-router-dom';
|
import { NavLink } from 'react-router-dom';
|
||||||
|
import { Shield, ShieldCheck, HelpCircle, Lock, Settings, LogOut } from 'lucide-react';
|
||||||
import { cn } from '@/utils/utils';
|
import { cn } from '@/utils/utils';
|
||||||
|
import { Card } from '@/components/ui/Card';
|
||||||
|
import { NEUMORPHISM } from '@/styles/Neumorphism';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
onLogout: () => void;
|
onLogout: () => void;
|
||||||
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Liste de tes liens réels avec leurs icônes associées
|
||||||
const NAVIGATION_ITEMS = [
|
const NAVIGATION_ITEMS = [
|
||||||
{ name: 'Trust Center', path: '/dashboard' },
|
{ name: 'Trust Center', path: '/dashboard', icon: ShieldCheck },
|
||||||
{ name: 'Service Desk', path: '/support' },
|
{ name: 'Service Desk', path: '/support', icon: HelpCircle },
|
||||||
{ name: 'Coffre-fort', path: '/vault' },
|
{ name: 'Coffre-fort', path: '/vault', icon: Lock },
|
||||||
{ name: 'Paramètres', path: '/account' },
|
{ name: 'Paramètres', path: '/account', icon: Settings },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Sidebar: React.FC<SidebarProps> = ({ onLogout }) => {
|
|
||||||
|
|
||||||
|
export const Sidebar: React.FC<SidebarProps> = ({ onLogout, className }) => {
|
||||||
return (
|
return (
|
||||||
<aside className="w-64 bg-slate-900 flex flex-col shrink-0">
|
<Card className={cn("w-60 flex flex-col p-6 shrink-0", className)}>
|
||||||
{/* En-tête / Logo */}
|
{/* 1. EN-TÊTE / LOGO */}
|
||||||
<div className="h-16 flex items-center px-6 border-b border-slate-800">
|
<div className="flex items-center gap-4 mb-10 px-2">
|
||||||
<span className="text-xl font-bold text-white tracking-widest">Aegis<span className="text-blue-500">.</span></span>
|
{/* 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>
|
</div>
|
||||||
|
|
||||||
{/* Menu de navigation URL-based */}
|
{/* 2. NAVIGATION PRINCIPALE (Basée sur l'URL) */}
|
||||||
<nav className="flex-1 px-4 py-6 space-y-2">
|
<nav className="flex flex-col gap-4 flex-1">
|
||||||
{NAVIGATION_ITEMS.map((item) => (
|
{NAVIGATION_ITEMS.map((item) => {
|
||||||
<NavLink
|
const Icon = item.icon;
|
||||||
key={item.path}
|
|
||||||
to={item.path}
|
return (
|
||||||
className={({ isActive }) =>
|
<NavLink
|
||||||
cn(
|
key={item.path}
|
||||||
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors duration-200",
|
to={item.path}
|
||||||
isActive
|
className={({ isActive }) =>
|
||||||
? "bg-blue-900/50 text-white border border-blue-800/50"
|
cn(
|
||||||
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200 border border-transparent"
|
"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
|
||||||
>
|
? cn(NEUMORPHISM.insetLight, NEUMORPHISM.insetDark, "!text-blue-600 dark:!text-blue-400 font-black")
|
||||||
{item.name}
|
: cn(NEUMORPHISM.lightShadow, NEUMORPHISM.darkShadow, "text-slate-500 dark:text-slate-400 font-bold hover:text-slate-700 dark:hover:text-slate-200")
|
||||||
</NavLink>
|
)
|
||||||
))}
|
}
|
||||||
|
>
|
||||||
|
{({ 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>
|
</nav>
|
||||||
|
|
||||||
{/* Zone de déconnexion */}
|
{/* 3. ZONE DE DÉCONNEXION */}
|
||||||
<div className="p-4 border-t border-slate-800">
|
<div className="pt-6 mt-6 border-t border-white/40 dark:border-slate-800/50">
|
||||||
<button
|
<button
|
||||||
onClick={onLogout}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { Button, type ButtonProps } from '@/components/ui/Button';
|
import { Button, type ButtonProps } from '@/components/ui/Button';
|
||||||
|
import { cn } from '@/utils/utils';
|
||||||
|
|
||||||
export interface BackButtonProps extends Omit<ButtonProps, 'onClick'> {
|
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;
|
to?: string;
|
||||||
/** Texte à afficher à côté de la flèche */
|
/** Texte par défaut du bouton */
|
||||||
label?: string;
|
label?: string;
|
||||||
/** Callback optionnel si vous souhaitez intercepter le clic */
|
/** Fonction personnalisée au clic */
|
||||||
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ export const BackButton: React.FC<BackButtonProps> = ({
|
|||||||
to,
|
to,
|
||||||
label = "Retour",
|
label = "Retour",
|
||||||
onClick,
|
onClick,
|
||||||
variant = "ghost",
|
variant = "ghost", // Style par défaut : ultra discret sur la surface Neumorphic
|
||||||
size = "sm",
|
size = "sm",
|
||||||
children,
|
children,
|
||||||
className,
|
className,
|
||||||
@@ -23,13 +24,14 @@ export const BackButton: React.FC<BackButtonProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
// Logique du clic (priorité : onClick perso > destination 'to' > retour historique)
|
||||||
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
if (onClick) {
|
if (onClick) {
|
||||||
onClick(e);
|
onClick(e);
|
||||||
} else if (to) {
|
} else if (to) {
|
||||||
navigate(to);
|
navigate(to);
|
||||||
} else {
|
} 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}
|
onClick={handleClick}
|
||||||
leftIcon={
|
leftIcon={
|
||||||
<svg
|
<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"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
viewBox="0 0 24 24"
|
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" />
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
|
||||||
</svg>
|
</svg>
|
||||||
}
|
}
|
||||||
className={className}
|
className={cn("group", className)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{children || label}
|
{children || label}
|
||||||
|
|||||||
@@ -1,27 +1,20 @@
|
|||||||
import React, { forwardRef } from 'react';
|
import React, { forwardRef } from 'react';
|
||||||
import { cn } from '@/utils/utils';
|
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 = {
|
const variantStyles = {
|
||||||
primary: "bg-blue-900 text-white hover:bg-blue-800 focus:ring-blue-900 shadow-sm border border-transparent",
|
primary: "text-[#090909] dark:text-white font-bold",
|
||||||
secondary: "bg-slate-900 text-white hover:bg-slate-800 focus:ring-slate-900 shadow-sm border border-transparent",
|
secondary: "text-slate-500 dark:text-slate-400 font-bold",
|
||||||
danger: "bg-red-600 text-white hover:bg-red-700 focus:ring-red-600 shadow-sm border border-transparent",
|
danger: "text-rose-600 dark:text-rose-400 font-bold",
|
||||||
success: "bg-emerald-600 text-white hover:bg-emerald-700 focus:ring-emerald-600 shadow-sm border border-transparent",
|
success: "text-emerald-600 dark:text-emerald-400 font-bold",
|
||||||
outline: "bg-white text-slate-700 border border-slate-300 hover:bg-slate-50 focus:ring-slate-900",
|
outline: "text-slate-700 dark:text-slate-200 border-slate-300 dark:border-slate-700",
|
||||||
ghost: "bg-transparent text-slate-600 hover:bg-slate-100 hover:text-slate-900 focus:ring-slate-900 border border-transparent",
|
ghost: "text-slate-600 dark:text-slate-400 border-transparent shadow-none dark:shadow-none",
|
||||||
};
|
|
||||||
|
|
||||||
// 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
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
variant?: keyof typeof variantStyles;
|
variant?: keyof typeof variantStyles;
|
||||||
size?: keyof typeof sizeStyles;
|
size?: keyof typeof NEUMORPHISM.sizeStyles;
|
||||||
isLoading?: boolean;
|
isLoading?: boolean;
|
||||||
leftIcon?: React.ReactNode;
|
leftIcon?: React.ReactNode;
|
||||||
rightIcon?: React.ReactNode;
|
rightIcon?: React.ReactNode;
|
||||||
@@ -43,28 +36,34 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
},
|
},
|
||||||
ref
|
ref
|
||||||
) => {
|
) => {
|
||||||
const newLocal = "ml-2 flex-shrink-0";
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
ref={ref} // Indispensable pour l'accessibilité ou des librairies externes
|
ref={ref}
|
||||||
type={type}
|
type={type}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
className={cn(
|
className={cn(
|
||||||
// Styles de base structurels
|
// Style de base & Neumorphism
|
||||||
"inline-flex items-center justify-center font-medium rounded-lg transition-all duration-200",
|
"inline-flex items-center justify-center cursor-pointer outline-none transition-all duration-300 ease-in-out",
|
||||||
"focus:outline-none active:scale-[0.98]",
|
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||||
"disabled:opacity-60 disabled:pointer-events-none disabled:active:scale-100",
|
"border border-[#e8e8e8] dark:border-[#1e293b]",
|
||||||
// Application dynamique des dictionnaires
|
"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],
|
variantStyles[variant],
|
||||||
sizeStyles[size],
|
NEUMORPHISM.sizeStyles[size],
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{/* Spinner SVG optimisé pour le chargement */}
|
{/* Spinner SVG si en chargement */}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<svg
|
<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"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
fill="none"
|
fill="none"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -81,7 +80,7 @@ export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
|||||||
<span className="truncate">{children}</span>
|
<span className="truncate">{children}</span>
|
||||||
|
|
||||||
{/* Icône de droite */}
|
{/* Icône de droite */}
|
||||||
{rightIcon && <span className={newLocal}>{rightIcon}</span>}
|
{rightIcon && <span className="ml-2 shrink-0">{rightIcon}</span>}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,35 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { cn } from '@/utils/utils';
|
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
|
<div
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -16,7 +40,11 @@ export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<
|
|||||||
({ className, ...props }, ref) => (
|
({ className, ...props }, ref) => (
|
||||||
<div
|
<div
|
||||||
ref={ref}
|
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}
|
{...props}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
@@ -25,7 +53,11 @@ CardHeader.displayName = "CardHeader";
|
|||||||
|
|
||||||
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
||||||
({ className, ...props }, ref) => (
|
({ 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";
|
CardContent.displayName = "CardContent";
|
||||||
+44
-21
@@ -1,17 +1,13 @@
|
|||||||
import React, { forwardRef, useId } from 'react';
|
import React, { forwardRef, useId } from 'react';
|
||||||
import { cn } from '@/utils/utils';
|
import { cn } from '@/utils/utils';
|
||||||
|
import { NEUMORPHISM } from '@/styles/Neumorphism';
|
||||||
|
|
||||||
const sizeStyles = {
|
export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size'> {
|
||||||
sm: "px-3 py-1.5 text-xs",
|
|
||||||
md: "px-4 py-2.5 text-sm",
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
|
||||||
label?: string;
|
label?: string;
|
||||||
error?: string;
|
error?: string;
|
||||||
sizeVariant?: keyof typeof sizeStyles; // Gestion de la taille
|
size?: keyof typeof NEUMORPHISM.sizeStyles;
|
||||||
leftIcon?: React.ReactNode; // Icône optionnelle intégrée à gauche
|
leftIcon?: React.ReactNode;
|
||||||
rightIcon?: React.ReactNode; // Icône optionnelle intégrée à droite
|
rightIcon?: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
export const Input = forwardRef<HTMLInputElement, InputProps>(
|
||||||
@@ -20,39 +16,63 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
label,
|
label,
|
||||||
error,
|
error,
|
||||||
id,
|
id,
|
||||||
sizeVariant = 'md',
|
size = 'md',
|
||||||
leftIcon,
|
leftIcon,
|
||||||
rightIcon,
|
rightIcon,
|
||||||
|
required,
|
||||||
|
disabled,
|
||||||
...props
|
...props
|
||||||
}, ref) => {
|
}, ref) => {
|
||||||
const autoId = useId();
|
const autoId = useId();
|
||||||
const inputId = id || autoId;
|
const inputId = id || autoId;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full flex flex-col gap-1.5">
|
||||||
|
{/* Label */}
|
||||||
{label && (
|
{label && (
|
||||||
<label htmlFor={inputId} className="block text-sm font-medium text-slate-700 mb-1.5">
|
<label
|
||||||
{label} {props.required && <span className="text-blue-900 font-bold">*</span>}
|
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>
|
</label>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="relative flex items-center w-full">
|
<div className="relative flex items-center w-full">
|
||||||
{/* Icône à gauche */}
|
{/* Icône à gauche */}
|
||||||
{leftIcon && (
|
{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}
|
{leftIcon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Champ HTML avec les ombres creusées centralisées */}
|
||||||
<input
|
<input
|
||||||
id={inputId}
|
id={inputId}
|
||||||
ref={ref}
|
ref={ref}
|
||||||
|
disabled={disabled}
|
||||||
className={cn(
|
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",
|
// Base & Couleurs de fond
|
||||||
sizeStyles[sizeVariant],
|
"w-full transition-all duration-300 ease-in-out outline-none border border-transparent",
|
||||||
leftIcon && "pl-9", // Décale le texte à droite si icône gauche
|
"bg-[#e8e8e8] dark:bg-[#1e293b]",
|
||||||
rightIcon && "pr-9", // Décale le texte à gauche si icône droite
|
"text-slate-800 dark:text-slate-100 font-medium placeholder:text-slate-400 dark:placeholder:text-slate-500",
|
||||||
error && "border-red-500 focus:ring-red-500 focus:border-red-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
|
className
|
||||||
)}
|
)}
|
||||||
{...props}
|
{...props}
|
||||||
@@ -60,13 +80,16 @@ export const Input = forwardRef<HTMLInputElement, InputProps>(
|
|||||||
|
|
||||||
{/* Icône à droite */}
|
{/* Icône à droite */}
|
||||||
{rightIcon && (
|
{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}
|
{rightIcon}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ interface PageHeaderProps {
|
|||||||
|
|
||||||
export const PageHeader: React.FC<PageHeaderProps> = ({ title, description, children }) => {
|
export const PageHeader: React.FC<PageHeaderProps> = ({ title, description, children }) => {
|
||||||
return (
|
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">
|
<CardContent className="p-6 sm:px-8 flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">
|
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import type { ManagedContract } from '@/types/ManagedContract';
|
import type { ManagedContract } from '@/types/ManagedContract';
|
||||||
import { StatusBadge } from '@/components/ui/StatusBadge';
|
import { StatusBadge } from '@/components/ui/StatusBadge';
|
||||||
|
import { Card } from './Card';
|
||||||
|
|
||||||
interface ServiceObjectProps {
|
interface ServiceObjectProps {
|
||||||
contract: ManagedContract;
|
contract: ManagedContract;
|
||||||
@@ -8,16 +9,15 @@ interface ServiceObjectProps {
|
|||||||
|
|
||||||
export const ServiceObject: React.FC<ServiceObjectProps> = ({ contract }) => {
|
export const ServiceObject: React.FC<ServiceObjectProps> = ({ contract }) => {
|
||||||
return (
|
return (
|
||||||
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center">
|
<Card variant= 'digged' className="p-4 rounded-lg border border-slate-100 flex flex-col">
|
||||||
<div>
|
<div className='flex flex-row justify-between'>
|
||||||
<h3 className="text-sm font-medium text-slate-900">{contract.service_name}</h3>
|
<h3 className="text-sm font-medium text-slate-900">{contract.service_name}</h3>
|
||||||
{contract.description && (
|
{/* Fonctionne instantanément avec 'Actif', 'En déploiement', 'Suspendu' */}
|
||||||
<p className="text-xs text-slate-500 mt-1">{contract.description}</p>
|
<StatusBadge status={contract.status} />
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
{contract.description && (
|
||||||
{/* Fonctionne instantanément avec 'Actif', 'En déploiement', 'Suspendu' */}
|
<p className="text-xs text-slate-500 mt-1">{contract.description}</p>
|
||||||
<StatusBadge status={contract.status} />
|
)}
|
||||||
</div>
|
</Card>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -55,7 +55,7 @@ export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, className }) =
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
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,
|
config.bg,
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export const EvolutionFilterBar: React.FC<EvolutionFilterBarProps> = ({
|
|||||||
<div className="relative min-w-60">
|
<div className="relative min-w-60">
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
sizeVariant="sm" // Format compact pour la barre de recherche
|
// Format compact pour la barre de recherche
|
||||||
placeholder="Rechercher une initiative..."
|
placeholder="Rechercher une initiative..."
|
||||||
value={searchQuery}
|
value={searchQuery}
|
||||||
onChange={(e) => onSearchChange(e.target.value)}
|
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>
|
<p className="text-sm text-slate-500">Catalogue des contrats d'infogérance actifs</p>
|
||||||
</div>
|
</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" />}
|
{error && <EmptyState message={error} className="text-red-500" />}
|
||||||
{isLoading && !error && <Loader />}
|
{isLoading && !error && <Loader />}
|
||||||
{!isLoading && !error && contracts.length === 0 && (
|
{!isLoading && !error && contracts.length === 0 && (
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ export const SupportCtaWidget: React.FC = () => {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="bg-blue-900 border-blue-800 text-white shadow-sm">
|
<Card>
|
||||||
<CardContent className="p-6">
|
<CardContent className="p-6">
|
||||||
<h3 className="text-lg font-semibold mb-2">Centre de Support</h3>
|
<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.
|
Déclarez un incident critique ou demandez une évolution de votre infrastructure.
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => navigate('/support')}
|
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é
|
Ouvrir un ticket sécurisé
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -8,16 +8,16 @@ interface DashboardLayoutProps {
|
|||||||
|
|
||||||
export default function DashboardLayout({ children, onLogout }: DashboardLayoutProps) {
|
export default function DashboardLayout({ children, onLogout }: DashboardLayoutProps) {
|
||||||
return (
|
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 */}
|
{/* Intégration de la navbar extraite */}
|
||||||
<Sidebar onLogout={onLogout} />
|
<Sidebar onLogout={onLogout} className='my-4 ml-4'/>
|
||||||
|
|
||||||
{/* CONTENU PRINCIPAL */}
|
{/* CONTENU PRINCIPAL */}
|
||||||
<main className="flex-1 overflow-y-auto scrollbar-hide">
|
<main className="flex-1 overflow-y-auto scrollbar-hide">
|
||||||
<div className="p-8">
|
<div>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -10,11 +10,11 @@ export default function TrustCenter() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="bg-slate-50 font-sans text-slate-900 relative min-h-[calc(100vh-4rem)]">
|
<div className="font-sans relative min-h-[calc(100vh-4rem)]">
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-8 py-8 space-y-6">
|
<main className="max-w-7xl mx-auto px-8 sm:px-8 space-y-6 m-4">
|
||||||
|
|
||||||
{/* HEADER EXTRAIT */}
|
{/* HEADER EXTRAIT */}
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="Tableau de bord de l'infrastructure"
|
title="Tableau de bord de l'infrastructure"
|
||||||
description="Espace sécurisé AEGIS • Synchronisation en temps réel"
|
description="Espace sécurisé AEGIS • Synchronisation en temps réel"
|
||||||
>
|
>
|
||||||
@@ -31,8 +31,8 @@ export default function TrustCenter() {
|
|||||||
</PageHeader>
|
</PageHeader>
|
||||||
|
|
||||||
{/* GRILLE DES WIDGETS */}
|
{/* GRILLE DES WIDGETS */}
|
||||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
|
||||||
<div className="lg:col-span-2 space-y-6">
|
<div className="lg:col-span-3 space-y-6">
|
||||||
<UptimeWidget />
|
<UptimeWidget />
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||||
<BackupWidget onClick={() => navigate('/backups')} />
|
<BackupWidget onClick={() => navigate('/backups')} />
|
||||||
@@ -40,7 +40,7 @@ export default function TrustCenter() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-6">
|
<div className="space-y-6 lg:col-span-2">
|
||||||
<ServicesWidget />
|
<ServicesWidget />
|
||||||
<SupportCtaWidget />
|
<SupportCtaWidget />
|
||||||
</div>
|
</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/*"],
|
"@assets/*": ["./src/assets/*"],
|
||||||
"@types/*": ["./src/types/*"],
|
"@types/*": ["./src/types/*"],
|
||||||
"@widgets/*": ["./src/components/widgets/*"],
|
"@widgets/*": ["./src/components/widgets/*"],
|
||||||
|
"@styles/*": ["./src/styles/*"],
|
||||||
},
|
},
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export default defineConfig({
|
|||||||
'@assets': path.resolve(__dirname, './src/assets'),
|
'@assets': path.resolve(__dirname, './src/assets'),
|
||||||
'@data': path.resolve(__dirname, './src/data'),
|
'@data': path.resolve(__dirname, './src/data'),
|
||||||
'@widgets': path.resolve(__dirname, './src/components/widgets'),
|
'@widgets': path.resolve(__dirname, './src/components/widgets'),
|
||||||
|
'@styles': path.resolve(__dirname, './src/styles'),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user