@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.
- package/dist/components/button.d.ts +2 -2
- package/dist/components/button.js +9 -3
- package/dist/components/button.js.map +1 -1
- package/dist/components/card.d.ts +4 -1
- package/dist/components/card.js +27 -1
- package/dist/components/card.js.map +1 -1
- package/dist/index.d.ts +129 -8
- package/dist/index.js +1104 -97
- package/dist/index.js.map +1 -1
- package/package.json +4 -2
- package/src/code-block/CodeBlock.tsx +59 -64
- package/src/components/button.tsx +9 -1
- package/src/components/card.tsx +42 -0
- package/src/components/icons.tsx +409 -0
- package/src/components/modal.tsx +96 -0
- package/src/data-display/ScoreCircle.tsx +144 -0
- package/src/data-display/index.ts +2 -7
- package/src/feedback/FeedbackWidget.tsx +121 -0
- package/src/feedback/index.ts +3 -12
- package/src/index.ts +15 -0
- package/src/navigation/Breadcrumb.tsx +26 -52
- package/src/navigation/PlatformShell.tsx +408 -0
- package/src/navigation/index.ts +2 -5
|
@@ -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
|
+
}
|
package/src/feedback/index.ts
CHANGED
|
@@ -1,12 +1,3 @@
|
|
|
1
|
-
export
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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=
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
+
}
|