Skip to content

Commit a1bfd16

Browse files
gcmsgclaude
andcommitted
feat(ui): redesign dashboard with crypto-network aesthetic
Overhaul the design system and 4 key pages (Landing, Directory, PublicProfile, ProviderDashboard) with a "Cryptographic Observatory" visual direction: - New color tokens: deep navy backgrounds, cyan/teal trust accents - Typography: Outfit (display) + JetBrains Mono (code/crypto) - Circular SVG reputation gauge replacing linear bars - Animated gradient orbs + dot-grid hero on landing page - Glassmorphic cards with subtle glow effects on hover - Enhanced header, badges, stat cards, and filter controls - Consistent rounded-xl card system with border-glow accents Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3f75f6c commit a1bfd16

File tree

13 files changed

+687
-441
lines changed

13 files changed

+687
-441
lines changed

web/app/index.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
66
<link rel="icon" type="image/png" href="/favicon.png" />
7+
<link rel="preconnect" href="https://fonts.googleapis.com" />
8+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9+
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
710
<title>PeerClaw — AI Agent Trust Platform</title>
811
</head>
912
<body>
Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { LucideIcon } from "lucide-react"
22
import { useTranslation } from "react-i18next"
3-
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
43
import { TrendingUp, TrendingDown } from "lucide-react"
54

65
interface AgentStatsCardProps {
@@ -14,25 +13,28 @@ export function AgentStatsCard({ title, value, change, icon: Icon }: AgentStatsC
1413
const { t } = useTranslation()
1514

1615
return (
17-
<Card>
18-
<CardHeader className="flex flex-row items-center justify-between pb-2">
19-
<CardTitle className="text-sm font-medium text-muted-foreground">
20-
{title}
21-
</CardTitle>
22-
<Icon className="size-4 text-muted-foreground" />
23-
</CardHeader>
24-
<CardContent>
25-
<div className="text-2xl font-bold">{value}</div>
16+
<div className="group relative overflow-hidden rounded-xl border border-border/60 bg-card p-5 transition-all duration-300 hover:border-primary/20 hover:shadow-[0_0_20px_oklch(0.72_0.15_192_/_0.05)]">
17+
{/* Subtle gradient accent at top */}
18+
<div className="absolute top-0 left-0 right-0 h-px bg-gradient-to-r from-transparent via-primary/30 to-transparent opacity-0 transition-opacity group-hover:opacity-100" />
19+
20+
<div className="flex items-center justify-between">
21+
<p className="text-sm font-medium text-muted-foreground">{title}</p>
22+
<div className="flex size-8 items-center justify-center rounded-lg bg-primary/8 transition-colors group-hover:bg-primary/12">
23+
<Icon className="size-4 text-primary/70" />
24+
</div>
25+
</div>
26+
<div className="mt-3">
27+
<p className="text-2xl font-bold tracking-tight">{value}</p>
2628
{change !== undefined && (
27-
<div className="flex items-center gap-1 mt-1">
29+
<div className="flex items-center gap-1 mt-1.5">
2830
{change >= 0 ? (
2931
<TrendingUp className="size-3 text-emerald-500" />
3032
) : (
31-
<TrendingDown className="size-3 text-red-500" />
33+
<TrendingDown className="size-3 text-red-400" />
3234
)}
3335
<span
3436
className={`text-xs font-medium ${
35-
change >= 0 ? "text-emerald-500" : "text-red-500"
37+
change >= 0 ? "text-emerald-500" : "text-red-400"
3638
}`}
3739
>
3840
{change >= 0 ? "+" : ""}
@@ -41,7 +43,7 @@ export function AgentStatsCard({ title, value, change, icon: Icon }: AgentStatsC
4143
<span className="text-xs text-muted-foreground">{t('common.vsLastPeriod')}</span>
4244
</div>
4345
)}
44-
</CardContent>
45-
</Card>
46+
</div>
47+
</div>
4648
)
4749
}

web/app/src/components/public/AgentDirectoryCard.tsx

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,68 +4,73 @@ import { VerifiedBadge } from "./VerifiedBadge"
44
import { ReputationMeter } from "./ReputationMeter"
55

66
const statusColors: Record<string, string> = {
7-
online: "bg-emerald-500",
7+
online: "bg-emerald-500 shadow-[0_0_6px_oklch(0.72_0.2_160_/_0.5)]",
88
offline: "bg-zinc-500",
9-
degraded: "bg-yellow-500",
9+
degraded: "bg-amber-500 shadow-[0_0_6px_oklch(0.8_0.15_85_/_0.5)]",
1010
}
1111

1212
export function AgentDirectoryCard({ agent }: { agent: PublicAgentProfile }) {
1313
return (
1414
<Link
1515
to={`/agents/${agent.id}`}
16-
className="group block rounded-lg border border-border bg-card p-4 transition-colors hover:border-primary/40 hover:bg-card/80"
16+
className="group relative block rounded-xl border border-border/60 bg-card p-4 transition-all duration-300 hover:border-primary/30 hover:shadow-[0_0_20px_oklch(0.72_0.15_192_/_0.06)]"
1717
>
18-
<div className="flex items-start justify-between gap-2">
18+
<div className="flex items-start justify-between gap-3">
1919
<div className="min-w-0 flex-1">
2020
<div className="flex items-center gap-2">
21-
<h3 className="truncate font-semibold text-sm text-foreground group-hover:text-primary">
21+
<h3 className="truncate font-semibold text-sm text-foreground transition-colors group-hover:text-primary">
2222
{agent.name}
2323
</h3>
2424
<span
25-
className={`size-2 rounded-full ${statusColors[agent.status] ?? "bg-zinc-500"}`}
25+
className={`size-2 shrink-0 rounded-full transition-all ${statusColors[agent.status] ?? "bg-zinc-500"}`}
2626
title={agent.status}
2727
/>
2828
{agent.verified && <VerifiedBadge />}
2929
</div>
3030
{agent.description && (
31-
<p className="mt-1 line-clamp-2 text-xs text-muted-foreground">
31+
<p className="mt-1.5 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
3232
{agent.description}
3333
</p>
3434
)}
3535
</div>
36-
<div className="w-20 shrink-0">
36+
<div className="shrink-0">
3737
<ReputationMeter score={agent.reputation_score} size="sm" />
3838
</div>
3939
</div>
4040

41+
{/* Capabilities */}
4142
<div className="mt-3 flex flex-wrap gap-1.5">
42-
{agent.capabilities?.slice(0, 4).map((cap) => (
43+
{agent.capabilities?.slice(0, 3).map((cap) => (
4344
<span
4445
key={cap}
45-
className="rounded-md bg-secondary px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground"
46+
className="rounded-md bg-secondary/80 px-1.5 py-0.5 text-[10px] font-medium text-secondary-foreground"
4647
>
4748
{cap}
4849
</span>
4950
))}
50-
{(agent.capabilities?.length ?? 0) > 4 && (
51+
{(agent.capabilities?.length ?? 0) > 3 && (
5152
<span className="text-[10px] text-muted-foreground">
52-
+{agent.capabilities!.length - 4}
53+
+{agent.capabilities!.length - 3}
5354
</span>
5455
)}
5556
</div>
5657

58+
{/* Protocols */}
5759
{agent.protocols && agent.protocols.length > 0 && (
5860
<div className="mt-2 flex gap-1.5">
5961
{agent.protocols.map((p) => (
6062
<span
6163
key={p}
62-
className="rounded bg-primary/10 px-1.5 py-0.5 text-[10px] font-medium text-primary"
64+
className="rounded-md bg-primary/8 px-1.5 py-0.5 text-[10px] font-mono font-medium text-primary"
6365
>
6466
{p}
6567
</span>
6668
))}
6769
</div>
6870
)}
71+
72+
{/* Subtle bottom accent on hover */}
73+
<div className="absolute bottom-0 left-4 right-4 h-px bg-gradient-to-r from-transparent via-primary/0 to-transparent transition-all duration-300 group-hover:via-primary/40" />
6974
</Link>
7075
)
7176
}

web/app/src/components/public/CategoryFilter.tsx

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ export function CategoryFilter({ selected, onChange }: CategoryFilterProps) {
2424
<div className="flex flex-wrap gap-2">
2525
<button
2626
onClick={() => onChange(undefined)}
27-
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
27+
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
2828
!selected
29-
? "bg-primary text-primary-foreground"
30-
: "bg-secondary text-secondary-foreground hover:bg-secondary/80"
29+
? "bg-primary text-primary-foreground shadow-[0_0_12px_oklch(0.72_0.15_192_/_0.15)]"
30+
: "bg-secondary/60 text-secondary-foreground hover:bg-secondary"
3131
}`}
3232
>
3333
{t('categoryFilter.all')}
@@ -36,10 +36,10 @@ export function CategoryFilter({ selected, onChange }: CategoryFilterProps) {
3636
<button
3737
key={cat.id}
3838
onClick={() => onChange(cat.slug === selected ? undefined : cat.slug)}
39-
className={`rounded-full px-3 py-1 text-xs font-medium transition-colors ${
39+
className={`rounded-lg px-3 py-1.5 text-xs font-medium transition-all ${
4040
selected === cat.slug
41-
? "bg-primary text-primary-foreground"
42-
: "bg-secondary text-secondary-foreground hover:bg-secondary/80"
41+
? "bg-primary text-primary-foreground shadow-[0_0_12px_oklch(0.72_0.15_192_/_0.15)]"
42+
: "bg-secondary/60 text-secondary-foreground hover:bg-secondary"
4343
}`}
4444
>
4545
{cat.icon && <span className="mr-1">{cat.icon}</span>}

web/app/src/components/public/PublicLayout.tsx

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -24,21 +24,21 @@ export function PublicLayout() {
2424

2525
return (
2626
<div className="min-h-screen bg-background">
27-
<header className="sticky top-0 z-50 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
27+
<header className="sticky top-0 z-50 border-b border-border/60 bg-background/80 backdrop-blur-xl supports-[backdrop-filter]:bg-background/60">
2828
<div className="mx-auto flex h-14 max-w-6xl items-center justify-between px-4">
29-
<Link to="/" className="flex items-center gap-2">
30-
<img src="/logo.jpg" alt="PeerClaw" className="size-7 rounded-md object-cover" />
31-
<span className="font-semibold text-sm">PeerClaw</span>
29+
<Link to="/" className="flex items-center gap-2.5 group">
30+
<img src="/logo.jpg" alt="PeerClaw" className="size-7 rounded-lg object-cover ring-1 ring-border/50 transition-all group-hover:ring-primary/40" />
31+
<span className="font-semibold text-sm tracking-tight">PeerClaw</span>
3232
</Link>
3333

34-
<nav className="flex items-center gap-4">
34+
<nav className="flex items-center gap-1">
3535
<NavLink
3636
to="/directory"
3737
className={({ isActive }) =>
38-
`text-sm transition-colors ${
38+
`rounded-md px-3 py-1.5 text-sm transition-all ${
3939
isActive
40-
? "text-foreground font-medium"
41-
: "text-muted-foreground hover:text-foreground"
40+
? "text-foreground font-medium bg-secondary/60"
41+
: "text-muted-foreground hover:text-foreground hover:bg-secondary/40"
4242
}`
4343
}
4444
>
@@ -47,10 +47,10 @@ export function PublicLayout() {
4747
<NavLink
4848
to="/playground"
4949
className={({ isActive }) =>
50-
`text-sm transition-colors ${
50+
`rounded-md px-3 py-1.5 text-sm transition-all ${
5151
isActive
52-
? "text-foreground font-medium"
53-
: "text-muted-foreground hover:text-foreground"
52+
? "text-foreground font-medium bg-secondary/60"
53+
: "text-muted-foreground hover:text-foreground hover:bg-secondary/40"
5454
}`
5555
}
5656
>
@@ -59,10 +59,10 @@ export function PublicLayout() {
5959
<NavLink
6060
to="/about"
6161
className={({ isActive }) =>
62-
`text-sm transition-colors ${
62+
`rounded-md px-3 py-1.5 text-sm transition-all ${
6363
isActive
64-
? "text-foreground font-medium"
65-
: "text-muted-foreground hover:text-foreground"
64+
? "text-foreground font-medium bg-secondary/60"
65+
: "text-muted-foreground hover:text-foreground hover:bg-secondary/40"
6666
}`
6767
}
6868
>
@@ -73,19 +73,19 @@ export function PublicLayout() {
7373
<NavLink
7474
to="/console"
7575
className={({ isActive }) =>
76-
`text-sm transition-colors ${
76+
`rounded-md px-3 py-1.5 text-sm transition-all ${
7777
isActive
78-
? "text-foreground font-medium"
79-
: "text-muted-foreground hover:text-foreground"
78+
? "text-foreground font-medium bg-secondary/60"
79+
: "text-muted-foreground hover:text-foreground hover:bg-secondary/40"
8080
}`
8181
}
8282
>
8383
{t('nav.console')}
8484
</NavLink>
8585
<DropdownMenu>
8686
<DropdownMenuTrigger asChild>
87-
<button className="flex items-center gap-1.5 rounded-md px-2 py-1.5 text-sm transition-colors hover:bg-accent/50 focus:outline-none">
88-
<div className="flex size-6 items-center justify-center rounded-full bg-accent text-xs font-medium">
87+
<button className="ml-1 flex items-center gap-1.5 rounded-lg px-2 py-1.5 text-sm transition-all hover:bg-secondary/40 focus:outline-none">
88+
<div className="flex size-6 items-center justify-center rounded-full bg-primary/15 text-xs font-semibold text-primary ring-1 ring-primary/20">
8989
{(user.display_name || user.email).charAt(0).toUpperCase()}
9090
</div>
9191
<span className="max-w-[120px] truncate text-sm text-foreground">
@@ -121,7 +121,7 @@ export function PublicLayout() {
121121
) : (
122122
<Link
123123
to="/login"
124-
className="rounded-md border border-border px-3 py-1.5 text-xs font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
124+
className="ml-1 rounded-lg border border-primary/30 bg-primary/5 px-3.5 py-1.5 text-xs font-medium text-primary transition-all hover:bg-primary/10 hover:border-primary/50"
125125
>
126126
{t('nav.signIn')}
127127
</Link>
@@ -130,7 +130,7 @@ export function PublicLayout() {
130130
href="https://github.com/peerclaw/peerclaw"
131131
target="_blank"
132132
rel="noopener noreferrer"
133-
className="text-muted-foreground hover:text-foreground transition-colors"
133+
className="ml-1 rounded-md p-1.5 text-muted-foreground hover:text-foreground hover:bg-secondary/40 transition-all"
134134
title={t('nav.github')}
135135
>
136136
<Github className="size-[18px]" />
Lines changed: 72 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
function scoreColor(score: number): string {
2-
if (score >= 0.7) return "text-emerald-400"
3-
if (score >= 0.4) return "text-yellow-400"
4-
return "text-red-400"
2+
if (score >= 0.7) return "oklch(0.72 0.18 160)"
3+
if (score >= 0.4) return "oklch(0.8 0.15 85)"
4+
return "oklch(0.65 0.2 25)"
55
}
66

7-
function barColor(score: number): string {
8-
if (score >= 0.7) return "bg-emerald-500"
9-
if (score >= 0.4) return "bg-yellow-500"
10-
return "bg-red-500"
7+
function scoreTextColor(score: number): string {
8+
if (score >= 0.7) return "text-emerald-400"
9+
if (score >= 0.4) return "text-amber-400"
10+
return "text-red-400"
1111
}
1212

1313
export function ReputationMeter({
@@ -19,23 +19,74 @@ export function ReputationMeter({
1919
}) {
2020
const pct = Math.round(score * 100)
2121

22-
const textSize = size === "lg" ? "text-2xl" : size === "md" ? "text-lg" : "text-sm"
23-
const barHeight = size === "lg" ? "h-3" : size === "md" ? "h-2" : "h-1.5"
22+
const dims = {
23+
sm: { svgSize: 64, radius: 26, stroke: 4, fontSize: "text-sm", labelSize: "text-[9px]" },
24+
md: { svgSize: 80, radius: 32, stroke: 5, fontSize: "text-lg", labelSize: "text-[10px]" },
25+
lg: { svgSize: 112, radius: 46, stroke: 6, fontSize: "text-2xl", labelSize: "text-xs" },
26+
}
27+
const d = dims[size]
28+
const circumference = 2 * Math.PI * d.radius
29+
const offset = circumference * (1 - score)
30+
const color = scoreColor(score)
31+
const center = d.svgSize / 2
2432

2533
return (
26-
<div className="flex flex-col gap-1">
27-
<div className="flex items-baseline gap-1">
28-
<span className={`font-bold tabular-nums ${textSize} ${scoreColor(score)}`}>
29-
{pct}
30-
</span>
31-
<span className="text-xs text-muted-foreground">/100</span>
32-
</div>
33-
<div className={`w-full rounded-full bg-muted ${barHeight}`}>
34-
<div
35-
className={`rounded-full ${barHeight} ${barColor(score)} transition-all`}
36-
style={{ width: `${pct}%` }}
34+
<div className="flex flex-col items-center">
35+
<svg
36+
width={d.svgSize}
37+
height={d.svgSize}
38+
viewBox={`0 0 ${d.svgSize} ${d.svgSize}`}
39+
className="drop-shadow-[0_0_8px_oklch(0.72_0.15_192_/_0.15)]"
40+
>
41+
{/* Background track */}
42+
<circle
43+
cx={center}
44+
cy={center}
45+
r={d.radius}
46+
fill="none"
47+
stroke="currentColor"
48+
strokeWidth={d.stroke}
49+
className="text-border"
50+
/>
51+
{/* Progress arc */}
52+
<circle
53+
cx={center}
54+
cy={center}
55+
r={d.radius}
56+
fill="none"
57+
stroke={color}
58+
strokeWidth={d.stroke}
59+
strokeLinecap="round"
60+
strokeDasharray={circumference}
61+
strokeDashoffset={offset}
62+
transform={`rotate(-90 ${center} ${center})`}
63+
className="transition-all duration-1000 ease-out"
64+
style={{
65+
filter: `drop-shadow(0 0 4px ${color})`,
66+
}}
3767
/>
38-
</div>
68+
{/* Score text */}
69+
<text
70+
x={center}
71+
y={center - 2}
72+
textAnchor="middle"
73+
dominantBaseline="central"
74+
className={`font-bold tabular-nums fill-current ${scoreTextColor(score)} ${d.fontSize}`}
75+
style={{ fontFamily: "'Outfit', sans-serif" }}
76+
>
77+
{pct}
78+
</text>
79+
<text
80+
x={center}
81+
y={center + (size === "lg" ? 18 : size === "md" ? 14 : 11)}
82+
textAnchor="middle"
83+
dominantBaseline="central"
84+
className={`fill-current text-muted-foreground ${d.labelSize}`}
85+
style={{ fontFamily: "'Outfit', sans-serif" }}
86+
>
87+
/100
88+
</text>
89+
</svg>
3990
</div>
4091
)
4192
}

0 commit comments

Comments
 (0)