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,695 @@
1
+ # Playback & Replay UI Patterns
2
+
3
+ Patterns for VCR-style playback controls, timeline scrubbing, replay viewers, and media-like navigation. Applies to video players, game replays, animation timelines, event history browsers, and debugging tools.
4
+
5
+ ## When to Use This Skill
6
+
7
+ - Game replay viewers
8
+ - Video/audio players
9
+ - Animation timeline editors
10
+ - Event log replay (debugging)
11
+ - Time-travel debugging
12
+ - Presentation/slideshow controls
13
+ - Data visualization playback
14
+ - Historical data scrubbing
15
+
16
+ ---
17
+
18
+ ## Core Concepts
19
+
20
+ ### Playback States
21
+
22
+ | State | Description | Visual Indicator |
23
+ |-------|-------------|------------------|
24
+ | **Stopped** | At beginning, not playing | Stop icon, progress at 0 |
25
+ | **Playing** | Auto-advancing through timeline | Play icon animating |
26
+ | **Paused** | Frozen at current position | Pause icon, position preserved |
27
+ | **Buffering** | Loading next content | Spinner/loading indicator |
28
+ | **Ended** | Reached end of timeline | Replay option |
29
+
30
+ ### Speed Options
31
+
32
+ ```typescript
33
+ const PLAYBACK_SPEEDS = [0.25, 0.5, 1, 1.5, 2, 4] as const;
34
+
35
+ function formatSpeed(speed: number): string {
36
+ if (speed === 1) return '1x';
37
+ if (speed < 1) return `${speed}x (slow)`;
38
+ return `${speed}x (fast)`;
39
+ }
40
+ ```
41
+
42
+ ---
43
+
44
+ ## Audit Checklist
45
+
46
+ ### Core Controls
47
+ - [ ] [CRITICAL] Play/Pause toggle button (single button, changes icon)
48
+ - [ ] [CRITICAL] Stop button (returns to start)
49
+ - [ ] [CRITICAL] Controls accessible via keyboard (Space = play/pause)
50
+ - [ ] [MAJOR] Step forward (advance one frame/event)
51
+ - [ ] [MAJOR] Step backward (go back one frame/event)
52
+ - [ ] [MINOR] Go to start button
53
+ - [ ] [MINOR] Go to end button
54
+
55
+ ### Timeline/Progress
56
+ - [ ] [CRITICAL] Progress bar showing current position
57
+ - [ ] [CRITICAL] Click on timeline to seek to position
58
+ - [ ] [MAJOR] Drag scrubber for precise seeking
59
+ - [ ] [MAJOR] Current time/position displayed
60
+ - [ ] [MAJOR] Total duration/length displayed
61
+ - [ ] [MINOR] Thumbnail preview on hover (for video)
62
+ - [ ] [MINOR] Event markers on timeline
63
+
64
+ ### Speed Control
65
+ - [ ] [MAJOR] Speed selector dropdown or buttons
66
+ - [ ] [MAJOR] Current speed displayed
67
+ - [ ] [MINOR] Keyboard shortcuts for speed (+/-)
68
+ - [ ] [MINOR] Speed persists across sessions
69
+
70
+ ### Event/Frame Information
71
+ - [ ] [MAJOR] Current event/frame details shown
72
+ - [ ] [MAJOR] Sequence number displayed (e.g., "Event 5 of 120")
73
+ - [ ] [MINOR] Event type/category indicator
74
+ - [ ] [MINOR] Timestamp of current event
75
+
76
+ ### Keyboard Shortcuts
77
+ - [ ] [CRITICAL] Space = Play/Pause
78
+ - [ ] [MAJOR] Left/Right arrows = Step backward/forward
79
+ - [ ] [MAJOR] Cmd/Ctrl+Left/Right = Jump to start/end
80
+ - [ ] [MINOR] [ / ] = Decrease/increase speed
81
+ - [ ] [MINOR] Escape = Stop
82
+ - [ ] [MINOR] Shortcuts discoverable (help modal)
83
+
84
+ ### Visual Feedback
85
+ - [ ] [CRITICAL] Playing state obviously different from paused
86
+ - [ ] [MAJOR] Progress updates smoothly during playback
87
+ - [ ] [MAJOR] Seeking provides immediate visual feedback
88
+ - [ ] [MINOR] Speed change confirmation (toast/indicator)
89
+ - [ ] [MINOR] Animation on state transitions
90
+
91
+ ### Accessibility
92
+ - [ ] [CRITICAL] All controls keyboard accessible
93
+ - [ ] [CRITICAL] ARIA labels on all buttons
94
+ - [ ] [MAJOR] Screen reader announces state changes
95
+ - [ ] [MAJOR] Focus visible on all controls
96
+ - [ ] [MINOR] High contrast mode support
97
+ - [ ] [MINOR] Controls work with reduced motion
98
+
99
+ ---
100
+
101
+ ## Implementation Patterns
102
+
103
+ ### Playback State Hook
104
+
105
+ ```typescript
106
+ type PlaybackState = 'stopped' | 'playing' | 'paused';
107
+
108
+ interface UseReplayPlayerOptions {
109
+ events: readonly Event[];
110
+ initialIndex?: number;
111
+ autoPlay?: boolean;
112
+ baseInterval?: number; // ms between events at 1x speed
113
+ }
114
+
115
+ interface UseReplayPlayerReturn {
116
+ // State
117
+ playbackState: PlaybackState;
118
+ currentIndex: number;
119
+ currentEvent: Event | null;
120
+ progress: number; // 0-100
121
+ speed: number;
122
+ totalEvents: number;
123
+
124
+ // Actions
125
+ play: () => void;
126
+ pause: () => void;
127
+ stop: () => void;
128
+ stepForward: () => void;
129
+ stepBackward: () => void;
130
+ jumpToIndex: (index: number) => void;
131
+ jumpToEvent: (eventId: string) => void;
132
+ seek: (progress: number) => void; // 0-100
133
+ setSpeed: (speed: number) => void;
134
+ }
135
+
136
+ function useReplayPlayer({
137
+ events,
138
+ initialIndex = 0,
139
+ autoPlay = false,
140
+ baseInterval = 1000,
141
+ }: UseReplayPlayerOptions): UseReplayPlayerReturn {
142
+ const [playbackState, setPlaybackState] = useState<PlaybackState>(
143
+ autoPlay ? 'playing' : 'stopped'
144
+ );
145
+ const [currentIndex, setCurrentIndex] = useState(initialIndex);
146
+ const [speed, setSpeed] = useState(1);
147
+
148
+ // Auto-advance interval
149
+ useEffect(() => {
150
+ if (playbackState !== 'playing') return;
151
+ if (currentIndex >= events.length - 1) {
152
+ setPlaybackState('paused');
153
+ return;
154
+ }
155
+
156
+ const interval = baseInterval / speed;
157
+ const timer = setTimeout(() => {
158
+ setCurrentIndex(prev => prev + 1);
159
+ }, interval);
160
+
161
+ return () => clearTimeout(timer);
162
+ }, [playbackState, currentIndex, speed, baseInterval, events.length]);
163
+
164
+ const play = useCallback(() => {
165
+ if (currentIndex >= events.length - 1) {
166
+ setCurrentIndex(0); // Restart if at end
167
+ }
168
+ setPlaybackState('playing');
169
+ }, [currentIndex, events.length]);
170
+
171
+ const pause = useCallback(() => setPlaybackState('paused'), []);
172
+
173
+ const stop = useCallback(() => {
174
+ setPlaybackState('stopped');
175
+ setCurrentIndex(0);
176
+ }, []);
177
+
178
+ const stepForward = useCallback(() => {
179
+ setPlaybackState('paused');
180
+ setCurrentIndex(prev => Math.min(prev + 1, events.length - 1));
181
+ }, [events.length]);
182
+
183
+ const stepBackward = useCallback(() => {
184
+ setPlaybackState('paused');
185
+ setCurrentIndex(prev => Math.max(prev - 1, 0));
186
+ }, []);
187
+
188
+ const seek = useCallback((progress: number) => {
189
+ const index = Math.round((progress / 100) * (events.length - 1));
190
+ setCurrentIndex(index);
191
+ }, [events.length]);
192
+
193
+ return {
194
+ playbackState,
195
+ currentIndex,
196
+ currentEvent: events[currentIndex] ?? null,
197
+ progress: events.length > 1 ? (currentIndex / (events.length - 1)) * 100 : 0,
198
+ speed,
199
+ totalEvents: events.length,
200
+ play,
201
+ pause,
202
+ stop,
203
+ stepForward,
204
+ stepBackward,
205
+ jumpToIndex: setCurrentIndex,
206
+ jumpToEvent: (id) => {
207
+ const idx = events.findIndex(e => e.id === id);
208
+ if (idx >= 0) setCurrentIndex(idx);
209
+ },
210
+ seek,
211
+ setSpeed,
212
+ };
213
+ }
214
+ ```
215
+
216
+ ### Replay Controls Component
217
+
218
+ ```tsx
219
+ interface ReplayControlsProps {
220
+ playbackState: PlaybackState;
221
+ canStepBackward: boolean;
222
+ canStepForward: boolean;
223
+ onPlay: () => void;
224
+ onPause: () => void;
225
+ onStop: () => void;
226
+ onStepForward: () => void;
227
+ onStepBackward: () => void;
228
+ }
229
+
230
+ function ReplayControls({
231
+ playbackState,
232
+ canStepBackward,
233
+ canStepForward,
234
+ onPlay,
235
+ onPause,
236
+ onStop,
237
+ onStepForward,
238
+ onStepBackward,
239
+ }: ReplayControlsProps) {
240
+ const isPlaying = playbackState === 'playing';
241
+
242
+ return (
243
+ <div
244
+ className="flex items-center gap-2"
245
+ role="group"
246
+ aria-label="Playback controls"
247
+ >
248
+ {/* Stop */}
249
+ <button
250
+ onClick={onStop}
251
+ className="p-2 rounded-lg hover:bg-surface-raised"
252
+ aria-label="Stop and return to start"
253
+ title="Stop (Esc)"
254
+ >
255
+ <StopIcon className="w-5 h-5" />
256
+ </button>
257
+
258
+ {/* Step Backward */}
259
+ <button
260
+ onClick={onStepBackward}
261
+ disabled={!canStepBackward}
262
+ className="p-2 rounded-lg hover:bg-surface-raised disabled:opacity-50"
263
+ aria-label="Step backward"
264
+ title="Previous event (Left arrow)"
265
+ >
266
+ <StepBackIcon className="w-5 h-5" />
267
+ </button>
268
+
269
+ {/* Play/Pause */}
270
+ <button
271
+ onClick={isPlaying ? onPause : onPlay}
272
+ className="p-3 rounded-full bg-accent hover:bg-accent/80"
273
+ aria-label={isPlaying ? 'Pause' : 'Play'}
274
+ title={isPlaying ? 'Pause (Space)' : 'Play (Space)'}
275
+ >
276
+ {isPlaying ? (
277
+ <PauseIcon className="w-6 h-6 text-white" />
278
+ ) : (
279
+ <PlayIcon className="w-6 h-6 text-white" />
280
+ )}
281
+ </button>
282
+
283
+ {/* Step Forward */}
284
+ <button
285
+ onClick={onStepForward}
286
+ disabled={!canStepForward}
287
+ className="p-2 rounded-lg hover:bg-surface-raised disabled:opacity-50"
288
+ aria-label="Step forward"
289
+ title="Next event (Right arrow)"
290
+ >
291
+ <StepForwardIcon className="w-5 h-5" />
292
+ </button>
293
+ </div>
294
+ );
295
+ }
296
+ ```
297
+
298
+ ### Timeline Component
299
+
300
+ ```tsx
301
+ interface ReplayTimelineProps {
302
+ progress: number; // 0-100
303
+ markers?: TimelineMarker[];
304
+ onSeek: (progress: number) => void;
305
+ currentSequence?: number;
306
+ }
307
+
308
+ interface TimelineMarker {
309
+ position: number; // 0-100
310
+ color: string;
311
+ label: string;
312
+ }
313
+
314
+ function ReplayTimeline({ progress, markers = [], onSeek, currentSequence }: ReplayTimelineProps) {
315
+ const trackRef = useRef<HTMLDivElement>(null);
316
+ const [isDragging, setIsDragging] = useState(false);
317
+ const [hoverPosition, setHoverPosition] = useState<number | null>(null);
318
+
319
+ const handleClick = (e: React.MouseEvent) => {
320
+ if (!trackRef.current) return;
321
+ const rect = trackRef.current.getBoundingClientRect();
322
+ const position = ((e.clientX - rect.left) / rect.width) * 100;
323
+ onSeek(Math.max(0, Math.min(100, position)));
324
+ };
325
+
326
+ const handleMouseMove = (e: React.MouseEvent) => {
327
+ if (!trackRef.current) return;
328
+ const rect = trackRef.current.getBoundingClientRect();
329
+ const position = ((e.clientX - rect.left) / rect.width) * 100;
330
+ setHoverPosition(Math.max(0, Math.min(100, position)));
331
+
332
+ if (isDragging) {
333
+ onSeek(position);
334
+ }
335
+ };
336
+
337
+ return (
338
+ <div className="w-full">
339
+ {/* Track */}
340
+ <div
341
+ ref={trackRef}
342
+ className="relative h-2 bg-gray-700 rounded-full cursor-pointer"
343
+ onClick={handleClick}
344
+ onMouseMove={handleMouseMove}
345
+ onMouseLeave={() => setHoverPosition(null)}
346
+ onMouseDown={() => setIsDragging(true)}
347
+ onMouseUp={() => setIsDragging(false)}
348
+ role="slider"
349
+ aria-label="Playback position"
350
+ aria-valuemin={0}
351
+ aria-valuemax={100}
352
+ aria-valuenow={Math.round(progress)}
353
+ tabIndex={0}
354
+ >
355
+ {/* Progress fill */}
356
+ <div
357
+ className="absolute left-0 top-0 h-full bg-accent rounded-full transition-all"
358
+ style={{ width: `${progress}%` }}
359
+ />
360
+
361
+ {/* Hover indicator */}
362
+ {hoverPosition !== null && (
363
+ <div
364
+ className="absolute top-0 h-full w-0.5 bg-white/50"
365
+ style={{ left: `${hoverPosition}%` }}
366
+ />
367
+ )}
368
+
369
+ {/* Markers */}
370
+ {markers.map((marker, idx) => (
371
+ <div
372
+ key={idx}
373
+ className="absolute top-1/2 -translate-y-1/2 w-2 h-2 rounded-full"
374
+ style={{
375
+ left: `${marker.position}%`,
376
+ backgroundColor: marker.color,
377
+ transform: 'translate(-50%, -50%)'
378
+ }}
379
+ title={marker.label}
380
+ />
381
+ ))}
382
+
383
+ {/* Scrubber handle */}
384
+ <div
385
+ className="absolute top-1/2 -translate-y-1/2 w-4 h-4 bg-white rounded-full shadow-lg border-2 border-accent"
386
+ style={{ left: `${progress}%`, transform: 'translate(-50%, -50%)' }}
387
+ />
388
+ </div>
389
+
390
+ {/* Position label */}
391
+ {currentSequence !== undefined && (
392
+ <div className="flex justify-between mt-1 text-xs text-gray-500">
393
+ <span>Event {currentSequence}</span>
394
+ <span>{Math.round(progress)}%</span>
395
+ </div>
396
+ )}
397
+ </div>
398
+ );
399
+ }
400
+ ```
401
+
402
+ ### Speed Selector Component
403
+
404
+ ```tsx
405
+ const PLAYBACK_SPEEDS = [0.25, 0.5, 1, 1.5, 2, 4] as const;
406
+
407
+ interface ReplaySpeedSelectorProps {
408
+ speed: number;
409
+ onSpeedChange: (speed: number) => void;
410
+ }
411
+
412
+ function ReplaySpeedSelector({ speed, onSpeedChange }: ReplaySpeedSelectorProps) {
413
+ return (
414
+ <div className="flex items-center gap-1" role="group" aria-label="Playback speed">
415
+ {PLAYBACK_SPEEDS.map(s => (
416
+ <button
417
+ key={s}
418
+ onClick={() => onSpeedChange(s)}
419
+ className={`
420
+ px-2 py-1 text-xs rounded transition-colors
421
+ ${speed === s
422
+ ? 'bg-accent text-white'
423
+ : 'text-gray-400 hover:text-white hover:bg-gray-700'
424
+ }
425
+ `}
426
+ aria-pressed={speed === s}
427
+ aria-label={`${s}x speed`}
428
+ >
429
+ {s}x
430
+ </button>
431
+ ))}
432
+ </div>
433
+ );
434
+ }
435
+ ```
436
+
437
+ ### Keyboard Shortcuts Hook
438
+
439
+ ```tsx
440
+ interface UseReplayKeyboardShortcutsOptions {
441
+ onPlay: () => void;
442
+ onPause: () => void;
443
+ onStepForward: () => void;
444
+ onStepBackward: () => void;
445
+ onSpeedUp: () => void;
446
+ onSpeedDown: () => void;
447
+ onGoToStart: () => void;
448
+ onGoToEnd: () => void;
449
+ playbackState: PlaybackState;
450
+ enabled?: boolean;
451
+ }
452
+
453
+ function useReplayKeyboardShortcuts({
454
+ onPlay,
455
+ onPause,
456
+ onStepForward,
457
+ onStepBackward,
458
+ onSpeedUp,
459
+ onSpeedDown,
460
+ onGoToStart,
461
+ onGoToEnd,
462
+ playbackState,
463
+ enabled = true,
464
+ }: UseReplayKeyboardShortcutsOptions) {
465
+ useEffect(() => {
466
+ if (!enabled) return;
467
+
468
+ const handleKeyDown = (e: KeyboardEvent) => {
469
+ // Ignore if typing in input
470
+ if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) {
471
+ return;
472
+ }
473
+
474
+ switch (e.key) {
475
+ case ' ':
476
+ e.preventDefault();
477
+ playbackState === 'playing' ? onPause() : onPlay();
478
+ break;
479
+ case 'ArrowLeft':
480
+ e.preventDefault();
481
+ if (e.metaKey || e.ctrlKey) {
482
+ onGoToStart();
483
+ } else {
484
+ onStepBackward();
485
+ }
486
+ break;
487
+ case 'ArrowRight':
488
+ e.preventDefault();
489
+ if (e.metaKey || e.ctrlKey) {
490
+ onGoToEnd();
491
+ } else {
492
+ onStepForward();
493
+ }
494
+ break;
495
+ case '[':
496
+ e.preventDefault();
497
+ onSpeedDown();
498
+ break;
499
+ case ']':
500
+ e.preventDefault();
501
+ onSpeedUp();
502
+ break;
503
+ case 'Escape':
504
+ e.preventDefault();
505
+ onGoToStart();
506
+ break;
507
+ }
508
+ };
509
+
510
+ document.addEventListener('keydown', handleKeyDown);
511
+ return () => document.removeEventListener('keydown', handleKeyDown);
512
+ }, [enabled, playbackState, onPlay, onPause, onStepForward, onStepBackward, onSpeedUp, onSpeedDown, onGoToStart, onGoToEnd]);
513
+ }
514
+ ```
515
+
516
+ ---
517
+
518
+ ## Visual States
519
+
520
+ ### Control Button States
521
+
522
+ ```typescript
523
+ const CONTROL_STATES = {
524
+ default: {
525
+ bg: 'bg-transparent',
526
+ hover: 'hover:bg-surface-raised',
527
+ text: 'text-gray-400',
528
+ },
529
+ active: {
530
+ bg: 'bg-accent',
531
+ hover: 'hover:bg-accent/80',
532
+ text: 'text-white',
533
+ },
534
+ disabled: {
535
+ bg: 'bg-transparent',
536
+ hover: '',
537
+ text: 'text-gray-600',
538
+ cursor: 'cursor-not-allowed',
539
+ },
540
+ };
541
+ ```
542
+
543
+ ### Playback State Indicators
544
+
545
+ ```typescript
546
+ const PLAYBACK_INDICATORS = {
547
+ stopped: {
548
+ text: 'Stopped',
549
+ color: 'text-gray-400',
550
+ icon: 'stop',
551
+ },
552
+ playing: {
553
+ text: 'Playing',
554
+ color: 'text-emerald-400',
555
+ icon: 'play',
556
+ pulse: true,
557
+ },
558
+ paused: {
559
+ text: 'Paused',
560
+ color: 'text-amber-400',
561
+ icon: 'pause',
562
+ },
563
+ };
564
+ ```
565
+
566
+ ---
567
+
568
+ ## Anti-Patterns
569
+
570
+ ### DON'T: Separate Play and Pause Buttons
571
+ ```tsx
572
+ // BAD - Takes up space, confusing
573
+ <button onClick={play}>Play</button>
574
+ <button onClick={pause}>Pause</button>
575
+
576
+ // GOOD - Single toggle button
577
+ <button onClick={isPlaying ? pause : play}>
578
+ {isPlaying ? <PauseIcon /> : <PlayIcon />}
579
+ </button>
580
+ ```
581
+
582
+ ### DON'T: No Keyboard Support
583
+ ```tsx
584
+ // BAD - Mouse only
585
+ <div onClick={handleSeek}>...</div>
586
+
587
+ // GOOD - Full keyboard support
588
+ <div
589
+ onClick={handleSeek}
590
+ onKeyDown={e => e.key === 'Enter' && handleSeek(e)}
591
+ tabIndex={0}
592
+ role="slider"
593
+ aria-label="Seek"
594
+ >...</div>
595
+ ```
596
+
597
+ ### DON'T: Hidden Progress Information
598
+ ```tsx
599
+ // BAD - No indication of position
600
+ <ProgressBar progress={progress} />
601
+
602
+ // GOOD - Clear position indication
603
+ <div>
604
+ <ProgressBar progress={progress} />
605
+ <span>Event {current} of {total} ({Math.round(progress)}%)</span>
606
+ </div>
607
+ ```
608
+
609
+ ### DON'T: No Speed Indication
610
+ ```tsx
611
+ // BAD - Speed selector without current value
612
+ <SpeedSelector onChange={setSpeed} />
613
+
614
+ // GOOD - Current speed always visible
615
+ <div className="flex items-center gap-2">
616
+ <span className="text-sm text-gray-400">Speed:</span>
617
+ <SpeedSelector speed={speed} onChange={setSpeed} />
618
+ </div>
619
+ ```
620
+
621
+ ---
622
+
623
+ ## Accessibility
624
+
625
+ ### ARIA Attributes
626
+
627
+ ```tsx
628
+ // Progress bar
629
+ <div
630
+ role="slider"
631
+ aria-label="Playback progress"
632
+ aria-valuemin={0}
633
+ aria-valuemax={100}
634
+ aria-valuenow={progress}
635
+ aria-valuetext={`${currentIndex + 1} of ${totalEvents} events`}
636
+ tabIndex={0}
637
+ >
638
+
639
+ // Play/Pause button
640
+ <button
641
+ aria-label={isPlaying ? 'Pause playback' : 'Start playback'}
642
+ aria-pressed={isPlaying}
643
+ >
644
+
645
+ // Speed selector
646
+ <div role="group" aria-label="Playback speed selection">
647
+ <button aria-pressed={speed === 1} aria-label="Normal speed">1x</button>
648
+ <button aria-pressed={speed === 2} aria-label="Double speed">2x</button>
649
+ </div>
650
+ ```
651
+
652
+ ### Screen Reader Announcements
653
+
654
+ ```tsx
655
+ function usePlaybackAnnouncements(playbackState: PlaybackState, currentIndex: number) {
656
+ const prevState = useRef(playbackState);
657
+
658
+ useEffect(() => {
659
+ if (playbackState !== prevState.current) {
660
+ const messages = {
661
+ playing: 'Playback started',
662
+ paused: `Playback paused at event ${currentIndex + 1}`,
663
+ stopped: 'Playback stopped, returned to start',
664
+ };
665
+ announceToScreenReader(messages[playbackState]);
666
+ prevState.current = playbackState;
667
+ }
668
+ }, [playbackState, currentIndex]);
669
+ }
670
+ ```
671
+
672
+ ---
673
+
674
+ ## Testing Checklist
675
+
676
+ - [ ] Play starts playback from current position
677
+ - [ ] Pause stops playback without changing position
678
+ - [ ] Stop returns to position 0
679
+ - [ ] Step forward advances by one event
680
+ - [ ] Step backward goes back by one event
681
+ - [ ] Seek jumps to clicked position
682
+ - [ ] Speed changes affect playback rate
683
+ - [ ] Space bar toggles play/pause
684
+ - [ ] Arrow keys step forward/backward
685
+ - [ ] Progress bar updates during playback
686
+ - [ ] Position indicator shows correct event number
687
+ - [ ] Controls disable at appropriate boundaries
688
+
689
+ ---
690
+
691
+ ## Related Skills
692
+
693
+ - `event-timeline-patterns` - For event list display
694
+ - `keyboard-shortcuts-patterns` - For shortcut implementation
695
+ - `turn-based-ui-patterns` - For game replay context