@marv3l/canopy-ui 1.0.1 → 1.0.2

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.
@@ -1,185 +1,185 @@
1
- "use client";
2
-
3
- // Import React runtime and component types
4
- import * as React from "react";
5
- // Import semantic SVG icons from lucide-react
6
- import { CheckCircle2, AlertCircle, AlertTriangle, Info, X } from "lucide-react";
7
- // Import toast hook and type interfaces
8
- import { useToast, ToastItem } from "./use-toast";
9
-
10
- // Props accepted by the root Toaster container mounted in the root layout
11
- export interface ToasterProps {
12
- // Global lifespan (in milliseconds) for all toasts; defaults to 4000ms
13
- defaultDuration?: number;
14
- // Screen viewport placement position
15
- position?: "top-right" | "bottom-right" | "top-center" | "bottom-center" | "top-left" | "bottom-left";
16
- }
17
-
18
- // Visual preset configurations mapped to your semantic theme tokens
19
- const variantStyles: Record<
20
- string,
21
- { bg: string; border: string; text: string; progress: string; icon: any }
22
- > = {
23
- default: {
24
- bg: "bg-card",
25
- border: "border-border",
26
- text: "text-card-foreground",
27
- progress: "bg-foreground/20",
28
- icon: null,
29
- },
30
- success: {
31
- bg: "bg-success-bg",
32
- border: "border-success/40",
33
- text: "text-success",
34
- progress: "bg-success",
35
- icon: CheckCircle2,
36
- },
37
- error: {
38
- bg: "bg-destructive/10",
39
- border: "border-destructive/30",
40
- text: "text-destructive",
41
- progress: "bg-destructive",
42
- icon: AlertCircle,
43
- },
44
- warning: {
45
- bg: "bg-warning-bg",
46
- border: "border-warning/40",
47
- text: "text-warning",
48
- progress: "bg-warning",
49
- icon: AlertTriangle,
50
- },
51
- info: {
52
- bg: "bg-accent",
53
- border: "border-primary/30",
54
- text: "text-accent-foreground",
55
- progress: "bg-primary",
56
- icon: Info,
57
- },
58
- };
59
-
60
- // Root Toaster Component placed into layout.tsx
61
- export function Toaster({ defaultDuration = 4000, position = "top-center" }: ToasterProps) {
62
- const { toasts, dismiss } = useToast();
63
-
64
- const positionClasses = {
65
- "top-right": "top-4 right-4 items-end",
66
- "top-left": "top-4 left-4 items-start",
67
- "bottom-right": "bottom-4 right-4 items-end",
68
- "bottom-left": "bottom-4 left-4 items-start",
69
- "top-center": "top-4 left-1/2 -translate-x-1/2 items-center",
70
- "bottom-center": "bottom-4 left-1/2 -translate-x-1/2 items-center",
71
- }[position];
72
-
73
- return (
74
- <>
75
- <style>{`
76
- @keyframes toast-progress {
77
- from {
78
- transform: scaleX(1);
79
- }
80
- to {
81
- transform: scaleX(0);
82
- }
83
- }
84
- `}</style>
85
-
86
- <div className={`fixed z-50 pointer-events-none flex flex-col gap-2 p-4 w-full max-w-sm ${positionClasses}`}>
87
- {toasts.map((item) => (
88
- <ToastElement
89
- key={item.id}
90
- toast={item}
91
- defaultDuration={defaultDuration}
92
- onDismiss={() => dismiss(item.id)}
93
- />
94
- ))}
95
- </div>
96
- </>
97
- );
98
- }
99
-
100
- // Atomic Toast Card Component
101
- function ToastElement({
102
- toast,
103
- defaultDuration,
104
- onDismiss,
105
- }: {
106
- toast: ToastItem;
107
- defaultDuration: number;
108
- onDismiss: () => void;
109
- }) {
110
- const duration = toast.duration ?? defaultDuration;
111
-
112
- React.useEffect(() => {
113
- if (duration <= 0) return;
114
- const timer = setTimeout(() => {
115
- onDismiss();
116
- }, duration);
117
- return () => clearTimeout(timer);
118
- }, [duration, onDismiss]);
119
-
120
- const variant = toast.variant || "default";
121
- const defaultStyle = variantStyles[variant] || variantStyles.default;
122
- const IconComponent = defaultStyle.icon;
123
-
124
- // 1. Only build inline styles for values that are explicitly provided
125
- const customInlineStyle: React.CSSProperties = {};
126
- if (toast.customColor?.bg) customInlineStyle.backgroundColor = toast.customColor.bg;
127
- if (toast.customColor?.border) customInlineStyle.borderColor = toast.customColor.border;
128
- if (toast.customColor?.text) customInlineStyle.color = toast.customColor.text;
129
-
130
- // 2. Prevent default Tailwind classes from overriding user custom classes or inline styles
131
- const userHasBg = Boolean(toast.customColor?.bg || toast.className?.match(/(?:^|\s)bg-/));
132
- const userHasBorder = Boolean(toast.customColor?.border || toast.className?.match(/(?:^|\s)border-/));
133
- const userHasText = Boolean(toast.customColor?.text || toast.className?.match(/(?:^|\s)text-/));
134
-
135
- return (
136
- <div
137
- style={customInlineStyle}
138
- className={`pointer-events-auto relative overflow-hidden flex items-start gap-3 w-full p-4 rounded-[var(--radius-lg,0.625rem)] border shadow-lg transition-all duration-200 backdrop-blur-sm ${
139
- !userHasBg ? defaultStyle.bg : ""
140
- } ${!userHasBorder ? defaultStyle.border : ""} ${
141
- !userHasText ? defaultStyle.text : ""
142
- } ${toast.className || ""}`}
143
- >
144
- {/* Render icon if preset defines one */}
145
- {IconComponent && (
146
- <IconComponent
147
- className="w-5 h-5 mt-0.5 shrink-0"
148
- style={{ color: toast.customColor?.icon }}
149
- />
150
- )}
151
-
152
- {/* Toast Content Area */}
153
- <div className="flex-1 text-sm space-y-1">
154
- {toast.title && <div className="font-semibold leading-tight">{toast.title}</div>}
155
- {toast.description && (
156
- <div className="opacity-90 leading-relaxed text-xs">
157
- {toast.description}
158
- </div>
159
- )}
160
- {toast.action && <div className="pt-1">{toast.action}</div>}
161
- </div>
162
-
163
- {/* Close button */}
164
- <button
165
- onClick={onDismiss}
166
- className="p-1 rounded-md opacity-70 hover:opacity-100 hover:bg-foreground/10 transition-colors"
167
- >
168
- <X className="w-4 h-4" />
169
- </button>
170
-
171
- {/* Animated Lifespan Progress Bar */}
172
- {duration > 0 && (
173
- <div
174
- className={`absolute bottom-0 left-0 right-0 h-1 origin-left ${
175
- !toast.customColor?.progress ? defaultStyle.progress : ""
176
- }`}
177
- style={{
178
- backgroundColor: toast.customColor?.progress,
179
- animation: `toast-progress ${duration}ms linear forwards`,
180
- }}
181
- />
182
- )}
183
- </div>
184
- );
1
+ "use client";
2
+
3
+ // Import React runtime and component types
4
+ import * as React from "react";
5
+ // Import semantic SVG icons from lucide-react
6
+ import { CheckCircle2, AlertCircle, AlertTriangle, Info, X } from "lucide-react";
7
+ // Import toast hook and type interfaces
8
+ import { useToast, ToastItem } from "./use-toast";
9
+
10
+ // Props accepted by the root Toaster container mounted in the root layout
11
+ export interface ToasterProps {
12
+ // Global lifespan (in milliseconds) for all toasts; defaults to 4000ms
13
+ defaultDuration?: number;
14
+ // Screen viewport placement position
15
+ position?: "top-right" | "bottom-right" | "top-center" | "bottom-center" | "top-left" | "bottom-left";
16
+ }
17
+
18
+ // Visual preset configurations mapped to your semantic theme tokens
19
+ const variantStyles: Record<
20
+ string,
21
+ { bg: string; border: string; text: string; progress: string; icon: any }
22
+ > = {
23
+ default: {
24
+ bg: "bg-card",
25
+ border: "border-border",
26
+ text: "text-card-foreground",
27
+ progress: "bg-foreground/20",
28
+ icon: null,
29
+ },
30
+ success: {
31
+ bg: "bg-success-bg",
32
+ border: "border-success/40",
33
+ text: "text-success",
34
+ progress: "bg-success",
35
+ icon: CheckCircle2,
36
+ },
37
+ error: {
38
+ bg: "bg-destructive/10",
39
+ border: "border-destructive/30",
40
+ text: "text-destructive",
41
+ progress: "bg-destructive",
42
+ icon: AlertCircle,
43
+ },
44
+ warning: {
45
+ bg: "bg-warning-bg",
46
+ border: "border-warning/40",
47
+ text: "text-warning",
48
+ progress: "bg-warning",
49
+ icon: AlertTriangle,
50
+ },
51
+ info: {
52
+ bg: "bg-accent",
53
+ border: "border-primary/30",
54
+ text: "text-accent-foreground",
55
+ progress: "bg-primary",
56
+ icon: Info,
57
+ },
58
+ };
59
+
60
+ // Root Toaster Component placed into layout.tsx
61
+ export function Toaster({ defaultDuration = 4000, position = "top-center" }: ToasterProps) {
62
+ const { toasts, dismiss } = useToast();
63
+
64
+ const positionClasses = {
65
+ "top-right": "top-4 right-4 items-end",
66
+ "top-left": "top-4 left-4 items-start",
67
+ "bottom-right": "bottom-4 right-4 items-end",
68
+ "bottom-left": "bottom-4 left-4 items-start",
69
+ "top-center": "top-4 left-1/2 -translate-x-1/2 items-center",
70
+ "bottom-center": "bottom-4 left-1/2 -translate-x-1/2 items-center",
71
+ }[position];
72
+
73
+ return (
74
+ <>
75
+ <style>{`
76
+ @keyframes toast-progress {
77
+ from {
78
+ transform: scaleX(1);
79
+ }
80
+ to {
81
+ transform: scaleX(0);
82
+ }
83
+ }
84
+ `}</style>
85
+
86
+ <div className={`fixed z-50 pointer-events-none flex flex-col gap-2 p-4 w-full max-w-sm ${positionClasses}`}>
87
+ {toasts.map((item) => (
88
+ <ToastElement
89
+ key={item.id}
90
+ toast={item}
91
+ defaultDuration={defaultDuration}
92
+ onDismiss={() => dismiss(item.id)}
93
+ />
94
+ ))}
95
+ </div>
96
+ </>
97
+ );
98
+ }
99
+
100
+ // Atomic Toast Card Component
101
+ function ToastElement({
102
+ toast,
103
+ defaultDuration,
104
+ onDismiss,
105
+ }: {
106
+ toast: ToastItem;
107
+ defaultDuration: number;
108
+ onDismiss: () => void;
109
+ }) {
110
+ const duration = toast.duration ?? defaultDuration;
111
+
112
+ React.useEffect(() => {
113
+ if (duration <= 0) return;
114
+ const timer = setTimeout(() => {
115
+ onDismiss();
116
+ }, duration);
117
+ return () => clearTimeout(timer);
118
+ }, [duration, onDismiss]);
119
+
120
+ const variant = toast.variant || "default";
121
+ const defaultStyle = variantStyles[variant] || variantStyles.default;
122
+ const IconComponent = defaultStyle.icon;
123
+
124
+ // 1. Only build inline styles for values that are explicitly provided
125
+ const customInlineStyle: React.CSSProperties = {};
126
+ if (toast.customColor?.bg) customInlineStyle.backgroundColor = toast.customColor.bg;
127
+ if (toast.customColor?.border) customInlineStyle.borderColor = toast.customColor.border;
128
+ if (toast.customColor?.text) customInlineStyle.color = toast.customColor.text;
129
+
130
+ // 2. Prevent default Tailwind classes from overriding user custom classes or inline styles
131
+ const userHasBg = Boolean(toast.customColor?.bg || toast.className?.match(/(?:^|\s)bg-/));
132
+ const userHasBorder = Boolean(toast.customColor?.border || toast.className?.match(/(?:^|\s)border-/));
133
+ const userHasText = Boolean(toast.customColor?.text || toast.className?.match(/(?:^|\s)text-/));
134
+
135
+ return (
136
+ <div
137
+ style={customInlineStyle}
138
+ className={`pointer-events-auto relative overflow-hidden flex items-start gap-3 w-full p-4 rounded-[var(--radius-lg,0.625rem)] border shadow-lg transition-all duration-200 backdrop-blur-sm ${
139
+ !userHasBg ? defaultStyle.bg : ""
140
+ } ${!userHasBorder ? defaultStyle.border : ""} ${
141
+ !userHasText ? defaultStyle.text : ""
142
+ } ${toast.className || ""}`}
143
+ >
144
+ {/* Render icon if preset defines one */}
145
+ {IconComponent && (
146
+ <IconComponent
147
+ className="w-5 h-5 mt-0.5 shrink-0"
148
+ style={{ color: toast.customColor?.icon }}
149
+ />
150
+ )}
151
+
152
+ {/* Toast Content Area */}
153
+ <div className="flex-1 text-sm space-y-1">
154
+ {toast.title && <div className="font-semibold leading-tight">{toast.title}</div>}
155
+ {toast.description && (
156
+ <div className="opacity-90 leading-relaxed text-xs">
157
+ {toast.description}
158
+ </div>
159
+ )}
160
+ {toast.action && <div className="pt-1">{toast.action}</div>}
161
+ </div>
162
+
163
+ {/* Close button */}
164
+ <button
165
+ onClick={onDismiss}
166
+ className="p-1 rounded-md opacity-70 hover:opacity-100 hover:bg-foreground/10 transition-colors"
167
+ >
168
+ <X className="w-4 h-4" />
169
+ </button>
170
+
171
+ {/* Animated Lifespan Progress Bar */}
172
+ {duration > 0 && (
173
+ <div
174
+ className={`absolute bottom-0 left-0 right-0 h-1 origin-left ${
175
+ !toast.customColor?.progress ? defaultStyle.progress : ""
176
+ }`}
177
+ style={{
178
+ backgroundColor: toast.customColor?.progress,
179
+ animation: `toast-progress ${duration}ms linear forwards`,
180
+ }}
181
+ />
182
+ )}
183
+ </div>
184
+ );
185
185
  }