@dynokostya/just-works 1.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/agents/csharp-code-writer.md +32 -0
- package/.claude/agents/diagrammer.md +49 -0
- package/.claude/agents/frontend-code-writer.md +36 -0
- package/.claude/agents/prompt-writer.md +38 -0
- package/.claude/agents/python-code-writer.md +32 -0
- package/.claude/agents/swift-code-writer.md +32 -0
- package/.claude/agents/typescript-code-writer.md +32 -0
- package/.claude/commands/git-sync.md +96 -0
- package/.claude/commands/project-docs.md +287 -0
- package/.claude/settings.json +112 -0
- package/.claude/settings.json.default +15 -0
- package/.claude/skills/csharp-coding/SKILL.md +368 -0
- package/.claude/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.claude/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.claude/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.claude/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.claude/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.claude/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.claude/skills/python-coding/SKILL.md +293 -0
- package/.claude/skills/react-coding/SKILL.md +264 -0
- package/.claude/skills/rest-api/SKILL.md +421 -0
- package/.claude/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.claude/skills/swift-coding/SKILL.md +401 -0
- package/.claude/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.claude/skills/typescript-coding/SKILL.md +464 -0
- package/.claude/statusline-command.sh +34 -0
- package/.codex/prompts/plan-reviewer.md +162 -0
- package/.codex/prompts/project-docs.md +287 -0
- package/.codex/skills/ddd-architecture-python/SKILL.md +288 -0
- package/.codex/skills/feature-driven-architecture-python/SKILL.md +302 -0
- package/.codex/skills/gemini-3-prompting/SKILL.md +483 -0
- package/.codex/skills/gpt-5-2-prompting/SKILL.md +295 -0
- package/.codex/skills/opus-4-6-prompting/SKILL.md +315 -0
- package/.codex/skills/plantuml-diagramming/SKILL.md +758 -0
- package/.codex/skills/python-coding/SKILL.md +293 -0
- package/.codex/skills/react-coding/SKILL.md +264 -0
- package/.codex/skills/rest-api/SKILL.md +421 -0
- package/.codex/skills/shadcn-ui-coding/SKILL.md +454 -0
- package/.codex/skills/tailwind-css-coding/SKILL.md +268 -0
- package/.codex/skills/typescript-coding/SKILL.md +464 -0
- package/AGENTS.md +57 -0
- package/CLAUDE.md +98 -0
- package/LICENSE +201 -0
- package/README.md +114 -0
- package/bin/cli.mjs +291 -0
- package/package.json +39 -0
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: shadcn-ui-coding
|
|
3
|
+
description: Apply when generating shadcn/ui code. Covers the copy-paste component model, Radix UI compound components, theming with CSS variables, Form/DataTable integrations, and CLI conventions. Does NOT cover general React or Tailwind CSS patterns.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# shadcn/ui coding
|
|
7
|
+
|
|
8
|
+
Match project conventions. Read `components.json` for style, Tailwind version, aliases, and RSC settings. Check `components/ui/` for local component source and `package.json` for React version (18 vs 19) and Radix package format (unified `radix-ui` vs individual `@radix-ui/react-*`). These defaults apply only when the project has no established convention.
|
|
9
|
+
|
|
10
|
+
## Never rules
|
|
11
|
+
|
|
12
|
+
These are unconditional. They prevent runtime errors, accessibility breakage, and infinite re-renders regardless of project style.
|
|
13
|
+
|
|
14
|
+
- **Never import shadcn components from npm** -- shadcn is not a package. Import from the local alias.
|
|
15
|
+
|
|
16
|
+
```tsx
|
|
17
|
+
// Wrong: shadcn is not an npm package
|
|
18
|
+
import { Button } from "shadcn/ui";
|
|
19
|
+
import { Button } from "@shadcn/ui";
|
|
20
|
+
|
|
21
|
+
// Correct: import from the local alias defined in components.json
|
|
22
|
+
import { Button } from "@/components/ui/button";
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
- **Never flatten Radix compound components** -- Dialog, DropdownMenu, Select, AlertDialog, Menubar, NavigationMenu, ContextMenu, and Tabs require their nested structure. Removing wrapper layers breaks ARIA roles and keyboard navigation.
|
|
26
|
+
|
|
27
|
+
```tsx
|
|
28
|
+
// Wrong: missing compound structure, broken accessibility
|
|
29
|
+
<Dialog>
|
|
30
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
31
|
+
<DialogTitle>Title</DialogTitle>
|
|
32
|
+
<DialogDescription>Desc</DialogDescription>
|
|
33
|
+
</Dialog>
|
|
34
|
+
|
|
35
|
+
// Correct: full compound structure preserved
|
|
36
|
+
<Dialog>
|
|
37
|
+
<DialogTrigger>Open</DialogTrigger>
|
|
38
|
+
<DialogContent>
|
|
39
|
+
<DialogHeader>
|
|
40
|
+
<DialogTitle>Title</DialogTitle>
|
|
41
|
+
<DialogDescription>Desc</DialogDescription>
|
|
42
|
+
</DialogHeader>
|
|
43
|
+
</DialogContent>
|
|
44
|
+
</Dialog>
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
- **Never omit `asChild` when wrapping custom elements in Trigger components** -- without `asChild`, the Trigger renders its own `<button>`, creating button-in-button nesting (invalid HTML, broken accessibility).
|
|
48
|
+
|
|
49
|
+
```tsx
|
|
50
|
+
// Wrong: renders <button><button>...</button></button>
|
|
51
|
+
<DialogTrigger>
|
|
52
|
+
<Button variant="outline">Open</Button>
|
|
53
|
+
</DialogTrigger>
|
|
54
|
+
|
|
55
|
+
// Correct: Slot merges props onto the child element
|
|
56
|
+
<DialogTrigger asChild>
|
|
57
|
+
<Button variant="outline">Open</Button>
|
|
58
|
+
</DialogTrigger>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
- **Never use hardcoded colors** -- use CSS variable tokens. Hardcoded values bypass dark mode and break theme consistency.
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
// Wrong: hardcoded color, breaks in dark mode
|
|
65
|
+
<div className="bg-blue-500 text-white">
|
|
66
|
+
|
|
67
|
+
// Correct: CSS variable tokens, theme-aware
|
|
68
|
+
<div className="bg-primary text-primary-foreground">
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **Never compose classNames with template literals** -- use `cn()` (clsx + tailwind-merge). Template literals can't resolve Tailwind class conflicts.
|
|
72
|
+
|
|
73
|
+
```tsx
|
|
74
|
+
// Wrong: conflicting classes not resolved
|
|
75
|
+
<div className={`px-2 ${className}`}>
|
|
76
|
+
|
|
77
|
+
// Correct: tailwind-merge resolves conflicts, last wins
|
|
78
|
+
import { cn } from "@/lib/utils";
|
|
79
|
+
<div className={cn("px-2", className)}>
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
- **Never spread `{...field}` on Select, Checkbox, or Switch** -- these use `onValueChange`/`onCheckedChange`, not `onChange`. Spreading `field` binds `onChange`, which is silently ignored.
|
|
83
|
+
|
|
84
|
+
```tsx
|
|
85
|
+
// Wrong: onChange is ignored by Radix Select
|
|
86
|
+
<Select {...field}>
|
|
87
|
+
|
|
88
|
+
// Correct: wire value and handler explicitly
|
|
89
|
+
<Select onValueChange={field.onChange} value={field.value}>
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- **Never define DataTable columns inside the component** -- new object references each render cause infinite re-renders with TanStack Table's referential equality checks.
|
|
93
|
+
|
|
94
|
+
```tsx
|
|
95
|
+
// Wrong: new columns array every render, infinite loop
|
|
96
|
+
function UsersTable({ data }: { data: User[] }) {
|
|
97
|
+
const columns: ColumnDef<User>[] = [
|
|
98
|
+
{ accessorKey: "name", header: "Name" },
|
|
99
|
+
];
|
|
100
|
+
return <DataTable columns={columns} data={data} />;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Correct: stable reference, defined outside component
|
|
104
|
+
const columns: ColumnDef<User>[] = [
|
|
105
|
+
{ accessorKey: "name", header: "Name" },
|
|
106
|
+
];
|
|
107
|
+
function UsersTable({ data }: { data: User[] }) {
|
|
108
|
+
return <DataTable columns={columns} data={data} />;
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- **Never put more or fewer than one child inside FormControl** -- uses Radix Slot internally, merges props onto exactly one child. Zero or multiple children is a runtime error.
|
|
113
|
+
- **Never build custom modals, dropdowns, or tooltips** -- use shadcn components (Dialog, DropdownMenu, Tooltip, Popover, Sheet). Hand-built versions lack focus trapping, keyboard nav, and portals.
|
|
114
|
+
- **Never nest TooltipProvider** -- mount once at app root. Nesting creates separate delay contexts.
|
|
115
|
+
- **Never hallucinate props** -- shadcn components only have props defined in their local source. Read `components/ui/` if unsure.
|
|
116
|
+
- **Never use `npx shadcn-ui@latest`** -- correct CLI is `npx shadcn@latest`.
|
|
117
|
+
- **Never use Toast** -- deprecated. Use Sonner (`npx shadcn@latest add sonner`).
|
|
118
|
+
|
|
119
|
+
## Component architecture
|
|
120
|
+
|
|
121
|
+
### Directory structure
|
|
122
|
+
|
|
123
|
+
```
|
|
124
|
+
components/
|
|
125
|
+
ui/ # CLI-managed shadcn source -- avoid editing directly
|
|
126
|
+
button.tsx
|
|
127
|
+
dialog.tsx
|
|
128
|
+
...
|
|
129
|
+
app/ # App-specific wrappers and compositions
|
|
130
|
+
confirm-dialog.tsx # wraps Dialog with app-specific logic
|
|
131
|
+
user-avatar.tsx # wraps Avatar with business defaults
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
Create wrappers in `components/app/` for app-specific behavior. Edit `components/ui/` directly only for global style changes.
|
|
135
|
+
|
|
136
|
+
### cn() utility
|
|
137
|
+
|
|
138
|
+
`cn()` from `@/lib/utils` combines `clsx` (conditional classes) and `tailwind-merge` (conflict resolution):
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { cn } from "@/lib/utils";
|
|
142
|
+
|
|
143
|
+
function Card({ className, isActive, ...props }: CardProps) {
|
|
144
|
+
return (
|
|
145
|
+
<div
|
|
146
|
+
className={cn(
|
|
147
|
+
"rounded-lg border bg-card p-6",
|
|
148
|
+
isActive && "border-primary",
|
|
149
|
+
className
|
|
150
|
+
)}
|
|
151
|
+
{...props}
|
|
152
|
+
/>
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### CVA (class-variance-authority)
|
|
158
|
+
|
|
159
|
+
shadcn components use CVA for variants. Export `*Variants` separately to style non-native elements (e.g., `<Link className={cn(buttonVariants({ variant: "outline" }))}>`). Use `VariantProps<typeof xVariants>` for type-safe variant props.
|
|
160
|
+
|
|
161
|
+
## Theming
|
|
162
|
+
|
|
163
|
+
### CSS variables
|
|
164
|
+
|
|
165
|
+
Format depends on Tailwind version:
|
|
166
|
+
|
|
167
|
+
**Tailwind v4 (current):** OKLCH color format, `@theme inline` directive:
|
|
168
|
+
|
|
169
|
+
```css
|
|
170
|
+
@theme inline {
|
|
171
|
+
--color-background: oklch(1 0 0);
|
|
172
|
+
--color-foreground: oklch(0.145 0 0);
|
|
173
|
+
--color-primary: oklch(0.205 0 0);
|
|
174
|
+
--color-primary-foreground: oklch(0.985 0 0);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
**Tailwind v3 (legacy):** HSL format (e.g., `--primary: 222.2 47.4% 11.2%`) in `@layer base`, referenced as `hsl(var(--primary))` in `tailwind.config.ts`.
|
|
179
|
+
|
|
180
|
+
### Dark mode
|
|
181
|
+
|
|
182
|
+
**Tailwind v4:** Use `@custom-variant` with `:where()` (not `:is()`) and include both `.dark` and `.dark *`:
|
|
183
|
+
|
|
184
|
+
```css
|
|
185
|
+
@custom-variant dark (&:where(.dark, .dark *));
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Tailwind v3:** `darkMode: "class"` in `tailwind.config.ts`.
|
|
189
|
+
|
|
190
|
+
Use `next-themes` for the theme provider. Never implement manual dark mode toggling.
|
|
191
|
+
|
|
192
|
+
### Adding custom colors
|
|
193
|
+
|
|
194
|
+
Define the variable in the theme, then reference with the semantic token:
|
|
195
|
+
|
|
196
|
+
```css
|
|
197
|
+
@theme inline {
|
|
198
|
+
--color-brand: oklch(0.6 0.2 250);
|
|
199
|
+
--color-brand-foreground: oklch(0.98 0 0);
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
```tsx
|
|
204
|
+
<div className="bg-brand text-brand-foreground">
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Composition patterns
|
|
208
|
+
|
|
209
|
+
### Compound components and Slot
|
|
210
|
+
|
|
211
|
+
Radix compound components use React context. Each piece must be nested correctly. `asChild` swaps the rendered element for its child via Slot (prop merging):
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// Slot merges Trigger's event handlers and ARIA props onto the Link
|
|
215
|
+
<NavigationMenuLink asChild>
|
|
216
|
+
<Link href="/about">About</Link>
|
|
217
|
+
</NavigationMenuLink>
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### DropdownMenu + Dialog combo
|
|
221
|
+
|
|
222
|
+
Wrap both in a shared parent. Use `e.preventDefault()` in the menu item's `onSelect` to prevent the menu from stealing focus from the dialog. Manage dialog state with `useState`, not `DialogTrigger` — the trigger lives inside the menu, not the dialog.
|
|
223
|
+
|
|
224
|
+
### Sidebar
|
|
225
|
+
|
|
226
|
+
The Sidebar component is complex (30+ sub-components). Minimum viable setup:
|
|
227
|
+
|
|
228
|
+
```tsx
|
|
229
|
+
import { SidebarProvider, SidebarTrigger, SidebarInset } from "@/components/ui/sidebar";
|
|
230
|
+
import { AppSidebar } from "@/components/app-sidebar";
|
|
231
|
+
|
|
232
|
+
export default function Layout({ children }: { children: React.ReactNode }) {
|
|
233
|
+
return (
|
|
234
|
+
<SidebarProvider>
|
|
235
|
+
<AppSidebar />
|
|
236
|
+
<SidebarInset>
|
|
237
|
+
<header>
|
|
238
|
+
<SidebarTrigger />
|
|
239
|
+
</header>
|
|
240
|
+
<main>{children}</main>
|
|
241
|
+
</SidebarInset>
|
|
242
|
+
</SidebarProvider>
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Forms
|
|
248
|
+
|
|
249
|
+
shadcn Form wraps react-hook-form with automatic ARIA wiring. Don't bypass with raw `register()`.
|
|
250
|
+
|
|
251
|
+
### Standard pattern
|
|
252
|
+
|
|
253
|
+
```tsx
|
|
254
|
+
const schema = z.object({
|
|
255
|
+
name: z.string().min(1, "Required"),
|
|
256
|
+
role: z.string().min(1, "Select a role"),
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
function CreateUserForm() {
|
|
260
|
+
const form = useForm<z.infer<typeof schema>>({
|
|
261
|
+
resolver: zodResolver(schema),
|
|
262
|
+
defaultValues: { name: "", role: "" }, // required: avoids controlled/uncontrolled warnings
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Form {...form}>
|
|
267
|
+
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
|
268
|
+
{/* Text input: spread field directly */}
|
|
269
|
+
<FormField control={form.control} name="name" render={({ field }) => (
|
|
270
|
+
<FormItem>
|
|
271
|
+
<FormLabel>Name</FormLabel>
|
|
272
|
+
<FormControl><Input {...field} /></FormControl>
|
|
273
|
+
<FormMessage />
|
|
274
|
+
</FormItem>
|
|
275
|
+
)} />
|
|
276
|
+
|
|
277
|
+
{/* Select: wire onValueChange explicitly, never spread field */}
|
|
278
|
+
<FormField control={form.control} name="role" render={({ field }) => (
|
|
279
|
+
<FormItem>
|
|
280
|
+
<FormLabel>Role</FormLabel>
|
|
281
|
+
<Select onValueChange={field.onChange} value={field.value}>
|
|
282
|
+
<FormControl>
|
|
283
|
+
<SelectTrigger><SelectValue placeholder="Select a role" /></SelectTrigger>
|
|
284
|
+
</FormControl>
|
|
285
|
+
<SelectContent>
|
|
286
|
+
<SelectItem value="admin">Admin</SelectItem>
|
|
287
|
+
<SelectItem value="member">Member</SelectItem>
|
|
288
|
+
</SelectContent>
|
|
289
|
+
</Select>
|
|
290
|
+
<FormMessage />
|
|
291
|
+
</FormItem>
|
|
292
|
+
)} />
|
|
293
|
+
|
|
294
|
+
<Button type="submit">Create</Button>
|
|
295
|
+
</form>
|
|
296
|
+
</Form>
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
Checkbox/Switch: use `checked={field.value} onCheckedChange={field.onChange}` (never spread `{...field}`).
|
|
302
|
+
|
|
303
|
+
### FormField nesting order
|
|
304
|
+
|
|
305
|
+
Always: `FormField > FormItem > FormLabel + FormControl > [input] + FormDescription + FormMessage`. FormControl must wrap exactly one child element (Slot constraint).
|
|
306
|
+
|
|
307
|
+
## Data tables
|
|
308
|
+
|
|
309
|
+
### File structure
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
components/
|
|
313
|
+
users/
|
|
314
|
+
columns.tsx # column definitions (stable reference, outside component)
|
|
315
|
+
data-table.tsx # reusable DataTable shell (pagination, sorting, filtering)
|
|
316
|
+
page.tsx # fetches data, renders <DataTable columns={columns} data={data} />
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
### Column rules
|
|
320
|
+
|
|
321
|
+
Pair column features with table feature configuration:
|
|
322
|
+
|
|
323
|
+
| Column Feature | Required Table Config |
|
|
324
|
+
|---|---|
|
|
325
|
+
| `enableSorting` on column | `getSortedRowModel()` in useReactTable |
|
|
326
|
+
| `enableColumnFilter` | `getFilteredRowModel()` |
|
|
327
|
+
| `cell` with row actions | Column with `id: "actions"`, no `accessorKey` |
|
|
328
|
+
|
|
329
|
+
### Row selection
|
|
330
|
+
|
|
331
|
+
Use `onCheckedChange` (not `onChange`) for the checkbox column:
|
|
332
|
+
|
|
333
|
+
```tsx
|
|
334
|
+
{
|
|
335
|
+
id: "select",
|
|
336
|
+
header: ({ table }) => (
|
|
337
|
+
<Checkbox
|
|
338
|
+
checked={table.getIsAllPageRowsSelected()}
|
|
339
|
+
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
|
340
|
+
/>
|
|
341
|
+
),
|
|
342
|
+
cell: ({ row }) => (
|
|
343
|
+
<Checkbox
|
|
344
|
+
checked={row.getIsSelected()}
|
|
345
|
+
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
|
346
|
+
/>
|
|
347
|
+
),
|
|
348
|
+
}
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Accessibility
|
|
352
|
+
|
|
353
|
+
Radix primitives provide: focus trapping in modals, arrow-key navigation in menus, `aria-expanded`/`aria-controls` on triggers, `role` attributes, Escape to close, screen reader announcements.
|
|
354
|
+
|
|
355
|
+
You handle: `FormLabel` association with inputs (via FormField), visible focus rings (`focus-visible:ring-2`), color contrast (WCAG AA minimum), skip-to-content links, meaningful `alt` text on images, `DialogTitle` and `DialogDescription` in every Dialog (required by Radix, warns if missing).
|
|
356
|
+
|
|
357
|
+
## CLI and configuration
|
|
358
|
+
|
|
359
|
+
### Commands
|
|
360
|
+
|
|
361
|
+
```bash
|
|
362
|
+
npx shadcn@latest init # initialize project, creates components.json
|
|
363
|
+
npx shadcn@latest add button # add a component
|
|
364
|
+
npx shadcn@latest add sonner # add sonner (toast replacement)
|
|
365
|
+
npx shadcn@latest diff button # show upstream changes to a component
|
|
366
|
+
npx shadcn@latest create # scaffold a new registry block (Dec 2025+)
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### components.json
|
|
370
|
+
|
|
371
|
+
Key settings:
|
|
372
|
+
|
|
373
|
+
| Field | Effect |
|
|
374
|
+
|---|---|
|
|
375
|
+
| `style` | `"new-york"` only. `"default"` is deprecated. |
|
|
376
|
+
| `rsc` | `true` adds `"use client"` to components automatically |
|
|
377
|
+
| `aliases.components` | Import path prefix (e.g., `@/components`) |
|
|
378
|
+
| `aliases.utils` | Path to `cn()` utility (e.g., `@/lib/utils`) |
|
|
379
|
+
|
|
380
|
+
### Deprecations and migrations
|
|
381
|
+
|
|
382
|
+
| Deprecated | Replacement |
|
|
383
|
+
|---|---|
|
|
384
|
+
| `shadcn-ui` CLI package | `shadcn` (use `npx shadcn@latest`) |
|
|
385
|
+
| `"default"` style | `"new-york"` style |
|
|
386
|
+
| Toast component | Sonner |
|
|
387
|
+
| `tailwindcss-animate` | `tw-animate-css` |
|
|
388
|
+
| Individual `@radix-ui/react-*` | Unified `radix-ui` package |
|
|
389
|
+
|
|
390
|
+
## Decision tables
|
|
391
|
+
|
|
392
|
+
### Overlay selection
|
|
393
|
+
|
|
394
|
+
| Need | Component | Key Trait |
|
|
395
|
+
|---|---|---|
|
|
396
|
+
| Confirm destructive action | AlertDialog | Blocks interaction, requires explicit response |
|
|
397
|
+
| Form or complex content | Dialog | Focus-trapped modal, closes on overlay click |
|
|
398
|
+
| Side panel (filters, nav, detail) | Sheet | Slides from edge, good for secondary content |
|
|
399
|
+
| Mobile-friendly bottom panel | Drawer | Touch-friendly, swipe to dismiss (uses vaul) |
|
|
400
|
+
| Anchored to trigger, lightweight | Popover | Positioned relative to trigger, no overlay |
|
|
401
|
+
| Brief hint on hover | Tooltip | Hover/focus only, no interactive content |
|
|
402
|
+
| Rich preview on hover | HoverCard | Hover card with delay, supports interactive content |
|
|
403
|
+
| Action list from trigger | DropdownMenu | Click to open, keyboard-navigable menu |
|
|
404
|
+
| Action list from right-click | ContextMenu | Right-click triggered, same API as DropdownMenu |
|
|
405
|
+
|
|
406
|
+
### Selection component
|
|
407
|
+
|
|
408
|
+
| Need | Component |
|
|
409
|
+
|---|---|
|
|
410
|
+
| Fixed list, <10 items | Select |
|
|
411
|
+
| Searchable list | Combobox (popover + command) |
|
|
412
|
+
| Searchable with groups/actions | Command (standalone) |
|
|
413
|
+
| Multi-select from small set | ToggleGroup |
|
|
414
|
+
|
|
415
|
+
### Customization approach
|
|
416
|
+
|
|
417
|
+
| Situation | Approach |
|
|
418
|
+
|---|---|
|
|
419
|
+
| Global style change (border radius, colors) | Edit `components/ui/` directly |
|
|
420
|
+
| App-specific defaults (icon + label combos) | Wrapper in `components/app/` |
|
|
421
|
+
| One-off layout composition | Inline composition in the page/feature |
|
|
422
|
+
|
|
423
|
+
## Testing
|
|
424
|
+
|
|
425
|
+
### Portal rendering
|
|
426
|
+
|
|
427
|
+
Dialog, Popover, Sheet, Select, and DropdownMenu content render into a portal at `document.body`. Query by role, not by DOM hierarchy:
|
|
428
|
+
|
|
429
|
+
```tsx
|
|
430
|
+
// Wrong: content is not inside the trigger's DOM subtree
|
|
431
|
+
const content = within(triggerParent).getByText("Option A");
|
|
432
|
+
|
|
433
|
+
// Correct: query from screen (portaled to body)
|
|
434
|
+
const content = screen.getByRole("option", { name: "Option A" });
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### User events
|
|
438
|
+
|
|
439
|
+
Radix components listen on `pointerdown`, not `click`. Use `userEvent.setup()` (not `fireEvent`):
|
|
440
|
+
|
|
441
|
+
```tsx
|
|
442
|
+
import userEvent from "@testing-library/user-event";
|
|
443
|
+
|
|
444
|
+
it("opens the dialog", async () => {
|
|
445
|
+
const user = userEvent.setup();
|
|
446
|
+
render(<MyDialog />);
|
|
447
|
+
await user.click(screen.getByRole("button", { name: "Open" }));
|
|
448
|
+
expect(screen.getByRole("dialog")).toBeInTheDocument();
|
|
449
|
+
});
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
### JSDOM limitations
|
|
453
|
+
|
|
454
|
+
JSDOM does not implement `pointerdown` or `resize` observers fully. For components that rely on these (Sheet, Drawer, Resizable), test in a real browser environment (Playwright) or mock the specific Radix primitive.
|