ux-toolkit 0.1.0 → 0.4.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 +113 -7
- package/agents/card-reviewer.md +173 -0
- package/agents/comparison-reviewer.md +143 -0
- package/agents/density-reviewer.md +207 -0
- package/agents/detail-page-reviewer.md +143 -0
- package/agents/editor-reviewer.md +165 -0
- package/agents/form-reviewer.md +156 -0
- package/agents/game-ui-reviewer.md +181 -0
- package/agents/list-page-reviewer.md +132 -0
- package/agents/navigation-reviewer.md +145 -0
- package/agents/panel-reviewer.md +182 -0
- package/agents/replay-reviewer.md +174 -0
- package/agents/settings-reviewer.md +166 -0
- package/agents/ux-auditor.md +145 -45
- package/agents/ux-engineer.md +211 -38
- package/dist/cli.js +172 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +172 -5
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +128 -4
- package/dist/index.d.ts +128 -4
- package/dist/index.js +172 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -4
- package/skills/canvas-grid-patterns/SKILL.md +367 -0
- package/skills/comparison-patterns/SKILL.md +354 -0
- package/skills/data-density-patterns/SKILL.md +493 -0
- package/skills/detail-page-patterns/SKILL.md +522 -0
- package/skills/drag-drop-patterns/SKILL.md +406 -0
- package/skills/editor-workspace-patterns/SKILL.md +552 -0
- package/skills/event-timeline-patterns/SKILL.md +542 -0
- package/skills/form-patterns/SKILL.md +608 -0
- package/skills/info-card-patterns/SKILL.md +531 -0
- package/skills/keyboard-shortcuts-patterns/SKILL.md +365 -0
- package/skills/list-page-patterns/SKILL.md +351 -0
- package/skills/modal-patterns/SKILL.md +750 -0
- package/skills/navigation-patterns/SKILL.md +476 -0
- package/skills/page-structure-patterns/SKILL.md +271 -0
- package/skills/playback-replay-patterns/SKILL.md +695 -0
- package/skills/react-ux-patterns/SKILL.md +434 -0
- package/skills/split-panel-patterns/SKILL.md +609 -0
- package/skills/status-visualization-patterns/SKILL.md +635 -0
- package/skills/toast-notification-patterns/SKILL.md +207 -0
- package/skills/turn-based-ui-patterns/SKILL.md +506 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
# Info Card UI Patterns
|
|
2
|
+
|
|
3
|
+
Patterns for card-based information displays including compact list cards, standard detail cards, and expanded views. Applies to entity displays, stat blocks, profile cards, and any summarized data presentation.
|
|
4
|
+
|
|
5
|
+
## When to Use This Skill
|
|
6
|
+
|
|
7
|
+
- Entity/item cards (users, products, units)
|
|
8
|
+
- Stat blocks (RPG characters, game units)
|
|
9
|
+
- Profile/contact cards
|
|
10
|
+
- Product comparison cards
|
|
11
|
+
- Dashboard summary cards
|
|
12
|
+
- Search result cards
|
|
13
|
+
- Notification cards
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Core Concepts
|
|
18
|
+
|
|
19
|
+
### Card Variants
|
|
20
|
+
|
|
21
|
+
| Variant | Height | Use Case | Information Density |
|
|
22
|
+
|---------|--------|----------|---------------------|
|
|
23
|
+
| **Compact** | 48-64px | List views, search results | Name + 2-3 key stats |
|
|
24
|
+
| **Standard** | 120-200px | Grid layouts, medium detail | Name + image + stats + actions |
|
|
25
|
+
| **Expanded** | 300px+ | Detail views, full info | Everything including secondary data |
|
|
26
|
+
|
|
27
|
+
### Information Hierarchy
|
|
28
|
+
|
|
29
|
+
1. **Primary**: Name/title, main identifier
|
|
30
|
+
2. **Secondary**: Key stats, status badges
|
|
31
|
+
3. **Tertiary**: Metadata, timestamps, secondary attributes
|
|
32
|
+
4. **Actions**: Buttons, menus, links
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Audit Checklist
|
|
37
|
+
|
|
38
|
+
### Compact Card (List Item)
|
|
39
|
+
- [ ] [CRITICAL] Primary identifier readable at glance
|
|
40
|
+
- [ ] [CRITICAL] Consistent height across all cards
|
|
41
|
+
- [ ] [MAJOR] Key stats visible without hover
|
|
42
|
+
- [ ] [MAJOR] Status indicator (badge/dot) if applicable
|
|
43
|
+
- [ ] [MAJOR] Click target is entire card
|
|
44
|
+
- [ ] [MINOR] Hover state indicates interactivity
|
|
45
|
+
- [ ] [MINOR] Truncation for long text with ellipsis
|
|
46
|
+
|
|
47
|
+
### Standard Card
|
|
48
|
+
- [ ] [CRITICAL] Clear visual hierarchy (title > stats > metadata)
|
|
49
|
+
- [ ] [CRITICAL] Actions accessible without opening detail view
|
|
50
|
+
- [ ] [MAJOR] Image/avatar if applicable
|
|
51
|
+
- [ ] [MAJOR] Consistent card dimensions in grid
|
|
52
|
+
- [ ] [MAJOR] Status clearly indicated
|
|
53
|
+
- [ ] [MINOR] Secondary actions in overflow menu
|
|
54
|
+
- [ ] [MINOR] Loading skeleton matches card shape
|
|
55
|
+
|
|
56
|
+
### Expanded Card / Detail View
|
|
57
|
+
- [ ] [CRITICAL] All relevant information accessible
|
|
58
|
+
- [ ] [MAJOR] Grouped by category/section
|
|
59
|
+
- [ ] [MAJOR] Collapsible sections for dense data
|
|
60
|
+
- [ ] [MAJOR] Print-friendly layout option
|
|
61
|
+
- [ ] [MINOR] Copy-to-clipboard for key values
|
|
62
|
+
- [ ] [MINOR] Share/export functionality
|
|
63
|
+
|
|
64
|
+
### Information Display
|
|
65
|
+
- [ ] [CRITICAL] Labels clearly associated with values
|
|
66
|
+
- [ ] [CRITICAL] Units displayed for numeric values
|
|
67
|
+
- [ ] [MAJOR] Consistent alignment (labels left, values right)
|
|
68
|
+
- [ ] [MAJOR] Color coding for status/quality values
|
|
69
|
+
- [ ] [MINOR] Tooltips for abbreviated labels
|
|
70
|
+
- [ ] [MINOR] Relative values (e.g., "+10%") where meaningful
|
|
71
|
+
|
|
72
|
+
### Status Badges
|
|
73
|
+
- [ ] [CRITICAL] Status immediately recognizable
|
|
74
|
+
- [ ] [CRITICAL] Color + text (not color alone)
|
|
75
|
+
- [ ] [MAJOR] Consistent badge styling across app
|
|
76
|
+
- [ ] [MAJOR] Badge size appropriate to context
|
|
77
|
+
- [ ] [MINOR] Badge tooltip with full status description
|
|
78
|
+
|
|
79
|
+
### Responsive Behavior
|
|
80
|
+
- [ ] [CRITICAL] Cards readable on mobile
|
|
81
|
+
- [ ] [MAJOR] Grid adapts column count to viewport
|
|
82
|
+
- [ ] [MAJOR] Compact variant for constrained space
|
|
83
|
+
- [ ] [MINOR] Touch-friendly action targets (44px+)
|
|
84
|
+
|
|
85
|
+
### Accessibility
|
|
86
|
+
- [ ] [CRITICAL] Card role and label for screen readers
|
|
87
|
+
- [ ] [CRITICAL] Interactive cards are focusable
|
|
88
|
+
- [ ] [MAJOR] Actions have accessible names
|
|
89
|
+
- [ ] [MAJOR] Color contrast meets WCAG AA
|
|
90
|
+
- [ ] [MINOR] Keyboard navigation between cards
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Implementation Patterns
|
|
95
|
+
|
|
96
|
+
### Compact Card Component
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
interface CompactCardProps {
|
|
100
|
+
id: string;
|
|
101
|
+
name: string;
|
|
102
|
+
subtitle?: string;
|
|
103
|
+
stats: Array<{ label: string; value: string | number }>;
|
|
104
|
+
status?: { label: string; variant: BadgeVariant };
|
|
105
|
+
onClick?: () => void;
|
|
106
|
+
className?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function CompactCard({
|
|
110
|
+
id,
|
|
111
|
+
name,
|
|
112
|
+
subtitle,
|
|
113
|
+
stats,
|
|
114
|
+
status,
|
|
115
|
+
onClick,
|
|
116
|
+
className = ''
|
|
117
|
+
}: CompactCardProps) {
|
|
118
|
+
return (
|
|
119
|
+
<div
|
|
120
|
+
onClick={onClick}
|
|
121
|
+
className={`
|
|
122
|
+
flex items-center justify-between gap-4 p-3
|
|
123
|
+
bg-surface-base rounded-lg border border-border-theme-subtle
|
|
124
|
+
hover:bg-surface-raised hover:border-border-theme
|
|
125
|
+
transition-colors cursor-pointer
|
|
126
|
+
${className}
|
|
127
|
+
`}
|
|
128
|
+
role="article"
|
|
129
|
+
aria-labelledby={`card-${id}-title`}
|
|
130
|
+
tabIndex={0}
|
|
131
|
+
onKeyDown={e => e.key === 'Enter' && onClick?.()}
|
|
132
|
+
>
|
|
133
|
+
{/* Left: Identity */}
|
|
134
|
+
<div className="flex-1 min-w-0">
|
|
135
|
+
<div className="flex items-center gap-2">
|
|
136
|
+
<h3
|
|
137
|
+
id={`card-${id}-title`}
|
|
138
|
+
className="font-semibold text-text-theme-primary truncate"
|
|
139
|
+
>
|
|
140
|
+
{name}
|
|
141
|
+
</h3>
|
|
142
|
+
{status && (
|
|
143
|
+
<Badge variant={status.variant} size="sm">
|
|
144
|
+
{status.label}
|
|
145
|
+
</Badge>
|
|
146
|
+
)}
|
|
147
|
+
</div>
|
|
148
|
+
{subtitle && (
|
|
149
|
+
<p className="text-sm text-text-theme-muted truncate">
|
|
150
|
+
{subtitle}
|
|
151
|
+
</p>
|
|
152
|
+
)}
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
{/* Right: Key Stats */}
|
|
156
|
+
<div className="flex items-center gap-4 flex-shrink-0">
|
|
157
|
+
{stats.slice(0, 3).map((stat, idx) => (
|
|
158
|
+
<div key={idx} className="text-right">
|
|
159
|
+
<div className="text-xs text-text-theme-muted uppercase tracking-wide">
|
|
160
|
+
{stat.label}
|
|
161
|
+
</div>
|
|
162
|
+
<div className="font-mono font-medium text-text-theme-primary">
|
|
163
|
+
{stat.value}
|
|
164
|
+
</div>
|
|
165
|
+
</div>
|
|
166
|
+
))}
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### Standard Card Component
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
interface StandardCardProps {
|
|
177
|
+
id: string;
|
|
178
|
+
name: string;
|
|
179
|
+
image?: string;
|
|
180
|
+
description?: string;
|
|
181
|
+
stats: Array<{ label: string; value: string | number; highlight?: boolean }>;
|
|
182
|
+
badges?: Array<{ label: string; variant: BadgeVariant }>;
|
|
183
|
+
actions?: Array<{ label: string; icon?: React.ReactNode; onClick: () => void }>;
|
|
184
|
+
onClick?: () => void;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function StandardCard({
|
|
188
|
+
id,
|
|
189
|
+
name,
|
|
190
|
+
image,
|
|
191
|
+
description,
|
|
192
|
+
stats,
|
|
193
|
+
badges = [],
|
|
194
|
+
actions = [],
|
|
195
|
+
onClick,
|
|
196
|
+
}: StandardCardProps) {
|
|
197
|
+
return (
|
|
198
|
+
<div
|
|
199
|
+
className="bg-surface-base rounded-xl border border-border-theme-subtle overflow-hidden hover:shadow-lg transition-shadow"
|
|
200
|
+
role="article"
|
|
201
|
+
aria-labelledby={`card-${id}-title`}
|
|
202
|
+
>
|
|
203
|
+
{/* Header with Image */}
|
|
204
|
+
{image && (
|
|
205
|
+
<div className="aspect-video bg-surface-deep">
|
|
206
|
+
<img src={image} alt="" className="w-full h-full object-cover" />
|
|
207
|
+
</div>
|
|
208
|
+
)}
|
|
209
|
+
|
|
210
|
+
{/* Content */}
|
|
211
|
+
<div className="p-4">
|
|
212
|
+
{/* Title Row */}
|
|
213
|
+
<div className="flex items-start justify-between gap-2 mb-2">
|
|
214
|
+
<h3
|
|
215
|
+
id={`card-${id}-title`}
|
|
216
|
+
className="font-semibold text-lg text-text-theme-primary"
|
|
217
|
+
onClick={onClick}
|
|
218
|
+
style={{ cursor: onClick ? 'pointer' : 'default' }}
|
|
219
|
+
>
|
|
220
|
+
{name}
|
|
221
|
+
</h3>
|
|
222
|
+
|
|
223
|
+
{/* Badges */}
|
|
224
|
+
{badges.length > 0 && (
|
|
225
|
+
<div className="flex gap-1 flex-shrink-0">
|
|
226
|
+
{badges.map((badge, idx) => (
|
|
227
|
+
<Badge key={idx} variant={badge.variant} size="sm">
|
|
228
|
+
{badge.label}
|
|
229
|
+
</Badge>
|
|
230
|
+
))}
|
|
231
|
+
</div>
|
|
232
|
+
)}
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
{/* Description */}
|
|
236
|
+
{description && (
|
|
237
|
+
<p className="text-sm text-text-theme-secondary mb-3 line-clamp-2">
|
|
238
|
+
{description}
|
|
239
|
+
</p>
|
|
240
|
+
)}
|
|
241
|
+
|
|
242
|
+
{/* Stats Grid */}
|
|
243
|
+
<div className="grid grid-cols-3 gap-3 mb-4">
|
|
244
|
+
{stats.slice(0, 6).map((stat, idx) => (
|
|
245
|
+
<div key={idx}>
|
|
246
|
+
<div className="text-xs text-text-theme-muted uppercase">
|
|
247
|
+
{stat.label}
|
|
248
|
+
</div>
|
|
249
|
+
<div className={`font-mono font-medium ${
|
|
250
|
+
stat.highlight ? 'text-accent' : 'text-text-theme-primary'
|
|
251
|
+
}`}>
|
|
252
|
+
{stat.value}
|
|
253
|
+
</div>
|
|
254
|
+
</div>
|
|
255
|
+
))}
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
{/* Actions */}
|
|
259
|
+
{actions.length > 0 && (
|
|
260
|
+
<div className="flex gap-2 pt-3 border-t border-border-theme-subtle">
|
|
261
|
+
{actions.map((action, idx) => (
|
|
262
|
+
<Button
|
|
263
|
+
key={idx}
|
|
264
|
+
variant={idx === 0 ? 'primary' : 'ghost'}
|
|
265
|
+
size="sm"
|
|
266
|
+
onClick={action.onClick}
|
|
267
|
+
>
|
|
268
|
+
{action.icon}
|
|
269
|
+
{action.label}
|
|
270
|
+
</Button>
|
|
271
|
+
))}
|
|
272
|
+
</div>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
</div>
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Stat Block Component
|
|
281
|
+
|
|
282
|
+
```tsx
|
|
283
|
+
interface StatBlockProps {
|
|
284
|
+
stats: Array<{
|
|
285
|
+
label: string;
|
|
286
|
+
value: string | number;
|
|
287
|
+
max?: number;
|
|
288
|
+
color?: 'default' | 'success' | 'warning' | 'danger';
|
|
289
|
+
}>;
|
|
290
|
+
columns?: 2 | 3 | 4;
|
|
291
|
+
variant?: 'inline' | 'stacked';
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function StatBlock({ stats, columns = 3, variant = 'stacked' }: StatBlockProps) {
|
|
295
|
+
const colorClasses = {
|
|
296
|
+
default: 'text-text-theme-primary',
|
|
297
|
+
success: 'text-emerald-400',
|
|
298
|
+
warning: 'text-amber-400',
|
|
299
|
+
danger: 'text-red-400',
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
return (
|
|
303
|
+
<dl className={`grid grid-cols-${columns} gap-4`}>
|
|
304
|
+
{stats.map((stat, idx) => (
|
|
305
|
+
<div
|
|
306
|
+
key={idx}
|
|
307
|
+
className={variant === 'inline' ? 'flex justify-between' : ''}
|
|
308
|
+
>
|
|
309
|
+
<dt className="text-xs text-text-theme-muted uppercase tracking-wide">
|
|
310
|
+
{stat.label}
|
|
311
|
+
</dt>
|
|
312
|
+
<dd className={`font-mono font-medium ${colorClasses[stat.color || 'default']}`}>
|
|
313
|
+
{stat.value}
|
|
314
|
+
{stat.max !== undefined && (
|
|
315
|
+
<span className="text-text-theme-muted">/{stat.max}</span>
|
|
316
|
+
)}
|
|
317
|
+
</dd>
|
|
318
|
+
</div>
|
|
319
|
+
))}
|
|
320
|
+
</dl>
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
### Badge Component
|
|
326
|
+
|
|
327
|
+
```tsx
|
|
328
|
+
type BadgeVariant = 'emerald' | 'amber' | 'rose' | 'cyan' | 'violet' | 'slate' | 'muted';
|
|
329
|
+
type BadgeSize = 'sm' | 'md' | 'lg';
|
|
330
|
+
|
|
331
|
+
interface BadgeProps {
|
|
332
|
+
children: React.ReactNode;
|
|
333
|
+
variant?: BadgeVariant;
|
|
334
|
+
size?: BadgeSize;
|
|
335
|
+
icon?: React.ReactNode;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function Badge({ children, variant = 'muted', size = 'md', icon }: BadgeProps) {
|
|
339
|
+
const variantClasses: Record<BadgeVariant, string> = {
|
|
340
|
+
emerald: 'bg-emerald-500/20 text-emerald-400 border-emerald-500/30',
|
|
341
|
+
amber: 'bg-amber-500/20 text-amber-400 border-amber-500/30',
|
|
342
|
+
rose: 'bg-rose-500/20 text-rose-400 border-rose-500/30',
|
|
343
|
+
cyan: 'bg-cyan-500/20 text-cyan-400 border-cyan-500/30',
|
|
344
|
+
violet: 'bg-violet-500/20 text-violet-400 border-violet-500/30',
|
|
345
|
+
slate: 'bg-slate-500/20 text-slate-400 border-slate-500/30',
|
|
346
|
+
muted: 'bg-gray-500/20 text-gray-400 border-gray-500/30',
|
|
347
|
+
};
|
|
348
|
+
|
|
349
|
+
const sizeClasses: Record<BadgeSize, string> = {
|
|
350
|
+
sm: 'px-1.5 py-0.5 text-xs',
|
|
351
|
+
md: 'px-2 py-1 text-sm',
|
|
352
|
+
lg: 'px-3 py-1.5 text-base',
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
return (
|
|
356
|
+
<span className={`
|
|
357
|
+
inline-flex items-center gap-1 rounded-full border font-medium
|
|
358
|
+
${variantClasses[variant]}
|
|
359
|
+
${sizeClasses[size]}
|
|
360
|
+
`}>
|
|
361
|
+
{icon}
|
|
362
|
+
{children}
|
|
363
|
+
</span>
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
### Card Grid Layout
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
interface CardGridProps {
|
|
372
|
+
children: React.ReactNode;
|
|
373
|
+
variant?: 'compact' | 'standard' | 'expanded';
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
function CardGrid({ children, variant = 'standard' }: CardGridProps) {
|
|
377
|
+
const gridClasses = {
|
|
378
|
+
compact: 'flex flex-col gap-2',
|
|
379
|
+
standard: 'grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4',
|
|
380
|
+
expanded: 'flex flex-col gap-6',
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
return (
|
|
384
|
+
<div className={gridClasses[variant]}>
|
|
385
|
+
{children}
|
|
386
|
+
</div>
|
|
387
|
+
);
|
|
388
|
+
}
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
---
|
|
392
|
+
|
|
393
|
+
## Visual Patterns
|
|
394
|
+
|
|
395
|
+
### Status Badge Colors
|
|
396
|
+
|
|
397
|
+
```typescript
|
|
398
|
+
const STATUS_BADGES = {
|
|
399
|
+
active: { label: 'Active', variant: 'emerald' as const },
|
|
400
|
+
inactive: { label: 'Inactive', variant: 'slate' as const },
|
|
401
|
+
pending: { label: 'Pending', variant: 'amber' as const },
|
|
402
|
+
error: { label: 'Error', variant: 'rose' as const },
|
|
403
|
+
premium: { label: 'Premium', variant: 'violet' as const },
|
|
404
|
+
new: { label: 'New', variant: 'cyan' as const },
|
|
405
|
+
};
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### Value Coloring
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
function getValueColor(value: number, thresholds: { good: number; warning: number }) {
|
|
412
|
+
if (value >= thresholds.good) return 'text-emerald-400';
|
|
413
|
+
if (value >= thresholds.warning) return 'text-amber-400';
|
|
414
|
+
return 'text-red-400';
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Example usage for health percentage
|
|
418
|
+
const healthColor = getValueColor(healthPercent, { good: 75, warning: 25 });
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Anti-Patterns
|
|
424
|
+
|
|
425
|
+
### DON'T: Inconsistent Card Heights
|
|
426
|
+
```tsx
|
|
427
|
+
// BAD - Cards of varying height in a row
|
|
428
|
+
<div className="flex gap-4">
|
|
429
|
+
<Card>Short content</Card>
|
|
430
|
+
<Card>Very long content that spans multiple lines...</Card>
|
|
431
|
+
</div>
|
|
432
|
+
|
|
433
|
+
// GOOD - Fixed or min-height for consistency
|
|
434
|
+
<div className="grid grid-cols-3 gap-4">
|
|
435
|
+
<Card className="min-h-[200px]">Short content</Card>
|
|
436
|
+
<Card className="min-h-[200px]">Long content...</Card>
|
|
437
|
+
</div>
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### DON'T: Color-Only Status
|
|
441
|
+
```tsx
|
|
442
|
+
// BAD - Status indicated only by color
|
|
443
|
+
<div className="w-3 h-3 rounded-full bg-green-500" />
|
|
444
|
+
|
|
445
|
+
// GOOD - Color + text
|
|
446
|
+
<Badge variant="emerald">Active</Badge>
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
### DON'T: Hidden Actions
|
|
450
|
+
```tsx
|
|
451
|
+
// BAD - Actions only appear on hover (inaccessible on touch)
|
|
452
|
+
<Card onMouseEnter={() => setShowActions(true)}>
|
|
453
|
+
{showActions && <ActionButtons />}
|
|
454
|
+
</Card>
|
|
455
|
+
|
|
456
|
+
// GOOD - Actions always visible or in consistent location
|
|
457
|
+
<Card>
|
|
458
|
+
<ActionButtons />
|
|
459
|
+
</Card>
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
### DON'T: Unlabeled Stats
|
|
463
|
+
```tsx
|
|
464
|
+
// BAD - Numbers without context
|
|
465
|
+
<div className="font-bold">42</div>
|
|
466
|
+
|
|
467
|
+
// GOOD - Label + value + unit
|
|
468
|
+
<div>
|
|
469
|
+
<span className="text-xs text-gray-500">Speed</span>
|
|
470
|
+
<span className="font-bold">42 mph</span>
|
|
471
|
+
</div>
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
---
|
|
475
|
+
|
|
476
|
+
## Accessibility
|
|
477
|
+
|
|
478
|
+
### Card Semantics
|
|
479
|
+
|
|
480
|
+
```tsx
|
|
481
|
+
// Article for standalone content
|
|
482
|
+
<article aria-labelledby={titleId}>
|
|
483
|
+
<h3 id={titleId}>{title}</h3>
|
|
484
|
+
...
|
|
485
|
+
</article>
|
|
486
|
+
|
|
487
|
+
// List item when in a list
|
|
488
|
+
<li role="article" aria-labelledby={titleId}>
|
|
489
|
+
...
|
|
490
|
+
</li>
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
### Keyboard Navigation
|
|
494
|
+
|
|
495
|
+
```tsx
|
|
496
|
+
// Make cards focusable and activatable
|
|
497
|
+
<div
|
|
498
|
+
role="article"
|
|
499
|
+
tabIndex={0}
|
|
500
|
+
onClick={handleClick}
|
|
501
|
+
onKeyDown={e => {
|
|
502
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
503
|
+
e.preventDefault();
|
|
504
|
+
handleClick();
|
|
505
|
+
}
|
|
506
|
+
}}
|
|
507
|
+
aria-label={`${name}, ${status}. Press Enter for details.`}
|
|
508
|
+
>
|
|
509
|
+
```
|
|
510
|
+
|
|
511
|
+
---
|
|
512
|
+
|
|
513
|
+
## Testing Checklist
|
|
514
|
+
|
|
515
|
+
- [ ] Compact cards maintain consistent height
|
|
516
|
+
- [ ] Standard cards align in grid properly
|
|
517
|
+
- [ ] Long text truncates with ellipsis
|
|
518
|
+
- [ ] Badges display correct color for status
|
|
519
|
+
- [ ] Actions trigger correct callbacks
|
|
520
|
+
- [ ] Cards are keyboard navigable
|
|
521
|
+
- [ ] Screen reader announces card content
|
|
522
|
+
- [ ] Loading skeletons match card dimensions
|
|
523
|
+
- [ ] Responsive layout works at all breakpoints
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## Related Skills
|
|
528
|
+
|
|
529
|
+
- `list-page-patterns` - For card list layouts
|
|
530
|
+
- `data-density-patterns` - For dense stat displays
|
|
531
|
+
- `status-visualization-patterns` - For health/progress indicators
|