From 54d84abc8b83d51fbbb4a9051b1929656c25d722 Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Wed, 11 Feb 2026 14:50:19 +0800 Subject: [PATCH] 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 --- apps/desktop/src/renderer/src/main.tsx | 1 + .../onboarding/components/welcome-step.tsx | 2 +- apps/web/app/header.tsx | 2 +- apps/web/app/layout.tsx | 26 +--- package.json | 2 +- packages/ui/package.json | 9 +- packages/ui/src/components/app-sidebar.tsx | 2 +- packages/ui/src/styles/fonts.ts | 17 +++ packages/ui/src/styles/globals.css | 117 +++++++++++++++++- pnpm-lock.yaml | 40 ++++-- 10 files changed, 177 insertions(+), 41 deletions(-) create mode 100644 packages/ui/src/styles/fonts.ts diff --git a/apps/desktop/src/renderer/src/main.tsx b/apps/desktop/src/renderer/src/main.tsx index a4e610e1..87caad2e 100644 --- a/apps/desktop/src/renderer/src/main.tsx +++ b/apps/desktop/src/renderer/src/main.tsx @@ -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( diff --git a/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx index 824333c1..c779cf22 100644 --- a/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx +++ b/apps/desktop/src/renderer/src/pages/onboarding/components/welcome-step.tsx @@ -27,7 +27,7 @@ export default function WelcomeStep({ onStart }: WelcomeStepProps) { {/* Brand Title */}
-

+

Welcome to Multica

diff --git a/apps/web/app/header.tsx b/apps/web/app/header.tsx index dee519d6..6c77f1b1 100644 --- a/apps/web/app/header.tsx +++ b/apps/web/app/header.tsx @@ -7,7 +7,7 @@ export function Header() {
Multica - + Multica
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx index 020bbdc6..69ea8aae 100644 --- a/apps/web/app/layout.tsx +++ b/apps/web/app/layout.tsx @@ -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 ( - - + + - + Multica diff --git a/packages/ui/src/styles/fonts.ts b/packages/ui/src/styles/fonts.ts new file mode 100644 index 00000000..a9694b2b --- /dev/null +++ b/packages/ui/src/styles/fonts.ts @@ -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' diff --git a/packages/ui/src/styles/globals.css b/packages/ui/src/styles/globals.css index aa693af3..98081a60 100644 --- a/packages/ui/src/styles/globals.css +++ b/packages/ui/src/styles/globals.css @@ -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); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f1db6aa..c136db71 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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