Grid

A flexible grid component for creating layouts with various styles and properties.

experimental
---
import Grid from '@/components/Grid.astro';
---

Grid Component

This page demonstrates various uses of our custom Grid component with responsive properties. Each example includes a description, usage code, and a rendered example within a Tailwind-styled container to visualize the changes.

---
//@ts-nocheck
type JustifyValue = "start" | "end" | "center" | "between";
type AlignValue = "start" | "end" | "center" | "baseline" | "stretch";
type DirectionValue = "row" | "row-dense" | "column" | "column-dense";
type Breakpoint = "initial" | "sm" | "md" | "lg" | "xl" | "2xl";
type SpacingValue = 0 | 1 | 2 | 3 | 4 | 5;
type Display = "none" | "inline-grid" | "grid";
type GridValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | string;
type GapValue = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | string;

interface Props {
  as?: "div" | "span";
  asChild?: boolean;
  display?: Partial<Record<Breakpoint, Display>> | Display;
  areas?: Partial<Record<Breakpoint, string>> | string;
  columns?: Partial<Record<Breakpoint, GridValue>> | GridValue;
  rows?: Partial<Record<Breakpoint, GridValue>> | GridValue;
  flow?: Partial<Record<Breakpoint, DirectionValue>> | DirectionValue;
  align?: Partial<Record<Breakpoint, AlignValue>> | AlignValue;
  justify?: Partial<Record<Breakpoint, JustifyValue>> | JustifyValue;
  gap?: Partial<Record<Breakpoint, GapValue>> | GapValue;
  gapX?: Partial<Record<Breakpoint, GapValue>> | GapValue;
  gapY?: Partial<Record<Breakpoint, GapValue>> | GapValue;
  margin?: Partial<Record<Breakpoint, SpacingValue>> | SpacingValue;
  padding?: Partial<Record<Breakpoint, SpacingValue>> | SpacingValue;
  class?: string;
}

const {
  as: Component = "div",
  asChild = false,
  display = "grid",
  areas,
  columns,
  rows,
  flow,
  align,
  justify,
  gap,
  gapX,
  gapY,
  margin,
  padding,
  class: className,
} = Astro.props;

const uniqueId = 'gx-' + Math.random().toString(36).slice(2, 6);
const PREFIX = uniqueId + "-";
const usedClasses = new Set<string>();

const breakpoints: Record<Breakpoint, string> = {
  initial: "0px",
  sm: "640px",
  md: "768px",
  lg: "1024px",
  xl: "1280px",
  "2xl": "1536px"
};

function generateCSS(): string {
  let css = "";
  const mediaQueries: Record<Breakpoint, string[]> = Object.fromEntries(
    Object.keys(breakpoints).map(bp => [bp, []])
  ) as Record<Breakpoint, string[]>;

  function addRule(rule: string, breakpoint?: Breakpoint): void {
    if (breakpoint && breakpoints[breakpoint]) {
      mediaQueries[breakpoint].push(rule);
    } else {
      css += rule + "\n";
    }
  }

  function processResponsiveValue<T>(
    prop: Partial<Record<Breakpoint, T>> | T | undefined,
    cssProperty: string,
    processor: (value: T) => string
  ): void {
    if (prop === undefined) return;

    if (typeof prop === "object" && !Array.isArray(prop)) {
      Object.entries(prop).forEach(([bp, value]) => {
        addRule(`.${PREFIX}${cssProperty}-${value}-${bp} { ${cssProperty}: ${processor(value)}; }`, bp as Breakpoint);
      });
    } else {
      addRule(`.${PREFIX}${cssProperty}-${prop} { ${cssProperty}: ${processor(prop as T)}; }`);
    }
  }

  processResponsiveValue(display, "display", value => value);
  processResponsiveValue(areas, "grid-template-areas", value => `"${value}"`);
  processResponsiveValue(columns, "grid-template-columns", value => typeof value === "number" ? `repeat(${value}, 1fr)` : value);
  processResponsiveValue(rows, "grid-template-rows", value => typeof value === "number" ? `repeat(${value}, 1fr)` : value);
  processResponsiveValue(flow, "grid-auto-flow", value => value);
  processResponsiveValue(align, "align-items", value => value);
  processResponsiveValue(justify, "justify-content", value => value === "between" ? "space-between" : value);

  function processGap(value: GapValue): string {
    return typeof value === "number" ? `${value * 0.25}rem` : value;
  }

  processResponsiveValue(gap, "gap", processGap);
  processResponsiveValue(gapX, "column-gap", processGap);
  processResponsiveValue(gapY, "row-gap", processGap);

  function processSpacing(value: SpacingValue): string {
    return value === 0 ? "0" : `${value * 0.25}rem`;
  }

  processResponsiveValue(margin, "margin", processSpacing);
  processResponsiveValue(padding, "padding", processSpacing);

  Object.entries(mediaQueries).forEach(([breakpoint, rules]) => {
    if (rules.length > 0) {
      if (breakpoint === "initial") {
        css += rules.join("\n") + "\n";
      } else {
        css += `@media (min-width: ${breakpoints[breakpoint as Breakpoint]}) {\n${rules.join("\n")}\n}\n`;
      }
    }
  });

  return css;
}

function getResponsiveClasses(
  prop: Partial<Record<Breakpoint, any>> | any | undefined,
  prefix: string
): string {
  if (prop === undefined) return "";

  if (typeof prop === "object" && !Array.isArray(prop)) {
    return Object.entries(prop)
      .map(([bp, value]) => {
        const cls = `${PREFIX}${prefix}-${value}-${bp}`;
        usedClasses.add(cls);
        return cls;
      })
      .join(" ");
  } else {
    const cls = `${PREFIX}${prefix}-${prop}`;
    usedClasses.add(cls);
    return cls;
  }
}

const classes = [
  getResponsiveClasses(display, "display"),
  getResponsiveClasses(areas, "grid-template-areas"),
  getResponsiveClasses(columns, "grid-template-columns"),
  getResponsiveClasses(rows, "grid-template-rows"),
  getResponsiveClasses(flow, "grid-auto-flow"),
  getResponsiveClasses(align, "align-items"),
  getResponsiveClasses(justify, "justify-content"),
  getResponsiveClasses(gap, "gap"),
  getResponsiveClasses(gapX, "column-gap"),
  getResponsiveClasses(gapY, "row-gap"),
  getResponsiveClasses(margin, "margin"),
  getResponsiveClasses(padding, "padding"),
  className
].filter(Boolean).join(" ");

const cssContent = generateCSS();

const Element = asChild ? Astro.slots.render('default') : Component;
---

<style set:html={cssContent}></style>

<div class={classes}>
  <slot />
</div>

Basic Grid

This example shows a basic grid with 3 columns.

<Grid columns={3} gap={4}>
  <div class="bg-blue-200 p-4">1</div>
  <div class="bg-blue-300 p-4">2</div>
  <div class="bg-blue-400 p-4">3</div>
  <div class="bg-blue-500 p-4">4</div>
  <div class="bg-blue-600 p-4">5</div>
  <div class="bg-blue-700 p-4">6</div>
</Grid>
1
2
3
4
5
6