ui-mirror-skill 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.
@@ -0,0 +1,758 @@
1
+ # UI Mirror: Component Catalog (Reference Example)
2
+
3
+ > **NOTE**: This catalog contains reference shadcn/ui component implementations as comparison baselines.
4
+ > When using this skill, read the actual component source files from the current project.
5
+ > Each entry includes a typical implementation, key CSS properties for comparison, and migration override templates.
6
+
7
+ ---
8
+
9
+ ## 1. Card
10
+
11
+ ### shadcn/ui Reference
12
+
13
+ ```tsx
14
+ // src/components/ui/card.tsx
15
+ import { cn } from "@/lib/utils"
16
+
17
+ function Card({ className, ...props }: React.ComponentProps<"div">) {
18
+ return (
19
+ <div
20
+ data-slot="card"
21
+ className={cn(
22
+ "bg-card text-card-foreground flex flex-col gap-2 rounded-lg border shadow-sm",
23
+ className
24
+ )}
25
+ {...props}
26
+ />
27
+ )
28
+ }
29
+
30
+ function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
31
+ return (
32
+ <div
33
+ data-slot="card-header"
34
+ className={cn(
35
+ "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-4 pt-4 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-4",
36
+ className
37
+ )}
38
+ {...props}
39
+ />
40
+ )
41
+ }
42
+
43
+ function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
44
+ return (
45
+ <div
46
+ data-slot="card-title"
47
+ className={cn("leading-none font-semibold", className)}
48
+ {...props}
49
+ />
50
+ )
51
+ }
52
+
53
+ function CardDescription({ className, ...props }: React.ComponentProps<"div">) {
54
+ return (
55
+ <div
56
+ data-slot="card-description"
57
+ className={cn("text-muted-foreground text-sm", className)}
58
+ {...props}
59
+ />
60
+ )
61
+ }
62
+
63
+ function CardContent({ className, ...props }: React.ComponentProps<"div">) {
64
+ return (
65
+ <div
66
+ data-slot="card-content"
67
+ className={cn("px-4 pb-4", className)}
68
+ {...props}
69
+ />
70
+ )
71
+ }
72
+
73
+ function CardFooter({ className, ...props }: React.ComponentProps<"div">) {
74
+ return (
75
+ <div
76
+ data-slot="card-footer"
77
+ className={cn("flex items-center px-4 pb-4 [.border-t]:pt-4", className)}
78
+ {...props}
79
+ />
80
+ )
81
+ }
82
+ ```
83
+
84
+ ### Key CSS Properties for Comparison
85
+
86
+ | Property | Default Value | What to Compare |
87
+ |----------|-------------|-----------------|
88
+ | `background` | `bg-card` (white) | Surface color |
89
+ | `border` | `border` (1px solid --border) | Border presence and color |
90
+ | `border-radius` | `rounded-lg` (--radius = 6px) | Corner rounding |
91
+ | `box-shadow` | `shadow-sm` | Shadow depth |
92
+ | `padding` (header) | `px-4 pt-4` (16px sides, 16px top) | Internal spacing |
93
+ | `padding` (content) | `px-4 pb-4` (16px sides, 16px bottom) | Internal spacing |
94
+ | `gap` (internal) | `gap-2` (8px) | Vertical spacing between sub-elements |
95
+ | `color` | `text-card-foreground` (near-black) | Text color |
96
+
97
+ ### Migration Override Template
98
+
99
+ ```css
100
+ /* Override card styling to match benchmark */
101
+ @layer components {
102
+ [data-slot="card"] {
103
+ --card-radius: {{benchmark_radius}};
104
+ --card-shadow: {{benchmark_shadow}};
105
+ --card-border: {{benchmark_border}};
106
+ border-radius: var(--card-radius);
107
+ box-shadow: var(--card-shadow);
108
+ border: var(--card-border);
109
+ }
110
+ [data-slot="card-header"] {
111
+ padding: {{benchmark_header_padding}};
112
+ }
113
+ [data-slot="card-content"] {
114
+ padding: {{benchmark_content_padding}};
115
+ }
116
+ }
117
+ ```
118
+
119
+ ---
120
+
121
+ ## 2. Button
122
+
123
+ ### shadcn/ui Reference
124
+
125
+ ```tsx
126
+ // src/components/ui/button.tsx
127
+ import { cva, type VariantProps } from "class-variance-authority"
128
+ import { cn } from "@/lib/utils"
129
+
130
+ const buttonVariants = cva(
131
+ "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
132
+ {
133
+ variants: {
134
+ variant: {
135
+ default:
136
+ "bg-primary text-primary-foreground shadow-md shadow-primary/20 hover:bg-primary/90",
137
+ destructive:
138
+ "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20",
139
+ outline:
140
+ "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground",
141
+ secondary:
142
+ "bg-secondary text-secondary-foreground hover:bg-secondary/80",
143
+ ghost:
144
+ "hover:bg-accent hover:text-accent-foreground",
145
+ link:
146
+ "text-primary underline-offset-4 hover:underline",
147
+ },
148
+ size: {
149
+ default: "h-9 px-4 py-2 has-[>svg]:px-3",
150
+ xs: "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
151
+ sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
152
+ lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
153
+ icon: "size-9",
154
+ "icon-xs": "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
155
+ "icon-sm": "size-8",
156
+ "icon-lg": "size-10",
157
+ },
158
+ },
159
+ defaultVariants: {
160
+ variant: "default",
161
+ size: "default",
162
+ },
163
+ }
164
+ )
165
+ ```
166
+
167
+ ### Key CSS Properties for Comparison
168
+
169
+ | Variant | Property | Default Value |
170
+ |---------|----------|-------------|
171
+ | **Base** | `border-radius` | `rounded-md` (~4px) |
172
+ | **Base** | `font-size` | `text-sm` (14px) |
173
+ | **Base** | `font-weight` | `font-medium` (500) |
174
+ | **Base** | `height` | `h-9` (36px) |
175
+ | **Base** | `padding` | `px-4 py-2` |
176
+ | **Base** | `transition` | `transition-all` |
177
+ | **Base** | `focus ring` | `ring-ring/50 ring-[3px]` |
178
+ | **default** | `background` | `bg-primary` (#6866F1) |
179
+ | **default** | `color` | `text-primary-foreground` (white) |
180
+ | **default** | `box-shadow` | `shadow-md shadow-primary/20` |
181
+ | **default** | `hover` | `bg-primary/90` |
182
+ | **destructive** | `background` | `bg-destructive` (red) |
183
+ | **outline** | `border` | `border` (1px) |
184
+ | **outline** | `background` | `bg-background` |
185
+ | **outline** | `box-shadow` | `shadow-xs` |
186
+ | **secondary** | `background` | `bg-secondary` |
187
+ | **ghost** | `background` | transparent |
188
+ | **ghost** | `hover` | `bg-accent` |
189
+
190
+ ### Size Variants
191
+
192
+ | Size | Height | Padding | Icon Size |
193
+ |------|--------|---------|-----------|
194
+ | `xs` | 24px (h-6) | px-2 | size-3 (12px) |
195
+ | `sm` | 32px (h-8) | px-3 | size-4 (16px) |
196
+ | `default` | 36px (h-9) | px-4 | size-4 (16px) |
197
+ | `lg` | 40px (h-10) | px-6 | size-4 (16px) |
198
+ | `icon` | 36px (size-9) | -- | size-4 |
199
+ | `icon-xs` | 24px (size-6) | -- | size-3 |
200
+ | `icon-sm` | 32px (size-8) | -- | size-4 |
201
+ | `icon-lg` | 40px (size-10) | -- | size-4 |
202
+
203
+ ### Migration Override Template
204
+
205
+ ```css
206
+ @layer components {
207
+ [data-slot="button"] {
208
+ border-radius: {{benchmark_radius}};
209
+ font-size: {{benchmark_font_size}};
210
+ font-weight: {{benchmark_font_weight}};
211
+ }
212
+ [data-slot="button"][data-variant="default"] {
213
+ box-shadow: {{benchmark_shadow}};
214
+ }
215
+ [data-slot="button"][data-size="default"] {
216
+ height: {{benchmark_height}};
217
+ padding-inline: {{benchmark_padding_x}};
218
+ }
219
+ }
220
+ ```
221
+
222
+ ---
223
+
224
+ ## 3. Input
225
+
226
+ ### shadcn/ui Reference
227
+
228
+ ```tsx
229
+ // src/components/ui/input.tsx
230
+ function Input({ className, type, ...props }: React.ComponentProps<"input">) {
231
+ return (
232
+ <input
233
+ type={type}
234
+ data-slot="input"
235
+ className={cn(
236
+ "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-sm transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
237
+ "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
238
+ "aria-invalid:ring-destructive/20 aria-invalid:border-destructive",
239
+ className
240
+ )}
241
+ {...props}
242
+ />
243
+ )
244
+ }
245
+ ```
246
+
247
+ ### Key CSS Properties for Comparison
248
+
249
+ | Property | Default Value |
250
+ |----------|-------------|
251
+ | `height` | `h-9` (36px) |
252
+ | `border-radius` | `rounded-md` (~4px) |
253
+ | `border` | `border-input` (1px solid) |
254
+ | `box-shadow` | `shadow-sm` |
255
+ | `padding` | `px-3 py-1` |
256
+ | `font-size` | `text-base` (16px mobile) / `md:text-sm` (14px desktop) |
257
+ | `background` | `bg-transparent` |
258
+ | `placeholder` | `text-muted-foreground` |
259
+ | `focus` | `border-ring` + `ring-ring/50 ring-[3px]` |
260
+ | `disabled` | `opacity-50` + `pointer-events-none` |
261
+ | `error` | `ring-destructive/20` + `border-destructive` |
262
+
263
+ ### Migration Override Template
264
+
265
+ ```css
266
+ @layer components {
267
+ [data-slot="input"] {
268
+ height: {{benchmark_height}};
269
+ border-radius: {{benchmark_radius}};
270
+ border-color: {{benchmark_border_color}};
271
+ box-shadow: {{benchmark_shadow}};
272
+ padding: {{benchmark_padding}};
273
+ font-size: {{benchmark_font_size}};
274
+ background: {{benchmark_bg}};
275
+ }
276
+ [data-slot="input"]:focus-visible {
277
+ border-color: {{benchmark_focus_border}};
278
+ box-shadow: {{benchmark_focus_shadow}};
279
+ }
280
+ }
281
+ ```
282
+
283
+ ---
284
+
285
+ ## 4. Badge
286
+
287
+ ### shadcn/ui Reference
288
+
289
+ ```tsx
290
+ // src/components/ui/badge.tsx
291
+ const badgeVariants = cva(
292
+ "inline-flex items-center justify-center rounded-full border border-transparent px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
293
+ {
294
+ variants: {
295
+ variant: {
296
+ default: "bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
297
+ secondary: "bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
298
+ destructive: "bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20",
299
+ outline: "border-border text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
300
+ ghost: "[a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
301
+ link: "text-primary underline-offset-4 [a&]:hover:underline",
302
+ },
303
+ },
304
+ defaultVariants: { variant: "default" },
305
+ }
306
+ )
307
+ ```
308
+
309
+ ### Key CSS Properties for Comparison
310
+
311
+ | Property | Default Value |
312
+ |----------|-------------|
313
+ | `border-radius` | `rounded-full` (pill shape) |
314
+ | `padding` | `px-2 py-0.5` |
315
+ | `font-size` | `text-xs` (12px) |
316
+ | `font-weight` | `font-medium` (500) |
317
+ | `border` | `border border-transparent` (default) / `border-border` (outline) |
318
+ | `icon size` | `size-3` (12px) |
319
+ | `gap` | `gap-1` (4px) |
320
+
321
+ ### Variants Summary
322
+
323
+ | Variant | Background | Text | Border |
324
+ |---------|-----------|------|--------|
325
+ | `default` | `bg-primary` | `text-primary-foreground` | transparent |
326
+ | `secondary` | `bg-secondary` | `text-secondary-foreground` | transparent |
327
+ | `destructive` | `bg-destructive` | `text-white` | transparent |
328
+ | `outline` | transparent | `text-foreground` | `border-border` |
329
+ | `ghost` | transparent | inherit | transparent |
330
+ | `link` | transparent | `text-primary` | transparent |
331
+
332
+ ### Migration Override Template
333
+
334
+ ```css
335
+ @layer components {
336
+ [data-slot="badge"] {
337
+ border-radius: {{benchmark_radius}};
338
+ padding: {{benchmark_padding}};
339
+ font-size: {{benchmark_font_size}};
340
+ font-weight: {{benchmark_font_weight}};
341
+ }
342
+ [data-slot="badge"][data-variant="default"] {
343
+ background: {{benchmark_default_bg}};
344
+ color: {{benchmark_default_text}};
345
+ }
346
+ }
347
+ ```
348
+
349
+ ---
350
+
351
+ ## 5. Dialog
352
+
353
+ ### shadcn/ui Reference
354
+
355
+ ```tsx
356
+ // src/components/ui/dialog.tsx — DialogContent (key component)
357
+ function DialogContent({ className, children, showCloseButton = true, ...props }) {
358
+ return (
359
+ <DialogPortal>
360
+ <DialogOverlay
361
+ className="data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50"
362
+ />
363
+ <DialogPrimitive.Content
364
+ data-slot="dialog-content"
365
+ className={cn(
366
+ "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
367
+ className
368
+ )}
369
+ {...props}
370
+ >
371
+ {children}
372
+ {showCloseButton && <CloseButton />}
373
+ </DialogPrimitive.Content>
374
+ </DialogPortal>
375
+ )
376
+ }
377
+ ```
378
+
379
+ ### Key CSS Properties for Comparison
380
+
381
+ | Property | Default Value |
382
+ |----------|-------------|
383
+ | `background` | `bg-background` |
384
+ | `border-radius` | `rounded-lg` (6px) |
385
+ | `border` | `border` (1px solid --border) |
386
+ | `box-shadow` | `shadow-lg` |
387
+ | `padding` | `p-6` (24px) |
388
+ | `max-width` | `max-w-[calc(100%-2rem)]` mobile / `sm:max-w-lg` (512px) desktop |
389
+ | `gap` (internal) | `gap-4` (16px) |
390
+ | `z-index` | `z-50` |
391
+ | **Overlay** | `bg-black/50` (50% opacity) |
392
+ | **Enter animation** | `zoom-in-95` + `fade-in-0` |
393
+ | **Exit animation** | `zoom-out-95` + `fade-out-0` |
394
+ | **Duration** | `duration-200` |
395
+
396
+ ### Sub-components
397
+
398
+ | Slot | Key Classes |
399
+ |------|------------|
400
+ | `dialog-header` | `flex flex-col gap-2 text-center sm:text-left` |
401
+ | `dialog-footer` | `flex flex-col-reverse gap-2 sm:flex-row sm:justify-end` |
402
+ | `dialog-title` | `text-lg leading-none font-semibold` |
403
+ | `dialog-description` | `text-muted-foreground text-sm` |
404
+
405
+ ### Migration Override Template
406
+
407
+ ```css
408
+ @layer components {
409
+ [data-slot="dialog-overlay"] {
410
+ background: {{benchmark_overlay_color}};
411
+ }
412
+ [data-slot="dialog-content"] {
413
+ border-radius: {{benchmark_radius}};
414
+ box-shadow: {{benchmark_shadow}};
415
+ padding: {{benchmark_padding}};
416
+ max-width: {{benchmark_max_width}};
417
+ }
418
+ [data-slot="dialog-title"] {
419
+ font-size: {{benchmark_title_size}};
420
+ font-weight: {{benchmark_title_weight}};
421
+ }
422
+ }
423
+ ```
424
+
425
+ ---
426
+
427
+ ## 6. Navigation (Sidebar)
428
+
429
+ ### shadcn/ui Reference
430
+
431
+ ```tsx
432
+ // src/components/ui/sidebar.tsx — key constants and patterns
433
+ const SIDEBAR_WIDTH = "16rem" // 256px
434
+ const SIDEBAR_WIDTH_MOBILE = "18rem" // 288px
435
+ const SIDEBAR_WIDTH_ICON = "3rem" // 48px collapsed
436
+
437
+ // SidebarMenuButton CVA
438
+ const sidebarMenuButtonVariants = cva(
439
+ "peer/menu-button flex w-full items-center gap-2 overflow-hidden rounded-md p-2 text-left text-sm outline-none ring-sidebar-ring transition-[width,height,padding] hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[active=true]:bg-sidebar-accent data-[active=true]:font-medium data-[active=true]:text-sidebar-accent-foreground ...",
440
+ {
441
+ variants: {
442
+ variant: {
443
+ default: "hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
444
+ outline: "bg-background shadow-[0_0_0_1px_hsl(var(--sidebar-border))] ...",
445
+ },
446
+ size: {
447
+ default: "h-8 text-sm",
448
+ sm: "h-7 text-xs",
449
+ lg: "h-12 text-sm group-data-[collapsible=icon]:!p-0",
450
+ },
451
+ },
452
+ }
453
+ )
454
+ ```
455
+
456
+ ### Key CSS Properties for Comparison
457
+
458
+ | Property | Default Value |
459
+ |----------|-------------|
460
+ | `width` | 16rem (256px) desktop, 18rem mobile |
461
+ | `background` | `bg-sidebar` (white) |
462
+ | `position` | `fixed inset-y-0` (desktop), Sheet (mobile) |
463
+ | `border` | `border-r` (left sidebar) |
464
+ | **Menu item** | |
465
+ | `height` | `h-8` (32px) default |
466
+ | `padding` | `p-2` (8px) |
467
+ | `border-radius` | `rounded-md` |
468
+ | `font-size` | `text-sm` (14px) |
469
+ | `hover` | `bg-sidebar-accent text-sidebar-accent-foreground` |
470
+ | `active` | `bg-sidebar-accent font-medium` |
471
+ | `icon size` | `size-4` (16px) |
472
+ | `gap` | `gap-2` (8px) |
473
+
474
+ ### Structure
475
+
476
+ ```
477
+ SidebarProvider
478
+ Sidebar (side="left" | "right", variant="sidebar" | "floating" | "inset")
479
+ SidebarHeader
480
+ SidebarContent
481
+ SidebarGroup
482
+ SidebarGroupLabel
483
+ SidebarGroupContent
484
+ SidebarMenu
485
+ SidebarMenuItem
486
+ SidebarMenuButton (isActive, tooltip)
487
+ SidebarMenuAction (showOnHover)
488
+ SidebarMenuBadge
489
+ SidebarMenuSub
490
+ SidebarMenuSubItem
491
+ SidebarMenuSubButton
492
+ SidebarFooter
493
+ SidebarRail
494
+ SidebarInset (main content area)
495
+ ```
496
+
497
+ ### Migration Override Template
498
+
499
+ ```css
500
+ @layer components {
501
+ [data-sidebar="sidebar"] {
502
+ background: {{benchmark_sidebar_bg}};
503
+ width: {{benchmark_sidebar_width}};
504
+ }
505
+ [data-sidebar="menu-button"] {
506
+ height: {{benchmark_item_height}};
507
+ padding: {{benchmark_item_padding}};
508
+ border-radius: {{benchmark_item_radius}};
509
+ font-size: {{benchmark_item_font_size}};
510
+ }
511
+ [data-sidebar="menu-button"]:hover {
512
+ background: {{benchmark_item_hover_bg}};
513
+ }
514
+ [data-sidebar="menu-button"][data-active="true"] {
515
+ background: {{benchmark_item_active_bg}};
516
+ font-weight: {{benchmark_item_active_weight}};
517
+ }
518
+ }
519
+ ```
520
+
521
+ ---
522
+
523
+ ## 7. Table
524
+
525
+ ### shadcn/ui Reference
526
+
527
+ ```tsx
528
+ // src/components/ui/table.tsx
529
+ function Table({ className, ...props }) {
530
+ return (
531
+ <div data-slot="table-container" className="relative w-full overflow-x-auto">
532
+ <table data-slot="table" className={cn("w-full caption-bottom text-sm", className)} {...props} />
533
+ </div>
534
+ )
535
+ }
536
+
537
+ function TableRow({ className, ...props }) {
538
+ return (
539
+ <tr
540
+ data-slot="table-row"
541
+ className={cn("hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors", className)}
542
+ {...props}
543
+ />
544
+ )
545
+ }
546
+
547
+ function TableHead({ className, ...props }) {
548
+ return (
549
+ <th
550
+ data-slot="table-head"
551
+ className={cn("text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap ...", className)}
552
+ {...props}
553
+ />
554
+ )
555
+ }
556
+
557
+ function TableCell({ className, ...props }) {
558
+ return (
559
+ <td
560
+ data-slot="table-cell"
561
+ className={cn("p-2 align-middle whitespace-nowrap ...", className)}
562
+ {...props}
563
+ />
564
+ )
565
+ }
566
+ ```
567
+
568
+ ### Key CSS Properties for Comparison
569
+
570
+ | Property | Default Value |
571
+ |----------|-------------|
572
+ | `font-size` | `text-sm` (14px) |
573
+ | **Header** | |
574
+ | `height` | `h-10` (40px) |
575
+ | `padding` | `px-2` (8px horizontal) |
576
+ | `font-weight` | `font-medium` (500) |
577
+ | `color` | `text-foreground` |
578
+ | `border` | `border-b` (via `[&_tr]:border-b` on thead) |
579
+ | **Row** | |
580
+ | `border` | `border-b` |
581
+ | `hover` | `bg-muted/50` |
582
+ | `selected` | `bg-muted` (via `data-[state=selected]`) |
583
+ | `transition` | `transition-colors` |
584
+ | **Cell** | |
585
+ | `padding` | `p-2` (8px) |
586
+ | `vertical-align` | `align-middle` |
587
+ | `white-space` | `whitespace-nowrap` |
588
+ | **Footer** | |
589
+ | `background` | `bg-muted/50` |
590
+ | `border` | `border-t` |
591
+
592
+ ### Migration Override Template
593
+
594
+ ```css
595
+ @layer components {
596
+ [data-slot="table"] {
597
+ font-size: {{benchmark_font_size}};
598
+ }
599
+ [data-slot="table-head"] {
600
+ height: {{benchmark_head_height}};
601
+ padding: {{benchmark_head_padding}};
602
+ font-weight: {{benchmark_head_weight}};
603
+ background: {{benchmark_head_bg}};
604
+ }
605
+ [data-slot="table-row"]:hover {
606
+ background: {{benchmark_row_hover_bg}};
607
+ }
608
+ [data-slot="table-cell"] {
609
+ padding: {{benchmark_cell_padding}};
610
+ }
611
+ }
612
+ ```
613
+
614
+ ---
615
+
616
+ ## 8. Form (react-hook-form + zod)
617
+
618
+ ### Typical Pattern
619
+
620
+ ```tsx
621
+ // Typical form structure in a shadcn/ui project
622
+ import { useForm } from "react-hook-form"
623
+ import { zodResolver } from "@hookform/resolvers/zod"
624
+ import { z } from "zod"
625
+ import { Input } from "@/components/ui/input"
626
+ import { Button } from "@/components/ui/button"
627
+ import { Label } from "@/components/ui/label"
628
+
629
+ const schema = z.object({
630
+ name: z.string().min(1, "Required"),
631
+ email: z.string().email("Invalid email"),
632
+ })
633
+
634
+ function MyForm() {
635
+ const form = useForm({
636
+ resolver: zodResolver(schema),
637
+ defaultValues: { name: "", email: "" },
638
+ })
639
+
640
+ return (
641
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-4">
642
+ <div className="flex flex-col gap-2">
643
+ <Label htmlFor="name">Name</Label>
644
+ <Input
645
+ id="name"
646
+ {...form.register("name")}
647
+ aria-invalid={!!form.formState.errors.name}
648
+ />
649
+ {form.formState.errors.name && (
650
+ <p className="text-sm text-destructive">
651
+ {form.formState.errors.name.message}
652
+ </p>
653
+ )}
654
+ </div>
655
+ <Button type="submit" loading={form.formState.isSubmitting}>
656
+ Submit
657
+ </Button>
658
+ </form>
659
+ )
660
+ }
661
+ ```
662
+
663
+ ### Key CSS Properties for Comparison
664
+
665
+ | Element | Default Value |
666
+ |---------|-------------|
667
+ | Form gap | `gap-4` (16px) between fields |
668
+ | Field gap | `gap-2` (8px) between label and input |
669
+ | Label | `text-sm font-medium` (via Label component) |
670
+ | Error text | `text-sm text-destructive` |
671
+ | Error input | `aria-invalid` -> `ring-destructive/20 border-destructive` |
672
+ | Submit button | Full-width or auto, `loading` prop for spinner |
673
+
674
+ ### Migration Override Template
675
+
676
+ ```css
677
+ @layer components {
678
+ form [data-slot="label"] {
679
+ font-size: {{benchmark_label_size}};
680
+ font-weight: {{benchmark_label_weight}};
681
+ color: {{benchmark_label_color}};
682
+ }
683
+ form .text-destructive {
684
+ font-size: {{benchmark_error_size}};
685
+ color: {{benchmark_error_color}};
686
+ }
687
+ }
688
+ ```
689
+
690
+ ---
691
+
692
+ ## General Migration Override Generation Strategy
693
+
694
+ ### Step 1: Identify Differences
695
+ For each component, compare the benchmark's CSS against the project values listed above. Note every property that differs.
696
+
697
+ ### Step 2: Classify Each Difference
698
+
699
+ | Classification | Action | Example |
700
+ |---------------|--------|---------|
701
+ | **Token-level** | Override CSS custom property in `:root` | `--primary` hue change |
702
+ | **Component-level** | Add `@layer components` override using `data-slot` selectors | Card `border-radius` |
703
+ | **Variant-level** | Add new CVA variant or modify existing | New button variant |
704
+ | **Layout-level** | Structural JSX change required | Different sidebar pattern |
705
+
706
+ ### Step 3: Generate Override File
707
+
708
+ ```css
709
+ /* {{benchmark-name}}-overrides.css */
710
+ /* Import after globals.css, before component styles */
711
+
712
+ /* Token overrides */
713
+ :root {
714
+ /* Only override tokens that differ */
715
+ --primary: {{new_oklch_value}};
716
+ --radius: {{new_radius}};
717
+ /* ... */
718
+ }
719
+
720
+ /* Component overrides via data-slot selectors */
721
+ @layer components {
722
+ [data-slot="card"] { /* ... */ }
723
+ [data-slot="button"] { /* ... */ }
724
+ [data-slot="input"] { /* ... */ }
725
+ [data-slot="badge"] { /* ... */ }
726
+ [data-slot="dialog-content"] { /* ... */ }
727
+ [data-slot="table-head"] { /* ... */ }
728
+ }
729
+ ```
730
+
731
+ ### Step 4: Validate Against Design Rules
732
+
733
+ Before applying any override, check it against design system rules rules:
734
+
735
+ | Check | Rule | What Could Break |
736
+ |-------|------|-----------------|
737
+ | No hardcoded HEX | C1 | Use OKLCH in token overrides |
738
+ | No dark mode | C2 | Skip any dark-mode tokens from benchmark |
739
+ | Radius limit | R4 | Cannot use `rounded-xl` or above |
740
+ | Shadow hierarchy | SH1-SH4 | Must maintain shadow depth ordering |
741
+ | No framer-motion | A1 | Cannot adopt benchmark's animation library if it uses FM |
742
+ | `data-slot` required | AC4 | All overridden components must keep `data-slot` |
743
+
744
+ ### Step 5: Scope the Override (optional)
745
+
746
+ To apply benchmark styling only to specific sections:
747
+
748
+ ```css
749
+ /* Scoped to a specific route or wrapper */
750
+ [data-theme="{{benchmark-slug}}"] {
751
+ --primary: {{value}};
752
+ --radius: {{value}};
753
+ }
754
+
755
+ [data-theme="{{benchmark-slug}}"] [data-slot="card"] {
756
+ /* Component overrides within scope */
757
+ }
758
+ ```