import * as React from 'react'; import { cn } from '../lib/utils'; // Count-aware responsive grid for dashboard tiles. Callers render tiles in // priority order and TileGrid derives the column rules from how many // children actually rendered — tiles entering or exiting the ecosystem // (Top content, Engagement, …) never require a call-site class change. // // Breakpoints are container queries, not viewport queries: available width // in wp-admin depends on the admin menu (folded vs not) and the panel the // grid sits in, so the same grid self-adjusts wherever it's mounted. // // Two variants encode different odd-count policies: // - `metric` (compact stat tiles): equal widths always; a short last row // (3+2, 4+3) is preferred over resizing tiles, because stat tiles scan // by uniformity. // - `panel` (tall list tiles): the FIRST child is promoted to a 2-column // hero when — and only when — the count is odd, which keeps every row // full-width (5 → hero+1 / 3, 7 → hero+2 / 4). Priority list == render // order, so when a conditional lead tile is absent the next tile // inherits hero status automatically. Note: a tile's prominence can // therefore change when unrelated tiles enter/exit (count parity flips). // That is by design, not a bug. export type TileGridVariant = 'metric' | 'panel'; // Hero promotion for odd panel counts. Span-2 is consistent across the // 2-col and wide tiers, so one pair of classes covers both. const PANEL_HERO = '@2xl:[&>*:first-child]:col-span-2'; // Class strings must be complete literals (Tailwind JIT cannot see // computed strings). @2xl ≈ the old `md:` viewport switch once admin // chrome is subtracted; @min-[100rem] ≈ the old `min-[1800px]:` 4-up tier. const PANEL_GRID: Record = { 1: 'grid-cols-1', 2: 'grid-cols-1 @2xl:grid-cols-2', // 3 fits one row at the wide tier, so the hero demotes back to span-1. 3: `grid-cols-1 @2xl:grid-cols-2 ${ PANEL_HERO } @min-[100rem]:grid-cols-3 @min-[100rem]:[&>*:first-child]:col-span-1`, 4: 'grid-cols-1 @2xl:grid-cols-2 @min-[100rem]:grid-cols-4', 5: `grid-cols-1 @2xl:grid-cols-2 ${ PANEL_HERO } @min-[100rem]:grid-cols-3`, 6: 'grid-cols-1 @2xl:grid-cols-2 @min-[100rem]:grid-cols-3', 7: `grid-cols-1 @2xl:grid-cols-2 ${ PANEL_HERO } @min-[100rem]:grid-cols-4`, // 8 breakdown cards. Step columns up early so tiles stay roughly square // instead of stretching into wide rectangles: 2-up from @2xl, 3-up by // ~1152px, 4-up by ~1536px (full-screen ≈1080p+ lands on 4). 8: 'grid-cols-1 @2xl:grid-cols-2 @min-[72rem]:grid-cols-3 @min-[96rem]:grid-cols-4', }; // Metric tiles are compact, so the strip stays on a single row above mobile // (2-up on mobile). 5 fits one row of 5 — the Dashboard KPI strip — rather // than wrapping to 3+2. 6+ fall back to balanced short last rows. @4xl unifies // the previously divergent Summary (xl:) and HeroChart (md:) viewport breakpoints. const METRIC_GRID: Record = { 1: 'grid-cols-1', 2: 'grid-cols-2', 3: 'grid-cols-1 @lg:grid-cols-3', 4: 'grid-cols-2 @4xl:grid-cols-4', 5: 'grid-cols-2 @3xl:grid-cols-5', 6: 'grid-cols-2 @4xl:grid-cols-3', 7: 'grid-cols-2 @4xl:grid-cols-4', 8: 'grid-cols-2 @4xl:grid-cols-4', }; const MAX_COUNT = 8; /** * Resolve the grid classes for a tile count. Exported for unit tests. * Counts above 8 fall back to the 8-tile layout (4 across, may leave a * short last row) — extend the tables when a 9th tile actually ships. */ export function tileGridClasses( variant: TileGridVariant, count: number ): string { const table = variant === 'panel' ? PANEL_GRID : METRIC_GRID; const clamped = Math.min( Math.max( count, 1 ), MAX_COUNT ); return table[ clamped ]; } interface Props { variant: TileGridVariant; children: React.ReactNode; className?: string; } export function TileGrid( { variant, children, className }: Props ) { // toArray drops null/false children, so conditionally-rendered tiles // change the count (and the layout) without any caller bookkeeping. const count = React.Children.toArray( children ).length; return (
{ children }
); }