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,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
|