@jgamaraalv/ts-dev-kit 2.2.0 → 3.0.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/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/CHANGELOG.md +52 -0
- package/README.md +106 -50
- package/package.json +2 -2
- package/skills/codebase-adapter/SKILL.md +237 -0
- package/skills/codebase-adapter/template.md +25 -0
- package/skills/conventional-commits/SKILL.md +12 -0
- package/skills/core-web-vitals/SKILL.md +102 -0
- package/skills/core-web-vitals/references/cls.md +154 -0
- package/skills/core-web-vitals/references/inp.md +140 -0
- package/skills/core-web-vitals/references/lcp.md +89 -0
- package/skills/core-web-vitals/references/tools.md +112 -0
- package/skills/core-web-vitals/scripts/visualize.py +222 -0
- package/skills/debug/SKILL.md +10 -27
- package/skills/debug/template.md +23 -0
- package/skills/{task → execute-task}/SKILL.md +78 -50
- package/skills/generate-prd/SKILL.md +56 -0
- package/skills/generate-prd/template.md +69 -0
- package/skills/generate-task/SKILL.md +136 -0
- package/skills/generate-task/template.md +71 -0
- package/skills/owasp-security-review/SKILL.md +1 -10
- package/skills/owasp-security-review/template.md +6 -0
- package/skills/tanstack-query/SKILL.md +348 -0
- package/skills/tanstack-query/references/advanced-patterns.md +376 -0
- package/skills/tanstack-query/references/api-reference.md +297 -0
- package/skills/tanstack-query/references/ssr-nextjs.md +272 -0
- package/skills/tanstack-query/references/testing.md +175 -0
- package/skills/ui-ux-guidelines/SKILL.md +1 -17
- package/skills/ui-ux-guidelines/template.md +15 -0
- /package/skills/{task → execute-task}/references/agent-dispatch.md +0 -0
- /package/skills/{task → execute-task}/references/verification-protocol.md +0 -0
- /package/skills/{task/references/output-templates.md → execute-task/template.md} +0 -0
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
# TanStack Query v5 — SSR & Next.js
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Core Concept](#core-concept)
|
|
5
|
+
- [Next.js Pages Router](#nextjs-pages-router)
|
|
6
|
+
- [Next.js App Router](#nextjs-app-router)
|
|
7
|
+
- [Streaming](#streaming)
|
|
8
|
+
- [Critical Gotchas](#critical-gotchas)
|
|
9
|
+
|
|
10
|
+
## Core Concept
|
|
11
|
+
|
|
12
|
+
Server rendering with TanStack Query follows three steps:
|
|
13
|
+
1. **Prefetch** data on the server with `queryClient.prefetchQuery()`
|
|
14
|
+
2. **Dehydrate** the cache with `dehydrate(queryClient)`
|
|
15
|
+
3. **Hydrate** on the client with `<HydrationBoundary state={dehydratedState}>`
|
|
16
|
+
|
|
17
|
+
## Next.js Pages Router
|
|
18
|
+
|
|
19
|
+
### getStaticProps / getServerSideProps
|
|
20
|
+
|
|
21
|
+
```tsx
|
|
22
|
+
import { dehydrate, QueryClient } from '@tanstack/react-query'
|
|
23
|
+
|
|
24
|
+
export async function getStaticProps() {
|
|
25
|
+
const queryClient = new QueryClient()
|
|
26
|
+
|
|
27
|
+
await queryClient.prefetchQuery({
|
|
28
|
+
queryKey: ['posts'],
|
|
29
|
+
queryFn: getPosts,
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
return {
|
|
33
|
+
props: {
|
|
34
|
+
dehydratedState: dehydrate(queryClient),
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### _app.tsx setup
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
import { HydrationBoundary, QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
44
|
+
import { useState } from 'react'
|
|
45
|
+
|
|
46
|
+
export default function MyApp({ Component, pageProps }) {
|
|
47
|
+
const [queryClient] = useState(() => new QueryClient({
|
|
48
|
+
defaultOptions: {
|
|
49
|
+
queries: { staleTime: 60 * 1000 },
|
|
50
|
+
},
|
|
51
|
+
}))
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<QueryClientProvider client={queryClient}>
|
|
55
|
+
<HydrationBoundary state={pageProps.dehydratedState}>
|
|
56
|
+
<Component {...pageProps} />
|
|
57
|
+
</HydrationBoundary>
|
|
58
|
+
</QueryClientProvider>
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Page component — uses useQuery normally
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
export default function PostsPage() {
|
|
67
|
+
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
|
|
68
|
+
// data is immediately available from the hydrated cache
|
|
69
|
+
}
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Next.js App Router
|
|
73
|
+
|
|
74
|
+
### Provider setup (app/providers.tsx)
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
'use client'
|
|
78
|
+
|
|
79
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
80
|
+
import { useState } from 'react'
|
|
81
|
+
|
|
82
|
+
function makeQueryClient() {
|
|
83
|
+
return new QueryClient({
|
|
84
|
+
defaultOptions: {
|
|
85
|
+
queries: {
|
|
86
|
+
staleTime: 60 * 1000, // must be > 0 for SSR
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
})
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let browserQueryClient: QueryClient | undefined
|
|
93
|
+
|
|
94
|
+
function getQueryClient() {
|
|
95
|
+
if (typeof window === 'undefined') {
|
|
96
|
+
// Server: always make a new query client
|
|
97
|
+
return makeQueryClient()
|
|
98
|
+
}
|
|
99
|
+
// Browser: reuse singleton
|
|
100
|
+
if (!browserQueryClient) browserQueryClient = makeQueryClient()
|
|
101
|
+
return browserQueryClient
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export default function Providers({ children }: { children: React.ReactNode }) {
|
|
105
|
+
const queryClient = getQueryClient()
|
|
106
|
+
|
|
107
|
+
return (
|
|
108
|
+
<QueryClientProvider client={queryClient}>
|
|
109
|
+
{children}
|
|
110
|
+
</QueryClientProvider>
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Why not `useState`?** If there is no Suspense boundary between the provider and suspending code, React throws away state on initial suspend. The singleton pattern avoids losing the client.
|
|
116
|
+
|
|
117
|
+
### Root layout (app/layout.tsx)
|
|
118
|
+
|
|
119
|
+
```tsx
|
|
120
|
+
import Providers from './providers'
|
|
121
|
+
|
|
122
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
123
|
+
return (
|
|
124
|
+
<html>
|
|
125
|
+
<body>
|
|
126
|
+
<Providers>{children}</Providers>
|
|
127
|
+
</body>
|
|
128
|
+
</html>
|
|
129
|
+
)
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Prefetch in Server Components
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
// app/posts/page.tsx (Server Component)
|
|
137
|
+
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
|
|
138
|
+
import Posts from './posts' // Client Component
|
|
139
|
+
|
|
140
|
+
export default async function PostsPage() {
|
|
141
|
+
const queryClient = new QueryClient()
|
|
142
|
+
|
|
143
|
+
await queryClient.prefetchQuery({
|
|
144
|
+
queryKey: ['posts'],
|
|
145
|
+
queryFn: getPosts,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
150
|
+
<Posts />
|
|
151
|
+
</HydrationBoundary>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
```tsx
|
|
157
|
+
// app/posts/posts.tsx (Client Component)
|
|
158
|
+
'use client'
|
|
159
|
+
|
|
160
|
+
import { useQuery } from '@tanstack/react-query'
|
|
161
|
+
|
|
162
|
+
export default function Posts() {
|
|
163
|
+
const { data } = useQuery({ queryKey: ['posts'], queryFn: getPosts })
|
|
164
|
+
// data is available immediately from hydration
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Nested prefetching
|
|
169
|
+
|
|
170
|
+
Multiple `<HydrationBoundary>` with separate queryClients at different levels is fine. Deeply nested Server Components can each prefetch their own data.
|
|
171
|
+
|
|
172
|
+
### Shared queryClient with React.cache
|
|
173
|
+
|
|
174
|
+
```tsx
|
|
175
|
+
import { cache } from 'react'
|
|
176
|
+
|
|
177
|
+
const getQueryClient = cache(() => new QueryClient())
|
|
178
|
+
|
|
179
|
+
// Any Server Component in the same request:
|
|
180
|
+
export default async function Page() {
|
|
181
|
+
const queryClient = getQueryClient()
|
|
182
|
+
await queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts })
|
|
183
|
+
return (
|
|
184
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
185
|
+
<Posts />
|
|
186
|
+
</HydrationBoundary>
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Downside: each `dehydrate()` serializes the entire cache, including previously serialized queries.
|
|
192
|
+
|
|
193
|
+
## Streaming
|
|
194
|
+
|
|
195
|
+
Available since v5.40.0. Pending queries can be dehydrated and streamed without `await`.
|
|
196
|
+
|
|
197
|
+
### Setup — configure dehydration to include pending queries
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
function makeQueryClient() {
|
|
201
|
+
return new QueryClient({
|
|
202
|
+
defaultOptions: {
|
|
203
|
+
queries: { staleTime: 60 * 1000 },
|
|
204
|
+
dehydrate: {
|
|
205
|
+
shouldDehydrateQuery: (query) =>
|
|
206
|
+
defaultShouldDehydrateQuery(query) || query.state.status === 'pending',
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
})
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Stream prefetch (no await)
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
// app/posts/page.tsx
|
|
217
|
+
export default function PostsPage() {
|
|
218
|
+
const queryClient = new QueryClient()
|
|
219
|
+
|
|
220
|
+
// No await — query starts but doesn't block rendering
|
|
221
|
+
queryClient.prefetchQuery({ queryKey: ['posts'], queryFn: getPosts })
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<HydrationBoundary state={dehydrate(queryClient)}>
|
|
225
|
+
<Posts />
|
|
226
|
+
</HydrationBoundary>
|
|
227
|
+
)
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
On the client, use `useSuspenseQuery` to consume the streamed promise:
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
'use client'
|
|
235
|
+
export default function Posts() {
|
|
236
|
+
const { data } = useSuspenseQuery({ queryKey: ['posts'], queryFn: getPosts })
|
|
237
|
+
// Suspends until streamed data arrives, then renders
|
|
238
|
+
}
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Non-JSON serialization
|
|
242
|
+
|
|
243
|
+
For data with Dates, Maps, etc., configure custom serialization:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
defaultOptions: {
|
|
247
|
+
dehydrate: { serializeData: superjson.serialize },
|
|
248
|
+
hydrate: { deserializeData: superjson.deserialize },
|
|
249
|
+
}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Critical Gotchas
|
|
253
|
+
|
|
254
|
+
1. **Never create QueryClient at module scope** — it leaks data between requests/users:
|
|
255
|
+
```tsx
|
|
256
|
+
// WRONG
|
|
257
|
+
const queryClient = new QueryClient()
|
|
258
|
+
export default function App() { ... }
|
|
259
|
+
|
|
260
|
+
// CORRECT
|
|
261
|
+
const [queryClient] = useState(() => new QueryClient())
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
2. **Set `staleTime > 0` for SSR** — with default `staleTime: 0`, the client immediately refetches on mount, negating the prefetch.
|
|
265
|
+
|
|
266
|
+
3. **Don't use `fetchQuery` in Server Components to render data** — if both a Server Component and a Client Component use the same data, they desync when the client revalidates. Use `prefetchQuery` (which only populates the cache) and let client components own the data rendering.
|
|
267
|
+
|
|
268
|
+
4. **`hydrate` only overwrites if incoming data is newer** — checked by timestamp.
|
|
269
|
+
|
|
270
|
+
5. **`Error` objects and `undefined` are not JSON-serializable** — handle serialization manually if dehydrating errors. By default, errors are redacted (replaced with `null`).
|
|
271
|
+
|
|
272
|
+
6. **Data ownership rule:** The component that renders the data should own it. Server Components prefetch; Client Components consume via `useQuery`/`useSuspenseQuery`.
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# TanStack Query v5 — Testing
|
|
2
|
+
|
|
3
|
+
## Setup
|
|
4
|
+
|
|
5
|
+
Use `@testing-library/react` (v14+) directly — no need for `@testing-library/react-hooks`.
|
|
6
|
+
|
|
7
|
+
### Create a wrapper with isolated QueryClient
|
|
8
|
+
|
|
9
|
+
```tsx
|
|
10
|
+
import { renderHook, waitFor } from '@testing-library/react'
|
|
11
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
12
|
+
import type { ReactNode } from 'react'
|
|
13
|
+
|
|
14
|
+
function createTestQueryClient() {
|
|
15
|
+
return new QueryClient({
|
|
16
|
+
defaultOptions: {
|
|
17
|
+
queries: {
|
|
18
|
+
retry: false, // CRITICAL: disable retries in tests
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
})
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function createWrapper() {
|
|
25
|
+
const queryClient = createTestQueryClient()
|
|
26
|
+
return function Wrapper({ children }: { children: ReactNode }) {
|
|
27
|
+
return (
|
|
28
|
+
<QueryClientProvider client={queryClient}>
|
|
29
|
+
{children}
|
|
30
|
+
</QueryClientProvider>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Testing a Custom Hook
|
|
37
|
+
|
|
38
|
+
```tsx
|
|
39
|
+
// hooks/useTodos.ts
|
|
40
|
+
export function useTodos() {
|
|
41
|
+
return useQuery({ queryKey: ['todos'], queryFn: fetchTodos })
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// hooks/useTodos.test.ts
|
|
45
|
+
import { renderHook, waitFor } from '@testing-library/react'
|
|
46
|
+
import { useTodos } from './useTodos'
|
|
47
|
+
|
|
48
|
+
test('fetches todos', async () => {
|
|
49
|
+
const { result } = renderHook(() => useTodos(), {
|
|
50
|
+
wrapper: createWrapper(),
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
|
54
|
+
expect(result.current.data).toEqual([{ id: 1, title: 'Test' }])
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Network Mocking
|
|
59
|
+
|
|
60
|
+
### With nock
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import nock from 'nock'
|
|
64
|
+
|
|
65
|
+
test('fetches data from API', async () => {
|
|
66
|
+
nock('http://example.com')
|
|
67
|
+
.get('/api/todos')
|
|
68
|
+
.reply(200, [{ id: 1, title: 'Test' }])
|
|
69
|
+
|
|
70
|
+
const { result } = renderHook(() => useTodos(), {
|
|
71
|
+
wrapper: createWrapper(),
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
|
75
|
+
expect(result.current.data).toEqual([{ id: 1, title: 'Test' }])
|
|
76
|
+
})
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### With msw (recommended)
|
|
80
|
+
|
|
81
|
+
```tsx
|
|
82
|
+
import { setupServer } from 'msw/node'
|
|
83
|
+
import { http, HttpResponse } from 'msw'
|
|
84
|
+
|
|
85
|
+
const server = setupServer(
|
|
86
|
+
http.get('/api/todos', () =>
|
|
87
|
+
HttpResponse.json([{ id: 1, title: 'Test' }])
|
|
88
|
+
),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
beforeAll(() => server.listen())
|
|
92
|
+
afterEach(() => server.resetHandlers())
|
|
93
|
+
afterAll(() => server.close())
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Testing Infinite Queries
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
import nock from 'nock'
|
|
100
|
+
|
|
101
|
+
test('fetches multiple pages', async () => {
|
|
102
|
+
const expectation = nock('http://example.com')
|
|
103
|
+
.persist() // .persist() for multiple requests
|
|
104
|
+
.query(true)
|
|
105
|
+
.get('/api/items')
|
|
106
|
+
.reply(200, (uri) => {
|
|
107
|
+
const url = new URL(`http://example.com${uri}`)
|
|
108
|
+
const page = url.searchParams.get('page') ?? '1'
|
|
109
|
+
return { items: [`item-${page}`], nextPage: Number(page) < 3 ? Number(page) + 1 : null }
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const { result } = renderHook(() => useInfiniteItems(), {
|
|
113
|
+
wrapper: createWrapper(),
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
|
117
|
+
expect(result.current.data.pages).toHaveLength(1)
|
|
118
|
+
|
|
119
|
+
// Fetch next page
|
|
120
|
+
result.current.fetchNextPage()
|
|
121
|
+
|
|
122
|
+
await waitFor(() => expect(result.current.data.pages).toHaveLength(2))
|
|
123
|
+
})
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Testing Mutations
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
test('creates a todo and invalidates list', async () => {
|
|
130
|
+
const queryClient = createTestQueryClient()
|
|
131
|
+
const wrapper = ({ children }: { children: ReactNode }) => (
|
|
132
|
+
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
// Pre-populate cache
|
|
136
|
+
queryClient.setQueryData(['todos'], [{ id: 1, title: 'Existing' }])
|
|
137
|
+
|
|
138
|
+
const { result } = renderHook(() => useCreateTodo(), { wrapper })
|
|
139
|
+
|
|
140
|
+
result.current.mutate({ title: 'New Todo' })
|
|
141
|
+
|
|
142
|
+
await waitFor(() => expect(result.current.isSuccess).toBe(true))
|
|
143
|
+
})
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
## Key Gotchas
|
|
147
|
+
|
|
148
|
+
1. **Always disable retries** — Default 3 retries with exponential backoff causes test timeouts:
|
|
149
|
+
```tsx
|
|
150
|
+
new QueryClient({ defaultOptions: { queries: { retry: false } } })
|
|
151
|
+
```
|
|
152
|
+
Note: explicit `retry` on individual queries overrides this default.
|
|
153
|
+
|
|
154
|
+
2. **Always use `waitFor`** — Query state updates are async. Never assert synchronously after `renderHook`.
|
|
155
|
+
|
|
156
|
+
3. **Isolate QueryClient per test** — Create a new `QueryClient` for each test to prevent cross-test contamination. Do not share a client across tests.
|
|
157
|
+
|
|
158
|
+
4. **Jest "did not exit" warning** — If you get this, set `gcTime: Infinity` on the test QueryClient to prevent garbage collection timers from hanging:
|
|
159
|
+
```tsx
|
|
160
|
+
new QueryClient({
|
|
161
|
+
defaultOptions: {
|
|
162
|
+
queries: { retry: false, gcTime: Infinity },
|
|
163
|
+
},
|
|
164
|
+
})
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
5. **Component testing** — For testing components (not just hooks), render the component inside the wrapper:
|
|
168
|
+
```tsx
|
|
169
|
+
import { render, screen, waitFor } from '@testing-library/react'
|
|
170
|
+
|
|
171
|
+
test('renders todo list', async () => {
|
|
172
|
+
render(<TodoList />, { wrapper: createWrapper() })
|
|
173
|
+
await waitFor(() => expect(screen.getByText('Test Todo')).toBeInTheDocument())
|
|
174
|
+
})
|
|
175
|
+
```
|
|
@@ -76,23 +76,7 @@ Dispatch hub for UI/UX rules. Load the relevant reference file for full details.
|
|
|
76
76
|
|
|
77
77
|
Group findings by file. Use `file:line` format (VS Code clickable). Be terse -- state issue and location. Skip explanation unless fix is non-obvious.
|
|
78
78
|
|
|
79
|
-
|
|
80
|
-
## src/Button.tsx
|
|
81
|
-
|
|
82
|
-
src/Button.tsx:42 - icon button missing aria-label
|
|
83
|
-
src/Button.tsx:18 - input lacks label
|
|
84
|
-
src/Button.tsx:55 - animation missing prefers-reduced-motion
|
|
85
|
-
src/Button.tsx:67 - transition: all -> list properties explicitly
|
|
86
|
-
|
|
87
|
-
## src/Modal.tsx
|
|
88
|
-
|
|
89
|
-
src/Modal.tsx:12 - missing overscroll-behavior: contain
|
|
90
|
-
src/Modal.tsx:34 - "..." -> "..."
|
|
91
|
-
|
|
92
|
-
## src/Card.tsx
|
|
93
|
-
|
|
94
|
-
pass
|
|
95
|
-
```
|
|
79
|
+
See [template.md](template.md) for the expected output format.
|
|
96
80
|
|
|
97
81
|
---
|
|
98
82
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
## src/Button.tsx
|
|
2
|
+
|
|
3
|
+
src/Button.tsx:42 - icon button missing aria-label
|
|
4
|
+
src/Button.tsx:18 - input lacks label
|
|
5
|
+
src/Button.tsx:55 - animation missing prefers-reduced-motion
|
|
6
|
+
src/Button.tsx:67 - transition: all -> list properties explicitly
|
|
7
|
+
|
|
8
|
+
## src/Modal.tsx
|
|
9
|
+
|
|
10
|
+
src/Modal.tsx:12 - missing overscroll-behavior: contain
|
|
11
|
+
src/Modal.tsx:34 - "Close" -> "Close dialog"
|
|
12
|
+
|
|
13
|
+
## src/Card.tsx
|
|
14
|
+
|
|
15
|
+
pass
|
|
File without changes
|
|
File without changes
|
|
File without changes
|