fix(ui): refine dashboard components for Warm Precision tone

- KpiCards: remove gradient/glow, flatten to data-oriented panels
- BandwidthChart: replace hardcoded blue (#38BDF8) with accent token,
  use token colors for axis text and cursor
- QuickActions: replace icon grid with command-style list rows
  with left-border hover interaction
- EventsTimeline: remove timeline/skeleton, tighten to log-stream
  layout with divide separators and monospace timestamps
- Light mode: bump border-default opacity 0.12→0.14, darken
  text-secondary for dense readability

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Jason Staack
2026-03-21 12:04:28 -05:00
parent b39014ef47
commit 298ed89c75
6 changed files with 273 additions and 155 deletions

View File

@@ -78,7 +78,7 @@ export function BandwidthChart({ devices }: BandwidthChartProps) {
<XAxis
type="number"
tickFormatter={formatAxisTick}
tick={{ fontSize: 11, fill: '#94a3b8' }}
tick={{ fontSize: 10, fill: 'hsl(var(--text-muted))' }}
axisLine={false}
tickLine={false}
/>
@@ -86,19 +86,19 @@ export function BandwidthChart({ devices }: BandwidthChartProps) {
type="category"
dataKey="hostname"
width={120}
tick={{ fontSize: 11, fill: '#cbd5e1' }}
tick={{ fontSize: 10, fill: 'hsl(var(--text-secondary))' }}
axisLine={false}
tickLine={false}
/>
<Tooltip
content={<BwTooltip />}
cursor={{ fill: '#334155', opacity: 0.5 }}
cursor={{ fill: 'hsl(var(--elevated))', opacity: 0.5 }}
/>
<Bar
dataKey="bandwidthBps"
fill="#38BDF8"
radius={[0, 4, 4, 0]}
maxBarSize={24}
fill="hsl(var(--accent))"
radius={[0, 2, 2, 0]}
maxBarSize={20}
/>
</BarChart>
</ResponsiveContainer>

View File

@@ -2,7 +2,6 @@ import { useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import { Bell, Server, HardDrive } from 'lucide-react'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Skeleton } from '@/components/ui/skeleton'
import { cn } from '@/lib/utils'
import { eventsApi, type DashboardEvent, type EventsParams } from '@/lib/eventsApi'
import { DeviceLink } from '@/components/ui/device-link'
@@ -83,22 +82,6 @@ function EventIcon({ event }: { event: DashboardEvent }) {
}
}
function TimelineSkeleton() {
return (
<div className="space-y-3">
{Array.from({ length: 5 }).map((_, i) => (
<div key={i} className="flex items-start gap-3 pl-3">
<Skeleton className="h-4 w-4 rounded-full shrink-0 mt-0.5" />
<div className="flex-1 space-y-1.5">
<Skeleton className="h-3.5 w-3/4" />
<Skeleton className="h-3 w-1/2" />
</div>
<Skeleton className="h-3 w-12 shrink-0" />
</div>
))}
</div>
)
}
export function EventsTimeline({ tenantId, isSuperAdmin }: EventsTimelineProps) {
const [filterType, setFilterType] = useState<EventFilter>(undefined)
@@ -115,86 +98,79 @@ export function EventsTimeline({ tenantId, isSuperAdmin }: EventsTimelineProps)
})
return (
<Card className="bg-panel border-border">
<CardHeader className="pb-2">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-sm font-medium text-text-secondary">
Recent Events
</CardTitle>
<div className="flex gap-1">
{FILTERS.map((f) => (
<button
key={f.label}
onClick={() => setFilterType(f.value)}
className={cn(
'px-2 py-0.5 rounded text-xs font-medium transition-colors',
filterType === f.value
? 'bg-accent/15 text-accent'
: 'text-text-muted hover:text-text-secondary hover:bg-elevated/50',
)}
>
{f.label}
</button>
))}
</div>
<div className="bg-panel border border-border rounded-sm">
<div className="px-3 py-2 border-b border-border-subtle bg-elevated flex items-center justify-between">
<span className="text-[7px] font-medium text-text-muted uppercase tracking-[1.5px]">
Recent Events
</span>
<div className="flex gap-0.5">
{FILTERS.map((f) => (
<button
key={f.label}
onClick={() => setFilterType(f.value)}
className={cn(
'px-1.5 py-0.5 rounded-sm text-[10px] font-medium transition-[background-color,color] duration-[50ms]',
filterType === f.value
? 'bg-accent-soft text-accent'
: 'text-text-muted hover:text-text-secondary',
)}
>
{f.label}
</button>
))}
</div>
</CardHeader>
<CardContent>
</div>
<div>
{isSuperAdmin && !tenantId ? (
<div className="flex items-center justify-center py-8 text-sm text-text-muted">
<div className="py-5 text-center text-[9px] text-text-muted">
Select a tenant to view events
</div>
) : isLoading ? (
<TimelineSkeleton />
<div className="py-5 text-center text-[9px] text-text-muted">
Loading
</div>
) : !events || events.length === 0 ? (
<div className="flex items-center justify-center py-8 text-sm text-text-muted">
<div className="py-5 text-center text-[9px] text-text-muted">
No recent events
</div>
) : (
<div className="max-h-[400px] overflow-y-auto pr-1">
<div className="relative border-l-2 border-border ml-2">
{events.map((event) => (
<div
key={event.id}
className="relative flex items-start gap-3 pb-3 pl-4 last:pb-0"
>
{/* Icon positioned over the timeline line */}
<div className="absolute -left-[9px] top-0.5 flex h-4 w-4 items-center justify-center rounded-full bg-panel">
<EventIcon event={event} />
</div>
{/* Content */}
<div className="flex-1 min-w-0 ml-1">
<p className="text-sm font-medium text-text-primary truncate">
{event.title}
</p>
<p className="text-xs text-text-muted truncate">
{event.description}
{event.device_hostname && (
<span className="ml-1 text-text-secondary">
&mdash;{' '}
{event.device_id ? (
<DeviceLink tenantId={tenantId} deviceId={event.device_id}>
{event.device_hostname}
</DeviceLink>
) : (
event.device_hostname
)}
</span>
)}
</p>
</div>
{/* Timestamp */}
<span className="text-xs text-text-muted whitespace-nowrap shrink-0 mt-0.5">
{formatRelativeTime(event.timestamp)}
<div className="max-h-[400px] overflow-y-auto divide-y divide-border-subtle">
{events.map((event) => (
<div
key={event.id}
className="flex items-center gap-2.5 px-3 py-1.5"
>
<div className="shrink-0">
<EventIcon event={event} />
</div>
<div className="flex-1 min-w-0">
<span className="text-xs text-text-primary truncate block">
{event.title}
</span>
<span className="text-[10px] text-text-muted truncate block">
{event.description}
{event.device_hostname && (
<span className="ml-1 text-text-secondary">
&mdash;{' '}
{event.device_id ? (
<DeviceLink tenantId={tenantId} deviceId={event.device_id}>
{event.device_hostname}
</DeviceLink>
) : (
event.device_hostname
)}
</span>
)}
</span>
</div>
))}
</div>
<span className="text-[10px] font-mono text-text-muted whitespace-nowrap shrink-0">
{formatRelativeTime(event.timestamp)}
</span>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div>
</div>
)
}

View File

@@ -44,45 +44,31 @@ function KpiCard({
const animatedValue = useAnimatedCounter(value, 800, decimals)
return (
<Card
<div
className={cn(
'bg-gradient-to-br from-[#f8f8ff] to-elevated dark:from-elevated dark:to-[#16162a] border-border transition-colors',
highlight && 'border-warning/30',
'bg-panel border border-border px-3 py-2.5 rounded-sm',
highlight && 'border-l-2 border-l-warning',
)}
>
<CardContent className="p-4">
<div className="flex items-center justify-between">
<div className="flex flex-col gap-1">
<span className="text-[10px] font-medium text-text-muted uppercase tracking-wider">
{label}
</span>
<div className="flex items-baseline gap-1">
<span
className={cn(
'text-2xl font-medium tabular-nums font-mono',
colorClass,
)}
>
{decimals > 0 ? animatedValue.toFixed(decimals) : animatedValue}
</span>
{suffix && (
<span className="text-sm font-medium text-text-muted">
{suffix}
</span>
)}
</div>
</div>
<div
className={cn(
'flex h-10 w-10 items-center justify-center rounded-lg bg-elevated/50',
colorClass,
)}
>
{icon}
</div>
</div>
</CardContent>
</Card>
<div className="text-[7px] font-medium text-text-muted uppercase tracking-[1.5px] mb-1">
{label}
</div>
<div className="flex items-baseline gap-1">
<span
className={cn(
'text-lg font-medium tabular-nums font-mono',
colorClass,
)}
>
{decimals > 0 ? animatedValue.toFixed(decimals) : animatedValue}
</span>
{suffix && (
<span className="text-xs text-text-muted">
{suffix}
</span>
)}
</div>
</div>
)
}

View File

@@ -72,34 +72,38 @@ export function QuickActions({ tenantId, isSuperAdmin }: QuickActionsProps) {
const actions = getActions(tenantId, isSuperAdmin)
return (
<Card className="bg-panel border-border">
<CardHeader className="pb-2">
<CardTitle className="text-sm font-medium text-text-secondary">
<div className="bg-panel border border-border rounded-sm">
<div className="px-3 py-2 border-b border-border-subtle bg-elevated">
<span className="text-[7px] font-medium text-text-muted uppercase tracking-[1.5px]">
Quick Actions
</CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 gap-2">
{actions.map((action) => (
<Link
key={action.label}
to={action.to}
className={cn(
'flex flex-col items-center gap-1.5 rounded-lg px-3 py-3',
'text-text-secondary hover:bg-elevated/50 hover:text-text-primary',
'transition-colors text-center',
)}
>
<div className="flex h-9 w-9 items-center justify-center rounded-lg bg-elevated/50">
{action.icon}
</div>
<span className="text-xs font-medium leading-tight">
</span>
</div>
<div className="divide-y divide-border-subtle">
{actions.map((action) => (
<Link
key={action.label}
to={action.to}
className={cn(
'flex items-center gap-2.5 px-3 py-2',
'text-text-secondary hover:text-text-primary',
'border-l-2 border-transparent hover:border-accent',
'transition-[border-color,color] duration-[50ms]',
)}
>
<div className="text-text-muted">
{action.icon}
</div>
<div className="min-w-0">
<span className="text-xs font-medium block">
{action.label}
</span>
</Link>
))}
</div>
</CardContent>
</Card>
<span className="text-[10px] text-text-muted block">
{action.description}
</span>
</div>
</Link>
))}
</div>
</div>
)
}

View File

@@ -35,10 +35,10 @@
--elevated: 39 22% 92%; /* #f0ede4 */
--border-subtle: 30 18% 13% / 0.06;
--border-default: 30 18% 13% / 0.12;
--border-default: 30 18% 13% / 0.14;
--text-primary: 43 27% 8%; /* #1a1810 */
--text-secondary: 40 10% 33%; /* #5e5a4e */
--text-secondary: 40 12% 30%; /* #564e42 — slightly darker for dense readability */
--text-muted: 36 7% 50%; /* #8a8578 */
--accent: 43 32% 40%; /* #8a7a48 */