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.
Files changed (44) hide show
  1. package/README.md +113 -7
  2. package/agents/card-reviewer.md +173 -0
  3. package/agents/comparison-reviewer.md +143 -0
  4. package/agents/density-reviewer.md +207 -0
  5. package/agents/detail-page-reviewer.md +143 -0
  6. package/agents/editor-reviewer.md +165 -0
  7. package/agents/form-reviewer.md +156 -0
  8. package/agents/game-ui-reviewer.md +181 -0
  9. package/agents/list-page-reviewer.md +132 -0
  10. package/agents/navigation-reviewer.md +145 -0
  11. package/agents/panel-reviewer.md +182 -0
  12. package/agents/replay-reviewer.md +174 -0
  13. package/agents/settings-reviewer.md +166 -0
  14. package/agents/ux-auditor.md +145 -45
  15. package/agents/ux-engineer.md +211 -38
  16. package/dist/cli.js +172 -5
  17. package/dist/cli.js.map +1 -1
  18. package/dist/index.cjs +172 -5
  19. package/dist/index.cjs.map +1 -1
  20. package/dist/index.d.cts +128 -4
  21. package/dist/index.d.ts +128 -4
  22. package/dist/index.js +172 -5
  23. package/dist/index.js.map +1 -1
  24. package/package.json +6 -4
  25. package/skills/canvas-grid-patterns/SKILL.md +367 -0
  26. package/skills/comparison-patterns/SKILL.md +354 -0
  27. package/skills/data-density-patterns/SKILL.md +493 -0
  28. package/skills/detail-page-patterns/SKILL.md +522 -0
  29. package/skills/drag-drop-patterns/SKILL.md +406 -0
  30. package/skills/editor-workspace-patterns/SKILL.md +552 -0
  31. package/skills/event-timeline-patterns/SKILL.md +542 -0
  32. package/skills/form-patterns/SKILL.md +608 -0
  33. package/skills/info-card-patterns/SKILL.md +531 -0
  34. package/skills/keyboard-shortcuts-patterns/SKILL.md +365 -0
  35. package/skills/list-page-patterns/SKILL.md +351 -0
  36. package/skills/modal-patterns/SKILL.md +750 -0
  37. package/skills/navigation-patterns/SKILL.md +476 -0
  38. package/skills/page-structure-patterns/SKILL.md +271 -0
  39. package/skills/playback-replay-patterns/SKILL.md +695 -0
  40. package/skills/react-ux-patterns/SKILL.md +434 -0
  41. package/skills/split-panel-patterns/SKILL.md +609 -0
  42. package/skills/status-visualization-patterns/SKILL.md +635 -0
  43. package/skills/toast-notification-patterns/SKILL.md +207 -0
  44. 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