Parent : [[INDEX-WEB-UIUX]]
Web UI/UX - Status Indicator Dots : micro-dots colorés, click-to-expand panel, polling auto
Problème
Un status textuel "En ligne" ou "5/5 services OK" prend trop de place dans un header compact. Un indicateur visuel minimal doit communiquer l'état instantanément.
Solution
4 micro-dots (1.5px × 1.5px) avec couleurs d'état : vert = OK, rouge = stale/error. Cliquer déploie un panel détaillé. Polling automatique toutes les 60 secondes.
function IndexHealthChip() {
const [status, setStatus] = useState<IndexStatus | null>(null)
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
// Polling toutes les 60s
useEffect(() => {
load()
const id = window.setInterval(load, 60_000)
return () => window.clearInterval(id)
}, [load])
// Ferme au clic dehors
useEffect(() => {
if (!open) return
const handler = (e: MouseEvent) => {
if (ref.current && !ref.current.contains(e.target as Node)) setOpen(false)
}
document.addEventListener('mousedown', handler)
return () => document.removeEventListener('mousedown', handler)
}, [open])
const shards = status?.shards ?? []
const staleCount = shards.filter((s) => !s.exists || s.stale).length
return (
<div ref={ref} className="relative shrink-0">
{/* Bouton fermé : 4 dots */}
<button
onClick={() => setOpen((v) => !v)}
className="flex items-center gap-1.5 px-2.5 py-1.5 rounded-[4px] transition-colors"
style={{ border: '1px solid var(--border)' }}
>
<span className="flex items-center gap-1">
{status === null
? [0, 1, 2, 3].map((i) => (
<span key={i} className="w-1.5 h-1.5 rounded-full" style={{ background: 'var(--border-hover)' }} />
))
: shards.map((s) => (
<span
key={s.source}
className="w-1.5 h-1.5 rounded-full transition-colors"
style={{ background: !s.exists || s.stale ? '#ef4444' : '#22c55e' }}
/>
))
}
</span>
<span
className="text-[9px]"
style={{
color: 'var(--text-3)',
display: 'inline-block',
transform: open ? 'rotate(180deg)' : 'rotate(0deg)',
transition: 'transform 0.15s',
}}
>
▾
</span>
</button>
{/* Panel déplié */}
{open && (
<div
className="absolute right-0 top-full mt-2 animate-fade-in-up z-50 rounded-[6px] py-3 px-4 flex flex-col gap-3"
style={{
background: 'var(--surface)',
border: '1px solid var(--border)',
minWidth: '190px',
boxShadow: '0 8px 32px rgba(0,0,0,0.4)',
}}
>
{shards.map((s) => {
const ok = s.exists && !s.stale
return (
<div key={s.source} className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: ok ? '#22c55e' : '#ef4444' }} />
<span className="text-xs font-medium flex-1" style={{ color: ok ? 'var(--text)' : 'var(--text-2)' }}>
{s.source.toUpperCase()}
</span>
{!ok && (
<span className="text-[9px] font-mono px-1 rounded" style={{ background: 'rgba(239,68,68,0.12)', color: '#ef4444' }}>
stale
</span>
)}
</div>
)
})}
{staleCount > 0 && (
<>
<div className="divider" />
<button
onClick={reloadStale}
className="text-[11px] font-semibold w-full text-left transition-colors"
style={{ color: 'var(--text-3)' }}
>
↻ Reload {staleCount} shard{staleCount > 1 ? 's' : ''}
</button>
</>
)}
</div>
)}
</div>
)
}
Points clés
- Micro-dots 1.5px : assez visibles, pas intrusifs dans un header
- Couleurs sémantiques : vert
#22c55e= OK, rouge#ef4444= KO - Fallback gris : quand
status === null(chargement initial) - Chevron rotation :
transform: rotate(180deg)indique l'état ouvert/fermé - Panel absolute :
right-0 top-full mt-2→ s'aligne sous le bouton - Shadow élevée :
0 8px 32px rgba(0,0,0,0.4)→ flotte au-dessus du contenu - Polling 60s : assez fréquent pour être réactif, pas trop pour ne pas spammer
- Click-outside : referme le panel au clic ailleurs
Pourquoi
- Les micro-dots communiquent 4 états en < 30px de largeur
- Le panel détaillé ne pollue pas l'interface par défaut
- Le polling automatique garde l'info fraîche sans action utilisateur
Quand l'utiliser
- Dashboard monitoring, état de services, health checks
- Header compact avec plusieurs sources
Quand NE PAS l'utiliser
- Si un seul état à afficher → texte simple suffit
- Si les états sont critiques et doivent toujours être visibles
Références
- ProjectAlpha/IndexHealthChip.tsx L1-163