feat(ui): unify font loading and add design system documentation

Font unification:
- Add @fontsource packages for Geist Sans, Geist Mono, Playfair Display
- Create fonts.ts for centralized font imports
- Import fonts in both web (layout.tsx) and desktop (main.tsx)
- Register --font-brand in Tailwind @theme inline block
- Fix font-brand class usage (replace arbitrary value syntax)

Design system documentation:
- Add comprehensive design philosophy comments to globals.css
- Document typography choices (why Geist, why Playfair for brand)
- Document color system approach (neutral grays, semantic colors only)
- Explain component library customizations

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Naiyuan Qing 2026-02-11 14:50:19 +08:00
parent f3c5068e74
commit 54d84abc8b
10 changed files with 177 additions and 41 deletions

View file

@ -1,6 +1,7 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
import "@multica/ui/fonts"
import "@multica/ui/globals.css"
ReactDOM.createRoot(document.getElementById('root')!).render(

View file

@ -27,7 +27,7 @@ export default function WelcomeStep({ onStart }: WelcomeStepProps) {
{/* Brand Title */}
<div className="flex items-center gap-2.5">
<MulticaIcon animate className="size-4 text-muted-foreground/70" />
<h1 className="text-2xl tracking-wide font-[family-name:var(--font-brand)]">
<h1 className="text-2xl tracking-wide font-brand">
Welcome to Multica
</h1>
</div>

View file

@ -7,7 +7,7 @@ export function Header() {
<header className="container flex justify-between items-center p-2">
<div className="flex items-center gap-2.5">
<img src="/logo.svg" alt="Multica" className="size-6 rounded-md" />
<span className="text-sm tracking-wide font-[family-name:var(--font-brand)]">
<span className="text-sm tracking-wide font-brand">
Multica
</span>
</div>

View file

@ -1,28 +1,10 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono, Inter, Playfair_Display } from "next/font/google";
import "@multica/ui/fonts";
import "@multica/ui/globals.css";
import { ThemeProvider } from "@multica/ui/components/theme-provider";
import { Toaster } from "@multica/ui/components/ui/sonner";
import { ServiceWorkerRegister } from "./sw-register";
const inter = Inter({ subsets: ["latin"], variable: "--font-sans" });
const geistSans = Geist({
variable: "--font-geist-sans",
subsets: ["latin"],
});
const geistMono = Geist_Mono({
variable: "--font-geist-mono",
subsets: ["latin"],
});
const playfair = Playfair_Display({
variable: "--font-brand",
subsets: ["latin"],
weight: ["400"],
});
export const metadata: Metadata = {
title: "Multica",
description: "Distributed AI agent framework",
@ -42,10 +24,8 @@ export default function RootLayout({
children: React.ReactNode;
}>) {
return (
<html lang="en" className={inter.variable} suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} ${playfair.variable} antialiased h-dvh flex flex-col`}
>
<html lang="en" suppressHydrationWarning>
<body className="font-sans antialiased h-dvh flex flex-col">
<ThemeProvider
attribute="class"
defaultTheme="system"

View file

@ -71,9 +71,9 @@
"croner": "^10.0.1",
"fast-glob": "^3.3.3",
"grammy": "^1.39.3",
"mysql2": "^3.14.1",
"json5": "^2.2.3",
"linkedom": "^0.18.12",
"mysql2": "^3.14.1",
"nestjs-pino": "^4.5.0",
"pino": "^10.3.0",
"pino-http": "^11.0.0",

View file

@ -3,12 +3,16 @@
"version": "0.1.0",
"private": true,
"type": "module",
"sideEffects": ["**/*.css"],
"sideEffects": [
"**/*.css",
"./src/styles/fonts.ts"
],
"scripts": {
"typecheck": "tsc --noEmit",
"lint": "eslint src"
},
"exports": {
"./fonts": "./src/styles/fonts.ts",
"./globals.css": "./src/styles/globals.css",
"./postcss.config": "./postcss.config.mjs",
"./lib/*": "./src/lib/*.ts",
@ -20,6 +24,9 @@
},
"dependencies": {
"@base-ui/react": "^1.1.0",
"@fontsource-variable/playfair-display": "^5.2.8",
"@fontsource/geist-mono": "^5.2.7",
"@fontsource/geist-sans": "^5.2.5",
"@hugeicons/core-free-icons": "catalog:",
"@hugeicons/react": "catalog:",
"@multica/store": "workspace:*",

View file

@ -22,7 +22,7 @@ export function AppSidebar({ children }: AppSidebarProps) {
alt="Multica"
className="size-7 rounded-md"
/>
<span className="text-sm tracking-wide font-[family-name:var(--font-brand)]">
<span className="text-sm tracking-wide font-brand">
Multica
</span>
</div>

View file

@ -0,0 +1,17 @@
// Unified font imports for Web and Desktop
// Using fontsource for consistent cross-platform font loading
// Geist Sans - Primary UI font
import '@fontsource/geist-sans/400.css'
import '@fontsource/geist-sans/500.css'
import '@fontsource/geist-sans/600.css'
import '@fontsource/geist-sans/700.css'
// Geist Mono - Code font
import '@fontsource/geist-mono/400.css'
import '@fontsource/geist-mono/500.css'
import '@fontsource/geist-mono/600.css'
import '@fontsource/geist-mono/700.css'
// Playfair Display - Brand font
import '@fontsource-variable/playfair-display'

View file

@ -1,3 +1,86 @@
/**
* =============================================================================
* MULTICA DESIGN SYSTEM
* =============================================================================
*
* Design Philosophy:
* - Restrained & Professional: This is a work tool, not a consumer app
* - Clear Communication: UI should inform, not decorate
* - Trust Through Transparency: Users need to understand what the AI is doing
*
* Key Principles:
* 1. RESTRAINT over decoration avoid flashy colors, excessive animations
* 2. CLARITY over cleverness obvious > subtle, explicit > implicit
* 3. CONSISTENCY over novelty use established patterns (Shadcn/UI)
* 4. DENSITY over sprawl respect user's screen real estate
*
* =============================================================================
* TYPOGRAPHY
* =============================================================================
*
* Font Stack (loaded via @fontsource, works across Web + Desktop):
*
* | Font | Variable | Usage |
* |---------------------------|---------------|------------------------------|
* | Geist Sans | font-sans | Primary UI text, body copy |
* | Geist Mono | font-mono | Code, technical values |
* | Playfair Display Variable | font-brand | Brand name "Multica" only |
*
* Why Geist?
* - Created by Vercel, optimized for UI/developer tools
* - Excellent legibility at small sizes (12-14px)
* - Neutral, professional appearance not "AI-ish" or trendy
* - Variable font = smaller bundle, flexible weights
*
* Why Playfair Display for brand?
* - Contrast with Geist creates clear hierarchy
* - Serif adds warmth/personality to otherwise minimal UI
* - Used ONLY for "Multica" text nowhere else
*
* =============================================================================
* COLOR SYSTEM
* =============================================================================
*
* Approach: Neutral grays + semantic colors only
*
* Base Palette (OKLCH for perceptual uniformity):
* - background/foreground: Near-black and near-white, minimal chroma
* - muted: Subtle gray for secondary elements
* - primary: Near-black (light mode), near-white (dark mode) NOT a brand color
*
* Why no brand color?
* - Purple/blue "AI colors" feel generic and dated
* - Work tools should recede, not demand attention
* - Color is reserved for STATE (running/success/error), not decoration
*
* Semantic Colors:
* - destructive: Red delete, danger, errors
* - tool-running: Blue active/in-progress state
* - tool-success: Green completed successfully
* - tool-error: Red/orange failed state
*
* Dark Mode:
* - True dark (#0f0f10), not gray
* - Slightly elevated surfaces for cards/popovers
* - Borders use subtle transparency (oklch with alpha)
*
* =============================================================================
* COMPONENT LIBRARY
* =============================================================================
*
* Built on: Shadcn/UI (https://ui.shadcn.com)
* - Copy-paste components, not a dependency
* - Radix UI primitives for accessibility
* - Tailwind CSS v4 for styling
*
* Customizations:
* - Slightly smaller radius (0.625rem base)
* - Muted color accents (no vibrant primary)
* - Custom scrollbar styling
*
* =============================================================================
*/
@import "tailwindcss";
@source "../../../apps/**/*.{ts,tsx}";
@source "../**/*.{ts,tsx}";
@ -10,8 +93,9 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-sans);
--font-mono: var(--font-geist-mono);
--font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
--font-brand: 'Playfair Display Variable', serif;
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@ -51,6 +135,21 @@
}
:root {
/* =========================================================================
* FONTS unified across Web (Next.js) and Desktop (Electron/Vite)
* Loaded via @fontsource packages in packages/ui/src/styles/fonts.ts
* ========================================================================= */
--font-sans: 'Geist Sans', ui-sans-serif, system-ui, sans-serif;
--font-mono: 'Geist Mono', ui-monospace, monospace;
--font-brand: 'Playfair Display Variable', serif;
/* =========================================================================
* COLORS Light mode
* Using OKLCH for perceptual uniformity across the palette
* Hue ~286 (cool gray with slight purple undertone) for neutrals
* ========================================================================= */
/* Base: pure white bg, near-black text */
--background: oklch(1 0 0);
--foreground: oklch(0.141 0.005 285.823);
--card: oklch(1 0 0);
@ -83,15 +182,23 @@
--sidebar-accent-foreground: oklch(0.21 0.006 285.885);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.705 0.015 286.067);
/* Scrollbar: subtle, non-distracting */
--scrollbar-thumb: oklch(0.82 0.003 286);
--scrollbar-thumb-hover: oklch(0.705 0.015 286.067);
--scrollbar-track: transparent;
--tool-running: oklch(0.6 0.18 250);
--tool-success: oklch(0.72 0.12 145);
--tool-error: oklch(0.65 0.2 25);
/* Tool execution states — these ARE allowed to use color for clarity */
--tool-running: oklch(0.6 0.18 250); /* Blue: active/in-progress */
--tool-success: oklch(0.72 0.12 145); /* Green: completed */
--tool-error: oklch(0.65 0.2 25); /* Red: failed */
}
/* =========================================================================
* COLORS Dark mode
* True dark theme (not gray), elevated surfaces for depth
* ========================================================================= */
.dark {
/* Base: near-black bg, near-white text */
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.985 0 0);
--card: oklch(0.21 0.006 285.885);

40
pnpm-lock.yaml generated
View file

@ -80,13 +80,13 @@ importers:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
'@mariozechner/pi-ai':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
'@mariozechner/pi-coding-agent':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
'@mozilla/readability':
specifier: ^0.6.0
version: 0.6.0
@ -597,13 +597,13 @@ importers:
dependencies:
'@mariozechner/pi-agent-core':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-ai':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@mariozechner/pi-coding-agent':
specifier: 'catalog:'
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))(ws@8.19.0)(zod@3.25.76)
version: 0.52.9(@modelcontextprotocol/sdk@1.26.0(zod@4.3.6))(ws@8.19.0)(zod@4.3.6)
'@multica/types':
specifier: workspace:*
version: link:../types
@ -731,6 +731,15 @@ importers:
'@base-ui/react':
specifier: ^1.1.0
version: 1.1.0(@types/react@19.2.13)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)
'@fontsource-variable/playfair-display':
specifier: ^5.2.8
version: 5.2.8
'@fontsource/geist-mono':
specifier: ^5.2.7
version: 5.2.7
'@fontsource/geist-sans':
specifier: ^5.2.5
version: 5.2.5
'@hugeicons/core-free-icons':
specifier: 'catalog:'
version: 3.1.1
@ -2234,6 +2243,15 @@ packages:
'@floating-ui/utils@0.2.10':
resolution: {integrity: sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==}
'@fontsource-variable/playfair-display@5.2.8':
resolution: {integrity: sha512-ZzVIXPOrL85yyOvZYoBzUszIJM+xKkHqni4IYn2CVLaGQQdJR8sBeC8yFNgjxSJ7ludTwta8qpULeOFuk5X75A==}
'@fontsource/geist-mono@5.2.7':
resolution: {integrity: sha512-xVPVFISJg/K0VVd+aQN0Y7X/sw9hUcJPyDWFJ5GpyU3bHELhoRsJkPSRSHXW32mOi0xZCUQDOaPj1sqIFJ1FGg==}
'@fontsource/geist-sans@5.2.5':
resolution: {integrity: sha512-anllOHyJbElRs9fV15TeDRqAeb1IKm4bSknPl6ZMoyPTx1BBy7logudcUwpNjmQLkzn4Q0JGQLRCUKJYoyST6A==}
'@google/genai@1.40.0':
resolution: {integrity: sha512-fhIww8smT0QYRX78qWOiz/nIQhHMF5wXOrlXvj33HBrz3vKDBb+wibLcEmTA+L9dmPD4KmfNr7UF3LDQVTXNjA==}
engines: {node: '>=20.0.0'}
@ -12029,6 +12047,12 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@fontsource-variable/playfair-display@5.2.8': {}
'@fontsource/geist-mono@5.2.7': {}
'@fontsource/geist-sans@5.2.5': {}
'@google/genai@1.40.0(@modelcontextprotocol/sdk@1.26.0(zod@3.25.76))':
dependencies:
google-auth-library: 10.5.0
@ -15933,7 +15957,7 @@ snapshots:
transitivePeerDependencies:
- supports-color
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
dependencies:
debug: 3.2.7
optionalDependencies:
@ -15964,7 +15988,7 @@ snapshots:
doctrine: 2.1.0
eslint: 9.39.2(jiti@2.6.1)
eslint-import-resolver-node: 0.3.9
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.55.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
hasown: 2.0.2
is-core-module: 2.16.1
is-glob: 4.0.3