-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
567aa8a
commit 3cb9899
Showing
7 changed files
with
347 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"editor.defaultFormatter": "biomejs.biome" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
{ | ||
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json", | ||
"organizeImports": { | ||
"enabled": true | ||
}, | ||
"formatter": { | ||
"enabled": true, | ||
"lineWidth": 100, | ||
"indentStyle": "space", | ||
"indentWidth": 2 | ||
}, | ||
"javascript": { | ||
"formatter": { | ||
"quoteStyle": "double", | ||
"semicolons": "asNeeded", | ||
"arrowParentheses": "asNeeded", | ||
"bracketSpacing": true, | ||
"trailingComma": "all" | ||
} | ||
}, | ||
"json": { | ||
"parser": { | ||
"allowComments": false, | ||
"allowTrailingCommas": true | ||
} | ||
}, | ||
"vcs": { | ||
"enabled": true, | ||
"clientKind": "git", | ||
"useIgnoreFile": true | ||
}, | ||
"linter": { | ||
"enabled": true, | ||
"rules": { | ||
"recommended": true, | ||
"suspicious": { | ||
"noExplicitAny": "off", | ||
"noArrayIndexKey": "off", | ||
"noConfusingLabels": "off", | ||
"noShadowRestrictedNames": "off" | ||
}, | ||
"security": { | ||
"noDangerouslySetInnerHtml": "off" | ||
}, | ||
"a11y": { | ||
"useAnchorContent": "off" | ||
}, | ||
"nursery": { | ||
"useGroupedTypeImport": "error" | ||
} | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,229 @@ | ||
"use client" | ||
|
||
import { Slot } from "@radix-ui/react-slot" | ||
import { type VariantProps, cx } from "cva" | ||
|
||
import { | ||
ComponentPropsWithoutRef, | ||
ElementRef, | ||
HTMLAttributes, | ||
MouseEventHandler, | ||
ReactElement, | ||
ReactNode, | ||
forwardRef, | ||
useCallback, | ||
useState, | ||
} from "react" | ||
import { IconClose } from "../../icons/IconClose" | ||
import { isReactElement } from "../../shared" | ||
import { Affixable } from "../../utils/Affixable" | ||
import { Action } from "../Action" | ||
import { Button } from "../Button" | ||
import { | ||
alertAffixVariants, | ||
alertRootVariants, | ||
alertTitleVariants, | ||
alertVariants, | ||
} from "./Alert.variants" | ||
|
||
/* ---------------------------------- Types --------------------------------- */ | ||
type ClosableProps = { | ||
/** | ||
* Is the alert closable? If true, a close icon will be displayed. | ||
* @default true | ||
*/ | ||
closable: true | ||
|
||
/** | ||
* An optional callback function to be called when the close icon is clicked. | ||
* This can be used to handle the removal of the tag. | ||
* If provided, the close icon will be displayed. | ||
*/ | ||
onClose?: MouseEventHandler<HTMLButtonElement> | ||
} | ||
|
||
type NotClosableProps = { | ||
/** | ||
* Is the alert closable? If true, a close button will be displayed and | ||
* when clicked on it will hide the alert element | ||
* @default true | ||
*/ | ||
closable?: false | ||
|
||
/** | ||
* An optional callback function to be called when the close button is clicked. | ||
* Requires the `closable` prop to be set to `true`. | ||
*/ | ||
onClose?: never | ||
} | ||
|
||
export type AlertProps = Omit<HTMLAttributes<HTMLDivElement>, "title" | "prefix"> & | ||
VariantProps<typeof alertVariants> & { | ||
/** | ||
* The slot to be rendered prefix the description. | ||
* This can be used to render an icon | ||
* or any other element prefix the description. Also accepts a string, | ||
* number, or any valid React element. | ||
* If the `prefix` prop is omitted, the default icon will be displayed. | ||
* | ||
* @example | ||
* // Display an alert with icon | ||
* <Alert prefix={<SuccessIcon />} /> | ||
*/ | ||
prefix?: ReactNode | ||
|
||
/** | ||
* The slot to be rendered suffix the description. | ||
* This can be a string, number or any valid React element. | ||
* If omitted, it will not be displayed. | ||
* | ||
* @example | ||
* // Display an alert with button | ||
* <Alert suffix={<Button size="sm">Save</Button>} /> | ||
*/ | ||
suffix?: ReactNode | ||
|
||
/** | ||
* The title to display within the Alert component. | ||
* This can be a string, number or any valid React element. | ||
* If omitted, no title will be displayed. | ||
* If a string is provided, it will be wrapped in an <AlertTitle /> component. | ||
* If a React element is provided, it will be rendered as-is. | ||
*/ | ||
title?: ReactNode | ||
} & (ClosableProps | NotClosableProps) | ||
|
||
/* ------------------------------- Components ------------------------------- */ | ||
export const AlertBase = forwardRef<HTMLDivElement, AlertProps>( | ||
( | ||
{ | ||
className, | ||
suffix, | ||
prefix, | ||
closable, | ||
theme, | ||
variant = "inline", | ||
children, | ||
title, | ||
onClose, | ||
...props | ||
}, | ||
ref, | ||
) => { | ||
const [visible, setVisible] = useState(true) | ||
|
||
/** | ||
* Handle the close event. | ||
* @param event - The event object | ||
*/ | ||
const handleClose = useCallback( | ||
(event: React.MouseEvent<HTMLButtonElement>) => { | ||
// Do not close if the event is prevented by the onClose callback | ||
if (!event.defaultPrevented) { | ||
setVisible(false) | ||
} | ||
|
||
if (onClose) { | ||
onClose(event) | ||
} | ||
}, | ||
[onClose], | ||
) | ||
|
||
if (!visible) { | ||
return null | ||
} | ||
|
||
return ( | ||
<AlertRoot ref={ref} className={cx(alertVariants({ variant, theme }), className)} {...props}> | ||
<Affixable variants={alertAffixVariants}>{prefix}</Affixable> | ||
|
||
<div | ||
className={cx( | ||
"flex grow flex-col items-start", | ||
variant === "expanded" && "items-start gap-3 px-2", | ||
variant === "inline" && "px-3 sm:flex-row sm:items-center sm:gap-2", | ||
variant === "inline" && closable && "pr-1", | ||
)} | ||
> | ||
<div | ||
className={cx( | ||
"flex grow flex-col items-start", | ||
variant === "expanded" && "items-start", | ||
variant === "inline" && "sm:flex-row sm:items-center sm:gap-2", | ||
)} | ||
> | ||
{title && <AlertTitle theme={theme}>{title}</AlertTitle>} | ||
{children && <AlertDescription>{children}</AlertDescription>} | ||
</div> | ||
|
||
<Affixable variants={alertAffixVariants} className="mt-3 sm:ml-auto sm:mt-0"> | ||
{suffix} | ||
</Affixable> | ||
</div> | ||
|
||
{closable && ( | ||
<AlertCloseButton className={cx(variant === "inline" && "mr-1")} onClick={handleClose} /> | ||
)} | ||
</AlertRoot> | ||
) | ||
}, | ||
) | ||
|
||
/* Root */ | ||
export const AlertRoot = forwardRef<HTMLDivElement, HTMLAttributes<HTMLDivElement>>( | ||
({ className, children, ...props }, ref) => { | ||
return ( | ||
<div ref={ref} className={cx(alertRootVariants({ className }))} role="alert" {...props}> | ||
{children} | ||
</div> | ||
) | ||
}, | ||
) | ||
|
||
/* Title */ | ||
export const AlertTitle = forwardRef< | ||
HTMLParagraphElement, | ||
HTMLAttributes<HTMLParagraphElement> & VariantProps<typeof alertTitleVariants> | ||
>(({ className, theme, children, ...props }, ref) => { | ||
const Component = isReactElement(children) ? Slot : "p" | ||
|
||
return ( | ||
<Component ref={ref} className={cx(alertTitleVariants({ theme }), className)} {...props}> | ||
{children} | ||
</Component> | ||
) | ||
}) | ||
|
||
/* Description */ | ||
export const AlertDescription = forwardRef< | ||
HTMLParagraphElement, | ||
HTMLAttributes<HTMLParagraphElement> | ||
>(({ className, children, ...props }, ref) => { | ||
const Component = isReactElement(children) ? Slot : "p" | ||
|
||
return ( | ||
<Component ref={ref} className={cx("text-start", className)} {...props}> | ||
{children} | ||
</Component> | ||
) | ||
}) | ||
|
||
/* CloseButton */ | ||
export const AlertCloseButton = forwardRef< | ||
ElementRef<typeof Button>, | ||
ComponentPropsWithoutRef<typeof Button> | ||
>(({ children, ...props }, ref) => { | ||
const renderCloseIcon = (children: ReactNode): ReactElement<HTMLElement> => { | ||
return isReactElement(children) ? children : <IconClose aria-label="Close" /> | ||
} | ||
|
||
return <Action ref={ref} prefix={renderCloseIcon(children)} {...props} /> | ||
}) | ||
|
||
export const Alert = Object.assign(AlertBase, { | ||
Root: AlertRoot, | ||
CloseButton: AlertCloseButton, | ||
Description: AlertDescription, | ||
Title: AlertTitle, | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { cva } from "../../shared" | ||
|
||
export const alertRootVariants = cva({ | ||
base: "flex items-start bg-gray-100 text-sm/relaxed", | ||
}) | ||
|
||
export const alertVariants = cva({ | ||
variants: { | ||
variant: { | ||
inline: "rounded-lg px-3 py-3 sm:items-center", | ||
expanded: "gap-1 rounded-r-lg border-l-2 p-4 pl-4", | ||
}, | ||
theme: { | ||
default: "border-gray-200 text-gray-600", | ||
info: "border-blue bg-blue-lighter text-blue-darker", | ||
success: "border-green bg-green-lighter text-green-darker", | ||
error: "border-red bg-red-lighter text-red-darker", | ||
warning: "border-yellow bg-yellow-lighter text-yellow-darker", | ||
}, | ||
}, | ||
defaultVariants: { | ||
variant: "inline", | ||
theme: "default", | ||
}, | ||
}) | ||
|
||
export const alertTitleVariants = cva({ | ||
base: "text-start font-medium", | ||
variants: { | ||
theme: { | ||
default: "text-gray-900", | ||
info: "text-blue-darker", | ||
success: "text-green-darker", | ||
error: "text-red-darker", | ||
warning: "text-yellow-darker", | ||
}, | ||
}, | ||
defaultVariants: { | ||
theme: "default", | ||
}, | ||
}) | ||
|
||
export const alertIconVariants = cva({ | ||
variants: { | ||
theme: { | ||
default: "text-gray-400", | ||
info: "text-blue", | ||
success: "text-green", | ||
error: "text-red", | ||
warning: "text-yellow", | ||
}, | ||
}, | ||
defaultVariants: { | ||
theme: "default", | ||
}, | ||
}) | ||
|
||
export const alertAffixVariants = cva({ | ||
base: "shrink-0", | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./Alert" |