veza/apps/web/src/components/data/Grid.tsx

218 lines
5.1 KiB
TypeScript
Raw Normal View History

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
) {
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}>
{children}
</div>
);
}