2025-12-03 21:56:50 +00:00
|
|
|
import { useMemo } from 'react';
|
|
|
|
|
import { cn } from '@/lib/utils';
|
|
|
|
|
|
|
|
|
|
export interface GridColumns {
|
|
|
|
|
sm?: number;
|
|
|
|
|
md?: number;
|
|
|
|
|
lg?: number;
|
|
|
|
|
xl?: number;
|
|
|
|
|
'2xl'?: number;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface GridProps {
|
|
|
|
|
children: React.ReactNode;
|
|
|
|
|
columns?: number | GridColumns;
|
|
|
|
|
gap?: number;
|
|
|
|
|
rowGap?: number;
|
|
|
|
|
columnGap?: number;
|
|
|
|
|
className?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Mapping des classes Tailwind pour les colonnes de grille
|
|
|
|
|
*/
|
|
|
|
|
const GRID_COLS_CLASSES: Record<number, string> = {
|
|
|
|
|
1: 'grid-cols-1',
|
|
|
|
|
2: 'grid-cols-2',
|
|
|
|
|
3: 'grid-cols-3',
|
|
|
|
|
4: 'grid-cols-4',
|
|
|
|
|
5: 'grid-cols-5',
|
|
|
|
|
6: 'grid-cols-6',
|
|
|
|
|
7: 'grid-cols-7',
|
|
|
|
|
8: 'grid-cols-8',
|
|
|
|
|
9: 'grid-cols-9',
|
|
|
|
|
10: 'grid-cols-10',
|
|
|
|
|
11: 'grid-cols-11',
|
|
|
|
|
12: 'grid-cols-12',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GRID_COLS_SM_CLASSES: Record<number, string> = {
|
|
|
|
|
1: 'sm:grid-cols-1',
|
|
|
|
|
2: 'sm:grid-cols-2',
|
|
|
|
|
3: 'sm:grid-cols-3',
|
|
|
|
|
4: 'sm:grid-cols-4',
|
|
|
|
|
5: 'sm:grid-cols-5',
|
|
|
|
|
6: 'sm:grid-cols-6',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GRID_COLS_MD_CLASSES: Record<number, string> = {
|
|
|
|
|
1: 'md:grid-cols-1',
|
|
|
|
|
2: 'md:grid-cols-2',
|
|
|
|
|
3: 'md:grid-cols-3',
|
|
|
|
|
4: 'md:grid-cols-4',
|
|
|
|
|
5: 'md:grid-cols-5',
|
|
|
|
|
6: 'md:grid-cols-6',
|
|
|
|
|
7: 'md:grid-cols-7',
|
|
|
|
|
8: 'md:grid-cols-8',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GRID_COLS_LG_CLASSES: Record<number, string> = {
|
|
|
|
|
1: 'lg:grid-cols-1',
|
|
|
|
|
2: 'lg:grid-cols-2',
|
|
|
|
|
3: 'lg:grid-cols-3',
|
|
|
|
|
4: 'lg:grid-cols-4',
|
|
|
|
|
5: 'lg:grid-cols-5',
|
|
|
|
|
6: 'lg:grid-cols-6',
|
|
|
|
|
7: 'lg:grid-cols-7',
|
|
|
|
|
8: 'lg:grid-cols-8',
|
|
|
|
|
9: 'lg:grid-cols-9',
|
|
|
|
|
10: 'lg:grid-cols-10',
|
|
|
|
|
11: 'lg:grid-cols-11',
|
|
|
|
|
12: 'lg:grid-cols-12',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GRID_COLS_XL_CLASSES: Record<number, string> = {
|
|
|
|
|
1: 'xl:grid-cols-1',
|
|
|
|
|
2: 'xl:grid-cols-2',
|
|
|
|
|
3: 'xl:grid-cols-3',
|
|
|
|
|
4: 'xl:grid-cols-4',
|
|
|
|
|
5: 'xl:grid-cols-5',
|
|
|
|
|
6: 'xl:grid-cols-6',
|
|
|
|
|
7: 'xl:grid-cols-7',
|
|
|
|
|
8: 'xl:grid-cols-8',
|
|
|
|
|
9: 'xl:grid-cols-9',
|
|
|
|
|
10: 'xl:grid-cols-10',
|
|
|
|
|
11: 'xl:grid-cols-11',
|
|
|
|
|
12: 'xl:grid-cols-12',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GRID_COLS_2XL_CLASSES: Record<number, string> = {
|
|
|
|
|
1: '2xl:grid-cols-1',
|
|
|
|
|
2: '2xl:grid-cols-2',
|
|
|
|
|
3: '2xl:grid-cols-3',
|
|
|
|
|
4: '2xl:grid-cols-4',
|
|
|
|
|
5: '2xl:grid-cols-5',
|
|
|
|
|
6: '2xl:grid-cols-6',
|
|
|
|
|
7: '2xl:grid-cols-7',
|
|
|
|
|
8: '2xl:grid-cols-8',
|
|
|
|
|
9: '2xl:grid-cols-9',
|
|
|
|
|
10: '2xl:grid-cols-10',
|
|
|
|
|
11: '2xl:grid-cols-11',
|
|
|
|
|
12: '2xl:grid-cols-12',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const GAP_CLASSES: Record<number, string> = {
|
|
|
|
|
0: 'gap-0',
|
|
|
|
|
1: 'gap-1',
|
|
|
|
|
2: 'gap-2',
|
|
|
|
|
3: 'gap-3',
|
|
|
|
|
4: 'gap-4',
|
|
|
|
|
5: 'gap-5',
|
|
|
|
|
6: 'gap-6',
|
|
|
|
|
7: 'gap-7',
|
|
|
|
|
8: 'gap-8',
|
|
|
|
|
9: 'gap-9',
|
|
|
|
|
10: 'gap-10',
|
|
|
|
|
11: 'gap-11',
|
|
|
|
|
12: 'gap-12',
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Composant Grid responsive pour afficher des items en grille.
|
|
|
|
|
*/
|
|
|
|
|
export function Grid({
|
|
|
|
|
children,
|
|
|
|
|
columns = 3,
|
|
|
|
|
gap,
|
|
|
|
|
rowGap,
|
|
|
|
|
columnGap,
|
|
|
|
|
className,
|
|
|
|
|
}: GridProps) {
|
|
|
|
|
const gridClasses = useMemo(() => {
|
|
|
|
|
const classes: string[] = ['grid'];
|
|
|
|
|
|
|
|
|
|
// Gestion des colonnes
|
|
|
|
|
if (typeof columns === 'number') {
|
|
|
|
|
// Colonnes fixes
|
|
|
|
|
const colsClass = GRID_COLS_CLASSES[columns] || `grid-cols-${columns}`;
|
|
|
|
|
classes.push(colsClass);
|
|
|
|
|
} else {
|
|
|
|
|
// Colonnes responsives
|
|
|
|
|
const { sm, md, lg, xl, '2xl': xl2 } = columns;
|
|
|
|
|
|
|
|
|
|
// Colonne par défaut (mobile first)
|
|
|
|
|
if (sm) {
|
|
|
|
|
classes.push(GRID_COLS_CLASSES[sm] || `grid-cols-${sm}`);
|
|
|
|
|
} else {
|
|
|
|
|
classes.push('grid-cols-1'); // Par défaut 1 colonne sur mobile
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (sm) {
|
|
|
|
|
classes.push(GRID_COLS_SM_CLASSES[sm] || `sm:grid-cols-${sm}`);
|
|
|
|
|
}
|
|
|
|
|
if (md) {
|
|
|
|
|
classes.push(GRID_COLS_MD_CLASSES[md] || `md:grid-cols-${md}`);
|
|
|
|
|
}
|
|
|
|
|
if (lg) {
|
|
|
|
|
classes.push(GRID_COLS_LG_CLASSES[lg] || `lg:grid-cols-${lg}`);
|
|
|
|
|
}
|
|
|
|
|
if (xl) {
|
|
|
|
|
classes.push(GRID_COLS_XL_CLASSES[xl] || `xl:grid-cols-${xl}`);
|
|
|
|
|
}
|
|
|
|
|
if (xl2) {
|
|
|
|
|
classes.push(GRID_COLS_2XL_CLASSES[xl2] || `2xl:grid-cols-${xl2}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gestion du gap
|
|
|
|
|
if (gap !== undefined) {
|
|
|
|
|
classes.push(GAP_CLASSES[gap] || `gap-${gap}`);
|
|
|
|
|
} else {
|
|
|
|
|
// Si rowGap et columnGap sont spécifiés, on les utilise
|
|
|
|
|
if (rowGap !== undefined) {
|
|
|
|
|
classes.push(`gap-y-${rowGap}`);
|
|
|
|
|
}
|
|
|
|
|
if (columnGap !== undefined) {
|
|
|
|
|
classes.push(`gap-x-${columnGap}`);
|
|
|
|
|
}
|
|
|
|
|
// Si aucun gap n'est spécifié, on utilise gap-4 par défaut
|
2025-12-13 02:34:34 +00:00
|
|
|
if (
|
|
|
|
|
gap === undefined &&
|
|
|
|
|
rowGap === undefined &&
|
|
|
|
|
columnGap === undefined
|
|
|
|
|
) {
|
2025-12-03 21:56:50 +00:00
|
|
|
classes.push('gap-4');
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return classes;
|
|
|
|
|
}, [columns, gap, rowGap, columnGap]);
|
|
|
|
|
|
|
|
|
|
// Calcul du style inline pour les colonnes personnalisées
|
|
|
|
|
const inlineStyle = useMemo(() => {
|
|
|
|
|
const style: React.CSSProperties = {};
|
|
|
|
|
|
|
|
|
|
if (typeof columns === 'number') {
|
|
|
|
|
// Si c'est un nombre et qu'il n'est pas dans les classes prédéfinies
|
|
|
|
|
if (columns > 12 || columns < 1) {
|
|
|
|
|
style.gridTemplateColumns = `repeat(${columns}, minmax(0, 1fr))`;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
// Pour les breakpoints responsives, on utilise les classes Tailwind
|
|
|
|
|
// mais on peut ajouter un style par défaut pour mobile
|
|
|
|
|
const { sm } = columns;
|
|
|
|
|
if (!sm) {
|
|
|
|
|
style.gridTemplateColumns = 'repeat(1, minmax(0, 1fr))';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Object.keys(style).length > 0 ? style : undefined;
|
|
|
|
|
}, [columns]);
|
|
|
|
|
|
|
|
|
|
return (
|
2025-12-13 02:34:34 +00:00
|
|
|
<div className={cn(...gridClasses, className)} style={inlineStyle}>
|
2025-12-03 21:56:50 +00:00
|
|
|
{children}
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|