UX/UI improvements: accessibility, polish, and responsiveness (10 items)
1. WCAG AA contrast fixes - --text-muted increased to #8b8b95 for 4.5:1+ ratio 2. ARIA roles - tabs, avatars, status cards, live regions, alerts across all pages 3. Unique head colors - 12 distinct colors per head via data-head CSS selectors 4. Toast notification system - ToastProvider with success/error/info/warning types 5. Structured per-head response cards - colored dot indicators, head summaries 6. Status visualization - colored status dots (healthy/degraded/offline) with glow 7. Collapsible avatar grid - toggle button on mobile, persists collapsed state 8. System color scheme detection - prefers-color-scheme media query + JS fallback 9. Markdown rendering - lightweight parser for code, lists, headings, links, bold/italic 10. Mobile touch targets - 44px minimum on all interactive elements per WCAG AAA Co-Authored-By: Nakamoto, S <defi@defi-oracle.io>
This commit is contained in:
@@ -1,11 +1,16 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import type { SystemStatus, VoiceProfile } from '../types'
|
||||
|
||||
function StatusCard({ label, value, unit }: { label: string; value: string | number | null; unit?: string }) {
|
||||
function StatusCard({ label, value, unit, statusClass }: {
|
||||
label: string; value: string | number | null; unit?: string; statusClass?: string
|
||||
}) {
|
||||
return (
|
||||
<div className="status-card">
|
||||
<div className="status-card" role="status" aria-label={`${label}: ${value ?? 'N/A'}${unit && value != null ? unit : ''}`}>
|
||||
<span className="status-label">{label}</span>
|
||||
<span className="status-value">{value ?? 'N/A'}{unit && value != null ? unit : ''}</span>
|
||||
<span className={`status-value ${statusClass || ''}`}>
|
||||
{statusClass && <span className={`status-dot ${statusClass}`} aria-hidden="true" />}
|
||||
{value ?? 'N/A'}{unit && value != null ? unit : ''}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -63,25 +68,34 @@ export function AdminPage({ authHeaders }: { authHeaders: () => Record<string, s
|
||||
return `${h}h ${m}m`
|
||||
}
|
||||
|
||||
if (loading) return <div className="page-loading">Loading admin dashboard...</div>
|
||||
const statusClass = status?.status === 'healthy' ? 'healthy' : status?.status === 'degraded' ? 'degraded' : status?.status === 'offline' ? 'offline' : ''
|
||||
|
||||
if (loading) return <div className="page-loading" role="status" aria-live="polite">Loading admin dashboard...</div>
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-tabs">
|
||||
<div className="admin-page" role="main" aria-label="Admin Dashboard">
|
||||
<div className="admin-tabs" role="tablist" aria-label="Admin sections">
|
||||
{(['overview', 'voices', 'agents', 'governance'] as const).map((t) => (
|
||||
<button key={t} className={tab === t ? 'active' : ''} onClick={() => setTab(t)}>
|
||||
<button
|
||||
key={t}
|
||||
className={tab === t ? 'active' : ''}
|
||||
onClick={() => setTab(t)}
|
||||
role="tab"
|
||||
aria-selected={tab === t}
|
||||
aria-controls={`panel-${t}`}
|
||||
>
|
||||
{t.charAt(0).toUpperCase() + t.slice(1)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{error && <div className="error-banner" onClick={() => setError(null)}>{error}</div>}
|
||||
{error && <div className="error-banner" role="alert" onClick={() => setError(null)}>{error}</div>}
|
||||
|
||||
{tab === 'overview' && (
|
||||
<div className="admin-section">
|
||||
<div className="admin-section" role="tabpanel" id="panel-overview" aria-label="System Overview">
|
||||
<h2>System Overview</h2>
|
||||
<div className="status-grid">
|
||||
<StatusCard label="Status" value={status?.status ?? 'unknown'} />
|
||||
<div className="status-grid" role="group" aria-label="System metrics">
|
||||
<StatusCard label="Status" value={status?.status ?? 'unknown'} statusClass={statusClass} />
|
||||
<StatusCard label="Uptime" value={status ? formatUptime(status.uptime_seconds) : 'N/A'} />
|
||||
<StatusCard label="Active Tasks" value={status?.active_tasks ?? 0} />
|
||||
<StatusCard label="Active Agents" value={status?.active_agents ?? 0} />
|
||||
@@ -93,11 +107,13 @@ export function AdminPage({ authHeaders }: { authHeaders: () => Record<string, s
|
||||
)}
|
||||
|
||||
{tab === 'voices' && (
|
||||
<div className="admin-section">
|
||||
<div className="admin-section" role="tabpanel" id="panel-voices" aria-label="Voice Library">
|
||||
<h2>Voice Library</h2>
|
||||
<div className="add-form">
|
||||
<input placeholder="Voice name" value={newVoiceName} onChange={(e) => setNewVoiceName(e.target.value)} />
|
||||
<select value={newVoiceLang} onChange={(e) => setNewVoiceLang(e.target.value)}>
|
||||
<div className="add-form" role="form" aria-label="Add voice">
|
||||
<label htmlFor="voice-name" className="sr-only">Voice name</label>
|
||||
<input id="voice-name" placeholder="Voice name" value={newVoiceName} onChange={(e) => setNewVoiceName(e.target.value)} />
|
||||
<label htmlFor="voice-lang" className="sr-only">Language</label>
|
||||
<select id="voice-lang" value={newVoiceLang} onChange={(e) => setNewVoiceLang(e.target.value)}>
|
||||
<option value="en-US">English (US)</option>
|
||||
<option value="en-GB">English (UK)</option>
|
||||
<option value="es-ES">Spanish</option>
|
||||
@@ -107,10 +123,10 @@ export function AdminPage({ authHeaders }: { authHeaders: () => Record<string, s
|
||||
</select>
|
||||
<button onClick={addVoice}>Add Voice</button>
|
||||
</div>
|
||||
<div className="voice-list">
|
||||
<div className="voice-list" role="list" aria-label="Voice profiles">
|
||||
{voices.length === 0 && <p className="muted">No voice profiles configured</p>}
|
||||
{voices.map((v) => (
|
||||
<div key={v.id} className="voice-card">
|
||||
<div key={v.id} className="voice-card" role="listitem">
|
||||
<strong>{v.name}</strong>
|
||||
<span className="muted">{v.language} | {v.provider}</span>
|
||||
<span className="muted">Pitch: {v.pitch}x | Speed: {v.speed}x</span>
|
||||
@@ -121,13 +137,13 @@ export function AdminPage({ authHeaders }: { authHeaders: () => Record<string, s
|
||||
)}
|
||||
|
||||
{tab === 'agents' && (
|
||||
<div className="admin-section">
|
||||
<div className="admin-section" role="tabpanel" id="panel-agents" aria-label="Agent Configuration">
|
||||
<h2>Agent Configuration</h2>
|
||||
<div className="agent-grid">
|
||||
<div className="agent-grid" role="list" aria-label="Active agents">
|
||||
{['Planner', 'Reasoner', 'Executor', 'Critic', '12 Heads', 'Witness'].map((a) => (
|
||||
<div key={a} className="agent-card">
|
||||
<div key={a} className="agent-card" role="listitem">
|
||||
<strong>{a}</strong>
|
||||
<span className="status-badge active">Active</span>
|
||||
<span className="status-badge active" role="status">Active</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@@ -135,10 +151,10 @@ export function AdminPage({ authHeaders }: { authHeaders: () => Record<string, s
|
||||
)}
|
||||
|
||||
{tab === 'governance' && (
|
||||
<div className="admin-section">
|
||||
<div className="admin-section" role="tabpanel" id="panel-governance" aria-label="Governance Mode">
|
||||
<h2>Governance Mode</h2>
|
||||
<div className="governance-info">
|
||||
<div className="governance-mode">
|
||||
<div className="governance-mode" role="status" aria-label="Current governance mode: Advisory">
|
||||
<span className="mode-label">Current Mode:</span>
|
||||
<span className="mode-value advisory">ADVISORY</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user