Skip to content

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) ​

css
--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) ​

css
--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) ​

css
--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 ​

css
--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 ​

css
--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(...):

js
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) ​

css
--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) ​

css
--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 ​

css
--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 ​

css
: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:

ExceptionReason
rounded (2px) on input/buttonTactile rounding to avoid visual harshness
rounded-full on PipelineFlow agent avatarCircular avatar requires rounding

→ The cost of visual consistency is accepting these two exceptions.


Focus Ring ​

css
*: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 lower

Core 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 CaseDecorative or SemanticColor Allowed?
BUY buttonDecorative (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 chipSemantic (suggested direction)✅ chip-pos
HOLD verdict chipNeutral❌ Grayscale
ETF kind labelDecorative (kind is structural info, not a state)❌ Grayscale mono
Macro role labelDecorative (role is structural)❌ Grayscale mono
Stale data markerSemantic (abnormal state)✅ text-stale or text-warn
Dialog "Add" buttonDecorative❌ Accent
Dialog "Delete" buttonSemantic (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:

ContainerPadding
Page mainpx-6 py-10 (max-w-6xl)
Cardp-5 (5 = 1.25rem = 20px)
Dialog bodyp-5
Dialog headerpx-5 py-4
Tab buttonpx-3 py-2
Button mdh-10 px-4
Button smh-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:

  • PipelineFlow component: 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 ​

DecisionReason
No shadcn/uiSingle-person internal tool; 9 custom components (Button/Card/Dialog/Field/StatusBadge/Tabs/SymbolSearch/PipelineFlow/HoldingCard) are sufficient.
No Radix UISame as above; Dialog uses native <dialog> which already handles focus management and ESC close.
No React QuerySWR is lighter; for simple GET scenarios, there is no difference.
No Next.jsStatic + Caddy file_server mode, no SSR daemon (learned from mc-website lesson).

Maintenance Rules ​

  • Adding a new component → First check if Card / Button / Field / Dialog can 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)