From f47556ef6efe8f7df94e816c649d49bad6e09201 Mon Sep 17 00:00:00 2001 From: Ho Ngoc Hai Date: Sun, 18 Jan 2026 23:57:52 +0700 Subject: [PATCH] feat: Add new agent skills for React state management, testing, UI components, and enterprise architecture, along with Tailwind CSS design system and frontend development workflows. --- .../react-enterprise-architect/SKILL.md | 219 +++++++++++ .agent/skills/react-state-management/SKILL.md | 306 +++++++++++++++ .agent/skills/react-testing-patterns/SKILL.md | 314 ++++++++++++++++ .agent/skills/react-ui-components/SKILL.md | 355 ++++++++++++++++++ .agent/skills/tailwind-design-system/SKILL.md | 308 +++++++++++++++ .agent/workflows/frontend-dev.md | 86 +++++ .agent/workflows/storybook-ops.md | 106 ++++++ 7 files changed, 1694 insertions(+) create mode 100644 .agent/skills/react-enterprise-architect/SKILL.md create mode 100644 .agent/skills/react-state-management/SKILL.md create mode 100644 .agent/skills/react-testing-patterns/SKILL.md create mode 100644 .agent/skills/react-ui-components/SKILL.md create mode 100644 .agent/skills/tailwind-design-system/SKILL.md create mode 100644 .agent/workflows/frontend-dev.md create mode 100644 .agent/workflows/storybook-ops.md diff --git a/.agent/skills/react-enterprise-architect/SKILL.md b/.agent/skills/react-enterprise-architect/SKILL.md new file mode 100644 index 00000000..6c78bb20 --- /dev/null +++ b/.agent/skills/react-enterprise-architect/SKILL.md @@ -0,0 +1,219 @@ +--- +name: react-enterprise-architect +description: React/Next.js enterprise architecture patterns cho GoodGo frontend apps. Use for project structure, component organization, data fetching, routing, và App Router patterns. +compatibility: "next>=16, react>=19, typescript>=5" +metadata: + author: Velik Ho + version: "1.0" +--- + +# React Enterprise Architect / Kiến Trúc React Enterprise + +## When to Use This Skill / Khi Nào Sử Dụng + +Use this skill when: +- Creating new frontend apps (web-client, web-admin, web-merchant...) +- Organizing feature modules / Tổ chức feature modules +- Setting up Next.js App Router / Thiết lập Next.js App Router +- Implementing data fetching patterns / Triển khai patterns fetch data +- Designing component hierarchies / Thiết kế cấu trúc component + +## Core Principles / Nguyên Tắc Cốt Lõi + +1. **Feature-Based Organization**: Group by feature, not by type +2. **Server-First**: Prefer Server Components, use Client Components sparingly +3. **Colocation**: Keep related code together +4. **Single Responsibility**: Each module has one clear purpose +5. **Type Safety**: TypeScript everywhere + +## Project Structure / Cấu Trúc Dự Án + +``` +apps/web-{app}/ +├── src/ +│ ├── app/ # Next.js App Router +│ │ ├── (routes)/ # Route groups +│ │ ├── api/ # API routes +│ │ ├── layout.tsx # Root layout +│ │ └── page.tsx # Home page +│ ├── features/ # Feature modules (CORE) +│ │ ├── auth/ # Authentication feature +│ │ ├── chat/ # Chat feature +│ │ ├── dashboard/ # Dashboard feature +│ │ └── shared/ # Shared components & utils +│ │ ├── components/ # Reusable components +│ │ ├── hooks/ # Custom hooks +│ │ ├── lib/ # Utilities +│ │ ├── types/ # Shared types +│ │ └── i18n/ # Internationalization +│ ├── stores/ # Zustand stores +│ │ └── __tests__/ # Store tests +│ ├── services/ # API service clients +│ ├── providers/ # React context providers +│ ├── styles/ # Global styles, theme +│ └── stories/ # Storybook stories (docs) +├── e2e/ # Playwright E2E tests +├── public/ # Static assets +└── .storybook/ # Storybook config +``` + +## Key Patterns / Mẫu Chính + +### Feature Module Structure + +``` +features/chat/ +├── components/ # Feature-specific components +│ ├── ChatInput.tsx +│ ├── ChatMessage.tsx +│ └── ChatSidebar.tsx +├── hooks/ # Feature-specific hooks +│ └── useChat.ts +├── types/ # Feature types +│ └── chat.types.ts +├── utils/ # Feature utilities +│ └── formatMessage.ts +└── index.ts # Public exports (barrel file) +``` + +### Server vs Client Components + +```tsx +// ✅ GOOD: Server Component (default) +// EN: No "use client" directive - renders on server +// VI: Không có "use client" - render trên server +async function ChatHistory({ userId }: { userId: string }) { + const messages = await fetchMessages(userId); // Server-side fetch + return ; +} + +// ✅ GOOD: Client Component (only when needed) +// EN: Add "use client" for interactivity +// VI: Thêm "use client" khi cần tương tác +'use client'; + +import { useState } from 'react'; + +function ChatInput() { + const [message, setMessage] = useState(''); + return setMessage(e.target.value)} />; +} +``` + +### Data Fetching Pattern + +```tsx +// EN: Server Component with async data fetching +// VI: Server Component với async data fetching +async function Dashboard() { + // Direct fetch in Server Component + const stats = await fetchStats(); + + return ( +
+ + {/* Client component for interactive chart */} + +
+ ); +} +``` + +### Route Organization (App Router) + +``` +app/ +├── (auth)/ # Auth route group (no layout) +│ ├── login/page.tsx +│ └── register/page.tsx +├── (dashboard)/ # Dashboard route group with layout +│ ├── layout.tsx # Shared dashboard layout +│ ├── page.tsx # /dashboard +│ └── settings/page.tsx # /dashboard/settings +├── layout.tsx # Root layout (providers, global UI) +├── loading.tsx # Global loading state +├── error.tsx # Global error boundary +└── not-found.tsx # 404 page +``` + +### Barrel Exports (index.ts) + +```typescript +// features/chat/index.ts +// EN: Export only public API, hide internal components +// VI: Chỉ export public API, ẩn internal components + +export { ChatContainer } from './components/ChatContainer'; +export { useChat } from './hooks/useChat'; +export type { Message, ChatState } from './types/chat.types'; + +// ❌ DON'T export internal components +// export { ChatMessageBubble } from './components/internal/ChatMessageBubble'; +``` + +## Common Mistakes / Lỗi Thường Gặp + +### 1. Using Client Components Unnecessarily + +```tsx +// ❌ BAD: Every component is client +'use client'; +function Header() { return

Title

; } + +// ✅ GOOD: Server Component (default) +function Header() { return

Title

; } +``` + +### 2. Mixing Feature Concerns + +```tsx +// ❌ BAD: Chat component directly uses auth store +import { useAuthStore } from '@/stores/auth-store'; + +function ChatInput() { + const user = useAuthStore(s => s.user); // Cross-feature dependency +} + +// ✅ GOOD: Receive user as prop from parent +function ChatInput({ userId }: { userId: string }) { + // Feature-isolated, receives data via props +} +``` + +### 3. Forgetting Loading/Error States + +```tsx +// ✅ GOOD: Always handle loading and error +// app/chat/loading.tsx +export default function Loading() { + return ; +} + +// app/chat/error.tsx +'use client'; +export default function Error({ error, reset }: { error: Error; reset: () => void }) { + return ; +} +``` + +## Quick Reference / Tham Chiếu Nhanh + +| Concept | Pattern | +|---------|---------| +| Folder structure | Feature-based (`features/`) | +| Components location | `features/{name}/components/` | +| Shared components | `features/shared/components/` | +| Stores | `stores/{name}-store.ts` | +| API routes | `app/api/{resource}/route.ts` | +| Page routes | `app/(group)/page.tsx` | +| Layouts | `app/layout.tsx`, route `layout.tsx` | +| Loading states | `loading.tsx` per route | +| Error handling | `error.tsx` per route | + +## Resources / Tài Nguyên + +- [React State Management](../react-state-management/SKILL.md) - Zustand, TanStack Query patterns +- [React UI Components](../react-ui-components/SKILL.md) - Component development +- [React Testing Patterns](../react-testing-patterns/SKILL.md) - Testing strategies +- [Project Rules](../project-rules/SKILL.md) - GoodGo coding standards +- [Next.js App Router Docs](https://nextjs.org/docs/app) diff --git a/.agent/skills/react-state-management/SKILL.md b/.agent/skills/react-state-management/SKILL.md new file mode 100644 index 00000000..e8a11094 --- /dev/null +++ b/.agent/skills/react-state-management/SKILL.md @@ -0,0 +1,306 @@ +--- +name: react-state-management +description: State management patterns với Zustand và TanStack Query cho React/Next.js. Use for client state, server state, caching, và optimistic updates. +compatibility: "zustand>=5, @tanstack/react-query>=5, react>=19" +metadata: + author: Velik Ho + version: "1.0" +--- + +# React State Management / Quản Lý State React + +## When to Use This Skill / Khi Nào Sử Dụng + +Use this skill when: +- Creating Zustand stores / Tạo Zustand stores +- Implementing TanStack Query / Triển khai TanStack Query +- Managing client vs server state / Quản lý client vs server state +- Implementing optimistic updates / Triển khai optimistic updates +- Testing stores and queries / Testing stores và queries + +## Core Concepts / Khái Niệm Cốt Lõi + +### State Categories + +| Category | Solution | Use Case | +|----------|----------|----------| +| **UI State** | Zustand | Theme, modals, local preferences | +| **Server State** | TanStack Query | API data, caching, sync | +| **URL State** | Next.js Router | Filters, pagination, search | +| **Form State** | React Hook Form | Form inputs, validation | + +## Key Patterns / Mẫu Chính + +### Zustand Store Pattern + +```typescript +// stores/auth-store.ts +import { create } from 'zustand'; +import { devtools, persist } from 'zustand/middleware'; + +/** + * EN: Auth store interface + * VI: Interface cho auth store + */ +interface AuthState { + user: User | null; + isAuthenticated: boolean; + // Actions + login: (user: User) => void; + logout: () => void; +} + +/** + * EN: Create store with middleware + * VI: Tạo store với middleware + */ +export const useAuthStore = create()( + devtools( + persist( + (set) => ({ + user: null, + isAuthenticated: false, + + login: (user) => set({ user, isAuthenticated: true }, false, 'auth/login'), + logout: () => set({ user: null, isAuthenticated: false }, false, 'auth/logout'), + }), + { name: 'auth-storage' } + ), + { name: 'AuthStore' } + ) +); +``` + +### Selector Pattern (Performance) + +```typescript +// ✅ GOOD: Use selectors to prevent unnecessary re-renders +// VI: Sử dụng selectors để tránh re-render không cần thiết +function UserAvatar() { + // Only re-renders when user.avatar changes + const avatar = useAuthStore((state) => state.user?.avatar); + return ; +} + +// ❌ BAD: Subscribes to entire store +function UserAvatar() { + const { user } = useAuthStore(); // Re-renders on any store change + return ; +} +``` + +### TanStack Query Pattern + +```typescript +// services/api.ts +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; + +/** + * EN: Query keys factory for type-safe keys + * VI: Factory cho query keys đảm bảo type-safe + */ +export const chatKeys = { + all: ['chats'] as const, + lists: () => [...chatKeys.all, 'list'] as const, + list: (filters: ChatFilters) => [...chatKeys.lists(), filters] as const, + details: () => [...chatKeys.all, 'detail'] as const, + detail: (id: string) => [...chatKeys.details(), id] as const, +}; + +/** + * EN: Custom hook for fetching chats + * VI: Custom hook để fetch chats + */ +export function useChats(filters: ChatFilters) { + return useQuery({ + queryKey: chatKeys.list(filters), + queryFn: () => fetchChats(filters), + staleTime: 5 * 60 * 1000, // 5 minutes + }); +} + +/** + * EN: Mutation with cache invalidation + * VI: Mutation với invalidation cache + */ +export function useCreateChat() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: createChat, + onSuccess: () => { + // Invalidate all chat lists + queryClient.invalidateQueries({ queryKey: chatKeys.lists() }); + }, + }); +} +``` + +### Optimistic Updates + +```typescript +/** + * EN: Optimistic update for better UX + * VI: Optimistic update cho UX tốt hơn + */ +export function useSendMessage() { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: sendMessage, + onMutate: async (newMessage) => { + // Cancel outgoing refetches + await queryClient.cancelQueries({ queryKey: chatKeys.detail(newMessage.chatId) }); + + // Snapshot previous value + const previous = queryClient.getQueryData(chatKeys.detail(newMessage.chatId)); + + // Optimistically update + queryClient.setQueryData(chatKeys.detail(newMessage.chatId), (old: Chat) => ({ + ...old, + messages: [...old.messages, { ...newMessage, status: 'sending' }], + })); + + return { previous }; + }, + onError: (err, newMessage, context) => { + // Rollback on error + queryClient.setQueryData( + chatKeys.detail(newMessage.chatId), + context?.previous + ); + }, + onSettled: (_, __, { chatId }) => { + // Always refetch after error or success + queryClient.invalidateQueries({ queryKey: chatKeys.detail(chatId) }); + }, + }); +} +``` + +### Provider Setup + +```tsx +// providers/query-provider.tsx +'use client'; + +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { ReactQueryDevtools } from '@tanstack/react-query-devtools'; +import { useState } from 'react'; + +/** + * EN: Query provider with client-side only instantiation + * VI: Query provider với khởi tạo chỉ phía client + */ +export function QueryProvider({ children }: { children: React.ReactNode }) { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: 60 * 1000, // 1 minute + refetchOnWindowFocus: false, + }, + }, + }) + ); + + return ( + + {children} + + + ); +} +``` + +## Testing Patterns / Mẫu Testing + +### Testing Zustand Stores + +```typescript +// stores/__tests__/auth-store.test.ts +import { describe, it, expect, beforeEach } from 'vitest'; +import { useAuthStore } from '../auth-store'; + +describe('AuthStore', () => { + beforeEach(() => { + // Reset store before each test + useAuthStore.setState({ user: null, isAuthenticated: false }); + }); + + it('should login user', () => { + const user = { id: '1', name: 'Test' }; + useAuthStore.getState().login(user); + + expect(useAuthStore.getState().user).toEqual(user); + expect(useAuthStore.getState().isAuthenticated).toBe(true); + }); + + it('should logout user', () => { + useAuthStore.setState({ user: { id: '1' }, isAuthenticated: true }); + useAuthStore.getState().logout(); + + expect(useAuthStore.getState().user).toBeNull(); + expect(useAuthStore.getState().isAuthenticated).toBe(false); + }); +}); +``` + +## Common Mistakes / Lỗi Thường Gặp + +### 1. Subscribing to Entire Store + +```typescript +// ❌ BAD: Component re-renders on any store change +const store = useAuthStore(); + +// ✅ GOOD: Select only needed data +const userName = useAuthStore((s) => s.user?.name); +``` + +### 2. Missing Query Key Invalidation + +```typescript +// ❌ BAD: Cache becomes stale after mutation +useMutation({ mutationFn: updateUser }); + +// ✅ GOOD: Invalidate related queries +useMutation({ + mutationFn: updateUser, + onSuccess: () => queryClient.invalidateQueries({ queryKey: userKeys.all }), +}); +``` + +### 3. Creating QueryClient in Component Body + +```tsx +// ❌ BAD: New client on every render +function App() { + const queryClient = new QueryClient(); // Recreated every render! +} + +// ✅ GOOD: Use useState or module scope +function App() { + const [queryClient] = useState(() => new QueryClient()); +} +``` + +## Quick Reference / Tham Chiếu Nhanh + +| Pattern | When to Use | +|---------|-------------| +| `create()` | Basic Zustand store | +| `devtools()` | Enable Redux DevTools | +| `persist()` | Persist to localStorage | +| `useQuery()` | Fetch & cache server data | +| `useMutation()` | Create/Update/Delete operations | +| `queryClient.invalidateQueries()` | Refresh cache after mutation | +| `queryClient.setQueryData()` | Optimistic updates | + +## Resources / Tài Nguyên + +- [React Enterprise Architect](../react-enterprise-architect/SKILL.md) - Architecture patterns +- [React Testing Patterns](../react-testing-patterns/SKILL.md) - Testing strategies +- [Zustand Docs](https://zustand-demo.pmnd.rs/) +- [TanStack Query Docs](https://tanstack.com/query/latest) diff --git a/.agent/skills/react-testing-patterns/SKILL.md b/.agent/skills/react-testing-patterns/SKILL.md new file mode 100644 index 00000000..c04a8d6a --- /dev/null +++ b/.agent/skills/react-testing-patterns/SKILL.md @@ -0,0 +1,314 @@ +--- +name: react-testing-patterns +description: Testing patterns cho React với Vitest, React Testing Library, và Playwright. Use for unit tests, component tests, E2E tests, và Storybook integration testing. +compatibility: "vitest>=4, @testing-library/react>=14, playwright>=1" +metadata: + author: Velik Ho + version: "1.0" +--- + +# React Testing Patterns / Mẫu Testing React + +## When to Use This Skill / Khi Nào Sử Dụng + +Use this skill when: +- Writing unit tests / Viết unit tests +- Testing React components / Testing React components +- Creating E2E tests with Playwright / Tạo E2E tests với Playwright +- Testing Storybook stories / Testing Storybook stories +- Mocking APIs and modules / Mocking APIs và modules + +## Testing Pyramid / Tháp Testing + +``` + ┌───────────┐ + │ E2E │ Playwright (few, slow, high confidence) + ├───────────┤ + │Integration│ RTL + Vitest (moderate) + ├───────────┤ + │ Unit │ Vitest (many, fast, focused) + └───────────┘ +``` + +## Key Patterns / Mẫu Chính + +### Test File Organization + +``` +src/ +├── features/auth/ +│ ├── components/ +│ │ └── LoginForm/ +│ │ ├── LoginForm.tsx +│ │ └── LoginForm.test.tsx # Component test +│ └── hooks/ +│ └── useAuth.test.ts # Hook test +├── stores/ +│ └── __tests__/ +│ └── auth-store.test.ts # Store test +└── __tests__/ # Integration tests + └── auth-flow.test.tsx +e2e/ +└── auth.spec.ts # E2E tests +``` + +### Unit Testing with Vitest + +```typescript +// hooks/useCounter.test.ts +import { renderHook, act } from '@testing-library/react'; +import { describe, it, expect } from 'vitest'; +import { useCounter } from './useCounter'; + +describe('useCounter', () => { + it('should increment counter', () => { + const { result } = renderHook(() => useCounter()); + + act(() => { + result.current.increment(); + }); + + expect(result.current.count).toBe(1); + }); + + it('should reset counter', () => { + const { result } = renderHook(() => useCounter(5)); + + act(() => { + result.current.reset(); + }); + + expect(result.current.count).toBe(0); + }); +}); +``` + +### Component Testing with RTL + +```tsx +// components/LoginForm/LoginForm.test.tsx +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { describe, it, expect, vi } from 'vitest'; +import { LoginForm } from './LoginForm'; + +describe('LoginForm', () => { + it('should submit form with valid data', async () => { + const onSubmit = vi.fn(); + const user = userEvent.setup(); + + render(); + + // Type in form fields + await user.type(screen.getByLabelText(/email/i), 'test@example.com'); + await user.type(screen.getByLabelText(/password/i), 'password123'); + + // Submit form + await user.click(screen.getByRole('button', { name: /login/i })); + + await waitFor(() => { + expect(onSubmit).toHaveBeenCalledWith({ + email: 'test@example.com', + password: 'password123', + }); + }); + }); + + it('should show validation errors', async () => { + const user = userEvent.setup(); + + render(); + + // Submit without filling form + await user.click(screen.getByRole('button', { name: /login/i })); + + expect(await screen.findByText(/email is required/i)).toBeInTheDocument(); + }); +}); +``` + +### Testing with Query Wrapper + +```tsx +// test/utils.tsx +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { render as rtlRender } from '@testing-library/react'; + +/** + * EN: Custom render with providers + * VI: Custom render với providers + */ +export function renderWithProviders(ui: React.ReactElement) { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }); + + return rtlRender( + + {ui} + + ); +} + +// Usage +import { renderWithProviders } from '@/test/utils'; + +it('should load data', async () => { + renderWithProviders(); + expect(await screen.findByText('John Doe')).toBeInTheDocument(); +}); +``` + +### Mocking with Vitest + +```typescript +// Mocking modules +import { vi } from 'vitest'; + +// Mock entire module +vi.mock('@/services/api', () => ({ + fetchUser: vi.fn().mockResolvedValue({ id: '1', name: 'Test' }), +})); + +// Mock specific function +import * as api from '@/services/api'; +vi.spyOn(api, 'fetchUser').mockResolvedValue({ id: '1', name: 'Test' }); + +// Mock with implementation +vi.mock('next/navigation', () => ({ + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + }), + usePathname: () => '/dashboard', +})); +``` + +### Playwright E2E Testing + +```typescript +// e2e/auth.spec.ts +import { test, expect } from '@playwright/test'; + +test.describe('Authentication', () => { + test('should login successfully', async ({ page }) => { + await page.goto('/login'); + + // Fill form + await page.getByLabel('Email').fill('test@example.com'); + await page.getByLabel('Password').fill('password123'); + + // Submit + await page.getByRole('button', { name: 'Login' }).click(); + + // Verify redirect + await expect(page).toHaveURL('/dashboard'); + await expect(page.getByText('Welcome back')).toBeVisible(); + }); + + test('should show error on invalid credentials', async ({ page }) => { + await page.goto('/login'); + + await page.getByLabel('Email').fill('wrong@example.com'); + await page.getByLabel('Password').fill('wrongpassword'); + await page.getByRole('button', { name: 'Login' }).click(); + + await expect(page.getByText('Invalid credentials')).toBeVisible(); + }); +}); +``` + +### Storybook Interaction Testing + +```tsx +// Button.stories.tsx +import type { Meta, StoryObj } from '@storybook/react'; +import { within, userEvent, expect } from '@storybook/test'; +import { Button } from './Button'; + +const meta: Meta = { + title: 'Components/Button', + component: Button, +}; + +export default meta; + +export const ClickTest: StoryObj = { + args: { children: 'Click me' }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const button = canvas.getByRole('button'); + + // Test interaction + await userEvent.click(button); + + // Assert + await expect(button).toHaveFocus(); + }, +}; +``` + +## Common Mistakes / Lỗi Thường Gặp + +### 1. Testing Implementation Details + +```tsx +// ❌ BAD: Testing internal state +expect(component.state.isOpen).toBe(true); + +// ✅ GOOD: Testing user-visible behavior +expect(screen.getByRole('dialog')).toBeVisible(); +``` + +### 2. Not Waiting for Async Operations + +```tsx +// ❌ BAD: Immediate assertion +render(); +expect(screen.getByText('Data')).toBeInTheDocument(); // Fails! + +// ✅ GOOD: Wait for element +render(); +expect(await screen.findByText('Data')).toBeInTheDocument(); +``` + +### 3. Using getBy* for Absent Elements + +```tsx +// ❌ BAD: Throws error if not found +expect(screen.getByText('Error')).not.toBeInTheDocument(); + +// ✅ GOOD: Use queryBy* for absence checks +expect(screen.queryByText('Error')).not.toBeInTheDocument(); +``` + +## Quick Reference / Tham Chiếu Nhanh + +| Query | Use Case | +|-------|----------| +| `getBy*` | Element must exist, throws if not | +| `queryBy*` | Element may not exist (for negative assertions) | +| `findBy*` | Async, waits for element | +| `*ByRole` | Preferred, most accessible | +| `*ByLabelText` | Form inputs | +| `*ByText` | Non-interactive text | +| `*ByTestId` | Last resort | + +| Tool | Use Case | +|------|----------| +| Vitest | Unit & integration tests | +| RTL | Component testing | +| userEvent | User interaction simulation | +| Playwright | E2E browser tests | +| MSW | API mocking | + +## Resources / Tài Nguyên + +- [React State Management](../react-state-management/SKILL.md) - Store testing +- [React UI Components](../react-ui-components/SKILL.md) - Component patterns +- [Testing Library Docs](https://testing-library.com/docs/react-testing-library/intro/) +- [Vitest Docs](https://vitest.dev/) +- [Playwright Docs](https://playwright.dev/) diff --git a/.agent/skills/react-ui-components/SKILL.md b/.agent/skills/react-ui-components/SKILL.md new file mode 100644 index 00000000..262b0531 --- /dev/null +++ b/.agent/skills/react-ui-components/SKILL.md @@ -0,0 +1,355 @@ +--- +name: react-ui-components +description: UI component patterns với React Aria, Radix UI, và Storybook cho GoodGo. Use for accessible components, composition patterns, animations, và component documentation. +compatibility: "react-aria>=3, @radix-ui/*>=1, storybook>=10, framer-motion>=12" +metadata: + author: Velik Ho + version: "1.0" +--- + +# React UI Components / Components UI React + +## When to Use This Skill / Khi Nào Sử Dụng + +Use this skill when: +- Creating reusable components / Tạo components tái sử dụng +- Implementing accessible UI / Triển khai UI accessible +- Writing Storybook stories / Viết Storybook stories +- Adding animations / Thêm animations +- Styling with CVA & Tailwind / Styling với CVA & Tailwind + +## Core Principles / Nguyên Tắc Cốt Lõi + +1. **Accessibility First**: Use React Aria/Radix for keyboard & screen reader support +2. **Composition over Props**: Build flexible components via composition +3. **Variant-based Styling**: Use CVA for maintainable variant logic +4. **Document with Stories**: Every component needs Storybook stories + +## Key Patterns / Mẫu Chính + +### Component Structure + +``` +features/shared/components/ +├── Button/ +│ ├── Button.tsx # Main component +│ ├── Button.stories.tsx # Storybook stories +│ ├── Button.test.tsx # Unit tests +│ └── index.ts # Barrel export +└── index.ts # Public exports +``` + +### CVA for Variants (Class Variance Authority) + +```tsx +// components/Button/Button.tsx +import { cva, type VariantProps } from 'class-variance-authority'; +import { clsx } from 'clsx'; + +/** + * EN: Button variants using CVA + * VI: Variants cho Button sử dụng CVA + */ +const buttonVariants = cva( + // Base styles + 'inline-flex items-center justify-center rounded-md font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 disabled:pointer-events-none disabled:opacity-50', + { + variants: { + variant: { + primary: 'bg-accent-primary text-white hover:bg-accent-primary/90', + secondary: 'bg-bg-secondary text-text-primary hover:bg-bg-tertiary', + ghost: 'hover:bg-bg-secondary', + destructive: 'bg-accent-error text-white hover:bg-accent-error/90', + }, + size: { + sm: 'h-8 px-3 text-sm', + md: 'h-10 px-4 text-base', + lg: 'h-12 px-6 text-lg', + }, + }, + defaultVariants: { + variant: 'primary', + size: 'md', + }, + } +); + +interface ButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + isLoading?: boolean; +} + +export function Button({ + className, + variant, + size, + isLoading, + children, + ...props +}: ButtonProps) { + return ( + + ); +} +``` + +### React Aria Pattern + +```tsx +// components/Switch/Switch.tsx +import { useToggleState } from 'react-stately'; +import { useSwitch, useFocusRing, VisuallyHidden } from 'react-aria'; +import { useRef } from 'react'; + +/** + * EN: Accessible switch using React Aria + * VI: Switch accessible sử dụng React Aria + */ +export function Switch({ children, ...props }) { + const state = useToggleState(props); + const ref = useRef(null); + const { inputProps } = useSwitch(props, state, ref); + const { focusProps, isFocusVisible } = useFocusRing(); + + return ( +