nexus/ui/src/components/ThemePaletteGrid.tsx

120 lines
2.9 KiB
TypeScript

import { Badge } from "@/components/ui/badge";
import { cn } from "@/lib/utils";
export interface PaletteRole {
name: string;
dark: { oklch: string; hex: string; wcagAA: boolean };
light: { oklch: string; hex: string; wcagAA: boolean };
}
interface ThemePaletteGridProps {
palette: PaletteRole[];
variant?: "dark" | "light";
className?: string;
}
const ROLE_LABELS: Record<string, string> = {
background: "Background",
surface: "Surface",
overlay: "Overlay",
text: "Text",
"accent-1": "Accent-1",
"accent-2": "Accent-2",
"accent-3": "Accent-3",
};
function Swatch({
role,
variant,
}: {
role: PaletteRole;
variant: "dark" | "light";
}) {
const entry = variant === "dark" ? role.dark : role.light;
const label = ROLE_LABELS[role.name] ?? role.name;
return (
<div className="flex flex-col items-center gap-1" style={{ minWidth: 56 }}>
<div
className="rounded-md border border-border"
style={{
width: 40,
height: 40,
backgroundColor: entry.hex,
minWidth: 40,
minHeight: 40,
}}
title={`${label}: ${entry.hex}`}
aria-label={`${label} swatch ${entry.hex}`}
/>
<span className="font-mono text-xs text-muted-foreground">
{entry.hex}
</span>
{entry.wcagAA ? (
<Badge
variant="default"
className="bg-[#40a02b] dark:bg-[#a6e3a1] text-white dark:text-black border-0 text-[10px] px-1.5 py-0"
>
AA
</Badge>
) : (
<Badge variant="destructive" className="text-[10px] px-1.5 py-0">
Fails AA
</Badge>
)}
</div>
);
}
function PaletteRow({
palette,
variant,
}: {
palette: PaletteRole[];
variant: "dark" | "light";
}) {
return (
<div className="flex flex-col gap-2">
<span className="text-xs font-medium text-muted-foreground capitalize">
{variant}
</span>
<div className="flex flex-row flex-wrap gap-2">
{palette.map((role) => (
<Swatch key={role.name} role={role} variant={variant} />
))}
</div>
</div>
);
}
export function ThemePaletteGrid({
palette,
variant = "dark",
className,
}: ThemePaletteGridProps) {
if (palette.length === 0) {
return (
<div
className={cn(
"flex flex-col items-center justify-center gap-2 rounded-lg border border-dashed border-border p-8 text-center",
className,
)}
>
<h3 className="text-base font-semibold text-foreground">
No palette yet
</h3>
<p className="text-sm text-muted-foreground max-w-xs">
Pick a seed color to generate a full OKLCH palette with dark and light
variants.
</p>
</div>
);
}
return (
<div className={cn("flex flex-col gap-4", className)}>
<PaletteRow palette={palette} variant="dark" />
<PaletteRow palette={palette} variant="light" />
</div>
);
}