143 lines
3.7 KiB
TypeScript
143 lines
3.7 KiB
TypeScript
import { cn } from "@/lib/utils";
|
|
|
|
/**
|
|
* Building blocks for the detail / settings rhythm system. All spacing
|
|
* resolves to CSS tokens declared in `app/globals.css` under @theme:
|
|
* --spacing-card → p-card (card interior padding)
|
|
* --spacing-card-gap → gap-card-gap (gap between sibling cards)
|
|
* --spacing-section → gap-section / pt-section (intra-card section gap)
|
|
* --spacing-chip → gap-chip (chip clusters, pill grids, button bars)
|
|
* --spacing-label → mb-label (label header → content)
|
|
* --spacing-stat → mb-stat (hero-stat label → big number)
|
|
* --spacing-stat-gap → gap-stat-gap (horizontal between hero-stat cols)
|
|
*
|
|
* Use these instead of raw `p-[15px]` / `gap-[9px]` so a single token
|
|
* change ripples across every page.
|
|
*/
|
|
|
|
/** Outer card frame. Glass surface, rounded corners, uniform padding. */
|
|
export function Panel({
|
|
children,
|
|
className,
|
|
as: Tag = "div",
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
/** Render as a different tag if needed (e.g. "section", "aside"). */
|
|
as?: keyof React.JSX.IntrinsicElements;
|
|
}) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const T = Tag as any;
|
|
return (
|
|
<T className={cn("glass rounded-2xl p-card", className)}>
|
|
{children}
|
|
</T>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Vertical container for sibling Panels. Replaces ad-hoc `space-y-N` on
|
|
* an aside or column wrapper — picks up the panel-to-panel rhythm.
|
|
*/
|
|
export function PanelStack({
|
|
children,
|
|
className,
|
|
as: Tag = "div",
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
as?: keyof React.JSX.IntrinsicElements;
|
|
}) {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const T = Tag as any;
|
|
return (
|
|
<T className={cn("flex flex-col gap-card-gap", className)}>
|
|
{children}
|
|
</T>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Vertical flow inside a Panel — pulls children apart by the
|
|
* intra-card section gap. Use for stacked blocks (header → flag pills
|
|
* → meta strip → hero stats), NOT for label→content (use PanelHeader for that).
|
|
*/
|
|
export function PanelSection({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div className={cn("flex flex-col gap-section", className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Mono caps label, e.g. "ACTRESSES" / "TAGS". Renders the label and
|
|
* applies the standard label→content margin to whatever sits beneath.
|
|
* Pair with the chip cluster or any other content block.
|
|
*/
|
|
export function PanelHeader({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
className={cn(
|
|
"text-[10px] uppercase tracking-wider font-mono text-[var(--color-fg-muted)] mb-label",
|
|
className,
|
|
)}
|
|
>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Horizontal flex-wrap chip strip used for actresses / genres / tags /
|
|
* collections / flag pill rows. Spacing is the unified `gap-chip` token.
|
|
*/
|
|
export function ChipCluster({
|
|
children,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div className={cn("flex flex-wrap gap-chip", className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Equal-column button row that sits OUTSIDE any Panel (Edit Metadata /
|
|
* Import / Delete pattern). Lives in the same vertical rhythm as
|
|
* Panels via PanelStack's gap-card-gap.
|
|
*/
|
|
export function ActionBar({
|
|
children,
|
|
cols = 3,
|
|
className,
|
|
}: {
|
|
children: React.ReactNode;
|
|
cols?: 2 | 3 | 4;
|
|
className?: string;
|
|
}) {
|
|
const colClass =
|
|
cols === 2 ? "grid-cols-2" : cols === 4 ? "grid-cols-4" : "grid-cols-3";
|
|
return (
|
|
<div className={cn("grid gap-chip", colClass, className)}>
|
|
{children}
|
|
</div>
|
|
);
|
|
}
|