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:
Ho Ngoc Hai
2026-01-18 23:57:52 +07:00
parent 5aa48eb29c
commit f47556ef6e
7 changed files with 1694 additions and 0 deletions

View 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)

View 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)

View 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/)

View 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/)

View 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/)

View 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)

View 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)