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 */
|
||||
|
||||
152
web_redesign.md
Normal file
152
web_redesign.md
Normal 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. It’s 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:
|
||||
- it’s trying to convince the user
|
||||
- it’s trying to impress the user
|
||||
- it’s trying to capture the user
|
||||
|
||||
Remove or rewrite it.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
The site should feel like:
|
||||
|
||||
“This exists. If you need it, you’ll understand it.”
|
||||
|
||||
Not:
|
||||
|
||||
“Please use this product.”
|
||||
Reference in New Issue
Block a user