273 lines
8.0 KiB
Markdown
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"`
|