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,522 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: detail-page-patterns
|
|
3
|
+
description: UX patterns for detail/view pages including headers with actions, tabbed content, multi-column layouts, and related data displays
|
|
4
|
+
license: MIT
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Detail Page UX Patterns
|
|
8
|
+
|
|
9
|
+
Detail pages display comprehensive information about a single entity with actions, related data, and navigation.
|
|
10
|
+
|
|
11
|
+
## Page Header Pattern
|
|
12
|
+
|
|
13
|
+
### Header with Back Navigation and Actions
|
|
14
|
+
```tsx
|
|
15
|
+
// REQUIRED: Detail page header structure
|
|
16
|
+
<PageLayout
|
|
17
|
+
title={item.name}
|
|
18
|
+
subtitle={item.subtitle || item.type}
|
|
19
|
+
backLink="/items"
|
|
20
|
+
backLabel="Back to Items"
|
|
21
|
+
headerContent={
|
|
22
|
+
<div className="flex items-center gap-2">
|
|
23
|
+
<Button variant="secondary" onClick={handleEdit}>
|
|
24
|
+
<EditIcon className="w-4 h-4 mr-1.5" />
|
|
25
|
+
Edit
|
|
26
|
+
</Button>
|
|
27
|
+
<Button variant="danger-outline" onClick={handleDelete}>
|
|
28
|
+
<TrashIcon className="w-4 h-4 mr-1.5" />
|
|
29
|
+
Delete
|
|
30
|
+
</Button>
|
|
31
|
+
</div>
|
|
32
|
+
}
|
|
33
|
+
>
|
|
34
|
+
{/* Page content */}
|
|
35
|
+
</PageLayout>
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Header Button Patterns
|
|
39
|
+
| Action | Variant | Icon Position |
|
|
40
|
+
|--------|---------|---------------|
|
|
41
|
+
| Edit | `secondary` | Left of text |
|
|
42
|
+
| Delete | `danger-outline` | Left of text |
|
|
43
|
+
| Save | `primary` | Left of text |
|
|
44
|
+
| Export | `ghost` | Left of text |
|
|
45
|
+
| More Actions | `ghost` | Icon only (kebab) |
|
|
46
|
+
|
|
47
|
+
## Tabbed Content Navigation
|
|
48
|
+
|
|
49
|
+
### URL-Synced Tabs
|
|
50
|
+
```tsx
|
|
51
|
+
interface TabConfig {
|
|
52
|
+
id: string;
|
|
53
|
+
label: string;
|
|
54
|
+
icon?: ReactNode;
|
|
55
|
+
count?: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function DetailPageTabs({ tabs, activeTab, onTabChange }: {
|
|
59
|
+
tabs: TabConfig[];
|
|
60
|
+
activeTab: string;
|
|
61
|
+
onTabChange: (tabId: string) => void;
|
|
62
|
+
}) {
|
|
63
|
+
const router = useRouter();
|
|
64
|
+
|
|
65
|
+
// Sync with URL on mount
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
const urlTab = router.query.tab as string;
|
|
68
|
+
if (urlTab && tabs.some(t => t.id === urlTab)) {
|
|
69
|
+
onTabChange(urlTab);
|
|
70
|
+
}
|
|
71
|
+
}, [router.query.tab]);
|
|
72
|
+
|
|
73
|
+
const handleTabClick = (tabId: string) => {
|
|
74
|
+
// Update URL without navigation
|
|
75
|
+
router.replace(
|
|
76
|
+
{ pathname: router.pathname, query: { ...router.query, tab: tabId } },
|
|
77
|
+
undefined,
|
|
78
|
+
{ shallow: true }
|
|
79
|
+
);
|
|
80
|
+
onTabChange(tabId);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div className="border-b border-border mb-6">
|
|
85
|
+
<div className="flex gap-1 -mb-px overflow-x-auto">
|
|
86
|
+
{tabs.map((tab) => {
|
|
87
|
+
const isActive = tab.id === activeTab;
|
|
88
|
+
return (
|
|
89
|
+
<button
|
|
90
|
+
key={tab.id}
|
|
91
|
+
onClick={() => handleTabClick(tab.id)}
|
|
92
|
+
className={`
|
|
93
|
+
flex items-center gap-2 px-4 py-3 text-sm font-medium
|
|
94
|
+
border-b-2 transition-colors whitespace-nowrap
|
|
95
|
+
${isActive
|
|
96
|
+
? 'border-accent text-accent'
|
|
97
|
+
: 'border-transparent text-text-secondary hover:text-white hover:border-border'
|
|
98
|
+
}
|
|
99
|
+
`}
|
|
100
|
+
>
|
|
101
|
+
{tab.icon}
|
|
102
|
+
{tab.label}
|
|
103
|
+
{tab.count !== undefined && (
|
|
104
|
+
<span className={`
|
|
105
|
+
px-1.5 py-0.5 text-xs rounded-full
|
|
106
|
+
${isActive ? 'bg-accent/20 text-accent' : 'bg-surface-raised text-text-muted'}
|
|
107
|
+
`}>
|
|
108
|
+
{tab.count}
|
|
109
|
+
</span>
|
|
110
|
+
)}
|
|
111
|
+
</button>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
</div>
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Tab Content Rendering
|
|
121
|
+
```tsx
|
|
122
|
+
// Tab content should lazy-render to avoid unnecessary data fetching
|
|
123
|
+
function DetailPage() {
|
|
124
|
+
const [activeTab, setActiveTab] = useState('overview');
|
|
125
|
+
|
|
126
|
+
return (
|
|
127
|
+
<>
|
|
128
|
+
<DetailPageTabs
|
|
129
|
+
tabs={[
|
|
130
|
+
{ id: 'overview', label: 'Overview', icon: <InfoIcon /> },
|
|
131
|
+
{ id: 'history', label: 'History', icon: <ClockIcon />, count: 12 },
|
|
132
|
+
{ id: 'settings', label: 'Settings', icon: <GearIcon /> },
|
|
133
|
+
]}
|
|
134
|
+
activeTab={activeTab}
|
|
135
|
+
onTabChange={setActiveTab}
|
|
136
|
+
/>
|
|
137
|
+
|
|
138
|
+
{/* Conditional rendering - only mount active tab */}
|
|
139
|
+
{activeTab === 'overview' && <OverviewTab data={data} />}
|
|
140
|
+
{activeTab === 'history' && <HistoryTab itemId={data.id} />}
|
|
141
|
+
{activeTab === 'settings' && <SettingsTab item={data} />}
|
|
142
|
+
</>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Multi-Column Layout
|
|
148
|
+
|
|
149
|
+
### Sidebar + Main Content
|
|
150
|
+
```tsx
|
|
151
|
+
// Desktop: Sidebar left, main content right
|
|
152
|
+
// Mobile: Stacked vertically
|
|
153
|
+
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
154
|
+
{/* Sidebar - summary/metadata */}
|
|
155
|
+
<div className="lg:col-span-1 space-y-4">
|
|
156
|
+
<SummaryCard item={item} />
|
|
157
|
+
<MetadataCard item={item} />
|
|
158
|
+
<QuickActionsCard onAction={handleAction} />
|
|
159
|
+
</div>
|
|
160
|
+
|
|
161
|
+
{/* Main content - detailed info */}
|
|
162
|
+
<div className="lg:col-span-2 space-y-6">
|
|
163
|
+
<PrimaryContentCard item={item} />
|
|
164
|
+
<RelatedItemsSection itemId={item.id} />
|
|
165
|
+
</div>
|
|
166
|
+
</div>
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Main Content + Sidebar (Reverse)
|
|
170
|
+
```tsx
|
|
171
|
+
// For content-heavy pages where main content should come first
|
|
172
|
+
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
|
173
|
+
{/* Main content - 3/4 width */}
|
|
174
|
+
<div className="lg:col-span-3 space-y-6">
|
|
175
|
+
<DetailedContentArea item={item} />
|
|
176
|
+
</div>
|
|
177
|
+
|
|
178
|
+
{/* Sidebar - 1/4 width */}
|
|
179
|
+
<div className="lg:col-span-1 space-y-4">
|
|
180
|
+
<StatusCard status={item.status} />
|
|
181
|
+
<RelatedLinksCard links={item.links} />
|
|
182
|
+
</div>
|
|
183
|
+
</div>
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Summary Card Pattern
|
|
187
|
+
|
|
188
|
+
### Entity Summary Header
|
|
189
|
+
```tsx
|
|
190
|
+
function SummaryCard({ item }) {
|
|
191
|
+
return (
|
|
192
|
+
<Card>
|
|
193
|
+
{/* Visual header with avatar/icon */}
|
|
194
|
+
<div className="flex items-start gap-4 mb-4">
|
|
195
|
+
<div className="w-16 h-16 rounded-xl bg-accent/20 flex items-center justify-center">
|
|
196
|
+
<ItemIcon className="w-8 h-8 text-accent" />
|
|
197
|
+
</div>
|
|
198
|
+
<div className="flex-1 min-w-0">
|
|
199
|
+
<h2 className="text-xl font-bold text-white truncate">{item.name}</h2>
|
|
200
|
+
<p className="text-sm text-text-secondary">{item.type}</p>
|
|
201
|
+
<StatusBadge status={item.status} className="mt-2" />
|
|
202
|
+
</div>
|
|
203
|
+
</div>
|
|
204
|
+
|
|
205
|
+
{/* Key stats grid */}
|
|
206
|
+
<div className="grid grid-cols-2 gap-3">
|
|
207
|
+
<StatItem label="Created" value={formatDate(item.createdAt)} />
|
|
208
|
+
<StatItem label="Updated" value={formatDate(item.updatedAt)} />
|
|
209
|
+
<StatItem label="Items" value={item.itemCount} />
|
|
210
|
+
<StatItem label="Score" value={`${item.score}%`} />
|
|
211
|
+
</div>
|
|
212
|
+
</Card>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function StatItem({ label, value }) {
|
|
217
|
+
return (
|
|
218
|
+
<div className="bg-surface-base/50 rounded-lg p-3">
|
|
219
|
+
<div className="text-xs text-text-muted uppercase tracking-wide mb-1">{label}</div>
|
|
220
|
+
<div className="text-sm font-medium text-white">{value}</div>
|
|
221
|
+
</div>
|
|
222
|
+
);
|
|
223
|
+
}
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
## Metadata Display
|
|
227
|
+
|
|
228
|
+
### Key-Value Metadata List
|
|
229
|
+
```tsx
|
|
230
|
+
function MetadataCard({ metadata }) {
|
|
231
|
+
return (
|
|
232
|
+
<Card>
|
|
233
|
+
<h3 className="text-sm font-semibold text-white mb-4">Details</h3>
|
|
234
|
+
<dl className="space-y-3">
|
|
235
|
+
{metadata.map(({ label, value, type }) => (
|
|
236
|
+
<div key={label} className="flex justify-between items-start gap-4">
|
|
237
|
+
<dt className="text-sm text-text-secondary flex-shrink-0">{label}</dt>
|
|
238
|
+
<dd className="text-sm text-white text-right">
|
|
239
|
+
{type === 'link' ? (
|
|
240
|
+
<a href={value.href} className="text-accent hover:underline">
|
|
241
|
+
{value.text}
|
|
242
|
+
</a>
|
|
243
|
+
) : type === 'badge' ? (
|
|
244
|
+
<Badge variant={value.variant}>{value.text}</Badge>
|
|
245
|
+
) : (
|
|
246
|
+
value
|
|
247
|
+
)}
|
|
248
|
+
</dd>
|
|
249
|
+
</div>
|
|
250
|
+
))}
|
|
251
|
+
</dl>
|
|
252
|
+
</Card>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
## Related Items Section
|
|
258
|
+
|
|
259
|
+
### Related Data with View All
|
|
260
|
+
```tsx
|
|
261
|
+
function RelatedItemsSection({ items, title, viewAllHref }) {
|
|
262
|
+
const displayItems = items.slice(0, 5);
|
|
263
|
+
const hasMore = items.length > 5;
|
|
264
|
+
|
|
265
|
+
return (
|
|
266
|
+
<Card>
|
|
267
|
+
<div className="flex items-center justify-between mb-4">
|
|
268
|
+
<h3 className="text-lg font-semibold text-white">{title}</h3>
|
|
269
|
+
{hasMore && (
|
|
270
|
+
<Link href={viewAllHref} className="text-sm text-accent hover:underline">
|
|
271
|
+
View all ({items.length})
|
|
272
|
+
</Link>
|
|
273
|
+
)}
|
|
274
|
+
</div>
|
|
275
|
+
|
|
276
|
+
{items.length === 0 ? (
|
|
277
|
+
<p className="text-sm text-text-secondary py-4 text-center">
|
|
278
|
+
No related items yet
|
|
279
|
+
</p>
|
|
280
|
+
) : (
|
|
281
|
+
<div className="space-y-2">
|
|
282
|
+
{displayItems.map((item) => (
|
|
283
|
+
<RelatedItemRow key={item.id} item={item} />
|
|
284
|
+
))}
|
|
285
|
+
</div>
|
|
286
|
+
)}
|
|
287
|
+
</Card>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function RelatedItemRow({ item }) {
|
|
292
|
+
return (
|
|
293
|
+
<Link href={`/items/${item.id}`}>
|
|
294
|
+
<a className="flex items-center gap-3 p-2 -mx-2 rounded-lg hover:bg-surface-raised/50 transition-colors group">
|
|
295
|
+
<div className="w-8 h-8 rounded bg-surface-base flex items-center justify-center">
|
|
296
|
+
<ItemIcon className="w-4 h-4 text-text-muted" />
|
|
297
|
+
</div>
|
|
298
|
+
<div className="flex-1 min-w-0">
|
|
299
|
+
<span className="text-sm text-white group-hover:text-accent truncate block">
|
|
300
|
+
{item.name}
|
|
301
|
+
</span>
|
|
302
|
+
<span className="text-xs text-text-muted">{item.type}</span>
|
|
303
|
+
</div>
|
|
304
|
+
<ChevronRightIcon className="w-4 h-4 text-text-muted group-hover:text-accent" />
|
|
305
|
+
</a>
|
|
306
|
+
</Link>
|
|
307
|
+
);
|
|
308
|
+
}
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## Timeline/History Pattern
|
|
312
|
+
|
|
313
|
+
### Activity Timeline
|
|
314
|
+
```tsx
|
|
315
|
+
function ActivityTimeline({ activities }) {
|
|
316
|
+
return (
|
|
317
|
+
<div className="relative">
|
|
318
|
+
{/* Vertical line */}
|
|
319
|
+
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-border" />
|
|
320
|
+
|
|
321
|
+
<div className="space-y-6">
|
|
322
|
+
{activities.map((activity, index) => (
|
|
323
|
+
<div key={activity.id} className="relative flex gap-4">
|
|
324
|
+
{/* Timeline dot */}
|
|
325
|
+
<div className={`
|
|
326
|
+
relative z-10 w-8 h-8 rounded-full flex items-center justify-center
|
|
327
|
+
${activity.type === 'create' ? 'bg-emerald-500/20 text-emerald-400' :
|
|
328
|
+
activity.type === 'update' ? 'bg-blue-500/20 text-blue-400' :
|
|
329
|
+
activity.type === 'delete' ? 'bg-red-500/20 text-red-400' :
|
|
330
|
+
'bg-surface-raised text-text-muted'}
|
|
331
|
+
`}>
|
|
332
|
+
<ActivityIcon type={activity.type} className="w-4 h-4" />
|
|
333
|
+
</div>
|
|
334
|
+
|
|
335
|
+
{/* Content */}
|
|
336
|
+
<div className="flex-1 pt-1">
|
|
337
|
+
<p className="text-sm text-white">{activity.description}</p>
|
|
338
|
+
<p className="text-xs text-text-muted mt-1">
|
|
339
|
+
{activity.user} - {formatRelativeTime(activity.timestamp)}
|
|
340
|
+
</p>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
))}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
## Collapsible Sections
|
|
351
|
+
|
|
352
|
+
### Expandable Detail Section
|
|
353
|
+
```tsx
|
|
354
|
+
function CollapsibleSection({ title, icon, defaultOpen = true, children }) {
|
|
355
|
+
const [isOpen, setIsOpen] = useState(defaultOpen);
|
|
356
|
+
|
|
357
|
+
return (
|
|
358
|
+
<Card>
|
|
359
|
+
<button
|
|
360
|
+
onClick={() => setIsOpen(!isOpen)}
|
|
361
|
+
className="w-full flex items-center justify-between p-4 -m-4 mb-0 hover:bg-surface-raised/30 rounded-t-xl transition-colors"
|
|
362
|
+
>
|
|
363
|
+
<div className="flex items-center gap-3">
|
|
364
|
+
{icon && <span className="text-accent">{icon}</span>}
|
|
365
|
+
<h3 className="text-base font-semibold text-white">{title}</h3>
|
|
366
|
+
</div>
|
|
367
|
+
<ChevronDownIcon
|
|
368
|
+
className={`w-5 h-5 text-text-muted transition-transform duration-200 ${
|
|
369
|
+
isOpen ? 'rotate-180' : ''
|
|
370
|
+
}`}
|
|
371
|
+
/>
|
|
372
|
+
</button>
|
|
373
|
+
|
|
374
|
+
{isOpen && (
|
|
375
|
+
<div className="pt-4 mt-4 border-t border-border">
|
|
376
|
+
{children}
|
|
377
|
+
</div>
|
|
378
|
+
)}
|
|
379
|
+
</Card>
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
## Delete Confirmation Flow
|
|
385
|
+
|
|
386
|
+
### Inline Delete with Confirmation
|
|
387
|
+
```tsx
|
|
388
|
+
function DeleteAction({ itemName, onDelete }) {
|
|
389
|
+
const [showConfirm, setShowConfirm] = useState(false);
|
|
390
|
+
const [isDeleting, setIsDeleting] = useState(false);
|
|
391
|
+
|
|
392
|
+
const handleDelete = async () => {
|
|
393
|
+
setIsDeleting(true);
|
|
394
|
+
try {
|
|
395
|
+
await onDelete();
|
|
396
|
+
} finally {
|
|
397
|
+
setIsDeleting(false);
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
|
|
401
|
+
if (!showConfirm) {
|
|
402
|
+
return (
|
|
403
|
+
<Button variant="danger-outline" onClick={() => setShowConfirm(true)}>
|
|
404
|
+
<TrashIcon className="w-4 h-4 mr-1.5" />
|
|
405
|
+
Delete
|
|
406
|
+
</Button>
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
return (
|
|
411
|
+
<div className="flex items-center gap-2">
|
|
412
|
+
<span className="text-sm text-text-secondary">Delete "{itemName}"?</span>
|
|
413
|
+
<Button
|
|
414
|
+
variant="danger"
|
|
415
|
+
size="sm"
|
|
416
|
+
onClick={handleDelete}
|
|
417
|
+
disabled={isDeleting}
|
|
418
|
+
>
|
|
419
|
+
{isDeleting ? 'Deleting...' : 'Confirm'}
|
|
420
|
+
</Button>
|
|
421
|
+
<Button
|
|
422
|
+
variant="ghost"
|
|
423
|
+
size="sm"
|
|
424
|
+
onClick={() => setShowConfirm(false)}
|
|
425
|
+
disabled={isDeleting}
|
|
426
|
+
>
|
|
427
|
+
Cancel
|
|
428
|
+
</Button>
|
|
429
|
+
</div>
|
|
430
|
+
);
|
|
431
|
+
}
|
|
432
|
+
```
|
|
433
|
+
|
|
434
|
+
## Edit Mode Pattern
|
|
435
|
+
|
|
436
|
+
### Inline Edit vs Modal Edit Decision
|
|
437
|
+
| Use Inline Edit | Use Modal Edit |
|
|
438
|
+
|-----------------|----------------|
|
|
439
|
+
| Single field change | Multiple related fields |
|
|
440
|
+
| Simple text/number | Complex form with validation |
|
|
441
|
+
| Frequent edits expected | Rare edits |
|
|
442
|
+
| No confirmation needed | Needs save/cancel flow |
|
|
443
|
+
|
|
444
|
+
### Inline Edit Field
|
|
445
|
+
```tsx
|
|
446
|
+
function InlineEditField({ value, onSave, label }) {
|
|
447
|
+
const [isEditing, setIsEditing] = useState(false);
|
|
448
|
+
const [editValue, setEditValue] = useState(value);
|
|
449
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
450
|
+
|
|
451
|
+
useEffect(() => {
|
|
452
|
+
if (isEditing) {
|
|
453
|
+
inputRef.current?.focus();
|
|
454
|
+
inputRef.current?.select();
|
|
455
|
+
}
|
|
456
|
+
}, [isEditing]);
|
|
457
|
+
|
|
458
|
+
const handleSave = async () => {
|
|
459
|
+
if (editValue !== value) {
|
|
460
|
+
await onSave(editValue);
|
|
461
|
+
}
|
|
462
|
+
setIsEditing(false);
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const handleKeyDown = (e: React.KeyboardEvent) => {
|
|
466
|
+
if (e.key === 'Enter') handleSave();
|
|
467
|
+
if (e.key === 'Escape') {
|
|
468
|
+
setEditValue(value);
|
|
469
|
+
setIsEditing(false);
|
|
470
|
+
}
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
if (!isEditing) {
|
|
474
|
+
return (
|
|
475
|
+
<button
|
|
476
|
+
onClick={() => setIsEditing(true)}
|
|
477
|
+
className="group flex items-center gap-2 text-left"
|
|
478
|
+
>
|
|
479
|
+
<span className="text-white">{value}</span>
|
|
480
|
+
<EditIcon className="w-3 h-3 text-text-muted opacity-0 group-hover:opacity-100" />
|
|
481
|
+
</button>
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
return (
|
|
486
|
+
<div className="flex items-center gap-2">
|
|
487
|
+
<input
|
|
488
|
+
ref={inputRef}
|
|
489
|
+
value={editValue}
|
|
490
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
491
|
+
onKeyDown={handleKeyDown}
|
|
492
|
+
onBlur={handleSave}
|
|
493
|
+
className="bg-surface-base border border-accent rounded px-2 py-1 text-white text-sm"
|
|
494
|
+
aria-label={label}
|
|
495
|
+
/>
|
|
496
|
+
</div>
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
```
|
|
500
|
+
|
|
501
|
+
## Audit Checklist for Detail Pages
|
|
502
|
+
|
|
503
|
+
### Critical (Must Fix)
|
|
504
|
+
- [ ] Has back navigation to parent list - users get trapped
|
|
505
|
+
- [ ] Delete has confirmation step - data loss risk
|
|
506
|
+
- [ ] Stacks to single column on mobile - mobile users blocked
|
|
507
|
+
- [ ] Loading states for async sections - appears broken
|
|
508
|
+
|
|
509
|
+
### Major (Should Fix)
|
|
510
|
+
- [ ] Has appropriate header actions (Edit, Delete) - no way to modify
|
|
511
|
+
- [ ] Uses tabs for multiple content sections - overwhelming without organization
|
|
512
|
+
- [ ] Tabs sync with URL (shareable) - can't share specific views
|
|
513
|
+
- [ ] Multi-column layout on desktop - wasted space
|
|
514
|
+
- [ ] Summary card with key stats - key info not visible
|
|
515
|
+
- [ ] Empty states for sections with no data - confusing gaps
|
|
516
|
+
|
|
517
|
+
### Minor (Nice to Have)
|
|
518
|
+
- [ ] Metadata displayed in scannable format - readability
|
|
519
|
+
- [ ] Related items show count and "view all" - navigation convenience
|
|
520
|
+
- [ ] Collapsible sections where appropriate - information density
|
|
521
|
+
- [ ] Timeline for history/activity - context for changes
|
|
522
|
+
- [ ] Edit flows are clear (inline vs modal) - consistency
|