273 lines
8.0 KiB
Markdown

# App Build Agent Prompt
Build ALL React MCP Apps for the {{NAME}} MCP server at {{DIR}}.
## Foundation + Tools Already Exist
DO NOT modify any existing files. Only ADD app files under `src/ui/react-app/`.
## Apps to Build
{{APP_LIST}}
## Quality Requirements — 2026 Standards
### Each App Directory Structure
```
src/ui/react-app/{app-name}/
├── App.tsx # Main component
├── index.html # Entry point
├── main.tsx # React mount with ErrorBoundary
├── styles.css # Dark theme styles
└── vite.config.ts # Build config
```
### main.tsx — With Error Boundary + Suspense
```tsx
import React, { Suspense } from 'react';
import ReactDOM from 'react-dom/client';
import './styles.css';
// Lazy load the main app component
const App = React.lazy(() => import('./App'));
// Error Boundary
class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error?: Error }
> {
constructor(props: { children: React.ReactNode }) {
super(props);
this.state = { hasError: false };
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error };
}
render() {
if (this.state.hasError) {
return (
<div style={{ padding: 40, color: '#f87171', background: '#0f172a', minHeight: '100vh' }}>
<h2>Something went wrong</h2>
<pre style={{ color: '#94a3b8' }}>{this.state.error?.message}</pre>
<button onClick={() => this.setState({ hasError: false })}
style={{ marginTop: 16, padding: '8px 16px', background: '#3b82f6', color: '#fff', border: 'none', borderRadius: 6, cursor: 'pointer' }}>
Retry
</button>
</div>
);
}
return this.props.children;
}
}
// Loading skeleton
function LoadingSkeleton() {
return (
<div style={{ padding: 40, background: '#0f172a', minHeight: '100vh' }}>
<div className="skeleton" style={{ height: 32, width: '40%', marginBottom: 24 }} />
<div className="skeleton" style={{ height: 200, width: '100%', marginBottom: 16 }} />
<div className="skeleton" style={{ height: 200, width: '100%' }} />
</div>
);
}
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<ErrorBoundary>
<Suspense fallback={<LoadingSkeleton />}>
<App />
</Suspense>
</ErrorBoundary>
</React.StrictMode>
);
```
### App.tsx — Modern React Patterns
```tsx
import React, { useState, useMemo, useCallback, useTransition } from 'react';
// Debounce hook for search
function useDebounce<T>(value: T, delay: number): T {
const [debounced, setDebounced] = React.useState(value);
React.useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(t);
}, [value, delay]);
return debounced;
}
// Toast notification system
function useToast() {
const [toasts, setToasts] = useState<Array<{ id: number; message: string; type: 'success' | 'error' }>>([]);
const show = useCallback((message: string, type: 'success' | 'error' = 'success') => {
const id = Date.now();
setToasts(prev => [...prev, { id, message, type }]);
setTimeout(() => setToasts(prev => prev.filter(t => t.id !== id)), 3000);
}, []);
return { toasts, show };
}
export default function App() {
const [search, setSearch] = useState('');
const [isPending, startTransition] = useTransition();
const debouncedSearch = useDebounce(search, 300);
const { toasts, show: showToast } = useToast();
// Mock data — replace with MCP tool calls
const [data] = useState(MOCK_DATA);
// Client-side filtering with useMemo
const filtered = useMemo(() =>
data.filter(item =>
item.name.toLowerCase().includes(debouncedSearch.toLowerCase())
), [data, debouncedSearch]);
return (
<div className="app">
{/* Header with search */}
<header className="header">
<h1>{{App Title}}</h1>
<div className="search-wrapper">
<input
type="text"
placeholder="Search..."
value={search}
onChange={(e) => startTransition(() => setSearch(e.target.value))}
className="search-input"
/>
{isPending && <span className="spinner" />}
</div>
</header>
{/* Stats cards */}
<div className="stats-grid">
{/* Stat cards with numbers */}
</div>
{/* Data grid or dashboard content */}
<div className="content">
{filtered.length === 0 ? (
<div className="empty-state">
<p>No results found</p>
<span className="empty-hint">Try adjusting your search</span>
</div>
) : (
<div className="data-grid">
{filtered.map(item => (
<div key={item.id} className="card">
{/* Card content */}
</div>
))}
</div>
)}
</div>
{/* Toast notifications */}
<div className="toast-container">
{toasts.map(t => (
<div key={t.id} className={`toast toast-${t.type}`}>{t.message}</div>
))}
</div>
</div>
);
}
```
### styles.css — Dark Theme with CSS Variables + Skeleton Animation
```css
:root {
--bg-primary: #0f172a;
--bg-secondary: #1e293b;
--bg-tertiary: #334155;
--text-primary: #f1f5f9;
--text-secondary: #94a3b8;
--text-muted: #64748b;
--accent: #3b82f6;
--accent-hover: #2563eb;
--success: #22c55e;
--warning: #f59e0b;
--error: #ef4444;
--border: #334155;
--radius: 8px;
--shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.3);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
/* Skeleton loading animation */
.skeleton {
background: linear-gradient(90deg, var(--bg-secondary) 25%, var(--bg-tertiary) 50%, var(--bg-secondary) 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
border-radius: var(--radius);
}
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
/* Search input */
.search-input {
background: var(--bg-secondary);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 10px 16px;
border-radius: var(--radius);
width: 280px;
transition: border-color 0.2s;
}
.search-input:focus { outline: none; border-color: var(--accent); }
.search-input::placeholder { color: var(--text-muted); }
/* Stats grid */
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
/* Cards */
.card {
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
transition: transform 0.15s, box-shadow 0.15s;
}
.card:hover { transform: translateY(-2px); box-shadow: var(--shadow); }
/* Empty state */
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-secondary); }
.empty-hint { font-size: 14px; color: var(--text-muted); }
/* Toast notifications */
.toast-container { position: fixed; bottom: 20px; right: 20px; display: flex; flex-direction: column; gap: 8px; z-index: 1000; }
.toast {
padding: 12px 20px;
border-radius: var(--radius);
color: white;
font-weight: 500;
animation: slideIn 0.3s ease;
box-shadow: var(--shadow);
}
.toast-success { background: var(--success); }
.toast-error { background: var(--error); }
@keyframes slideIn { from { transform: translateX(100%); opacity: 0; } to { transform: translateX(0); opacity: 1; } }
/* Responsive */
@media (max-width: 768px) {
.stats-grid { grid-template-columns: repeat(2, 1fr); }
.search-input { width: 100%; }
}
@media (max-width: 480px) {
.stats-grid { grid-template-columns: 1fr; }
}
```
## Rules
- Each app MUST have: ErrorBoundary, Suspense, loading skeleton, empty state, toast system
- Debounced search on all grids/lists
- CSS custom properties (not hardcoded colors)
- Mobile responsive
- DO NOT modify existing files
- Commit when done: `git add src/ui/ && git commit -m "{{name}}: Add {{count}} React MCP apps"`