diff --git a/apps/storybook/package.json b/apps/storybook/package.json index 45e7516f..4fd5eb8d 100644 --- a/apps/storybook/package.json +++ b/apps/storybook/package.json @@ -85,10 +85,12 @@ "micro-packed": "^0.6.3", "multiformats": "^13.3.0", "nanostores": "0.11.3", + "next-themes": "^0.4.6", "react": "^18.3.1", "react-dom": "^18.3.1", "react-dropzone": "^14.2.9", "react-hook-form": "^7.53.0", + "sonner": "^2.0.3", "stream-browserify": "^3.0.0", "tailwind-merge": "^2.5.2", "tailwindcss": "^3.4.13", diff --git a/apps/storybook/src/components/ui/sonner.tsx b/apps/storybook/src/components/ui/sonner.tsx new file mode 100644 index 00000000..0c7e9d47 --- /dev/null +++ b/apps/storybook/src/components/ui/sonner.tsx @@ -0,0 +1,29 @@ +import { useTheme } from "next-themes"; +import { Toaster as Sonner } from "sonner"; + +type ToasterProps = React.ComponentProps; + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = "system" } = useTheme(); + + return ( + + ); +}; + +export { Toaster }; diff --git a/apps/storybook/src/components/ui/toast.tsx b/apps/storybook/src/components/ui/toast.tsx deleted file mode 100644 index 7d044d85..00000000 --- a/apps/storybook/src/components/ui/toast.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import * as ToastPrimitives from "@radix-ui/react-toast"; -import { type VariantProps, cva } from "class-variance-authority"; -import { X } from "lucide-react"; -import * as React from "react"; - -import { cn } from "#lib/utils"; - -const ToastProvider = ToastPrimitives.Provider; - -const ToastViewport = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastViewport.displayName = ToastPrimitives.Viewport.displayName; - -const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 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", - { - variants: { - variant: { - default: "border bg-background text-foreground", - destructive: - "destructive group border-destructive bg-destructive text-destructive-foreground", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -const Toast = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef & - VariantProps ->(({ className, variant, ...props }, ref) => { - return ( - - ); -}); -Toast.displayName = ToastPrimitives.Root.displayName; - -const ToastAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastAction.displayName = ToastPrimitives.Action.displayName; - -const ToastClose = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - -)); -ToastClose.displayName = ToastPrimitives.Close.displayName; - -const ToastTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastTitle.displayName = ToastPrimitives.Title.displayName; - -const ToastDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -ToastDescription.displayName = ToastPrimitives.Description.displayName; - -type ToastProps = React.ComponentPropsWithoutRef; - -type ToastActionElement = React.ReactElement; - -export { - type ToastProps, - type ToastActionElement, - ToastProvider, - ToastViewport, - Toast, - ToastTitle, - ToastDescription, - ToastClose, - ToastAction, -}; diff --git a/apps/storybook/src/components/ui/toaster.tsx b/apps/storybook/src/components/ui/toaster.tsx deleted file mode 100644 index 924bae82..00000000 --- a/apps/storybook/src/components/ui/toaster.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { useToast } from "@geist/ui-react/hooks/shadcn/use-toast"; -import { - Toast, - ToastClose, - ToastDescription, - ToastProvider, - ToastTitle, - ToastViewport, -} from "#components/ui/toast"; - -export function Toaster({ - className, -}: { - className?: string; -}) { - const { toasts } = useToast(); - - return ( - - {toasts.map(({ id, title, description, action, ...props }) => ( - -
- {title && {title}} - {description && {description}} -
- {action} - -
- ))} - -
- ); -} diff --git a/apps/storybook/src/stories/decorators/toaster.tsx b/apps/storybook/src/stories/decorators/toaster.tsx index 9e4fc7f5..d2068a35 100644 --- a/apps/storybook/src/stories/decorators/toaster.tsx +++ b/apps/storybook/src/stories/decorators/toaster.tsx @@ -1,4 +1,4 @@ -import { Toaster } from "#components/ui/toaster"; +import { Toaster } from "#components/ui/sonner"; export const withToaster = () => { return (Story: any, context: any) => { diff --git a/apps/storybook/src/stories/filecoin/upload-toast.tsx b/apps/storybook/src/stories/filecoin/upload-toast.tsx index c48d4a50..da55c3f7 100644 --- a/apps/storybook/src/stories/filecoin/upload-toast.tsx +++ b/apps/storybook/src/stories/filecoin/upload-toast.tsx @@ -1,8 +1,8 @@ -import { toast } from "@geist/ui-react/hooks/shadcn/use-toast"; import { IpfsGateway, getGatewayUrlWithCid, } from "@geist/ui-react/lib/filecoin/gateway"; +import { toast } from "sonner"; export const uploadSuccessToast = ({ cid, @@ -11,8 +11,7 @@ export const uploadSuccessToast = ({ }: { cid: string; name?: string; gateway?: IpfsGateway }) => { const url = getGatewayUrlWithCid(cid, gateway); - toast({ - title: "File uploaded", + toast.success("File uploaded", { description: (
{name && Name:} diff --git a/packages/ui-react/src/hooks/shadcn/use-toast.ts b/packages/ui-react/src/hooks/shadcn/use-toast.ts deleted file mode 100644 index b6d930a6..00000000 --- a/packages/ui-react/src/hooks/shadcn/use-toast.ts +++ /dev/null @@ -1,188 +0,0 @@ -import * as React from "react"; - -import type { ToastActionElement, ToastProps } from "#components/ui/toast"; - -const TOAST_LIMIT = 1; -const TOAST_REMOVE_DELAY = 1000000; - -type ToasterToast = ToastProps & { - id: string; - title?: React.ReactNode; - description?: React.ReactNode; - action?: ToastActionElement; -}; - -const actionTypes = { - ADD_TOAST: "ADD_TOAST", - UPDATE_TOAST: "UPDATE_TOAST", - DISMISS_TOAST: "DISMISS_TOAST", - REMOVE_TOAST: "REMOVE_TOAST", -} as const; - -let count = 0; - -function genId() { - count = (count + 1) % Number.MAX_SAFE_INTEGER; - return count.toString(); -} - -type ActionType = typeof actionTypes; - -type Action = - | { - type: ActionType["ADD_TOAST"]; - toast: ToasterToast; - } - | { - type: ActionType["UPDATE_TOAST"]; - toast: Partial; - } - | { - type: ActionType["DISMISS_TOAST"]; - toastId?: ToasterToast["id"]; - } - | { - type: ActionType["REMOVE_TOAST"]; - toastId?: ToasterToast["id"]; - }; - -interface State { - toasts: ToasterToast[]; -} - -const toastTimeouts = new Map>(); - -const addToRemoveQueue = (toastId: string) => { - if (toastTimeouts.has(toastId)) { - return; - } - - const timeout = setTimeout(() => { - toastTimeouts.delete(toastId); - dispatch({ - type: "REMOVE_TOAST", - toastId: toastId, - }); - }, TOAST_REMOVE_DELAY); - - toastTimeouts.set(toastId, timeout); -}; - -export const reducer = (state: State, action: Action): State => { - switch (action.type) { - case "ADD_TOAST": - return { - ...state, - toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), - }; - - case "UPDATE_TOAST": - return { - ...state, - toasts: state.toasts.map((t) => - t.id === action.toast.id ? { ...t, ...action.toast } : t, - ), - }; - - case "DISMISS_TOAST": { - const { toastId } = action; - - // ! Side effects ! - This could be extracted into a dismissToast() action, - // but I'll keep it here for simplicity - if (toastId) { - addToRemoveQueue(toastId); - } else { - state.toasts.forEach((toast) => { - addToRemoveQueue(toast.id); - }); - } - - return { - ...state, - toasts: state.toasts.map((t) => - t.id === toastId || toastId === undefined - ? { - ...t, - open: false, - } - : t, - ), - }; - } - case "REMOVE_TOAST": - if (action.toastId === undefined) { - return { - ...state, - toasts: [], - }; - } - return { - ...state, - toasts: state.toasts.filter((t) => t.id !== action.toastId), - }; - } -}; - -const listeners: Array<(state: State) => void> = []; - -let memoryState: State = { toasts: [] }; - -function dispatch(action: Action) { - memoryState = reducer(memoryState, action); - listeners.forEach((listener) => { - listener(memoryState); - }); -} - -type Toast = Omit; - -function toast({ ...props }: Toast) { - const id = genId(); - - const update = (props: ToasterToast) => - dispatch({ - type: "UPDATE_TOAST", - toast: { ...props, id }, - }); - const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }); - - dispatch({ - type: "ADD_TOAST", - toast: { - ...props, - id, - open: true, - onOpenChange: (open) => { - if (!open) dismiss(); - }, - }, - }); - - return { - id: id, - dismiss, - update, - }; -} - -function useToast() { - const [state, setState] = React.useState(memoryState); - - React.useEffect(() => { - listeners.push(setState); - return () => { - const index = listeners.indexOf(setState); - if (index > -1) { - listeners.splice(index, 1); - } - }; - }, [state]); - - return { - ...state, - toast, - dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), - }; -} - -export { useToast, toast }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a653d647..8dc30884 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -338,6 +338,9 @@ importers: nanostores: specifier: 0.11.3 version: 0.11.3 + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react: specifier: ^18.3.1 version: 18.3.1 @@ -350,6 +353,9 @@ importers: react-hook-form: specifier: ^7.53.0 version: 7.53.0(react@18.3.1) + sonner: + specifier: ^2.0.3 + version: 2.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) stream-browserify: specifier: ^3.0.0 version: 3.0.0 @@ -11194,6 +11200,12 @@ packages: resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} engines: {node: '>= 0.4.0'} + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + nlcst-to-string@4.0.0: resolution: {integrity: sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==} @@ -12706,6 +12718,12 @@ packages: sonic-boom@3.8.1: resolution: {integrity: sha512-y4Z8LCDBuum+PBP3lSV7RHrXscqksve/bi0as7mhwVnBW+/wUqKT/2Kb7um8yqcFy0duYbbPxzt89Zy2nOCaxg==} + sonner@2.0.3: + resolution: {integrity: sha512-njQ4Hht92m0sMqqHVDL32V2Oun9W1+PHO9NDv9FHfJjT3JT22IG4Jpo3FPQy+mouRKCXFWO+r67v6MrHX2zeIA==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -27938,6 +27956,11 @@ snapshots: netmask@2.0.2: {} + next-themes@0.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + nlcst-to-string@4.0.0: dependencies: '@types/nlcst': 2.0.3 @@ -29918,6 +29941,11 @@ snapshots: dependencies: atomic-sleep: 1.0.0 + sonner@2.0.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + source-map-js@1.2.1: {} source-map-support@0.5.13: