import * as stylex from "@stylexjs/stylex"; import { CSSProperties, type PropsWithChildren, memo, useEffect, useRef, } from "react"; import { color, duration, size, timingFunction } from "./tokens.stylex"; // This needs styling. The component currently only exists // so that we can use it with its API const fadeIn = stylex.keyframes({ from: { opacity: 0, }, to: { opacity: 1, }, }); const shake = stylex.keyframes({ "0%": { transform: "translateX(0)", }, "25%": { transform: "translateX(-4px)", }, "50%": { transform: "translateX(4px)", }, "75%": { transform: "translateX(-4px)", }, "100%": { transform: "translateX(0)", }, }); const pop = stylex.keyframes({ "0%": { transform: "scale(1, 1)", }, "50%": { transform: "scale(1.05, 1.05)", }, "100%": { transform: "scale(1, 1)", }, }); const flash = stylex.keyframes({ "0%": { opacity: 0, filter: "brightness(1)", }, "50%": { opacity: 1, filter: "brightness(1.75)", }, "100%": { filter: "brightness(1)", }, }); const styles = stylex.create({ alert: { display: "flex", alignItems: "center", gap: size.px4, padding: size.px4, margin: `${size.px4} 0`, }, error: { backgroundColor: color.red400, }, warning: { backgroundColor: color.apricot300, }, info: { backgroundColor: color.blue400, }, success: { backgroundColor: color.apple400, }, icon: { width: size.px6, height: size.px6, minWidth: size.px6, minHeight: size.px6, mixBlendMode: "overlay", }, }); const fadeInStyles = stylex.create({ info: { animationName: fadeIn, animationDuration: duration.slow, animationTimingFunction: timingFunction.fast, }, error: { animationName: `${fadeIn}, ${shake}`, animationDuration: `${duration.slow}, ${duration.default}`, animationTimingFunction: `${timingFunction.fast}, ${timingFunction.fast}`, }, warning: { animationName: flash, animationDuration: duration.default, animationTimingFunction: "ease-in-out", animationFillMode: "both", }, success: { animationName: `${fadeIn}, ${pop}`, animationDuration: `${duration.default}, ${duration.default}`, animationTimingFunction: "ease-in-out, ease-in-out", // eslint-disable-next-line @stylexjs/valid-styles animationFillMode: "both, both", // TODO: Issue? }, }); /** * A simple alert banner. * * Inspired by: * https://mui.com/material-ui/react-alert/ */ export interface AlertProps extends PropsWithChildren { severity: "error" | "warning" | "info" | "success"; // TODO: title?: string; icon?: React.ReactNode; fadeIn?: boolean; /** * When true, the alert will scroll into view when it is rendered. */ scrollIntoView?: boolean; } //#region Icons // These icons were optimized using SVGOMG and originate from our internal icon set: https://jakearchibald.github.io/svgomg const styleXIconProps = stylex.props(styles.icon); const ErrorIcon = memo(() => ( // biome-ignore lint/a11y/noSvgWithoutTitle: :nopers: )); const WarningIcon = memo(() => ( // biome-ignore lint/a11y/noSvgWithoutTitle: :nopers: )); const InformationIcon = memo(() => ( // biome-ignore lint/a11y/noSvgWithoutTitle: :nopers: )); const SuccessIcon = memo(() => ( // biome-ignore lint/a11y/noSvgWithoutTitle: :nopers: )); //#endregion const iconMap = { error: , warning: , info: , success: , }; const ariaLiveMap = { error: "assertive", warning: "polite", info: undefined, // Same as "off" success: undefined, // Same as "off" } as const; // TODO: More a11ly according to https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Roles/alert_role export default memo(function Alert(props: AlertProps) { const divRef = useRef(null); useEffect(() => { if (props.scrollIntoView) { divRef.current?.scrollIntoView({ behavior: "smooth" }); } }, [props.scrollIntoView]); return (
{props.icon ?? iconMap[props.severity]}
{props.children}
); });