@react-spa-scaffold/mcp 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +423 -0
- package/dist/features/index.d.ts +5 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +3 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/registry.d.ts +10 -0
- package/dist/features/registry.d.ts.map +1 -0
- package/dist/features/registry.js +508 -0
- package/dist/features/registry.js.map +1 -0
- package/dist/features/types.d.ts +45 -0
- package/dist/features/types.d.ts.map +1 -0
- package/dist/features/types.js +5 -0
- package/dist/features/types.js.map +1 -0
- package/dist/features/versions.d.ts +16 -0
- package/dist/features/versions.d.ts.map +1 -0
- package/dist/features/versions.js +46 -0
- package/dist/features/versions.js.map +1 -0
- package/dist/features/versions.json +5 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/resources/docs.d.ts +29 -0
- package/dist/resources/docs.d.ts.map +1 -0
- package/dist/resources/docs.js +105 -0
- package/dist/resources/docs.js.map +1 -0
- package/dist/resources/index.d.ts +2 -0
- package/dist/resources/index.d.ts.map +1 -0
- package/dist/resources/index.js +2 -0
- package/dist/resources/index.js.map +1 -0
- package/dist/server.d.ts +12 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +115 -0
- package/dist/server.js.map +1 -0
- package/dist/tools/get-example.d.ts +51 -0
- package/dist/tools/get-example.d.ts.map +1 -0
- package/dist/tools/get-example.js +90 -0
- package/dist/tools/get-example.js.map +1 -0
- package/dist/tools/get-features.d.ts +30 -0
- package/dist/tools/get-features.d.ts.map +1 -0
- package/dist/tools/get-features.js +46 -0
- package/dist/tools/get-features.js.map +1 -0
- package/dist/tools/get-scaffold.d.ts +77 -0
- package/dist/tools/get-scaffold.d.ts.map +1 -0
- package/dist/tools/get-scaffold.js +153 -0
- package/dist/tools/get-scaffold.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/utils/docs.d.ts +14 -0
- package/dist/utils/docs.d.ts.map +1 -0
- package/dist/utils/docs.js +64 -0
- package/dist/utils/docs.js.map +1 -0
- package/dist/utils/examples.d.ts +27 -0
- package/dist/utils/examples.d.ts.map +1 -0
- package/dist/utils/examples.js +399 -0
- package/dist/utils/examples.js.map +1 -0
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +5 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/paths.d.ts +28 -0
- package/dist/utils/paths.d.ts.map +1 -0
- package/dist/utils/paths.js +40 -0
- package/dist/utils/paths.js.map +1 -0
- package/dist/utils/scaffold.d.ts +50 -0
- package/dist/utils/scaffold.d.ts.map +1 -0
- package/dist/utils/scaffold.js +500 -0
- package/dist/utils/scaffold.js.map +1 -0
- package/dist/version.d.ts +5 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +19 -0
- package/dist/version.js.map +1 -0
- package/package.json +63 -0
- package/templates/.bundled +0 -0
- package/templates/CLAUDE.md +145 -0
- package/templates/docs/API_REFERENCE.md +58 -0
- package/templates/docs/ARCHITECTURE.md +185 -0
- package/templates/docs/CODING_STANDARDS.md +53 -0
- package/templates/docs/COMPONENT_GUIDELINES.md +301 -0
- package/templates/docs/E2E_TESTING.md +116 -0
- package/templates/docs/INTERNATIONALIZATION.md +67 -0
- package/templates/docs/TESTING.md +259 -0
- package/templates/docs/WORKFLOW.md +170 -0
- package/templates/src/App.tsx +42 -0
- package/templates/src/components/layout/Header.tsx +19 -0
- package/templates/src/components/layout/index.ts +1 -0
- package/templates/src/components/shared/ErrorBoundary/ErrorBoundary.tsx +104 -0
- package/templates/src/components/shared/ErrorBoundary/index.ts +1 -0
- package/templates/src/components/shared/LanguageSwitcher/LanguageSwitcher.tsx +45 -0
- package/templates/src/components/shared/LanguageSwitcher/index.ts +1 -0
- package/templates/src/components/shared/SEO/SEO.tsx +55 -0
- package/templates/src/components/shared/SEO/index.ts +1 -0
- package/templates/src/components/shared/ThemeToggle/ThemeToggle.tsx +41 -0
- package/templates/src/components/shared/ThemeToggle/index.ts +1 -0
- package/templates/src/components/shared/index.ts +4 -0
- package/templates/src/components/ui/button.tsx +48 -0
- package/templates/src/components/ui/dropdown-menu.tsx +228 -0
- package/templates/src/components/ui/form-error.tsx +95 -0
- package/templates/src/components/ui/loading.tsx +58 -0
- package/templates/src/components/ui/skeleton.tsx +52 -0
- package/templates/src/components/ui/sonner.tsx +34 -0
- package/templates/src/components/ui/spinner.tsx +40 -0
- package/templates/src/components/ui/visually-hidden.tsx +51 -0
- package/templates/src/contexts/mobileContext.tsx +66 -0
- package/templates/src/contexts/queryContext.tsx +28 -0
- package/templates/src/hooks/index.ts +7 -0
- package/templates/src/hooks/useContactForm.ts +33 -0
- package/templates/src/hooks/useExampleQuery.ts +20 -0
- package/templates/src/hooks/useLanguage.ts +23 -0
- package/templates/src/hooks/useMediaQuery.ts +53 -0
- package/templates/src/hooks/useThemeEffect.ts +31 -0
- package/templates/src/hooks/useTouchSizes.ts +16 -0
- package/templates/src/i18n/config.ts +11 -0
- package/templates/src/i18n/detectLanguage.ts +57 -0
- package/templates/src/i18n/index.ts +20 -0
- package/templates/src/i18n/loadCatalog.ts +30 -0
- package/templates/src/index.css +98 -0
- package/templates/src/lib/api.ts +142 -0
- package/templates/src/lib/config.ts +15 -0
- package/templates/src/lib/constants.ts +8 -0
- package/templates/src/lib/env.ts +53 -0
- package/templates/src/lib/format.ts +119 -0
- package/templates/src/lib/index.ts +24 -0
- package/templates/src/lib/routes.ts +11 -0
- package/templates/src/lib/storage.ts +91 -0
- package/templates/src/lib/storageKeys.ts +10 -0
- package/templates/src/lib/utils.ts +6 -0
- package/templates/src/lib/validations.ts +39 -0
- package/templates/src/locales/de.po +65 -0
- package/templates/src/locales/en.po +65 -0
- package/templates/src/locales/es.po +65 -0
- package/templates/src/main.tsx +107 -0
- package/templates/src/mocks/fixtures/index.ts +1 -0
- package/templates/src/mocks/fixtures/todos.ts +40 -0
- package/templates/src/mocks/handlers/index.ts +7 -0
- package/templates/src/mocks/handlers/todos.ts +59 -0
- package/templates/src/mocks/index.ts +3 -0
- package/templates/src/mocks/node.ts +9 -0
- package/templates/src/pages/Home.tsx +27 -0
- package/templates/src/pages/NotFound.tsx +28 -0
- package/templates/src/pages/index.ts +2 -0
- package/templates/src/stores/index.ts +2 -0
- package/templates/src/stores/preferencesStore.ts +85 -0
- package/templates/src/test/index.ts +8 -0
- package/templates/src/test/mocks.ts +17 -0
- package/templates/src/test/providers.tsx +54 -0
- package/templates/src/test-setup.ts +54 -0
- package/templates/src/types/api.ts +31 -0
- package/templates/src/types/index.ts +2 -0
- package/templates/src/types/preferences.ts +5 -0
- package/templates/src/vite-env.d.ts +10 -0
- package/templates/tests/unit/components/ErrorBoundary.test.tsx +193 -0
- package/templates/tests/unit/components/Header.test.tsx +33 -0
- package/templates/tests/unit/components/LanguageSwitcher.test.tsx +40 -0
- package/templates/tests/unit/components/Loading.test.tsx +76 -0
- package/templates/tests/unit/components/SEO.test.tsx +80 -0
- package/templates/tests/unit/components/ThemeToggle.test.tsx +62 -0
- package/templates/tests/unit/contexts/mobileContext.test.tsx +54 -0
- package/templates/tests/unit/hooks/useContactForm.test.ts +60 -0
- package/templates/tests/unit/hooks/useExampleQuery.test.tsx +94 -0
- package/templates/tests/unit/hooks/useLanguage.test.tsx +75 -0
- package/templates/tests/unit/hooks/useMediaQuery.test.ts +57 -0
- package/templates/tests/unit/hooks/useThemeEffect.test.ts +42 -0
- package/templates/tests/unit/i18n/detectLanguage.test.ts +40 -0
- package/templates/tests/unit/i18n/loadCatalog.test.ts +70 -0
- package/templates/tests/unit/lib/api.test.ts +142 -0
- package/templates/tests/unit/lib/format.test.ts +100 -0
- package/templates/tests/unit/lib/storage.test.ts +90 -0
- package/templates/tests/unit/lib/utils.test.ts +19 -0
- package/templates/tests/unit/lib/validations.test.ts +56 -0
- package/templates/tests/unit/stores/preferencesStore.test.ts +75 -0
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
# Testing Guidelines
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
- **Framework**: Vitest + React Testing Library
|
|
6
|
+
- **Coverage threshold**: 80% (lines, branches, functions, statements)
|
|
7
|
+
- **Test location**: `tests/unit/` mirroring `src/` structure
|
|
8
|
+
|
|
9
|
+
## File Structure
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
tests/unit/
|
|
13
|
+
├── components/ # Component tests
|
|
14
|
+
├── hooks/ # Hook tests
|
|
15
|
+
├── lib/ # Utility function tests
|
|
16
|
+
├── stores/ # Zustand store tests
|
|
17
|
+
├── contexts/ # Context provider tests
|
|
18
|
+
└── i18n/ # Internationalization tests
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Naming Conventions
|
|
22
|
+
|
|
23
|
+
```typescript
|
|
24
|
+
// File: [name].test.ts or [name].test.tsx (for JSX)
|
|
25
|
+
// Describe blocks: match the module/function name
|
|
26
|
+
// Test names: describe behavior, not implementation
|
|
27
|
+
|
|
28
|
+
describe('formatDate', () => {
|
|
29
|
+
it('returns "Invalid date" for invalid input', () => {});
|
|
30
|
+
it('formats date with custom options', () => {});
|
|
31
|
+
});
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Imports
|
|
35
|
+
|
|
36
|
+
```typescript
|
|
37
|
+
// Vitest - test framework
|
|
38
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
39
|
+
|
|
40
|
+
// Testing Library - DOM utilities
|
|
41
|
+
import { screen, renderHook, act, waitFor, fireEvent } from '@testing-library/react';
|
|
42
|
+
import userEvent from '@testing-library/user-event';
|
|
43
|
+
|
|
44
|
+
// Custom utilities from @/test
|
|
45
|
+
import { render, mockMatchMedia, createTestQueryClient, server } from '@/test';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Core Patterns
|
|
49
|
+
|
|
50
|
+
### Use `it.each` for Similar Tests
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
// ✅ Good - parameterized tests
|
|
54
|
+
it.each([
|
|
55
|
+
{ input: 0, expected: '0 Bytes' },
|
|
56
|
+
{ input: 1024, expected: '1 KB' },
|
|
57
|
+
{ input: 1024 * 1024, expected: '1 MB' },
|
|
58
|
+
])('formats $input bytes', ({ input, expected }) => {
|
|
59
|
+
expect(formatBytes(input)).toBe(expected);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ❌ Avoid - repetitive individual tests
|
|
63
|
+
it('formats 0 bytes', () => expect(formatBytes(0)).toBe('0 Bytes'));
|
|
64
|
+
it('formats 1024 bytes', () => expect(formatBytes(1024)).toBe('1 KB'));
|
|
65
|
+
it('formats 1MB', () => expect(formatBytes(1024 * 1024)).toBe('1 MB'));
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Use Shared Mocks
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { mockMatchMedia } from '@/test';
|
|
72
|
+
|
|
73
|
+
describe('useMediaQuery', () => {
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
window.matchMedia = mockMatchMedia(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('detects desktop viewport', () => {
|
|
79
|
+
window.matchMedia = mockMatchMedia(true); // matches min-width query
|
|
80
|
+
const { result } = renderHook(() => useIsDesktop());
|
|
81
|
+
expect(result.current).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Component Testing
|
|
87
|
+
|
|
88
|
+
```typescript
|
|
89
|
+
import { screen } from '@testing-library/react';
|
|
90
|
+
import { render } from '@/test';
|
|
91
|
+
|
|
92
|
+
describe('Header', () => {
|
|
93
|
+
it('renders navigation links', () => {
|
|
94
|
+
render(<Header />);
|
|
95
|
+
expect(screen.getByRole('navigation')).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Hook Testing
|
|
101
|
+
|
|
102
|
+
```typescript
|
|
103
|
+
import { renderHook, act } from '@testing-library/react';
|
|
104
|
+
|
|
105
|
+
describe('useCounter', () => {
|
|
106
|
+
it('increments value', () => {
|
|
107
|
+
const { result } = renderHook(() => useCounter());
|
|
108
|
+
|
|
109
|
+
act(() => result.current.increment());
|
|
110
|
+
|
|
111
|
+
expect(result.current.count).toBe(1);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Async Testing
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
// For promises
|
|
120
|
+
it('fetches data', async () => {
|
|
121
|
+
const result = await fetchData();
|
|
122
|
+
expect(result).toBeDefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// For state updates
|
|
126
|
+
it('updates after async action', async () => {
|
|
127
|
+
const { result } = renderHook(() => useAsync());
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(result.current.data).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Mocking
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
import { mockMatchMedia } from '@/test';
|
|
139
|
+
|
|
140
|
+
// Mock modules at top of file
|
|
141
|
+
vi.mock('@/lib/storage', () => ({
|
|
142
|
+
setStorageItem: vi.fn(() => true),
|
|
143
|
+
}));
|
|
144
|
+
|
|
145
|
+
// Mock browser APIs using shared utilities
|
|
146
|
+
beforeEach(() => {
|
|
147
|
+
window.matchMedia = mockMatchMedia(false);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
afterEach(() => {
|
|
151
|
+
vi.restoreAllMocks();
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// Mock fetch for API tests
|
|
155
|
+
const mockFetch = vi.fn();
|
|
156
|
+
beforeEach(() => {
|
|
157
|
+
global.fetch = mockFetch;
|
|
158
|
+
});
|
|
159
|
+
mockFetch.mockResolvedValueOnce({ ok: true, json: () => Promise.resolve({}) });
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### MSW (Mock Service Worker)
|
|
163
|
+
|
|
164
|
+
MSW handlers are organized in `src/mocks/`:
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
src/mocks/
|
|
168
|
+
├── handlers/
|
|
169
|
+
│ ├── index.ts # Combines all handlers
|
|
170
|
+
│ └── todos.ts # Example domain handlers
|
|
171
|
+
├── fixtures/
|
|
172
|
+
│ └── todos.ts # Response data
|
|
173
|
+
├── browser.ts # Browser worker setup
|
|
174
|
+
└── node.ts # Node server for tests
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Override handlers per-test:
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { http, HttpResponse, server } from '@/test';
|
|
181
|
+
|
|
182
|
+
it('handles API error', async () => {
|
|
183
|
+
server.use(http.get('/api/todos', () => new HttpResponse(null, { status: 500 })));
|
|
184
|
+
// Test error handling...
|
|
185
|
+
});
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
MSW handlers auto-reset after each test via `src/test-setup.ts`.
|
|
189
|
+
|
|
190
|
+
### Store Testing (Zustand)
|
|
191
|
+
|
|
192
|
+
```typescript
|
|
193
|
+
import { act } from '@testing-library/react';
|
|
194
|
+
|
|
195
|
+
describe('preferencesStore', () => {
|
|
196
|
+
beforeEach(() => {
|
|
197
|
+
usePreferencesStore.setState({ theme: 'light' });
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it.each(['light', 'dark', 'system'] as const)('sets theme to %s', (theme) => {
|
|
201
|
+
act(() => usePreferencesStore.getState().setTheme(theme));
|
|
202
|
+
expect(usePreferencesStore.getState().theme).toBe(theme);
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Test Organization
|
|
208
|
+
|
|
209
|
+
### Structure Within Test Files
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
describe('ModuleName', () => {
|
|
213
|
+
// Setup/teardown at top
|
|
214
|
+
beforeEach(() => {});
|
|
215
|
+
afterEach(() => {});
|
|
216
|
+
|
|
217
|
+
// Group related tests
|
|
218
|
+
describe('methodName', () => {
|
|
219
|
+
it('handles normal case', () => {});
|
|
220
|
+
it('handles edge case', () => {});
|
|
221
|
+
it('handles error case', () => {});
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
### What to Test
|
|
227
|
+
|
|
228
|
+
| Type | Test Focus |
|
|
229
|
+
| ---------- | --------------------------------------------- |
|
|
230
|
+
| Components | Rendering, user interactions, accessibility |
|
|
231
|
+
| Hooks | Return values, state changes, side effects |
|
|
232
|
+
| Utils | Input/output, edge cases, error handling |
|
|
233
|
+
| Stores | State mutations, computed values, persistence |
|
|
234
|
+
|
|
235
|
+
### What NOT to Test
|
|
236
|
+
|
|
237
|
+
- Implementation details (internal state, private methods)
|
|
238
|
+
- Third-party library internals
|
|
239
|
+
- Static content without logic
|
|
240
|
+
- TypeScript types (compiler handles this)
|
|
241
|
+
|
|
242
|
+
## Running Tests
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
npm test # Run all tests
|
|
246
|
+
npm run test:watch # Watch mode
|
|
247
|
+
npm run test:coverage # With coverage report
|
|
248
|
+
npm run test:ui # Visual UI
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
## Checklist for New Tests
|
|
252
|
+
|
|
253
|
+
- [ ] File follows `[name].test.ts(x)` naming
|
|
254
|
+
- [ ] Uses `it.each` for parameterized cases
|
|
255
|
+
- [ ] Mocks are cleared in `beforeEach`/`afterEach`
|
|
256
|
+
- [ ] No duplicate helper functions
|
|
257
|
+
- [ ] Tests behavior, not implementation
|
|
258
|
+
- [ ] Async tests use `await`/`waitFor` properly
|
|
259
|
+
- [ ] Coverage threshold maintained (80%)
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Workflow Commands
|
|
2
|
+
|
|
3
|
+
Custom commands to support TDD feature development.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
### /implement
|
|
8
|
+
|
|
9
|
+
**Purpose**: Setup for TDD feature implementation.
|
|
10
|
+
|
|
11
|
+
**Usage**:
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/implement <feature description>
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
**What it does**:
|
|
18
|
+
|
|
19
|
+
1. Creates a task branch
|
|
20
|
+
2. Provides instructions to run `/feature-dev` with TDD context
|
|
21
|
+
3. Reminds you to run `/check` before committing
|
|
22
|
+
|
|
23
|
+
**Note**: This is a setup command. You'll manually run `/feature-dev` with the TDD instructions provided.
|
|
24
|
+
|
|
25
|
+
### /check
|
|
26
|
+
|
|
27
|
+
**Purpose**: Pre-commit quality validation.
|
|
28
|
+
|
|
29
|
+
**Usage**:
|
|
30
|
+
|
|
31
|
+
```
|
|
32
|
+
/check
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**Runs**:
|
|
36
|
+
|
|
37
|
+
1. `npm run test` - All tests must pass
|
|
38
|
+
2. `npm run typecheck` - No TypeScript errors
|
|
39
|
+
3. `npm run lint` - No lint errors
|
|
40
|
+
4. `npm run build` - Build must succeed
|
|
41
|
+
|
|
42
|
+
### /feature-dev
|
|
43
|
+
|
|
44
|
+
**Purpose**: Full guided feature development (plugin command).
|
|
45
|
+
|
|
46
|
+
**Usage**:
|
|
47
|
+
|
|
48
|
+
```
|
|
49
|
+
/feature-dev:feature-dev <feature description>
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**7 Phases**:
|
|
53
|
+
|
|
54
|
+
1. Discovery - Understand requirements
|
|
55
|
+
2. Codebase Exploration - Find patterns
|
|
56
|
+
3. Clarifying Questions - Resolve ambiguities
|
|
57
|
+
4. Architecture Design - Plan approach
|
|
58
|
+
5. Implementation - Build the feature
|
|
59
|
+
6. Quality Review - Check code quality
|
|
60
|
+
7. Summary - Document what was built
|
|
61
|
+
|
|
62
|
+
## TDD Workflow
|
|
63
|
+
|
|
64
|
+
The `tdd-workflow` skill (`.claude/skills/tdd-workflow/SKILL.md`) provides TDD guidance.
|
|
65
|
+
|
|
66
|
+
### Red-Green-Refactor
|
|
67
|
+
|
|
68
|
+
1. **RED**: Write failing test first
|
|
69
|
+
2. **GREEN**: Minimal code to pass
|
|
70
|
+
3. **REFACTOR**: Clean up (tests stay green)
|
|
71
|
+
|
|
72
|
+
### Test Location
|
|
73
|
+
|
|
74
|
+
Tests go in `tests/unit/` mirroring `src/`:
|
|
75
|
+
|
|
76
|
+
| Source | Test |
|
|
77
|
+
| --------------------------- | --------------------------------------- |
|
|
78
|
+
| `src/hooks/useAuth.ts` | `tests/unit/hooks/useAuth.test.ts` |
|
|
79
|
+
| `src/components/Button.tsx` | `tests/unit/components/Button.test.tsx` |
|
|
80
|
+
|
|
81
|
+
### Test Patterns
|
|
82
|
+
|
|
83
|
+
See [docs/TESTING.md](TESTING.md) for detailed patterns.
|
|
84
|
+
|
|
85
|
+
## Typical Workflow
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
1. /implement Add user authentication
|
|
89
|
+
→ Creates branch, shows TDD instructions
|
|
90
|
+
|
|
91
|
+
2. /feature-dev:feature-dev Add user authentication
|
|
92
|
+
→ Follow the 7 phases
|
|
93
|
+
→ During Phase 5, apply TDD (tests first)
|
|
94
|
+
|
|
95
|
+
3. /check
|
|
96
|
+
→ Verify all quality gates pass
|
|
97
|
+
|
|
98
|
+
4. Commit
|
|
99
|
+
→ Create conventional commit
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## File Structure
|
|
103
|
+
|
|
104
|
+
```
|
|
105
|
+
.claude/
|
|
106
|
+
├── commands/
|
|
107
|
+
│ ├── implement/implement.md # /implement
|
|
108
|
+
│ └── quality/check.md # /check
|
|
109
|
+
├── skills/
|
|
110
|
+
│ └── tdd-workflow/SKILL.md # TDD guidance
|
|
111
|
+
└── settings.local.json # Permissions
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## MCP Server Integration
|
|
115
|
+
|
|
116
|
+
Use MCP servers instead of WebSearch for documentation. They provide **structured, version-accurate data** directly from source.
|
|
117
|
+
|
|
118
|
+
### Why MCP over WebSearch?
|
|
119
|
+
|
|
120
|
+
| Benefit | Explanation |
|
|
121
|
+
| ----------------- | ------------------------------------------------------ |
|
|
122
|
+
| **Accuracy** | Fetches from official sources, not outdated blog posts |
|
|
123
|
+
| **Version-aware** | Gets docs for the exact library version in use |
|
|
124
|
+
| **Structured** | Returns code snippets, types, examples consistently |
|
|
125
|
+
| **Faster** | Direct API calls vs parsing HTML from search results |
|
|
126
|
+
|
|
127
|
+
### Shadcn MCP (UI Components)
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
mcp__shadcn__search_items_in_registries # Find components
|
|
131
|
+
mcp__shadcn__view_items_in_registries # View component code
|
|
132
|
+
mcp__shadcn__get_item_examples_from_registries # Usage examples
|
|
133
|
+
mcp__shadcn__get_add_command_for_items # CLI add command
|
|
134
|
+
mcp__shadcn__list_items_in_registries # List all components
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Context7 MCP (All 3rd Party Libraries)
|
|
138
|
+
|
|
139
|
+
Use for **any npm package**—not just React libraries:
|
|
140
|
+
|
|
141
|
+
```
|
|
142
|
+
resolve-library-id # Get library ID (e.g., "react-hook-form")
|
|
143
|
+
get-library-docs # Fetch documentation
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Common libraries in this project**:
|
|
147
|
+
|
|
148
|
+
- `react-hook-form` - Form handling
|
|
149
|
+
- `@tanstack/react-query` - Server state
|
|
150
|
+
- `zustand` - Client state
|
|
151
|
+
- `zod` - Validation schemas
|
|
152
|
+
- `@lingui/react` - i18n
|
|
153
|
+
- `msw` - API mocking
|
|
154
|
+
|
|
155
|
+
### Decision Flow
|
|
156
|
+
|
|
157
|
+
```
|
|
158
|
+
Need UI component? → Shadcn MCP
|
|
159
|
+
Need library docs? → Context7 MCP (any npm package)
|
|
160
|
+
Need general info? → WebSearch (fallback only)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
### Adding Shadcn Components
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npx shadcn@latest add button # Single component
|
|
167
|
+
npx shadcn@latest add dialog card input # Multiple components
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
Components are installed to `src/components/ui/`. Import directly (no barrel exports).
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro';
|
|
2
|
+
import { lazy, Suspense } from 'react';
|
|
3
|
+
import { Route, Routes } from 'react-router';
|
|
4
|
+
|
|
5
|
+
import { Header } from '@/components/layout';
|
|
6
|
+
import { SEO } from '@/components/shared';
|
|
7
|
+
import { PageLoading } from '@/components/ui/loading';
|
|
8
|
+
import { SkipLink } from '@/components/ui/visually-hidden';
|
|
9
|
+
import { useThemeEffect } from '@/hooks';
|
|
10
|
+
import { ROUTES } from '@/lib/routes';
|
|
11
|
+
|
|
12
|
+
// Lazy load pages for code splitting
|
|
13
|
+
// eslint-disable-next-line lingui/no-unlocalized-strings
|
|
14
|
+
const HomePage = lazy(() => import('@/pages/Home').then((m) => ({ default: m.HomePage })));
|
|
15
|
+
// eslint-disable-next-line lingui/no-unlocalized-strings
|
|
16
|
+
const NotFoundPage = lazy(() => import('@/pages/NotFound').then((m) => ({ default: m.NotFoundPage })));
|
|
17
|
+
|
|
18
|
+
export default function App() {
|
|
19
|
+
const { t } = useLingui();
|
|
20
|
+
useThemeEffect();
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="bg-background text-foreground min-h-screen">
|
|
24
|
+
<SEO
|
|
25
|
+
description={t({
|
|
26
|
+
message: 'A modern React 19 application with TypeScript and Vite',
|
|
27
|
+
comment: 'Default site-wide meta description for SEO',
|
|
28
|
+
})}
|
|
29
|
+
/>
|
|
30
|
+
<SkipLink />
|
|
31
|
+
<Header />
|
|
32
|
+
<main id="main">
|
|
33
|
+
<Suspense fallback={<PageLoading />}>
|
|
34
|
+
<Routes>
|
|
35
|
+
<Route path={ROUTES.HOME} element={<HomePage />} />
|
|
36
|
+
<Route path={ROUTES.NOT_FOUND} element={<NotFoundPage />} />
|
|
37
|
+
</Routes>
|
|
38
|
+
</Suspense>
|
|
39
|
+
</main>
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Trans } from '@lingui/react/macro';
|
|
2
|
+
|
|
3
|
+
import { LanguageSwitcher, ThemeToggle } from '@/components/shared';
|
|
4
|
+
|
|
5
|
+
export function Header() {
|
|
6
|
+
return (
|
|
7
|
+
<header className="border-border border-b">
|
|
8
|
+
<div className="container mx-auto flex h-14 items-center justify-between px-4">
|
|
9
|
+
<h1 className="text-lg font-semibold">
|
|
10
|
+
<Trans comment="Application name displayed in the header navigation">My App</Trans>
|
|
11
|
+
</h1>
|
|
12
|
+
<div className="flex items-center gap-2">
|
|
13
|
+
<LanguageSwitcher />
|
|
14
|
+
<ThemeToggle />
|
|
15
|
+
</div>
|
|
16
|
+
</div>
|
|
17
|
+
</header>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Header } from './Header';
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Component, type ErrorInfo, type ReactNode } from 'react';
|
|
2
|
+
import { Trans } from '@lingui/react/macro';
|
|
3
|
+
|
|
4
|
+
import { SENTRY_CONFIG } from '@/lib/config';
|
|
5
|
+
|
|
6
|
+
interface Props {
|
|
7
|
+
children: ReactNode;
|
|
8
|
+
fallback?: ReactNode;
|
|
9
|
+
onError?: (error: Error, errorInfo: ErrorInfo) => void;
|
|
10
|
+
onReset?: () => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface State {
|
|
14
|
+
hasError: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class ErrorBoundary extends Component<Props, State> {
|
|
19
|
+
constructor(props: Props) {
|
|
20
|
+
super(props);
|
|
21
|
+
this.state = { hasError: false, error: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
static getDerivedStateFromError(error: Error): State {
|
|
25
|
+
return { hasError: true, error };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
|
29
|
+
console.error('ErrorBoundary caught an error:', error, errorInfo);
|
|
30
|
+
|
|
31
|
+
// Call custom error handler if provided
|
|
32
|
+
this.props.onError?.(error, errorInfo);
|
|
33
|
+
|
|
34
|
+
// Report to Sentry in production (if enabled and configured)
|
|
35
|
+
if (import.meta.env.PROD && SENTRY_CONFIG.enabled && SENTRY_CONFIG.dsn) {
|
|
36
|
+
// eslint-disable-next-line lingui/no-unlocalized-strings
|
|
37
|
+
import('@sentry/react')
|
|
38
|
+
.then((Sentry) => {
|
|
39
|
+
Sentry.captureException(error, {
|
|
40
|
+
extra: { componentStack: errorInfo.componentStack },
|
|
41
|
+
});
|
|
42
|
+
})
|
|
43
|
+
.catch(() => {
|
|
44
|
+
// Sentry failed to load, error already logged above
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Reset the error boundary state
|
|
51
|
+
*/
|
|
52
|
+
reset = () => {
|
|
53
|
+
this.props.onReset?.();
|
|
54
|
+
this.setState({ hasError: false, error: null });
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
render() {
|
|
58
|
+
if (this.state.hasError) {
|
|
59
|
+
if (this.props.fallback) {
|
|
60
|
+
return this.props.fallback;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return (
|
|
64
|
+
<div className="flex min-h-screen items-center justify-center p-4">
|
|
65
|
+
<div className="text-center">
|
|
66
|
+
<h1 className="text-destructive text-2xl font-bold">
|
|
67
|
+
<Trans comment="Error boundary - main error heading">Something went wrong</Trans>
|
|
68
|
+
</h1>
|
|
69
|
+
<p className="text-muted-foreground mt-2">
|
|
70
|
+
<Trans comment="Error boundary - error explanation">
|
|
71
|
+
We're sorry, but something unexpected happened.
|
|
72
|
+
</Trans>
|
|
73
|
+
</p>
|
|
74
|
+
{import.meta.env.DEV && this.state.error && (
|
|
75
|
+
<details className="bg-muted mt-4 rounded-md p-4 text-left">
|
|
76
|
+
<summary className="cursor-pointer font-medium">
|
|
77
|
+
<Trans comment="Error boundary - debug section heading">Error details</Trans>
|
|
78
|
+
</summary>
|
|
79
|
+
<pre className="mt-2 overflow-auto text-sm">{this.state.error.message}</pre>
|
|
80
|
+
<pre className="mt-1 overflow-auto text-xs opacity-75">{this.state.error.stack}</pre>
|
|
81
|
+
</details>
|
|
82
|
+
)}
|
|
83
|
+
<div className="mt-6 flex justify-center gap-3">
|
|
84
|
+
<button
|
|
85
|
+
onClick={this.reset}
|
|
86
|
+
className="bg-secondary text-secondary-foreground rounded px-4 py-2 transition-colors hover:opacity-90"
|
|
87
|
+
>
|
|
88
|
+
<Trans comment="Error boundary - try again button">Try Again</Trans>
|
|
89
|
+
</button>
|
|
90
|
+
<button
|
|
91
|
+
onClick={() => window.location.reload()}
|
|
92
|
+
className="bg-primary text-primary-foreground rounded px-4 py-2 transition-colors hover:opacity-90"
|
|
93
|
+
>
|
|
94
|
+
<Trans comment="Error boundary - refresh button">Refresh Page</Trans>
|
|
95
|
+
</button>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return this.props.children;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { ErrorBoundary } from './ErrorBoundary';
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useLingui } from '@lingui/react/macro';
|
|
2
|
+
import { Languages } from 'lucide-react';
|
|
3
|
+
|
|
4
|
+
import { Button } from '@/components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '@/components/ui/dropdown-menu';
|
|
11
|
+
import { useLanguage } from '@/hooks/useLanguage';
|
|
12
|
+
import { LOCALE_LABELS } from '@/i18n';
|
|
13
|
+
|
|
14
|
+
export function LanguageSwitcher() {
|
|
15
|
+
const { t } = useLingui();
|
|
16
|
+
const { currentLocale, changeLanguage, supportedLocales } = useLanguage();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<DropdownMenu>
|
|
20
|
+
<DropdownMenuTrigger asChild>
|
|
21
|
+
<Button
|
|
22
|
+
variant="ghost"
|
|
23
|
+
size="icon"
|
|
24
|
+
aria-label={t({
|
|
25
|
+
message: 'Change language',
|
|
26
|
+
comment: 'Accessibility label for the language selector dropdown button',
|
|
27
|
+
})}
|
|
28
|
+
>
|
|
29
|
+
<Languages className="size-5" />
|
|
30
|
+
</Button>
|
|
31
|
+
</DropdownMenuTrigger>
|
|
32
|
+
<DropdownMenuContent align="end">
|
|
33
|
+
{supportedLocales.map((locale) => (
|
|
34
|
+
<DropdownMenuItem
|
|
35
|
+
key={locale}
|
|
36
|
+
onClick={() => changeLanguage(locale)}
|
|
37
|
+
className={currentLocale === locale ? 'bg-accent' : ''}
|
|
38
|
+
>
|
|
39
|
+
{LOCALE_LABELS[locale].native}
|
|
40
|
+
</DropdownMenuItem>
|
|
41
|
+
))}
|
|
42
|
+
</DropdownMenuContent>
|
|
43
|
+
</DropdownMenu>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { LanguageSwitcher } from './LanguageSwitcher';
|