primus-saas-react 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 (45) hide show
  1. package/dist/styles.css +568 -0
  2. package/package.json +15 -9
  3. package/DEMO.md +0 -68
  4. package/INTEGRATION.md +0 -702
  5. package/build_log.txt +0 -0
  6. package/postcss.config.js +0 -6
  7. package/src/components/ai/AICopilot.tsx +0 -88
  8. package/src/components/auth/PrimusLogin.tsx +0 -298
  9. package/src/components/auth/UserProfile.tsx +0 -26
  10. package/src/components/banking/accounts/AccountDashboard.tsx +0 -67
  11. package/src/components/banking/cards/CreditCardVisual.tsx +0 -67
  12. package/src/components/banking/credit/CreditScoreCard.tsx +0 -80
  13. package/src/components/banking/kyc/KYCVerification.tsx +0 -76
  14. package/src/components/banking/loans/LoanCalculator.tsx +0 -106
  15. package/src/components/banking/transactions/TransactionHistory.tsx +0 -74
  16. package/src/components/crud/PrimusDataTable.tsx +0 -220
  17. package/src/components/crud/PrimusModal.tsx +0 -68
  18. package/src/components/dashboard/PrimusDashboard.tsx +0 -145
  19. package/src/components/documents/DocumentViewer.tsx +0 -107
  20. package/src/components/featureflags/FeatureFlagToggle.tsx +0 -64
  21. package/src/components/insurance/agents/AgentDirectory.tsx +0 -72
  22. package/src/components/insurance/claims/ClaimStatusTracker.tsx +0 -78
  23. package/src/components/insurance/fraud/FraudDetectionDashboard.tsx +0 -68
  24. package/src/components/insurance/policies/PolicyCard.tsx +0 -77
  25. package/src/components/insurance/premium/PremiumCalculator.tsx +0 -104
  26. package/src/components/insurance/quotes/QuoteComparison.tsx +0 -75
  27. package/src/components/layout/PrimusHeader.tsx +0 -75
  28. package/src/components/layout/PrimusLayout.tsx +0 -47
  29. package/src/components/layout/PrimusSidebar.tsx +0 -102
  30. package/src/components/logging/LogViewer.tsx +0 -90
  31. package/src/components/notifications/NotificationFeed.tsx +0 -106
  32. package/src/components/notifications/PrimusNotificationCenter.tsx +0 -282
  33. package/src/components/payments/CheckoutForm.tsx +0 -167
  34. package/src/components/security/SecurityDashboard.tsx +0 -83
  35. package/src/components/shared/Button.tsx +0 -36
  36. package/src/components/shared/Input.tsx +0 -36
  37. package/src/components/storage/FileUploader.tsx +0 -79
  38. package/src/context/PrimusProvider.tsx +0 -156
  39. package/src/context/PrimusThemeProvider.tsx +0 -160
  40. package/src/hooks/useNotifications.ts +0 -58
  41. package/src/hooks/usePrimusAuth.ts +0 -3
  42. package/src/hooks/useRealtimeNotifications.ts +0 -114
  43. package/src/index.ts +0 -42
  44. package/tailwind.config.js +0 -18
  45. package/tsconfig.json +0 -28
@@ -1,282 +0,0 @@
1
- import { useState, useEffect, useCallback } from 'react';
2
- import { Bell, Send, X, Check, AlertTriangle, Info, AlertCircle } from 'lucide-react';
3
- import { usePrimusTheme, PrimusTheme, themeColors } from '../../context/PrimusThemeProvider';
4
-
5
- interface Notification {
6
- id: string;
7
- title: string;
8
- message: string;
9
- type: 'info' | 'success' | 'warning' | 'error';
10
- timestamp: string;
11
- read: boolean;
12
- }
13
-
14
- const API_URL = 'http://localhost:5222';
15
-
16
- interface PrimusNotificationCenterProps {
17
- /** Override the theme from context (optional) */
18
- theme?: PrimusTheme;
19
- /** Custom API URL (optional) */
20
- apiUrl?: string;
21
- }
22
-
23
- /**
24
- * PrimusNotificationCenter - Enterprise Notification Component
25
- *
26
- * Features:
27
- * - Automatic theme inheritance from PrimusThemeProvider
28
- * - Bell icon with animated badge
29
- * - Admin panel for sending notifications
30
- * - Real-time polling (3 second intervals)
31
- * - Type-specific icons and colors
32
- *
33
- * @example
34
- * ```tsx
35
- * // With theme provider (recommended)
36
- * <PrimusThemeProvider defaultTheme="dark">
37
- * <PrimusNotificationCenter />
38
- * </PrimusThemeProvider>
39
- *
40
- * // With manual override
41
- * <PrimusNotificationCenter theme="light" />
42
- * ```
43
- */
44
- export function PrimusNotificationCenter({ theme: themeOverride, apiUrl = API_URL }: PrimusNotificationCenterProps) {
45
- const themeContext = usePrimusTheme();
46
- const theme = themeOverride || themeContext.theme;
47
- const t = themeColors[theme];
48
-
49
- const [notifications, setNotifications] = useState<Notification[]>([]);
50
- const [isOpen, setIsOpen] = useState(false);
51
- const [showSendForm, setShowSendForm] = useState(false);
52
- const [unreadCount, setUnreadCount] = useState(0);
53
- const [newTitle, setNewTitle] = useState('');
54
- const [newMessage, setNewMessage] = useState('');
55
- const [newType, setNewType] = useState<'info' | 'success' | 'warning' | 'error'>('info');
56
- const [sending, setSending] = useState(false);
57
-
58
- const fetchNotifications = useCallback(async () => {
59
- try {
60
- const response = await fetch(`${apiUrl}/api/notifications`);
61
- if (response.ok) {
62
- const data = await response.json();
63
- setNotifications(data);
64
- setUnreadCount(data.filter((n: Notification) => !n.read).length);
65
- }
66
- } catch (error) {
67
- // Silent fail - backend may not be connected
68
- }
69
- }, [apiUrl]);
70
-
71
- useEffect(() => {
72
- fetchNotifications();
73
- const interval = setInterval(fetchNotifications, 3000);
74
- return () => clearInterval(interval);
75
- }, [fetchNotifications]);
76
-
77
- const sendNotification = async () => {
78
- if (!newTitle.trim() || !newMessage.trim()) return;
79
- setSending(true);
80
- try {
81
- const response = await fetch(`${apiUrl}/api/notifications`, {
82
- method: 'POST',
83
- headers: { 'Content-Type': 'application/json' },
84
- body: JSON.stringify({ title: newTitle, message: newMessage, type: newType })
85
- });
86
- if (response.ok) {
87
- setNewTitle('');
88
- setNewMessage('');
89
- setNewType('info');
90
- setShowSendForm(false);
91
- await fetchNotifications();
92
- }
93
- } catch (error) {
94
- console.error('Failed to send notification');
95
- }
96
- setSending(false);
97
- };
98
-
99
- const markAsRead = async (id: string) => {
100
- try {
101
- await fetch(`${apiUrl}/api/notifications/${id}/read`, { method: 'PUT' });
102
- setNotifications(prev => prev.map(n => n.id === id ? { ...n, read: true } : n));
103
- setUnreadCount(prev => Math.max(0, prev - 1));
104
- } catch {
105
- // Silent fail
106
- }
107
- };
108
-
109
- const getTypeIcon = (type: string) => {
110
- const iconClass = "h-4 w-4";
111
- switch (type) {
112
- case 'success': return <Check className={`${iconClass} ${t.success}`} />;
113
- case 'warning': return <AlertTriangle className={`${iconClass} ${t.warning}`} />;
114
- case 'error': return <AlertCircle className={`${iconClass} ${t.error}`} />;
115
- default: return <Info className={`${iconClass} ${t.info}`} />;
116
- }
117
- };
118
-
119
- const getTypeBg = (type: string) => {
120
- if (theme === 'light') {
121
- switch (type) {
122
- case 'success': return 'bg-emerald-50 border-emerald-200';
123
- case 'warning': return 'bg-amber-50 border-amber-200';
124
- case 'error': return 'bg-rose-50 border-rose-200';
125
- default: return 'bg-blue-50 border-blue-200';
126
- }
127
- } else {
128
- switch (type) {
129
- case 'success': return 'bg-emerald-900/20 border-emerald-800';
130
- case 'warning': return 'bg-amber-900/20 border-amber-800';
131
- case 'error': return 'bg-rose-900/20 border-rose-800';
132
- default: return 'bg-blue-900/20 border-blue-800';
133
- }
134
- }
135
- };
136
-
137
- return (
138
- <div className="relative font-sans">
139
- {/* Bell Button */}
140
- <button
141
- onClick={() => setIsOpen(!isOpen)}
142
- className={`relative p-2.5 rounded-xl ${t.bgHover} transition-all duration-200 group`}
143
- >
144
- <Bell className={`h-5 w-5 ${t.textSecondary} group-hover:${t.accentText} transition-colors`} />
145
- {unreadCount > 0 && (
146
- <span className={`absolute -top-0.5 -right-0.5 h-5 w-5 ${t.badge} rounded-full flex items-center justify-center text-white text-xs font-bold shadow-lg animate-pulse`}>
147
- {unreadCount > 9 ? '9+' : unreadCount}
148
- </span>
149
- )}
150
- </button>
151
-
152
- {/* Dropdown Panel */}
153
- {isOpen && (
154
- <>
155
- <div className="fixed inset-0 z-40" onClick={() => setIsOpen(false)} />
156
-
157
- <div className={`absolute right-0 mt-3 w-96 ${t.bg} border ${t.border} rounded-2xl shadow-2xl z-50 overflow-hidden`}>
158
- {/* Header */}
159
- <div className={`px-5 py-4 border-b ${t.border} flex items-center justify-between`}>
160
- <div className="flex items-center gap-3">
161
- <div className={`p-2 rounded-xl ${t.bgTertiary}`}>
162
- <Bell className={`h-5 w-5 ${t.accentText}`} />
163
- </div>
164
- <div>
165
- <h3 className={`font-semibold ${t.text}`}>Notifications</h3>
166
- <p className={`text-xs ${t.textMuted}`}>
167
- {unreadCount > 0 ? `${unreadCount} unread` : 'All caught up!'}
168
- </p>
169
- </div>
170
- </div>
171
- <button onClick={() => setIsOpen(false)} className={`p-1.5 rounded-lg ${t.bgHover} transition-colors`}>
172
- <X className={`h-4 w-4 ${t.textSecondary}`} />
173
- </button>
174
- </div>
175
-
176
- {/* Admin Panel */}
177
- <div className={`p-4 border-b ${t.border} ${t.bgSecondary}`}>
178
- <button
179
- onClick={() => setShowSendForm(!showSendForm)}
180
- className={`w-full px-4 py-2.5 ${t.accent} ${t.accentHover} rounded-xl text-white text-sm font-medium flex items-center justify-center gap-2 transition-all duration-200 shadow-lg`}
181
- >
182
- <Send className="h-4 w-4" />
183
- Send New Notification
184
- </button>
185
-
186
- {showSendForm && (
187
- <div className="mt-4 space-y-3">
188
- <input
189
- type="text"
190
- placeholder="Notification title..."
191
- value={newTitle}
192
- onChange={(e) => setNewTitle(e.target.value)}
193
- className={`w-full px-4 py-2.5 ${t.bg} border ${t.border} rounded-xl ${t.text} text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`}
194
- />
195
- <input
196
- type="text"
197
- placeholder="Message content..."
198
- value={newMessage}
199
- onChange={(e) => setNewMessage(e.target.value)}
200
- className={`w-full px-4 py-2.5 ${t.bg} border ${t.border} rounded-xl ${t.text} text-sm focus:outline-none focus:ring-2 focus:ring-blue-500`}
201
- />
202
- <div className="flex gap-2">
203
- {(['info', 'success', 'warning', 'error'] as const).map((type) => (
204
- <button
205
- key={type}
206
- onClick={() => setNewType(type)}
207
- className={`flex-1 py-2 px-3 rounded-lg text-xs font-medium capitalize transition-all border ${newType === type
208
- ? `${getTypeBg(type)} ${t.text}`
209
- : `${t.bg} ${t.textSecondary} ${t.border} ${t.bgHover}`
210
- }`}
211
- >
212
- {type}
213
- </button>
214
- ))}
215
- </div>
216
- <button
217
- onClick={sendNotification}
218
- disabled={sending || !newTitle.trim() || !newMessage.trim()}
219
- className={`w-full py-2.5 ${t.accent} ${t.accentHover} disabled:opacity-50 rounded-xl text-white text-sm font-medium`}
220
- >
221
- {sending ? 'Sending...' : 'Send Notification'}
222
- </button>
223
- </div>
224
- )}
225
- </div>
226
-
227
- {/* Notification List */}
228
- <div className="max-h-80 overflow-y-auto">
229
- {notifications.length === 0 ? (
230
- <div className="px-5 py-12 text-center">
231
- <div className={`mx-auto w-12 h-12 rounded-full ${t.bgSecondary} flex items-center justify-center mb-3`}>
232
- <Bell className={`h-6 w-6 ${t.textMuted}`} />
233
- </div>
234
- <p className={`${t.textSecondary} text-sm`}>No notifications yet</p>
235
- </div>
236
- ) : (
237
- <div className={`divide-y ${t.borderSecondary}`}>
238
- {notifications.map((notification) => (
239
- <div
240
- key={notification.id}
241
- onClick={() => markAsRead(notification.id)}
242
- className={`px-5 py-4 ${t.bgHover} transition-colors cursor-pointer ${!notification.read ? t.bgTertiary : ''
243
- }`}
244
- >
245
- <div className="flex gap-3">
246
- <div className={`mt-0.5 p-2 rounded-lg border ${getTypeBg(notification.type)}`}>
247
- {getTypeIcon(notification.type)}
248
- </div>
249
- <div className="flex-1 min-w-0">
250
- <div className="flex items-center gap-2">
251
- <p className={`text-sm font-medium ${t.text}`}>{notification.title}</p>
252
- {!notification.read && <span className={`h-2 w-2 rounded-full ${t.badge}`} />}
253
- </div>
254
- <p className={`text-sm ${t.textSecondary} mt-0.5 line-clamp-2`}>{notification.message}</p>
255
- <p className={`text-xs ${t.textMuted} mt-1.5`}>
256
- {new Date(notification.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
257
- </p>
258
- </div>
259
- </div>
260
- </div>
261
- ))}
262
- </div>
263
- )}
264
- </div>
265
-
266
- {/* Footer */}
267
- {notifications.length > 0 && (
268
- <div className={`px-5 py-3 border-t ${t.border} ${t.bgSecondary} flex items-center justify-between`}>
269
- <button onClick={fetchNotifications} className={`text-sm ${t.accentText} font-medium`}>Refresh</button>
270
- <button className={`text-sm ${t.textSecondary}`}>View all →</button>
271
- </div>
272
- )}
273
- </div>
274
- </>
275
- )}
276
- </div>
277
- );
278
- }
279
-
280
- // Aliases
281
- export const NotificationBell = PrimusNotificationCenter;
282
- export const PrimusNotifications = PrimusNotificationCenter;
@@ -1,167 +0,0 @@
1
- import React, { useState } from 'react';
2
- import { Button } from '../shared/Button';
3
- import { Input } from '../shared/Input';
4
- import { LockClosedIcon, CheckCircleIcon, XCircleIcon } from '@heroicons/react/24/solid';
5
-
6
- export interface CheckoutFormProps {
7
- amount: number;
8
- currency?: string;
9
- apiUrl?: string;
10
- onSuccess?: (result: any) => void;
11
- onError?: (error: string) => void;
12
- }
13
-
14
- export const CheckoutForm: React.FC<CheckoutFormProps> = ({
15
- amount,
16
- currency = 'USD',
17
- apiUrl = 'http://localhost:5221',
18
- onSuccess,
19
- onError
20
- }) => {
21
- const [loading, setLoading] = useState(false);
22
- const [cardholderName, setCardholderName] = useState('');
23
- const [cardNumber, setCardNumber] = useState('');
24
- const [expiry, setExpiry] = useState('');
25
- const [cvc, setCvc] = useState('');
26
- const [error, setError] = useState<string | null>(null);
27
- const [success, setSuccess] = useState(false);
28
-
29
- const handleSubmit = async (e: React.FormEvent) => {
30
- e.preventDefault();
31
- setError(null);
32
- setSuccess(false);
33
- setLoading(true);
34
-
35
- try {
36
- const response = await fetch(`${apiUrl}/api/payments/charge`, {
37
- method: 'POST',
38
- headers: { 'Content-Type': 'application/json' },
39
- body: JSON.stringify({
40
- amount,
41
- currency,
42
- cardholderName,
43
- cardNumber: cardNumber.replace(/\s/g, ''),
44
- expiry,
45
- cvc
46
- })
47
- });
48
-
49
- const data = await response.json();
50
-
51
- if (!response.ok) {
52
- throw new Error(data.error || 'Payment failed');
53
- }
54
-
55
- setSuccess(true);
56
- onSuccess?.(data);
57
-
58
- // Reset form after success
59
- setTimeout(() => {
60
- setCardholderName('');
61
- setCardNumber('');
62
- setExpiry('');
63
- setCvc('');
64
- setSuccess(false);
65
- }, 3000);
66
-
67
- } catch (err: any) {
68
- const errorMessage = err.message || 'Payment processing failed';
69
- setError(errorMessage);
70
- onError?.(errorMessage);
71
- } finally {
72
- setLoading(false);
73
- }
74
- };
75
-
76
- return (
77
- <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
78
- <div className="flex justify-between items-center mb-6">
79
- <h3 className="text-lg font-medium text-gray-900">Payment Details</h3>
80
- <div className="flex items-center text-gray-500 text-sm">
81
- <LockClosedIcon className="h-4 w-4 mr-1" />
82
- Secure Payment
83
- </div>
84
- </div>
85
-
86
- <div className="mb-6 p-4 bg-gray-50 rounded-lg flex justify-between items-center">
87
- <span className="text-gray-600">Total to pay</span>
88
- <span className="text-2xl font-bold text-gray-900">
89
- {new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)}
90
- </span>
91
- </div>
92
-
93
- {error && (
94
- <div className="mb-4 p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-2">
95
- <XCircleIcon className="h-5 w-5 text-red-500 flex-shrink-0 mt-0.5" />
96
- <p className="text-sm text-red-700">{error}</p>
97
- </div>
98
- )}
99
-
100
- {success && (
101
- <div className="mb-4 p-3 bg-green-50 border border-green-200 rounded-lg flex items-start gap-2">
102
- <CheckCircleIcon className="h-5 w-5 text-green-500 flex-shrink-0 mt-0.5" />
103
- <p className="text-sm text-green-700">Payment successful!</p>
104
- </div>
105
- )}
106
-
107
- <form onSubmit={handleSubmit} className="space-y-4">
108
- <Input
109
- label="Cardholder Name"
110
- placeholder="John Doe"
111
- value={cardholderName}
112
- onChange={e => setCardholderName(e.target.value)}
113
- required
114
- disabled={loading}
115
- />
116
-
117
- <div className="relative">
118
- <Input
119
- label="Card Number"
120
- placeholder="4242 4242 4242 4242"
121
- value={cardNumber}
122
- onChange={e => setCardNumber(e.target.value)}
123
- required
124
- disabled={loading}
125
- />
126
- <div className="absolute right-3 top-[34px] flex gap-1">
127
- <div className="h-5 w-8 bg-gray-200 rounded"></div>
128
- <div className="h-5 w-8 bg-gray-200 rounded"></div>
129
- </div>
130
- </div>
131
-
132
- <div className="grid grid-cols-2 gap-4">
133
- <Input
134
- label="Expiry Date"
135
- placeholder="MM/YY"
136
- value={expiry}
137
- onChange={e => setExpiry(e.target.value)}
138
- maxLength={5}
139
- required
140
- disabled={loading}
141
- />
142
- <Input
143
- label="CVC"
144
- placeholder="123"
145
- value={cvc}
146
- onChange={e => setCvc(e.target.value)}
147
- maxLength={4}
148
- required
149
- disabled={loading}
150
- />
151
- </div>
152
-
153
- <div className="pt-4">
154
- <Button type="submit" className="w-full bg-primus-600 hover:bg-primus-700" size="lg" disabled={loading}>
155
- {loading ? 'Processing...' : `Pay ${new Intl.NumberFormat('en-US', { style: 'currency', currency }).format(amount)}`}
156
- </Button>
157
- <p className="text-center text-xs text-gray-400 mt-3">
158
- Powered by Primus Payments. Your data is encrypted.
159
- </p>
160
- <p className="text-center text-xs text-gray-500 mt-2">
161
- Test card: 4242 4242 4242 4242
162
- </p>
163
- </div>
164
- </form>
165
- </div>
166
- );
167
- };
@@ -1,83 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
-
3
- interface Vulnerability {
4
- id: string;
5
- severity: string;
6
- type: string;
7
- title: string;
8
- description: string;
9
- file?: string;
10
- line?: number;
11
- recommendation: string;
12
- }
13
-
14
- export const SecurityDashboard: React.FC<{ apiUrl?: string }> = ({
15
- apiUrl = 'http://localhost:5221'
16
- }) => {
17
- const [vulnerabilities, setVulnerabilities] = useState<Vulnerability[]>([]);
18
- const [loading, setLoading] = useState(true);
19
-
20
- useEffect(() => {
21
- fetchVulnerabilities();
22
- }, []);
23
-
24
- const fetchVulnerabilities = async () => {
25
- try {
26
- const response = await fetch(`${apiUrl}/api/security/vulnerabilities`);
27
- if (response.ok) {
28
- const data = await response.json();
29
- setVulnerabilities(data);
30
- }
31
- } catch (error) {
32
- console.error('Failed to fetch vulnerabilities:', error);
33
- } finally {
34
- setLoading(false);
35
- }
36
- };
37
-
38
- const getSeverityColor = (severity: string) => {
39
- switch (severity.toLowerCase()) {
40
- case 'critical': return 'bg-red-100 text-red-800';
41
- case 'high': return 'bg-orange-100 text-orange-800';
42
- case 'medium': return 'bg-yellow-100 text-yellow-800';
43
- case 'low': return 'bg-blue-100 text-blue-800';
44
- default: return 'bg-gray-100 text-gray-800';
45
- }
46
- };
47
-
48
- if (loading) return <div className="p-4">Scanning for vulnerabilities...</div>;
49
-
50
- return (
51
- <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
52
- <div className="flex justify-between items-center mb-4">
53
- <h3 className="text-lg font-medium text-gray-900">Security Dashboard</h3>
54
- <button
55
- onClick={fetchVulnerabilities}
56
- className="px-3 py-1 bg-primus-600 text-white rounded text-sm hover:bg-primus-700"
57
- >
58
- Rescan
59
- </button>
60
- </div>
61
-
62
- <div className="space-y-3">
63
- {vulnerabilities.map((vuln) => (
64
- <div key={vuln.id} className="p-4 border border-gray-200 rounded-lg">
65
- <div className="flex justify-between items-start mb-2">
66
- <h4 className="font-medium text-gray-900">{vuln.title}</h4>
67
- <span className={`px-2 py-1 rounded text-xs font-medium ${getSeverityColor(vuln.severity)}`}>
68
- {vuln.severity}
69
- </span>
70
- </div>
71
- <p className="text-sm text-gray-600 mb-2">{vuln.description}</p>
72
- {vuln.file && (
73
- <p className="text-xs text-gray-500 mb-2">
74
- {vuln.file}{vuln.line ? `:${vuln.line}` : ''}
75
- </p>
76
- )}
77
- <p className="text-sm text-primus-600">💡 {vuln.recommendation}</p>
78
- </div>
79
- ))}
80
- </div>
81
- </div>
82
- );
83
- };
@@ -1,36 +0,0 @@
1
- import React from 'react';
2
- import { clsx, type ClassValue } from 'clsx';
3
- import { twMerge } from 'tailwind-merge';
4
-
5
- function cn(...inputs: ClassValue[]) {
6
- return twMerge(clsx(inputs));
7
- }
8
-
9
- export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
10
- variant?: 'primary' | 'secondary' | 'outline' | 'ghost';
11
- size?: 'sm' | 'md' | 'lg';
12
- }
13
-
14
- export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
15
- ({ className, variant = 'primary', size = 'md', ...props }, ref) => {
16
- return (
17
- <button
18
- ref={ref}
19
- className={cn(
20
- "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
21
- {
22
- 'bg-gray-900 text-gray-50 hover:bg-gray-900/90': variant === 'primary',
23
- 'bg-gray-100 text-gray-900 hover:bg-gray-100/80': variant === 'secondary',
24
- 'border border-gray-200 bg-white hover:bg-gray-100 hover:text-gray-900': variant === 'outline',
25
- 'hover:bg-gray-100 hover:text-gray-900': variant === 'ghost',
26
- 'h-9 px-3': size === 'sm',
27
- 'h-10 px-4 py-2': size === 'md',
28
- 'h-11 px-8': size === 'lg',
29
- },
30
- className
31
- )}
32
- {...props}
33
- />
34
- );
35
- }
36
- );
@@ -1,36 +0,0 @@
1
- import React from 'react';
2
- import { clsx, type ClassValue } from 'clsx';
3
- import { twMerge } from 'tailwind-merge';
4
-
5
- function cn(...inputs: ClassValue[]) {
6
- return twMerge(clsx(inputs));
7
- }
8
-
9
- export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
10
- label?: string;
11
- error?: string;
12
- }
13
-
14
- export const Input = React.forwardRef<HTMLInputElement, InputProps>(
15
- ({ className, label, error, ...props }, ref) => {
16
- return (
17
- <div className="w-full">
18
- {label && (
19
- <label className="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70 mb-2 block text-gray-700">
20
- {label}
21
- </label>
22
- )}
23
- <input
24
- ref={ref}
25
- className={cn(
26
- "flex h-10 w-full rounded-md border border-gray-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-gray-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
27
- error && "border-red-500 focus-visible:ring-red-500",
28
- className
29
- )}
30
- {...props}
31
- />
32
- {error && <p className="text-sm text-red-500 mt-1">{error}</p>}
33
- </div>
34
- );
35
- }
36
- );