Skip to content

Commit e3548e9

Browse files
dreynowclaude
andcommitted
feat: complete light theme redesign for Observatory
Full visual redesign from dark developer theme to light financial theme. Instrument Serif headings for institutional authority, DM Sans body, Geist for data tables. Warm neutral palette (#FAFAF8 bg) with deepened gold accent (#B08D3E). Tight 4/6/8px border radius. 6-item sidebar. DESIGN.md created as source of truth. 22 files updated: Layout, all pages, all shared components, CSS, constants. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 389f756 commit e3548e9

23 files changed

Lines changed: 758 additions & 645 deletions

‎CLAUDE.md‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,9 @@ python tests/interop/test_cross_sdk.py
9696
| kanoniv-agent-auth | crates.io | Check Cargo.toml |
9797
| kanoniv-auth | PyPI | Check sdks/kanoniv-auth/pyproject.toml |
9898
| @kanoniv/agent-auth | npm | Check js/package.json |
99+
100+
## Design System
101+
Always read DESIGN.md before making any visual or UI decisions.
102+
All font choices, colors, spacing, and aesthetic direction are defined there.
103+
Do not deviate without explicit user approval.
104+
In QA mode, flag any code that doesn't match DESIGN.md.

‎DESIGN.md‎

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
# Design System - Kanoniv Observatory
2+
3+
## Product Context
4+
- **What this is:** AP authorization dashboard for AI agents - the proof and control layer between AI agents and QuickBooks
5+
- **Who it's for:** Accounting firms managing AI agents that process invoices, create bills, and handle payments
6+
- **Space/industry:** Fintech / AP automation / Agent authorization. Peers: Melio, Bill.com, Ramp, QuickBooks
7+
- **Project type:** Data-heavy web app / dashboard
8+
9+
## Aesthetic Direction
10+
- **Direction:** Luxury/Refined
11+
- **Decoration level:** Intentional - subtle card elevation, thin borders, gold accent lines. Not cold-minimal, not noisy-expressive.
12+
- **Mood:** Institutional authority with warmth. The dashboard a CFO trusts with financial controls. Feels like wealth management, not a SaaS app.
13+
- **Reference sites:** Melio (meliopayments.com) for onboarding flow and layout patterns. Stripe Dashboard for data density.
14+
- **Key insight:** Every finance dashboard uses cool blue-grays and sans-serif because they copy each other. Observatory is an AUTHORIZATION layer - it should feel institutional and weighty, like a legal document. Warm neutrals + serif headings differentiate from the entire category.
15+
16+
## Typography
17+
- **Display/Hero:** Instrument Serif - Modern serif with institutional authority. No finance dashboard uses serif headings. This communicates "this is the law, not a suggestion."
18+
- **Body/UI:** DM Sans - Clean geometric sans-serif. Professional, readable, neutral. Excellent weight range.
19+
- **Data/Tables:** Geist - Purpose-built for data display. Perfect tabular-nums. Clean at small sizes.
20+
- **Code:** Geist Mono - Monospace companion to Geist. For DIDs, tokens, scopes.
21+
- **Loading:** Google Fonts for Instrument Serif + DM Sans. Self-host Geist/Geist Mono via npm (geist package).
22+
- **Scale:**
23+
- H1: 32px / Instrument Serif / line-height 1.2
24+
- H2: 24px / Instrument Serif / line-height 1.2
25+
- H3: 18px / Instrument Serif / line-height 1.3
26+
- Body: 14px / DM Sans / line-height 1.6
27+
- Small: 13px / DM Sans / line-height 1.5
28+
- Caption: 12px / DM Sans / line-height 1.5
29+
- Label: 10px / DM Sans / font-weight 700 / uppercase / letter-spacing 0.1em
30+
- Data: 14px / Geist / tabular-nums
31+
- Mono: 13px / Geist Mono
32+
33+
## Color
34+
35+
### Light Theme (Primary)
36+
- **Approach:** Restrained - one warm accent (gold) + warm neutrals. Color is rare and meaningful.
37+
- **Background:** #FAFAF8 - warm off-white (NOT pure white)
38+
- **Surface (cards):** #FFFFFF - pure white cards float on warm background
39+
- **Surface hover:** #F7F6F3 - subtle warm hover state
40+
- **Border:** #E8E5DE - warm border (NOT cool gray)
41+
- **Border subtle:** #F0EDE6 - lighter border for inner dividers
42+
- **Text primary:** #1A1814 - warm near-black (NOT pure black)
43+
- **Text secondary:** #6B6760 - warm medium gray
44+
- **Text hint:** #9C978E - warm light gray for labels and hints
45+
- **Gold accent:** #B08D3E - deepened from original #C5A572 for better contrast on white
46+
- **Gold hover:** #C5A572 - original gold, brighter on interaction
47+
- **Gold tint:** #FAF6ED - very light gold for selected states and backgrounds
48+
- **Gold border:** #E8DCC4 - light gold for accent borders
49+
- **Gold text (small):** #8B7130 - darker gold for small text (better contrast)
50+
- **Success:** #1A7A42 - muted deep green
51+
- **Success bg:** #EDFAF2 / **Success border:** #C6F0D6
52+
- **Warning:** #B8860B - dark goldenrod (harmonizes with gold accent)
53+
- **Warning bg:** #FFF8E8 / **Warning border:** #F0DDB0
54+
- **Error:** #C23A3A - muted red
55+
- **Error bg:** #FDF0F0 / **Error border:** #F0C6C6
56+
- **Info:** #2E6DA4 - muted blue
57+
- **Info bg:** #EDF4FB / **Info border:** #B8D4F0
58+
59+
### Dark Theme (Secondary)
60+
- Strategy: Warm dark surfaces, NOT pure black. Reduce accent brightness slightly.
61+
- **Background:** #0F0E0C
62+
- **Surface:** #1A1814
63+
- **Surface hover:** #242118
64+
- **Border:** #2E2A22
65+
- **Border subtle:** #1E1B16
66+
- **Text primary:** #E8E5DE
67+
- **Text secondary:** #9C978E
68+
- **Text hint:** #6B6760
69+
- **Gold accent:** #C5A572
70+
- **Gold hover:** #D4BC94
71+
- **Gold tint:** #1E1A12
72+
73+
### Shadows
74+
- **Small:** 0 1px 2px rgba(26, 24, 20, 0.04)
75+
- **Medium:** 0 2px 8px rgba(26, 24, 20, 0.06)
76+
- **Large:** 0 4px 16px rgba(26, 24, 20, 0.08)
77+
78+
## Spacing
79+
- **Base unit:** 4px
80+
- **Density:** Comfortable - finance users need clarity, not cramming
81+
- **Scale:** 2xs(2px) xs(4px) sm(8px) md(12px) lg(16px) xl(24px) 2xl(32px) 3xl(48px) 4xl(64px)
82+
83+
## Layout
84+
- **Approach:** Grid-disciplined - strict alignment for data-heavy dashboard
85+
- **Grid:** 12-column at desktop, 6-column at tablet, stack on mobile
86+
- **Max content width:** 1200px
87+
- **Sidebar:** 220px fixed, collapsible. White surface, border-right separator.
88+
- **Border radius:** sm: 4px (inputs, small elements), md: 6px (cards, buttons), lg: 8px (modals, large containers), full: 9999px (pills, badges)
89+
- **Card pattern:** White surface, 1px warm border, small shadow. No rounded-xl bubbly SaaS look.
90+
91+
## Motion
92+
- **Approach:** Intentional - subtle entrance animations, meaningful state transitions. NOT expressive/playful.
93+
- **Framework:** Framer Motion (already in use)
94+
- **Easing:** enter: ease-out / exit: ease-in / move: cubic-bezier(0.25, 0.1, 0.25, 1)
95+
- **Duration:** micro: 100ms / short: 150ms / medium: 250ms / long: 400ms
96+
- **Patterns:**
97+
- Page enter: opacity 0->1, y 8->0, duration 200ms
98+
- Stagger children: 60ms between items (lighter than current 80ms)
99+
- No spring pops on data elements (springs are playful - wrong for finance)
100+
- Skeleton loaders for async data (pulse animation)
101+
- Height collapse for dismiss/remove
102+
103+
## Sidebar Navigation
104+
Six items maximum (from Melio research):
105+
1. Home (dashboard)
106+
2. Clients (firm/QB connections)
107+
3. Agents (registry + delegation)
108+
4. Escalations (pending approvals)
109+
5. Audit (provenance + export)
110+
6. Settings
111+
112+
Icons: Lucide React, 18px, stroke-width 1.5. Active state: gold tint background + gold text.
113+
114+
## Accessibility Notes
115+
- Gold accent (#B08D3E) on white: 3.5:1 contrast - passes AA for large text (18px+) and UI components
116+
- For small gold text, use darker variant (#8B7130) at 5.2:1 contrast
117+
- Primary buttons: gold bg (#B08D3E) with white text (#FFFFFF) - 4.5:1 passes AA
118+
- All semantic colors chosen for WCAG AA compliance on their respective backgrounds
119+
- Focus rings: 2px gold outline with gold-tint offset
120+
121+
## Decisions Log
122+
| Date | Decision | Rationale |
123+
|------|----------|-----------|
124+
| 2026-03-24 | Initial design system created | /design-consultation based on Melio competitive research. Light theme mandatory for finance product. |
125+
| 2026-03-24 | Instrument Serif for headings | Institutional authority differentiator. No finance dashboard uses serif - communicates "this is the law." |
126+
| 2026-03-24 | Warm neutrals over cool grays | Every competitor uses cool blue-gray. Warm palette (#FAFAF8 bg) with gold feels like wealth management. |
127+
| 2026-03-24 | Gold accent retained + deepened | Brand continuity. Deepened from #C5A572 to #B08D3E for white-bg contrast. No competitor uses gold. |
128+
| 2026-03-24 | Tight border radius (4/6/8px) | Financial precision. No bubbly SaaS rounding. Tight corners communicate exactness. |

‎apps/observatory/index.html‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,10 @@
33
<head>
44
<meta charset="UTF-8" />
55
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
6-
<title>Agent Trust Observatory</title>
6+
<title>Kanoniv Observatory</title>
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=DM+Sans:ital,opsz,wght@0,9..40,100..1000;1,9..40,100..1000&family=Instrument+Serif:ital@0;1&display=swap" rel="stylesheet">
710
</head>
811
<body>
912
<div id="root"></div>

‎apps/observatory/src/components/ActivityFeed.tsx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,34 @@ export const ActivityFeed: React.FC<ActivityFeedProps> = ({ entries, limit = 10
1919
<div className="space-y-1.5">
2020
{items.map((entry, i) => {
2121
const isExp = expanded === entry.id;
22-
const colors = actionColor[entry.action] || 'bg-zinc-500/10 text-zinc-400 border-zinc-500/20';
22+
const colors = actionColor[entry.action] || 'bg-[#F7F6F3] text-[#6B6760] border-[#E8E5DE]';
2323
return (
2424
<motion.div
2525
key={entry.id}
2626
initial={{ opacity: 0, y: 8 }}
2727
animate={{ opacity: 1, y: 0 }}
2828
transition={{ delay: i * 0.06 }}
2929
className={cn(
30-
'rounded-lg bg-[#12121a] border cursor-pointer transition-colors',
31-
isExp ? 'border-[#C5A572]/30' : 'border-white/[.07] hover:border-white/[.12]',
30+
'rounded-md bg-white border cursor-pointer transition-colors',
31+
isExp ? 'border-[#E8DCC4] shadow-[0_1px_2px_rgba(26,24,20,0.04)]' : 'border-[#E8E5DE] hover:border-[#E8DCC4]',
3232
)}
3333
onClick={() => setExpanded(isExp ? null : entry.id)}
3434
>
3535
<div className="flex items-center gap-3 px-3 py-2.5">
36-
<span className="text-xs font-medium text-[#E8E8ED] min-w-[80px]">{entry.agent_name}</span>
36+
<span className="text-xs font-medium text-[#1A1814] min-w-[80px]">{entry.agent_name}</span>
3737
<span className={cn('text-[10px] px-1.5 py-0.5 rounded border', colors)}>
3838
{entry.action}
3939
</span>
4040
{entry.entity_ids.length > 0 && (
41-
<span className="text-[10px] font-mono text-zinc-600 truncate flex-1">
41+
<span className="text-[10px] font-mono text-[#9C978E] truncate flex-1">
4242
{entry.entity_ids.map(id => shortId(id)).join(', ')}
4343
</span>
4444
)}
45-
<span className="text-[10px] text-zinc-600 shrink-0">{timeAgo(entry.created_at)}</span>
45+
<span className="text-[10px] text-[#9C978E] shrink-0">{timeAgo(entry.created_at)}</span>
4646
</div>
4747
{isExp && Object.keys(entry.metadata).length > 0 && (
48-
<div className="px-3 pb-2.5 border-t border-white/5">
49-
<pre className="text-[9px] text-zinc-500 mt-2 overflow-x-auto">
48+
<div className="px-3 pb-2.5 border-t border-[#F0EDE6]">
49+
<pre className="text-[9px] text-[#6B6760] mt-2 overflow-x-auto font-mono">
5050
{JSON.stringify(entry.metadata, null, 2)}
5151
</pre>
5252
</div>

‎apps/observatory/src/components/AgentCard.tsx‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,33 +16,33 @@ export const AgentCard: React.FC<AgentCardProps> = ({ agent, selected, onClick }
1616
<div
1717
onClick={onClick}
1818
className={cn(
19-
'relative flex items-center gap-3 px-3 py-3 rounded-lg cursor-pointer transition-colors border',
19+
'relative flex items-center gap-3 px-3 py-3 rounded-md cursor-pointer transition-colors border',
2020
selected
21-
? 'bg-[#C5A572]/5 border-[#C5A572]/30'
22-
: 'border-transparent hover:bg-white/[.03]',
21+
? 'bg-[#FAF6ED] border-[#E8DCC4]'
22+
: 'border-transparent hover:bg-[#F7F6F3]',
2323
)}
2424
>
2525
{selected && (
26-
<div className="absolute left-0 top-2 bottom-2 w-0.5 bg-[#C5A572] rounded-r" />
26+
<div className="absolute left-0 top-2 bottom-2 w-0.5 bg-[#B08D3E] rounded-r" />
2727
)}
2828
<ReputationBadge score={score} size="sm" />
2929
<div className="flex-1 min-w-0">
3030
<div className="flex items-center gap-2">
31-
<span className="text-xs font-medium text-[#E8E8ED]">{agent.name}</span>
31+
<span className="text-xs font-medium text-[#1A1814]">{agent.name}</span>
3232
<span className={cn('w-1.5 h-1.5 rounded-full', statusDot(agent.status))} />
3333
</div>
3434
{agent.description && (
35-
<div className="text-[10px] text-zinc-600 mt-0.5 truncate">{agent.description}</div>
35+
<div className="text-[10px] text-[#9C978E] mt-0.5 truncate">{agent.description}</div>
3636
)}
3737
{agent.capabilities.length > 0 && (
3838
<div className="flex flex-wrap gap-1 mt-1.5">
3939
{agent.capabilities.slice(0, 4).map(cap => (
40-
<span key={cap} className="text-[9px] px-1.5 py-0.5 rounded bg-white/[.04] text-zinc-500 border border-white/[.06]">
40+
<span key={cap} className="text-[9px] px-1.5 py-0.5 rounded bg-[#FAFAF8] text-[#6B6760] border border-[#F0EDE6]">
4141
{cap}
4242
</span>
4343
))}
4444
{agent.capabilities.length > 4 && (
45-
<span className="text-[9px] text-zinc-600">+{agent.capabilities.length - 4}</span>
45+
<span className="text-[9px] text-[#9C978E]">+{agent.capabilities.length - 4}</span>
4646
)}
4747
</div>
4848
)}

‎apps/observatory/src/components/DelegationCard.tsx‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ export const DelegationCard: React.FC<DelegationCardProps> = ({ delegation, onRe
1414

1515
return (
1616
<div className={cn(
17-
'rounded-lg bg-[#12121a] border border-white/[.07] px-3 py-2.5',
17+
'rounded-lg bg-white border border-[#E8E5DE] px-3 py-2.5',
1818
expired && 'opacity-50',
1919
)}>
2020
<div className="flex items-center gap-2 mb-2">
21-
<span className="text-xs font-medium text-[#E8E8ED]">{delegation.grantor_name}</span>
22-
<ArrowRight className="w-3 h-3 text-[#C5A572]" />
23-
<span className="text-xs font-medium text-[#E8E8ED]">{delegation.agent_name}</span>
21+
<span className="text-xs font-medium text-[#1A1814]">{delegation.grantor_name}</span>
22+
<ArrowRight className="w-3 h-3 text-[#B08D3E]" />
23+
<span className="text-xs font-medium text-[#1A1814]">{delegation.agent_name}</span>
2424
{delegation.expires_at && (
25-
<span className="flex items-center gap-1 text-[9px] text-zinc-600 ml-auto">
25+
<span className="flex items-center gap-1 text-[9px] text-[#9C978E] ml-auto">
2626
<Clock className="w-3 h-3" />
2727
{expired ? 'expired' : timeAgo(delegation.expires_at).replace(' ago', '')}
2828
</span>
@@ -34,7 +34,7 @@ export const DelegationCard: React.FC<DelegationCardProps> = ({ delegation, onRe
3434
key={scope}
3535
aria-label={`Remove scope: ${scope}`}
3636
onClick={e => { e.stopPropagation(); onRemoveScope?.(delegation.id, delegation.scopes, scope); }}
37-
className="text-[9px] px-1.5 py-0.5 rounded bg-[#C5A572]/10 text-[#C5A572] border border-[#C5A572]/20 hover:bg-red-500/10 hover:text-red-400 hover:border-red-500/20 transition-colors"
37+
className="text-[9px] px-1.5 py-0.5 rounded bg-[#FAF6ED] text-[#B08D3E] border border-[#E8DCC4] hover:bg-red-500/10 hover:text-[#C23A3A] hover:border-red-500/20 transition-colors"
3838
>
3939
{scope}
4040
</button>
@@ -43,7 +43,7 @@ export const DelegationCard: React.FC<DelegationCardProps> = ({ delegation, onRe
4343
<button
4444
aria-label={`Revoke delegation from ${delegation.grantor_name} to ${delegation.agent_name}`}
4545
onClick={e => { e.stopPropagation(); onRevoke(delegation.id); }}
46-
className="ml-auto p-1 rounded hover:bg-red-500/10 text-zinc-600 hover:text-red-400 transition-colors"
46+
className="ml-auto p-1 rounded hover:bg-red-500/10 text-[#9C978E] hover:text-[#C23A3A] transition-colors"
4747
>
4848
<Trash2 className="w-3 h-3" />
4949
</button>

‎apps/observatory/src/components/ErrorBoundary.tsx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,18 @@ export class ErrorBoundary extends React.Component<
2525
render() {
2626
if (this.state.hasError) {
2727
return (
28-
<div className="flex items-center justify-center h-screen bg-[#0a0a0f]">
28+
<div className="flex items-center justify-center h-screen bg-[#FAFAF8]">
2929
<div className="max-w-md text-center p-8">
3030
<div className="w-12 h-12 rounded-xl bg-red-500/10 border border-red-500/20 flex items-center justify-center mx-auto mb-4">
31-
<span className="text-red-400 text-xl">!</span>
31+
<span className="text-[#C23A3A] text-xl">!</span>
3232
</div>
33-
<h1 className="text-lg font-bold text-[#E8E8ED] mb-2">Something went wrong</h1>
34-
<p className="text-sm text-zinc-500 mb-4">
33+
<h1 className="text-lg font-bold text-[#1A1814] mb-2">Something went wrong</h1>
34+
<p className="text-sm text-[#9C978E] mb-4">
3535
{this.state.error?.message || 'An unexpected error occurred.'}
3636
</p>
3737
<button
3838
onClick={this.handleReload}
39-
className="px-4 py-2 rounded-lg bg-[#C5A572] text-[#0a0a0f] text-sm font-bold hover:bg-[#D4BC94] transition-colors"
39+
className="px-4 py-2 rounded-lg bg-[#B08D3E] text-white text-sm font-bold hover:bg-[#C5A572] transition-colors"
4040
>
4141
Reload
4242
</button>

0 commit comments

Comments
 (0)