251 lines
6.2 KiB
TypeScript
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 }; |