@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.
- package/LICENSE +20 -20
- package/README.md +261 -261
- package/dist/index.js +0 -0
- package/package.json +1 -1
- package/templates/toast/toast.tsx +184 -184
- package/templates/toast/use-toast.ts +213 -213
|
@@ -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
|
}
|