feat(core): initialize project skeleton with Electron + React + TypeScript
Set up the complete project foundation for ClawX, a graphical AI assistant: - Electron main process with IPC handlers, menu, tray, and gateway management - React renderer with routing, layout components, and page scaffolding - Zustand state management for gateway, settings, channels, skills, chat, and cron - shadcn/ui components with Tailwind CSS and CSS variable theming - Build tooling with Vite, electron-builder, and TypeScript configuration - Testing setup with Vitest and Playwright - Development configurations (ESLint, Prettier, gitignore, env example)
This commit is contained in:
206
src/components/layout/Sidebar.tsx
Normal file
206
src/components/layout/Sidebar.tsx
Normal file
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* Sidebar Component
|
||||
* Navigation sidebar with menu items
|
||||
*/
|
||||
import { useState, useEffect } from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
import {
|
||||
Home,
|
||||
MessageSquare,
|
||||
Radio,
|
||||
Puzzle,
|
||||
Clock,
|
||||
Settings,
|
||||
ChevronLeft,
|
||||
ChevronRight,
|
||||
Terminal,
|
||||
ExternalLink,
|
||||
} from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import { useGatewayStore } from '@/stores/gateway';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
interface NavItemProps {
|
||||
to: string;
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
badge?: string;
|
||||
collapsed?: boolean;
|
||||
}
|
||||
|
||||
function NavItem({ to, icon, label, badge, collapsed }: NavItemProps) {
|
||||
return (
|
||||
<NavLink
|
||||
to={to}
|
||||
className={({ isActive }) =>
|
||||
cn(
|
||||
'flex items-center gap-3 rounded-lg px-3 py-2 text-sm font-medium transition-colors',
|
||||
'hover:bg-accent hover:text-accent-foreground',
|
||||
isActive
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground',
|
||||
collapsed && 'justify-center px-2'
|
||||
)
|
||||
}
|
||||
>
|
||||
{icon}
|
||||
{!collapsed && (
|
||||
<>
|
||||
<span className="flex-1">{label}</span>
|
||||
{badge && (
|
||||
<Badge variant="secondary" className="ml-auto">
|
||||
{badge}
|
||||
</Badge>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
export function Sidebar() {
|
||||
const location = useLocation();
|
||||
const sidebarCollapsed = useSettingsStore((state) => state.sidebarCollapsed);
|
||||
const setSidebarCollapsed = useSettingsStore((state) => state.setSidebarCollapsed);
|
||||
const devModeUnlocked = useSettingsStore((state) => state.devModeUnlocked);
|
||||
const setDevModeUnlocked = useSettingsStore((state) => state.setDevModeUnlocked);
|
||||
const gatewayStatus = useGatewayStore((state) => state.status);
|
||||
|
||||
const [versionClicks, setVersionClicks] = useState(0);
|
||||
const [appVersion, setAppVersion] = useState('0.1.0');
|
||||
|
||||
// Get app version
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.invoke('app:version').then((version) => {
|
||||
setAppVersion(version as string);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// Handle version click for dev mode unlock
|
||||
const handleVersionClick = () => {
|
||||
const clicks = versionClicks + 1;
|
||||
setVersionClicks(clicks);
|
||||
|
||||
if (clicks >= 5) {
|
||||
if (!devModeUnlocked) {
|
||||
setDevModeUnlocked(true);
|
||||
toast.success('Developer mode unlocked!');
|
||||
}
|
||||
setVersionClicks(0);
|
||||
}
|
||||
|
||||
// Reset after 2 seconds of inactivity
|
||||
setTimeout(() => setVersionClicks(0), 2000);
|
||||
};
|
||||
|
||||
// Open developer console
|
||||
const openDevConsole = () => {
|
||||
window.electron.openExternal('http://localhost:18789');
|
||||
};
|
||||
|
||||
const navItems = [
|
||||
{ to: '/', icon: <Home className="h-5 w-5" />, label: 'Dashboard' },
|
||||
{ to: '/chat', icon: <MessageSquare className="h-5 w-5" />, label: 'Chat' },
|
||||
{ to: '/channels', icon: <Radio className="h-5 w-5" />, label: 'Channels' },
|
||||
{ to: '/skills', icon: <Puzzle className="h-5 w-5" />, label: 'Skills' },
|
||||
{ to: '/cron', icon: <Clock className="h-5 w-5" />, label: 'Cron Tasks' },
|
||||
{ to: '/settings', icon: <Settings className="h-5 w-5" />, label: 'Settings' },
|
||||
];
|
||||
|
||||
return (
|
||||
<aside
|
||||
className={cn(
|
||||
'fixed left-0 top-0 z-40 flex h-screen flex-col border-r bg-background transition-all duration-300',
|
||||
sidebarCollapsed ? 'w-16' : 'w-64'
|
||||
)}
|
||||
>
|
||||
{/* Header with drag region for macOS */}
|
||||
<div className="drag-region flex h-14 items-center border-b px-4">
|
||||
{/* macOS traffic light spacing */}
|
||||
<div className="w-16" />
|
||||
{!sidebarCollapsed && (
|
||||
<h1 className="no-drag text-xl font-bold">ClawX</h1>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Navigation */}
|
||||
<nav className="flex-1 space-y-1 p-2">
|
||||
{navItems.map((item) => (
|
||||
<NavItem
|
||||
key={item.to}
|
||||
{...item}
|
||||
collapsed={sidebarCollapsed}
|
||||
/>
|
||||
))}
|
||||
</nav>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="border-t p-2 space-y-2">
|
||||
{/* Gateway Status */}
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center gap-2 rounded-lg px-3 py-2',
|
||||
sidebarCollapsed && 'justify-center px-2'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'h-2 w-2 rounded-full',
|
||||
gatewayStatus.state === 'running' && 'bg-green-500',
|
||||
gatewayStatus.state === 'starting' && 'bg-yellow-500 animate-pulse',
|
||||
gatewayStatus.state === 'stopped' && 'bg-gray-400',
|
||||
gatewayStatus.state === 'error' && 'bg-red-500'
|
||||
)}
|
||||
/>
|
||||
{!sidebarCollapsed && (
|
||||
<span className="text-xs text-muted-foreground">
|
||||
Gateway: {gatewayStatus.state}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Developer Mode Button */}
|
||||
{devModeUnlocked && !sidebarCollapsed && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="w-full justify-start"
|
||||
onClick={openDevConsole}
|
||||
>
|
||||
<Terminal className="h-4 w-4 mr-2" />
|
||||
Developer Console
|
||||
<ExternalLink className="h-3 w-3 ml-auto" />
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* Version */}
|
||||
<button
|
||||
onClick={handleVersionClick}
|
||||
className={cn(
|
||||
'w-full text-xs text-muted-foreground hover:text-foreground transition-colors',
|
||||
sidebarCollapsed ? 'text-center' : 'px-3'
|
||||
)}
|
||||
>
|
||||
{sidebarCollapsed ? `v${appVersion.split('.')[0]}` : `ClawX v${appVersion}`}
|
||||
</button>
|
||||
|
||||
{/* Collapse Toggle */}
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="w-full"
|
||||
onClick={() => setSidebarCollapsed(!sidebarCollapsed)}
|
||||
>
|
||||
{sidebarCollapsed ? (
|
||||
<ChevronRight className="h-4 w-4" />
|
||||
) : (
|
||||
<ChevronLeft className="h-4 w-4" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</aside>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user