codecruise 0.1.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/LICENSE +21 -0
- package/README.md +111 -0
- package/bin/codecruise.js +68 -0
- package/config/CLAUDE.md +107 -0
- package/config/agents/analyst.md +48 -0
- package/config/agents/architect-reviewer.md +161 -0
- package/config/agents/architect.md +119 -0
- package/config/agents/critic.md +63 -0
- package/config/agents/developer.md +96 -0
- package/config/agents/devops.md +81 -0
- package/config/agents/orchestrator.md +91 -0
- package/config/agents/planner.md +139 -0
- package/config/agents/retro.md +52 -0
- package/config/agents/reviewer.md +101 -0
- package/config/agents/security-reviewer.md +57 -0
- package/config/agents/stack/expo/AGENT.md +473 -0
- package/config/agents/stack/expo/rules/critical.md +427 -0
- package/config/agents/stack/expo/rules/native.md +455 -0
- package/config/agents/stack/expo/rules/navigation.md +445 -0
- package/config/agents/stack/expo/rules/performance.md +415 -0
- package/config/agents/stack/fastify/AGENT.md +397 -0
- package/config/agents/stack/fastify/rules/api-design.md +283 -0
- package/config/agents/stack/fastify/rules/critical.md +232 -0
- package/config/agents/stack/fastify/rules/queues.md +303 -0
- package/config/agents/stack/fastify/rules/security.md +384 -0
- package/config/agents/stack/index.yaml +48 -0
- package/config/agents/stack/nextjs/AGENT.md +421 -0
- package/config/agents/stack/nextjs/rules/components.md +413 -0
- package/config/agents/stack/nextjs/rules/critical.md +391 -0
- package/config/agents/stack/nextjs/rules/performance.md +403 -0
- package/config/agents/stack/nextjs/rules/styling.md +334 -0
- package/config/agents/stack/shared-ts/AGENT.md +384 -0
- package/config/agents/stack/shared-ts/rules/critical.md +315 -0
- package/config/agents/stack/shared-ts/rules/patterns.md +384 -0
- package/config/agents/stack/shared-ts/rules/zod.md +427 -0
- package/config/agents/tester.md +79 -0
- package/config/commands/architect-discuss.md +366 -0
- package/config/commands/architect-list.md +160 -0
- package/config/commands/architect-review.md +111 -0
- package/config/commands/architect.md +118 -0
- package/config/commands/compact.md +118 -0
- package/config/commands/companion.md +279 -0
- package/config/commands/dashboard.md +152 -0
- package/config/commands/doctor.md +227 -0
- package/config/commands/dogfood-report.md +101 -0
- package/config/commands/flags/run-autonomous.md +110 -0
- package/config/commands/flags/run-pause.md +80 -0
- package/config/commands/ingest.md +173 -0
- package/config/commands/init.md +128 -0
- package/config/commands/metrics.md +87 -0
- package/config/commands/parallel.md +320 -0
- package/config/commands/pause.md +55 -0
- package/config/commands/plan-review.md +130 -0
- package/config/commands/plan.md +216 -0
- package/config/commands/production-check.md +308 -0
- package/config/commands/refine.md +323 -0
- package/config/commands/resume.md +72 -0
- package/config/commands/retro.md +121 -0
- package/config/commands/retry.md +75 -0
- package/config/commands/role.md +310 -0
- package/config/commands/run.md +417 -0
- package/config/commands/scope.md +85 -0
- package/config/commands/setup-permissions.md +104 -0
- package/config/commands/skip.md +75 -0
- package/config/commands/spec-forge.md +213 -0
- package/config/commands/spec-help.md +194 -0
- package/config/commands/spec-patch.md +342 -0
- package/config/commands/spec-resolve.md +110 -0
- package/config/commands/spec-review.md +153 -0
- package/config/commands/status.md +114 -0
- package/config/commands/sync.md +131 -0
- package/config/commands/task.md +138 -0
- package/config/commands/verify.md +124 -0
- package/config/hooks/README.md +632 -0
- package/config/hooks/activity-log.sh +187 -0
- package/config/hooks/anti-rationalize.sh +52 -0
- package/config/hooks/capture-verification.sh +112 -0
- package/config/hooks/collect-metrics.sh +135 -0
- package/config/hooks/enforce-file-scope.sh +75 -0
- package/config/hooks/enforce-state-machine.sh +161 -0
- package/config/hooks/enforce-tdd.sh +180 -0
- package/config/hooks/format.sh +40 -0
- package/config/hooks/lib/activity-helpers.sh +162 -0
- package/config/hooks/lib/read-settings.sh +71 -0
- package/config/hooks/load-context-skills.sh +95 -0
- package/config/hooks/notify.sh +81 -0
- package/config/hooks/pre-commit.sample +35 -0
- package/config/hooks/protect-files.sh +63 -0
- package/config/hooks/track-agents.sh +41 -0
- package/config/hooks/track-commands.sh +37 -0
- package/config/hooks/track-enforcement.sh +44 -0
- package/config/hooks/track-ooda.sh +77 -0
- package/config/hooks/validate-commit-msg.sh +35 -0
- package/config/hooks/validate-plan.sh +213 -0
- package/config/hooks/verify-criteria.sh +46 -0
- package/config/hooks/verify-todo-completion.sh +140 -0
- package/config/rules/comments.md +25 -0
- package/config/rules/decision-rules.md +308 -0
- package/config/rules/hygiene.md +247 -0
- package/config/rules/pattern-detection.md +372 -0
- package/config/rules/profiles.md +193 -0
- package/config/rules/recovery.md +83 -0
- package/config/rules/scope-detection.md +213 -0
- package/config/rules/standards.md +127 -0
- package/config/rules/workflow.md +121 -0
- package/config/schemas.md +767 -0
- package/config/settings.json +195 -0
- package/config/skills/backend/SKILL.md +734 -0
- package/config/skills/database/SKILL.md +426 -0
- package/config/skills/frontend/SKILL.md +434 -0
- package/config/skills/git/SKILL.md +396 -0
- package/config/skills/index.yaml +36 -0
- package/config/skills/observability/SKILL.md +430 -0
- package/config/skills/package-dev/SKILL.md +498 -0
- package/config/skills/performance/SKILL.md +378 -0
- package/config/skills/resilience/SKILL.md +573 -0
- package/config/skills/testing/SKILL.md +398 -0
- package/config/skills/testing-patterns/SKILL.md +276 -0
- package/config/skills/typescript/SKILL.md +152 -0
- package/config/templates/CLAUDE.md +70 -0
- package/config/templates/README.md +117 -0
- package/config/templates/steering/adr-template.md +102 -0
- package/config/templates/steering/product.md +60 -0
- package/config/templates/steering/rfc-template.md +159 -0
- package/config/templates/steering/structure.md +146 -0
- package/config/templates/steering/tech.md +85 -0
- package/package.json +40 -0
- package/src/install.js +163 -0
- package/src/report.js +310 -0
|
@@ -0,0 +1,413 @@
|
|
|
1
|
+
# Component Rules - React & shadcn/ui
|
|
2
|
+
|
|
3
|
+
Patterns for building maintainable React components.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Component Structure
|
|
8
|
+
|
|
9
|
+
### COMP-001: Use function components
|
|
10
|
+
|
|
11
|
+
```typescript
|
|
12
|
+
// GOOD
|
|
13
|
+
function UserCard({ user }: { user: User }) {
|
|
14
|
+
return <div>...</div>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// Also good - arrow function for small components
|
|
18
|
+
const Badge = ({ label }: { label: string }) => (
|
|
19
|
+
<span className="badge">{label}</span>
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
// Export named, not default (better refactoring)
|
|
23
|
+
export function UserCard({ user }: { user: User }) { ... }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### COMP-002: Prop types with interface
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
interface UserCardProps {
|
|
30
|
+
user: User;
|
|
31
|
+
onEdit?: (user: User) => void;
|
|
32
|
+
className?: string;
|
|
33
|
+
children?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function UserCard({ user, onEdit, className, children }: UserCardProps) {
|
|
37
|
+
return (
|
|
38
|
+
<Card className={cn('p-4', className)}>
|
|
39
|
+
<h3>{user.name}</h3>
|
|
40
|
+
{children}
|
|
41
|
+
{onEdit && <Button onClick={() => onEdit(user)}>Edit</Button>}
|
|
42
|
+
</Card>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### COMP-003: Composition over configuration
|
|
48
|
+
|
|
49
|
+
```typescript
|
|
50
|
+
// BAD - Too many props
|
|
51
|
+
<Card
|
|
52
|
+
title="Settings"
|
|
53
|
+
subtitle="Manage your account"
|
|
54
|
+
icon={<Settings />}
|
|
55
|
+
actions={[{ label: 'Save', onClick: save }]}
|
|
56
|
+
footer={<HelpLink />}
|
|
57
|
+
/>
|
|
58
|
+
|
|
59
|
+
// GOOD - Composable
|
|
60
|
+
<Card>
|
|
61
|
+
<CardHeader>
|
|
62
|
+
<CardIcon><Settings /></CardIcon>
|
|
63
|
+
<CardTitle>Settings</CardTitle>
|
|
64
|
+
<CardDescription>Manage your account</CardDescription>
|
|
65
|
+
</CardHeader>
|
|
66
|
+
<CardContent>...</CardContent>
|
|
67
|
+
<CardFooter>
|
|
68
|
+
<HelpLink />
|
|
69
|
+
<Button onClick={save}>Save</Button>
|
|
70
|
+
</CardFooter>
|
|
71
|
+
</Card>
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## shadcn/ui Patterns
|
|
77
|
+
|
|
78
|
+
### COMP-004: Use shadcn components correctly
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
// Button variants
|
|
82
|
+
<Button>Default</Button>
|
|
83
|
+
<Button variant="secondary">Secondary</Button>
|
|
84
|
+
<Button variant="destructive">Delete</Button>
|
|
85
|
+
<Button variant="outline">Outline</Button>
|
|
86
|
+
<Button variant="ghost">Ghost</Button>
|
|
87
|
+
<Button size="sm">Small</Button>
|
|
88
|
+
<Button size="icon"><Plus /></Button>
|
|
89
|
+
|
|
90
|
+
// With loading state
|
|
91
|
+
<Button disabled={isLoading}>
|
|
92
|
+
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
|
|
93
|
+
{isLoading ? 'Saving...' : 'Save'}
|
|
94
|
+
</Button>
|
|
95
|
+
|
|
96
|
+
// As link
|
|
97
|
+
<Button asChild>
|
|
98
|
+
<Link href="/dashboard">Go to Dashboard</Link>
|
|
99
|
+
</Button>
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
### COMP-005: Form components with React Hook Form
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { useForm } from 'react-hook-form';
|
|
106
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
107
|
+
import {
|
|
108
|
+
Form,
|
|
109
|
+
FormControl,
|
|
110
|
+
FormDescription,
|
|
111
|
+
FormField,
|
|
112
|
+
FormItem,
|
|
113
|
+
FormLabel,
|
|
114
|
+
FormMessage,
|
|
115
|
+
} from '@/components/ui/form';
|
|
116
|
+
|
|
117
|
+
function ProfileForm() {
|
|
118
|
+
const form = useForm<FormData>({
|
|
119
|
+
resolver: zodResolver(schema),
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<Form {...form}>
|
|
124
|
+
<form onSubmit={form.handleSubmit(onSubmit)}>
|
|
125
|
+
<FormField
|
|
126
|
+
control={form.control}
|
|
127
|
+
name="username"
|
|
128
|
+
render={({ field }) => (
|
|
129
|
+
<FormItem>
|
|
130
|
+
<FormLabel>Username</FormLabel>
|
|
131
|
+
<FormControl>
|
|
132
|
+
<Input placeholder="johndoe" {...field} />
|
|
133
|
+
</FormControl>
|
|
134
|
+
<FormDescription>
|
|
135
|
+
This is your public display name.
|
|
136
|
+
</FormDescription>
|
|
137
|
+
<FormMessage />
|
|
138
|
+
</FormItem>
|
|
139
|
+
)}
|
|
140
|
+
/>
|
|
141
|
+
</form>
|
|
142
|
+
</Form>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
### COMP-006: Dialog and Sheet patterns
|
|
148
|
+
|
|
149
|
+
```typescript
|
|
150
|
+
// Controlled dialog
|
|
151
|
+
function EditUserDialog({ user, open, onOpenChange }) {
|
|
152
|
+
return (
|
|
153
|
+
<Dialog open={open} onOpenChange={onOpenChange}>
|
|
154
|
+
<DialogContent>
|
|
155
|
+
<DialogHeader>
|
|
156
|
+
<DialogTitle>Edit User</DialogTitle>
|
|
157
|
+
<DialogDescription>
|
|
158
|
+
Make changes to the user profile.
|
|
159
|
+
</DialogDescription>
|
|
160
|
+
</DialogHeader>
|
|
161
|
+
<UserForm user={user} onSuccess={() => onOpenChange(false)} />
|
|
162
|
+
</DialogContent>
|
|
163
|
+
</Dialog>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// With trigger
|
|
168
|
+
function CreateUserButton() {
|
|
169
|
+
return (
|
|
170
|
+
<Dialog>
|
|
171
|
+
<DialogTrigger asChild>
|
|
172
|
+
<Button>Create User</Button>
|
|
173
|
+
</DialogTrigger>
|
|
174
|
+
<DialogContent>
|
|
175
|
+
<DialogHeader>
|
|
176
|
+
<DialogTitle>Create User</DialogTitle>
|
|
177
|
+
</DialogHeader>
|
|
178
|
+
<UserForm />
|
|
179
|
+
</DialogContent>
|
|
180
|
+
</Dialog>
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
---
|
|
186
|
+
|
|
187
|
+
## State Patterns
|
|
188
|
+
|
|
189
|
+
### COMP-007: Lift state appropriately
|
|
190
|
+
|
|
191
|
+
```typescript
|
|
192
|
+
// State lives in lowest common ancestor
|
|
193
|
+
|
|
194
|
+
// Parent manages shared state
|
|
195
|
+
function UserList() {
|
|
196
|
+
const [selectedId, setSelectedId] = useState<string | null>(null);
|
|
197
|
+
|
|
198
|
+
return (
|
|
199
|
+
<div className="flex">
|
|
200
|
+
<UserSidebar
|
|
201
|
+
selectedId={selectedId}
|
|
202
|
+
onSelect={setSelectedId}
|
|
203
|
+
/>
|
|
204
|
+
<UserDetail userId={selectedId} />
|
|
205
|
+
</div>
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Children receive props, no local state needed
|
|
210
|
+
function UserSidebar({ selectedId, onSelect }) {
|
|
211
|
+
const { data: users } = useUsers();
|
|
212
|
+
|
|
213
|
+
return (
|
|
214
|
+
<ul>
|
|
215
|
+
{users.map(user => (
|
|
216
|
+
<li
|
|
217
|
+
key={user.id}
|
|
218
|
+
className={cn(user.id === selectedId && 'bg-accent')}
|
|
219
|
+
onClick={() => onSelect(user.id)}
|
|
220
|
+
>
|
|
221
|
+
{user.name}
|
|
222
|
+
</li>
|
|
223
|
+
))}
|
|
224
|
+
</ul>
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### COMP-008: Use compound components for complex UI
|
|
230
|
+
|
|
231
|
+
```typescript
|
|
232
|
+
// Compound component pattern
|
|
233
|
+
const TabsContext = createContext<{...} | null>(null);
|
|
234
|
+
|
|
235
|
+
function Tabs({ children, defaultValue }) {
|
|
236
|
+
const [value, setValue] = useState(defaultValue);
|
|
237
|
+
|
|
238
|
+
return (
|
|
239
|
+
<TabsContext.Provider value={{ value, setValue }}>
|
|
240
|
+
<div className="tabs">{children}</div>
|
|
241
|
+
</TabsContext.Provider>
|
|
242
|
+
);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function TabsList({ children }) {
|
|
246
|
+
return <div className="tabs-list">{children}</div>;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function TabsTrigger({ value, children }) {
|
|
250
|
+
const { value: selected, setValue } = useContext(TabsContext);
|
|
251
|
+
|
|
252
|
+
return (
|
|
253
|
+
<button
|
|
254
|
+
className={cn('tab', value === selected && 'active')}
|
|
255
|
+
onClick={() => setValue(value)}
|
|
256
|
+
>
|
|
257
|
+
{children}
|
|
258
|
+
</button>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function TabsContent({ value, children }) {
|
|
263
|
+
const { value: selected } = useContext(TabsContext);
|
|
264
|
+
if (value !== selected) return null;
|
|
265
|
+
return <div className="tab-content">{children}</div>;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Usage
|
|
269
|
+
<Tabs defaultValue="account">
|
|
270
|
+
<TabsList>
|
|
271
|
+
<TabsTrigger value="account">Account</TabsTrigger>
|
|
272
|
+
<TabsTrigger value="security">Security</TabsTrigger>
|
|
273
|
+
</TabsList>
|
|
274
|
+
<TabsContent value="account">...</TabsContent>
|
|
275
|
+
<TabsContent value="security">...</TabsContent>
|
|
276
|
+
</Tabs>
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
---
|
|
280
|
+
|
|
281
|
+
## Event Handling
|
|
282
|
+
|
|
283
|
+
### COMP-009: Memoize callbacks appropriately
|
|
284
|
+
|
|
285
|
+
```typescript
|
|
286
|
+
// Only memoize when passing to memoized children
|
|
287
|
+
// or when callback identity matters
|
|
288
|
+
|
|
289
|
+
// Usually NOT needed
|
|
290
|
+
function UserList() {
|
|
291
|
+
const handleClick = (id: string) => {
|
|
292
|
+
// This is fine, UserItem likely re-renders anyway
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
return users.map(u => <UserItem key={u.id} onClick={() => handleClick(u.id)} />);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Memoize when passing to virtualized lists or heavy components
|
|
299
|
+
const handleClick = useCallback((id: string) => {
|
|
300
|
+
setSelected(id);
|
|
301
|
+
}, []);
|
|
302
|
+
|
|
303
|
+
// Or when used in effects
|
|
304
|
+
const fetchUser = useCallback(async () => {
|
|
305
|
+
const user = await getUser(userId);
|
|
306
|
+
setUser(user);
|
|
307
|
+
}, [userId]);
|
|
308
|
+
|
|
309
|
+
useEffect(() => {
|
|
310
|
+
fetchUser();
|
|
311
|
+
}, [fetchUser]);
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
### COMP-010: Handle async actions in forms
|
|
315
|
+
|
|
316
|
+
```typescript
|
|
317
|
+
function SubmitButton() {
|
|
318
|
+
const { pending } = useFormStatus();
|
|
319
|
+
|
|
320
|
+
return (
|
|
321
|
+
<Button type="submit" disabled={pending}>
|
|
322
|
+
{pending ? (
|
|
323
|
+
<>
|
|
324
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
325
|
+
Saving...
|
|
326
|
+
</>
|
|
327
|
+
) : (
|
|
328
|
+
'Save'
|
|
329
|
+
)}
|
|
330
|
+
</Button>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function UserForm() {
|
|
335
|
+
const [state, formAction] = useFormState(createUser, initialState);
|
|
336
|
+
|
|
337
|
+
return (
|
|
338
|
+
<form action={formAction}>
|
|
339
|
+
{state?.error && <Alert variant="destructive">{state.error}</Alert>}
|
|
340
|
+
<Input name="email" />
|
|
341
|
+
<SubmitButton />
|
|
342
|
+
</form>
|
|
343
|
+
);
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
349
|
+
## Accessibility
|
|
350
|
+
|
|
351
|
+
### COMP-011: Use semantic HTML
|
|
352
|
+
|
|
353
|
+
```typescript
|
|
354
|
+
// GOOD
|
|
355
|
+
<nav aria-label="Main navigation">
|
|
356
|
+
<ul>
|
|
357
|
+
<li><Link href="/">Home</Link></li>
|
|
358
|
+
</ul>
|
|
359
|
+
</nav>
|
|
360
|
+
|
|
361
|
+
<main>
|
|
362
|
+
<article>
|
|
363
|
+
<h1>Title</h1>
|
|
364
|
+
<p>Content</p>
|
|
365
|
+
</article>
|
|
366
|
+
</main>
|
|
367
|
+
|
|
368
|
+
<button onClick={handleClick}>Click me</button>
|
|
369
|
+
|
|
370
|
+
// BAD
|
|
371
|
+
<div onClick={handleClick}>Click me</div>
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### COMP-012: Accessible forms
|
|
375
|
+
|
|
376
|
+
```typescript
|
|
377
|
+
// Labels
|
|
378
|
+
<label htmlFor="email">Email</label>
|
|
379
|
+
<input id="email" type="email" aria-describedby="email-hint" />
|
|
380
|
+
<p id="email-hint">We'll never share your email.</p>
|
|
381
|
+
|
|
382
|
+
// Required fields
|
|
383
|
+
<label htmlFor="name">
|
|
384
|
+
Name <span aria-hidden="true">*</span>
|
|
385
|
+
<span className="sr-only">(required)</span>
|
|
386
|
+
</label>
|
|
387
|
+
<input id="name" required aria-required="true" />
|
|
388
|
+
|
|
389
|
+
// Error states
|
|
390
|
+
<input
|
|
391
|
+
id="email"
|
|
392
|
+
aria-invalid={hasError}
|
|
393
|
+
aria-describedby={hasError ? 'email-error' : undefined}
|
|
394
|
+
/>
|
|
395
|
+
{hasError && <p id="email-error" role="alert">Invalid email</p>}
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
### COMP-013: Screen reader utilities
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
// Visually hidden but accessible
|
|
402
|
+
<span className="sr-only">Open menu</span>
|
|
403
|
+
|
|
404
|
+
// Skip link
|
|
405
|
+
<a href="#main-content" className="sr-only focus:not-sr-only">
|
|
406
|
+
Skip to main content
|
|
407
|
+
</a>
|
|
408
|
+
|
|
409
|
+
// Live regions
|
|
410
|
+
<div aria-live="polite" aria-atomic="true">
|
|
411
|
+
{notification}
|
|
412
|
+
</div>
|
|
413
|
+
```
|