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