@oppulence/design-system 1.0.3 → 1.0.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/hooks/use-resize-observer.ts +24 -0
  2. package/lib/ai.ts +31 -0
  3. package/package.json +19 -1
  4. package/src/components/atoms/animated-size-container.tsx +59 -0
  5. package/src/components/atoms/button.tsx +2 -0
  6. package/src/components/atoms/currency-input.tsx +16 -0
  7. package/src/components/atoms/icons.tsx +840 -0
  8. package/src/components/atoms/image.tsx +23 -0
  9. package/src/components/atoms/index.ts +10 -0
  10. package/src/components/atoms/loader.tsx +92 -0
  11. package/src/components/atoms/quantity-input.tsx +103 -0
  12. package/src/components/atoms/record-button.tsx +178 -0
  13. package/src/components/atoms/submit-button.tsx +26 -0
  14. package/src/components/atoms/text-effect.tsx +251 -0
  15. package/src/components/atoms/text-shimmer.tsx +74 -0
  16. package/src/components/molecules/actions.tsx +53 -0
  17. package/src/components/molecules/branch.tsx +192 -0
  18. package/src/components/molecules/code-block.tsx +151 -0
  19. package/src/components/molecules/form.tsx +177 -0
  20. package/src/components/molecules/index.ts +12 -0
  21. package/src/components/molecules/inline-citation.tsx +295 -0
  22. package/src/components/molecules/message.tsx +64 -0
  23. package/src/components/molecules/sources.tsx +116 -0
  24. package/src/components/molecules/suggestion.tsx +53 -0
  25. package/src/components/molecules/task.tsx +74 -0
  26. package/src/components/molecules/time-range-input.tsx +73 -0
  27. package/src/components/molecules/tool-call-indicator.tsx +42 -0
  28. package/src/components/molecules/tool.tsx +130 -0
  29. package/src/components/organisms/combobox-dropdown.tsx +171 -0
  30. package/src/components/organisms/conversation.tsx +98 -0
  31. package/src/components/organisms/date-range-picker.tsx +53 -0
  32. package/src/components/organisms/editor/extentions/bubble-menu/bubble-item.tsx +30 -0
  33. package/src/components/organisms/editor/extentions/bubble-menu/bubble-menu-button.tsx +27 -0
  34. package/src/components/organisms/editor/extentions/bubble-menu/index.tsx +63 -0
  35. package/src/components/organisms/editor/extentions/bubble-menu/link-item.tsx +104 -0
  36. package/src/components/organisms/editor/extentions/register.ts +22 -0
  37. package/src/components/organisms/editor/index.tsx +50 -0
  38. package/src/components/organisms/editor/styles.css +31 -0
  39. package/src/components/organisms/editor/utils.ts +19 -0
  40. package/src/components/organisms/index.ts +11 -0
  41. package/src/components/organisms/multiple-selector.tsx +632 -0
  42. package/src/components/organisms/prompt-input.tsx +747 -0
  43. package/src/components/organisms/reasoning.tsx +170 -0
  44. package/src/components/organisms/response.tsx +121 -0
  45. package/src/components/organisms/toast-toaster.tsx +84 -0
  46. package/src/components/organisms/toast.tsx +124 -0
  47. package/src/components/organisms/use-toast.tsx +206 -0
  48. package/src/styles/globals.css +169 -212
@@ -0,0 +1,170 @@
1
+ "use client";
2
+
3
+ import { useControllableState } from "@radix-ui/react-use-controllable-state";
4
+ import { BrainIcon, ChevronDownIcon } from "lucide-react";
5
+ import type { ComponentProps } from "react";
6
+ import { createContext, memo, useContext, useEffect, useState } from "react";
7
+
8
+ import {
9
+ Collapsible,
10
+ CollapsibleContent,
11
+ CollapsibleTrigger,
12
+ } from "../molecules/collapsible";
13
+ import { Response } from "./response";
14
+
15
+ type ReasoningContextValue = {
16
+ isStreaming: boolean;
17
+ isOpen: boolean;
18
+ setIsOpen: (open: boolean) => void;
19
+ duration: number;
20
+ };
21
+
22
+ const ReasoningContext = createContext<ReasoningContextValue | null>(null);
23
+
24
+ const useReasoning = () => {
25
+ const context = useContext(ReasoningContext);
26
+ if (!context) {
27
+ throw new Error("Reasoning components must be used within Reasoning");
28
+ }
29
+ return context;
30
+ };
31
+
32
+ export type ReasoningProps = Omit<
33
+ ComponentProps<typeof Collapsible>,
34
+ "className"
35
+ > & {
36
+ isStreaming?: boolean;
37
+ open?: boolean;
38
+ defaultOpen?: boolean;
39
+ onOpenChange?: (open: boolean) => void;
40
+ duration?: number;
41
+ };
42
+
43
+ const AUTO_CLOSE_DELAY = 1000;
44
+ const MS_IN_S = 1000;
45
+
46
+ export const Reasoning = memo(
47
+ ({
48
+ isStreaming = false,
49
+ open,
50
+ defaultOpen = true,
51
+ onOpenChange,
52
+ duration: durationProp,
53
+ children,
54
+ ...props
55
+ }: ReasoningProps) => {
56
+ const [isOpen, setIsOpen] = useControllableState({
57
+ prop: open,
58
+ defaultProp: defaultOpen,
59
+ onChange: onOpenChange,
60
+ });
61
+ const [duration, setDuration] = useControllableState({
62
+ prop: durationProp,
63
+ defaultProp: 0,
64
+ });
65
+
66
+ const [hasAutoClosedRef, setHasAutoClosedRef] = useState(false);
67
+ const [startTime, setStartTime] = useState<number | null>(null);
68
+
69
+ useEffect(() => {
70
+ if (isStreaming) {
71
+ if (startTime === null) {
72
+ setStartTime(Date.now());
73
+ }
74
+ } else if (startTime !== null) {
75
+ setDuration(Math.ceil((Date.now() - startTime) / MS_IN_S));
76
+ setStartTime(null);
77
+ }
78
+ }, [isStreaming, startTime, setDuration]);
79
+
80
+ useEffect(() => {
81
+ if (defaultOpen && !isStreaming && isOpen && !hasAutoClosedRef) {
82
+ const timer = setTimeout(() => {
83
+ setIsOpen(false);
84
+ setHasAutoClosedRef(true);
85
+ }, AUTO_CLOSE_DELAY);
86
+
87
+ return () => clearTimeout(timer);
88
+ }
89
+ }, [isStreaming, isOpen, defaultOpen, setIsOpen, hasAutoClosedRef]);
90
+
91
+ const handleOpenChange = (newOpen: boolean) => {
92
+ setIsOpen(newOpen);
93
+ };
94
+
95
+ return (
96
+ <ReasoningContext.Provider
97
+ value={{ isStreaming, isOpen, setIsOpen, duration }}
98
+ >
99
+ <Collapsible
100
+ className="not-prose mb-4"
101
+ onOpenChange={handleOpenChange}
102
+ open={isOpen}
103
+ {...props}
104
+ >
105
+ {children}
106
+ </Collapsible>
107
+ </ReasoningContext.Provider>
108
+ );
109
+ },
110
+ );
111
+
112
+ export type ReasoningTriggerProps = Omit<
113
+ ComponentProps<typeof CollapsibleTrigger>,
114
+ "className"
115
+ >;
116
+
117
+ export const ReasoningTrigger = memo(
118
+ ({ children, ...props }: ReasoningTriggerProps) => {
119
+ const { isStreaming, isOpen, duration } = useReasoning();
120
+
121
+ return (
122
+ <CollapsibleTrigger
123
+ className="flex items-center gap-2 text-muted-foreground text-sm"
124
+ {...props}
125
+ >
126
+ {children ?? (
127
+ <>
128
+ <BrainIcon className="size-4" />
129
+ {isStreaming || duration === 0 ? (
130
+ <p>Thinking...</p>
131
+ ) : (
132
+ <p>
133
+ Thought for {duration} {duration === 1 ? "second" : "seconds"}
134
+ </p>
135
+ )}
136
+ <ChevronDownIcon
137
+ className={
138
+ isOpen
139
+ ? "size-4 text-muted-foreground transition-transform rotate-180"
140
+ : "size-4 text-muted-foreground transition-transform"
141
+ }
142
+ />
143
+ </>
144
+ )}
145
+ </CollapsibleTrigger>
146
+ );
147
+ },
148
+ );
149
+
150
+ export type ReasoningContentProps = Omit<
151
+ ComponentProps<typeof CollapsibleContent>,
152
+ "className"
153
+ > & {
154
+ children: string;
155
+ };
156
+
157
+ export const ReasoningContent = memo(
158
+ ({ children, ...props }: ReasoningContentProps) => (
159
+ <CollapsibleContent
160
+ className="mt-4 text-sm data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-top-2 data-[state=open]:slide-in-from-top-2 text-popover-foreground outline-none data-[state=closed]:animate-out data-[state=open]:animate-in"
161
+ {...props}
162
+ >
163
+ <Response>{children}</Response>
164
+ </CollapsibleContent>
165
+ ),
166
+ );
167
+
168
+ Reasoning.displayName = "Reasoning";
169
+ ReasoningTrigger.displayName = "ReasoningTrigger";
170
+ ReasoningContent.displayName = "ReasoningContent";
@@ -0,0 +1,121 @@
1
+ "use client";
2
+
3
+ import { type ComponentProps, memo, type ReactNode } from "react";
4
+ import { Streamdown } from "streamdown";
5
+
6
+ import { Table } from "../molecules/table";
7
+
8
+ type ResponseProps = Omit<ComponentProps<typeof Streamdown>, "className">;
9
+
10
+ const CustomUnorderedList = ({
11
+ children,
12
+ ...props
13
+ }: {
14
+ node?: unknown;
15
+ children?: ReactNode;
16
+ }) => (
17
+ <ul className="list-none m-0 p-0 leading-relaxed" {...props}>
18
+ {children}
19
+ </ul>
20
+ );
21
+
22
+ const CustomOrderedList = ({
23
+ children,
24
+ ...props
25
+ }: {
26
+ node?: unknown;
27
+ children?: ReactNode;
28
+ }) => (
29
+ <ol
30
+ className="list-none m-0 p-0 leading-relaxed"
31
+ {...props}
32
+ data-streamdown="unordered-list"
33
+ >
34
+ {children}
35
+ </ol>
36
+ );
37
+
38
+ const CustomListItem = ({
39
+ children,
40
+ ...props
41
+ }: {
42
+ node?: unknown;
43
+ children?: ReactNode;
44
+ }) => (
45
+ <li
46
+ className="py-0 my-0 leading-relaxed"
47
+ {...props}
48
+ data-streamdown="list-item"
49
+ >
50
+ {children}
51
+ </li>
52
+ );
53
+
54
+ const isExternalUrl = (href?: string) => {
55
+ if (!href) return false;
56
+ return /^https?:\/\//i.test(href);
57
+ };
58
+
59
+ export const Response = memo(
60
+ ({ ...props }: ResponseProps) => (
61
+ <Streamdown
62
+ className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 space-y-4 [&>h3+ul]:!mt-2 [&>h3+ol]:!mt-2 [&>h4+ul]:!mt-2 [&>h4+ol]:!mt-2 [&>ul]:!my-0 [&>ul]:!-mt-4 [&>*+ul]:!mt-2 [&>ol]:!my-0 [&>ol]:!-mt-4 [&>*+ol]:!mt-2"
63
+ components={{
64
+ ul: (componentProps) => <CustomUnorderedList {...componentProps} />,
65
+ ol: (componentProps) => <CustomOrderedList {...componentProps} />,
66
+ li: (componentProps) => <CustomListItem {...componentProps} />,
67
+ h2: ({ children, ...componentProps }) => (
68
+ <h3
69
+ className="font-medium text-sm text-primary tracking-wide"
70
+ {...componentProps}
71
+ >
72
+ {children}
73
+ </h3>
74
+ ),
75
+ h3: ({ children, ...componentProps }) => (
76
+ <h3
77
+ className="font-medium text-sm text-primary tracking-wide"
78
+ {...componentProps}
79
+ >
80
+ {children}
81
+ </h3>
82
+ ),
83
+ h4: ({ children, ...componentProps }) => (
84
+ <h4
85
+ className="font-medium text-sm text-primary tracking-wide"
86
+ {...componentProps}
87
+ >
88
+ {children}
89
+ </h4>
90
+ ),
91
+ p: ({ children, ...componentProps }) => (
92
+ <p className="leading-relaxed" {...componentProps}>
93
+ {children}
94
+ </p>
95
+ ),
96
+ table: (componentProps) => (
97
+ <div className="relative overflow-x-auto">
98
+ <Table variant="bordered" {...componentProps} />
99
+ </div>
100
+ ),
101
+ a: (componentProps) => {
102
+ if (isExternalUrl(componentProps.href)) {
103
+ return (
104
+ <a
105
+ {...componentProps}
106
+ rel="noreferrer"
107
+ target="_blank"
108
+ />
109
+ );
110
+ }
111
+
112
+ return <a {...componentProps} />;
113
+ },
114
+ }}
115
+ {...props}
116
+ />
117
+ ),
118
+ (prevProps, nextProps) => prevProps.children === nextProps.children,
119
+ );
120
+
121
+ Response.displayName = "Response";
@@ -0,0 +1,84 @@
1
+ "use client";
2
+
3
+ import { Icons } from "../atoms/icons";
4
+ import { Progress } from "../atoms/progress";
5
+ import { Spinner } from "../atoms/spinner";
6
+ import {
7
+ Toast,
8
+ ToastClose,
9
+ ToastDescription,
10
+ ToastProvider,
11
+ ToastTitle,
12
+ ToastViewport,
13
+ } from "./toast";
14
+ import { useToast } from "./use-toast";
15
+
16
+ export function ToastToaster() {
17
+ const { toasts } = useToast();
18
+
19
+ return (
20
+ <ToastProvider>
21
+ {toasts.map(
22
+ ({
23
+ id,
24
+ title,
25
+ description,
26
+ progress = 0,
27
+ action,
28
+ footer,
29
+ ...props
30
+ }) => {
31
+ return (
32
+ <Toast key={id} {...props}>
33
+ <div className="flex flex-col">
34
+ <div className="flex w-full">
35
+ <div className="space-y-2 w-full justify-center">
36
+ <div className="flex space-x-2 justify-between">
37
+ <div className="flex space-x-2 items-center">
38
+ {props?.variant && (
39
+ <div className="w-[20px] h-[20px] flex items-center">
40
+ {props.variant === "ai" && (
41
+ <Icons.AI size={16} color="#0064D9" />
42
+ )}
43
+ {props?.variant === "success" && (
44
+ <Icons.Check size={16} />
45
+ )}
46
+ {props?.variant === "error" && (
47
+ <Icons.Error size={16} color="#FF3638" />
48
+ )}
49
+ {props?.variant === "progress" && <Spinner />}
50
+ {props?.variant === "spinner" && <Spinner />}
51
+ </div>
52
+ )}
53
+ <div>{title && <ToastTitle>{title}</ToastTitle>}</div>
54
+ </div>
55
+
56
+ <div>
57
+ {props?.variant === "progress" && (
58
+ <span className="text-sm text-[#878787]">
59
+ {progress}%
60
+ </span>
61
+ )}
62
+ </div>
63
+ </div>
64
+
65
+ {props.variant === "progress" && <Progress value={progress} />}
66
+
67
+ {description && (
68
+ <ToastDescription>{description}</ToastDescription>
69
+ )}
70
+ </div>
71
+ {action}
72
+ <ToastClose />
73
+ </div>
74
+
75
+ <div className="w-full flex justify-end">{footer}</div>
76
+ </div>
77
+ </Toast>
78
+ );
79
+ },
80
+ )}
81
+ <ToastViewport />
82
+ </ToastProvider>
83
+ );
84
+ }
@@ -0,0 +1,124 @@
1
+ import * as ToastPrimitives from "@radix-ui/react-toast";
2
+ import { type VariantProps, cva } from "class-variance-authority";
3
+ import { X } from "lucide-react";
4
+ import * as React from "react";
5
+
6
+ const ToastProvider = ToastPrimitives.Provider;
7
+
8
+ const ToastViewport = React.forwardRef<
9
+ React.ElementRef<typeof ToastPrimitives.Viewport>,
10
+ Omit<React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>, "className">
11
+ >((props, ref) => (
12
+ <ToastPrimitives.Viewport
13
+ ref={ref}
14
+ className="fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:left-0 sm:top-auto sm:flex-col md:max-w-[420px]"
15
+ {...props}
16
+ />
17
+ ));
18
+ ToastViewport.displayName = ToastPrimitives.Viewport.displayName;
19
+
20
+ const toastVariants = cva(
21
+ "dark:bg-secondary text-foreground border bg-[#F6F6F3] group pointer-events-auto relative flex w-full items-center overflow-hidden border p-5 pr-5 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-left-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
22
+ {
23
+ variants: {
24
+ variant: {
25
+ default: "",
26
+ error: "",
27
+ success: "",
28
+ progress: "",
29
+ spinner: "",
30
+ ai: "",
31
+ destructive:
32
+ "destructive group border-destructive bg-destructive text-destructive-foreground",
33
+ },
34
+ },
35
+ defaultVariants: {
36
+ variant: "default",
37
+ },
38
+ },
39
+ );
40
+
41
+ const Toast = React.forwardRef<
42
+ React.ElementRef<typeof ToastPrimitives.Root>,
43
+ Omit<React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root>, "className"> &
44
+ VariantProps<typeof toastVariants>
45
+ >(({ variant, ...props }, ref) => {
46
+ return (
47
+ <ToastPrimitives.Root
48
+ ref={ref}
49
+ className={toastVariants({ variant })}
50
+ {...props}
51
+ />
52
+ );
53
+ });
54
+ Toast.displayName = ToastPrimitives.Root.displayName;
55
+
56
+ const ToastAction = React.forwardRef<
57
+ React.ElementRef<typeof ToastPrimitives.Action>,
58
+ Omit<React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>, "className">
59
+ >((props, ref) => (
60
+ <ToastPrimitives.Action
61
+ ref={ref}
62
+ className="inline-flex h-8 shrink-0 items-center justify-center border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive"
63
+ {...props}
64
+ />
65
+ ));
66
+ ToastAction.displayName = ToastPrimitives.Action.displayName;
67
+
68
+ const ToastClose = React.forwardRef<
69
+ React.ElementRef<typeof ToastPrimitives.Close>,
70
+ Omit<React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>, "className">
71
+ >((props, ref) => (
72
+ <ToastPrimitives.Close
73
+ ref={ref}
74
+ className="absolute right-2 top-2 p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50"
75
+ toast-close=""
76
+ {...props}
77
+ >
78
+ <X className="h-4 w-4" />
79
+ </ToastPrimitives.Close>
80
+ ));
81
+ ToastClose.displayName = ToastPrimitives.Close.displayName;
82
+
83
+ const ToastTitle = React.forwardRef<
84
+ React.ElementRef<typeof ToastPrimitives.Title>,
85
+ Omit<React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>, "className">
86
+ >((props, ref) => (
87
+ <ToastPrimitives.Title
88
+ ref={ref}
89
+ className="text-sm"
90
+ {...props}
91
+ />
92
+ ));
93
+ ToastTitle.displayName = ToastPrimitives.Title.displayName;
94
+
95
+ const ToastDescription = React.forwardRef<
96
+ React.ElementRef<typeof ToastPrimitives.Description>,
97
+ Omit<
98
+ React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>,
99
+ "className"
100
+ >
101
+ >((props, ref) => (
102
+ <ToastPrimitives.Description
103
+ ref={ref}
104
+ className="text-xs text-[#878787]"
105
+ {...props}
106
+ />
107
+ ));
108
+ ToastDescription.displayName = ToastPrimitives.Description.displayName;
109
+
110
+ type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>;
111
+
112
+ type ToastActionElement = React.ReactElement<typeof ToastAction>;
113
+
114
+ export {
115
+ type ToastProps,
116
+ type ToastActionElement,
117
+ ToastProvider,
118
+ ToastViewport,
119
+ Toast,
120
+ ToastTitle,
121
+ ToastDescription,
122
+ ToastClose,
123
+ ToastAction,
124
+ };
@@ -0,0 +1,206 @@
1
+ // Inspired by react-hot-toast library
2
+ import * as React from "react";
3
+ import type { ToastActionElement, ToastProps } from "./toast";
4
+
5
+ const TOAST_LIMIT = 1;
6
+ const TOAST_REMOVE_DELAY = 1000000;
7
+
8
+ type ToasterToast = Omit<ToastProps, "title"> & {
9
+ id: string;
10
+ title?: React.ReactNode;
11
+ description?: React.ReactNode;
12
+ action?: ToastActionElement;
13
+ progress?: number;
14
+ footer?: React.ReactNode;
15
+ };
16
+
17
+ const actionTypes = {
18
+ ADD_TOAST: "ADD_TOAST",
19
+ UPDATE_TOAST: "UPDATE_TOAST",
20
+ DISMISS_TOAST: "DISMISS_TOAST",
21
+ REMOVE_TOAST: "REMOVE_TOAST",
22
+ } as const;
23
+
24
+ let count = 0;
25
+
26
+ function genId() {
27
+ count = (count + 1) % Number.MAX_VALUE;
28
+ return count.toString();
29
+ }
30
+
31
+ type ActionType = typeof actionTypes;
32
+
33
+ type Action =
34
+ | {
35
+ type: ActionType["ADD_TOAST"];
36
+ toast: ToasterToast;
37
+ }
38
+ | {
39
+ type: ActionType["UPDATE_TOAST"];
40
+ toast: Partial<ToasterToast>;
41
+ }
42
+ | {
43
+ type: ActionType["DISMISS_TOAST"];
44
+ toastId?: ToasterToast["id"];
45
+ }
46
+ | {
47
+ type: ActionType["REMOVE_TOAST"];
48
+ toastId?: ToasterToast["id"];
49
+ };
50
+
51
+ interface State {
52
+ toasts: ToasterToast[];
53
+ }
54
+
55
+ const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>();
56
+
57
+ const addToRemoveQueue = (toastId: string) => {
58
+ if (toastTimeouts.has(toastId)) {
59
+ return;
60
+ }
61
+
62
+ const timeout = setTimeout(() => {
63
+ toastTimeouts.delete(toastId);
64
+ dispatch({
65
+ type: "REMOVE_TOAST",
66
+ toastId: toastId,
67
+ });
68
+ }, TOAST_REMOVE_DELAY);
69
+
70
+ toastTimeouts.set(toastId, timeout);
71
+ };
72
+
73
+ export const reducer = (state: State, action: Action): State => {
74
+ switch (action.type) {
75
+ case "ADD_TOAST": {
76
+ const existingIndex = state.toasts.findIndex(
77
+ (t) => t.id === action.toast.id,
78
+ );
79
+ if (existingIndex !== -1) {
80
+ return {
81
+ ...state,
82
+ toasts: state.toasts.map((t) =>
83
+ t.id === action.toast.id
84
+ ? { ...t, ...action.toast, open: true }
85
+ : t,
86
+ ),
87
+ };
88
+ }
89
+ return {
90
+ ...state,
91
+ toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
92
+ };
93
+ }
94
+
95
+ case "UPDATE_TOAST":
96
+ return {
97
+ ...state,
98
+ toasts: state.toasts.map((t) =>
99
+ t.id === action.toast.id ? { ...t, ...action.toast } : t,
100
+ ),
101
+ };
102
+
103
+ case "DISMISS_TOAST": {
104
+ const { toastId } = action;
105
+
106
+ if (toastId) {
107
+ addToRemoveQueue(toastId);
108
+ } else {
109
+ for (const toast of state.toasts) {
110
+ addToRemoveQueue(toast.id);
111
+ }
112
+ }
113
+
114
+ return {
115
+ ...state,
116
+ toasts: state.toasts.map((t) =>
117
+ t.id === toastId || toastId === undefined
118
+ ? {
119
+ ...t,
120
+ open: false,
121
+ }
122
+ : t,
123
+ ),
124
+ };
125
+ }
126
+ case "REMOVE_TOAST":
127
+ if (action.toastId === undefined) {
128
+ return {
129
+ ...state,
130
+ toasts: [],
131
+ };
132
+ }
133
+ return {
134
+ ...state,
135
+ toasts: state.toasts.filter((t) => t.id !== action.toastId),
136
+ };
137
+ }
138
+ };
139
+
140
+ const listeners: Array<(state: State) => void> = [];
141
+
142
+ let memoryState: State = { toasts: [] };
143
+
144
+ function dispatch(action: Action) {
145
+ memoryState = reducer(memoryState, action);
146
+ for (const listener of listeners) {
147
+ listener(memoryState);
148
+ }
149
+ }
150
+
151
+ type Toast = Omit<ToasterToast, "id"> & { id?: string };
152
+
153
+ function toast({ id: providedId, ...props }: Toast) {
154
+ const id = providedId ?? genId();
155
+
156
+ const update = (props: ToasterToast) =>
157
+ dispatch({
158
+ type: "UPDATE_TOAST",
159
+ toast: { ...props, id },
160
+ });
161
+
162
+ const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id });
163
+
164
+ dispatch({
165
+ type: "ADD_TOAST",
166
+ toast: {
167
+ ...props,
168
+ id,
169
+ open: true,
170
+ onOpenChange: (open) => {
171
+ if (!open) dismiss();
172
+ },
173
+ },
174
+ });
175
+
176
+ return {
177
+ id: id,
178
+ dismiss,
179
+ update,
180
+ };
181
+ }
182
+
183
+ function useToast() {
184
+ const [state, setState] = React.useState<State>(memoryState);
185
+
186
+ React.useEffect(() => {
187
+ listeners.push(setState);
188
+ return () => {
189
+ const index = listeners.indexOf(setState);
190
+
191
+ if (index > -1) {
192
+ listeners.splice(index, 1);
193
+ }
194
+ };
195
+ }, [state]);
196
+
197
+ return {
198
+ ...state,
199
+ toast,
200
+ dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
201
+ update: (toastId: string, props: ToasterToast) =>
202
+ dispatch({ type: "UPDATE_TOAST", toast: { ...props, id: toastId } }),
203
+ };
204
+ }
205
+
206
+ export { useToast, toast };