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,635 @@
1
+ # Status Visualization UI Patterns
2
+
3
+ Patterns for visualizing status, health, progress, and resource values. Applies to health bars, progress indicators, gauges, resource meters, and any numeric status display.
4
+
5
+ ## When to Use This Skill
6
+
7
+ - Health/HP bars (games)
8
+ - Progress bars and indicators
9
+ - Resource meters (mana, energy, fuel)
10
+ - Capacity gauges (storage, battery)
11
+ - Temperature/heat displays
12
+ - Skill/stat bars (RPGs)
13
+ - Budget/quota tracking
14
+ - Performance metrics
15
+
16
+ ---
17
+
18
+ ## Core Concepts
19
+
20
+ ### Visualization Types
21
+
22
+ | Type | Use Case | Best For |
23
+ |------|----------|----------|
24
+ | **Bar** | Linear progress, capacity | Health, storage, progress |
25
+ | **Pip/Segment** | Discrete units | Lives, charges, armor points |
26
+ | **Gauge/Arc** | Circular display | Speed, percentage, completion |
27
+ | **Numeric** | Exact values | Stats, scores, counts |
28
+ | **Threshold** | Range-based status | Temperature, danger levels |
29
+
30
+ ### Value States
31
+
32
+ | State | Color | Threshold (typical) |
33
+ |-------|-------|---------------------|
34
+ | **Healthy/Good** | Green | 75-100% |
35
+ | **Warning** | Yellow/Amber | 25-74% |
36
+ | **Critical** | Red | 1-24% |
37
+ | **Depleted/Zero** | Gray | 0% |
38
+ | **Overload** | Purple/Pink | >100% |
39
+
40
+ ---
41
+
42
+ ## Audit Checklist
43
+
44
+ ### Bar/Progress Display
45
+ - [ ] [CRITICAL] Current value visually represented
46
+ - [ ] [CRITICAL] Maximum value indicated (bar length or number)
47
+ - [ ] [MAJOR] Percentage/ratio readable at a glance
48
+ - [ ] [MAJOR] Color reflects status (green/yellow/red)
49
+ - [ ] [MAJOR] Smooth animation on value change
50
+ - [ ] [MINOR] Tooltip with exact values
51
+ - [ ] [MINOR] Accessibility: announced to screen readers
52
+
53
+ ### Pip/Segment Display
54
+ - [ ] [CRITICAL] Filled vs empty pips clearly distinct
55
+ - [ ] [CRITICAL] Total count immediately visible
56
+ - [ ] [MAJOR] Consistent pip sizing
57
+ - [ ] [MAJOR] Partial pip state (if applicable)
58
+ - [ ] [MINOR] Pip groupings for large counts (5s, 10s)
59
+ - [ ] [MINOR] Animation on pip change
60
+
61
+ ### Numeric Display
62
+ - [ ] [CRITICAL] Current value prominent
63
+ - [ ] [CRITICAL] Maximum value visible (if applicable)
64
+ - [ ] [MAJOR] Unit/label included
65
+ - [ ] [MAJOR] Formatted appropriately (commas, decimals)
66
+ - [ ] [MINOR] Change indicator (delta, arrow)
67
+ - [ ] [MINOR] Comparison to baseline/previous
68
+
69
+ ### Threshold Indicators
70
+ - [ ] [CRITICAL] Current level clearly marked
71
+ - [ ] [CRITICAL] Threshold boundaries visible
72
+ - [ ] [MAJOR] Labels for each threshold zone
73
+ - [ ] [MAJOR] Visual warning at dangerous thresholds
74
+ - [ ] [MINOR] Effects list at current threshold
75
+ - [ ] [MINOR] Next threshold distance shown
76
+
77
+ ### Color & Contrast
78
+ - [ ] [CRITICAL] Status colors distinguishable
79
+ - [ ] [CRITICAL] Sufficient contrast (WCAG AA)
80
+ - [ ] [MAJOR] Color not the only indicator
81
+ - [ ] [MAJOR] Consistent color scheme across app
82
+ - [ ] [MINOR] High contrast mode support
83
+ - [ ] [MINOR] Color blind friendly alternatives
84
+
85
+ ### Animation & Feedback
86
+ - [ ] [MAJOR] Smooth transitions on value change
87
+ - [ ] [MAJOR] Flash/pulse on critical threshold
88
+ - [ ] [MINOR] Damage/change animation
89
+ - [ ] [MINOR] Celebration on full/completion
90
+
91
+ ### Accessibility
92
+ - [ ] [CRITICAL] Values announced to screen readers
93
+ - [ ] [CRITICAL] Color + text/shape for status
94
+ - [ ] [MAJOR] aria-valuenow, aria-valuemin, aria-valuemax
95
+ - [ ] [MAJOR] Accessible name for each meter
96
+ - [ ] [MINOR] Announce threshold changes
97
+
98
+ ---
99
+
100
+ ## Implementation Patterns
101
+
102
+ ### Progress Bar Component
103
+
104
+ ```tsx
105
+ interface ProgressBarProps {
106
+ value: number;
107
+ max: number;
108
+ label?: string;
109
+ showValue?: boolean;
110
+ size?: 'sm' | 'md' | 'lg';
111
+ variant?: 'default' | 'segmented';
112
+ animated?: boolean;
113
+ }
114
+
115
+ function ProgressBar({
116
+ value,
117
+ max,
118
+ label,
119
+ showValue = true,
120
+ size = 'md',
121
+ variant = 'default',
122
+ animated = true,
123
+ }: ProgressBarProps) {
124
+ const percentage = Math.min(100, Math.max(0, (value / max) * 100));
125
+ const color = getStatusColor(percentage);
126
+
127
+ const sizeClasses = {
128
+ sm: 'h-1',
129
+ md: 'h-2',
130
+ lg: 'h-4',
131
+ };
132
+
133
+ return (
134
+ <div className="w-full">
135
+ {/* Label row */}
136
+ {(label || showValue) && (
137
+ <div className="flex justify-between items-center mb-1">
138
+ {label && (
139
+ <span className="text-sm text-text-theme-secondary">{label}</span>
140
+ )}
141
+ {showValue && (
142
+ <span className="text-sm font-mono text-text-theme-primary">
143
+ {value}/{max}
144
+ </span>
145
+ )}
146
+ </div>
147
+ )}
148
+
149
+ {/* Bar */}
150
+ <div
151
+ className={`w-full bg-gray-700 rounded-full overflow-hidden ${sizeClasses[size]}`}
152
+ role="progressbar"
153
+ aria-valuenow={value}
154
+ aria-valuemin={0}
155
+ aria-valuemax={max}
156
+ aria-label={label || 'Progress'}
157
+ >
158
+ <div
159
+ className={`
160
+ h-full ${color} rounded-full
161
+ ${animated ? 'transition-all duration-300 ease-out' : ''}
162
+ `}
163
+ style={{ width: `${percentage}%` }}
164
+ />
165
+ </div>
166
+ </div>
167
+ );
168
+ }
169
+
170
+ function getStatusColor(percentage: number): string {
171
+ if (percentage <= 25) return 'bg-red-500';
172
+ if (percentage <= 50) return 'bg-amber-500';
173
+ if (percentage <= 75) return 'bg-yellow-500';
174
+ return 'bg-emerald-500';
175
+ }
176
+ ```
177
+
178
+ ### Health Bar with Damage Animation
179
+
180
+ ```tsx
181
+ interface HealthBarProps {
182
+ current: number;
183
+ max: number;
184
+ previousValue?: number;
185
+ showDamage?: boolean;
186
+ }
187
+
188
+ function HealthBar({ current, max, previousValue, showDamage = true }: HealthBarProps) {
189
+ const currentPercent = (current / max) * 100;
190
+ const previousPercent = previousValue ? (previousValue / max) * 100 : currentPercent;
191
+ const damage = previousPercent - currentPercent;
192
+
193
+ return (
194
+ <div className="relative w-full h-4 bg-gray-800 rounded overflow-hidden">
195
+ {/* Damage flash (shows briefly when taking damage) */}
196
+ {showDamage && damage > 0 && (
197
+ <div
198
+ className="absolute h-full bg-red-400 animate-damage-flash"
199
+ style={{
200
+ left: `${currentPercent}%`,
201
+ width: `${damage}%`
202
+ }}
203
+ />
204
+ )}
205
+
206
+ {/* Current health */}
207
+ <div
208
+ className={`
209
+ absolute h-full transition-all duration-500 ease-out
210
+ ${currentPercent <= 25 ? 'bg-red-500' : ''}
211
+ ${currentPercent > 25 && currentPercent <= 50 ? 'bg-amber-500' : ''}
212
+ ${currentPercent > 50 ? 'bg-emerald-500' : ''}
213
+ `}
214
+ style={{ width: `${currentPercent}%` }}
215
+ />
216
+
217
+ {/* Value overlay */}
218
+ <div className="absolute inset-0 flex items-center justify-center">
219
+ <span className="text-xs font-bold text-white drop-shadow">
220
+ {current} / {max}
221
+ </span>
222
+ </div>
223
+ </div>
224
+ );
225
+ }
226
+
227
+ // CSS for damage flash animation
228
+ // @keyframes damage-flash {
229
+ // 0% { opacity: 1; }
230
+ // 100% { opacity: 0; width: 0; }
231
+ // }
232
+ ```
233
+
234
+ ### Pip Display Component
235
+
236
+ ```tsx
237
+ interface PipDisplayProps {
238
+ filled: number;
239
+ total: number;
240
+ filledColor?: string;
241
+ emptyColor?: string;
242
+ size?: 'sm' | 'md' | 'lg';
243
+ groupSize?: number;
244
+ }
245
+
246
+ function PipDisplay({
247
+ filled,
248
+ total,
249
+ filledColor = 'bg-emerald-500',
250
+ emptyColor = 'bg-gray-600',
251
+ size = 'md',
252
+ groupSize = 5,
253
+ }: PipDisplayProps) {
254
+ const sizeClasses = {
255
+ sm: 'w-2 h-2',
256
+ md: 'w-3 h-3',
257
+ lg: 'w-4 h-4',
258
+ };
259
+
260
+ const pips = [];
261
+ for (let i = 0; i < total; i++) {
262
+ const isFilled = i < filled;
263
+ const isGroupEnd = (i + 1) % groupSize === 0 && i < total - 1;
264
+
265
+ pips.push(
266
+ <React.Fragment key={i}>
267
+ <span
268
+ className={`
269
+ rounded-full border
270
+ ${sizeClasses[size]}
271
+ ${isFilled
272
+ ? `${filledColor} border-transparent`
273
+ : `${emptyColor} border-gray-500`
274
+ }
275
+ `}
276
+ aria-hidden="true"
277
+ />
278
+ {isGroupEnd && <span className="w-1" />}
279
+ </React.Fragment>
280
+ );
281
+ }
282
+
283
+ return (
284
+ <div
285
+ className="flex items-center gap-1"
286
+ role="meter"
287
+ aria-valuenow={filled}
288
+ aria-valuemin={0}
289
+ aria-valuemax={total}
290
+ aria-label={`${filled} of ${total}`}
291
+ >
292
+ {pips}
293
+ </div>
294
+ );
295
+ }
296
+ ```
297
+
298
+ ### Heat/Temperature Gauge
299
+
300
+ ```tsx
301
+ interface HeatGaugeProps {
302
+ heat: number;
303
+ maxHeat: number;
304
+ heatSinks: number;
305
+ thresholds?: HeatThreshold[];
306
+ }
307
+
308
+ interface HeatThreshold {
309
+ value: number;
310
+ label: string;
311
+ effects: string[];
312
+ }
313
+
314
+ const DEFAULT_HEAT_THRESHOLDS: HeatThreshold[] = [
315
+ { value: 0, label: 'Cool', effects: [] },
316
+ { value: 5, label: 'Warm', effects: ['+1 to hit'] },
317
+ { value: 10, label: 'Hot', effects: ['+2 to hit', 'Movement -1'] },
318
+ { value: 15, label: 'Critical', effects: ['+3 to hit', 'Movement -2', 'Shutdown risk'] },
319
+ ];
320
+
321
+ function HeatGauge({
322
+ heat,
323
+ maxHeat,
324
+ heatSinks,
325
+ thresholds = DEFAULT_HEAT_THRESHOLDS
326
+ }: HeatGaugeProps) {
327
+ const percentage = (heat / maxHeat) * 100;
328
+ const currentThreshold = thresholds.reduce((prev, curr) =>
329
+ heat >= curr.value ? curr : prev
330
+ );
331
+
332
+ const getHeatColor = () => {
333
+ if (heat >= 15) return 'bg-red-500';
334
+ if (heat >= 10) return 'bg-orange-500';
335
+ if (heat >= 5) return 'bg-amber-500';
336
+ return 'bg-cyan-500';
337
+ };
338
+
339
+ return (
340
+ <div className="bg-gray-900 rounded-lg p-4">
341
+ {/* Header */}
342
+ <div className="flex justify-between items-center mb-2">
343
+ <span className="text-sm font-medium text-text-theme-secondary">
344
+ Heat ({heatSinks} sinks)
345
+ </span>
346
+ <span className={`text-lg font-bold ${heat >= 15 ? 'text-red-400 animate-pulse' : 'text-text-theme-primary'}`}>
347
+ {heat} / {maxHeat}
348
+ </span>
349
+ </div>
350
+
351
+ {/* Bar with threshold markers */}
352
+ <div className="relative">
353
+ <div className="h-4 bg-gray-700 rounded-full overflow-hidden">
354
+ <div
355
+ className={`h-full ${getHeatColor()} transition-all duration-300`}
356
+ style={{ width: `${percentage}%` }}
357
+ />
358
+ </div>
359
+
360
+ {/* Threshold markers */}
361
+ {thresholds.map((t, idx) => (
362
+ <div
363
+ key={idx}
364
+ className="absolute top-0 h-4 w-0.5 bg-gray-500"
365
+ style={{ left: `${(t.value / maxHeat) * 100}%` }}
366
+ title={t.label}
367
+ />
368
+ ))}
369
+ </div>
370
+
371
+ {/* Current effects */}
372
+ {currentThreshold.effects.length > 0 && (
373
+ <div className="mt-2 text-xs text-red-400">
374
+ {currentThreshold.effects.join(' • ')}
375
+ </div>
376
+ )}
377
+
378
+ {/* Dissipation info */}
379
+ <div className="mt-1 text-xs text-text-theme-muted">
380
+ Dissipation: {heatSinks} heat/turn
381
+ </div>
382
+ </div>
383
+ );
384
+ }
385
+ ```
386
+
387
+ ### Stat Block with Bars
388
+
389
+ ```tsx
390
+ interface StatBarProps {
391
+ label: string;
392
+ current: number;
393
+ max: number;
394
+ color?: string;
395
+ }
396
+
397
+ function StatBar({ label, current, max, color = 'bg-accent' }: StatBarProps) {
398
+ const percentage = (current / max) * 100;
399
+
400
+ return (
401
+ <div className="flex items-center gap-3">
402
+ <span className="w-20 text-sm text-text-theme-secondary">{label}</span>
403
+ <div className="flex-1 h-2 bg-gray-700 rounded-full overflow-hidden">
404
+ <div
405
+ className={`h-full ${color} transition-all duration-300`}
406
+ style={{ width: `${percentage}%` }}
407
+ />
408
+ </div>
409
+ <span className="w-16 text-right text-sm font-mono">
410
+ {current}/{max}
411
+ </span>
412
+ </div>
413
+ );
414
+ }
415
+
416
+ function StatBlock({ stats }: { stats: StatBarProps[] }) {
417
+ return (
418
+ <div className="space-y-2">
419
+ {stats.map((stat, idx) => (
420
+ <StatBar key={idx} {...stat} />
421
+ ))}
422
+ </div>
423
+ );
424
+ }
425
+ ```
426
+
427
+ ### Circular Gauge
428
+
429
+ ```tsx
430
+ interface CircularGaugeProps {
431
+ value: number;
432
+ max: number;
433
+ label?: string;
434
+ size?: number;
435
+ strokeWidth?: number;
436
+ }
437
+
438
+ function CircularGauge({
439
+ value,
440
+ max,
441
+ label,
442
+ size = 100,
443
+ strokeWidth = 8
444
+ }: CircularGaugeProps) {
445
+ const radius = (size - strokeWidth) / 2;
446
+ const circumference = radius * 2 * Math.PI;
447
+ const percentage = Math.min(100, (value / max) * 100);
448
+ const offset = circumference - (percentage / 100) * circumference;
449
+
450
+ const color = percentage <= 25 ? '#ef4444'
451
+ : percentage <= 50 ? '#f59e0b'
452
+ : '#10b981';
453
+
454
+ return (
455
+ <div className="relative inline-flex items-center justify-center">
456
+ <svg width={size} height={size} className="-rotate-90">
457
+ {/* Background circle */}
458
+ <circle
459
+ cx={size / 2}
460
+ cy={size / 2}
461
+ r={radius}
462
+ strokeWidth={strokeWidth}
463
+ stroke="#374151"
464
+ fill="none"
465
+ />
466
+ {/* Progress circle */}
467
+ <circle
468
+ cx={size / 2}
469
+ cy={size / 2}
470
+ r={radius}
471
+ strokeWidth={strokeWidth}
472
+ stroke={color}
473
+ fill="none"
474
+ strokeLinecap="round"
475
+ strokeDasharray={circumference}
476
+ strokeDashoffset={offset}
477
+ className="transition-all duration-500 ease-out"
478
+ />
479
+ </svg>
480
+
481
+ {/* Center content */}
482
+ <div className="absolute inset-0 flex flex-col items-center justify-center">
483
+ <span className="text-2xl font-bold">{Math.round(percentage)}%</span>
484
+ {label && <span className="text-xs text-text-theme-muted">{label}</span>}
485
+ </div>
486
+ </div>
487
+ );
488
+ }
489
+ ```
490
+
491
+ ---
492
+
493
+ ## Color Systems
494
+
495
+ ### Status Color Scale
496
+
497
+ ```typescript
498
+ const STATUS_COLORS = {
499
+ // By percentage
500
+ getByPercentage: (pct: number) => {
501
+ if (pct <= 0) return { bg: 'bg-gray-500', text: 'text-gray-400' };
502
+ if (pct <= 25) return { bg: 'bg-red-500', text: 'text-red-400' };
503
+ if (pct <= 50) return { bg: 'bg-amber-500', text: 'text-amber-400' };
504
+ if (pct <= 75) return { bg: 'bg-yellow-500', text: 'text-yellow-400' };
505
+ return { bg: 'bg-emerald-500', text: 'text-emerald-400' };
506
+ },
507
+
508
+ // Named states
509
+ healthy: { bg: 'bg-emerald-500', text: 'text-emerald-400' },
510
+ warning: { bg: 'bg-amber-500', text: 'text-amber-400' },
511
+ critical: { bg: 'bg-red-500', text: 'text-red-400' },
512
+ depleted: { bg: 'bg-gray-500', text: 'text-gray-400' },
513
+ overload: { bg: 'bg-violet-500', text: 'text-violet-400' },
514
+ };
515
+ ```
516
+
517
+ ### Heat Color Scale
518
+
519
+ ```typescript
520
+ const HEAT_COLORS = {
521
+ cold: 'bg-cyan-500', // Below normal
522
+ cool: 'bg-blue-500', // Normal
523
+ warm: 'bg-amber-400', // Elevated
524
+ hot: 'bg-orange-500', // High
525
+ critical: 'bg-red-500', // Dangerous
526
+ };
527
+ ```
528
+
529
+ ---
530
+
531
+ ## Anti-Patterns
532
+
533
+ ### DON'T: No Max Value Reference
534
+ ```tsx
535
+ // BAD - No context for the value
536
+ <div className="h-2 bg-green-500" style={{ width: '60%' }} />
537
+
538
+ // GOOD - Shows current/max
539
+ <ProgressBar value={60} max={100} showValue />
540
+ ```
541
+
542
+ ### DON'T: Color Only
543
+ ```tsx
544
+ // BAD - Status only indicated by color
545
+ <div className="w-full h-2 bg-red-500" />
546
+
547
+ // GOOD - Color + numeric + label
548
+ <div>
549
+ <div className="flex justify-between text-sm mb-1">
550
+ <span>Health</span>
551
+ <span className="text-red-400">15/100 (Critical)</span>
552
+ </div>
553
+ <div className="h-2 bg-red-500" style={{ width: '15%' }} />
554
+ </div>
555
+ ```
556
+
557
+ ### DON'T: Instant Jumps
558
+ ```tsx
559
+ // BAD - Value jumps instantly
560
+ <div style={{ width: `${percentage}%` }} />
561
+
562
+ // GOOD - Smooth transition
563
+ <div
564
+ className="transition-all duration-300 ease-out"
565
+ style={{ width: `${percentage}%` }}
566
+ />
567
+ ```
568
+
569
+ ---
570
+
571
+ ## Accessibility
572
+
573
+ ### ARIA for Progress/Meters
574
+
575
+ ```tsx
576
+ // Progress bar (determinate progress toward goal)
577
+ <div
578
+ role="progressbar"
579
+ aria-valuenow={current}
580
+ aria-valuemin={0}
581
+ aria-valuemax={max}
582
+ aria-label="Download progress"
583
+ >
584
+
585
+ // Meter (measurement within known range)
586
+ <div
587
+ role="meter"
588
+ aria-valuenow={current}
589
+ aria-valuemin={0}
590
+ aria-valuemax={max}
591
+ aria-label="CPU usage"
592
+ >
593
+ ```
594
+
595
+ ### Announcing Changes
596
+
597
+ ```tsx
598
+ function useStatusAnnouncer(value: number, max: number, label: string) {
599
+ const prevValue = useRef(value);
600
+ const percentage = Math.round((value / max) * 100);
601
+
602
+ useEffect(() => {
603
+ if (value !== prevValue.current) {
604
+ const direction = value > prevValue.current ? 'increased' : 'decreased';
605
+ const status = percentage <= 25 ? 'critical' : percentage <= 50 ? 'warning' : 'normal';
606
+
607
+ announceToScreenReader(`${label} ${direction} to ${value} of ${max}. Status: ${status}`);
608
+ prevValue.current = value;
609
+ }
610
+ }, [value, max, label, percentage]);
611
+ }
612
+ ```
613
+
614
+ ---
615
+
616
+ ## Testing Checklist
617
+
618
+ - [ ] Bar fills to correct percentage
619
+ - [ ] Color changes at threshold boundaries
620
+ - [ ] Animation smooth on value change
621
+ - [ ] Value display shows correct numbers
622
+ - [ ] Pip display shows correct filled count
623
+ - [ ] Circular gauge renders correctly
624
+ - [ ] Threshold effects display at correct levels
625
+ - [ ] Accessibility attributes present
626
+ - [ ] Screen reader announces changes
627
+ - [ ] Works in high contrast mode
628
+
629
+ ---
630
+
631
+ ## Related Skills
632
+
633
+ - `info-card-patterns` - For stat display cards
634
+ - `canvas-grid-patterns` - For in-game health displays
635
+ - `data-density-patterns` - For dense stat layouts