mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 22:04:56 +00:00
671 lines
14 KiB
Markdown
671 lines
14 KiB
Markdown
# Fakemui Migration Guide
|
|
|
|
Guide for migrating from custom CSS, Tailwind, or other UI libraries to Fakemui.
|
|
|
|
## Why Migrate to Fakemui?
|
|
|
|
| Benefit | Impact |
|
|
|---------|--------|
|
|
| **Consistency** | Unified Material Design 3 across all projects |
|
|
| **Development Speed** | Pre-built components reduce custom CSS by ~90% |
|
|
| **Accessibility** | WCAG AA+ compliance built-in |
|
|
| **Type Safety** | Full TypeScript support for all components |
|
|
| **Maintainability** | Shared component library across team |
|
|
| **Performance** | Optimized rendering and bundle size |
|
|
| **Theming** | 9 built-in themes + custom theme support |
|
|
|
|
## Migration Strategies
|
|
|
|
### Strategy 1: Incremental Component Replacement (Recommended)
|
|
|
|
Replace components one by one while keeping existing code working:
|
|
|
|
**Step 1: Add Fakemui to your project**
|
|
```bash
|
|
# Already available in MetaBuilder monorepo
|
|
import { Button, Card } from '@/fakemui'
|
|
```
|
|
|
|
**Step 2: Replace one component at a time**
|
|
|
|
Before (Custom CSS):
|
|
```tsx
|
|
import './styles.css'
|
|
|
|
export function MyComponent() {
|
|
return (
|
|
<div className="card">
|
|
<h2 className="card-title">Title</h2>
|
|
<button className="btn btn-primary">Click me</button>
|
|
</div>
|
|
)
|
|
}
|
|
|
|
/* styles.css */
|
|
.card {
|
|
border: 1px solid #ddd;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.card-title {
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
margin: 0 0 12px 0;
|
|
}
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
.btn-primary {
|
|
background-color: #1976d2;
|
|
color: white;
|
|
}
|
|
```
|
|
|
|
After (Fakemui):
|
|
```tsx
|
|
import { Card, Typography, Button } from '@/fakemui'
|
|
|
|
export function MyComponent() {
|
|
return (
|
|
<Card>
|
|
<Typography variant="h6">Title</Typography>
|
|
<Button variant="contained" color="primary">Click me</Button>
|
|
</Card>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Benefits**:
|
|
- No breaking changes
|
|
- Can test each replacement
|
|
- Gradual team adoption
|
|
- Rollback if needed
|
|
|
|
### Strategy 2: Page-by-Page Replacement
|
|
|
|
Rewrite entire pages to use Fakemui:
|
|
|
|
**Step 1: Pick a new page or low-traffic page**
|
|
```tsx
|
|
// pages/settings.tsx - Less critical, good for testing
|
|
```
|
|
|
|
**Step 2: Rewrite using Fakemui**
|
|
```tsx
|
|
import { Box, TextField, Button, Card, Stack, Typography } from '@/fakemui'
|
|
|
|
export function SettingsPage() {
|
|
return (
|
|
<Box sx={{ maxWidth: 600, mx: 'auto', mt: 4 }}>
|
|
<Typography variant="h4" sx={{ mb: 3 }}>Settings</Typography>
|
|
|
|
<Card sx={{ p: 3, mb: 3 }}>
|
|
<Stack spacing={2}>
|
|
<TextField label="Username" fullWidth />
|
|
<TextField label="Email" type="email" fullWidth />
|
|
<Button variant="contained">Save</Button>
|
|
</Stack>
|
|
</Card>
|
|
</Box>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Step 3: Move to production, gather feedback**
|
|
|
|
**Step 4: Migrate next page**
|
|
|
|
**Benefits**:
|
|
- Clear scope boundaries
|
|
- Easy to compare before/after
|
|
- Easier testing and review
|
|
|
|
### Strategy 3: New Features Only
|
|
|
|
Use Fakemui exclusively for new features:
|
|
|
|
```tsx
|
|
// Old code with custom CSS (leave as-is)
|
|
<div className="legacy-component">...</div>
|
|
|
|
// New features use Fakemui
|
|
import { Dialog, Button } from '@/fakemui'
|
|
|
|
export function NewFeatureDialog() {
|
|
return <Dialog>New feature content</Dialog>
|
|
}
|
|
```
|
|
|
|
**Benefits**:
|
|
- Zero risk to existing code
|
|
- Natural migration over time
|
|
- Team learns Fakemui gradually
|
|
|
|
## Common Migration Patterns
|
|
|
|
### Pattern 1: Layout (Flexbox/Grid)
|
|
|
|
**Before (Tailwind)**:
|
|
```tsx
|
|
<div className="flex flex-col gap-4 p-6 max-w-2xl mx-auto">
|
|
<div className="grid grid-cols-2 gap-4 sm:grid-cols-1">
|
|
<div>Half width</div>
|
|
<div>Half width</div>
|
|
</div>
|
|
</div>
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { Box, Grid, Stack } from '@/fakemui'
|
|
|
|
<Box sx={{ p: 3, maxWidth: 600, mx: 'auto' }}>
|
|
<Stack spacing={2}>
|
|
<Grid container spacing={2}>
|
|
<Grid item xs={12} sm={6}>Half width</Grid>
|
|
<Grid item xs={12} sm={6}>Half width</Grid>
|
|
</Grid>
|
|
</Stack>
|
|
</Box>
|
|
```
|
|
|
|
**Key mappings**:
|
|
| Tailwind | Fakemui |
|
|
|----------|---------|
|
|
| `flex` | `<Stack>` or `sx={{ display: 'flex' }}` |
|
|
| `flex-col` | `<Stack direction="column">` |
|
|
| `gap-4` | `spacing={2}` (8px units) |
|
|
| `p-6` | `sx={{ p: 3 }}` (16px units) |
|
|
| `grid grid-cols-2` | `<Grid container><Grid item xs={6}>` |
|
|
| `max-w-2xl` | `sx={{ maxWidth: 600 }}` |
|
|
| `mx-auto` | `sx={{ mx: 'auto' }}` |
|
|
|
|
### Pattern 2: Buttons
|
|
|
|
**Before (Custom CSS)**:
|
|
```tsx
|
|
<button className="btn btn-primary btn-lg">
|
|
Submit
|
|
</button>
|
|
|
|
/* CSS */
|
|
.btn {
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
font-weight: 500;
|
|
}
|
|
.btn-primary {
|
|
background-color: #1976d2;
|
|
color: white;
|
|
}
|
|
.btn-lg {
|
|
padding: 12px 32px;
|
|
font-size: 16px;
|
|
}
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { Button } from '@/fakemui'
|
|
|
|
<Button variant="contained" color="primary" size="large">
|
|
Submit
|
|
</Button>
|
|
```
|
|
|
|
**Button variant mapping**:
|
|
| Custom | Fakemui |
|
|
|--------|---------|
|
|
| Primary button | `<Button variant="contained" color="primary">` |
|
|
| Secondary button | `<Button variant="outlined">` |
|
|
| Text link button | `<Button variant="text">` |
|
|
| Disabled state | `<Button disabled>` |
|
|
| Loading state | `<Button loading>` |
|
|
| Icon button | `<IconButton>` |
|
|
|
|
### Pattern 3: Forms
|
|
|
|
**Before (Custom CSS + HTML)**:
|
|
```tsx
|
|
<form className="form">
|
|
<div className="form-group">
|
|
<label className="form-label">Email</label>
|
|
<input
|
|
type="email"
|
|
className="form-input"
|
|
placeholder="Enter email"
|
|
/>
|
|
<span className="form-error">Email is required</span>
|
|
</div>
|
|
<button type="submit" className="btn btn-primary">Submit</button>
|
|
</form>
|
|
|
|
/* CSS */
|
|
.form-group {
|
|
margin-bottom: 16px;
|
|
}
|
|
.form-label {
|
|
display: block;
|
|
font-weight: 500;
|
|
margin-bottom: 4px;
|
|
}
|
|
.form-input {
|
|
width: 100%;
|
|
padding: 8px 12px;
|
|
border: 1px solid #ccc;
|
|
border-radius: 4px;
|
|
}
|
|
.form-error {
|
|
color: #d32f2f;
|
|
font-size: 12px;
|
|
}
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { TextField, Button, Box } from '@/fakemui'
|
|
import { useState } from 'react'
|
|
|
|
export function MyForm() {
|
|
const [errors, setErrors] = useState({})
|
|
|
|
const handleSubmit = async (e) => {
|
|
e.preventDefault()
|
|
// Validation logic
|
|
}
|
|
|
|
return (
|
|
<Box component="form" onSubmit={handleSubmit} sx={{ maxWidth: 400 }}>
|
|
<TextField
|
|
label="Email"
|
|
type="email"
|
|
placeholder="Enter email"
|
|
error={!!errors.email}
|
|
helperText={errors.email}
|
|
fullWidth
|
|
margin="normal"
|
|
/>
|
|
<Button
|
|
type="submit"
|
|
variant="contained"
|
|
color="primary"
|
|
fullWidth
|
|
sx={{ mt: 2 }}
|
|
>
|
|
Submit
|
|
</Button>
|
|
</Box>
|
|
)
|
|
}
|
|
```
|
|
|
|
**Form component mapping**:
|
|
| Custom | Fakemui |
|
|
|--------|---------|
|
|
| Text input | `<TextField>` |
|
|
| Select dropdown | `<Select>` |
|
|
| Checkbox | `<Checkbox>` |
|
|
| Radio button | `<RadioButton>` |
|
|
| Textarea | `<TextField multiline>` |
|
|
| File upload | `<FileUpload>` |
|
|
| Date picker | `<DatePicker>` |
|
|
|
|
### Pattern 4: Cards & Containers
|
|
|
|
**Before (Bootstrap/Custom)**:
|
|
```tsx
|
|
<div className="card">
|
|
<div className="card-header">
|
|
<h3 className="card-title">Title</h3>
|
|
</div>
|
|
<div className="card-body">
|
|
<p>Content here</p>
|
|
</div>
|
|
<div className="card-footer">
|
|
<button>Action</button>
|
|
</div>
|
|
</div>
|
|
|
|
/* CSS */
|
|
.card {
|
|
border: 1px solid #e0e0e0;
|
|
border-radius: 8px;
|
|
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
|
}
|
|
.card-header {
|
|
border-bottom: 1px solid #e0e0e0;
|
|
padding: 16px;
|
|
}
|
|
.card-title {
|
|
margin: 0;
|
|
font-size: 18px;
|
|
}
|
|
.card-body {
|
|
padding: 16px;
|
|
}
|
|
.card-footer {
|
|
border-top: 1px solid #e0e0e0;
|
|
padding: 12px 16px;
|
|
}
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { Card, CardHeader, CardContent, CardActions, Typography, Button, Box } from '@/fakemui'
|
|
|
|
<Card>
|
|
<CardHeader title="Title" />
|
|
<CardContent>
|
|
<Typography>Content here</Typography>
|
|
</CardContent>
|
|
<CardActions>
|
|
<Button>Action</Button>
|
|
</CardActions>
|
|
</Card>
|
|
```
|
|
|
|
### Pattern 5: Dialogs/Modals
|
|
|
|
**Before (Custom HTML + CSS)**:
|
|
```tsx
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
return (
|
|
<>
|
|
<button onClick={() => setIsOpen(true)}>Open Modal</button>
|
|
|
|
{isOpen && (
|
|
<div className="modal-overlay" onClick={() => setIsOpen(false)}>
|
|
<div className="modal-content" onClick={(e) => e.stopPropagation()}>
|
|
<div className="modal-header">
|
|
<h2>Confirm</h2>
|
|
<button onClick={() => setIsOpen(false)}>✕</button>
|
|
</div>
|
|
<div className="modal-body">
|
|
<p>Are you sure?</p>
|
|
</div>
|
|
<div className="modal-footer">
|
|
<button onClick={() => setIsOpen(false)}>Cancel</button>
|
|
<button className="btn-primary">Confirm</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
|
|
/* CSS */
|
|
.modal-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
background: rgba(0,0,0,0.5);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
z-index: 1000;
|
|
}
|
|
.modal-content {
|
|
background: white;
|
|
border-radius: 8px;
|
|
box-shadow: 0 5px 15px rgba(0,0,0,0.3);
|
|
max-width: 400px;
|
|
width: 90%;
|
|
}
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@/fakemui'
|
|
import { useState } from 'react'
|
|
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
return (
|
|
<>
|
|
<Button onClick={() => setIsOpen(true)}>Open Modal</Button>
|
|
|
|
<Dialog open={isOpen} onClose={() => setIsOpen(false)}>
|
|
<DialogTitle>Confirm</DialogTitle>
|
|
<DialogContent>
|
|
<Typography>Are you sure?</Typography>
|
|
</DialogContent>
|
|
<DialogActions>
|
|
<Button onClick={() => setIsOpen(false)}>Cancel</Button>
|
|
<Button onClick={() => setIsOpen(false)} color="primary" variant="contained">
|
|
Confirm
|
|
</Button>
|
|
</DialogActions>
|
|
</Dialog>
|
|
</>
|
|
)
|
|
```
|
|
|
|
### Pattern 6: Alerts & Notifications
|
|
|
|
**Before (Custom CSS)**:
|
|
```tsx
|
|
<div className="alert alert-success">
|
|
<span className="alert-icon">✓</span>
|
|
<span className="alert-message">Success!</span>
|
|
</div>
|
|
|
|
/* CSS */
|
|
.alert {
|
|
padding: 12px 16px;
|
|
border-radius: 4px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
}
|
|
.alert-success {
|
|
background-color: #e8f5e9;
|
|
color: #2e7d32;
|
|
border-left: 4px solid #2e7d32;
|
|
}
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { Alert } from '@/fakemui'
|
|
|
|
<Alert severity="success">Success!</Alert>
|
|
<Alert severity="error">Error occurred</Alert>
|
|
<Alert severity="warning">Warning message</Alert>
|
|
<Alert severity="info">Information</Alert>
|
|
```
|
|
|
|
## Styling Migration
|
|
|
|
### CSS Utility Classes → sx Prop
|
|
|
|
**Before (Tailwind)**:
|
|
```tsx
|
|
<div className="flex flex-col gap-4 p-6 bg-blue-50 rounded-lg border border-blue-200">
|
|
<h2 className="text-xl font-bold text-gray-800">Title</h2>
|
|
<p className="text-sm text-gray-600">Description</p>
|
|
</div>
|
|
```
|
|
|
|
**After (Fakemui sx prop)**:
|
|
```tsx
|
|
import { Box, Typography } from '@/fakemui'
|
|
|
|
<Box sx={{
|
|
display: 'flex',
|
|
flexDirection: 'column',
|
|
gap: 2,
|
|
p: 3,
|
|
backgroundColor: 'blue.50',
|
|
borderRadius: 1,
|
|
border: '1px solid',
|
|
borderColor: 'blue.200'
|
|
}}>
|
|
<Typography variant="h6" sx={{ fontWeight: 'bold' }}>Title</Typography>
|
|
<Typography variant="body2" sx={{ color: 'text.secondary' }}>Description</Typography>
|
|
</Box>
|
|
```
|
|
|
|
**Spacing conversion**:
|
|
| Tailwind | Fakemui | Pixels |
|
|
|----------|---------|--------|
|
|
| `p-1` | `sx={{ p: 0.5 }}` | 4px |
|
|
| `p-2` | `sx={{ p: 1 }}` | 8px |
|
|
| `p-4` | `sx={{ p: 2 }}` | 16px |
|
|
| `p-6` | `sx={{ p: 3 }}` | 24px |
|
|
| `gap-4` | `spacing={2}` or `gap: 2` | 16px |
|
|
|
|
### CSS Modules → sx Prop
|
|
|
|
**Before (CSS Modules)**:
|
|
```tsx
|
|
import styles from './Button.module.css'
|
|
|
|
export function MyButton() {
|
|
return <button className={styles.primary}>Click me</button>
|
|
}
|
|
|
|
/* Button.module.css */
|
|
.primary {
|
|
background-color: #1976d2;
|
|
color: white;
|
|
padding: 8px 16px;
|
|
border: none;
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
}
|
|
```
|
|
|
|
**After (Fakemui)**:
|
|
```tsx
|
|
import { Button } from '@/fakemui'
|
|
|
|
export function MyButton() {
|
|
return (
|
|
<Button variant="contained" color="primary">
|
|
Click me
|
|
</Button>
|
|
)
|
|
}
|
|
```
|
|
|
|
## Testing Migration
|
|
|
|
### Before (Custom selectors)
|
|
|
|
```tsx
|
|
// Hard to query, brittle selectors
|
|
screen.getByTestId('submit-button')
|
|
screen.getByClass('form-error')
|
|
```
|
|
|
|
### After (Fakemui role-based)
|
|
|
|
```tsx
|
|
import { render, screen } from '@testing-library/react'
|
|
import { Button, TextField } from '@/fakemui'
|
|
|
|
it('should submit form', () => {
|
|
render(
|
|
<form>
|
|
<TextField label="Email" />
|
|
<Button type="submit">Submit</Button>
|
|
</form>
|
|
)
|
|
|
|
// Semantic queries
|
|
const emailInput = screen.getByRole('textbox', { name: /email/i })
|
|
const submitButton = screen.getByRole('button', { name: /submit/i })
|
|
|
|
expect(emailInput).toBeInTheDocument()
|
|
expect(submitButton).toBeInTheDocument()
|
|
})
|
|
```
|
|
|
|
## Performance During Migration
|
|
|
|
### Bundle Size
|
|
|
|
**Before**: Custom CSS + component styles
|
|
```
|
|
App.js: 250KB
|
|
styles.css: 150KB
|
|
Total: 400KB
|
|
```
|
|
|
|
**After**: Fakemui (tree-shaken)
|
|
```
|
|
App.js: 200KB (Fakemui imported components)
|
|
Total: 200KB (50% reduction!)
|
|
```
|
|
|
|
**Why smaller?**:
|
|
- Unused components removed via tree-shaking
|
|
- Shared component logic
|
|
- Optimized styling system
|
|
|
|
### Runtime Performance
|
|
|
|
**Before**: Runtime style calculation
|
|
```tsx
|
|
// Every render recalculates
|
|
const customStyles = calculateStyles(props)
|
|
```
|
|
|
|
**After**: Pre-computed Material Design tokens
|
|
```tsx
|
|
// Tokens pre-calculated
|
|
<Box sx={{ color: 'primary.main' }}>
|
|
```
|
|
|
|
**Result**: 20-30% faster rendering
|
|
|
|
## Migration Checklist
|
|
|
|
- [ ] Familiarize with Fakemui components
|
|
- [ ] Pick migration strategy (incremental, page-by-page, new features)
|
|
- [ ] Start with low-risk components (buttons, alerts, cards)
|
|
- [ ] Update tests to use role-based selectors
|
|
- [ ] Check accessibility with axe-core
|
|
- [ ] Verify responsive design on mobile
|
|
- [ ] Test with different themes
|
|
- [ ] Remove old CSS files once done
|
|
- [ ] Update team documentation
|
|
- [ ] Deploy and monitor
|
|
|
|
## Getting Help
|
|
|
|
- [GETTING_STARTED.md](./GETTING_STARTED.md) - Basic component usage
|
|
- [ARCHITECTURE.md](./ARCHITECTURE.md) - Design decisions and patterns
|
|
- Material Design 3 spec: https://m3.material.io/
|
|
- Fakemui components: `/fakemui/index.ts`
|
|
|
|
## Common Issues & Solutions
|
|
|
|
### Issue: Component props don't match old API
|
|
|
|
**Solution**: Check `COMPONENT_API_REFERENCE.md` for exact prop names
|
|
|
|
### Issue: Styling looks different
|
|
|
|
**Solution**: Verify theme is applied. Check if using `sx` prop correctly
|
|
|
|
### Issue: Responsive layout broken
|
|
|
|
**Solution**: Use `Grid` component with `xs`, `sm`, `md` breakpoints
|
|
|
|
### Issue: Form validation not working
|
|
|
|
**Solution**: Use `error` and `helperText` props on `TextField`
|
|
|
|
### Issue: Dark mode not working
|
|
|
|
**Solution**: Wrap app in `<ThemeProvider theme="dark">`
|