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,104 +0,0 @@
1
- import React, { useState } from 'react';
2
-
3
- export const PremiumCalculator: React.FC<{ apiUrl?: string }> = ({
4
- apiUrl = 'http://localhost:5221'
5
- }) => {
6
- const [coverageAmount, setCoverageAmount] = useState(100000);
7
- const [age, setAge] = useState(35);
8
- const [riskScore, setRiskScore] = useState(30);
9
- const [result, setResult] = useState<any>(null);
10
- const [loading, setLoading] = useState(false);
11
-
12
- const calculate = async () => {
13
- setLoading(true);
14
- try {
15
- const response = await fetch(`${apiUrl}/api/insurance/premium/calculate`, {
16
- method: 'POST',
17
- headers: { 'Content-Type': 'application/json' },
18
- body: JSON.stringify({ coverageAmount, age, riskScore })
19
- });
20
- if (response.ok) {
21
- const data = await response.json();
22
- setResult(data);
23
- }
24
- } catch (error) {
25
- console.error('Failed to calculate premium:', error);
26
- } finally {
27
- setLoading(false);
28
- }
29
- };
30
-
31
- return (
32
- <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
33
- <h3 className="text-lg font-medium text-gray-900 mb-4">Premium Calculator</h3>
34
-
35
- <div className="space-y-4 mb-6">
36
- <div>
37
- <label className="block text-sm font-medium text-gray-700 mb-1">
38
- Coverage Amount: ${coverageAmount.toLocaleString()}
39
- </label>
40
- <input
41
- type="range"
42
- min="10000"
43
- max="1000000"
44
- step="10000"
45
- value={coverageAmount}
46
- onChange={(e) => setCoverageAmount(Number(e.target.value))}
47
- className="w-full"
48
- />
49
- </div>
50
-
51
- <div>
52
- <label className="block text-sm font-medium text-gray-700 mb-1">
53
- Age: {age}
54
- </label>
55
- <input
56
- type="range"
57
- min="18"
58
- max="80"
59
- value={age}
60
- onChange={(e) => setAge(Number(e.target.value))}
61
- className="w-full"
62
- />
63
- </div>
64
-
65
- <div>
66
- <label className="block text-sm font-medium text-gray-700 mb-1">
67
- Risk Score: {riskScore}
68
- </label>
69
- <input
70
- type="range"
71
- min="0"
72
- max="100"
73
- value={riskScore}
74
- onChange={(e) => setRiskScore(Number(e.target.value))}
75
- className="w-full"
76
- />
77
- </div>
78
- </div>
79
-
80
- <button
81
- onClick={calculate}
82
- disabled={loading}
83
- className="w-full bg-primus-600 text-white py-2 px-4 rounded hover:bg-primus-700 disabled:opacity-50"
84
- >
85
- {loading ? 'Calculating...' : 'Calculate Premium'}
86
- </button>
87
-
88
- {result && (
89
- <div className="mt-6 p-4 bg-gray-50 rounded-lg">
90
- <div className="grid grid-cols-2 gap-4">
91
- <div>
92
- <p className="text-sm text-gray-600">Monthly Premium</p>
93
- <p className="text-2xl font-bold text-gray-900">${result.monthlyPremium}</p>
94
- </div>
95
- <div>
96
- <p className="text-sm text-gray-600">Annual Premium</p>
97
- <p className="text-2xl font-bold text-gray-900">${result.annualPremium}</p>
98
- </div>
99
- </div>
100
- </div>
101
- )}
102
- </div>
103
- );
104
- };
@@ -1,75 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
-
3
- interface Quote {
4
- id: string;
5
- type: string;
6
- monthlyPremium: number;
7
- coverageAmount: number;
8
- status: string;
9
- validUntil: string;
10
- }
11
-
12
- export const QuoteComparison: React.FC<{ apiUrl?: string }> = ({
13
- apiUrl = 'http://localhost:5221'
14
- }) => {
15
- const [quotes, setQuotes] = useState<Quote[]>([]);
16
- const [loading, setLoading] = useState(true);
17
-
18
- useEffect(() => {
19
- fetchQuotes();
20
- }, []);
21
-
22
- const fetchQuotes = async () => {
23
- try {
24
- const response = await fetch(`${apiUrl}/api/insurance/quotes`);
25
- if (response.ok) {
26
- const data = await response.json();
27
- setQuotes(data);
28
- }
29
- } catch (error) {
30
- console.error('Failed to fetch quotes:', error);
31
- } finally {
32
- setLoading(false);
33
- }
34
- };
35
-
36
- if (loading) return <div className="p-4">Loading quotes...</div>;
37
-
38
- return (
39
- <div className="bg-white p-6 rounded-lg border border-gray-200 shadow-sm">
40
- <h3 className="text-lg font-medium text-gray-900 mb-4">Insurance Quotes</h3>
41
-
42
- <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
43
- {quotes.map((quote) => (
44
- <div key={quote.id} className="p-4 border-2 border-gray-200 rounded-lg hover:border-primus-500 transition-colors">
45
- <div className="flex justify-between items-start mb-3">
46
- <h4 className="font-medium text-gray-900">{quote.type}</h4>
47
- <span className={`px-2 py-1 rounded text-xs font-medium ${quote.status === 'active' ? 'bg-green-100 text-green-800' : 'bg-blue-100 text-blue-800'
48
- }`}>
49
- {quote.status}
50
- </span>
51
- </div>
52
-
53
- <div className="mb-3">
54
- <div className="text-3xl font-bold text-gray-900">
55
- ${quote.monthlyPremium}
56
- <span className="text-sm font-normal text-gray-500">/mo</span>
57
- </div>
58
- <div className="text-sm text-gray-600">
59
- Coverage: ${quote.coverageAmount.toLocaleString()}
60
- </div>
61
- </div>
62
-
63
- <div className="text-xs text-gray-500">
64
- Valid until: {new Date(quote.validUntil).toLocaleDateString()}
65
- </div>
66
-
67
- <button className="mt-3 w-full px-4 py-2 bg-primus-600 text-white rounded hover:bg-primus-700">
68
- Accept Quote
69
- </button>
70
- </div>
71
- ))}
72
- </div>
73
- </div>
74
- );
75
- };
@@ -1,75 +0,0 @@
1
- import React from 'react';
2
-
3
- export interface PrimusHeaderProps {
4
- /** Page title */
5
- title?: string;
6
- /** Breadcrumb items */
7
- breadcrumbs?: { label: string; href?: string }[];
8
- /** Right side actions */
9
- actions?: React.ReactNode;
10
- /** User info/avatar */
11
- user?: {
12
- name: string;
13
- email?: string;
14
- avatar?: string;
15
- };
16
- /** On user click */
17
- onUserClick?: () => void;
18
- }
19
-
20
- export const PrimusHeader: React.FC<PrimusHeaderProps> = ({
21
- title,
22
- breadcrumbs,
23
- actions,
24
- user,
25
- onUserClick,
26
- }) => {
27
- return (
28
- <div className="flex items-center justify-between w-full">
29
- {/* Left side */}
30
- <div className="flex items-center gap-4">
31
- {breadcrumbs && breadcrumbs.length > 0 && (
32
- <nav className="flex items-center gap-2 text-sm">
33
- {breadcrumbs.map((crumb, index) => (
34
- <React.Fragment key={index}>
35
- {index > 0 && <span className="text-gray-500">/</span>}
36
- <span className={index === breadcrumbs.length - 1 ? 'text-white' : 'text-gray-400 hover:text-white cursor-pointer'}>
37
- {crumb.label}
38
- </span>
39
- </React.Fragment>
40
- ))}
41
- </nav>
42
- )}
43
- {title && !breadcrumbs && (
44
- <h1 className="text-xl font-semibold">{title}</h1>
45
- )}
46
- </div>
47
-
48
- {/* Right side */}
49
- <div className="flex items-center gap-4">
50
- {actions}
51
-
52
- {user && (
53
- <button
54
- onClick={onUserClick}
55
- className="flex items-center gap-3 px-3 py-1.5 rounded-lg hover:bg-gray-700/50 transition-colors"
56
- >
57
- <div className="text-right hidden sm:block">
58
- <p className="text-sm font-medium">{user.name}</p>
59
- {user.email && (
60
- <p className="text-xs text-gray-400">{user.email}</p>
61
- )}
62
- </div>
63
- {user.avatar ? (
64
- <img src={user.avatar} alt={user.name} className="w-8 h-8 rounded-full" />
65
- ) : (
66
- <div className="w-8 h-8 rounded-full bg-gradient-to-br from-purple-500 to-pink-500 flex items-center justify-center text-sm font-medium">
67
- {user.name.charAt(0).toUpperCase()}
68
- </div>
69
- )}
70
- </button>
71
- )}
72
- </div>
73
- </div>
74
- );
75
- };
@@ -1,47 +0,0 @@
1
- import React from 'react';
2
-
3
- export interface PrimusLayoutProps {
4
- children: React.ReactNode;
5
- sidebar?: React.ReactNode;
6
- header?: React.ReactNode;
7
- /** Dark mode */
8
- darkMode?: boolean;
9
- }
10
-
11
- export const PrimusLayout: React.FC<PrimusLayoutProps> = ({
12
- children,
13
- sidebar,
14
- header,
15
- }) => {
16
- // We rely on the global body background (set in App.css) for the main texture/color.
17
- // This layout provides structure and glass panels.
18
-
19
- return (
20
- <div className={`min-h-screen flex text-white relative`}>
21
- {/* Sidebar Container */}
22
- {sidebar && (
23
- <aside className="w-72 flex-shrink-0 flex flex-col fixed inset-y-0 left-0 z-50">
24
- {sidebar}
25
- </aside>
26
- )}
27
-
28
- {/* Main Content Area */}
29
- <div className={`flex-1 flex flex-col min-h-screen ${sidebar ? 'pl-72' : ''}`}>
30
-
31
- {/* Header - Floating Glass Bar */}
32
- {header && (
33
- <header className="h-20 flex items-center px-8 fixed top-0 right-0 left-72 z-40 bg-slate-900/10 backdrop-blur-md border-b border-white/5 transition-all duration-300">
34
- {header}
35
- </header>
36
- )}
37
-
38
- {/* Content Scrollable Area */}
39
- <main className="flex-1 p-8 pt-28 overflow-x-hidden">
40
- <div className="max-w-7xl mx-auto space-y-8 animate-enter">
41
- {children}
42
- </div>
43
- </main>
44
- </div>
45
- </div>
46
- );
47
- };
@@ -1,102 +0,0 @@
1
- import React from 'react';
2
-
3
- export interface SidebarItem {
4
- id: string;
5
- label: string;
6
- icon?: React.ReactNode;
7
- href?: string;
8
- onClick?: () => void;
9
- active?: boolean;
10
- badge?: string | number;
11
- }
12
-
13
- export interface PrimusSidebarProps {
14
- /** App logo or title */
15
- logo?: React.ReactNode;
16
- /** Navigation items */
17
- items: SidebarItem[];
18
- /** Footer content */
19
- footer?: React.ReactNode;
20
- /** Callback when item clicked */
21
- onItemClick?: (item: SidebarItem) => void;
22
- /** Currently active item id */
23
- activeId?: string;
24
- }
25
-
26
- export const PrimusSidebar: React.FC<PrimusSidebarProps> = ({
27
- logo,
28
- items,
29
- footer,
30
- onItemClick,
31
- activeId,
32
- }) => {
33
- return (
34
- <div className="flex flex-col h-full bg-slate-900/50 backdrop-blur-xl border-r border-white/10 relative overflow-hidden">
35
- {/* Ambient Background Glow */}
36
- <div className="absolute top-0 left-0 w-full h-64 bg-purple-500/10 blur-[60px] pointer-events-none" />
37
-
38
- {/* Logo */}
39
- {logo && (
40
- <div className="h-20 flex items-center px-6 relative z-10 border-b border-white/5">
41
- {logo}
42
- </div>
43
- )}
44
-
45
- {/* Navigation */}
46
- <nav className="flex-1 py-6 px-3 overflow-y-auto relative z-10">
47
- <ul className="space-y-1">
48
- {items.map((item) => {
49
- const isActive = activeId === item.id || item.active;
50
- return (
51
- <li key={item.id}>
52
- <button
53
- onClick={() => {
54
- item.onClick?.();
55
- onItemClick?.(item);
56
- }}
57
- className={`group relative w-full flex items-center gap-3 px-4 py-3 rounded-xl text-sm font-medium transition-all duration-300 ${isActive
58
- ? 'text-white shadow-lg shadow-purple-500/20 ring-1 ring-white/10'
59
- : 'text-gray-400 hover:text-white hover:bg-white/5'
60
- }`}
61
- >
62
- {/* Active State Background Gradient */}
63
- {isActive && (
64
- <div className="absolute inset-0 rounded-xl bg-gradient-to-r from-purple-600/90 to-pink-600/90 opacity-100 transition-opacity" />
65
- )}
66
-
67
- {/* Content */}
68
- <span className="relative z-10 w-5 h-5 flex items-center justify-center transition-transform group-hover:scale-110 duration-300">
69
- {item.icon}
70
- </span>
71
- <span className="relative z-10 flex-1 text-left">{item.label}</span>
72
-
73
- {/* Badge */}
74
- {item.badge && (
75
- <span className={`relative z-10 px-2 py-0.5 text-xs font-semibold rounded-full border ${isActive
76
- ? 'bg-white/20 border-white/20 text-white'
77
- : 'bg-gray-800 border-gray-700 text-gray-300 group-hover:bg-gray-700'
78
- }`}>
79
- {item.badge}
80
- </span>
81
- )}
82
-
83
- {/* Active Indicator Dot */}
84
- {isActive && (
85
- <div className="absolute left-0 top-1/2 -translate-y-1/2 w-1 h-8 bg-white rounded-r-full shadow-[0_0_12px_rgba(255,255,255,0.6)] hidden" />
86
- )}
87
- </button>
88
- </li>
89
- );
90
- })}
91
- </ul>
92
- </nav>
93
-
94
- {/* Footer */}
95
- {footer && (
96
- <div className="p-4 border-t border-white/5 bg-black/20 backdrop-blur-md relative z-10">
97
- {footer}
98
- </div>
99
- )}
100
- </div>
101
- );
102
- };
@@ -1,90 +0,0 @@
1
- import React, { useState, useEffect } from 'react';
2
-
3
- interface LogEntry {
4
- id: string;
5
- timestamp: string;
6
- level: string;
7
- message: string;
8
- source: string;
9
- }
10
-
11
- export const LogViewer: React.FC<{ apiUrl?: string }> = ({
12
- apiUrl = 'http://localhost:5221'
13
- }) => {
14
- const [logs, setLogs] = useState<LogEntry[]>([]);
15
- const [filter, setFilter] = useState<string>('');
16
- const [loading, setLoading] = useState(true);
17
-
18
- useEffect(() => {
19
- fetchLogs();
20
- const interval = setInterval(fetchLogs, 10000); // Refresh every 10s
21
- return () => clearInterval(interval);
22
- }, [filter]);
23
-
24
- const fetchLogs = async () => {
25
- try {
26
- const url = filter
27
- ? `${apiUrl}/api/logs?level=${filter}`
28
- : `${apiUrl}/api/logs`;
29
- const response = await fetch(url);
30
- if (response.ok) {
31
- const data = await response.json();
32
- setLogs(data);
33
- }
34
- } catch (error) {
35
- console.error('Failed to fetch logs:', error);
36
- } finally {
37
- setLoading(false);
38
- }
39
- };
40
-
41
- const getLevelColor = (level: string) => {
42
- switch (level.toUpperCase()) {
43
- case 'ERROR': return 'text-red-600 bg-red-50';
44
- case 'WARNING': return 'text-yellow-600 bg-yellow-50';
45
- case 'INFO': return 'text-blue-600 bg-blue-50';
46
- default: return 'text-gray-600 bg-gray-50';
47
- }
48
- };
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">System Logs</h3>
54
- <select
55
- value={filter}
56
- onChange={(e) => setFilter(e.target.value)}
57
- className="px-3 py-1 border border-gray-300 rounded-md text-sm"
58
- >
59
- <option value="">All Levels</option>
60
- <option value="INFO">Info</option>
61
- <option value="WARNING">Warning</option>
62
- <option value="ERROR">Error</option>
63
- </select>
64
- </div>
65
-
66
- {loading ? (
67
- <div className="text-center py-8 text-gray-500">Loading logs...</div>
68
- ) : (
69
- <div className="space-y-2 max-h-96 overflow-y-auto">
70
- {logs.map((log) => (
71
- <div key={log.id} className="p-3 bg-gray-50 rounded border border-gray-200">
72
- <div className="flex items-start gap-3">
73
- <span className={`px-2 py-0.5 rounded text-xs font-medium ${getLevelColor(log.level)}`}>
74
- {log.level}
75
- </span>
76
- <div className="flex-1 min-w-0">
77
- <p className="text-sm text-gray-900">{log.message}</p>
78
- <div className="flex gap-4 mt-1 text-xs text-gray-500">
79
- <span>{new Date(log.timestamp).toLocaleString()}</span>
80
- <span>{log.source}</span>
81
- </div>
82
- </div>
83
- </div>
84
- </div>
85
- ))}
86
- </div>
87
- )}
88
- </div>
89
- );
90
- };
@@ -1,106 +0,0 @@
1
- import React from 'react';
2
- import { useNotifications, type Notification } from '../../hooks/useNotifications';
3
- // import { Button } from '../shared/Button';
4
- import { BellIcon, CheckCircleIcon, ExclamationTriangleIcon, InformationCircleIcon, XCircleIcon } from '@heroicons/react/24/outline';
5
- import { Popover, Transition } from '@headlessui/react';
6
- import { Fragment } from 'react';
7
- import { clsx } from 'clsx';
8
-
9
- const NotificationIcon = ({ type }: { type: Notification['type'] }) => {
10
- switch (type) {
11
- case 'success': return <CheckCircleIcon className="h-6 w-6 text-green-500" />;
12
- case 'warning': return <ExclamationTriangleIcon className="h-6 w-6 text-yellow-500" />;
13
- case 'error': return <XCircleIcon className="h-6 w-6 text-red-500" />;
14
- default: return <InformationCircleIcon className="h-6 w-6 text-blue-500" />;
15
- }
16
- };
17
-
18
- export const NotificationFeed: React.FC = () => {
19
- const { notifications, unreadCount, markAsRead, markAllAsRead } = useNotifications();
20
-
21
- return (
22
- <Popover className="relative">
23
- {({ open }) => (
24
- <>
25
- <Popover.Button className={clsx(
26
- "relative p-2 rounded-full hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-primus-500",
27
- open && "bg-gray-100"
28
- )}>
29
- <BellIcon className="h-6 w-6 text-gray-600" aria-hidden="true" />
30
- {unreadCount > 0 && (
31
- <span className="absolute top-1 right-1 block h-2.5 w-2.5 rounded-full bg-red-500 ring-2 ring-white" />
32
- )}
33
- </Popover.Button>
34
- <Transition
35
- as={Fragment}
36
- enter="transition ease-out duration-200"
37
- enterFrom="opacity-0 translate-y-1"
38
- enterTo="opacity-100 translate-y-0"
39
- leave="transition ease-in duration-150"
40
- leaveFrom="opacity-100 translate-y-0"
41
- leaveTo="opacity-0 translate-y-1"
42
- >
43
- <Popover.Panel className="absolute right-0 z-10 mt-2 w-80 origin-top-right rounded-md bg-white py-1 shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none sm:w-96">
44
- <div className="px-4 py-3 flex items-center justify-between border-b border-gray-100">
45
- <h3 className="text-sm font-semibold text-gray-900">Notifications</h3>
46
- {unreadCount > 0 && (
47
- <button
48
- onClick={() => markAllAsRead()}
49
- className="text-xs font-medium text-primus-600 hover:text-primus-500"
50
- >
51
- Mark all as read
52
- </button>
53
- )}
54
- </div>
55
- <div className="max-h-96 overflow-y-auto">
56
- {notifications.length === 0 ? (
57
- <div className="px-4 py-6 text-center text-sm text-gray-500">
58
- No new notifications
59
- </div>
60
- ) : (
61
- <div className="divide-y divide-gray-100">
62
- {notifications.map((notification) => (
63
- <div
64
- key={notification.id}
65
- className={clsx(
66
- "flex gap-3 px-4 py-4 hover:bg-gray-50 transition-colors cursor-pointer",
67
- !notification.read && "bg-blue-50/50"
68
- )}
69
- onClick={() => markAsRead(notification.id)}
70
- >
71
- <div className="flex-shrink-0 mt-0.5">
72
- <NotificationIcon type={notification.type} />
73
- </div>
74
- <div className="flex-1 min-w-0">
75
- <p className="text-sm font-medium text-gray-900">
76
- {notification.title}
77
- </p>
78
- <p className="text-sm text-gray-500 truncate">
79
- {notification.message}
80
- </p>
81
- <p className="mt-1 text-xs text-gray-400">
82
- {new Date(notification.timestamp).toLocaleDateString()}
83
- </p>
84
- </div>
85
- {!notification.read && (
86
- <div className="flex-shrink-0 self-center">
87
- <span className="block h-2 w-2 rounded-full bg-primus-500" />
88
- </div>
89
- )}
90
- </div>
91
- ))}
92
- </div>
93
- )}
94
- </div>
95
- <div className="border-t border-gray-100 px-4 py-2">
96
- <a href="#" className="block text-center text-sm font-medium text-primus-600 hover:text-primus-500">
97
- View all notifications
98
- </a>
99
- </div>
100
- </Popover.Panel>
101
- </Transition>
102
- </>
103
- )}
104
- </Popover>
105
- );
106
- };