Initial commit from template
This commit is contained in:
249
.claude/commands/component.md
Normal file
249
.claude/commands/component.md
Normal file
@@ -0,0 +1,249 @@
|
||||
# UI Component Generator
|
||||
|
||||
Du bist ein Experte für React/Next.js Components mit Tailwind CSS und Spartan UI. Erstelle moderne, wiederverwendbare UI Components.
|
||||
|
||||
## Deine Aufgaben
|
||||
|
||||
Erstelle Components die:
|
||||
- TypeScript mit korrekten Props-Typen verwenden
|
||||
- Tailwind CSS für Styling nutzen
|
||||
- Spartan UI Patterns folgen
|
||||
- Accessible sind (ARIA, Keyboard Navigation)
|
||||
- Server/Client Component korrekt trennen
|
||||
|
||||
## Component Template
|
||||
|
||||
```typescript
|
||||
// components/ui/[ComponentName].tsx
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ComponentNameProps {
|
||||
children?: React.ReactNode;
|
||||
className?: string;
|
||||
variant?: 'default' | 'primary' | 'secondary';
|
||||
size?: 'sm' | 'md' | 'lg';
|
||||
}
|
||||
|
||||
export function ComponentName({
|
||||
children,
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
}: ComponentNameProps) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
// Base styles
|
||||
'rounded-lg border',
|
||||
// Variants
|
||||
{
|
||||
'bg-background': variant === 'default',
|
||||
'bg-primary text-primary-foreground': variant === 'primary',
|
||||
'bg-secondary text-secondary-foreground': variant === 'secondary',
|
||||
},
|
||||
// Sizes
|
||||
{
|
||||
'p-2 text-sm': size === 'sm',
|
||||
'p-4 text-base': size === 'md',
|
||||
'p-6 text-lg': size === 'lg',
|
||||
},
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Häufige Components
|
||||
|
||||
### Button
|
||||
```typescript
|
||||
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||
variant?: 'default' | 'primary' | 'outline' | 'ghost' | 'destructive';
|
||||
size?: 'sm' | 'md' | 'lg' | 'icon';
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
children,
|
||||
className,
|
||||
variant = 'default',
|
||||
size = 'md',
|
||||
loading = false,
|
||||
disabled,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center rounded-md font-medium transition-colors',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2',
|
||||
'disabled:pointer-events-none disabled:opacity-50',
|
||||
{
|
||||
'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
|
||||
'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'default',
|
||||
'border border-input hover:bg-accent': variant === 'outline',
|
||||
'hover:bg-accent': variant === 'ghost',
|
||||
'bg-destructive text-destructive-foreground hover:bg-destructive/90': variant === 'destructive',
|
||||
},
|
||||
{
|
||||
'h-8 px-3 text-sm': size === 'sm',
|
||||
'h-10 px-4': size === 'md',
|
||||
'h-12 px-6 text-lg': size === 'lg',
|
||||
'h-10 w-10': size === 'icon',
|
||||
},
|
||||
className
|
||||
)}
|
||||
disabled={disabled || loading}
|
||||
{...props}
|
||||
>
|
||||
{loading && <Spinner className="mr-2 h-4 w-4" />}
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Card
|
||||
```typescript
|
||||
export function Card({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return (
|
||||
<div className={cn('rounded-xl border bg-card p-6 shadow-sm', className)}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function CardHeader({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={cn('mb-4', className)}>{children}</div>;
|
||||
}
|
||||
|
||||
export function CardTitle({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <h3 className={cn('text-xl font-semibold', className)}>{children}</h3>;
|
||||
}
|
||||
|
||||
export function CardContent({ children, className }: { children: React.ReactNode; className?: string }) {
|
||||
return <div className={cn('', className)}>{children}</div>;
|
||||
}
|
||||
```
|
||||
|
||||
### Input
|
||||
```typescript
|
||||
interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||
label?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export function Input({ label, error, className, id, ...props }: InputProps) {
|
||||
const inputId = id || label?.toLowerCase().replace(/\s/g, '-');
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
{label && (
|
||||
<label htmlFor={inputId} className="text-sm font-medium">
|
||||
{label}
|
||||
</label>
|
||||
)}
|
||||
<input
|
||||
id={inputId}
|
||||
className={cn(
|
||||
'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2',
|
||||
'text-sm placeholder:text-muted-foreground',
|
||||
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring',
|
||||
'disabled:cursor-not-allowed disabled:opacity-50',
|
||||
error && 'border-destructive',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
{error && <p className="text-sm text-destructive">{error}</p>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Modal/Dialog
|
||||
```typescript
|
||||
'use client';
|
||||
|
||||
import { useEffect } from 'react';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface ModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function Modal({ open, onClose, children, className }: ModalProps) {
|
||||
useEffect(() => {
|
||||
const handleEscape = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') onClose();
|
||||
};
|
||||
|
||||
if (open) {
|
||||
document.addEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = 'hidden';
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('keydown', handleEscape);
|
||||
document.body.style.overflow = '';
|
||||
};
|
||||
}, [open, onClose]);
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center">
|
||||
<div className="fixed inset-0 bg-black/50" onClick={onClose} />
|
||||
<div
|
||||
className={cn(
|
||||
'relative z-50 w-full max-w-lg rounded-lg bg-background p-6 shadow-lg',
|
||||
className
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## Client vs Server Components
|
||||
|
||||
```typescript
|
||||
// Server Component (default) - keine Interaktivität
|
||||
// components/UserList.tsx
|
||||
export async function UserList() {
|
||||
const users = await fetchUsers();
|
||||
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
|
||||
}
|
||||
|
||||
// Client Component - mit Interaktivität
|
||||
// components/Counter.tsx
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
|
||||
export function Counter() {
|
||||
const [count, setCount] = useState(0);
|
||||
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
|
||||
}
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **cn() Helper** - Für conditional classes
|
||||
2. **Forwardref** - Für DOM-Zugriff von außen
|
||||
3. **Composition** - Kleine, kombinierbare Components
|
||||
4. **Accessibility** - ARIA Labels, Keyboard Support
|
||||
5. **Dark Mode** - CSS Variables für Theming
|
||||
|
||||
---
|
||||
|
||||
Frage den Benutzer: Welche Component möchtest du erstellen?
|
||||
Beschreibe die gewünschte Funktionalität und das Design.
|
||||
Reference in New Issue
Block a user