Update and refine alert modals
This commit is contained in:
@@ -1,52 +1,117 @@
|
||||
"use client";
|
||||
|
||||
/**
|
||||
* Figma: "Modal / Alert" (6351-14646)
|
||||
* https://www.figma.com/design/agv0VBLiBlcnSAaiAORgPR/Community-Rule-System?node-id=6351-14646
|
||||
*/
|
||||
|
||||
import { memo } from "react";
|
||||
import { AlertView } from "./Alert.view";
|
||||
import type { AlertProps } from "./Alert.types";
|
||||
|
||||
function layoutFor(
|
||||
type: NonNullable<AlertProps["type"]>,
|
||||
size: NonNullable<AlertProps["size"]>,
|
||||
): {
|
||||
containerClasses: string;
|
||||
titleClasses: string;
|
||||
descriptionClasses: string;
|
||||
} {
|
||||
if (type === "toast") {
|
||||
const padH =
|
||||
size === "s"
|
||||
? "px-[var(--space-500)]"
|
||||
: "px-[var(--space-1200)]";
|
||||
const containerClasses = `flex gap-[var(--space-300)] items-center ${padH} pb-[var(--space-500)] pt-[var(--space-400)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)] border-solid`;
|
||||
if (size === "s") {
|
||||
return {
|
||||
containerClasses,
|
||||
titleClasses:
|
||||
"font-inter text-[14px] leading-[18px] font-medium tracking-[0%]",
|
||||
descriptionClasses:
|
||||
"font-inter text-[14px] leading-[20px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
|
||||
};
|
||||
}
|
||||
return {
|
||||
containerClasses,
|
||||
titleClasses:
|
||||
"font-inter text-[18px] leading-[24px] font-medium tracking-[0%]",
|
||||
descriptionClasses:
|
||||
"font-inter text-[18px] leading-[1.3] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
|
||||
};
|
||||
}
|
||||
|
||||
if (size === "s") {
|
||||
return {
|
||||
containerClasses:
|
||||
"flex gap-[var(--space-300)] items-center p-[var(--space-300)] rounded-[var(--radius-200,8px)] border-solid",
|
||||
titleClasses:
|
||||
"font-inter text-[14px] leading-[18px] font-medium tracking-[0%]",
|
||||
descriptionClasses:
|
||||
"font-inter text-[14px] leading-[20px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
|
||||
};
|
||||
}
|
||||
return {
|
||||
containerClasses:
|
||||
"flex gap-[var(--space-300)] items-center px-[var(--space-600)] py-[var(--space-400)] rounded-[var(--radius-200,8px)] border-solid",
|
||||
titleClasses:
|
||||
"font-inter text-[16px] leading-[20px] font-medium tracking-[0%]",
|
||||
descriptionClasses:
|
||||
"font-inter text-[16px] leading-[24px] font-normal tracking-[0%] mt-[var(--spacing-scale-004)]",
|
||||
};
|
||||
}
|
||||
|
||||
const AlertContainer = memo<AlertProps>(
|
||||
({
|
||||
title,
|
||||
description,
|
||||
status: statusProp = "default",
|
||||
type: typeProp = "toast",
|
||||
size: sizeProp = "m",
|
||||
hasLeadingIcon = true,
|
||||
hasBodyText = true,
|
||||
hasTrailingIcon: hasTrailingIconProp,
|
||||
onClose,
|
||||
className = "",
|
||||
}) => {
|
||||
const status = statusProp;
|
||||
const type = typeProp;
|
||||
// Determine background and border colors based on status and type
|
||||
const size = sizeProp;
|
||||
|
||||
const getStatusStyles = () => {
|
||||
switch (status) {
|
||||
case "positive":
|
||||
return {
|
||||
background: "bg-[var(--color-kiwi-kiwi0)]",
|
||||
background:
|
||||
"bg-[var(--color-surface-invert-positive-secondary,var(--color-kiwi-kiwi0))]",
|
||||
borderColor:
|
||||
type === "toast"
|
||||
? "var(--color-border-invert-positive-primary)"
|
||||
: undefined,
|
||||
titleColor: "text-[var(--color-content-invert-primary)]",
|
||||
descriptionColor: "text-[var(--color-content-invert-secondary)]",
|
||||
descriptionColor:
|
||||
"text-[var(--color-content-invert-secondary)]",
|
||||
iconColor: "var(--color-kiwi-kiwi500)",
|
||||
closeButtonIconColor: "var(--color-content-invert-primary)",
|
||||
};
|
||||
case "warning":
|
||||
return {
|
||||
background: "bg-[var(--color-yellow-yellow0)]",
|
||||
background:
|
||||
"bg-[var(--color-surface-invert-warning-secondary,var(--color-yellow-yellow0))]",
|
||||
borderColor:
|
||||
type === "toast"
|
||||
? "var(--color-border-invert-warning-primary)"
|
||||
: undefined,
|
||||
titleColor: "text-[var(--color-content-invert-primary)]",
|
||||
descriptionColor: "text-[var(--color-content-invert-secondary)]",
|
||||
descriptionColor:
|
||||
"text-[var(--color-content-invert-secondary)]",
|
||||
iconColor: "var(--color-yellow-yellow500)",
|
||||
closeButtonIconColor: "var(--color-content-invert-primary)",
|
||||
};
|
||||
case "danger":
|
||||
return {
|
||||
background: "bg-[var(--color-red-red0)]",
|
||||
background:
|
||||
"bg-[var(--color-surface-invert-negative-secondary,var(--color-red-red0))]",
|
||||
borderColor:
|
||||
type === "toast"
|
||||
? "var(--color-border-invert-negative-primary)"
|
||||
@@ -67,18 +132,14 @@ const AlertContainer = memo<AlertProps>(
|
||||
titleColor: "text-[var(--color-content-default-primary)]",
|
||||
descriptionColor: "text-[var(--color-content-default-primary)]",
|
||||
iconColor: "var(--color-content-default-brand-primary)",
|
||||
closeButtonIconColor: "var(--color-content-default-brand-primary)",
|
||||
closeButtonIconColor:
|
||||
"var(--color-content-default-brand-primary)",
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const statusStyles = getStatusStyles();
|
||||
|
||||
const containerClasses = `flex gap-[var(--space-300)] items-center ${
|
||||
type === "toast"
|
||||
? `pb-[var(--space-500)] pt-[var(--space-400)] px-[var(--space-1200)] rounded-tl-[var(--radius-200,8px)] rounded-tr-[var(--radius-200,8px)]`
|
||||
: `px-[var(--spacing-scale-024)] py-[var(--spacing-scale-016)] rounded-[var(--radius-200,8px)]`
|
||||
} ${statusStyles.background} border-solid`;
|
||||
const layout = layoutFor(type, size);
|
||||
|
||||
const containerStyle =
|
||||
type === "toast" && statusStyles.borderColor
|
||||
@@ -88,15 +149,14 @@ const AlertContainer = memo<AlertProps>(
|
||||
}
|
||||
: undefined;
|
||||
|
||||
const titleClasses =
|
||||
type === "banner"
|
||||
? `font-inter text-[16px] leading-[20px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`
|
||||
: `font-inter text-[18px] leading-[24px] font-medium tracking-[0%] ${statusStyles.titleColor} relative shrink-0 w-full`;
|
||||
const containerClasses = `${layout.containerClasses} ${statusStyles.background}`;
|
||||
|
||||
const descriptionClasses =
|
||||
type === "banner"
|
||||
? `font-inter text-[16px] leading-[24px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`
|
||||
: `font-inter text-[18px] leading-[23.4px] font-normal tracking-[0%] ${statusStyles.descriptionColor} relative shrink-0 w-full mt-[var(--spacing-scale-004)]`;
|
||||
const titleClasses = `${layout.titleClasses} ${statusStyles.titleColor} relative shrink-0 w-full`;
|
||||
const descriptionClasses = `${layout.descriptionClasses} ${statusStyles.descriptionColor} relative shrink-0 w-full`;
|
||||
|
||||
const hasTrailingIcon =
|
||||
hasTrailingIconProp ?? Boolean(onClose);
|
||||
const showClose = hasTrailingIcon && Boolean(onClose);
|
||||
|
||||
return (
|
||||
<AlertView
|
||||
@@ -106,6 +166,7 @@ const AlertContainer = memo<AlertProps>(
|
||||
type={type}
|
||||
hasLeadingIcon={hasLeadingIcon}
|
||||
hasBodyText={hasBodyText}
|
||||
hasTrailingIcon={showClose}
|
||||
className={className}
|
||||
containerClasses={containerClasses}
|
||||
containerStyle={containerStyle}
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import type { AlertSizeValue } from "../../../../lib/propNormalization";
|
||||
|
||||
export type AlertStatusValue = "default" | "positive" | "warning" | "danger";
|
||||
|
||||
export type AlertTypeValue = "toast" | "banner";
|
||||
|
||||
export type { AlertSizeValue };
|
||||
|
||||
export interface AlertProps {
|
||||
title: string;
|
||||
description?: string;
|
||||
@@ -13,6 +17,11 @@ export interface AlertProps {
|
||||
* Alert type.
|
||||
*/
|
||||
type?: AlertTypeValue;
|
||||
/**
|
||||
* Density / typography scale (Figma Modal Alert S | M).
|
||||
* @default "m"
|
||||
*/
|
||||
size?: AlertSizeValue;
|
||||
/**
|
||||
* Whether to show the leading icon (Figma prop).
|
||||
* @default true
|
||||
@@ -23,6 +32,11 @@ export interface AlertProps {
|
||||
* @default true
|
||||
*/
|
||||
hasBodyText?: boolean;
|
||||
/**
|
||||
* Trailing dismiss control (Figma `hasTrailingIcon`).
|
||||
* When omitted, defaults to `true` when `onClose` is provided, else `false`.
|
||||
*/
|
||||
hasTrailingIcon?: boolean;
|
||||
onClose?: () => void;
|
||||
className?: string;
|
||||
}
|
||||
@@ -34,6 +48,7 @@ export interface AlertViewProps {
|
||||
type: "toast" | "banner";
|
||||
hasLeadingIcon: boolean;
|
||||
hasBodyText: boolean;
|
||||
hasTrailingIcon: boolean;
|
||||
className: string;
|
||||
containerClasses: string;
|
||||
containerStyle?: React.CSSProperties;
|
||||
|
||||
@@ -8,6 +8,7 @@ export function AlertView({
|
||||
type: _type,
|
||||
hasLeadingIcon,
|
||||
hasBodyText,
|
||||
hasTrailingIcon,
|
||||
className,
|
||||
containerClasses,
|
||||
containerStyle,
|
||||
@@ -54,40 +55,42 @@ export function AlertView({
|
||||
<p className={descriptionClasses}>{description}</p>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
palette="default"
|
||||
size="large"
|
||||
onClick={onClose}
|
||||
ariaLabel="Close alert"
|
||||
className="shrink-0 [&_svg_path]:transition-colors [&_svg_path]:duration-200 hover:[&_svg_path]:fill-[var(--color-content-default-primary)]"
|
||||
>
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{hasTrailingIcon && onClose ? (
|
||||
<Button
|
||||
buttonType="ghost"
|
||||
palette="default"
|
||||
size="large"
|
||||
onClick={onClose}
|
||||
ariaLabel="Close alert"
|
||||
className="shrink-0 [&_svg_path]:transition-colors [&_svg_path]:duration-200 hover:[&_svg_path]:fill-[var(--color-content-default-primary)]"
|
||||
>
|
||||
<mask
|
||||
id="mask0_21296_8285"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_21296_8285)">
|
||||
<path
|
||||
d="M5.33327 15.5448L4.45508 14.6666L9.12174 9.99993L4.45508 5.33327L5.33327 4.45508L9.99993 9.12174L14.6666 4.45508L15.5448 5.33327L10.8781 9.99993L15.5448 14.6666L14.6666 15.5448L9.99993 10.8781L5.33327 15.5448Z"
|
||||
fill={closeButtonIconColor}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</Button>
|
||||
<mask
|
||||
id="mask0_21296_8285"
|
||||
style={{ maskType: "alpha" }}
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="0"
|
||||
y="0"
|
||||
width="20"
|
||||
height="20"
|
||||
>
|
||||
<rect width="20" height="20" fill="#D9D9D9" />
|
||||
</mask>
|
||||
<g mask="url(#mask0_21296_8285)">
|
||||
<path
|
||||
d="M5.33327 15.5448L4.45508 14.6666L9.12174 9.99993L4.45508 5.33327L5.33327 4.45508L9.99993 9.12174L14.6666 4.45508L15.5448 5.33327L10.8781 9.99993L15.5448 14.6666L14.6666 15.5448L9.99993 10.8781L5.33327 15.5448Z"
|
||||
fill={closeButtonIconColor}
|
||||
/>
|
||||
</g>
|
||||
</svg>
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user