@nqlib/nqui 0.5.5 → 0.6.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.
Files changed (53) hide show
  1. package/dist/components/index.d.ts +3 -0
  2. package/dist/components/index.d.ts.map +1 -1
  3. package/dist/components/theme-appearance-menu.d.ts +9 -0
  4. package/dist/components/theme-appearance-menu.d.ts.map +1 -0
  5. package/dist/components/theme-toggle.d.ts +2 -0
  6. package/dist/components/theme-toggle.d.ts.map +1 -0
  7. package/dist/components/ui/checkbox.d.ts.map +1 -1
  8. package/dist/components/ui/toggle-group.d.ts.map +1 -1
  9. package/dist/elevation-debate.html +286 -0
  10. package/dist/nqui.cjs.js +15 -13
  11. package/dist/nqui.es.js +2743 -2638
  12. package/dist/styles.css +169 -270
  13. package/docs/components/README.md +2 -1
  14. package/docs/components/nqui-scroll-area.md +69 -0
  15. package/docs/nqui-skills/AGENT_PROMPT.md +190 -0
  16. package/docs/nqui-skills/COMPONENTS_INDEX.md +51 -1
  17. package/docs/nqui-skills/COMPOSITION.md +321 -0
  18. package/docs/nqui-skills/ELEVATION.md +154 -0
  19. package/docs/nqui-skills/EVAL.md +148 -0
  20. package/docs/nqui-skills/HUMAN_GUIDE.md +18 -0
  21. package/docs/nqui-skills/MIGRATION.md +133 -0
  22. package/docs/nqui-skills/MOTION.md +189 -0
  23. package/docs/nqui-skills/README.md +2 -0
  24. package/docs/nqui-skills/READ_BUDGET.md +60 -0
  25. package/docs/nqui-skills/RECIPES.md +735 -0
  26. package/docs/nqui-skills/SKILL.md +58 -1
  27. package/docs/nqui-skills/STATES.md +154 -0
  28. package/docs/nqui-skills/THEMING.md +203 -0
  29. package/docs/nqui-skills/WRITING.md +205 -0
  30. package/docs/nqui-skills/adapt/SKILL.md +5 -2
  31. package/docs/nqui-skills/animate/SKILL.md +5 -2
  32. package/docs/nqui-skills/audit/SKILL.md +5 -2
  33. package/docs/nqui-skills/bolder/SKILL.md +5 -2
  34. package/docs/nqui-skills/clarify/SKILL.md +5 -2
  35. package/docs/nqui-skills/colorize/SKILL.md +5 -2
  36. package/docs/nqui-skills/delight/SKILL.md +5 -4
  37. package/docs/nqui-skills/distill/SKILL.md +5 -2
  38. package/docs/nqui-skills/impeccable/SKILL.md +0 -16
  39. package/docs/nqui-skills/impeccable/reference/INDEX.md +26 -0
  40. package/docs/nqui-skills/layout/SKILL.md +5 -2
  41. package/docs/nqui-skills/nqui-components/SKILL.md +32 -9
  42. package/docs/nqui-skills/nqui-composition/SKILL.md +148 -0
  43. package/docs/nqui-skills/nqui-data-tables/SKILL.md +127 -0
  44. package/docs/nqui-skills/nqui-design-system/SKILL.md +22 -1
  45. package/docs/nqui-skills/nqui-install/SKILL.md +1 -0
  46. package/docs/nqui-skills/overdrive/SKILL.md +5 -2
  47. package/docs/nqui-skills/polish/SKILL.md +5 -4
  48. package/docs/nqui-skills/quieter/SKILL.md +5 -2
  49. package/docs/nqui-skills/shape/SKILL.md +5 -2
  50. package/docs/nqui-skills/typeset/SKILL.md +5 -2
  51. package/package.json +2 -1
  52. package/scripts/cli.js +2 -0
  53. package/scripts/install-claude-skills.js +109 -0
@@ -0,0 +1,735 @@
1
+ ---
2
+ name: nqui-recipes
3
+ description: Concrete component combinations for common product situations. Use when you know WHAT you want to build but not WHICH components to combine. Each recipe is opinionated — copy the blueprint, then adapt. Pair with nqui-composition/SKILL.md for the philosophy.
4
+ ---
5
+
6
+ # nqui Recipes — Component Combos
7
+
8
+ These are not exhaustive — they are the **canonical answer** for situations that show up on every product. Copy the blueprint, then adapt. If you find yourself reaching outside a recipe, ask whether you genuinely need to or whether you're decorating.
9
+
10
+ Naming convention: a recipe is named for the **user's intent**, not the components used. "Confirm a destructive action" — not "AlertDialog recipe."
11
+
12
+ ---
13
+
14
+ ## Selection & state recipes
15
+
16
+ ### 1. Status pill (tag with semantic meaning)
17
+
18
+ **Intent:** show status that the user reads at a glance — open / in-progress / done, online / offline, paid / overdue.
19
+
20
+ ```tsx
21
+ <Badge variant="secondary">In progress</Badge>
22
+ // or with a colored dot for stronger affordance:
23
+ <div className="inline-flex items-center gap-1.5">
24
+ <span className="size-2 rounded-full bg-emerald-500" />
25
+ <span className="text-xs font-medium">Active</span>
26
+ </div>
27
+ ```
28
+
29
+ **Don't:** use `border-left: 4px solid color` on a card to indicate status. Use a `Badge` or dot.
30
+
31
+ ---
32
+
33
+ ### 2. Priority indicator inside a dense list
34
+
35
+ **Intent:** mark each row in a long list with a priority, without dominating the row.
36
+
37
+ ```tsx
38
+ <Item variant="ghost">
39
+ <ItemMedia>
40
+ <span className={`size-2 rounded-full ${priorityColor}`} />
41
+ </ItemMedia>
42
+ <ItemContent>
43
+ <ItemTitle>Implement OAuth flow</ItemTitle>
44
+ </ItemContent>
45
+ <ItemMedia>
46
+ <Avatar className="size-5"><AvatarFallback>AT</AvatarFallback></Avatar>
47
+ </ItemMedia>
48
+ </Item>
49
+ ```
50
+
51
+ **Key choice:** 8×8 dot > colored Badge for dense lists. A dot encodes a single dimension efficiently; a Badge takes more horizontal space and competes with the title.
52
+
53
+ ---
54
+
55
+ ### 3. Bulk selection with sticky action bar
56
+
57
+ **Intent:** user selects multiple rows, then takes one action on all of them.
58
+
59
+ ```tsx
60
+ // Each row
61
+ <Item variant="ghost">
62
+ <ItemMedia><Checkbox checked={isSelected} onCheckedChange={...} /></ItemMedia>
63
+ <ItemContent>...</ItemContent>
64
+ </Item>
65
+
66
+ // Below the list, sticky action bar appears when selection > 0
67
+ {selectedCount > 0 && (
68
+ <div className="sticky bottom-4 mx-auto flex items-center gap-2 rounded-full bg-foreground text-background px-4 py-2 shadow-lg">
69
+ <span className="text-sm">{selectedCount} selected</span>
70
+ <Separator orientation="vertical" className="h-4 bg-background/30" />
71
+ <Button size="sm" variant="ghost" className="text-background hover:bg-background/10">Archive</Button>
72
+ <Button size="sm" variant="ghost" className="text-background hover:bg-background/10">Move</Button>
73
+ <Button size="sm" variant="ghost" className="text-background hover:bg-background/10" onClick={clearAll}>Cancel</Button>
74
+ </div>
75
+ )}
76
+ ```
77
+
78
+ **Why this combo:** sticky pill at bottom is the Linear / Gmail pattern. It doesn't shift layout, it appears in a fixed location, it shows the count + actions in one strip.
79
+
80
+ ---
81
+
82
+ ## Form recipes
83
+
84
+ ### 4. Settings page (one topic per surface)
85
+
86
+ **Intent:** user changes preferences grouped by theme.
87
+
88
+ ```tsx
89
+ <form>
90
+ <FieldSet>
91
+ <FieldLegend>General</FieldLegend>
92
+ <FieldGroup className="flex flex-col gap-5 max-w-lg">
93
+ <Field>
94
+ <FieldLabel htmlFor="name">Display name</FieldLabel>
95
+ <Input id="name" />
96
+ <FieldDescription>Shown to teammates.</FieldDescription>
97
+ </Field>
98
+ <Field>
99
+ <FieldLabel>Region</FieldLabel>
100
+ <Select>...</Select>
101
+ </Field>
102
+ <Field orientation="horizontal" className="items-center justify-between rounded-lg border p-3">
103
+ <FieldContent>
104
+ <FieldLabel>Email digest</FieldLabel>
105
+ <FieldDescription>Weekly Monday summary.</FieldDescription>
106
+ </FieldContent>
107
+ <Switch />
108
+ </Field>
109
+ </FieldGroup>
110
+ </FieldSet>
111
+ <div className="flex gap-2 pt-6 border-t">
112
+ <Button type="submit">Save</Button>
113
+ <Button type="button" variant="outline">Cancel</Button>
114
+ </div>
115
+ </form>
116
+ ```
117
+
118
+ **Don't:** wrap every field in its own Card. The FieldSet IS the grouping.
119
+
120
+ ---
121
+
122
+ ### 5. Inline edit (click cell → edit in place → save)
123
+
124
+ **Intent:** user edits a value without leaving context (renaming a file, changing a status).
125
+
126
+ ```tsx
127
+ {isEditing ? (
128
+ <InputGroup>
129
+ <InputGroupInput value={draft} onChange={...} autoFocus />
130
+ <InputGroupButton onClick={save}>Save</InputGroupButton>
131
+ <InputGroupButton onClick={cancel} variant="ghost">Cancel</InputGroupButton>
132
+ </InputGroup>
133
+ ) : (
134
+ <button
135
+ className="-mx-1 -my-0.5 rounded px-1 py-0.5 text-left hover:bg-muted/60 focus-visible:bg-muted/60"
136
+ onClick={() => setIsEditing(true)}
137
+ >
138
+ {value}
139
+ </button>
140
+ )}
141
+ ```
142
+
143
+ **Why this combo:** the negative margin + hover background gives a subtle affordance that the text is editable without making it look like an input by default. Apple does this in Finder for filenames.
144
+
145
+ ---
146
+
147
+ ### 6. Form validation with inline error
148
+
149
+ **Intent:** validate as the user moves between fields; show errors without alarming them.
150
+
151
+ ```tsx
152
+ <Field data-invalid={hasError}>
153
+ <FieldLabel htmlFor="email">Work email</FieldLabel>
154
+ <Input id="email" aria-invalid={hasError} aria-describedby="email-error" />
155
+ {hasError && <FieldError id="email-error">Use your company email</FieldError>}
156
+ </Field>
157
+ ```
158
+
159
+ **Don't:** use a global `Alert` at the top of the form for per-field errors. Inline beside the field is faster to fix and lower-anxiety.
160
+
161
+ **Do use a top `Alert`** for server-level errors that don't map to a field: "Couldn't reach the server. Try again."
162
+
163
+ ---
164
+
165
+ ## Navigation & search recipes
166
+
167
+ ### 7. Filter + search + sort toolbar
168
+
169
+ **Intent:** narrow a list of items.
170
+
171
+ ```tsx
172
+ <div className="flex flex-col gap-2 border-b px-4 py-3">
173
+ {/* Search row */}
174
+ <div className="relative">
175
+ <Search01Icon className="absolute left-2.5 top-1/2 -translate-y-1/2 size-3.5 text-muted-foreground" />
176
+ <Input className="pl-8 h-7" placeholder="Search…" />
177
+ </div>
178
+ {/* Filter row in muted container */}
179
+ <div className="rounded-md border border-input/50 bg-muted/30 p-1 flex items-center gap-1">
180
+ <ToggleGroup type="single" value={filter} onValueChange={setFilter} size="sm">
181
+ <ToggleGroupItem value="all">All</ToggleGroupItem>
182
+ <ToggleGroupItem value="open">Open</ToggleGroupItem>
183
+ <ToggleGroupItem value="done">Done</ToggleGroupItem>
184
+ </ToggleGroup>
185
+ <Separator orientation="vertical" className="h-5 mx-1" />
186
+ <Select size="sm">
187
+ <SelectTrigger className="h-6 border-0 shadow-none"><SelectValue /></SelectTrigger>
188
+ <SelectContent>...</SelectContent>
189
+ </Select>
190
+ </div>
191
+ </div>
192
+ ```
193
+
194
+ **Anti-pattern:** putting `ToggleGroup` directly on `bg-background`. It floats and competes with the list below. Always nest in `bg-muted/30` or `bg-muted/40`.
195
+
196
+ ---
197
+
198
+ ### 8. Power-user command palette (Cmd+K)
199
+
200
+ **Intent:** keyboard-driven search across pages and quick actions.
201
+
202
+ ```tsx
203
+ <CommandPalette open={open} onOpenChange={setOpen}>
204
+ <CommandInput placeholder="Search pages and actions…" />
205
+ <CommandList>
206
+ <CommandEmpty>No results.</CommandEmpty>
207
+ <CommandGroup heading="Pages">
208
+ {pages.map(page => (
209
+ <CommandItem key={page.path} onSelect={() => navigate(page.path)}>
210
+ {page.title}
211
+ <CommandShortcut>⌘{page.shortcut}</CommandShortcut>
212
+ </CommandItem>
213
+ ))}
214
+ </CommandGroup>
215
+ <CommandSeparator />
216
+ <CommandGroup heading="Actions">
217
+ <CommandItem onSelect={createNew}>Create new…<CommandShortcut>⌘N</CommandShortcut></CommandItem>
218
+ </CommandGroup>
219
+ </CommandList>
220
+ </CommandPalette>
221
+ ```
222
+
223
+ **Key choice:** CommandShortcut on the right of each item, not as a separate column. The Kbd badge in-row is faster to scan.
224
+
225
+ ---
226
+
227
+ ### 9. Breadcrumb in detail view header
228
+
229
+ **Intent:** show user's location and let them go back one level.
230
+
231
+ ```tsx
232
+ <Breadcrumb>
233
+ <BreadcrumbList>
234
+ <BreadcrumbItem><BreadcrumbLink asChild><Link to="/">Projects</Link></BreadcrumbLink></BreadcrumbItem>
235
+ <BreadcrumbSeparator />
236
+ <BreadcrumbItem><BreadcrumbLink asChild><Link to="/projects/atp">ATP</Link></BreadcrumbLink></BreadcrumbItem>
237
+ <BreadcrumbSeparator />
238
+ <BreadcrumbItem><BreadcrumbPage>Issue #412</BreadcrumbPage></BreadcrumbItem>
239
+ </BreadcrumbList>
240
+ </Breadcrumb>
241
+ ```
242
+
243
+ **Don't:** put a back arrow in addition to the breadcrumb. One affordance, used consistently.
244
+
245
+ ---
246
+
247
+ ## State recipes (loading / empty / error)
248
+
249
+ ### 10. The three states every list needs
250
+
251
+ **Intent:** never show a blank screen.
252
+
253
+ ```tsx
254
+ {loading ? (
255
+ // Skeleton matches the shape of the real content
256
+ <div className="flex flex-col gap-1 p-2">
257
+ {[1,2,3].map(n => (
258
+ <div key={n} className="flex gap-3 px-3 py-2.5">
259
+ <Skeleton className="size-2 rounded-full mt-1" />
260
+ <div className="flex flex-col gap-1.5 flex-1">
261
+ <Skeleton className="h-3 w-16" />
262
+ <Skeleton className="h-4 w-3/4" />
263
+ </div>
264
+ </div>
265
+ ))}
266
+ </div>
267
+ ) : items.length === 0 ? (
268
+ <Empty className="py-12">
269
+ <EmptyHeader>
270
+ <EmptyMedia variant="icon"><FolderIcon /></EmptyMedia>
271
+ <EmptyTitle>No issues yet</EmptyTitle>
272
+ <EmptyDescription>Create your first issue to start tracking.</EmptyDescription>
273
+ </EmptyHeader>
274
+ <EmptyContent><Button>New issue</Button></EmptyContent>
275
+ </Empty>
276
+ ) : error ? (
277
+ <Alert variant="destructive">
278
+ <AlertTitle>Couldn't load issues</AlertTitle>
279
+ <AlertDescription>{error.message}</AlertDescription>
280
+ <AlertAction onClick={retry}>Try again</AlertAction>
281
+ </Alert>
282
+ ) : (
283
+ <ItemGroup>{items.map(...)}</ItemGroup>
284
+ )}
285
+ ```
286
+
287
+ **Don't:** show a generic `Spinner` for list loading. Skeletons set expectations about what's coming.
288
+ **Don't:** use a `Toast` for load errors. The user is looking at the list; the error belongs in the list.
289
+
290
+ ---
291
+
292
+ ### 11. Confirm a destructive action
293
+
294
+ **Intent:** double-check before deleting.
295
+
296
+ ```tsx
297
+ <AlertDialog>
298
+ <AlertDialogTrigger asChild>
299
+ <Button variant="destructive">Delete project</Button>
300
+ </AlertDialogTrigger>
301
+ <AlertDialogContent>
302
+ <AlertDialogHeader>
303
+ <AlertDialogTitle>Delete this project?</AlertDialogTitle>
304
+ <AlertDialogDescription>
305
+ All 412 issues and 1.2 GB of attachments will be removed. This cannot be undone.
306
+ </AlertDialogDescription>
307
+ </AlertDialogHeader>
308
+ <AlertDialogFooter>
309
+ <AlertDialogCancel>Cancel</AlertDialogCancel>
310
+ <AlertDialogAction onClick={confirmDelete}>Delete</AlertDialogAction>
311
+ </AlertDialogFooter>
312
+ </AlertDialogContent>
313
+ </AlertDialog>
314
+ ```
315
+
316
+ **Make the description specific.** "Are you sure?" is useless. State exactly what will be lost.
317
+
318
+ ---
319
+
320
+ ### 12. Optimistic-success toast with undo
321
+
322
+ **Intent:** user takes an action; we apply it immediately and let them undo.
323
+
324
+ ```tsx
325
+ function handleArchive(id) {
326
+ optimisticArchive(id) // updates UI immediately
327
+ toast.success("Issue archived", {
328
+ action: { label: "Undo", onClick: () => optimisticRestore(id) },
329
+ duration: 6000,
330
+ })
331
+ }
332
+ ```
333
+
334
+ **Key choice:** 6-second duration (Gmail standard). Long enough to react, short enough not to linger.
335
+
336
+ ---
337
+
338
+ ## Layout recipes
339
+
340
+ ### 13. Page header with breadcrumb + actions
341
+
342
+ **Intent:** the top of every detail page.
343
+
344
+ ```tsx
345
+ <div className="flex items-start justify-between gap-4 border-b px-6 py-4">
346
+ <div className="flex flex-col gap-2 min-w-0">
347
+ <Breadcrumb>...</Breadcrumb>
348
+ <h1 className="text-xl font-semibold truncate">Page title</h1>
349
+ <p className="text-sm text-muted-foreground">One sentence describing what this page is.</p>
350
+ </div>
351
+ <div className="flex items-center gap-2 shrink-0">
352
+ <Button variant="outline" size="sm">Share</Button>
353
+ <Button size="sm">Primary action</Button>
354
+ </div>
355
+ </div>
356
+ ```
357
+
358
+ **Single source of "where am I" + "what can I do here." Don't repeat the page title anywhere below this header.**
359
+
360
+ ---
361
+
362
+ ### 14. Metric row on a dashboard
363
+
364
+ **Intent:** show 3-4 key numbers at the top of a data page.
365
+
366
+ ```tsx
367
+ <div className="grid grid-cols-2 md:grid-cols-4 gap-4 px-6 py-4">
368
+ {metrics.map(m => (
369
+ <div key={m.label} className="flex flex-col gap-1">
370
+ <span className="text-xs uppercase tracking-wider text-muted-foreground font-semibold">{m.label}</span>
371
+ <span className="text-2xl font-semibold tabular-nums">{m.value}</span>
372
+ {m.delta && (
373
+ <span className={`text-xs ${m.delta > 0 ? 'text-emerald-600' : 'text-red-600'}`}>
374
+ {m.delta > 0 ? '↑' : '↓'} {Math.abs(m.delta)}% vs last week
375
+ </span>
376
+ )}
377
+ </div>
378
+ ))}
379
+ </div>
380
+ ```
381
+
382
+ **Don't:** wrap each metric in a Card. The values ARE the focus — Cards would make them compete with each other and dilute attention.
383
+ **Don't:** put fake numbers (`12,345`). Use real data or skip the section.
384
+
385
+ ---
386
+
387
+ ### 15. Resizable sidebar that remembers width
388
+
389
+ **Intent:** persistent app sidebar the user can resize.
390
+
391
+ ```tsx
392
+ <ResizablePanelGroup direction="horizontal" autoSaveId="app-sidebar">
393
+ <ResizablePanel defaultSize={20} minSize={15} maxSize={35} collapsible>
394
+ <Sidebar>...</Sidebar>
395
+ </ResizablePanel>
396
+ <ResizableHandle withHandle />
397
+ <ResizablePanel defaultSize={80}>
398
+ <Outlet />
399
+ </ResizablePanel>
400
+ </ResizablePanelGroup>
401
+ ```
402
+
403
+ **Note:** `ResizablePanelGroup` requires a parent with bounded width/height. If it's not sizing, see `COMPOSITION.md` → Master-Detail Split → CSS-flex fallback.
404
+
405
+ ---
406
+
407
+ ## Anti-recipes (what NOT to combine)
408
+
409
+ These combinations look reasonable but are wrong. They show up in AI-generated code constantly.
410
+
411
+ | ❌ Anti-combo | ✅ Use instead | Why |
412
+ |--------------|---------------|-----|
413
+ | `Card` + `Card` (nested) | `Card` + `Separator` for sub-sections | Nested cards add visual weight without adding meaning. Flatten. |
414
+ | `RadioGroup` in a horizontal toolbar | `ToggleGroup type="single"` | RadioGroup is for forms (stacked, with descriptions). ToggleGroup is for inline choice. |
415
+ | `Dialog` for a 1-question yes/no | `AlertDialog` if destructive, `Popover` if not | Dialog is for input forms. Anything smaller is a different component. |
416
+ | `Tooltip` as the only label for an icon button | `aria-label` on the button + Tooltip for power users | Tooltips don't exist on touch. The button must be understandable without one. |
417
+ | `Sheet` for a confirmation | `AlertDialog` | Sheets are for content/forms. Confirmations are modal-centered. |
418
+ | `Spinner` for list loading | `Skeleton` matching the shape | Skeletons preview what's coming; spinners just say "wait." |
419
+ | `Toast` for form errors | Inline `FieldError` next to the field | Errors must be co-located with the cause. |
420
+ | `border-left: 4px solid color` as status | `Badge` or colored dot | Side-stripe borders are the #1 AI design tell. |
421
+ | Two `variant="default"` buttons side-by-side | One default + one outline | Two primaries = no primary. |
422
+ | `Dialog` with >5 fields | `Sheet` (side panel) or a dedicated route | Long forms in modals trap users and lose scroll context. |
423
+ | `font-bold` on every label | `text-sm font-medium` for labels, `font-semibold` for headings only | Bold inflation kills hierarchy. |
424
+
425
+ ---
426
+
427
+ ## Auth recipes
428
+
429
+ Auth is the most-requested pattern that the kit must cover well. Get these wrong and consumers feel the whole kit is unreliable.
430
+
431
+ ### 16. Login form (email + password)
432
+
433
+ **Intent:** the canonical sign-in screen.
434
+
435
+ ```tsx
436
+ <div className="flex min-h-screen items-center justify-center px-4">
437
+ <Card className="w-full max-w-sm">
438
+ <CardHeader>
439
+ <CardTitle>Sign in</CardTitle>
440
+ <CardDescription>Continue to your workspace.</CardDescription>
441
+ </CardHeader>
442
+ <CardContent>
443
+ <form onSubmit={handleSubmit}>
444
+ <FieldGroup className="flex flex-col gap-4">
445
+ <Field>
446
+ <FieldLabel htmlFor="email">Work email</FieldLabel>
447
+ <Input id="email" type="email" autoComplete="email" required />
448
+ </Field>
449
+ <Field>
450
+ <div className="flex items-center justify-between">
451
+ <FieldLabel htmlFor="password">Password</FieldLabel>
452
+ <Link to="/reset" className="text-xs text-muted-foreground hover:underline">Forgot?</Link>
453
+ </div>
454
+ <Input id="password" type="password" autoComplete="current-password" required />
455
+ </Field>
456
+ {error && <FieldError>{error}</FieldError>}
457
+ </FieldGroup>
458
+ <Button type="submit" disabled={isPending} className="w-full mt-6">
459
+ {isPending ? <><Spinner className="size-4" />Signing in…</> : "Sign in"}
460
+ </Button>
461
+ </form>
462
+ </CardContent>
463
+ <CardFooter className="flex justify-center text-xs text-muted-foreground">
464
+ Don't have an account? <Link to="/signup" className="ml-1 text-foreground hover:underline">Create one</Link>
465
+ </CardFooter>
466
+ </Card>
467
+ </div>
468
+ ```
469
+
470
+ **Rules:**
471
+ - Single primary action ("Sign in") with loading state. Never "Submit".
472
+ - "Forgot?" link inline with the password label, not below the form.
473
+ - Errors: specific ("Incorrect email or password.") not generic ("Auth failed").
474
+ - `autoComplete` attributes mandatory — password managers depend on them.
475
+ - Single card, centered. Not a sidebar+content layout for a sign-in screen.
476
+
477
+ ### 17. Signup (OAuth + email)
478
+
479
+ **Intent:** offer one or two OAuth providers + email signup.
480
+
481
+ ```tsx
482
+ <Card className="w-full max-w-sm">
483
+ <CardHeader>
484
+ <CardTitle>Create your account</CardTitle>
485
+ </CardHeader>
486
+ <CardContent className="flex flex-col gap-4">
487
+ {/* OAuth row */}
488
+ <div className="flex flex-col gap-2">
489
+ <Button variant="outline" onClick={() => signInWith("google")} className="w-full">
490
+ <GoogleIcon /> Continue with Google
491
+ </Button>
492
+ <Button variant="outline" onClick={() => signInWith("github")} className="w-full">
493
+ <GithubIcon /> Continue with GitHub
494
+ </Button>
495
+ </div>
496
+
497
+ {/* Divider */}
498
+ <div className="relative my-2">
499
+ <Separator />
500
+ <span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 bg-card px-2 text-[10px] uppercase tracking-wider text-muted-foreground">
501
+ or with email
502
+ </span>
503
+ </div>
504
+
505
+ {/* Email form */}
506
+ <form onSubmit={handleSubmit}>
507
+ <FieldGroup className="flex flex-col gap-4">
508
+ <Field>
509
+ <FieldLabel htmlFor="email">Work email</FieldLabel>
510
+ <Input id="email" type="email" autoComplete="email" required />
511
+ </Field>
512
+ <Field>
513
+ <FieldLabel htmlFor="password">Password</FieldLabel>
514
+ <Input id="password" type="password" autoComplete="new-password" required />
515
+ <FieldDescription>At least 8 characters.</FieldDescription>
516
+ </Field>
517
+ </FieldGroup>
518
+ <Button type="submit" className="w-full mt-6">Create account</Button>
519
+ </form>
520
+
521
+ <p className="text-[11px] text-center text-muted-foreground mt-2">
522
+ By creating an account you agree to our{" "}
523
+ <Link to="/terms" className="text-foreground hover:underline">Terms</Link>{" "}
524
+ and <Link to="/privacy" className="text-foreground hover:underline">Privacy Policy</Link>.
525
+ </p>
526
+ </CardContent>
527
+ </Card>
528
+ ```
529
+
530
+ **Rules:**
531
+ - OAuth buttons are `variant="outline"` (secondary visual weight — choice, not push).
532
+ - "Continue with X" not "Sign in with X" — works for both signup and login.
533
+ - Divider uses a centered label, not just a horizontal line.
534
+ - Terms/Privacy in muted small text below the form, not as a checkbox above. (Implicit consent on signup is industry standard for SaaS; require explicit checkbox only if you have a legal reason.)
535
+ - `autoComplete="new-password"` on signup password field (different from login).
536
+
537
+ ### 18. Password reset (two-step flow)
538
+
539
+ **Intent:** request reset, then enter new password.
540
+
541
+ ```tsx
542
+ // Step 1: request reset
543
+ <Card className="w-full max-w-sm">
544
+ <CardHeader>
545
+ <CardTitle>Reset your password</CardTitle>
546
+ <CardDescription>We'll email you a link to set a new password.</CardDescription>
547
+ </CardHeader>
548
+ <CardContent>
549
+ <form>
550
+ <FieldGroup>
551
+ <Field>
552
+ <FieldLabel htmlFor="email">Email</FieldLabel>
553
+ <Input id="email" type="email" required />
554
+ </Field>
555
+ </FieldGroup>
556
+ <Button type="submit" className="w-full mt-6">Send reset link</Button>
557
+ </form>
558
+ </CardContent>
559
+ </Card>
560
+
561
+ // Step 2: success state (after submit)
562
+ <Card className="w-full max-w-sm">
563
+ <CardContent className="flex flex-col items-center text-center gap-3 py-8">
564
+ <div className="size-10 rounded-full bg-muted flex items-center justify-center">
565
+ <CheckIcon className="size-5 text-foreground" />
566
+ </div>
567
+ <div className="flex flex-col gap-1">
568
+ <p className="text-base font-semibold">Check your email</p>
569
+ <p className="text-sm text-muted-foreground">
570
+ We sent a reset link to <span className="text-foreground">{email}</span>.
571
+ It expires in 1 hour.
572
+ </p>
573
+ </div>
574
+ <Button variant="outline" size="sm" asChild>
575
+ <Link to="/login">Back to sign in</Link>
576
+ </Button>
577
+ </CardContent>
578
+ </Card>
579
+ ```
580
+
581
+ **Rules:**
582
+ - ALWAYS show the success state on submit, even if the email doesn't exist. (Security: don't leak whether an email is registered.)
583
+ - Include the email address in the success message — confirms what the user submitted.
584
+ - Mention expiration window — manages expectations, reduces support tickets.
585
+ - "Back to sign in" link in case they came here by mistake.
586
+
587
+ ### 19. Magic link (passwordless)
588
+
589
+ **Intent:** email link, no password ever.
590
+
591
+ ```tsx
592
+ <Card className="w-full max-w-sm">
593
+ <CardHeader>
594
+ <CardTitle>Sign in to Acme</CardTitle>
595
+ <CardDescription>Enter your email — we'll send a sign-in link.</CardDescription>
596
+ </CardHeader>
597
+ <CardContent>
598
+ <form onSubmit={handleSubmit}>
599
+ <FieldGroup>
600
+ <Field>
601
+ <FieldLabel htmlFor="email">Work email</FieldLabel>
602
+ <Input id="email" type="email" autoComplete="email" required />
603
+ </Field>
604
+ </FieldGroup>
605
+ <Button type="submit" disabled={isPending} className="w-full mt-6">
606
+ {isPending ? "Sending…" : "Send sign-in link"}
607
+ </Button>
608
+ </form>
609
+ <Separator className="my-6" />
610
+ <Button variant="ghost" size="sm" className="w-full" asChild>
611
+ <Link to="/sso"><Building2Icon className="size-4" /> Sign in with SSO</Link>
612
+ </Button>
613
+ </CardContent>
614
+ </Card>
615
+ ```
616
+
617
+ **Rules:**
618
+ - Same security rule as password reset: show success regardless of whether the email is registered.
619
+ - SSO link below a separator if the product supports both — don't put it inline competing with the primary email flow.
620
+
621
+ ### 20. OTP / 2FA code entry
622
+
623
+ **Intent:** 6-digit verification code (post-login, post-magic-link, or post-signup).
624
+
625
+ ```tsx
626
+ <Card className="w-full max-w-sm">
627
+ <CardHeader>
628
+ <CardTitle>Enter verification code</CardTitle>
629
+ <CardDescription>
630
+ We sent a 6-digit code to <span className="text-foreground">{email}</span>.
631
+ </CardDescription>
632
+ </CardHeader>
633
+ <CardContent className="flex flex-col gap-4">
634
+ <InputOTP maxLength={6} value={code} onChange={setCode}>
635
+ <InputOTPGroup>
636
+ <InputOTPSlot index={0} />
637
+ <InputOTPSlot index={1} />
638
+ <InputOTPSlot index={2} />
639
+ <InputOTPSlot index={3} />
640
+ <InputOTPSlot index={4} />
641
+ <InputOTPSlot index={5} />
642
+ </InputOTPGroup>
643
+ </InputOTP>
644
+
645
+ {error && <FieldError>{error}</FieldError>}
646
+
647
+ <Button onClick={verify} disabled={code.length < 6 || isPending} className="w-full">
648
+ {isPending ? "Verifying…" : "Verify"}
649
+ </Button>
650
+
651
+ <div className="flex items-center justify-between text-xs text-muted-foreground">
652
+ <span>Code expires in {timeLeft}</span>
653
+ <button onClick={resend} disabled={!canResend} className="text-foreground hover:underline disabled:text-muted-foreground disabled:no-underline">
654
+ Resend code
655
+ </button>
656
+ </div>
657
+ </CardContent>
658
+ </Card>
659
+ ```
660
+
661
+ **Rules:**
662
+ - Auto-submit when 6 digits entered — don't make the user click Verify after typing the last digit.
663
+ - Show the email so users know which inbox to check.
664
+ - "Resend code" rate-limited (30-60s cooldown). Show countdown, then enable.
665
+ - Errors are specific: "Incorrect code." not "Verification failed."
666
+
667
+ ### 21. Email verification banner (post-signup)
668
+
669
+ **Intent:** unobtrusive reminder for unverified accounts.
670
+
671
+ ```tsx
672
+ {!user.emailVerified && (
673
+ <Alert>
674
+ <AlertTitle>Verify your email</AlertTitle>
675
+ <AlertDescription>
676
+ Check <span className="text-foreground">{user.email}</span> for a verification link.
677
+ {" "}
678
+ <button onClick={resend} className="text-foreground hover:underline">
679
+ Resend
680
+ </button>
681
+ </AlertDescription>
682
+ </Alert>
683
+ )}
684
+ ```
685
+
686
+ **Rules:**
687
+ - Use `Alert` (informational), not a modal — don't block the user.
688
+ - Place at the top of the app shell, above main content.
689
+ - Resend link inline in the description, not as a separate button.
690
+ - Don't dismiss it permanently — let it persist until verified.
691
+
692
+ ### 22. SSO landing (enterprise)
693
+
694
+ **Intent:** workspace/email entry → redirect to SSO provider.
695
+
696
+ ```tsx
697
+ <Card className="w-full max-w-sm">
698
+ <CardHeader>
699
+ <CardTitle>Sign in with SSO</CardTitle>
700
+ <CardDescription>Enter your work email to find your organization.</CardDescription>
701
+ </CardHeader>
702
+ <CardContent>
703
+ <form onSubmit={lookup}>
704
+ <FieldGroup>
705
+ <Field>
706
+ <FieldLabel htmlFor="sso-email">Work email</FieldLabel>
707
+ <Input id="sso-email" type="email" required />
708
+ <FieldDescription>We'll redirect you to your SSO provider.</FieldDescription>
709
+ </Field>
710
+ </FieldGroup>
711
+ <Button type="submit" className="w-full mt-6">Continue</Button>
712
+ </form>
713
+ </CardContent>
714
+ <CardFooter className="text-xs text-muted-foreground justify-center">
715
+ <Link to="/login" className="hover:underline">Use email and password instead</Link>
716
+ </CardFooter>
717
+ </Card>
718
+ ```
719
+
720
+ **Rules:**
721
+ - Hide the actual SSO provider from the user — they enter email, the system figures out the provider from the domain.
722
+ - Always offer an escape hatch ("use password instead") in case SSO is misconfigured or the user is on a personal account.
723
+
724
+ ---
725
+
726
+ ## Combo synthesis — how to think about new ones
727
+
728
+ When you face a situation not listed here, ask:
729
+
730
+ 1. **What is the user's verb?** (browse, decide, create, confirm, monitor, configure)
731
+ 2. **Is it inline (in flow) or modal (interrupts flow)?**
732
+ 3. **Does it need depth?** (overlays, drawers) or **breadth?** (always visible)
733
+ 4. **What's the smallest set of components that achieves it without decoration?**
734
+
735
+ Then check `COMPONENTS_INDEX.md` decision trees, pick the components, and verify against this file's anti-recipes before writing JSX.