refactor: dashboard design critique — eliminate redundancy, differentiate sections

- Remove GreetingStatStrip (duplicated PerformanceCards data)
- Strip left-border accent from stat cards (AI slop pattern)
- Redesign KnowledgeBaseCards: icon grid → compact row list with icon badges
- Redesign TeamSummary: distinct inline-row layout, no longer identical twin
- Differentiate hover: stat cards use subtle border-hover, sessions keep springy lift
- Add loading skeletons to PerformanceCards, KnowledgeBaseCards, TeamSummary
- Add error state to PerformanceCards
- Extract timeAgo() to shared lib/timeAgo.ts (replaced 4 duplicates)
- Fix Skeleton bg-brand-border (undefined CSS var) → border-default
- Fix double text-xs text-[0.5625rem] class conflicts across dashboard

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-29 17:06:30 -04:00
parent 677c8f88ea
commit 912075cd43
11 changed files with 105 additions and 85 deletions

View File

@@ -3,17 +3,20 @@ import { useNavigate } from 'react-router-dom'
import { Users, AlertTriangle, Activity, ArrowRight } from 'lucide-react'
import { usePermissions } from '@/hooks/usePermissions'
import { aiSessionsApi } from '@/api/aiSessions'
import { Skeleton } from '@/components/ui/Skeleton'
export function TeamSummary() {
const { isAccountOwner } = usePermissions()
const navigate = useNavigate()
const [escalationCount, setEscalationCount] = useState(0)
const [loading, setLoading] = useState(true)
useEffect(() => {
if (!isAccountOwner) return
if (!isAccountOwner) { setLoading(false); return }
aiSessionsApi.getEscalationQueue()
.then((esc) => setEscalationCount(esc.length))
.catch(() => {})
.finally(() => setLoading(false))
}, [isAccountOwner])
if (!isAccountOwner) return null
@@ -38,23 +41,28 @@ export function TeamSummary() {
Manage <ArrowRight size={10} />
</button>
</div>
<div className="grid grid-cols-3 divide-x" style={{ borderColor: 'var(--color-border-default)' }}>
{items.map((item) => (
<div className="p-4 space-y-3">
{loading ? Array.from({ length: 3 }).map((_, i) => (
<div key={i} className="flex items-center gap-3 px-3 py-2.5">
<Skeleton className="h-4 w-4 rounded shrink-0" />
<Skeleton className="h-4 flex-1 max-w-28" />
<Skeleton className="h-5 w-8" />
</div>
)) : items.map((item) => (
<button
key={item.label}
onClick={() => navigate(item.href)}
className="flex flex-col items-center gap-2 py-5 rounded-lg hover:bg-card-hover transition-all duration-350"
style={{ transition: 'transform 350ms cubic-bezier(0.34, 1.56, 0.64, 1), background 200ms ease' }}
onMouseEnter={e => { e.currentTarget.style.transform = 'translateY(-4px)' }}
onMouseLeave={e => { e.currentTarget.style.transform = 'translateY(0)' }}
className="flex w-full items-center gap-3 rounded-lg px-3 py-2.5 hover:bg-[var(--color-bg-card-hover)] transition-colors group"
>
<item.icon size={20} style={{ color: item.color }} />
<p className="font-heading text-xl font-extrabold text-foreground">{item.value}</p>
<p className="font-sans text-xs text-[0.5625rem] uppercase tracking-[0.1em] text-muted-foreground">
<item.icon size={15} style={{ color: item.color }} className="shrink-0" />
<span className="flex-1 text-left text-[0.8125rem] text-muted-foreground group-hover:text-foreground transition-colors">
{item.label}
</p>
</span>
<span className="font-heading text-lg font-bold text-foreground tabular-nums">
{item.value}
</span>
</button>
))}
)))}
</div>
</div>
)