/**
* Card (web). A bordered surface — the default container for grouped
* content. Hairline border is the resting elevation (no shadow by
* default); `interactive` warms the border on hover so clickable cards
* read as clickable.
*
* …
*
*
*/
import { forwardRef, type CSSProperties, type ReactNode } from "react";
import { useTheme } from "@plyxui/styles";
import { radius as radiusScale, spacing } from "@plyxui/core";
export type CardVariant = "outline" | "raised" | "sunken";
export type CardPadding = "none" | "sm" | "md" | "lg";
export type CardRadius = "sm" | "md" | "lg" | "xl";
const padMap: Record = {
none: 0,
sm: spacing[3],
md: spacing[4],
lg: spacing[6],
};
export interface CardProps {
variant?: CardVariant;
padding?: CardPadding;
radius?: CardRadius;
/** Warm the border on hover; renders as a button when onClick is set. */
interactive?: boolean;
onClick?: () => void;
className?: string;
style?: CSSProperties;
children?: ReactNode;
"aria-label"?: string;
}
export const Card = forwardRef(function Card(
{ variant = "outline", padding = "md", radius = "lg", interactive, onClick, className, style, children, ...aria },
ref,
) {
const { colors } = useTheme();
const bg =
variant === "raised" ? colors.surfaceFill : variant === "sunken" ? colors.containerFill : colors.surfaceFill;
const base: CSSProperties = {
background: bg,
border: `1px solid ${colors.stroke}`,
borderRadius: radiusScale[radius],
padding: padMap[padding],
cursor: interactive || onClick ? "pointer" : undefined,
transition: "border-color 0.2s cubic-bezier(0.23,1,0.32,1), transform 0.2s cubic-bezier(0.23,1,0.32,1)",
...style,
};
const hoverOn = (e: React.MouseEvent) => {
if (!interactive && !onClick) return;
e.currentTarget.style.borderColor = colors.primaryOrange;
};
const hoverOff = (e: React.MouseEvent) => {
if (!interactive && !onClick) return;
e.currentTarget.style.borderColor = colors.stroke;
};
return (
{children}
);
});