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:
parent
f3c5068e74
commit
54d84abc8b
10 changed files with 177 additions and 41 deletions
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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:*",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
17
packages/ui/src/styles/fonts.ts
Normal file
17
packages/ui/src/styles/fonts.ts
Normal 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'
|
||||
|
|
@ -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
40
pnpm-lock.yaml
generated
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue