1import { cva, type VariantProps } from 'class-variance-authority';
2import React from 'react';
3import { cn } from '../lib/cn';
4
5const gridVariants = cva('grid', {
6 variants: {
7 columns: {
8 '1': 'grid-cols-1',
9 '2': 'grid-cols-2',
10 '3': 'grid-cols-3',
11 '4': 'grid-cols-4',
12 '5': 'grid-cols-5',
13 '6': 'grid-cols-6',
14 '7': 'grid-cols-7',
15 '8': 'grid-cols-8',
16 '9': 'grid-cols-9',
17 '10': 'grid-cols-10',
18 '11': 'grid-cols-11',
19 '12': 'grid-cols-12',
20 none: 'grid-cols-none',
21 subgrid: 'grid-cols-subgrid',
22 },
23 rows: {
24 '1': 'grid-rows-1',
25 '2': 'grid-rows-2',
26 '3': 'grid-rows-3',
27 '4': 'grid-rows-4',
28 '5': 'grid-rows-5',
29 '6': 'grid-rows-6',
30 none: 'grid-rows-none',
31 subgrid: 'grid-rows-subgrid',
32 },
33 flow: {
34 row: 'grid-flow-row',
35 col: 'grid-flow-col',
36 dense: 'grid-flow-dense',
37 'row-dense': 'grid-flow-row-dense',
38 'col-dense': 'grid-flow-col-dense',
39 },
40 align: {
41 start: 'items-start',
42 center: 'items-center',
43 end: 'items-end',
44 baseline: 'items-baseline',
45 stretch: 'items-stretch',
46 },
47 justify: {
48 start: 'justify-items-start',
49 center: 'justify-items-center',
50 end: 'justify-items-end',
51 stretch: 'justify-items-stretch',
52 },
53 gap: {
54 '0': 'gap-0',
55 '1': 'gap-1',
56 '2': 'gap-2',
57 '3': 'gap-3',
58 '4': 'gap-4',
59 '5': 'gap-5',
60 '6': 'gap-6',
61 '8': 'gap-8',
62 '10': 'gap-10',
63 '12': 'gap-12',
64 '16': 'gap-16',
65 '20': 'gap-20',
66 '24': 'gap-24',
67 '32': 'gap-32',
68 },
69 gapX: {
70 '0': 'gap-x-0',
71 '1': 'gap-x-1',
72 '2': 'gap-x-2',
73 '3': 'gap-x-3',
74 '4': 'gap-x-4',
75 '5': 'gap-x-5',
76 '6': 'gap-x-6',
77 '8': 'gap-x-8',
78 '10': 'gap-x-10',
79 '12': 'gap-x-12',
80 '16': 'gap-x-16',
81 '20': 'gap-x-20',
82 '24': 'gap-x-24',
83 '32': 'gap-x-32',
84 },
85 gapY: {
86 '0': 'gap-y-0',
87 '1': 'gap-y-1',
88 '2': 'gap-y-2',
89 '3': 'gap-y-3',
90 '4': 'gap-y-4',
91 '5': 'gap-y-5',
92 '6': 'gap-y-6',
93 '8': 'gap-y-8',
94 '10': 'gap-y-10',
95 '12': 'gap-y-12',
96 '16': 'gap-y-16',
97 '20': 'gap-y-20',
98 '24': 'gap-y-24',
99 '32': 'gap-y-32',
100 },
101 },
102 defaultVariants: {
103 columns: '1',
104 },
105});
106
107const GRID_RESPONSIVE_COLUMNS = {
108 sm: {
109 '1': 'sm:grid-cols-1',
110 '2': 'sm:grid-cols-2',
111 '3': 'sm:grid-cols-3',
112 '4': 'sm:grid-cols-4',
113 '5': 'sm:grid-cols-5',
114 '6': 'sm:grid-cols-6',
115 '7': 'sm:grid-cols-7',
116 '8': 'sm:grid-cols-8',
117 '9': 'sm:grid-cols-9',
118 '10': 'sm:grid-cols-10',
119 '11': 'sm:grid-cols-11',
120 '12': 'sm:grid-cols-12',
121 none: 'sm:grid-cols-none',
122 subgrid: 'sm:grid-cols-subgrid',
123 },
124 md: {
125 '1': 'md:grid-cols-1',
126 '2': 'md:grid-cols-2',
127 '3': 'md:grid-cols-3',
128 '4': 'md:grid-cols-4',
129 '5': 'md:grid-cols-5',
130 '6': 'md:grid-cols-6',
131 '7': 'md:grid-cols-7',
132 '8': 'md:grid-cols-8',
133 '9': 'md:grid-cols-9',
134 '10': 'md:grid-cols-10',
135 '11': 'md:grid-cols-11',
136 '12': 'md:grid-cols-12',
137 none: 'md:grid-cols-none',
138 subgrid: 'md:grid-cols-subgrid',
139 },
140 lg: {
141 '1': 'lg:grid-cols-1',
142 '2': 'lg:grid-cols-2',
143 '3': 'lg:grid-cols-3',
144 '4': 'lg:grid-cols-4',
145 '5': 'lg:grid-cols-5',
146 '6': 'lg:grid-cols-6',
147 '7': 'lg:grid-cols-7',
148 '8': 'lg:grid-cols-8',
149 '9': 'lg:grid-cols-9',
150 '10': 'lg:grid-cols-10',
151 '11': 'lg:grid-cols-11',
152 '12': 'lg:grid-cols-12',
153 none: 'lg:grid-cols-none',
154 subgrid: 'lg:grid-cols-subgrid',
155 },
156 xl: {
157 '1': 'xl:grid-cols-1',
158 '2': 'xl:grid-cols-2',
159 '3': 'xl:grid-cols-3',
160 '4': 'xl:grid-cols-4',
161 '5': 'xl:grid-cols-5',
162 '6': 'xl:grid-cols-6',
163 '7': 'xl:grid-cols-7',
164 '8': 'xl:grid-cols-8',
165 '9': 'xl:grid-cols-9',
166 '10': 'xl:grid-cols-10',
167 '11': 'xl:grid-cols-11',
168 '12': 'xl:grid-cols-12',
169 none: 'xl:grid-cols-none',
170 subgrid: 'xl:grid-cols-subgrid',
171 },
172} as const;
173
174type GridColumns = keyof typeof GRID_RESPONSIVE_COLUMNS.sm;
175type GridBreakpoint = keyof typeof GRID_RESPONSIVE_COLUMNS;
176type GridResponsiveColumns = { base?: GridColumns } & Partial<Record<GridBreakpoint, GridColumns>>;
177
178type GridOwnProps<E extends React.ElementType> = Omit<
179 VariantProps<typeof gridVariants>,
180 'columns'
181> & {
182 as?: E;
183 columns?: GridColumns | GridResponsiveColumns | null;
184 minChildWidth?: string;
185 className?: string;
186 style?: React.CSSProperties;
187};
188
189type GridProps<E extends React.ElementType = 'div'> = GridOwnProps<E> &
190 Omit<React.ComponentPropsWithRef<E>, keyof GridOwnProps<E>>;
191
192const BREAKPOINTS: GridBreakpoint[] = ['sm', 'md', 'lg', 'xl'];
193
194const Grid = <E extends React.ElementType = 'div'>({
195 as,
196 columns,
197 minChildWidth,
198 rows,
199 flow,
200 align,
201 justify,
202 gap,
203 gapX,
204 gapY,
205 className,
206 style,
207 ...props
208}: GridProps<E>) => {
209 const Component: React.ElementType = as ?? 'div';
210 const responsive = typeof columns === 'object' && columns !== null ? columns : null;
211 const base = typeof columns === 'object' && columns !== null ? columns.base : columns;
212
213 return (
214 <Component
215 style={
216 minChildWidth
217 ? {
218 gridTemplateColumns: `repeat(auto-fit, minmax(min(${minChildWidth}, 100%), 1fr))`,
219 ...style,
220 }
221 : style
222 }
223 className={cn(
224 gridVariants({ columns: base, rows, flow, align, justify, gap, gapX, gapY }),
225 responsive &&
226 BREAKPOINTS.map((breakpoint) => {
227 const value = responsive[breakpoint];
228 return value ? GRID_RESPONSIVE_COLUMNS[breakpoint][value] : null;
229 }),
230 className,
231 )}
232 {...props}
233 />
234 );
235};
236
237Grid.displayName = 'Grid';
238
239export { Grid, gridVariants };
240export type { GridBreakpoint, GridColumns, GridProps, GridResponsiveColumns };