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:
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
—{' '}
|
||||
{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">
|
||||
—{' '}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 */
|
||||
|
||||
Reference in New Issue
Block a user