@aiready/components 0.11.13 → 0.11.15

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.
@@ -0,0 +1,121 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { MessageSquare, X, Send, Loader2 } from 'lucide-react';
6
+ import { cn } from '../utils/cn';
7
+
8
+ export interface FeedbackWidgetProps {
9
+ apiEndpoint?: string;
10
+ onSuccess?: (message: string) => void;
11
+ onError?: (error: any) => void;
12
+ title?: string;
13
+ description?: string;
14
+ className?: string;
15
+ }
16
+
17
+ export function FeedbackWidget({
18
+ apiEndpoint = '/api/feedback',
19
+ onSuccess,
20
+ onError,
21
+ title = 'Share Feedback',
22
+ description = 'What features would you like to see? Found a bug? Let us know!',
23
+ className,
24
+ }: FeedbackWidgetProps) {
25
+ const [isOpen, setIsOpen] = useState(false);
26
+ const [message, setMessage] = useState('');
27
+ const [status, setStatus] = useState<'idle' | 'loading'>('idle');
28
+
29
+ const handleSubmit = async (e: React.FormEvent) => {
30
+ e.preventDefault();
31
+ if (!message.trim()) return;
32
+
33
+ setStatus('loading');
34
+ try {
35
+ const res = await fetch(apiEndpoint, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ message }),
39
+ });
40
+
41
+ if (!res.ok) throw new Error('Failed to send feedback');
42
+
43
+ onSuccess?.(message);
44
+ setMessage('');
45
+ setIsOpen(false);
46
+ } catch (err) {
47
+ onError?.(err);
48
+ } finally {
49
+ setStatus('idle');
50
+ }
51
+ };
52
+
53
+ return (
54
+ <div className={cn('fixed bottom-6 right-6 z-50', className)}>
55
+ <AnimatePresence>
56
+ {isOpen && (
57
+ <motion.div
58
+ initial={{ opacity: 0, scale: 0.9, y: 20 }}
59
+ animate={{ opacity: 1, scale: 1, y: 0 }}
60
+ exit={{ opacity: 0, scale: 0.9, y: 20 }}
61
+ className="absolute bottom-16 right-0 w-80 rounded-2xl p-4 bg-slate-900 border border-cyan-500/30 shadow-2xl backdrop-blur-xl"
62
+ >
63
+ <div className="flex items-center justify-between mb-4">
64
+ <h4 className="font-bold text-white flex items-center gap-2 text-sm">
65
+ <MessageSquare className="w-4 h-4 text-cyan-400" />
66
+ {title}
67
+ </h4>
68
+ <button
69
+ onClick={() => setIsOpen(false)}
70
+ className="text-slate-500 hover:text-white"
71
+ >
72
+ <X className="w-4 h-4" />
73
+ </button>
74
+ </div>
75
+
76
+ <p className="text-xs text-slate-400 mb-4">{description}</p>
77
+
78
+ <form onSubmit={handleSubmit} className="space-y-3">
79
+ <textarea
80
+ autoFocus
81
+ value={message}
82
+ onChange={(e) => setMessage(e.target.value)}
83
+ placeholder="Type your feedback here..."
84
+ required
85
+ className="w-full bg-slate-800/50 border border-slate-700 rounded-xl px-3 py-2 text-sm text-white h-24 resize-none focus:outline-none focus:ring-1 focus:ring-cyan-500 transition-all"
86
+ />
87
+ <button
88
+ type="submit"
89
+ disabled={status === 'loading' || !message.trim()}
90
+ className="w-full py-2 bg-cyan-600 hover:bg-cyan-500 disabled:opacity-50 text-white font-bold rounded-lg text-sm transition-all flex items-center justify-center gap-2"
91
+ >
92
+ {status === 'loading' ? (
93
+ <Loader2 className="w-4 h-4 animate-spin" />
94
+ ) : (
95
+ <>
96
+ <Send className="w-3 h-3" />
97
+ Send Feedback
98
+ </>
99
+ )}
100
+ </button>
101
+ </form>
102
+ </motion.div>
103
+ )}
104
+ </AnimatePresence>
105
+
106
+ <motion.button
107
+ whileHover={{ scale: 1.05 }}
108
+ whileTap={{ scale: 0.95 }}
109
+ onClick={() => setIsOpen(!isOpen)}
110
+ className="w-12 h-12 bg-gradient-to-br from-cyan-600 to-blue-600 text-white rounded-full flex items-center justify-center shadow-lg shadow-cyan-500/20 group overflow-hidden relative"
111
+ >
112
+ <div className="absolute inset-0 bg-white/20 opacity-0 group-hover:opacity-100 transition-opacity" />
113
+ {isOpen ? (
114
+ <X className="w-6 h-6" />
115
+ ) : (
116
+ <MessageSquare className="w-6 h-6" />
117
+ )}
118
+ </motion.button>
119
+ </div>
120
+ );
121
+ }
@@ -1,12 +1,3 @@
1
- export {
2
- LoadingSpinner,
3
- LoadingOverlay,
4
- type LoadingSpinnerProps,
5
- type LoadingOverlayProps,
6
- } from './LoadingSpinner';
7
- export {
8
- ErrorDisplay,
9
- EmptyState,
10
- type ErrorDisplayProps,
11
- type EmptyStateProps,
12
- } from './ErrorDisplay';
1
+ export * from './LoadingSpinner';
2
+ export * from './ErrorDisplay';
3
+ export * from './FeedbackWidget';
package/src/index.ts CHANGED
@@ -7,10 +7,15 @@ export {
7
7
  CardTitle,
8
8
  CardDescription,
9
9
  CardContent,
10
+ GlassCard,
11
+ GlassCardHeader,
12
+ GlassCardContent,
10
13
  } from './components/card';
11
14
  export { Input, type InputProps } from './components/input';
12
15
  export { Label, type LabelProps } from './components/label';
13
16
  export { Badge, badgeVariants, type BadgeProps } from './components/badge';
17
+ export { Modal, type ModalProps } from './components/modal';
18
+ export * from './components/icons';
14
19
 
15
20
  // Layout Components
16
21
  export { Container, type ContainerProps } from './components/container';
@@ -39,16 +44,24 @@ export { CodeBlock, InlineCode, type CodeBlockProps } from './code-block';
39
44
  // Navigation
40
45
  export {
41
46
  Breadcrumb,
47
+ PlatformShell,
42
48
  type BreadcrumbProps,
43
49
  type BreadcrumbItem,
50
+ type PlatformShellProps,
51
+ type NavItem,
52
+ type User,
53
+ type Team,
54
+ type TeamMember,
44
55
  } from './navigation';
45
56
 
46
57
  // Data Display
47
58
  export {
48
59
  ScoreBar,
49
60
  ScoreCard,
61
+ ScoreCircle,
50
62
  type ScoreBarProps,
51
63
  type ScoreCardProps,
64
+ type ScoreCircleProps,
52
65
  type ScoreRating,
53
66
  } from './data-display';
54
67
 
@@ -57,10 +70,12 @@ export {
57
70
  LoadingSpinner,
58
71
  LoadingOverlay,
59
72
  ErrorDisplay,
73
+ FeedbackWidget,
60
74
  EmptyState,
61
75
  type LoadingSpinnerProps,
62
76
  type LoadingOverlayProps,
63
77
  type ErrorDisplayProps,
78
+ type FeedbackWidgetProps,
64
79
  type EmptyStateProps,
65
80
  } from './feedback';
66
81
 
@@ -1,69 +1,43 @@
1
1
  'use client';
2
2
 
3
- import React from 'react';
3
+ import { motion } from 'framer-motion';
4
4
  import { cn } from '../utils/cn';
5
5
 
6
6
  export interface BreadcrumbItem {
7
7
  label: string;
8
- href?: string;
8
+ href: string;
9
9
  }
10
10
 
11
11
  export interface BreadcrumbProps {
12
12
  items: BreadcrumbItem[];
13
- separator?: React.ReactNode;
14
13
  className?: string;
15
14
  }
16
15
 
17
- const DefaultSeparator = () => (
18
- <svg
19
- className="h-4 w-4 text-slate-400"
20
- fill="none"
21
- viewBox="0 0 24 24"
22
- strokeWidth="1.5"
23
- stroke="currentColor"
24
- >
25
- <path
26
- strokeLinecap="round"
27
- strokeLinejoin="round"
28
- d="M8.25 4.5l7.5 7.5-7.5 7.5"
29
- />
30
- </svg>
31
- );
32
-
33
- export function Breadcrumb({ items, separator, className }: BreadcrumbProps) {
34
- const Separator = separator || <DefaultSeparator />;
35
-
16
+ export function Breadcrumb({ items, className }: BreadcrumbProps) {
36
17
  return (
37
- <nav
38
- className={cn('flex items-center gap-1 text-sm', className)}
39
- aria-label="Breadcrumb"
40
- >
41
- <ol className="flex items-center gap-1">
42
- {items.map((item, index) => {
43
- const isLast = index === items.length - 1;
44
-
45
- return (
46
- <li key={index} className="flex items-center gap-1">
47
- {index > 0 && <span className="flex-shrink-0">{Separator}</span>}
48
- {item.href && !isLast ? (
49
- <a
50
- href={item.href}
51
- className="text-slate-600 hover:text-slate-900 transition-colors"
52
- >
53
- {item.label}
54
- </a>
55
- ) : (
56
- <span
57
- className={
58
- isLast ? 'font-medium text-slate-900' : 'text-slate-600'
59
- }
60
- >
61
- {item.label}
62
- </span>
63
- )}
64
- </li>
65
- );
66
- })}
18
+ <nav aria-label="Breadcrumb" className={cn('mb-8', className)}>
19
+ <ol className="flex items-center gap-2 text-sm">
20
+ {items.map((item, index) => (
21
+ <motion.li
22
+ key={item.href}
23
+ initial={{ opacity: 0, x: -10 }}
24
+ animate={{ opacity: 1, x: 0 }}
25
+ transition={{ delay: index * 0.1 }}
26
+ className="flex items-center gap-2"
27
+ >
28
+ {index > 0 && <span className="text-slate-600">/</span>}
29
+ {index === items.length - 1 ? (
30
+ <span className="text-cyan-400 font-medium">{item.label}</span>
31
+ ) : (
32
+ <a
33
+ href={item.href}
34
+ className="text-slate-400 hover:text-white transition-colors"
35
+ >
36
+ {item.label}
37
+ </a>
38
+ )}
39
+ </motion.li>
40
+ ))}
67
41
  </ol>
68
42
  </nav>
69
43
  );
@@ -0,0 +1,408 @@
1
+ 'use client';
2
+
3
+ import React, { useState, useRef, useEffect } from 'react';
4
+ import { motion, AnimatePresence } from 'framer-motion';
5
+ import { cn } from '../utils/cn';
6
+ import {
7
+ RocketIcon,
8
+ ChartIcon,
9
+ TrendingUpIcon,
10
+ RobotIcon,
11
+ SettingsIcon,
12
+ } from '../components/icons';
13
+
14
+ // --- Types ---
15
+
16
+ export interface NavItem {
17
+ href: string;
18
+ label: string;
19
+ icon: React.ElementType;
20
+ }
21
+
22
+ export interface User {
23
+ id: string;
24
+ name?: string | null;
25
+ email?: string | null;
26
+ image?: string | null;
27
+ }
28
+
29
+ export interface Team {
30
+ id: string;
31
+ name: string;
32
+ }
33
+
34
+ export interface TeamMember {
35
+ teamId: string;
36
+ team: Team;
37
+ }
38
+
39
+ export interface PlatformShellProps {
40
+ children: React.ReactNode;
41
+ user: User | null;
42
+ teams?: TeamMember[];
43
+ overallScore?: number | null;
44
+ activePage?: string;
45
+ pathname?: string;
46
+ onNavigate?: (href: string) => void;
47
+ onSignOut?: () => void;
48
+ onSwitchTeam?: (teamId: string | 'personal') => void;
49
+ logoUrl?: string;
50
+ navItems?: NavItem[];
51
+ LinkComponent?: React.ElementType;
52
+ }
53
+
54
+ // --- Internal Components ---
55
+
56
+ function NavItemComponent({
57
+ href,
58
+ label,
59
+ icon: Icon,
60
+ isActive,
61
+ onClick,
62
+ LinkComponent = 'a',
63
+ }: NavItem & {
64
+ isActive: boolean;
65
+ onClick?: (e: React.MouseEvent) => void;
66
+ LinkComponent?: any;
67
+ }) {
68
+ const Component = LinkComponent;
69
+ return (
70
+ <Component href={href} onClick={onClick} className="block group">
71
+ <div
72
+ className={cn(
73
+ 'flex items-center gap-3 px-4 py-3 rounded-xl transition-all',
74
+ isActive
75
+ ? 'bg-cyan-500/10 text-cyan-400 border border-cyan-500/20'
76
+ : 'text-slate-400 hover:text-white hover:bg-slate-800/50 border border-transparent'
77
+ )}
78
+ >
79
+ <Icon
80
+ className={cn(
81
+ 'w-5 h-5',
82
+ isActive
83
+ ? 'text-cyan-400'
84
+ : 'text-slate-500 group-hover:text-slate-300'
85
+ )}
86
+ />
87
+ <span className="text-sm font-semibold tracking-tight">{label}</span>
88
+ {isActive && (
89
+ <motion.div
90
+ layoutId="sidebar-active"
91
+ className="ml-auto w-1.5 h-1.5 rounded-full bg-cyan-400 shadow-[0_0_8px_rgba(34,211,238,0.8)]"
92
+ />
93
+ )}
94
+ </div>
95
+ </Component>
96
+ );
97
+ }
98
+
99
+ function UserMenu({
100
+ user,
101
+ teams = [],
102
+ currentTeamId,
103
+ onSwitchTeam,
104
+ onSignOut,
105
+ }: {
106
+ user: User;
107
+ teams: TeamMember[];
108
+ currentTeamId: string | 'personal';
109
+ onSwitchTeam?: (teamId: string | 'personal') => void;
110
+ onSignOut?: () => void;
111
+ }) {
112
+ const [isOpen, setIsOpen] = useState(false);
113
+ const dropdownRef = useRef<HTMLDivElement>(null);
114
+
115
+ useEffect(() => {
116
+ function handleClickOutside(event: MouseEvent) {
117
+ if (
118
+ dropdownRef.current &&
119
+ !dropdownRef.current.contains(event.target as Node)
120
+ ) {
121
+ setIsOpen(false);
122
+ }
123
+ }
124
+ document.addEventListener('mousedown', handleClickOutside);
125
+ return () => document.removeEventListener('mousedown', handleClickOutside);
126
+ }, []);
127
+
128
+ const currentWorkspaceName =
129
+ currentTeamId === 'personal'
130
+ ? 'Personal Workspace'
131
+ : teams.find((t) => t.teamId === currentTeamId)?.team.name ||
132
+ 'Team Workspace';
133
+
134
+ return (
135
+ <div className="relative" ref={dropdownRef}>
136
+ <button
137
+ onClick={() => setIsOpen(!isOpen)}
138
+ className="flex items-center gap-2 p-1.5 rounded-xl hover:bg-slate-800/50 transition-all border border-transparent hover:border-slate-700/50"
139
+ >
140
+ {user.image ? (
141
+ <img
142
+ src={user.image}
143
+ alt={user.name || 'User'}
144
+ className="w-8 h-8 rounded-lg border border-cyan-500/30"
145
+ />
146
+ ) : (
147
+ <div className="w-8 h-8 rounded-lg bg-indigo-600 flex items-center justify-center text-xs font-bold text-white">
148
+ {user.name?.[0] || user.email?.[0]}
149
+ </div>
150
+ )}
151
+ <div className="text-left hidden sm:block">
152
+ <p className="text-xs font-bold text-white leading-none mb-0.5">
153
+ {user.name || user.email?.split('@')[0]}
154
+ </p>
155
+ <p className="text-[10px] text-slate-400 leading-none truncate max-w-[100px]">
156
+ {currentWorkspaceName}
157
+ </p>
158
+ </div>
159
+ <svg
160
+ className={cn(
161
+ 'w-4 h-4 text-slate-500 transition-transform',
162
+ isOpen && 'rotate-180'
163
+ )}
164
+ fill="none"
165
+ viewBox="0 0 24 24"
166
+ stroke="currentColor"
167
+ >
168
+ <path
169
+ strokeLinecap="round"
170
+ strokeLinejoin="round"
171
+ strokeWidth={2}
172
+ d="M19 9l-7 7-7-7"
173
+ />
174
+ </svg>
175
+ </button>
176
+
177
+ <AnimatePresence>
178
+ {isOpen && (
179
+ <motion.div
180
+ initial={{ opacity: 0, y: 10, scale: 0.95 }}
181
+ animate={{ opacity: 1, y: 0, scale: 1 }}
182
+ exit={{ opacity: 0, y: 10, scale: 0.95 }}
183
+ className="absolute right-0 mt-2 w-64 bg-slate-900 border border-slate-700 rounded-2xl shadow-2xl z-50 overflow-hidden"
184
+ >
185
+ <div className="p-4 border-b border-slate-800">
186
+ <p className="text-sm font-bold text-white">{user.name}</p>
187
+ <p className="text-xs text-slate-400 truncate">{user.email}</p>
188
+ </div>
189
+
190
+ <div className="p-2 border-b border-slate-800">
191
+ <p className="px-3 py-1 text-[10px] font-black text-slate-500 uppercase tracking-widest mb-1">
192
+ Workspaces
193
+ </p>
194
+ <button
195
+ onClick={() => {
196
+ onSwitchTeam?.('personal');
197
+ setIsOpen(false);
198
+ }}
199
+ className={cn(
200
+ 'w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
201
+ currentTeamId === 'personal'
202
+ ? 'bg-cyan-500/10 text-cyan-400'
203
+ : 'text-slate-400 hover:bg-slate-800 hover:text-white'
204
+ )}
205
+ >
206
+ <div className="w-5 h-5 rounded bg-slate-700 flex items-center justify-center text-[10px] font-bold">
207
+ P
208
+ </div>
209
+ Personal
210
+ </button>
211
+ {teams.map((t) => (
212
+ <button
213
+ key={t.teamId}
214
+ onClick={() => {
215
+ onSwitchTeam?.(t.teamId);
216
+ setIsOpen(false);
217
+ }}
218
+ className={cn(
219
+ 'w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors',
220
+ currentTeamId === t.teamId
221
+ ? 'bg-purple-500/10 text-purple-400'
222
+ : 'text-slate-400 hover:bg-slate-800 hover:text-white'
223
+ )}
224
+ >
225
+ <div className="w-5 h-5 rounded bg-purple-600 flex items-center justify-center text-[10px] font-bold">
226
+ {t.team.name[0]}
227
+ </div>
228
+ {t.team.name}
229
+ </button>
230
+ ))}
231
+ </div>
232
+
233
+ <div className="p-2">
234
+ <button
235
+ onClick={onSignOut}
236
+ className="w-full flex items-center gap-3 px-3 py-2 rounded-lg text-sm text-red-400 hover:bg-red-500/10 transition-colors"
237
+ >
238
+ <svg
239
+ className="w-4 h-4"
240
+ fill="none"
241
+ viewBox="0 0 24 24"
242
+ stroke="currentColor"
243
+ >
244
+ <path
245
+ strokeLinecap="round"
246
+ strokeLinejoin="round"
247
+ strokeWidth={2}
248
+ d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"
249
+ />
250
+ </svg>
251
+ Sign out
252
+ </button>
253
+ </div>
254
+ </motion.div>
255
+ )}
256
+ </AnimatePresence>
257
+ </div>
258
+ );
259
+ }
260
+
261
+ // --- Main Shell ---
262
+
263
+ export function PlatformShell({
264
+ children,
265
+ user,
266
+ teams = [],
267
+ overallScore,
268
+ activePage,
269
+ pathname = '',
270
+ onNavigate,
271
+ onSignOut,
272
+ onSwitchTeam,
273
+ logoUrl = '/logo-text-transparent-dark-theme.png',
274
+ navItems = [
275
+ { href: '/dashboard', label: 'Dashboard', icon: RocketIcon },
276
+ { href: '/strategy', label: 'Scan Strategy', icon: SettingsIcon },
277
+ { href: '/trends', label: 'Trends Explorer', icon: TrendingUpIcon },
278
+ { href: '/map', label: 'Codebase Map', icon: RobotIcon },
279
+ { href: '/metrics', label: 'Methodology', icon: ChartIcon },
280
+ ],
281
+ LinkComponent = 'a',
282
+ }: PlatformShellProps) {
283
+ const [currentTeamId, setCurrentTeamId] = useState<string | 'personal'>(
284
+ 'personal'
285
+ );
286
+
287
+ const handleSwitchTeam = (teamId: string | 'personal') => {
288
+ setCurrentTeamId(teamId);
289
+ onSwitchTeam?.(teamId);
290
+ };
291
+
292
+ const Sidebar = () => (
293
+ <aside className="hidden lg:flex flex-col w-64 bg-slate-950/50 border-r border-slate-800 h-screen sticky top-0 overflow-y-auto">
294
+ <div className="p-6 flex flex-col h-full">
295
+ <div className="flex items-center justify-center mb-10">
296
+ <img src={logoUrl} alt="AIReady" className="h-9 w-auto" />
297
+ </div>
298
+
299
+ <nav className="space-y-1.5 flex-1">
300
+ <p className="px-4 py-2 text-[10px] font-black text-slate-500 uppercase tracking-widest mb-2">
301
+ Workspace
302
+ </p>
303
+ {navItems.map((item) => (
304
+ <NavItemComponent
305
+ key={item.href}
306
+ {...item}
307
+ LinkComponent={LinkComponent}
308
+ isActive={
309
+ pathname === item.href ||
310
+ (item.href !== '/dashboard' && pathname.startsWith(item.href))
311
+ }
312
+ onClick={(e) => {
313
+ if (onNavigate) {
314
+ e.preventDefault();
315
+ onNavigate(item.href);
316
+ }
317
+ }}
318
+ />
319
+ ))}
320
+ </nav>
321
+
322
+ <div className="mt-auto pt-6 space-y-4">
323
+ <div className="p-4 rounded-2xl bg-gradient-to-br from-indigo-500/10 to-purple-500/10 border border-indigo-500/20">
324
+ <p className="text-xs font-bold text-white mb-1">AI Insights</p>
325
+ <p className="text-[10px] text-slate-400 leading-relaxed">
326
+ {overallScore != null
327
+ ? `Your codebase is ${overallScore}% ready for AI agents.`
328
+ : 'Run a scan to see your AI-readiness score.'}
329
+ </p>
330
+ </div>
331
+ </div>
332
+ </div>
333
+ </aside>
334
+ );
335
+
336
+ const Navbar = () => (
337
+ <header className="sticky top-0 z-20 h-16 border-b border-indigo-500/10 backdrop-blur-md bg-slate-950/20 px-4 sm:px-6 lg:px-8">
338
+ <div className="h-full flex items-center justify-between">
339
+ <div className="flex items-center gap-4">
340
+ <p className="text-xs font-semibold text-slate-500 uppercase tracking-widest hidden sm:block">
341
+ {activePage || 'Dashboard'}
342
+ </p>
343
+ </div>
344
+
345
+ <div className="flex items-center gap-4">
346
+ {user && (
347
+ <UserMenu
348
+ user={user}
349
+ teams={teams}
350
+ currentTeamId={currentTeamId}
351
+ onSwitchTeam={handleSwitchTeam}
352
+ onSignOut={onSignOut}
353
+ />
354
+ )}
355
+ </div>
356
+ </div>
357
+ </header>
358
+ );
359
+
360
+ return (
361
+ <div
362
+ className={cn(
363
+ 'min-h-screen bg-[#0a0a0f]',
364
+ user && 'flex overflow-hidden'
365
+ )}
366
+ >
367
+ {user && <Sidebar />}
368
+
369
+ <div className={cn('flex-1 flex flex-col min-w-0', user && 'h-screen')}>
370
+ {user && <Navbar />}
371
+
372
+ <main
373
+ className={cn('relative flex-1', user && 'overflow-y-auto', 'z-10')}
374
+ >
375
+ {user && (
376
+ <>
377
+ <div className="absolute inset-0 pointer-events-none -z-10 overflow-hidden">
378
+ <div
379
+ className="absolute rounded-full blur-[60px] opacity-20 bg-radial-gradient from-blue-600/60 to-transparent w-96 h-96 -top-48 -right-48"
380
+ style={{
381
+ background:
382
+ 'radial-gradient(circle, rgba(59, 130, 246, 0.6), transparent)',
383
+ }}
384
+ />
385
+ <div
386
+ className="absolute rounded-full blur-[60px] opacity-20 bg-radial-gradient from-purple-600/60 to-transparent w-80 h-80 bottom-0 -left-40"
387
+ style={{
388
+ background:
389
+ 'radial-gradient(circle, rgba(139, 92, 246, 0.6), transparent)',
390
+ }}
391
+ />
392
+ </div>
393
+ <div
394
+ className="absolute inset-0 opacity-10 -z-10"
395
+ style={{
396
+ backgroundImage:
397
+ 'linear-gradient(rgba(59, 130, 246, 0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(59, 130, 246, 0.03) 1px, transparent 1px)',
398
+ backgroundSize: '50px 50px',
399
+ }}
400
+ />
401
+ </>
402
+ )}
403
+ {children}
404
+ </main>
405
+ </div>
406
+ </div>
407
+ );
408
+ }