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,542 @@
1
+ # Event Timeline UI Patterns
2
+
3
+ Patterns for chronological event displays, activity logs, history views, and audit trails. Applies to event logs, activity feeds, version history, notification streams, and debugging timelines.
4
+
5
+ ## When to Use This Skill
6
+
7
+ - Activity feeds / event logs
8
+ - Audit trails
9
+ - Version history
10
+ - Notification centers
11
+ - Chat/message history
12
+ - Debugging timelines
13
+ - Git commit history
14
+ - Order/transaction history
15
+
16
+ ---
17
+
18
+ ## Core Concepts
19
+
20
+ ### Event Types
21
+
22
+ | Type | Icon Color | Use Case |
23
+ |------|------------|----------|
24
+ | **Info** | Blue | General events, navigation |
25
+ | **Success** | Green | Completed actions, achievements |
26
+ | **Warning** | Amber | Potential issues, notifications |
27
+ | **Error** | Red | Failures, critical events |
28
+ | **System** | Gray | Automated events, metadata |
29
+
30
+ ### Display Modes
31
+
32
+ | Mode | Description | Best For |
33
+ |------|-------------|----------|
34
+ | **Timeline** | Vertical line connecting events | Sequential narrative |
35
+ | **List** | Simple list without connectors | Dense logs, search results |
36
+ | **Grouped** | Events grouped by date/type | Long histories |
37
+ | **Compact** | Single-line per event | Maximum density |
38
+
39
+ ---
40
+
41
+ ## Audit Checklist
42
+
43
+ ### Event Display
44
+ - [ ] [CRITICAL] Event type clearly indicated (icon/color)
45
+ - [ ] [CRITICAL] Timestamp visible for each event
46
+ - [ ] [CRITICAL] Event description readable
47
+ - [ ] [MAJOR] Actor/source identified (who did it)
48
+ - [ ] [MAJOR] Consistent event formatting
49
+ - [ ] [MINOR] Event metadata expandable
50
+ - [ ] [MINOR] Raw data accessible (for debugging)
51
+
52
+ ### Chronological Order
53
+ - [ ] [CRITICAL] Events sorted correctly (newest first OR oldest first)
54
+ - [ ] [CRITICAL] Sort order matches user expectation
55
+ - [ ] [MAJOR] Date separators for long timelines
56
+ - [ ] [MAJOR] Relative timestamps ("5 min ago")
57
+ - [ ] [MINOR] Toggle between relative/absolute time
58
+ - [ ] [MINOR] Timezone handling documented
59
+
60
+ ### Filtering & Search
61
+ - [ ] [MAJOR] Filter by event type
62
+ - [ ] [MAJOR] Filter by date range
63
+ - [ ] [MAJOR] Search event content
64
+ - [ ] [MINOR] Filter by actor/source
65
+ - [ ] [MINOR] Save filter presets
66
+ - [ ] [MINOR] Clear all filters button
67
+
68
+ ### Navigation
69
+ - [ ] [CRITICAL] Scroll to see more events
70
+ - [ ] [MAJOR] Jump to specific event (by ID or link)
71
+ - [ ] [MAJOR] Jump to date/time
72
+ - [ ] [MINOR] Keyboard navigation (up/down arrows)
73
+ - [ ] [MINOR] Infinite scroll OR pagination
74
+
75
+ ### Performance
76
+ - [ ] [CRITICAL] Initial load is fast (<100 events visible)
77
+ - [ ] [MAJOR] Virtualized list for large datasets
78
+ - [ ] [MAJOR] Lazy loading for older events
79
+ - [ ] [MINOR] Skeleton loading states
80
+ - [ ] [MINOR] Smooth scroll behavior
81
+
82
+ ### Interactivity
83
+ - [ ] [MAJOR] Click event for details
84
+ - [ ] [MAJOR] Selected event highlighted
85
+ - [ ] [MINOR] Copy event ID/link
86
+ - [ ] [MINOR] Expand/collapse event details
87
+ - [ ] [MINOR] Context menu for actions
88
+
89
+ ### Accessibility
90
+ - [ ] [CRITICAL] Events announced to screen readers
91
+ - [ ] [CRITICAL] Keyboard navigable
92
+ - [ ] [MAJOR] Focus indicator visible
93
+ - [ ] [MAJOR] Color not the only type indicator
94
+ - [ ] [MINOR] Live region for new events
95
+
96
+ ---
97
+
98
+ ## Implementation Patterns
99
+
100
+ ### Event Item Component
101
+
102
+ ```tsx
103
+ interface TimelineEvent {
104
+ id: string;
105
+ type: 'info' | 'success' | 'warning' | 'error' | 'system';
106
+ category: string;
107
+ title: string;
108
+ description?: string;
109
+ timestamp: Date;
110
+ actor?: { name: string; avatar?: string };
111
+ metadata?: Record<string, unknown>;
112
+ }
113
+
114
+ interface EventTimelineItemProps {
115
+ event: TimelineEvent;
116
+ isSelected?: boolean;
117
+ onClick?: (event: TimelineEvent) => void;
118
+ showConnector?: boolean;
119
+ }
120
+
121
+ function EventTimelineItem({
122
+ event,
123
+ isSelected,
124
+ onClick,
125
+ showConnector = true
126
+ }: EventTimelineItemProps) {
127
+ const typeConfig = {
128
+ info: { color: 'blue', icon: InfoIcon },
129
+ success: { color: 'emerald', icon: CheckIcon },
130
+ warning: { color: 'amber', icon: AlertIcon },
131
+ error: { color: 'red', icon: ErrorIcon },
132
+ system: { color: 'gray', icon: GearIcon },
133
+ };
134
+
135
+ const config = typeConfig[event.type];
136
+ const Icon = config.icon;
137
+
138
+ return (
139
+ <div
140
+ className={`
141
+ relative flex gap-4 p-4 cursor-pointer
142
+ hover:bg-surface-raised/50 transition-colors
143
+ ${isSelected ? 'bg-surface-raised border-l-2 border-accent' : ''}
144
+ `}
145
+ onClick={() => onClick?.(event)}
146
+ role="article"
147
+ aria-labelledby={`event-${event.id}-title`}
148
+ tabIndex={0}
149
+ onKeyDown={e => e.key === 'Enter' && onClick?.(event)}
150
+ >
151
+ {/* Timeline connector */}
152
+ {showConnector && (
153
+ <div className="absolute left-8 top-12 bottom-0 w-0.5 bg-border-theme-subtle" />
154
+ )}
155
+
156
+ {/* Event icon */}
157
+ <div className={`
158
+ flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center
159
+ bg-${config.color}-500/20 text-${config.color}-400
160
+ `}>
161
+ <Icon className="w-4 h-4" />
162
+ </div>
163
+
164
+ {/* Event content */}
165
+ <div className="flex-1 min-w-0">
166
+ <div className="flex items-start justify-between gap-2">
167
+ <div>
168
+ <h4
169
+ id={`event-${event.id}-title`}
170
+ className="font-medium text-text-theme-primary"
171
+ >
172
+ {event.title}
173
+ </h4>
174
+ {event.description && (
175
+ <p className="text-sm text-text-theme-secondary mt-0.5">
176
+ {event.description}
177
+ </p>
178
+ )}
179
+ </div>
180
+
181
+ {/* Timestamp */}
182
+ <time
183
+ dateTime={event.timestamp.toISOString()}
184
+ className="text-xs text-text-theme-muted flex-shrink-0"
185
+ title={event.timestamp.toLocaleString()}
186
+ >
187
+ {formatRelativeTime(event.timestamp)}
188
+ </time>
189
+ </div>
190
+
191
+ {/* Actor & Category */}
192
+ <div className="flex items-center gap-3 mt-2 text-xs text-text-theme-muted">
193
+ {event.actor && (
194
+ <span className="flex items-center gap-1">
195
+ {event.actor.avatar && (
196
+ <img src={event.actor.avatar} className="w-4 h-4 rounded-full" alt="" />
197
+ )}
198
+ {event.actor.name}
199
+ </span>
200
+ )}
201
+ <span className="px-1.5 py-0.5 rounded bg-surface-raised">
202
+ {event.category}
203
+ </span>
204
+ </div>
205
+ </div>
206
+ </div>
207
+ );
208
+ }
209
+ ```
210
+
211
+ ### Event Timeline Container
212
+
213
+ ```tsx
214
+ interface EventTimelineProps {
215
+ events: readonly TimelineEvent[];
216
+ onEventClick?: (event: TimelineEvent) => void;
217
+ onLoadMore?: () => void;
218
+ hasMore?: boolean;
219
+ isLoading?: boolean;
220
+ selectedEventId?: string;
221
+ maxHeight?: string;
222
+ }
223
+
224
+ function EventTimeline({
225
+ events,
226
+ onEventClick,
227
+ onLoadMore,
228
+ hasMore,
229
+ isLoading,
230
+ selectedEventId,
231
+ maxHeight = '600px',
232
+ }: EventTimelineProps) {
233
+ const containerRef = useRef<HTMLDivElement>(null);
234
+
235
+ // Infinite scroll detection
236
+ useEffect(() => {
237
+ if (!onLoadMore || !hasMore || isLoading) return;
238
+
239
+ const observer = new IntersectionObserver(
240
+ entries => {
241
+ if (entries[0].isIntersecting) {
242
+ onLoadMore();
243
+ }
244
+ },
245
+ { root: containerRef.current, threshold: 0.1 }
246
+ );
247
+
248
+ const sentinel = document.getElementById('timeline-sentinel');
249
+ if (sentinel) observer.observe(sentinel);
250
+
251
+ return () => observer.disconnect();
252
+ }, [onLoadMore, hasMore, isLoading]);
253
+
254
+ if (events.length === 0) {
255
+ return <EmptyState message="No events found" />;
256
+ }
257
+
258
+ // Group events by date
259
+ const groupedEvents = groupEventsByDate(events);
260
+
261
+ return (
262
+ <div
263
+ ref={containerRef}
264
+ className="overflow-y-auto"
265
+ style={{ maxHeight }}
266
+ role="feed"
267
+ aria-label="Event timeline"
268
+ >
269
+ {Object.entries(groupedEvents).map(([date, dateEvents]) => (
270
+ <div key={date}>
271
+ {/* Date separator */}
272
+ <div className="sticky top-0 bg-surface-deep px-4 py-2 text-xs font-medium text-text-theme-muted border-b border-border-theme-subtle">
273
+ {formatDateHeader(date)}
274
+ </div>
275
+
276
+ {/* Events for this date */}
277
+ {dateEvents.map((event, idx) => (
278
+ <EventTimelineItem
279
+ key={event.id}
280
+ event={event}
281
+ isSelected={event.id === selectedEventId}
282
+ onClick={onEventClick}
283
+ showConnector={idx < dateEvents.length - 1}
284
+ />
285
+ ))}
286
+ </div>
287
+ ))}
288
+
289
+ {/* Load more sentinel */}
290
+ <div id="timeline-sentinel" className="h-4" />
291
+
292
+ {/* Loading indicator */}
293
+ {isLoading && (
294
+ <div className="flex items-center justify-center py-4">
295
+ <Spinner size="sm" />
296
+ <span className="ml-2 text-sm text-text-theme-muted">Loading more...</span>
297
+ </div>
298
+ )}
299
+ </div>
300
+ );
301
+ }
302
+ ```
303
+
304
+ ### Filter Bar Component
305
+
306
+ ```tsx
307
+ interface TimelineFiltersProps {
308
+ eventTypes: string[];
309
+ selectedTypes: string[];
310
+ dateRange: { start: Date | null; end: Date | null };
311
+ searchQuery: string;
312
+ onTypeChange: (types: string[]) => void;
313
+ onDateRangeChange: (range: { start: Date | null; end: Date | null }) => void;
314
+ onSearchChange: (query: string) => void;
315
+ onClearFilters: () => void;
316
+ }
317
+
318
+ function TimelineFilters({
319
+ eventTypes,
320
+ selectedTypes,
321
+ dateRange,
322
+ searchQuery,
323
+ onTypeChange,
324
+ onDateRangeChange,
325
+ onSearchChange,
326
+ onClearFilters,
327
+ }: TimelineFiltersProps) {
328
+ const hasActiveFilters = selectedTypes.length > 0 || dateRange.start || searchQuery;
329
+
330
+ return (
331
+ <div className="flex flex-wrap items-center gap-3 p-4 bg-surface-base border-b border-border-theme-subtle">
332
+ {/* Search */}
333
+ <div className="relative flex-1 min-w-[200px]">
334
+ <SearchIcon className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-text-theme-muted" />
335
+ <input
336
+ type="text"
337
+ placeholder="Search events..."
338
+ value={searchQuery}
339
+ onChange={e => onSearchChange(e.target.value)}
340
+ className="w-full pl-9 pr-3 py-2 bg-surface-raised rounded-lg border border-border-theme-subtle"
341
+ />
342
+ </div>
343
+
344
+ {/* Type filter */}
345
+ <MultiSelect
346
+ options={eventTypes.map(t => ({ value: t, label: t }))}
347
+ selected={selectedTypes}
348
+ onChange={onTypeChange}
349
+ placeholder="Event type"
350
+ />
351
+
352
+ {/* Date range */}
353
+ <DateRangePicker
354
+ value={dateRange}
355
+ onChange={onDateRangeChange}
356
+ placeholder="Date range"
357
+ />
358
+
359
+ {/* Clear filters */}
360
+ {hasActiveFilters && (
361
+ <Button variant="ghost" size="sm" onClick={onClearFilters}>
362
+ <XIcon className="w-4 h-4 mr-1" />
363
+ Clear
364
+ </Button>
365
+ )}
366
+ </div>
367
+ );
368
+ }
369
+ ```
370
+
371
+ ### Compact Event List
372
+
373
+ ```tsx
374
+ function CompactEventList({ events, onEventClick }: CompactListProps) {
375
+ return (
376
+ <div className="divide-y divide-border-theme-subtle">
377
+ {events.map(event => (
378
+ <button
379
+ key={event.id}
380
+ onClick={() => onEventClick?.(event)}
381
+ className="w-full flex items-center gap-3 px-4 py-2 hover:bg-surface-raised text-left"
382
+ >
383
+ <EventTypeIcon type={event.type} size="sm" />
384
+ <span className="flex-1 truncate text-sm">{event.title}</span>
385
+ <time className="text-xs text-text-theme-muted">
386
+ {formatRelativeTime(event.timestamp)}
387
+ </time>
388
+ </button>
389
+ ))}
390
+ </div>
391
+ );
392
+ }
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Helper Functions
398
+
399
+ ### Relative Time Formatting
400
+
401
+ ```typescript
402
+ function formatRelativeTime(date: Date): string {
403
+ const now = new Date();
404
+ const diffMs = now.getTime() - date.getTime();
405
+ const diffSec = Math.floor(diffMs / 1000);
406
+ const diffMin = Math.floor(diffSec / 60);
407
+ const diffHour = Math.floor(diffMin / 60);
408
+ const diffDay = Math.floor(diffHour / 24);
409
+
410
+ if (diffSec < 60) return 'Just now';
411
+ if (diffMin < 60) return `${diffMin}m ago`;
412
+ if (diffHour < 24) return `${diffHour}h ago`;
413
+ if (diffDay < 7) return `${diffDay}d ago`;
414
+
415
+ return date.toLocaleDateString();
416
+ }
417
+ ```
418
+
419
+ ### Event Grouping
420
+
421
+ ```typescript
422
+ function groupEventsByDate(events: TimelineEvent[]): Record<string, TimelineEvent[]> {
423
+ return events.reduce((groups, event) => {
424
+ const dateKey = event.timestamp.toISOString().split('T')[0];
425
+ if (!groups[dateKey]) groups[dateKey] = [];
426
+ groups[dateKey].push(event);
427
+ return groups;
428
+ }, {} as Record<string, TimelineEvent[]>);
429
+ }
430
+ ```
431
+
432
+ ---
433
+
434
+ ## Anti-Patterns
435
+
436
+ ### DON'T: No Date Context
437
+ ```tsx
438
+ // BAD - Timestamps with no anchor
439
+ <span>5 hours ago</span>
440
+ <span>3 hours ago</span>
441
+
442
+ // GOOD - Date separators for context
443
+ <DateSeparator>Today</DateSeparator>
444
+ <Event time="5 hours ago" />
445
+ <DateSeparator>Yesterday</DateSeparator>
446
+ <Event time="3 hours ago" />
447
+ ```
448
+
449
+ ### DON'T: Unvirtualized Long Lists
450
+ ```tsx
451
+ // BAD - Renders 10,000 events at once
452
+ {events.map(e => <Event key={e.id} event={e} />)}
453
+
454
+ // GOOD - Virtualized rendering
455
+ <VirtualList
456
+ items={events}
457
+ itemHeight={80}
458
+ renderItem={e => <Event event={e} />}
459
+ />
460
+ ```
461
+
462
+ ### DON'T: Color-Only Type Indication
463
+ ```tsx
464
+ // BAD - Type only indicated by color
465
+ <div className="w-3 h-3 rounded-full bg-red-500" />
466
+
467
+ // GOOD - Color + icon + label
468
+ <div className="flex items-center gap-2">
469
+ <ErrorIcon className="text-red-500" />
470
+ <span className="text-red-400">Error</span>
471
+ </div>
472
+ ```
473
+
474
+ ---
475
+
476
+ ## Accessibility
477
+
478
+ ### Live Region for New Events
479
+
480
+ ```tsx
481
+ function EventTimeline({ events }: TimelineProps) {
482
+ const [newEvent, setNewEvent] = useState<string | null>(null);
483
+
484
+ // Announce new events
485
+ useEffect(() => {
486
+ const latest = events[0];
487
+ if (latest && latest.id !== previousLatestId.current) {
488
+ setNewEvent(`New event: ${latest.title}`);
489
+ previousLatestId.current = latest.id;
490
+ }
491
+ }, [events]);
492
+
493
+ return (
494
+ <>
495
+ {/* Screen reader announcement */}
496
+ <div aria-live="polite" className="sr-only">
497
+ {newEvent}
498
+ </div>
499
+
500
+ {/* Timeline content */}
501
+ <div role="feed" aria-label="Event timeline">
502
+ {events.map(event => ...)}
503
+ </div>
504
+ </>
505
+ );
506
+ }
507
+ ```
508
+
509
+ ### Keyboard Navigation
510
+
511
+ | Key | Action |
512
+ |-----|--------|
513
+ | `ArrowDown` | Next event |
514
+ | `ArrowUp` | Previous event |
515
+ | `Enter` | Select/expand event |
516
+ | `Home` | First event |
517
+ | `End` | Last event |
518
+ | `PageDown` | Skip 10 events forward |
519
+ | `PageUp` | Skip 10 events backward |
520
+
521
+ ---
522
+
523
+ ## Testing Checklist
524
+
525
+ - [ ] Events display in correct chronological order
526
+ - [ ] Relative timestamps update correctly
527
+ - [ ] Date separators appear between days
528
+ - [ ] Filter by type works correctly
529
+ - [ ] Search filters event content
530
+ - [ ] Infinite scroll loads more events
531
+ - [ ] Selected event is highlighted
532
+ - [ ] Click on event triggers callback
533
+ - [ ] Keyboard navigation works
534
+ - [ ] Screen reader announces events
535
+
536
+ ---
537
+
538
+ ## Related Skills
539
+
540
+ - `playback-replay-patterns` - For event replay
541
+ - `list-page-patterns` - For timeline layouts
542
+ - `data-density-patterns` - For compact event displays