82 lines
2.5 KiB
JavaScript
82 lines
2.5 KiB
JavaScript
import * as React from 'react';
|
|
import { cn } from '@/lib/utils';
|
|
|
|
/**
|
|
* Layout helpers.
|
|
*
|
|
* These exist so vertical rhythm and grid behaviour are declared once. The
|
|
* reference pages each invented their own spacing (`space-y-5`, `space-y-6`,
|
|
* `space-y-8` on sibling pages), which is exactly the drift a design system is
|
|
* supposed to prevent.
|
|
*/
|
|
|
|
const GAPS = { sm: 'gap-3', default: 'gap-4', lg: 'gap-6' };
|
|
|
|
/**
|
|
* Grid — responsive column layouts with the app's standard breakpoints.
|
|
*
|
|
* `cols` is the count at the largest breakpoint; the intermediate steps are
|
|
* chosen to avoid orphan cards (6 → 2/3/6 rather than 1/3/6).
|
|
*/
|
|
/** @param {any} props */
|
|
export function Grid({ cols = 3, gap = 'default', className, children, ...props }) {
|
|
const columns = {
|
|
1: 'grid-cols-1',
|
|
2: 'grid-cols-1 sm:grid-cols-2',
|
|
3: 'grid-cols-1 md:grid-cols-2 lg:grid-cols-3',
|
|
4: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-4',
|
|
5: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-5',
|
|
6: 'grid-cols-2 md:grid-cols-3 lg:grid-cols-6',
|
|
}[cols];
|
|
|
|
return (
|
|
<div className={cn('grid', columns, GAPS[gap], className)} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/** Stack — vertical rhythm. `space` maps to the two spacings pages should use. */
|
|
/** @param {any} props */
|
|
export function Stack({ space = 'default', className, children, ...props }) {
|
|
const spacing = {
|
|
xs: 'space-y-2',
|
|
sm: 'space-y-3',
|
|
default: 'space-y-5',
|
|
lg: 'space-y-6',
|
|
xl: 'space-y-8',
|
|
}[space];
|
|
|
|
return (
|
|
<div className={cn(spacing, className)} {...props}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* PageSection — a titled region of a page. Pairs a SectionHeader-style label
|
|
* with its content at consistent spacing.
|
|
*/
|
|
/** @param {any} props */
|
|
export function PageSection({ title, subtitle, actions, level = 'overline', className, children }) {
|
|
return (
|
|
<section className={cn('space-y-3', className)}>
|
|
{title && (
|
|
<div className="flex items-center justify-between gap-3">
|
|
{level === 'overline' ? (
|
|
<h2 className="text-overline uppercase text-ink-4 font-heading">{title}</h2>
|
|
) : (
|
|
<div>
|
|
<h2 className="text-title-lg font-heading text-ink-1">{title}</h2>
|
|
{subtitle && <p className="text-body-sm text-ink-3 mt-0.5">{subtitle}</p>}
|
|
</div>
|
|
)}
|
|
{actions && <div className="flex items-center gap-2 shrink-0">{actions}</div>}
|
|
</div>
|
|
)}
|
|
{children}
|
|
</section>
|
|
);
|
|
}
|