mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-28 15:54:56 +00:00
541 lines
14 KiB
Markdown
541 lines
14 KiB
Markdown
# Atomic Design Quick Start Guide
|
|
|
|
## Using Atomic Design in MetaBuilder
|
|
|
|
This guide shows you how to use the atomic design structure when building features.
|
|
|
|
## Basic Import Patterns
|
|
|
|
### Option 1: Import from atomic folders (recommended for new code)
|
|
|
|
```typescript
|
|
// Atoms - basic UI elements
|
|
import { Button, Input, Label, Badge, Avatar } from '@/components/atoms'
|
|
|
|
// Molecules - simple composites
|
|
import {
|
|
AppHeader,
|
|
AppFooter,
|
|
ProfileCard,
|
|
SecurityWarningDialog
|
|
} from '@/components/molecules'
|
|
|
|
// Organisms - complex features
|
|
import {
|
|
ComponentCatalog,
|
|
PropertyInspector,
|
|
SchemaEditor,
|
|
UserManagement
|
|
} from '@/components/organisms'
|
|
|
|
// Pages
|
|
import Level4 from '@/components/Level4'
|
|
```
|
|
|
|
### Option 2: Import directly (existing code continues to work)
|
|
|
|
```typescript
|
|
// Still works - no breaking changes
|
|
import { Button } from '@/components/ui/button'
|
|
import { AppHeader } from '@/components/shared/AppHeader'
|
|
import { ComponentCatalog } from '@/components/ComponentCatalog'
|
|
```
|
|
|
|
## Real-World Examples
|
|
|
|
### Example 1: Building a Settings Panel (Organism)
|
|
|
|
```typescript
|
|
// src/components/organisms/SettingsPanel.tsx
|
|
import { useState } from 'react'
|
|
import { Button, Input, Label, Switch } from '@/components/atoms'
|
|
import { AppHeader } from '@/components/molecules'
|
|
import { Card } from '@/components/ui/card'
|
|
import { toast } from 'sonner'
|
|
|
|
export function SettingsPanel() {
|
|
const [notifications, setNotifications] = useState(true)
|
|
const [email, setEmail] = useState('')
|
|
|
|
const handleSave = () => {
|
|
toast.success('Settings saved!')
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<AppHeader />
|
|
|
|
<Card className="p-6 space-y-4">
|
|
<h2 className="text-2xl font-bold">Settings</h2>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
value={email}
|
|
onChange={(e) => setEmail(e.target.value)}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between">
|
|
<Label htmlFor="notifications">Enable Notifications</Label>
|
|
<Switch
|
|
id="notifications"
|
|
checked={notifications}
|
|
onCheckedChange={setNotifications}
|
|
/>
|
|
</div>
|
|
|
|
<Button onClick={handleSave}>Save Settings</Button>
|
|
</Card>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Example 2: Creating a Status Badge (Molecule)
|
|
|
|
```typescript
|
|
// src/components/molecules/StatusBadge.tsx
|
|
import { Badge, Avatar } from '@/components/atoms'
|
|
|
|
interface StatusBadgeProps {
|
|
user: { name: string; avatar: string }
|
|
status: 'online' | 'offline' | 'away'
|
|
showAvatar?: boolean
|
|
}
|
|
|
|
export function StatusBadge({
|
|
user,
|
|
status,
|
|
showAvatar = true
|
|
}: StatusBadgeProps) {
|
|
const variants = {
|
|
online: 'default',
|
|
offline: 'secondary',
|
|
away: 'outline'
|
|
} as const
|
|
|
|
return (
|
|
<div className="flex items-center gap-2">
|
|
{showAvatar && <Avatar src={user.avatar} alt={user.name} />}
|
|
<Badge variant={variants[status]}>
|
|
{status}
|
|
</Badge>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// Usage in a page
|
|
import { StatusBadge } from '@/components/molecules'
|
|
|
|
function UserList() {
|
|
return (
|
|
<div>
|
|
{users.map(user => (
|
|
<StatusBadge
|
|
key={user.id}
|
|
user={user}
|
|
status={user.status}
|
|
/>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Example 3: Building a Complex Form (Organism)
|
|
|
|
```typescript
|
|
// src/components/organisms/UserProfileForm.tsx
|
|
import { useState } from 'react'
|
|
import { useKV } from '@github/spark/hooks'
|
|
import { Button, Input, Label, Avatar } from '@/components/atoms'
|
|
import { ProfileCard } from '@/components/molecules'
|
|
import { Card, CardHeader, CardContent, CardFooter } from '@/components/ui/card'
|
|
import { toast } from 'sonner'
|
|
|
|
interface UserProfileFormProps {
|
|
userId: string
|
|
onSave?: (data: UserData) => void
|
|
}
|
|
|
|
export function UserProfileForm({ userId, onSave }: UserProfileFormProps) {
|
|
const [users, setUsers] = useKV('users', [])
|
|
const user = users.find(u => u.id === userId)
|
|
|
|
const [formData, setFormData] = useState({
|
|
name: user?.name || '',
|
|
email: user?.email || '',
|
|
bio: user?.bio || ''
|
|
})
|
|
|
|
const handleSubmit = (e: React.FormEvent) => {
|
|
e.preventDefault()
|
|
|
|
setUsers(current =>
|
|
current.map(u => u.id === userId ? { ...u, ...formData } : u)
|
|
)
|
|
|
|
toast.success('Profile updated!')
|
|
onSave?.(formData)
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<CardHeader>
|
|
<h2 className="text-2xl font-semibold">Edit Profile</h2>
|
|
</CardHeader>
|
|
|
|
<CardContent>
|
|
<form onSubmit={handleSubmit} className="space-y-4">
|
|
<div className="flex justify-center mb-6">
|
|
<ProfileCard user={user} />
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="name">Name</Label>
|
|
<Input
|
|
id="name"
|
|
value={formData.name}
|
|
onChange={(e) => setFormData(prev => ({
|
|
...prev,
|
|
name: e.target.value
|
|
}))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="email">Email</Label>
|
|
<Input
|
|
id="email"
|
|
type="email"
|
|
value={formData.email}
|
|
onChange={(e) => setFormData(prev => ({
|
|
...prev,
|
|
email: e.target.value
|
|
}))}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="bio">Bio</Label>
|
|
<Input
|
|
id="bio"
|
|
value={formData.bio}
|
|
onChange={(e) => setFormData(prev => ({
|
|
...prev,
|
|
bio: e.target.value
|
|
}))}
|
|
/>
|
|
</div>
|
|
</form>
|
|
</CardContent>
|
|
|
|
<CardFooter className="flex gap-2">
|
|
<Button type="submit" onClick={handleSubmit}>
|
|
Save Changes
|
|
</Button>
|
|
<Button variant="outline" onClick={() => setFormData({
|
|
name: user?.name || '',
|
|
email: user?.email || '',
|
|
bio: user?.bio || ''
|
|
})}>
|
|
Reset
|
|
</Button>
|
|
</CardFooter>
|
|
</Card>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Example 4: Composing a Page from Components
|
|
|
|
```typescript
|
|
// src/components/Level2.tsx
|
|
import { useState } from 'react'
|
|
import { Button } from '@/components/atoms'
|
|
import { AppHeader, AppFooter, ProfileCard } from '@/components/molecules'
|
|
import {
|
|
UserProfileForm,
|
|
IRCWebchat,
|
|
CommentsList
|
|
} from '@/components/organisms'
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
|
|
|
interface Level2Props {
|
|
user: User
|
|
onLogout: () => void
|
|
onNavigate: (level: number) => void
|
|
}
|
|
|
|
export function Level2({ user, onLogout, onNavigate }: Level2Props) {
|
|
const [activeTab, setActiveTab] = useState('profile')
|
|
|
|
return (
|
|
<div className="min-h-screen flex flex-col">
|
|
<AppHeader />
|
|
|
|
<main className="flex-1 container mx-auto py-8">
|
|
<div className="mb-6">
|
|
<h1 className="text-3xl font-bold">User Dashboard</h1>
|
|
<p className="text-muted-foreground">Welcome back, {user.username}!</p>
|
|
</div>
|
|
|
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
|
<TabsList>
|
|
<TabsTrigger value="profile">Profile</TabsTrigger>
|
|
<TabsTrigger value="chat">Chat</TabsTrigger>
|
|
<TabsTrigger value="comments">Comments</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="profile">
|
|
<UserProfileForm userId={user.id} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="chat">
|
|
<IRCWebchat user={user} />
|
|
</TabsContent>
|
|
|
|
<TabsContent value="comments">
|
|
<CommentsList userId={user.id} />
|
|
</TabsContent>
|
|
</Tabs>
|
|
</main>
|
|
|
|
<AppFooter />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
export default Level2
|
|
```
|
|
|
|
## Decision Tree: Which Category?
|
|
|
|
### Is it a single UI element?
|
|
**→ Use an ATOM (from shadcn/ui)**
|
|
- Examples: Button, Input, Badge, Avatar
|
|
- Import from: `@/components/atoms` or `@/components/ui/*`
|
|
|
|
### Is it 2-5 atoms working together?
|
|
**→ Create a MOLECULE**
|
|
- Examples: FormField, SearchBar, CardHeader
|
|
- Put in: `src/components/molecules/`
|
|
- Export from: `@/components/molecules/index.ts`
|
|
|
|
### Is it a complex feature with business logic?
|
|
**→ Create an ORGANISM**
|
|
- Examples: UserManagement, SchemaEditor, Chat
|
|
- Put in: `src/components/organisms/`
|
|
- Export from: `@/components/organisms/index.ts`
|
|
|
|
### Is it a complete page/view?
|
|
**→ It's a PAGE**
|
|
- Examples: Level1, Level2, Level3
|
|
- Keep at: `src/components/`
|
|
|
|
## Best Practices
|
|
|
|
### 1. Keep Atoms Generic
|
|
```typescript
|
|
// ✅ Good - generic and reusable
|
|
export function StatusBadge({ status }: { status: string }) {
|
|
return <Badge>{status}</Badge>
|
|
}
|
|
|
|
// ❌ Bad - too specific
|
|
export function UserOnlineStatusBadgeForLevel2Dashboard({ status }: Props) {
|
|
return <Badge>{status}</Badge>
|
|
}
|
|
```
|
|
|
|
### 2. Molecules Should Be Self-Contained
|
|
```typescript
|
|
// ✅ Good - everything needed is inside
|
|
export function SearchBar({ onSearch }: { onSearch: (q: string) => void }) {
|
|
const [query, setQuery] = useState('')
|
|
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Input value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
<Button onClick={() => onSearch(query)}>Search</Button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ❌ Bad - state managed externally
|
|
export function SearchBar({ query, setQuery, onSearch }: Props) {
|
|
return (
|
|
<div className="flex gap-2">
|
|
<Input value={query} onChange={(e) => setQuery(e.target.value)} />
|
|
<Button onClick={onSearch}>Search</Button>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 3. Organisms Can Have Complex Logic
|
|
```typescript
|
|
// ✅ Good - organism handles complex state and business logic
|
|
export function UserManagement() {
|
|
const [users, setUsers] = useKV('users', [])
|
|
const [loading, setLoading] = useState(false)
|
|
const [filter, setFilter] = useState('')
|
|
|
|
const filteredUsers = users.filter(u =>
|
|
u.name.toLowerCase().includes(filter.toLowerCase())
|
|
)
|
|
|
|
const handleAddUser = async (userData: UserData) => {
|
|
setLoading(true)
|
|
// Complex user creation logic
|
|
await Database.addUser(userData)
|
|
setUsers(current => [...current, newUser])
|
|
setLoading(false)
|
|
}
|
|
|
|
return (
|
|
<Card>
|
|
<SearchBar onSearch={setFilter} />
|
|
<UserList users={filteredUsers} />
|
|
<UserForm onSubmit={handleAddUser} loading={loading} />
|
|
</Card>
|
|
)
|
|
}
|
|
```
|
|
|
|
### 4. Use Composition
|
|
```typescript
|
|
// ✅ Good - compose from smaller parts
|
|
function SettingsPage() {
|
|
return (
|
|
<div>
|
|
<AppHeader />
|
|
<ThemeEditor />
|
|
<SMTPConfigEditor />
|
|
<AppFooter />
|
|
</div>
|
|
)
|
|
}
|
|
|
|
// ❌ Bad - monolithic component
|
|
function SettingsPage() {
|
|
// 500 lines of code doing everything
|
|
}
|
|
```
|
|
|
|
## Testing by Atomic Level
|
|
|
|
### Testing Atoms (from shadcn)
|
|
```typescript
|
|
import { render, screen } from '@testing-library/react'
|
|
import { Button } from '@/components/atoms'
|
|
|
|
test('Button renders text', () => {
|
|
render(<Button>Click me</Button>)
|
|
expect(screen.getByText('Click me')).toBeInTheDocument()
|
|
})
|
|
```
|
|
|
|
### Testing Molecules
|
|
```typescript
|
|
import { render, screen } from '@testing-library/react'
|
|
import { ProfileCard } from '@/components/molecules'
|
|
|
|
test('ProfileCard displays user information', () => {
|
|
const user = { name: 'John Doe', email: 'john@example.com' }
|
|
render(<ProfileCard user={user} />)
|
|
|
|
expect(screen.getByText('John Doe')).toBeInTheDocument()
|
|
expect(screen.getByText('john@example.com')).toBeInTheDocument()
|
|
})
|
|
```
|
|
|
|
### Testing Organisms
|
|
```typescript
|
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
import userEvent from '@testing-library/user-event'
|
|
import { UserManagement } from '@/components/organisms'
|
|
|
|
test('UserManagement allows creating users', async () => {
|
|
render(<UserManagement />)
|
|
|
|
await userEvent.click(screen.getByRole('button', { name: /add user/i }))
|
|
await userEvent.type(screen.getByLabelText(/name/i), 'Jane Doe')
|
|
await userEvent.click(screen.getByRole('button', { name: /save/i }))
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByText('Jane Doe')).toBeInTheDocument()
|
|
})
|
|
})
|
|
```
|
|
|
|
## Common Patterns
|
|
|
|
### Pattern 1: Form Field (Molecule)
|
|
```typescript
|
|
import { Input, Label } from '@/components/atoms'
|
|
|
|
export function FormField({
|
|
label,
|
|
id,
|
|
error,
|
|
...props
|
|
}: FormFieldProps) {
|
|
return (
|
|
<div className="space-y-2">
|
|
<Label htmlFor={id}>{label}</Label>
|
|
<Input id={id} {...props} />
|
|
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
### Pattern 2: Data Table (Organism)
|
|
```typescript
|
|
import { Button, Input } from '@/components/atoms'
|
|
import { Table } from '@/components/ui/table'
|
|
import { useState } from 'react'
|
|
|
|
export function UserTable({ data }: { data: User[] }) {
|
|
const [filter, setFilter] = useState('')
|
|
const [sortBy, setSortBy] = useState('name')
|
|
|
|
const filteredData = data
|
|
.filter(row => row.name.includes(filter))
|
|
.sort((a, b) => a[sortBy].localeCompare(b[sortBy]))
|
|
|
|
return (
|
|
<div className="space-y-4">
|
|
<Input
|
|
placeholder="Filter..."
|
|
value={filter}
|
|
onChange={(e) => setFilter(e.target.value)}
|
|
/>
|
|
<Table>
|
|
{/* Table implementation */}
|
|
</Table>
|
|
</div>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Questions?
|
|
|
|
- **Where do I put a component?** Use the decision tree above
|
|
- **Can atoms import molecules?** No, never
|
|
- **Can organisms import other organisms?** Yes, that's fine
|
|
- **Do I have to move existing components?** No, exports make it virtual
|
|
- **Should I refactor everything now?** No, use atomic design for new code
|
|
|
|
## Summary
|
|
|
|
- **Atoms**: Basic UI elements (shadcn/ui)
|
|
- **Molecules**: 2-5 atoms working together
|
|
- **Organisms**: Complex features with logic
|
|
- **Pages**: Complete views
|
|
|
|
Import from `@/components/atoms`, `@/components/molecules`, or `@/components/organisms` for cleaner code!
|