import { findFocusableAncestor } from "@prismicio/editor-support/DOM"; import { Icon, IconName, ProgressCircle, Text } from "@prismicio/editor-ui"; import { clsx } from "clsx"; import { LinkProps } from "next/link"; import { createElement, type CSSProperties, type FC, type HTMLAttributes, type ImgHTMLAttributes, type MouseEvent, type PropsWithChildren, type ReactNode, } from "react"; import styles from "./Card.module.css"; export type CardProps = PropsWithChildren< { checked?: boolean; size?: "small" | "medium"; style?: CSSProperties; variant?: "solid" | "outlined"; disabled?: boolean; } & ( | // Props for rendering a non-interactive `div` element. NarrowedCardProps<{ interactive?: false }> // Props for rendering an interactive `div` element. | NarrowedCardProps<{ interactive: true; onClick?: (event: MouseEvent) => void; }> // Props for rendering an `a` element. | NarrowedCardProps<{ interactive: true; href: string; component?: "a" }> // Props for rendering any link `component`. | NarrowedCardProps<{ interactive: true; href: string; component: FC; replace?: boolean; }> ) >; // This type is used to spread the `Card`'s `otherProps` before they can be // narrowed down. type NarrowedCardProps = NarrowedProps< T, "component" | "href" | "interactive" | "onClick" | "replace" >; /** * Construct a type with the properties of T and a set of optional properties K * (excluding those already in type T). */ type NarrowedProps = T & Omit>, keyof T>; export const Card: FC = (props) => { const { checked = false, size = "medium", variant = "solid", interactive: _interactive, disabled: _disabled, onClick: _onClick, href: _href, component = "a", replace: _replace, ...otherProps } = props; const elementProps = { ...otherProps, className: clsx(styles.root, styles[`size-${size}`], styles[variant], { [styles.interactive]: props.interactive, }), "data-state": checked === true ? "checked" : undefined, "data-disabled": props.disabled === true ? "" : undefined, }; if (props.interactive === true && props.href === undefined) { return (
{ if (props.disabled === true || props.onClick === undefined) return; const target = event.target as HTMLElement; if (findFocusableAncestor(target) === event.currentTarget) { props.onClick(event); } }} tabIndex={props.disabled === true ? undefined : 0} /> ); } else if (props.interactive === true) { return createElement(component, { ...elementProps, href: props.href, onClick: (event) => { const target = event.target as HTMLElement; if (findFocusableAncestor(target) !== event.currentTarget) { event.preventDefault(); } }, ...(component === "a" ? {} : { replace: props.replace }), }); } else { return
; } }; type CardMediaProps = { overlay?: ReactNode } & ( | ({ component?: "img" } & ImgHTMLAttributes) | ({ component: "div" } & HTMLAttributes) ); export const CardMedia: FC = ({ className, component = "img", overlay, ...otherProps }) => (
{createElement(component, { ...otherProps, className: clsx(styles[`mediaComponent-${component}`], className), })} {Boolean(overlay) ? (
{overlay}
) : undefined}
); export const CardActions: FC = (props) => (
); type CardFooterProps = { loading?: boolean; startIcon?: IconName; action?: ReactNode; subtitle?: ReactNode; title?: ReactNode; error?: boolean; }; export const CardFooter: FC = ({ action, loading = false, startIcon, subtitle, title, error = false, ...otherProps }) => { const highlightColor = error ? "tomato11" : "grey11"; return (
{(startIcon || loading) && (
{loading && } {!loading && startIcon && ( )}
)}
{title} {subtitle}
{action}
); }; export const CardStatus: FC = ({ children, ...otherProps }) => (
{children}
);