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:
74
src/components/common/ErrorBoundary.tsx
Normal file
74
src/components/common/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* Error Boundary Component
|
||||
* Catches and displays errors in the component tree
|
||||
*/
|
||||
import { Component, ReactNode } from 'react';
|
||||
import { AlertTriangle, RefreshCw } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
|
||||
interface Props {
|
||||
children: ReactNode;
|
||||
fallback?: ReactNode;
|
||||
}
|
||||
|
||||
interface State {
|
||||
hasError: boolean;
|
||||
error: Error | null;
|
||||
}
|
||||
|
||||
export class ErrorBoundary extends Component<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = { hasError: false, error: null };
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error): State {
|
||||
return { hasError: true, error };
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error('Error caught by boundary:', error, errorInfo);
|
||||
}
|
||||
|
||||
handleReset = () => {
|
||||
this.setState({ hasError: false, error: null });
|
||||
};
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center p-6">
|
||||
<Card className="max-w-md">
|
||||
<CardHeader>
|
||||
<div className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-6 w-6 text-destructive" />
|
||||
<CardTitle>Something went wrong</CardTitle>
|
||||
</div>
|
||||
<CardDescription>
|
||||
An unexpected error occurred. Please try again.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{this.state.error && (
|
||||
<pre className="rounded-lg bg-muted p-4 text-sm overflow-auto max-h-40">
|
||||
{this.state.error.message}
|
||||
</pre>
|
||||
)}
|
||||
<Button onClick={this.handleReset} className="w-full">
|
||||
<RefreshCw className="mr-2 h-4 w-4" />
|
||||
Try Again
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
36
src/components/common/LoadingSpinner.tsx
Normal file
36
src/components/common/LoadingSpinner.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Loading Spinner Component
|
||||
* Displays a spinning loader animation
|
||||
*/
|
||||
import { Loader2 } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface LoadingSpinnerProps {
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const sizeClasses = {
|
||||
sm: 'h-4 w-4',
|
||||
md: 'h-8 w-8',
|
||||
lg: 'h-12 w-12',
|
||||
};
|
||||
|
||||
export function LoadingSpinner({ size = 'md', className }: LoadingSpinnerProps) {
|
||||
return (
|
||||
<div className={cn('flex items-center justify-center', className)}>
|
||||
<Loader2 className={cn('animate-spin text-primary', sizeClasses[size])} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Full page loading spinner
|
||||
*/
|
||||
export function PageLoader() {
|
||||
return (
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<LoadingSpinner size="lg" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
46
src/components/common/StatusBadge.tsx
Normal file
46
src/components/common/StatusBadge.tsx
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Status Badge Component
|
||||
* Displays connection/state status with color coding
|
||||
*/
|
||||
import { cn } from '@/lib/utils';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
type Status = 'connected' | 'disconnected' | 'connecting' | 'error' | 'running' | 'stopped' | 'starting';
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: Status;
|
||||
label?: string;
|
||||
showDot?: boolean;
|
||||
}
|
||||
|
||||
const statusConfig: Record<Status, { label: string; variant: 'success' | 'secondary' | 'warning' | 'destructive' }> = {
|
||||
connected: { label: 'Connected', variant: 'success' },
|
||||
running: { label: 'Running', variant: 'success' },
|
||||
disconnected: { label: 'Disconnected', variant: 'secondary' },
|
||||
stopped: { label: 'Stopped', variant: 'secondary' },
|
||||
connecting: { label: 'Connecting', variant: 'warning' },
|
||||
starting: { label: 'Starting', variant: 'warning' },
|
||||
error: { label: 'Error', variant: 'destructive' },
|
||||
};
|
||||
|
||||
export function StatusBadge({ status, label, showDot = true }: StatusBadgeProps) {
|
||||
const config = statusConfig[status];
|
||||
const displayLabel = label || config.label;
|
||||
|
||||
return (
|
||||
<Badge variant={config.variant} className="gap-1.5">
|
||||
{showDot && (
|
||||
<span
|
||||
className={cn(
|
||||
'h-1.5 w-1.5 rounded-full',
|
||||
config.variant === 'success' && 'bg-green-600',
|
||||
config.variant === 'secondary' && 'bg-gray-400',
|
||||
config.variant === 'warning' && 'bg-yellow-600 animate-pulse',
|
||||
config.variant === 'destructive' && 'bg-red-600'
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{displayLabel}
|
||||
</Badge>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user