create toaster,tooltip,add icons

This commit is contained in:
yzned
2025-07-18 18:29:43 +03:00
committed by yzned
parent ca6dfe6ccb
commit d249f0d291
15 changed files with 640 additions and 38 deletions

View File

@@ -0,0 +1,320 @@
import CopyIcon from "@/icons/copy.svg?react";
import SuccessIcon from "@/icons/check-circle.svg?react";
import ErrorIcon from "@/icons/warning.svg?react";
import InfoIcon from "@/icons/info.svg?react";
import * as ToastPrimitives from "@radix-ui/react-toast";
import { type VariantProps, cva } from "class-variance-authority";
import * as React from "react";
import Close from "@/icons/x-mark.svg?react";
import { cn } from "@/lib/utils";
import clsx from "clsx";
import { useToast } from "@/lib/hooks/use-toast";
const ToastProvider = ToastPrimitives.Provider;
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className,
)}
{...props}
/>
));
ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
const toastVariants = cva(
"group min-h-[88px] min-w-[300px] pointer-events-auto relative flex w-full items-center justify-between overflow-hidden rounded-2xl p-4 pr-4 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full dark:border-neutral-800",
{
variants: {
variant: {
success: "bg-feedback-positive-100",
warning: "bg-feedback-attention-100",
error: "bg-feedback-negative-100",
info: "bg-feedback-info-100",
},
},
defaultVariants: {
variant: "success",
},
},
);
const ToastIcon = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant }) => {
if (variant === "success") {
return (
<SuccessIcon className={clsx(className, "text-feedback-positive-900")} />
);
}
if (variant === "warning") {
return (
<ErrorIcon className={clsx(className, "text-feedback-attention-900")} />
);
}
if (variant === "error") {
return (
<ErrorIcon className={clsx(className, "text-feedback-negative-900")} />
);
}
if (variant === "info") {
return <InfoIcon className={clsx(className, "text-feedback-info-900")} />;
}
});
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
);
});
Toast.displayName = ToastPrimitives.Root.displayName;
const ToastCopy = ({
text,
href,
toastAction = () => {
if (href) {
navigator.clipboard.writeText(href);
}
},
}: {
text: string;
href?: string;
toastAction?: () => void;
}) => (
<button
onClick={toastAction}
type="button"
className="flex flex-row gap-2 group mb-2"
>
<div className="flex flex-col items-center justify-center size-6 bg-[#888787] rounded-full">
<CopyIcon className="size-[10px]" />
</div>
<span className="text-[#888787] hover:text-white ease-in-out duration-100 text-base font-medium font-['Articulat CF'] leading-normal">
{text}
</span>
</button>
);
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border border-neutral-200 bg-transparent px-3 text-sm font-medium transition-colors hover:bg-neutral-100 focus:outline-none focus:ring-1 focus:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-neutral-100/40 group-[.destructive]:hover:border-red-500/30 group-[.destructive]:hover:bg-red-500 group-[.destructive]:hover:text-neutral-50 group-[.destructive]:focus:ring-red-500 dark:border-neutral-800 dark:hover:bg-neutral-800 dark:focus:ring-neutral-300 dark:group-[.destructive]:border-neutral-800/40 dark:group-[.destructive]:hover:border-red-900/30 dark:group-[.destructive]:hover:bg-red-900 dark:group-[.destructive]:hover:text-neutral-50 dark:group-[.destructive]:focus:ring-red-900",
className,
)}
{...props}
/>
));
ToastAction.displayName = ToastPrimitives.Action.displayName;
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn("my-auto mx-0 rounded-md cursor-pointer", className)}
toast-close=""
{...props}
>
<Close className="size-5 text-[#C0C0C0]" />
</ToastPrimitives.Close>
));
ToastClose.displayName = ToastPrimitives.Close.displayName;
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn(
"text-text-light-900 text-[18px] leading-[120%] sm:text-lg font-medium font-articulat leading-normal [&+div]:text-xs",
className,
)}
{...props}
/>
));
ToastTitle.displayName = ToastPrimitives.Title.displayName;
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn(
"text-text-light-500 text-[14px] font-medium font-articulat leading-normal mb-2",
className,
)}
{...props}
/>
));
const ToastTimeline = ({
duration,
isPaused,
variant,
}: {
duration: number;
isPaused: boolean;
variant?: "error" | "success" | "warning" | "info" | null;
}) => {
return (
<div>
<div
className={cn(
"absolute bottom-2 left-4 h-[3px] w-[90%] rounded bg-fill-100",
)}
/>
<div
data-variant={variant}
className={cn(
"absolute bottom-2 left-4 h-[3px] w-[90%] data-[variant=error]:bg-feedback-negative-900 rounded data-[variant=success]:bg-feedback-positive-900 data-[variant=warning]:bg-feedback-attention-900 data-[variant=info]:bg-feedback-info-900 toast-progress-animation",
)}
style={{
animationDuration: `${duration}ms`,
animationPlayState: isPaused ? "paused" : "running",
}}
/>
</div>
);
};
ToastDescription.displayName = ToastPrimitives.Description.displayName;
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
type ToastActionElement = React.ReactElement<typeof ToastAction>;
export function Toaster() {
const { toasts, isPaused, setIsPaused } = useToast();
React.useEffect(() => {
const handleFocus = () => {
setIsPaused(false);
};
const handleBlur = () => {
setIsPaused(true);
};
window.addEventListener("focus", handleFocus);
window.addEventListener("blur", handleBlur);
return () => {
window.removeEventListener("focus", handleFocus);
window.removeEventListener("blur", handleBlur);
};
}, [setIsPaused]);
return (
<ToastProvider>
{toasts.map(
({
id,
title,
description,
action,
copy,
href,
toastAction,
duration = 5000,
withTimeline,
...props
}) => (
<Toast
key={id}
duration={duration}
{...props}
onMouseEnter={() => setIsPaused(true)}
onMouseLeave={() => setIsPaused(false)}
>
<ToastIcon className="size-12 mr-4" variant={props.variant} />
<div className="grid gap-2 w-full">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>
{/* {props.variant === "errorWithCopy" && (
<p className="text-[16px] mb-2">
To solve problems, contact us <br />
via{" "}
<a
href={"https://discord.gg/cytonic"}
target="_blank"
rel="noreferrer"
className="text-brand relative"
>
Discord
<div className="h-[1px] w-[56px] bg-brand absolute left-0" />
</a>{" "}
and send the error text
</p>
)} */}
{description}
</ToastDescription>
)}
{copy && (
<ToastCopy text={copy} href={href} toastAction={toastAction} />
)}
</div>
{action}
<ToastClose />
{withTimeline && (
<ToastTimeline
duration={duration}
isPaused={isPaused}
variant={props.variant}
/>
)}
</Toast>
),
)}
<ToastViewport />
</ToastProvider>
);
}
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
ToastIcon,
ToastCopy,
ToastTimeline,
};

View File

@@ -0,0 +1,59 @@
import * as React from "react";
import * as TooltipPrimitive from "@radix-ui/react-tooltip";
import { cn } from "@/lib/utils";
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
);
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
);
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />;
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-fill-900 text-text-light-200 font-[600] text-[18px] text animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-balance",
className,
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-primary fill-primary z-50 size-2.5 translate-y-[calc(-50%)] rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
);
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider };