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.
This commit is contained in:
219
.agent/skills/react-enterprise-architect/SKILL.md
Normal file
219
.agent/skills/react-enterprise-architect/SKILL.md
Normal file
@@ -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 <MessageList messages={messages} />;
|
||||
}
|
||||
|
||||
// ✅ 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 <input value={message} onChange={(e) => 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 (
|
||||
<div>
|
||||
<StatsCards data={stats} />
|
||||
{/* Client component for interactive chart */}
|
||||
<InteractiveChart initialData={stats} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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 <h1>Title</h1>; }
|
||||
|
||||
// ✅ GOOD: Server Component (default)
|
||||
function Header() { return <h1>Title</h1>; }
|
||||
```
|
||||
|
||||
### 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 <ChatSkeleton />;
|
||||
}
|
||||
|
||||
// app/chat/error.tsx
|
||||
'use client';
|
||||
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
||||
return <ErrorMessage error={error} onRetry={reset} />;
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
306
.agent/skills/react-state-management/SKILL.md
Normal file
306
.agent/skills/react-state-management/SKILL.md
Normal file
@@ -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<AuthState>()(
|
||||
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 <Avatar src={avatar} />;
|
||||
}
|
||||
|
||||
// ❌ BAD: Subscribes to entire store
|
||||
function UserAvatar() {
|
||||
const { user } = useAuthStore(); // Re-renders on any store change
|
||||
return <Avatar src={user?.avatar} />;
|
||||
}
|
||||
```
|
||||
|
||||
### 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 (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{children}
|
||||
<ReactQueryDevtools initialIsOpen={false} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## 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)
|
||||
314
.agent/skills/react-testing-patterns/SKILL.md
Normal file
314
.agent/skills/react-testing-patterns/SKILL.md
Normal file
@@ -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(<LoginForm onSubmit={onSubmit} />);
|
||||
|
||||
// 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(<LoginForm onSubmit={vi.fn()} />);
|
||||
|
||||
// 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(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
{ui}
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
// Usage
|
||||
import { renderWithProviders } from '@/test/utils';
|
||||
|
||||
it('should load data', async () => {
|
||||
renderWithProviders(<UserProfile userId="1" />);
|
||||
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<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
};
|
||||
|
||||
export default meta;
|
||||
|
||||
export const ClickTest: StoryObj<typeof Button> = {
|
||||
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(<AsyncComponent />);
|
||||
expect(screen.getByText('Data')).toBeInTheDocument(); // Fails!
|
||||
|
||||
// ✅ GOOD: Wait for element
|
||||
render(<AsyncComponent />);
|
||||
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/)
|
||||
355
.agent/skills/react-ui-components/SKILL.md
Normal file
355
.agent/skills/react-ui-components/SKILL.md
Normal file
@@ -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<HTMLButtonElement>,
|
||||
VariantProps<typeof buttonVariants> {
|
||||
isLoading?: boolean;
|
||||
}
|
||||
|
||||
export function Button({
|
||||
className,
|
||||
variant,
|
||||
size,
|
||||
isLoading,
|
||||
children,
|
||||
...props
|
||||
}: ButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={clsx(buttonVariants({ variant, size }), className)}
|
||||
disabled={isLoading || props.disabled}
|
||||
{...props}
|
||||
>
|
||||
{isLoading ? <Spinner /> : children}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 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<HTMLInputElement>(null);
|
||||
const { inputProps } = useSwitch(props, state, ref);
|
||||
const { focusProps, isFocusVisible } = useFocusRing();
|
||||
|
||||
return (
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<VisuallyHidden>
|
||||
<input {...inputProps} {...focusProps} ref={ref} />
|
||||
</VisuallyHidden>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-10 h-6 rounded-full transition-colors',
|
||||
state.isSelected ? 'bg-accent-primary' : 'bg-bg-tertiary',
|
||||
isFocusVisible && 'ring-2 ring-border-focus'
|
||||
)}
|
||||
>
|
||||
<div
|
||||
className={clsx(
|
||||
'w-5 h-5 rounded-full bg-white shadow-md transition-transform',
|
||||
state.isSelected && 'translate-x-4'
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
{children}
|
||||
</label>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Radix UI Pattern
|
||||
|
||||
```tsx
|
||||
// components/Dialog/Dialog.tsx
|
||||
import * as DialogPrimitive from '@radix-ui/react-dialog';
|
||||
import { clsx } from 'clsx';
|
||||
|
||||
/**
|
||||
* EN: Dialog using Radix primitives
|
||||
* VI: Dialog sử dụng Radix primitives
|
||||
*/
|
||||
export const Dialog = DialogPrimitive.Root;
|
||||
export const DialogTrigger = DialogPrimitive.Trigger;
|
||||
|
||||
export function DialogContent({ className, children, ...props }) {
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fixed inset-0 bg-black/50 backdrop-blur-sm" />
|
||||
<DialogPrimitive.Content
|
||||
className={clsx(
|
||||
'fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2',
|
||||
'bg-bg-elevated rounded-xl shadow-xl p-6 max-w-md w-full',
|
||||
'focus:outline-none',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
{children}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
|
||||
export const DialogTitle = DialogPrimitive.Title;
|
||||
export const DialogDescription = DialogPrimitive.Description;
|
||||
export const DialogClose = DialogPrimitive.Close;
|
||||
```
|
||||
|
||||
### Framer Motion Animations
|
||||
|
||||
```tsx
|
||||
// components/FadeIn/FadeIn.tsx
|
||||
import { motion } from 'framer-motion';
|
||||
|
||||
/**
|
||||
* EN: Fade-in animation wrapper
|
||||
* VI: Wrapper animation fade-in
|
||||
*/
|
||||
export function FadeIn({ children, delay = 0 }) {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 10 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{
|
||||
duration: 0.3,
|
||||
delay,
|
||||
ease: [0.4, 0, 0.2, 1], // ease-out
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// Stagger animation for lists
|
||||
export function StaggerList({ children }) {
|
||||
return (
|
||||
<motion.ul
|
||||
initial="hidden"
|
||||
animate="visible"
|
||||
variants={{
|
||||
hidden: {},
|
||||
visible: { transition: { staggerChildren: 0.05 } },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.ul>
|
||||
);
|
||||
}
|
||||
|
||||
export function StaggerItem({ children }) {
|
||||
return (
|
||||
<motion.li
|
||||
variants={{
|
||||
hidden: { opacity: 0, x: -10 },
|
||||
visible: { opacity: 1, x: 0 },
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</motion.li>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### Storybook Stories
|
||||
|
||||
```tsx
|
||||
// components/Button/Button.stories.tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Button } from './Button';
|
||||
|
||||
/**
|
||||
* EN: Button component stories
|
||||
* VI: Stories cho Button component
|
||||
*/
|
||||
const meta: Meta<typeof Button> = {
|
||||
title: 'Components/Button',
|
||||
component: Button,
|
||||
tags: ['autodocs'],
|
||||
argTypes: {
|
||||
variant: {
|
||||
control: 'select',
|
||||
options: ['primary', 'secondary', 'ghost', 'destructive'],
|
||||
},
|
||||
size: {
|
||||
control: 'select',
|
||||
options: ['sm', 'md', 'lg'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Button>;
|
||||
|
||||
export const Primary: Story = {
|
||||
args: {
|
||||
children: 'Primary Button',
|
||||
variant: 'primary',
|
||||
},
|
||||
};
|
||||
|
||||
export const Secondary: Story = {
|
||||
args: {
|
||||
children: 'Secondary Button',
|
||||
variant: 'secondary',
|
||||
},
|
||||
};
|
||||
|
||||
export const Loading: Story = {
|
||||
args: {
|
||||
children: 'Loading...',
|
||||
isLoading: true,
|
||||
},
|
||||
};
|
||||
|
||||
// Interactive story with play function
|
||||
export const Interactive: Story = {
|
||||
args: { children: 'Click me' },
|
||||
play: async ({ canvasElement }) => {
|
||||
const canvas = within(canvasElement);
|
||||
const button = canvas.getByRole('button');
|
||||
await userEvent.click(button);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Common Mistakes / Lỗi Thường Gặp
|
||||
|
||||
### 1. Not Using Accessible Primitives
|
||||
|
||||
```tsx
|
||||
// ❌ BAD: Custom toggle without accessibility
|
||||
<div onClick={toggle} className="switch" />
|
||||
|
||||
// ✅ GOOD: Use React Aria or Radix
|
||||
<Switch isSelected={on} onChange={toggle} />
|
||||
```
|
||||
|
||||
### 2. Hardcoding Styles Instead of Variants
|
||||
|
||||
```tsx
|
||||
// ❌ BAD: Conditional className logic scattered
|
||||
<button className={isLarge ? 'h-12 px-6' : 'h-10 px-4'} />
|
||||
|
||||
// ✅ GOOD: Use CVA variants
|
||||
<button className={buttonVariants({ size: isLarge ? 'lg' : 'md' })} />
|
||||
```
|
||||
|
||||
### 3. Missing Focus Indicators
|
||||
|
||||
```tsx
|
||||
// ❌ BAD: Removes focus outline
|
||||
.button:focus { outline: none; }
|
||||
|
||||
// ✅ GOOD: Custom focus ring
|
||||
.button:focus-visible {
|
||||
outline: none;
|
||||
ring: 2px solid var(--border-focus);
|
||||
}
|
||||
```
|
||||
|
||||
## Quick Reference / Tham Chiếu Nhanh
|
||||
|
||||
| Library | Use Case |
|
||||
|---------|----------|
|
||||
| CVA | Variant-based styling |
|
||||
| clsx | Conditional classNames |
|
||||
| React Aria | Low-level accessible hooks |
|
||||
| Radix UI | High-level accessible primitives |
|
||||
| Framer Motion | Animations |
|
||||
| Storybook | Component documentation |
|
||||
|
||||
## Resources / Tài Nguyên
|
||||
|
||||
- [React Accessibility](../react-accessibility/SKILL.md) - A11y patterns
|
||||
- [Tailwind Design System](../tailwind-design-system/SKILL.md) - Styling patterns
|
||||
- [React Testing Patterns](../react-testing-patterns/SKILL.md) - Component testing
|
||||
- [React Aria Docs](https://react-spectrum.adobe.com/react-aria/)
|
||||
- [Radix UI Docs](https://www.radix-ui.com/)
|
||||
- [Storybook Docs](https://storybook.js.org/)
|
||||
308
.agent/skills/tailwind-design-system/SKILL.md
Normal file
308
.agent/skills/tailwind-design-system/SKILL.md
Normal file
@@ -0,0 +1,308 @@
|
||||
---
|
||||
name: tailwind-design-system
|
||||
description: Tailwind CSS 4 design system patterns cho GoodGo frontends. Use for CSS variables theming, dark mode, glassmorphism, brand colors, typography, và responsive patterns.
|
||||
compatibility: "tailwindcss>=4, postcss>=8"
|
||||
metadata:
|
||||
author: Velik Ho
|
||||
version: "1.0"
|
||||
---
|
||||
|
||||
# Tailwind Design System / Hệ Thống Design Tailwind
|
||||
|
||||
## When to Use This Skill / Khi Nào Sử Dụng
|
||||
|
||||
Use this skill when:
|
||||
- Creating design tokens / Tạo design tokens
|
||||
- Implementing dark mode / Triển khai dark mode
|
||||
- Using glassmorphism effects / Sử dụng glassmorphism
|
||||
- Working with brand colors / Làm việc với brand colors
|
||||
- Building responsive layouts / Xây dựng layouts responsive
|
||||
|
||||
## Core Concepts / Khái Niệm Cốt Lõi
|
||||
|
||||
### CSS Variables First (Tailwind CSS 4)
|
||||
|
||||
Tailwind CSS 4 uses CSS-first configuration with `@theme` directive.
|
||||
|
||||
```css
|
||||
/* src/styles/theme.css */
|
||||
@theme {
|
||||
/* Color tokens */
|
||||
--color-bg-primary: #0a0a0a;
|
||||
--color-bg-secondary: #141414;
|
||||
--color-text-primary: #fafafa;
|
||||
--color-accent-primary: #3b82f6;
|
||||
|
||||
/* Spacing */
|
||||
--space-1: 0.25rem;
|
||||
--space-2: 0.5rem;
|
||||
--space-4: 1rem;
|
||||
|
||||
/* Border radius */
|
||||
--radius-md: 0.5rem;
|
||||
--radius-lg: 0.75rem;
|
||||
}
|
||||
```
|
||||
|
||||
## Key Patterns / Mẫu Chính
|
||||
|
||||
### Color Tokens
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
bg: {
|
||||
primary: 'var(--bg-primary)',
|
||||
secondary: 'var(--bg-secondary)',
|
||||
tertiary: 'var(--bg-tertiary)',
|
||||
elevated: 'var(--bg-elevated)',
|
||||
},
|
||||
text: {
|
||||
primary: 'var(--text-primary)',
|
||||
secondary: 'var(--text-secondary)',
|
||||
tertiary: 'var(--text-tertiary)',
|
||||
},
|
||||
accent: {
|
||||
primary: 'var(--accent-primary)',
|
||||
success: 'var(--accent-success)',
|
||||
warning: 'var(--accent-warning)',
|
||||
error: 'var(--accent-error)',
|
||||
},
|
||||
brand: {
|
||||
primary: {
|
||||
DEFAULT: 'var(--brand-primary)',
|
||||
light: 'var(--brand-primary-light)',
|
||||
dark: 'var(--brand-primary-dark)',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### Dark Mode Pattern
|
||||
|
||||
```css
|
||||
/* theme.css */
|
||||
:root {
|
||||
--bg-primary: #ffffff;
|
||||
--text-primary: #1a1a1a;
|
||||
}
|
||||
|
||||
[data-theme="dark"] {
|
||||
--bg-primary: #0a0a0a;
|
||||
--text-primary: #fafafa;
|
||||
}
|
||||
```
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
darkMode: ['class', '[data-theme="dark"]'],
|
||||
// ...
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Usage in component
|
||||
<div className="bg-bg-primary text-text-primary">
|
||||
{/* Automatically adapts to theme */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Glassmorphism Utilities
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
glass: {
|
||||
bg: 'var(--glass-bg-default)',
|
||||
'bg-subtle': 'var(--glass-bg-subtle)',
|
||||
'bg-hover': 'var(--glass-bg-hover)',
|
||||
border: 'var(--glass-border-default)',
|
||||
},
|
||||
},
|
||||
backdropBlur: {
|
||||
glass: 'var(--glass-blur-md)',
|
||||
'glass-lg': 'var(--glass-blur-lg)',
|
||||
},
|
||||
boxShadow: {
|
||||
glass: 'var(--glass-shadow-md)',
|
||||
'glass-lg': 'var(--glass-shadow-lg)',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [
|
||||
function({ addUtilities }) {
|
||||
addUtilities({
|
||||
'.glass-panel': {
|
||||
background: 'var(--glass-bg-default)',
|
||||
'backdrop-filter': 'blur(var(--glass-blur-md))',
|
||||
border: '1px solid var(--glass-border-default)',
|
||||
'box-shadow': 'var(--glass-shadow-md)',
|
||||
},
|
||||
});
|
||||
},
|
||||
],
|
||||
};
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Usage
|
||||
<div className="glass-panel rounded-lg p-4">
|
||||
Glassmorphism effect
|
||||
</div>
|
||||
|
||||
// Or custom glass
|
||||
<div className="bg-glass-bg backdrop-blur-glass border border-glass-border shadow-glass rounded-xl">
|
||||
Custom glass
|
||||
</div>
|
||||
```
|
||||
|
||||
### Typography Scale
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
fontSize: {
|
||||
'xs': ['var(--text-xs)', { lineHeight: '1.5' }],
|
||||
'sm': ['var(--text-sm)', { lineHeight: '1.5' }],
|
||||
'base': ['var(--text-base)', { lineHeight: '1.5' }],
|
||||
'lg': ['var(--text-lg)', { lineHeight: '1.5' }],
|
||||
'xl': ['var(--text-xl)', { lineHeight: '1.4' }],
|
||||
'2xl': ['var(--text-2xl)', { lineHeight: '1.3' }],
|
||||
'3xl': ['var(--text-3xl)', { lineHeight: '1.2' }],
|
||||
},
|
||||
fontWeight: {
|
||||
normal: 'var(--font-normal)',
|
||||
medium: 'var(--font-medium)',
|
||||
semibold: 'var(--font-semibold)',
|
||||
bold: 'var(--font-bold)',
|
||||
},
|
||||
```
|
||||
|
||||
### Spacing Scale
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
spacing: {
|
||||
'0': 'var(--space-0)',
|
||||
'1': 'var(--space-1)',
|
||||
'2': 'var(--space-2)',
|
||||
'3': 'var(--space-3)',
|
||||
'4': 'var(--space-4)',
|
||||
'6': 'var(--space-6)',
|
||||
'8': 'var(--space-8)',
|
||||
'12': 'var(--space-12)',
|
||||
'16': 'var(--space-16)',
|
||||
'sidebar': 'var(--sidebar-width)',
|
||||
},
|
||||
```
|
||||
|
||||
### Animation Tokens
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
transitionTimingFunction: {
|
||||
smooth: 'var(--motion-ease-smooth)',
|
||||
glide: 'var(--motion-ease-glide)',
|
||||
},
|
||||
transitionDuration: {
|
||||
instant: 'var(--motion-duration-instant)',
|
||||
quick: 'var(--motion-duration-quick)',
|
||||
normal: 'var(--motion-duration-normal)',
|
||||
},
|
||||
```
|
||||
|
||||
### Responsive Patterns
|
||||
|
||||
```tsx
|
||||
// Mobile-first responsive
|
||||
<div className="
|
||||
p-4 md:p-6 lg:p-8
|
||||
text-base md:text-lg
|
||||
grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3
|
||||
gap-4 md:gap-6
|
||||
">
|
||||
{/* Content */}
|
||||
</div>
|
||||
|
||||
// Container pattern
|
||||
<div className="max-w-container-lg mx-auto px-4 md:px-6">
|
||||
{/* Centered content */}
|
||||
</div>
|
||||
```
|
||||
|
||||
### Brand Gradients
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
backgroundImage: {
|
||||
'brand-gradient': 'var(--brand-gradient-primary)',
|
||||
'brand-gradient-accent': 'var(--brand-gradient-accent)',
|
||||
},
|
||||
```
|
||||
|
||||
```tsx
|
||||
// Usage
|
||||
<button className="bg-brand-gradient text-white">
|
||||
Gradient Button
|
||||
</button>
|
||||
```
|
||||
|
||||
## Common Mistakes / Lỗi Thường Gặp
|
||||
|
||||
### 1. Hardcoding Colors
|
||||
|
||||
```tsx
|
||||
// ❌ BAD: Hardcoded color
|
||||
<div className="bg-[#1a1a1a] text-[#fafafa]">
|
||||
|
||||
// ✅ GOOD: Use design tokens
|
||||
<div className="bg-bg-primary text-text-primary">
|
||||
```
|
||||
|
||||
### 2. Not Using CSS Variables
|
||||
|
||||
```css
|
||||
/* ❌ BAD: Static values */
|
||||
.button { background: #3b82f6; }
|
||||
|
||||
/* ✅ GOOD: CSS variables for theming */
|
||||
.button { background: var(--accent-primary); }
|
||||
```
|
||||
|
||||
### 3. Inconsistent Spacing
|
||||
|
||||
```tsx
|
||||
// ❌ BAD: Arbitrary values
|
||||
<div className="p-[13px] m-[7px]">
|
||||
|
||||
// ✅ GOOD: Use spacing scale
|
||||
<div className="p-3 m-2">
|
||||
```
|
||||
|
||||
## Quick Reference / Tham Chiếu Nhanh
|
||||
|
||||
| Token | Usage |
|
||||
|-------|-------|
|
||||
| `bg-bg-*` | Background colors |
|
||||
| `text-text-*` | Text colors |
|
||||
| `bg-accent-*` | Accent backgrounds |
|
||||
| `border-border-*` | Border colors |
|
||||
| `glass-panel` | Glassmorphism preset |
|
||||
| `bg-brand-gradient` | Brand gradient |
|
||||
| `backdrop-blur-glass` | Glass blur |
|
||||
| `shadow-glass` | Glass shadow |
|
||||
|
||||
## Resources / Tài Nguyên
|
||||
|
||||
- [React UI Components](../react-ui-components/SKILL.md) - Component patterns
|
||||
- [Tailwind CSS Docs](https://tailwindcss.com/docs)
|
||||
- [Design Tokens Spec](https://design-tokens.github.io/community-group/format/)
|
||||
86
.agent/workflows/frontend-dev.md
Normal file
86
.agent/workflows/frontend-dev.md
Normal file
@@ -0,0 +1,86 @@
|
||||
---
|
||||
description: Development workflow cho frontend apps (web-client, web-admin, etc.) - chạy dev server, lint, typecheck, build
|
||||
---
|
||||
|
||||
# Frontend Development Workflow
|
||||
|
||||
Workflow phát triển cho frontend React/Next.js apps trong GoodGo platform.
|
||||
|
||||
## Prerequisites / Yêu Cầu
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm (workspace manager)
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Navigate to Frontend App
|
||||
|
||||
```bash
|
||||
cd apps/web-client # hoặc web-admin, web-merchant
|
||||
```
|
||||
|
||||
### 2. Install Dependencies
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 3. Run Development Server
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
Server sẽ chạy tại `http://localhost:3000`
|
||||
|
||||
### 4. Type Checking
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm typecheck
|
||||
```
|
||||
|
||||
### 5. Linting
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm lint
|
||||
```
|
||||
|
||||
### 6. Build Production
|
||||
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
|
||||
### 7. Run Unit Tests
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm vitest run
|
||||
```
|
||||
|
||||
### 8. Run E2E Tests
|
||||
|
||||
```bash
|
||||
pnpm playwright test
|
||||
```
|
||||
|
||||
## Quick Commands / Lệnh Nhanh
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `pnpm dev` | Start dev server |
|
||||
| `pnpm build` | Production build |
|
||||
| `pnpm lint` | ESLint check |
|
||||
| `pnpm typecheck` | TypeScript check |
|
||||
| `pnpm vitest` | Run unit tests (watch) |
|
||||
| `pnpm vitest run` | Run unit tests (once) |
|
||||
| `pnpm playwright test` | Run E2E tests |
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [React Enterprise Architect](/.agent/skills/react-enterprise-architect/SKILL.md)
|
||||
- [React Testing Patterns](/.agent/skills/react-testing-patterns/SKILL.md)
|
||||
106
.agent/workflows/storybook-ops.md
Normal file
106
.agent/workflows/storybook-ops.md
Normal file
@@ -0,0 +1,106 @@
|
||||
---
|
||||
description: Storybook operations - chạy Storybook dev server, build, và testing
|
||||
---
|
||||
|
||||
# Storybook Operations Workflow
|
||||
|
||||
Workflow cho Storybook development và testing trong GoodGo frontend apps.
|
||||
|
||||
## Prerequisites / Yêu Cầu
|
||||
|
||||
- Node.js 20+
|
||||
- pnpm
|
||||
- Frontend app dependencies installed
|
||||
|
||||
## Steps
|
||||
|
||||
### 1. Navigate to Frontend App
|
||||
|
||||
```bash
|
||||
cd apps/web-client # hoặc web-admin, web-merchant
|
||||
```
|
||||
|
||||
### 2. Start Storybook Dev Server
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm storybook
|
||||
```
|
||||
|
||||
Storybook sẽ chạy tại `http://localhost:6006`
|
||||
|
||||
### 3. Build Storybook (Static)
|
||||
|
||||
```bash
|
||||
pnpm build-storybook
|
||||
```
|
||||
|
||||
Output sẽ nằm trong `storybook-static/`
|
||||
|
||||
### 4. Run Storybook Tests
|
||||
|
||||
// turbo
|
||||
```bash
|
||||
pnpm test-storybook
|
||||
```
|
||||
|
||||
### 5. A11y Testing (Accessibility)
|
||||
|
||||
Storybook đã cài đặt `@storybook/addon-a11y`. Để kiểm tra:
|
||||
|
||||
1. Mở Storybook UI
|
||||
2. Chọn component story
|
||||
3. Click tab "Accessibility" ở panel bên dưới
|
||||
4. Xem violations và suggestions
|
||||
|
||||
### 6. Visual Regression (Optional)
|
||||
|
||||
Nếu có Chromatic:
|
||||
|
||||
```bash
|
||||
npx chromatic --project-token=<token>
|
||||
```
|
||||
|
||||
## Quick Commands / Lệnh Nhanh
|
||||
|
||||
| Command | Description |
|
||||
|---------|-------------|
|
||||
| `pnpm storybook` | Start Storybook dev |
|
||||
| `pnpm build-storybook` | Build static Storybook |
|
||||
| `pnpm test-storybook` | Run interaction tests |
|
||||
|
||||
## Story File Structure
|
||||
|
||||
```
|
||||
src/features/shared/components/Button/
|
||||
├── Button.tsx
|
||||
├── Button.stories.tsx # ← Storybook stories
|
||||
└── Button.test.tsx
|
||||
```
|
||||
|
||||
## Story Template
|
||||
|
||||
```tsx
|
||||
import type { Meta, StoryObj } from '@storybook/react';
|
||||
import { Component } from './Component';
|
||||
|
||||
const meta: Meta<typeof Component> = {
|
||||
title: 'Components/Component',
|
||||
component: Component,
|
||||
tags: ['autodocs'],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof Component>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
// default props
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
## Related Skills
|
||||
|
||||
- [React UI Components](/.agent/skills/react-ui-components/SKILL.md)
|
||||
- [React Testing Patterns](/.agent/skills/react-testing-patterns/SKILL.md)
|
||||
Reference in New Issue
Block a user