Updates
This commit is contained in:
@@ -18,7 +18,7 @@ export default function Border({ spacing, ...props }: TWUI_BORDER_PROPS) {
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"relative flex items-center gap-2 border rounded",
|
||||
"border-slate-300 dark:border-white/20",
|
||||
"border-slate-300 dark:border-white/10",
|
||||
spacing
|
||||
? spacing == "normal"
|
||||
? "px-3 py-2"
|
||||
|
||||
@@ -23,7 +23,7 @@ export default function Breadcrumbs() {
|
||||
});
|
||||
|
||||
pathLinks.forEach((linkText, index, array) => {
|
||||
if (!linkText?.match(/./) || index == 1) {
|
||||
if (!linkText?.match(/./)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ export default function Breadcrumbs() {
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="gap-4">
|
||||
<Row className="gap-4 flex-nowrap whitespace-nowrap overflow-x-auto w-full">
|
||||
{links.map((linkObject, index, array) => {
|
||||
if (index === links.length - 1) {
|
||||
return (
|
||||
|
||||
@@ -3,7 +3,11 @@ import { twMerge } from "tailwind-merge";
|
||||
|
||||
/**
|
||||
* # General Card
|
||||
* @className_wrapper twui-card
|
||||
* @className twui-card
|
||||
* @className twui-card-link
|
||||
*
|
||||
* @info use the classname `nested-link` to prevent the card from being clickable when
|
||||
* a link (or the target element with this calss) inside the card is clicked.
|
||||
*/
|
||||
export default function Card({
|
||||
href,
|
||||
@@ -24,7 +28,9 @@ export default function Card({
|
||||
className={twMerge(
|
||||
"flex flex-row items-center p-4 rounded bg-white dark:bg-white/10",
|
||||
"border border-slate-200 dark:border-white/10 border-solid",
|
||||
href ? "hover:bg-slate-100 dark:hover:bg-white/30" : "",
|
||||
href
|
||||
? "hover:bg-slate-100 dark:hover:bg-white/30 hover:border-slate-400 dark:hover:border-white/20"
|
||||
: "",
|
||||
"twui-card",
|
||||
props.className
|
||||
)}
|
||||
@@ -35,7 +41,26 @@ export default function Card({
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
<a href={href} {...linkProps}>
|
||||
<a
|
||||
href={href}
|
||||
{...linkProps}
|
||||
onClick={(e) => {
|
||||
const targetEl = e.target as HTMLElement;
|
||||
if (targetEl.closest(".nested-link")) {
|
||||
e.preventDefault();
|
||||
} else if (e.ctrlKey) {
|
||||
window.open(href, "_blank");
|
||||
} else {
|
||||
window.location.href = href;
|
||||
}
|
||||
linkProps?.onClick?.(e);
|
||||
}}
|
||||
className={twMerge(
|
||||
"cursor-pointer",
|
||||
"twui-card-link",
|
||||
linkProps?.className
|
||||
)}
|
||||
>
|
||||
{component}
|
||||
</a>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
import React, {
|
||||
DetailedHTMLProps,
|
||||
HTMLAttributes,
|
||||
PropsWithChildren,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const TWUIDropdownContentPositions = [
|
||||
"left",
|
||||
"right",
|
||||
"center",
|
||||
] as const;
|
||||
|
||||
export type TWUI_DROPDOWN_PROPS = PropsWithChildren &
|
||||
DetailedHTMLProps<HTMLAttributes<HTMLDivElement>, HTMLDivElement> & {
|
||||
target: React.ReactNode;
|
||||
contentWrapperProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
targetWrapperProps?: DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
debounce?: number;
|
||||
hoverOpen?: boolean;
|
||||
position?: (typeof TWUIDropdownContentPositions)[number];
|
||||
topOffset?: number;
|
||||
externalSetOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
|
||||
let timeout: any;
|
||||
|
||||
/**
|
||||
* # Toggle Component
|
||||
* @className_wrapper twui-dropdown-wrapper
|
||||
* @className_wrapper twui-dropdown-target
|
||||
* @className_wrapper twui-dropdown-content
|
||||
*/
|
||||
export default function Dropdown({
|
||||
contentWrapperProps,
|
||||
targetWrapperProps,
|
||||
hoverOpen,
|
||||
debounce = 500,
|
||||
target,
|
||||
position = "center",
|
||||
topOffset,
|
||||
externalSetOpen,
|
||||
...props
|
||||
}: TWUI_DROPDOWN_PROPS) {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
|
||||
const dropdownRef = React.useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleClickOutside = React.useCallback((e: MouseEvent) => {
|
||||
const targetEl = e.target as HTMLElement;
|
||||
const closestWrapper = targetEl.closest(".twui-dropdown-wrapper");
|
||||
|
||||
if (!closestWrapper) {
|
||||
externalSetOpen?.(false);
|
||||
return setOpen(false);
|
||||
}
|
||||
if (closestWrapper && closestWrapper !== dropdownRef.current) {
|
||||
externalSetOpen?.(false);
|
||||
return setOpen(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
React.useEffect(() => {
|
||||
document.addEventListener("click", handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener("click", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-col items-center relative",
|
||||
"twui-dropdown-wrapper",
|
||||
props.className
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
if (!hoverOpen) return;
|
||||
window.clearTimeout(timeout);
|
||||
externalSetOpen?.(true);
|
||||
setOpen(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!hoverOpen) return;
|
||||
timeout = setTimeout(() => {
|
||||
externalSetOpen?.(false);
|
||||
setOpen(false);
|
||||
}, debounce);
|
||||
}}
|
||||
onBlur={() => {
|
||||
window.clearTimeout(timeout);
|
||||
}}
|
||||
ref={dropdownRef}
|
||||
>
|
||||
<div
|
||||
onClick={() => {
|
||||
externalSetOpen?.(!open);
|
||||
setOpen(!open);
|
||||
}}
|
||||
className={twMerge(
|
||||
"cursor-pointer",
|
||||
"twui-dropdown-target",
|
||||
targetWrapperProps?.className
|
||||
)}
|
||||
>
|
||||
{target}
|
||||
</div>
|
||||
|
||||
<div
|
||||
{...contentWrapperProps}
|
||||
className={twMerge(
|
||||
"absolute z-10",
|
||||
position == "left"
|
||||
? "left-0"
|
||||
: position == "right"
|
||||
? "right-0"
|
||||
: "",
|
||||
open ? "flex" : "hidden",
|
||||
"twui-dropdown-content",
|
||||
contentWrapperProps?.className
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
if (!hoverOpen) return;
|
||||
window.clearTimeout(timeout);
|
||||
}}
|
||||
onBlur={() => {
|
||||
if (!hoverOpen) return;
|
||||
window.clearTimeout(timeout);
|
||||
}}
|
||||
style={{
|
||||
top: `calc(100% + ${topOffset || 0}px)`,
|
||||
...contentWrapperProps?.style,
|
||||
}}
|
||||
>
|
||||
{props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -8,6 +8,7 @@ type Props = DetailedHTMLProps<
|
||||
HTMLDivElement
|
||||
> & {
|
||||
target: React.ReactNode;
|
||||
targetRef?: React.MutableRefObject<HTMLDivElement | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -15,11 +16,12 @@ type Props = DetailedHTMLProps<
|
||||
* @className_wrapper twui-modal-root
|
||||
* @className_wrapper twui-modal
|
||||
*/
|
||||
export default function Modal({ target, ...props }: Props) {
|
||||
export default function Modal({ target, targetRef, ...props }: Props) {
|
||||
const [wrapper, setWrapper] = React.useState<HTMLDivElement | null>(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const wrapperEl = document.createElement("div");
|
||||
|
||||
wrapperEl.className = twMerge(
|
||||
"fixed z-[200000] top-0 left-0 w-screen h-screen",
|
||||
"flex flex-col items-center justify-center",
|
||||
@@ -57,6 +59,7 @@ export default function Modal({ target, ...props }: Props) {
|
||||
const root = createRoot(wrapper);
|
||||
root.render(modalEl);
|
||||
}}
|
||||
ref={targetRef as any}
|
||||
>
|
||||
{target}
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export default function Paper({
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center p-4 rounded bg-white dark:bg-white/10",
|
||||
"flex flex-col items-start p-4 rounded bg-white dark:bg-white/10 gap-4",
|
||||
"border border-slate-200 dark:border-white/10 border-solid w-full",
|
||||
"twui-paper",
|
||||
props.className
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Input from "../form/Input";
|
||||
import Input, { InputProps } from "../form/Input";
|
||||
import Button from "../layout/Button";
|
||||
import Row from "../layout/Row";
|
||||
import { Search as SearchIcon } from "lucide-react";
|
||||
@@ -11,20 +11,13 @@ import React, {
|
||||
|
||||
let timeout: any;
|
||||
|
||||
export type SearchProps = DetailedHTMLProps<
|
||||
export type SearchProps<KeyType extends string> = DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
dispatch?: (value?: string) => void;
|
||||
delay?: number;
|
||||
inputProps?: DetailedHTMLProps<
|
||||
InputHTMLAttributes<HTMLInputElement>,
|
||||
HTMLInputElement
|
||||
> &
|
||||
DetailedHTMLProps<
|
||||
TextareaHTMLAttributes<HTMLTextAreaElement>,
|
||||
HTMLTextAreaElement
|
||||
>;
|
||||
inputProps?: InputProps<KeyType>;
|
||||
buttonProps?: DetailedHTMLProps<
|
||||
React.ButtonHTMLAttributes<HTMLButtonElement>,
|
||||
HTMLButtonElement
|
||||
@@ -37,13 +30,13 @@ export type SearchProps = DetailedHTMLProps<
|
||||
* @className_circle twui-search-input
|
||||
* @className_circle twui-search-button
|
||||
*/
|
||||
export default function Search({
|
||||
export default function Search<KeyType extends string>({
|
||||
dispatch,
|
||||
delay = 500,
|
||||
inputProps,
|
||||
buttonProps,
|
||||
...props
|
||||
}: SearchProps) {
|
||||
}: SearchProps<KeyType>) {
|
||||
const [input, setInput] = React.useState("");
|
||||
|
||||
React.useEffect(() => {
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import { LucideProps, Star } from "lucide-react";
|
||||
import React, {
|
||||
DetailedHTMLProps,
|
||||
ForwardRefExoticComponent,
|
||||
HTMLAttributes,
|
||||
RefAttributes,
|
||||
} from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
type StarProps = {
|
||||
total?: number;
|
||||
value?: number;
|
||||
size?: number;
|
||||
starProps?: LucideProps;
|
||||
allowRating?: boolean;
|
||||
setValueExternal?: React.Dispatch<React.SetStateAction<number>>;
|
||||
};
|
||||
|
||||
export type TWUI_STAR_RATING_PROPS = DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> &
|
||||
StarProps;
|
||||
|
||||
let timeout: any;
|
||||
|
||||
/**
|
||||
* # Star Rating Component
|
||||
* @className_wrapper twui-star-rating
|
||||
*/
|
||||
export default function StarRating({
|
||||
total = 5,
|
||||
value = 0,
|
||||
size,
|
||||
starProps,
|
||||
allowRating,
|
||||
setValueExternal,
|
||||
...props
|
||||
}: TWUI_STAR_RATING_PROPS) {
|
||||
const totalArray = Array(total).fill(null);
|
||||
|
||||
const [finalValue, setFinalValue] = React.useState(value);
|
||||
const [selectedStarValue, setSelectedStarValue] = React.useState(value);
|
||||
|
||||
const starClicked = React.useRef(false);
|
||||
const sectionHovered = React.useRef(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
window.clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
setValueExternal?.(finalValue);
|
||||
}, 500);
|
||||
}, [selectedStarValue]);
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"flex flex-row items-center gap-0 -ml-[2px]",
|
||||
"twui-star-rating",
|
||||
props.className
|
||||
)}
|
||||
onMouseEnter={() => {
|
||||
sectionHovered.current = true;
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
sectionHovered.current = false;
|
||||
}}
|
||||
>
|
||||
{totalArray.map((_, index) => {
|
||||
return (
|
||||
<StarComponent
|
||||
{...{
|
||||
total,
|
||||
value,
|
||||
size,
|
||||
starProps,
|
||||
index,
|
||||
allowRating,
|
||||
finalValue,
|
||||
setFinalValue,
|
||||
starClicked,
|
||||
selectedStarValue,
|
||||
sectionHovered,
|
||||
setSelectedStarValue,
|
||||
}}
|
||||
key={index}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function StarComponent({
|
||||
value = 0,
|
||||
size = 20,
|
||||
starProps,
|
||||
index,
|
||||
allowRating,
|
||||
finalValue,
|
||||
setFinalValue,
|
||||
starClicked,
|
||||
sectionHovered,
|
||||
setSelectedStarValue,
|
||||
selectedStarValue,
|
||||
}: StarProps & {
|
||||
index: number;
|
||||
finalValue: number;
|
||||
setFinalValue: React.Dispatch<React.SetStateAction<number>>;
|
||||
setSelectedStarValue: React.Dispatch<React.SetStateAction<number>>;
|
||||
starClicked: React.MutableRefObject<boolean>;
|
||||
sectionHovered: React.MutableRefObject<boolean>;
|
||||
selectedStarValue: number;
|
||||
}) {
|
||||
const isActive = index < finalValue;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={twMerge("p-[2px]", allowRating && "cursor-pointer")}
|
||||
onMouseEnter={() => {
|
||||
if (!allowRating) return;
|
||||
|
||||
setFinalValue(index + 1);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
if (!allowRating) return;
|
||||
|
||||
setTimeout(() => {
|
||||
if (sectionHovered.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!starClicked.current) {
|
||||
setFinalValue(0);
|
||||
}
|
||||
|
||||
if (selectedStarValue) {
|
||||
setFinalValue(selectedStarValue);
|
||||
}
|
||||
}, 200);
|
||||
}}
|
||||
onClick={() => {
|
||||
if (!allowRating) return;
|
||||
|
||||
starClicked.current = true;
|
||||
setSelectedStarValue(index + 1);
|
||||
}}
|
||||
>
|
||||
<Star
|
||||
size={size}
|
||||
className={twMerge(
|
||||
"text-slate-300 dark:text-white/20",
|
||||
isActive &&
|
||||
"text-orange-500 dark:text-orange-400 fill-orange-500 dark:fill-orange-400",
|
||||
// allowRating &&
|
||||
// "hover:text-orange-500 hover:dark:text-orange-400 hover:fill-orange-500 hover:dark:fill-orange-400",
|
||||
starProps?.className
|
||||
)}
|
||||
{...starProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import Border from "./Border";
|
||||
import Stack from "../layout/Stack";
|
||||
import Row from "../layout/Row";
|
||||
import Span from "../layout/Span";
|
||||
|
||||
export type TWUITabsObject = {
|
||||
title: string;
|
||||
value: string;
|
||||
content: React.ReactNode;
|
||||
defaultActive?: boolean;
|
||||
};
|
||||
|
||||
export type TWUI_TOGGLE_PROPS = React.ComponentProps<typeof Stack> & {
|
||||
tabsContentArray: TWUITabsObject[];
|
||||
tabsBorderProps?: React.ComponentProps<typeof Border>;
|
||||
tabsButtonsWrapperProps?: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
>;
|
||||
centered?: boolean;
|
||||
debounce?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* # Tabs Component
|
||||
* @className twui-tabs-wrapper
|
||||
* @className twui-tab-buttons
|
||||
* @className twui-tab-button-active
|
||||
* @className twui-tab-buttons-wrapper
|
||||
*/
|
||||
export default function Tabs({
|
||||
tabsContentArray,
|
||||
tabsBorderProps,
|
||||
tabsButtonsWrapperProps,
|
||||
centered,
|
||||
debounce = 100,
|
||||
...props
|
||||
}: TWUI_TOGGLE_PROPS) {
|
||||
const values = tabsContentArray.map((obj) => obj.value);
|
||||
|
||||
const [activeValue, setActiveValue] = React.useState(
|
||||
tabsContentArray.find((ctn) => ctn.defaultActive)?.value ||
|
||||
values[0] ||
|
||||
undefined
|
||||
);
|
||||
|
||||
const targetContent = tabsContentArray.find(
|
||||
(ctn) => ctn.value == activeValue
|
||||
);
|
||||
|
||||
return (
|
||||
<Stack
|
||||
{...props}
|
||||
className={twMerge("w-full", "twui-tabs-wrapper", props.className)}
|
||||
>
|
||||
<div
|
||||
{...tabsButtonsWrapperProps}
|
||||
className={twMerge(
|
||||
"w-full",
|
||||
"twui-tab-buttons-wrapper",
|
||||
tabsButtonsWrapperProps?.className
|
||||
)}
|
||||
>
|
||||
<Border className="p-0 w-full" {...tabsBorderProps}>
|
||||
<Row
|
||||
className={twMerge(
|
||||
"gap-0 items-stretch w-full",
|
||||
centered && "justify-center"
|
||||
)}
|
||||
>
|
||||
{values.map((value, index) => {
|
||||
const targetObject = tabsContentArray.find(
|
||||
(ctn) => ctn.value == value
|
||||
);
|
||||
|
||||
const isActive = value == activeValue;
|
||||
|
||||
return (
|
||||
<span
|
||||
className={twMerge(
|
||||
"px-6 py-2 rounded -ml-[1px]",
|
||||
isActive
|
||||
? "bg-blue-500 text-white outline-none twui-tab-button-active"
|
||||
: "text-slate-400 dark:text-white/40 hover:text-slate-800 dark:hover:text-white" +
|
||||
" cursor-pointer",
|
||||
"twui-tab-buttons"
|
||||
)}
|
||||
onClick={() => {
|
||||
setActiveValue(undefined);
|
||||
setTimeout(() => {
|
||||
setActiveValue(value);
|
||||
}, debounce);
|
||||
}}
|
||||
key={index}
|
||||
>
|
||||
{targetObject?.title}
|
||||
</span>
|
||||
);
|
||||
})}
|
||||
</Row>
|
||||
</Border>
|
||||
</div>
|
||||
{targetContent?.content}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export type TWUITabsObject = {
|
||||
title: string;
|
||||
value: string;
|
||||
content: React.ReactNode;
|
||||
defaultActive?: boolean;
|
||||
};
|
||||
|
||||
export type TWUI_TOGGLE_PROPS = PropsWithChildren &
|
||||
React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
color?: "normal" | "secondary" | "error" | "success" | "gray";
|
||||
variant?: "normal" | "outlined" | "ghost";
|
||||
};
|
||||
|
||||
/**
|
||||
* # Tabs Component
|
||||
* @className twui-tag
|
||||
*/
|
||||
export default function Tag({
|
||||
color,
|
||||
variant,
|
||||
children,
|
||||
...props
|
||||
}: TWUI_TOGGLE_PROPS) {
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"text-xs px-2 py-0.5 rounded-full outline outline-0",
|
||||
color == "secondary"
|
||||
? "bg-violet-600 outline-violet-600"
|
||||
: color == "success"
|
||||
? "bg-emerald-700 outline-emerald-700"
|
||||
: color == "error"
|
||||
? "bg-orange-700 outline-orange-700"
|
||||
: color == "gray"
|
||||
? "bg-slate-100 outline-slate-200 dark:bg-white/10 dark:outline-white/20 text-slate-500 dark:text-white"
|
||||
: "bg-blue-600 outline-blue-600",
|
||||
variant == "outlined"
|
||||
? "!bg-transparent outline-1 " +
|
||||
(color == "secondary"
|
||||
? "text-violet-600"
|
||||
: color == "success"
|
||||
? "text-emerald-700 dark:text-emerald-400"
|
||||
: color == "error"
|
||||
? "text-orange-700"
|
||||
: color == "gray"
|
||||
? "text-slate-700 dark:text-white/80"
|
||||
: "text-blue-600")
|
||||
: variant == "ghost"
|
||||
? "!bg-transparent outline-none border-none " +
|
||||
(color == "secondary"
|
||||
? "text-violet-600"
|
||||
: color == "success"
|
||||
? "text-emerald-700 dark:text-emerald-400"
|
||||
: color == "error"
|
||||
? "text-orange-700"
|
||||
: color == "gray"
|
||||
? "text-slate-700 dark:text-white/80"
|
||||
: "text-blue-600")
|
||||
: "",
|
||||
|
||||
"twui-tag",
|
||||
props.className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
import React, { DetailedHTMLProps, HTMLAttributes } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import Card from "./Card";
|
||||
import { X } from "lucide-react";
|
||||
import Span from "../layout/Span";
|
||||
|
||||
export const ToastStyles = ["normal", "success", "error"] as const;
|
||||
export const ToastColors = ToastStyles;
|
||||
|
||||
type Props = DetailedHTMLProps<
|
||||
HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & {
|
||||
open?: boolean;
|
||||
setOpen?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
closeDelay?: number;
|
||||
color?: (typeof ToastStyles)[number];
|
||||
};
|
||||
|
||||
/**
|
||||
* # Toast Component
|
||||
* @className twui-toast-root
|
||||
* @className twui-toast
|
||||
* @className twui-toast-success
|
||||
* @className twui-toast-error
|
||||
*/
|
||||
export default function Toast({
|
||||
open,
|
||||
setOpen,
|
||||
closeDelay = 4000,
|
||||
color,
|
||||
...props
|
||||
}: Props) {
|
||||
if (!open) return null;
|
||||
|
||||
const toastEl = (
|
||||
<Card
|
||||
{...props}
|
||||
className={twMerge(
|
||||
"pl-6 pr-8 py-4 bg-blue-700 dark:bg-blue-800",
|
||||
color == "success"
|
||||
? "bg-emerald-600 dark:bg-emerald-700 twui-toast-success"
|
||||
: color == "error"
|
||||
? "bg-orange-600 dark:bg-orange-700 twui-toast-error"
|
||||
: "",
|
||||
props.className,
|
||||
"twui-toast"
|
||||
)}
|
||||
>
|
||||
<Span
|
||||
className={twMerge(
|
||||
"absolute top-2 right-2 z-[100] cursor-pointer"
|
||||
)}
|
||||
onClick={(e) => {
|
||||
const targetEl = e.target as HTMLElement;
|
||||
const rootWrapperEl = targetEl.closest(".twui-toast-root");
|
||||
|
||||
if (rootWrapperEl) {
|
||||
rootWrapperEl.parentElement?.removeChild(rootWrapperEl);
|
||||
setOpen?.(false);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<X size={15} />
|
||||
</Span>
|
||||
{props.children}
|
||||
</Card>
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
const wrapperEl = document.createElement("div");
|
||||
|
||||
wrapperEl.className = twMerge(
|
||||
"fixed z-[200000] bottom-10 right-10",
|
||||
"flex flex-col items-center justify-center",
|
||||
"twui-toast-root"
|
||||
);
|
||||
|
||||
document.body.appendChild(wrapperEl);
|
||||
const root = createRoot(wrapperEl);
|
||||
root.render(toastEl);
|
||||
|
||||
setTimeout(() => {
|
||||
closeToast({ wrapperEl });
|
||||
setOpen?.(false);
|
||||
}, closeDelay);
|
||||
|
||||
return function () {
|
||||
closeToast({ wrapperEl });
|
||||
};
|
||||
}, []);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
function closeToast({ wrapperEl }: { wrapperEl: HTMLDivElement | null }) {
|
||||
if (!wrapperEl) return;
|
||||
wrapperEl.parentElement?.removeChild(wrapperEl);
|
||||
}
|
||||
Reference in New Issue
Block a user