GUI Design System
Token three layers + Typography three layers + IA decisions. This chapter is for frontend developers/designers, explaining why the GUI is designed this way and not another.
← 09-troubleshooting · Wiki Index
Design Philosophy
It follows a middle path between "finance" and "editorial feel." It is not a pure monochrome magazine style (financial scenarios need to retain red/green semantics for gains/losses), nor is it the dense chart flow of a Bloomberg terminal.
Borrowed from Monochrome
- ✅ Restraint: Color is not used for decoration, only for data semantics.
- ✅ Typographic hierarchy: Display serif (Playfair) for large hero text.
- ✅ Zero border-radius principle (except for tactile rounding on buttons/inputs).
- ✅ Line-based structure for separation, not shadows or rings.
Rejected Elements
- ❌ Pure black-and-white magazine style (financial tools cannot omit red/green for gains/losses).
- ❌ 8xl/9xl large text (a dashboard is not a Vogue cover).
- ❌ Light backgrounds (financial scenarios require long usage; dark mode is easier on the eyes).
- ❌ Full serif body text (serif numbers have variable widths that harm readability).
Token System (CSS Variables)
Source: invest-gui/src/index.css :root
Surface Container Layer (4 Levels)
--surface-base: #0a0a0b; /* Body bottom layer */
--surface-raised: #131417; /* Cards / dialogs */
--surface-overlay: #1c1d22; /* Hover / nested elements */
--surface-inverse: #f5f5f5; /* Marketing hero inverse area */→ Three densities of dark background + one inverse white background (for marketing empty states like "How our AI makes decisions").
Text Layer (3+1 Levels)
--text-primary: #f5f5f5;
--text-secondary: #a1a1aa;
--text-tertiary: #71717a;
--text-inverse: #0a0a0b;→ Tertiary for labels/hints/timestamps; secondary for placeholders/secondary info; primary for body text.
Border Layer (2 Levels)
--border-subtle: rgba(255,255,255,0.08); /* Separation only */
--border-strong: rgba(255,255,255,0.16); /* Card edges / focus start point */→ Use borders instead of shadows for separation. Shadows are hard to see on dark backgrounds anyway.
Accent Monochrome Decorative Class
--accent: #f5f5f5;
--accent-hover: #ffffff;
--accent-foreground: #0a0a0b;→ Used only for primary CTA / focus / wordmark / active state. Prohibited for background fill decoration.
Data Semantic Layer
--pos: #4ade80; /* Up / profit / healthy */
--pos-bg: rgba(74,222,128,0.10);
--neg: #f87171; /* Down / loss / error */
--neg-bg: rgba(248,113,113,0.10);
--warn: #fbbf24; /* Warning / near threshold */
--warn-bg: rgba(251,191,36,0.10);
--stale: #71717a; /* Stale data */→ Only for numbers/chips/status badges, prohibited for decoration. → kind=etf using a blue ring was decorative (deprecated); BUY=green is semantic (retained).
Tailwind Integration
tailwind.config.js bridges using var(...):
colors: {
surface: {
base: "var(--surface-base)",
raised: "var(--surface-raised)",
...
},
}→ Components can use bg-[var(--surface-raised)] or bg-surface-raised (semantically consistent).
→ For future light mode: only change the var values in :root, components remain unchanged.
Typography Three Layers
Display: Playfair Display (serif)
--font-display: "Playfair Display", "Source Han Serif SC", Georgia, serif;Where to use:
- Wordmark
invest - Page titles (Dashboard "Holdings", Strategy "Strategies")
- Hero large text (Total Assets ¥XXX,XXX)
- Committee verdict large text ("TRIM" / "ACCUMULATE")
- Empty state main slogans
Do not use for: body text, tables, numbers (serif numbers have variable widths).
UI: Inter (sans)
--font-ui: "Inter", "PingFang SC", "Hiragino Sans GB", system-ui, sans-serif;Where to use: Default for all UI text—buttons, labels, tables, menus, explanatory paragraphs, error messages.
Mono: JetBrains Mono
--font-mono: "JetBrains Mono", ui-monospace, monospace;Where to use:
- All numbers (paired with
tabular-nums) - Timestamps (
02:17:18) - Symbols (
NDQ.AX/GC=F) - Task IDs / logs / pre code blocks
→ Using mono for numbers solves both serif number width jitter and table column alignment.
Zero Border-Radius Principle
:root {
/* No --radius, but none given by default either */
}tailwind.config.js does not extend borderRadius, using Tailwind defaults. All components do not use rounded-*—right angles are the default.
Exceptions:
| Exception | Reason |
|---|---|
rounded (2px) on input/button | Tactile rounding to avoid visual harshness |
rounded-full on PipelineFlow agent avatar | Circular avatar requires rounding |
→ The cost of visual consistency is accepting these two exceptions.
Focus Ring
*:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}→ High-contrast outline, not reliant on color for identification. → Meets accessibility WCAG AAA requirements; keyboard navigation friendly.
Information Architecture (IA)
Top Navigation Hierarchy (Not 7 Equal Items)
invest Dashboard Transactions Strategies Committee System
└──┬──┘ └──────────────────────────┬──────────────────────────┘ └──┬──┘
wordmark Core Daily 4 Items Secondary
display High User Frequency Smaller font + one gray level lowerCore vs. Secondary Judgment: How many times do you open it per day?
- Dashboard / Transactions / Strategies / Committee → Users check multiple times daily.
- System → Occasional diagnostics / checking if cron jobs ran.
Active state uses a 2px solid bottom border instead of color—color is reserved for data semantics.
Committee 7-Tab One-Stop Shop
Historical lesson: Previously, there were three routes (/committee, /system, /transparency) all explaining "How AI makes decisions," confusing users.
After merging:
/committee = Everything about "How AI makes decisions"
- Triggers / Live Feed
- Verdict Archive (historical markdown)
- Decision Replay (Pipeline animation)
- 4 Roles + Rules (full system prompts)
- Historical Hit Rate
- LLM Usage
- Tool Calls
/system = Internal State + Data Sources
- Cron Jobs
- Market Regime
- Data Sources
- PnL History
- Long-term Patterns
- Dreams→ User wants to know "How did AI calculate that?" → /committee; "Is the system running?" → /system.
See adr/ for the decision record on IA consolidation.
Dashboard Hero Landmark
Two facts must be immediately visible upon entering the Dashboard:
┌─────────────────────────────────────────────────────┐
│ Total Assets · in CNY Last Committee │
│ │
│ ¥225,378 ←─── Playfair display │ TRIM (italic) │
│ 5xl/6xl │ NDQ.AX · 05-07 │
│ │ Confidence 75% │
│ Cash ¥15,511 Holdings ¥209,867│ │
│ AUD=4.92 │ View Full Record →│
└─────────────────────────────────────────────────────┘Design points:
- Total assets use Playfair large text → ceremonial feel + instantly readable.
- Numbers use mono, not serif → solves width jitter.
- When data cannot be fetched, display
¥—instead of¥0(do not pretend zero assets to avoid misleading).
Data Semantic Color vs. Decorative Color (Key Distinction)
| Use Case | Decorative or Semantic | Color Allowed? |
|---|---|---|
| BUY button | Decorative (it's an action, not a state) | ❌ Use accent only |
| Unrealized gain +12% | Semantic (price went up) | ✅ text-pos |
| Unrealized loss -3% | Semantic (price went down) | ✅ text-neg |
| BUY verdict chip | Semantic (suggested direction) | ✅ chip-pos |
| HOLD verdict chip | Neutral | ❌ Grayscale |
| ETF kind label | Decorative (kind is structural info, not a state) | ❌ Grayscale mono |
| Macro role label | Decorative (role is structural) | ❌ Grayscale mono |
| Stale data marker | Semantic (abnormal state) | ✅ text-stale or text-warn |
| Dialog "Add" button | Decorative | ❌ Accent |
| Dialog "Delete" button | Semantic (destructive action) | ✅ chip-neg |
Judgment Formula: "Can it be mapped to up/down, success/failure, healthy/stale?" Yes → use color; just a classification label → grayscale + mono.
Spacing / Rhythm
No enforced grid system, but default padding:
| Container | Padding |
|---|---|
| Page main | px-6 py-10 (max-w-6xl) |
| Card | p-5 (5 = 1.25rem = 20px) |
| Dialog body | p-5 |
| Dialog header | px-5 py-4 |
| Tab button | px-3 py-2 |
| Button md | h-10 px-4 |
| Button sm | h-8 px-3 |
Vertical rhythm: Use mt-10 border-t border-[var(--border-subtle)] pt-6 for strong structural separation between sections.
Animation
The motion (formerly framer-motion v12) library is used only for:
PipelineFlowcomponent: particle flow animation + agent avatar selection state.- Everything else does not animate.
Design intent: Financial tools should not have fancy animations; transition-colors duration-100 is sufficient for hover states.
Reasons for Not Using shadcn/ui
| Decision | Reason |
|---|---|
| No shadcn/ui | Single-person internal tool; 9 custom components (Button/Card/Dialog/Field/StatusBadge/Tabs/SymbolSearch/PipelineFlow/HoldingCard) are sufficient. |
| No Radix UI | Same as above; Dialog uses native <dialog> which already handles focus management and ESC close. |
| No React Query | SWR is lighter; for simple GET scenarios, there is no difference. |
| No Next.js | Static + Caddy file_server mode, no SSR daemon (learned from mc-website lesson). |
Maintenance Rules
- Adding a new component → First check if
Card/Button/Field/Dialogcan be reused. - Adding a new color → First ask "Is it decorative or semantic?" If a token can be used, do not hardcode.
- Adding a new font → In 90% of cases, the existing three layers are sufficient. Propose new fonts in the wiki first for discussion.
- Changing a token → Update this wiki chapter simultaneously + notify all places using hardcoded colors.
Next Steps
→ invest-gui/README.md — Frontend repository structure
→ adr/ — IA decision records (why transparency was merged into committee)