CUB-196: add accessibility and edge case fixes for CameraCard

This commit is contained in:
rex-bot
2026-05-21 11:59:40 +00:00
parent e82208f897
commit 4ab7d41329
7 changed files with 717 additions and 11 deletions
+31 -11
View File
@@ -4,9 +4,10 @@ import type { CameraStatus } from '../types'
// ── Helpers ────────────────────────────────────────────────────────────────
function formatRelativeTime(iso: string): string {
const now = Date.now()
const then = new Date(iso).getTime()
const diffSec = Math.floor((now - then) / 1000)
if (isNaN(then)) return 'unknown'
const diffSec = Math.floor((Date.now() - then) / 1000)
if (diffSec < 10) return 'just now'
if (diffSec < 60) return `${diffSec}s ago`
@@ -29,6 +30,7 @@ function batteryColor(pct: number | null): { bar: string; text: string } {
}
function formatTimeLeft(sec: number): string {
if (sec <= 0 || !isFinite(sec)) return '--'
const m = Math.floor(sec / 60)
const s = sec % 60
return `${m}m ${s}s left`
@@ -56,7 +58,7 @@ export default function CameraCard({ camera }: CameraCardProps) {
const batt = batteryColor(battery_pct)
return (
<div
<article
className={`rounded-xl border bg-rig-dark-800/60 transition-colors ${
online
? 'border-rig-dark-600 hover:border-rig-accent/40'
@@ -66,14 +68,19 @@ export default function CameraCard({ camera }: CameraCardProps) {
{/* ── Header ── */}
<div className="flex items-center justify-between px-4 pt-4 pb-2">
<div className="flex items-center gap-2">
<Video className="h-4 w-4 text-rig-accent" />
<h3 className="text-sm font-semibold text-rig-dark-100 truncate max-w-[180px]">
<Video className="h-4 w-4 text-rig-accent" aria-hidden="true" />
<h3
className="text-sm font-semibold text-rig-dark-100 truncate max-w-[180px]"
title={friendly_name}
>
{friendly_name}
</h3>
</div>
{/* Online / Offline badge */}
<span
role="status"
aria-label={online ? 'Camera online' : 'Camera offline'}
className={`inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium ${
online
? 'bg-rig-success/15 text-rig-success'
@@ -81,9 +88,9 @@ export default function CameraCard({ camera }: CameraCardProps) {
}`}
>
{online ? (
<Wifi className="h-3 w-3" />
<Wifi className="h-3 w-3" aria-hidden="true" />
) : (
<WifiOff className="h-3 w-3" />
<WifiOff className="h-3 w-3" aria-hidden="true" />
)}
{online ? 'Online' : 'Offline'}
</span>
@@ -103,7 +110,11 @@ export default function CameraCard({ camera }: CameraCardProps) {
<div className="flex items-center gap-2">
{recording ? (
<>
<span className="relative flex h-2.5 w-2.5">
<span
className="relative flex h-2.5 w-2.5"
role="status"
aria-label="Recording in progress"
>
<span className="absolute inline-flex h-full w-full animate-ping rounded-full bg-rig-danger opacity-75" />
<span className="relative inline-flex h-2.5 w-2.5 rounded-full bg-rig-danger" />
</span>
@@ -130,7 +141,14 @@ export default function CameraCard({ camera }: CameraCardProps) {
{battery_pct !== null ? `${battery_pct}%` : 'N/A'}
</span>
</div>
<div className="h-1.5 w-full overflow-hidden rounded-full bg-rig-dark-700">
<div
className="h-1.5 w-full overflow-hidden rounded-full bg-rig-dark-700"
role="progressbar"
aria-valuenow={battery_pct ?? 0}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`Battery ${battery_pct !== null ? battery_pct + '%' : 'unknown'}`}
>
<div
className={`h-full rounded-full transition-all ${batt.bar}`}
style={{ width: battery_pct !== null ? `${Math.min(100, Math.max(0, battery_pct))}%` : '0%' }}
@@ -148,8 +166,10 @@ export default function CameraCard({ camera }: CameraCardProps) {
<span className="text-rig-success">Live</span>
</>
) : (
<span className="text-rig-dark-400">{formatRelativeTime(last_seen)}</span>
<span className="text-rig-dark-400">Offline</span>
)}
<span className="text-rig-dark-500">·</span>
<span className="text-rig-dark-400">{formatRelativeTime(last_seen)}</span>
</div>
{video_remaining_sec !== null && (
@@ -159,6 +179,6 @@ export default function CameraCard({ camera }: CameraCardProps) {
</div>
)}
</div>
</div>
</article>
)
}