Files
klz-cables.com/components/ui/Grid.tsx
2025-12-29 18:18:48 +01:00

251 lines
6.2 KiB
TypeScript

import React, { forwardRef, ReactNode, HTMLAttributes } from 'react';
import { cn } from '../../lib/utils';
import { getViewport } from '../../lib/responsive';
// Grid column types
type GridCols = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
// Grid gap types
type GridGap = 'none' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | '2xl' | 'responsive';
// Grid props interface
interface GridProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
cols?: GridCols;
gap?: GridGap;
colsSm?: GridCols;
colsMd?: GridCols;
colsLg?: GridCols;
colsXl?: GridCols;
alignItems?: 'start' | 'center' | 'end' | 'stretch';
justifyItems?: 'start' | 'center' | 'end' | 'stretch';
// Mobile-first stacking
stackMobile?: boolean;
// Responsive columns
responsiveCols?: {
mobile?: GridCols;
tablet?: GridCols;
desktop?: GridCols;
};
}
// Grid item props interface
interface GridItemProps extends HTMLAttributes<HTMLDivElement> {
children?: ReactNode;
colSpan?: GridCols;
colSpanSm?: GridCols;
colSpanMd?: GridCols;
colSpanLg?: GridCols;
colSpanXl?: GridCols;
rowSpan?: GridCols;
rowSpanSm?: GridCols;
rowSpanMd?: GridCols;
rowSpanLg?: GridCols;
rowSpanXl?: GridCols;
}
// Helper function to get gap styles
const getGapStyles = (gap: GridGap, responsiveGap?: boolean) => {
if (gap === 'responsive' || responsiveGap) {
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8';
}
switch (gap) {
case 'none':
return 'gap-0';
case 'xs':
return 'gap-1';
case 'sm':
return 'gap-2';
case 'md':
return 'gap-4';
case 'lg':
return 'gap-6';
case 'xl':
return 'gap-8';
case '2xl':
return 'gap-12';
default:
return 'gap-4';
}
};
// Helper function to get column classes
const getColClasses = (cols: GridCols | undefined, breakpoint: string = '') => {
if (!cols) return '';
const prefix = breakpoint ? `${breakpoint}:` : '';
return `${prefix}grid-cols-${cols}`;
};
// Helper function to get span classes
const getSpanClasses = (span: GridCols | undefined, type: 'col' | 'row', breakpoint: string = '') => {
if (!span) return '';
const prefix = breakpoint ? `${breakpoint}:` : '';
const typePrefix = type === 'col' ? 'col' : 'row';
return `${prefix}${typePrefix}-span-${span}`;
};
// Helper function to get responsive column classes
const getResponsiveColClasses = (responsiveCols: GridProps['responsiveCols']) => {
if (!responsiveCols) return '';
let classes = '';
// Mobile (default)
if (responsiveCols.mobile) {
classes += `grid-cols-${responsiveCols.mobile} `;
}
// Tablet
if (responsiveCols.tablet) {
classes += `md:grid-cols-${responsiveCols.tablet} `;
}
// Desktop
if (responsiveCols.desktop) {
classes += `lg:grid-cols-${responsiveCols.desktop} `;
}
return classes;
};
// Main Grid Component
export const Grid = forwardRef<HTMLDivElement, GridProps>(
(
{
cols = 1,
gap = 'md',
colsSm,
colsMd,
colsLg,
colsXl,
alignItems,
justifyItems,
className = '',
children,
stackMobile = false,
responsiveCols,
...props
},
ref
) => {
// Get responsive column configuration
const getResponsiveColumns = () => {
if (responsiveCols) {
return getResponsiveColClasses(responsiveCols);
}
if (stackMobile) {
// Mobile-first: 1 column, then scale up
return `grid-cols-1 sm:grid-cols-2 ${colsMd ? `md:grid-cols-${colsMd}` : 'md:grid-cols-3'} ${colsLg ? `lg:grid-cols-${colsLg}` : ''}`;
}
// Default responsive behavior
let colClasses = `grid-cols-${cols}`;
if (colsSm) colClasses += ` sm:grid-cols-${colsSm}`;
if (colsMd) colClasses += ` md:grid-cols-${colsMd}`;
if (colsLg) colClasses += ` lg:grid-cols-${colsLg}`;
if (colsXl) colClasses += ` xl:grid-cols-${colsXl}`;
return colClasses;
};
// Get responsive gap
const getResponsiveGap = () => {
if (gap === 'responsive') {
return 'gap-2 xs:gap-3 sm:gap-4 md:gap-6 lg:gap-8';
}
// Mobile-first gap scaling
if (stackMobile) {
return 'gap-3 sm:gap-4 md:gap-6 lg:gap-8';
}
return getGapStyles(gap);
};
return (
<div
ref={ref}
className={cn(
// Base grid
'grid',
// Responsive columns
getResponsiveColumns(),
// Gap (responsive)
getResponsiveGap(),
// Alignment
alignItems && `items-${alignItems}`,
justifyItems && `justify-items-${justifyItems}`,
// Mobile-specific: ensure full width
'w-full',
// Custom classes
className
)}
// Add role for accessibility
role="grid"
{...props}
>
{children}
</div>
);
}
);
Grid.displayName = 'Grid';
// Grid Item Component
export const GridItem = forwardRef<HTMLDivElement, GridItemProps>(
(
{
colSpan,
colSpanSm,
colSpanMd,
colSpanLg,
colSpanXl,
rowSpan,
rowSpanSm,
rowSpanMd,
rowSpanLg,
rowSpanXl,
className = '',
children,
...props
},
ref
) => {
return (
<div
ref={ref}
className={cn(
// Column spans
getSpanClasses(colSpan, 'col'),
getSpanClasses(colSpanSm, 'col', 'sm'),
getSpanClasses(colSpanMd, 'col', 'md'),
getSpanClasses(colSpanLg, 'col', 'lg'),
getSpanClasses(colSpanXl, 'col', 'xl'),
// Row spans
getSpanClasses(rowSpan, 'row'),
getSpanClasses(rowSpanSm, 'row', 'sm'),
getSpanClasses(rowSpanMd, 'row', 'md'),
getSpanClasses(rowSpanLg, 'row', 'lg'),
getSpanClasses(rowSpanXl, 'row', 'xl'),
// Ensure item doesn't overflow
'min-w-0',
// Custom classes
className
)}
// Add role for accessibility
role="gridcell"
{...props}
>
{children}
</div>
);
}
);
GridItem.displayName = 'GridItem';
// Export types for external use
export type { GridProps, GridItemProps, GridCols, GridGap };