oh-my-ag 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/.agent/skills/_shared/api-contracts/README.md +56 -0
  2. package/.agent/skills/_shared/api-contracts/template.md +88 -0
  3. package/.agent/skills/_shared/clarification-protocol.md +217 -0
  4. package/.agent/skills/_shared/common-checklist.md +31 -0
  5. package/.agent/skills/_shared/context-budget.md +118 -0
  6. package/.agent/skills/_shared/context-loading.md +105 -0
  7. package/.agent/skills/_shared/difficulty-guide.md +55 -0
  8. package/.agent/skills/_shared/lessons-learned.md +113 -0
  9. package/.agent/skills/_shared/memory-protocol.md +79 -0
  10. package/.agent/skills/_shared/reasoning-templates.md +161 -0
  11. package/.agent/skills/_shared/skill-routing.md +80 -0
  12. package/.agent/skills/_shared/verify.sh +252 -0
  13. package/.agent/skills/backend-agent/SKILL.md +47 -0
  14. package/.agent/skills/backend-agent/resources/api-template.py +326 -0
  15. package/.agent/skills/backend-agent/resources/checklist.md +36 -0
  16. package/.agent/skills/backend-agent/resources/error-playbook.md +98 -0
  17. package/.agent/skills/backend-agent/resources/examples.md +85 -0
  18. package/.agent/skills/backend-agent/resources/execution-protocol.md +45 -0
  19. package/.agent/skills/backend-agent/resources/snippets.md +197 -0
  20. package/.agent/skills/backend-agent/resources/tech-stack.md +39 -0
  21. package/.agent/skills/commit/SKILL.md +121 -0
  22. package/.agent/skills/commit/config/commit-config.yaml +55 -0
  23. package/.agent/skills/commit/resources/conventional-commits.md +166 -0
  24. package/.agent/skills/debug-agent/SKILL.md +51 -0
  25. package/.agent/skills/debug-agent/resources/bug-report-template.md +332 -0
  26. package/.agent/skills/debug-agent/resources/checklist.md +30 -0
  27. package/.agent/skills/debug-agent/resources/common-patterns.md +734 -0
  28. package/.agent/skills/debug-agent/resources/debugging-checklist.md +362 -0
  29. package/.agent/skills/debug-agent/resources/error-playbook.md +94 -0
  30. package/.agent/skills/debug-agent/resources/examples.md +87 -0
  31. package/.agent/skills/debug-agent/resources/execution-protocol.md +51 -0
  32. package/.agent/skills/frontend-agent/SKILL.md +48 -0
  33. package/.agent/skills/frontend-agent/resources/checklist.md +38 -0
  34. package/.agent/skills/frontend-agent/resources/component-template.tsx +92 -0
  35. package/.agent/skills/frontend-agent/resources/error-playbook.md +108 -0
  36. package/.agent/skills/frontend-agent/resources/examples.md +77 -0
  37. package/.agent/skills/frontend-agent/resources/execution-protocol.md +49 -0
  38. package/.agent/skills/frontend-agent/resources/snippets.md +205 -0
  39. package/.agent/skills/frontend-agent/resources/tailwind-rules.md +343 -0
  40. package/.agent/skills/frontend-agent/resources/tech-stack.md +36 -0
  41. package/.agent/skills/mobile-agent/SKILL.md +46 -0
  42. package/.agent/skills/mobile-agent/resources/checklist.md +35 -0
  43. package/.agent/skills/mobile-agent/resources/error-playbook.md +106 -0
  44. package/.agent/skills/mobile-agent/resources/examples.md +79 -0
  45. package/.agent/skills/mobile-agent/resources/execution-protocol.md +49 -0
  46. package/.agent/skills/mobile-agent/resources/screen-template.dart +298 -0
  47. package/.agent/skills/mobile-agent/resources/snippets.md +235 -0
  48. package/.agent/skills/mobile-agent/resources/tech-stack.md +45 -0
  49. package/.agent/skills/orchestrator/SKILL.md +99 -0
  50. package/.agent/skills/orchestrator/config/cli-config.yaml +78 -0
  51. package/.agent/skills/orchestrator/resources/memory-schema.md +212 -0
  52. package/.agent/skills/orchestrator/resources/subagent-prompt-template.md +153 -0
  53. package/.agent/skills/orchestrator/scripts/parallel-run.sh +330 -0
  54. package/.agent/skills/orchestrator/scripts/spawn-agent.sh +263 -0
  55. package/.agent/skills/orchestrator/templates/backend-task.md +18 -0
  56. package/.agent/skills/orchestrator/templates/debug-task.md +16 -0
  57. package/.agent/skills/orchestrator/templates/frontend-task.md +17 -0
  58. package/.agent/skills/orchestrator/templates/mobile-task.md +17 -0
  59. package/.agent/skills/orchestrator/templates/qa-task.md +16 -0
  60. package/.agent/skills/orchestrator/templates/tasks-example.yaml +15 -0
  61. package/.agent/skills/pm-agent/SKILL.md +47 -0
  62. package/.agent/skills/pm-agent/resources/error-playbook.md +75 -0
  63. package/.agent/skills/pm-agent/resources/examples.md +121 -0
  64. package/.agent/skills/pm-agent/resources/execution-protocol.md +46 -0
  65. package/.agent/skills/pm-agent/resources/task-template.json +57 -0
  66. package/.agent/skills/qa-agent/SKILL.md +43 -0
  67. package/.agent/skills/qa-agent/resources/checklist.md +294 -0
  68. package/.agent/skills/qa-agent/resources/error-playbook.md +95 -0
  69. package/.agent/skills/qa-agent/resources/examples.md +100 -0
  70. package/.agent/skills/qa-agent/resources/execution-protocol.md +50 -0
  71. package/.agent/skills/qa-agent/resources/self-check.md +27 -0
  72. package/.agent/skills/workflow-guide/SKILL.md +57 -0
  73. package/.agent/skills/workflow-guide/resources/examples.md +68 -0
  74. package/README.ko.md +459 -0
  75. package/README.md +563 -0
  76. package/bin/cli.js +205 -0
  77. package/package.json +75 -0
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Component Template for Frontend Agent
3
+ *
4
+ * This is a reference template for creating new components.
5
+ * Follow this structure for consistency.
6
+ */
7
+
8
+ import { cn } from '@/lib/utils';
9
+
10
+ // 1. Type Definitions
11
+ interface ComponentNameProps {
12
+ className?: string;
13
+ children: React.ReactNode;
14
+ // Add specific props here
15
+ variant?: 'default' | 'primary' | 'secondary';
16
+ size?: 'sm' | 'md' | 'lg';
17
+ onClick?: () => void;
18
+ disabled?: boolean;
19
+ }
20
+
21
+ // 2. Main Component
22
+ export function ComponentName({
23
+ className,
24
+ children,
25
+ variant = 'default',
26
+ size = 'md',
27
+ onClick,
28
+ disabled = false,
29
+ }: ComponentNameProps) {
30
+ // 3. Hooks (if needed)
31
+ // const [state, setState] = useState();
32
+
33
+ // 4. Effects (if needed)
34
+ // useEffect(() => {}, []);
35
+
36
+ // 5. Event Handlers
37
+ const handleClick = () => {
38
+ if (disabled) return;
39
+ onClick?.();
40
+ };
41
+
42
+ // 6. Computed Values
43
+ const classes = cn(
44
+ // Base styles
45
+ 'inline-flex items-center justify-center rounded-md font-medium transition-colors',
46
+ // Variants
47
+ {
48
+ 'bg-primary text-primary-foreground hover:bg-primary/90': variant === 'primary',
49
+ 'bg-secondary text-secondary-foreground hover:bg-secondary/80': variant === 'secondary',
50
+ 'bg-background text-foreground hover:bg-accent': variant === 'default',
51
+ },
52
+ // Sizes
53
+ {
54
+ 'h-8 px-3 text-sm': size === 'sm',
55
+ 'h-10 px-4 text-base': size === 'md',
56
+ 'h-12 px-6 text-lg': size === 'lg',
57
+ },
58
+ // States
59
+ {
60
+ 'opacity-50 cursor-not-allowed': disabled,
61
+ },
62
+ className
63
+ );
64
+
65
+ // 7. Render
66
+ return (
67
+ <button
68
+ type="button"
69
+ className={classes}
70
+ onClick={handleClick}
71
+ disabled={disabled}
72
+ aria-disabled={disabled}
73
+ >
74
+ {children}
75
+ </button>
76
+ );
77
+ }
78
+
79
+ // 8. Sub-components (if needed)
80
+ ComponentName.Slot = function ComponentNameSlot({
81
+ className,
82
+ children,
83
+ }: ComponentNameProps) {
84
+ return (
85
+ <div className={cn('flex items-center gap-2', className)}>
86
+ {children}
87
+ </div>
88
+ );
89
+ };
90
+
91
+ // 9. Display Name (for debugging)
92
+ ComponentName.displayName = 'ComponentName';
@@ -0,0 +1,108 @@
1
+ # Frontend Agent - Error Recovery Playbook
2
+
3
+ When you encounter a failure, find the matching scenario and follow the recovery steps.
4
+ Do NOT stop or ask for help until you have exhausted the playbook.
5
+
6
+ ---
7
+
8
+ ## TypeScript Compilation Error
9
+
10
+ **Symptoms**: `TS2322`, `TS2345`, `Type X is not assignable to type Y`
11
+
12
+ 1. Read the error — which file, which line, which types conflict
13
+ 2. Check: is the interface/type definition correct?
14
+ 3. Check: is the API response type matching the expected shape?
15
+ 4. If API mismatch: update the type to match actual response (don't cast with `as any`)
16
+ 5. If generic issue: use explicit type parameter `<Type>` instead of inference
17
+ 6. **절대 하지 말 것**: `@ts-ignore`, `as any` — 타입 문제를 해결하지 않고 숨기는 것
18
+
19
+ ---
20
+
21
+ ## Build Error
22
+
23
+ **Symptoms**: `next build` fails, `Module not found`, `SyntaxError`
24
+
25
+ 1. Read the full error — which module, which file
26
+ 2. If missing dependency: note in result as "requires `npm install X`" — do NOT install yourself
27
+ 3. If import path wrong: use `search_for_pattern("export.*ComponentName")` to find actual path
28
+ 4. If dynamic import issue: ensure component is client-side (`'use client'`)
29
+ 5. Re-run build after fix to confirm
30
+
31
+ ---
32
+
33
+ ## Test Failure
34
+
35
+ **Symptoms**: `vitest` FAILED, `expect(X).toBe(Y)` assertion errors
36
+
37
+ 1. Read the error — expected vs received, which test file
38
+ 2. `find_symbol("ComponentName")` to check current implementation
39
+ 3. Determine: test outdated or implementation wrong?
40
+ - Test expects old behavior → update test
41
+ - Component bug → fix component
42
+ 4. Re-run the specific test: `npx vitest run path/to/test.ts`
43
+ 5. **3회 실패 시**: 다른 접근 방식 시도. progress에 기록
44
+
45
+ ---
46
+
47
+ ## Hydration Mismatch (Next.js)
48
+
49
+ **Symptoms**: `Hydration failed`, `Text content does not match server-rendered HTML`
50
+
51
+ 1. Find the component that renders differently on server vs client
52
+ 2. Common causes:
53
+ - `Date.now()` or `Math.random()` in render
54
+ - Browser-only APIs (`window`, `localStorage`) without `useEffect`
55
+ - Conditional rendering based on client-only state
56
+ 3. Fix: wrap client-only code in `useEffect` + state, or use `'use client'`
57
+ 4. If third-party component: wrap with `dynamic(() => import(...), { ssr: false })`
58
+
59
+ ---
60
+
61
+ ## API Integration Error
62
+
63
+ **Symptoms**: `Network Error`, `CORS`, `401 Unauthorized`, wrong data shape
64
+
65
+ 1. **CORS**: Check backend CORS config — is frontend origin allowed?
66
+ 2. **401**: Check token — is it in the header? is it expired?
67
+ 3. **Wrong data**: Log `response.data` and compare with expected type
68
+ 4. **Network Error**: Is the backend running? Correct port?
69
+ 5. If backend isn't your responsibility: document the expected API contract in result
70
+
71
+ ---
72
+
73
+ ## Styling / Layout Broken
74
+
75
+ **Symptoms**: Component renders but looks wrong, responsive breakpoint fails
76
+
77
+ 1. Check Tailwind classes — typo? wrong breakpoint prefix?
78
+ 2. Check parent container — is it blocking layout? (`overflow-hidden`, fixed width)
79
+ 3. Test at specific breakpoints: 320px, 768px, 1024px, 1440px
80
+ 4. Use browser DevTools to inspect computed styles
81
+ 5. If dark mode issue: check `dark:` variants applied
82
+
83
+ ---
84
+
85
+ ## Rate Limit / Quota Error (Gemini API)
86
+
87
+ **Symptoms**: `429`, `RESOURCE_EXHAUSTED`, `rate limit exceeded`
88
+
89
+ 1. **즉시 멈춤** — 추가 API 호출 하지 말 것
90
+ 2. 현재까지 작업을 `progress-{agent-id}.md`에 저장
91
+ 3. `result-{agent-id}.md`에 Status: `quota_exceeded` 기록
92
+ 4. 남은 작업 목록을 명시
93
+
94
+ ---
95
+
96
+ ## Serena Memory 접근 불가
97
+
98
+ 1. 1회 재시도
99
+ 2. 2회 연속 실패: 로컬 파일 `/tmp/progress-{agent-id}.md` 사용
100
+ 3. result에 `memory_fallback: true` 플래그 추가
101
+
102
+ ---
103
+
104
+ ## 일반 원칙
105
+
106
+ - **3회 실패**: 같은 접근 3번 실패하면 반드시 다른 방법 시도
107
+ - **막힘**: 5턴 이상 진전 없으면 현재 상태 저장하고 `Status: blocked` 기록
108
+ - **범위 초과**: backend 문제 발견 시 result에 기록만, 직접 수정하지 말 것
@@ -0,0 +1,77 @@
1
+ # Frontend Agent - Examples
2
+
3
+ ## Example 1: Feature Component
4
+
5
+ **Input**: "Create a todo list page with add, toggle, and delete"
6
+
7
+ **Output**:
8
+ ```
9
+ ## Task: Todo List Page
10
+
11
+ ### Implementation
12
+ - Components: TodoList, TodoItem, AddTodoForm
13
+ - Routes: /todos (page)
14
+ - State: TanStack Query for server state, local state for form
15
+
16
+ ### Files Created/Modified
17
+ - src/types/todo.ts (NEW) - Todo interface
18
+ - src/lib/api/todos.ts (NEW) - API client with TanStack Query hooks
19
+ - src/components/todo/TodoItem.tsx (NEW) - Single todo display
20
+ - src/components/todo/AddTodoForm.tsx (NEW) - Form with validation
21
+ - src/components/todo/TodoList.tsx (NEW) - List container
22
+ - src/app/todos/page.tsx (NEW) - Page component
23
+ - tests/components/TodoList.test.tsx (NEW)
24
+
25
+ ### Testing
26
+ - Unit tests: 5 passing
27
+ - Keyboard navigation: verified
28
+ - Responsive: tested at 4 breakpoints
29
+ ```
30
+
31
+ ## Example 2: Form with Validation
32
+
33
+ **Input**: "Create a user profile edit form"
34
+
35
+ **Output**:
36
+ ```
37
+ ## Task: Profile Edit Form
38
+
39
+ ### Implementation
40
+ - Components: ProfileForm (React Hook Form + Zod)
41
+ - Validation: name (required, 2-50 chars), email (valid format), bio (optional, max 500)
42
+ - API: PATCH /api/users/me via TanStack Query mutation
43
+ - UX: optimistic update, error toast, success redirect
44
+
45
+ ### Files Created
46
+ - src/types/user.ts
47
+ - src/lib/api/users.ts
48
+ - src/components/profile/ProfileForm.tsx
49
+ - src/app/profile/edit/page.tsx
50
+ - tests/components/ProfileForm.test.tsx
51
+ ```
52
+
53
+ ## Example 3: Responsive Dashboard
54
+
55
+ **Input**: "Build a dashboard with stats cards and a chart"
56
+
57
+ **Output**:
58
+ ```
59
+ ## Task: Dashboard Page
60
+
61
+ ### Implementation
62
+ - Components: StatCard, StatsGrid, ActivityChart
63
+ - Layout: CSS Grid - 1 col (mobile), 2 col (tablet), 4 col (desktop)
64
+ - Chart: recharts with responsive container
65
+ - Data: TanStack Query with 30s refetch interval
66
+
67
+ ### Accessibility
68
+ - [x] All cards have aria-label with stat value
69
+ - [x] Chart has aria-describedby with text summary
70
+ - [x] Color contrast 4.5:1 on all text
71
+
72
+ ### Files Created
73
+ - src/components/dashboard/StatCard.tsx
74
+ - src/components/dashboard/StatsGrid.tsx
75
+ - src/components/dashboard/ActivityChart.tsx
76
+ - src/app/dashboard/page.tsx
77
+ ```
@@ -0,0 +1,49 @@
1
+ # Frontend Agent - Execution Protocol
2
+
3
+ ## Step 0: Prepare
4
+ 1. **Assess difficulty** — see `../_shared/difficulty-guide.md`
5
+ - **Simple**: Skip to Step 3 | **Medium**: All 4 steps | **Complex**: All steps + checkpoints
6
+ 2. **Check lessons** — read your domain section in `../_shared/lessons-learned.md`
7
+ 3. **Clarify requirements** — follow `../_shared/clarification-protocol.md`
8
+ - Check **Uncertainty Triggers**: 비즈니스 로직, 보안/인증, 기존 코드 충돌?
9
+ - Determine level: LOW → proceed | MEDIUM → present options | HIGH → ask immediately
10
+ 4. **Budget context** — follow `../_shared/context-budget.md` (read symbols, not whole files)
11
+
12
+ **⚠️ Intelligent Escalation**: When uncertain, escalate early. Don't blindly proceed.
13
+
14
+ Follow these steps in order (adjust depth by difficulty).
15
+
16
+ ## Step 1: Analyze
17
+ - Read the task requirements carefully
18
+ - Identify which components, pages, and hooks are needed
19
+ - Check existing code with Serena: `get_symbols_overview("src/components")`, `find_symbol("ComponentName")`
20
+ - Review existing patterns: `find_referencing_symbols("Button")` to understand usage conventions
21
+ - List assumptions; ask if unclear
22
+
23
+ ## Step 2: Plan
24
+ - Decide on component structure (which are new, which extend existing)
25
+ - Define props interfaces with TypeScript
26
+ - Plan state management approach (local state, Context, Zustand)
27
+ - Identify API integration points (TanStack Query hooks)
28
+ - Plan responsive breakpoints and accessibility requirements
29
+
30
+ ## Step 3: Implement
31
+ - Create/modify files in this order:
32
+ 1. TypeScript types/interfaces
33
+ 2. API client hooks (TanStack Query)
34
+ 3. Reusable UI components (shadcn/ui based)
35
+ 4. Feature components (compose UI + logic)
36
+ 5. Page components (route-level)
37
+ 6. Tests (unit + integration)
38
+ - Use `resources/component-template.tsx` as reference
39
+ - Follow `resources/tailwind-rules.md` for styling
40
+
41
+ ## Step 4: Verify
42
+ - Run `resources/checklist.md` items
43
+ - Run `../_shared/common-checklist.md` items
44
+ - Check TypeScript strict mode: no errors
45
+ - Verify responsive design at 320px, 768px, 1024px, 1440px
46
+ - Test keyboard navigation and screen reader compatibility
47
+
48
+ ## On Error
49
+ See `resources/error-playbook.md` for recovery steps.
@@ -0,0 +1,205 @@
1
+ # Frontend Agent - Code Snippets
2
+
3
+ Copy-paste ready patterns. Use these as starting points, adapt to the specific task.
4
+
5
+ ---
6
+
7
+ ## React Component with Props
8
+
9
+ ```tsx
10
+ interface CardProps {
11
+ title: string;
12
+ description?: string;
13
+ onClick?: () => void;
14
+ }
15
+
16
+ export function Card({ title, description, onClick }: CardProps) {
17
+ return (
18
+ <button
19
+ onClick={onClick}
20
+ className="rounded-lg border bg-card p-4 text-left shadow-sm transition-colors hover:bg-accent"
21
+ >
22
+ <h3 className="text-lg font-semibold">{title}</h3>
23
+ {description && (
24
+ <p className="mt-1 text-sm text-muted-foreground">{description}</p>
25
+ )}
26
+ </button>
27
+ );
28
+ }
29
+ ```
30
+
31
+ ---
32
+
33
+ ## TanStack Query Hook
34
+
35
+ ```tsx
36
+ import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
37
+
38
+ interface Todo {
39
+ id: string;
40
+ title: string;
41
+ completed: boolean;
42
+ }
43
+
44
+ export function useTodos() {
45
+ return useQuery<Todo[]>({
46
+ queryKey: ["todos"],
47
+ queryFn: async () => {
48
+ const res = await fetch("/api/todos", {
49
+ headers: { Authorization: `Bearer ${getToken()}` },
50
+ });
51
+ if (!res.ok) throw new Error("Failed to fetch");
52
+ return res.json();
53
+ },
54
+ });
55
+ }
56
+
57
+ export function useCreateTodo() {
58
+ const queryClient = useQueryClient();
59
+ return useMutation({
60
+ mutationFn: async (data: { title: string }) => {
61
+ const res = await fetch("/api/todos", {
62
+ method: "POST",
63
+ headers: {
64
+ "Content-Type": "application/json",
65
+ Authorization: `Bearer ${getToken()}`,
66
+ },
67
+ body: JSON.stringify(data),
68
+ });
69
+ if (!res.ok) throw new Error("Failed to create");
70
+ return res.json();
71
+ },
72
+ onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
73
+ });
74
+ }
75
+ ```
76
+
77
+ ---
78
+
79
+ ## Form with React Hook Form + Zod
80
+
81
+ ```tsx
82
+ "use client";
83
+
84
+ import { useForm } from "react-hook-form";
85
+ import { zodResolver } from "@hookform/resolvers/zod";
86
+ import { z } from "zod";
87
+
88
+ const schema = z.object({
89
+ email: z.string().email("Invalid email"),
90
+ password: z.string().min(8, "At least 8 characters"),
91
+ });
92
+
93
+ type FormData = z.infer<typeof schema>;
94
+
95
+ export function LoginForm({ onSubmit }: { onSubmit: (data: FormData) => void }) {
96
+ const {
97
+ register,
98
+ handleSubmit,
99
+ formState: { errors, isSubmitting },
100
+ } = useForm<FormData>({ resolver: zodResolver(schema) });
101
+
102
+ return (
103
+ <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
104
+ <div>
105
+ <label htmlFor="email" className="text-sm font-medium">
106
+ Email
107
+ </label>
108
+ <input
109
+ id="email"
110
+ type="email"
111
+ {...register("email")}
112
+ className="mt-1 w-full rounded-md border px-3 py-2"
113
+ aria-invalid={!!errors.email}
114
+ />
115
+ {errors.email && (
116
+ <p className="mt-1 text-sm text-destructive">{errors.email.message}</p>
117
+ )}
118
+ </div>
119
+ <div>
120
+ <label htmlFor="password" className="text-sm font-medium">
121
+ Password
122
+ </label>
123
+ <input
124
+ id="password"
125
+ type="password"
126
+ {...register("password")}
127
+ className="mt-1 w-full rounded-md border px-3 py-2"
128
+ aria-invalid={!!errors.password}
129
+ />
130
+ {errors.password && (
131
+ <p className="mt-1 text-sm text-destructive">{errors.password.message}</p>
132
+ )}
133
+ </div>
134
+ <button
135
+ type="submit"
136
+ disabled={isSubmitting}
137
+ className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
138
+ >
139
+ {isSubmitting ? "Signing in..." : "Sign in"}
140
+ </button>
141
+ </form>
142
+ );
143
+ }
144
+ ```
145
+
146
+ ---
147
+
148
+ ## Loading / Error / Empty States
149
+
150
+ ```tsx
151
+ interface AsyncStateProps<T> {
152
+ data: T | undefined;
153
+ isLoading: boolean;
154
+ error: Error | null;
155
+ empty: React.ReactNode;
156
+ children: (data: T) => React.ReactNode;
157
+ }
158
+
159
+ export function AsyncState<T>({ data, isLoading, error, empty, children }: AsyncStateProps<T>) {
160
+ if (isLoading) return <div className="flex justify-center p-8"><Spinner /></div>;
161
+ if (error) return <ErrorCard message={error.message} />;
162
+ if (!data || (Array.isArray(data) && data.length === 0)) return <>{empty}</>;
163
+ return <>{children(data)}</>;
164
+ }
165
+ ```
166
+
167
+ ---
168
+
169
+ ## Responsive Grid Layout
170
+
171
+ ```tsx
172
+ export function StatsGrid({ children }: { children: React.ReactNode }) {
173
+ return (
174
+ <div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
175
+ {children}
176
+ </div>
177
+ );
178
+ }
179
+ ```
180
+
181
+ ---
182
+
183
+ ## Vitest Component Test
184
+
185
+ ```tsx
186
+ import { render, screen } from "@testing-library/react";
187
+ import userEvent from "@testing-library/user-event";
188
+ import { describe, it, expect, vi } from "vitest";
189
+ import { Card } from "./Card";
190
+
191
+ describe("Card", () => {
192
+ it("renders title and description", () => {
193
+ render(<Card title="Test" description="Desc" />);
194
+ expect(screen.getByText("Test")).toBeInTheDocument();
195
+ expect(screen.getByText("Desc")).toBeInTheDocument();
196
+ });
197
+
198
+ it("calls onClick when clicked", async () => {
199
+ const onClick = vi.fn();
200
+ render(<Card title="Test" onClick={onClick} />);
201
+ await userEvent.click(screen.getByText("Test"));
202
+ expect(onClick).toHaveBeenCalledOnce();
203
+ });
204
+ });
205
+ ```