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.
- package/README.md +95 -0
- package/bin/cli.mjs +121 -0
- package/package.json +34 -0
- package/skill/SKILL.md +751 -0
- package/skill/references/analysis-dimensions.md +382 -0
- package/skill/references/component-catalog.md +758 -0
- package/skill/references/css-token-mapping.md +359 -0
- package/skill/references/output-template.md +249 -0
- package/skill/scripts/compare_tokens.py +741 -0
- package/skill/scripts/download_screenshot.py +125 -0
- package/skill/scripts/extract_design_tokens.py +617 -0
- package/skill/scripts/generate_migration.py +580 -0
- package/skill/scripts/generate_radar_chart.py +267 -0
|
@@ -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
|
+
```
|