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

View File

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

View File

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

View File

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

View File

@@ -35,10 +35,10 @@
--elevated: 39 22% 92%; /* #f0ede4 */ --elevated: 39 22% 92%; /* #f0ede4 */
--border-subtle: 30 18% 13% / 0.06; --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-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 */ --text-muted: 36 7% 50%; /* #8a8578 */
--accent: 43 32% 40%; /* #8a7a48 */ --accent: 43 32% 40%; /* #8a7a48 */

152
web_redesign.md Normal file
View File

@@ -0,0 +1,152 @@
We need to redesign the website to remove all startup / SaaS / marketing patterns.
Current issue:
It feels too “salesy” and corporate — like a product trying to convince people to use it.
That is the wrong tone.
---
## Core direction
This should feel like:
- a tool
- a system
- a project that exists because it needed to exist
NOT:
- a startup landing page
- a marketing funnel
- a conversion-optimized site
---
## Tone
- Direct
- Honest
- Slightly indifferent to whether the user signs up
- No hype language
- No promises
- No “transform your workflow” nonsense
---
## Remove immediately
- Any “hero pitch” language
- Any “why choose us” sections
- Any fake trust signals (logos, testimonials, “companies use this”)
- Any email capture / gated content
- Any “Get Started Now” pressure CTAs
---
## Replace with
### 1. Simple top section
- Project name
- One blunt sentence describing what it is
- Links:
- GitHub
- Docs
- Download / Self-host
- SaaS (optional, not emphasized)
Example tone:
“This is a network control system. Its in active development. Things may break.”
---
### 2. “What it does” (not marketing)
- Bullet points
- No adjectives
- No hype
Bad:
“Powerful, scalable, next-gen network automation”
Good:
- Monitor device state
- Push configuration
- Track changes
- Run commands across devices
---
### 3. “What it is not”
This is important for your voice.
Example:
- Not finished
- Not stable
- Not for everyone
- Not trying to replace everything
---
### 4. Screenshots
- Real UI
- No mockups
- No gradients / marketing framing
- No captions trying to sell
---
### 5. Development status
- Be explicit:
- versioning
- breaking changes
- expectations
---
### 6. Pricing (if present)
- Simple
- No anchoring tricks
- No “save 20%”
- No fake urgency
---
## Visual direction
- Use the same Warm Precision system
- No big hero sections
- No oversized typography
- No soft gradients
- No “landing page layout”
Layout should feel like:
- documentation
- system panel
- terminal-adjacent
---
## Anti-pattern check
If any section feels like:
- its trying to convince the user
- its trying to impress the user
- its trying to capture the user
Remove or rewrite it.
---
## Goal
The site should feel like:
“This exists. If you need it, youll understand it.”
Not:
“Please use this product.”