750 lines
33 KiB
Markdown
750 lines
33 KiB
Markdown
|
|
# MOPC Platform - UI/UX Architecture
|
||
|
|
|
||
|
|
## Overview
|
||
|
|
|
||
|
|
The MOPC platform uses a mobile-first responsive design built with:
|
||
|
|
|
||
|
|
- **Next.js App Router** - Server Components by default
|
||
|
|
- **shadcn/ui** - Accessible, customizable component library
|
||
|
|
- **Tailwind CSS** - Utility-first styling
|
||
|
|
- **Radix UI** - Headless accessible primitives
|
||
|
|
- **Motion** (Framer Motion) - Buttery smooth animations
|
||
|
|
- **Vaul** - Native-feeling mobile drawers
|
||
|
|
- **Sonner** - Beautiful toast notifications
|
||
|
|
- **cmdk** - Command palette (⌘K)
|
||
|
|
|
||
|
|
## Design Philosophy
|
||
|
|
|
||
|
|
**CRITICAL: Avoid "AI-built" aesthetic. Platform must look professionally designed.**
|
||
|
|
|
||
|
|
### What to AVOID (typical AI-generated look)
|
||
|
|
| Don't | Why | Instead |
|
||
|
|
|-------|-----|---------|
|
||
|
|
| Cards everywhere | Generic, lazy layout | Use varied layouts: tables, lists, grids, hero sections |
|
||
|
|
| Same border-radius on everything | Monotonous | Vary: sharp corners for data, rounded for actions |
|
||
|
|
| Identical padding/spacing | Robotic feel | Use intentional rhythm: tight for data, generous for CTAs |
|
||
|
|
| Blue/purple gradients | Screams "AI template" | Use brand colors with restraint |
|
||
|
|
| Stock icons everywhere | Impersonal | Custom icons or carefully curated set |
|
||
|
|
| Centered everything | No visual hierarchy | Left-align content, strategic centering |
|
||
|
|
| Gray backgrounds | Dull, corporate | Subtle off-white textures, strategic white space |
|
||
|
|
| "Dashboard" with 6 equal cards | The #1 AI cliché | Prioritize: hero metric, then supporting data |
|
||
|
|
|
||
|
|
### What TO DO (professional design)
|
||
|
|
| Do | Why | Example |
|
||
|
|
|----|-----|---------|
|
||
|
|
| Visual hierarchy | Guides the eye | Large numbers for KPIs, smaller for details |
|
||
|
|
| Intentional white space | Breathability | 32-48px between sections, not uniform 16px |
|
||
|
|
| Typography scale | Professional rhythm | 12/14/16/20/24/32/48px - skip sizes intentionally |
|
||
|
|
| Micro-interactions | Delight users | Button hover states, loading skeletons |
|
||
|
|
| Consistent but varied | Not monotonous | Same colors, different layouts per page |
|
||
|
|
| Data density where needed | Efficient | Tables for lists, not cards |
|
||
|
|
| Strategic color accents | Draw attention | Red only for primary CTAs, not decoration |
|
||
|
|
| Real content sizes | Accommodate reality | Long project names, international characters |
|
||
|
|
|
||
|
|
## Brand Colors
|
||
|
|
|
||
|
|
```css
|
||
|
|
:root {
|
||
|
|
/* Brand Colors */
|
||
|
|
--color-primary: #de0f1e; /* Primary Red - CTAs, alerts */
|
||
|
|
--color-primary-hover: #c00d1a;
|
||
|
|
--color-secondary: #053d57; /* Dark Blue - headers, sidebar */
|
||
|
|
--color-accent: #557f8c; /* Teal - links, secondary elements */
|
||
|
|
--color-background: #fefefe; /* White - backgrounds */
|
||
|
|
|
||
|
|
/* Semantic */
|
||
|
|
--color-success: #10b981;
|
||
|
|
--color-warning: #f59e0b;
|
||
|
|
--color-error: #ef4444;
|
||
|
|
|
||
|
|
/* Neutrals (warm, not cold gray) */
|
||
|
|
--color-gray-50: #fafaf9;
|
||
|
|
--color-gray-100: #f5f5f4;
|
||
|
|
--color-gray-200: #e7e5e4;
|
||
|
|
--color-gray-500: #78716c;
|
||
|
|
--color-gray-900: #1c1917;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Typography
|
||
|
|
|
||
|
|
- **Font Family**: Montserrat
|
||
|
|
- **Headings**: 600/700 weight
|
||
|
|
- **Body**: 300/400 weight (Montserrat Light)
|
||
|
|
|
||
|
|
```css
|
||
|
|
:root {
|
||
|
|
--font-family: 'Montserrat', system-ui, sans-serif;
|
||
|
|
--font-size-xs: 0.75rem; /* 12px */
|
||
|
|
--font-size-sm: 0.875rem; /* 14px */
|
||
|
|
--font-size-base: 1rem; /* 16px */
|
||
|
|
--font-size-lg: 1.25rem; /* 20px */
|
||
|
|
--font-size-xl: 1.5rem; /* 24px */
|
||
|
|
--font-size-2xl: 2rem; /* 32px */
|
||
|
|
--font-size-3xl: 3rem; /* 48px */
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Design Principles
|
||
|
|
|
||
|
|
1. **Mobile First**: Base styles for mobile, enhanced for larger screens
|
||
|
|
2. **Accessibility**: WCAG 2.1 AA compliance, keyboard navigation, screen reader support
|
||
|
|
3. **Performance**: Server Components, minimal client JavaScript
|
||
|
|
4. **Consistency**: Design tokens, component library, consistent patterns
|
||
|
|
5. **Feedback**: Loading states, error messages, success confirmations
|
||
|
|
|
||
|
|
## Responsive Breakpoints
|
||
|
|
|
||
|
|
```css
|
||
|
|
/* Tailwind CSS default breakpoints */
|
||
|
|
sm: 640px /* Small tablets */
|
||
|
|
md: 768px /* Tablets */
|
||
|
|
lg: 1024px /* Laptops */
|
||
|
|
xl: 1280px /* Desktops */
|
||
|
|
2xl: 1536px /* Large monitors */
|
||
|
|
```
|
||
|
|
|
||
|
|
## Layout Architecture
|
||
|
|
|
||
|
|
### Application Shell
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ DESKTOP LAYOUT │
|
||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────┐ ┌──────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ │ │ HEADER │ │
|
||
|
|
│ │ │ │ Logo Search (optional) User Menu │ │
|
||
|
|
│ │ │ └──────────────────────────────────────────────────────┘ │
|
||
|
|
│ │ │ ┌──────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ SIDEBAR │ │ │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ │ - Dashboard│ │ MAIN CONTENT │ │
|
||
|
|
│ │ - Rounds │ │ │ │
|
||
|
|
│ │ - Projects │ │ │ │
|
||
|
|
│ │ - Jury │ │ │ │
|
||
|
|
│ │ - Reports │ │ │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ └─────────────┘ └──────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
|
|
│ MOBILE LAYOUT │
|
||
|
|
├─────────────────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ ☰ Logo User Avatar │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ MAIN CONTENT │ │
|
||
|
|
│ │ (full width) │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ │ │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ ┌──────────────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ 🏠 📋 📊 👤 │ │
|
||
|
|
│ │ Home Projects Reports Profile │ │
|
||
|
|
│ └──────────────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### Layout Components
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/components/layouts/app-layout.tsx
|
||
|
|
|
||
|
|
import { Sidebar } from './sidebar'
|
||
|
|
import { Header } from './header'
|
||
|
|
import { MobileNav } from './mobile-nav'
|
||
|
|
|
||
|
|
export function AppLayout({ children }: { children: React.ReactNode }) {
|
||
|
|
return (
|
||
|
|
<div className="min-h-screen bg-background">
|
||
|
|
{/* Desktop Sidebar */}
|
||
|
|
<Sidebar className="hidden lg:fixed lg:inset-y-0 lg:flex lg:w-64" />
|
||
|
|
|
||
|
|
{/* Main Content Area */}
|
||
|
|
<div className="lg:pl-64">
|
||
|
|
<Header />
|
||
|
|
<main className="p-4 lg:p-8">{children}</main>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Mobile Bottom Navigation */}
|
||
|
|
<MobileNav className="fixed bottom-0 left-0 right-0 lg:hidden" />
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Component Hierarchy
|
||
|
|
|
||
|
|
```
|
||
|
|
src/components/
|
||
|
|
├── ui/ # shadcn/ui base components
|
||
|
|
│ ├── button.tsx
|
||
|
|
│ ├── input.tsx
|
||
|
|
│ ├── card.tsx
|
||
|
|
│ ├── table.tsx
|
||
|
|
│ ├── dialog.tsx
|
||
|
|
│ ├── dropdown-menu.tsx
|
||
|
|
│ ├── form.tsx
|
||
|
|
│ ├── select.tsx
|
||
|
|
│ ├── textarea.tsx
|
||
|
|
│ ├── badge.tsx
|
||
|
|
│ ├── progress.tsx
|
||
|
|
│ ├── skeleton.tsx
|
||
|
|
│ ├── toast.tsx
|
||
|
|
│ └── ...
|
||
|
|
├── layouts/ # Layout components
|
||
|
|
│ ├── app-layout.tsx
|
||
|
|
│ ├── auth-layout.tsx
|
||
|
|
│ ├── sidebar.tsx
|
||
|
|
│ ├── header.tsx
|
||
|
|
│ └── mobile-nav.tsx
|
||
|
|
├── forms/ # Form components
|
||
|
|
│ ├── evaluation-form.tsx
|
||
|
|
│ ├── project-import-form.tsx
|
||
|
|
│ ├── round-settings-form.tsx
|
||
|
|
│ └── user-invite-form.tsx
|
||
|
|
├── data-display/ # Data display components
|
||
|
|
│ ├── project-card.tsx
|
||
|
|
│ ├── project-list.tsx
|
||
|
|
│ ├── project-table.tsx
|
||
|
|
│ ├── evaluation-summary.tsx
|
||
|
|
│ ├── progress-tracker.tsx
|
||
|
|
│ └── stats-card.tsx
|
||
|
|
└── shared/ # Shared utility components
|
||
|
|
├── file-viewer.tsx
|
||
|
|
├── loading-state.tsx
|
||
|
|
├── error-state.tsx
|
||
|
|
├── empty-state.tsx
|
||
|
|
└── confirm-dialog.tsx
|
||
|
|
```
|
||
|
|
|
||
|
|
## Page Layouts by View
|
||
|
|
|
||
|
|
### Admin Dashboard
|
||
|
|
|
||
|
|
```
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ ADMIN DASHBOARD │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌───────────┐ │
|
||
|
|
│ │ Projects │ │ Evaluations │ │ Jury Active │ │ Time Left │ │
|
||
|
|
│ │ 130 │ │ 234/390 │ │ 12/15 │ │ 5 days │ │
|
||
|
|
│ └─────────────┘ └─────────────┘ └─────────────┘ └───────────┘ │
|
||
|
|
│ │
|
||
|
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ PROGRESS BY PROJECT │ │
|
||
|
|
│ │ ████████████████████████████░░░░░░░░░░░░ 60% Complete │ │
|
||
|
|
│ └────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────┐ ┌────────────────────────────┐ │
|
||
|
|
│ │ JURY PROGRESS │ │ RECENT ACTIVITY │ │
|
||
|
|
│ │ │ │ │ │
|
||
|
|
│ │ Alice ████████░░ 80% │ │ • John submitted eval... │ │
|
||
|
|
│ │ Bob ██████░░░░ 60% │ │ • Sarah started eval... │ │
|
||
|
|
│ │ Carol ████░░░░░░ 40% │ │ • Admin extended window │ │
|
||
|
|
│ │ ... │ │ • ... │ │
|
||
|
|
│ └─────────────────────────────┘ └────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
Mobile: Stats stack vertically, Progress & Activity in tabs
|
||
|
|
```
|
||
|
|
|
||
|
|
### Jury Project List
|
||
|
|
|
||
|
|
```
|
||
|
|
DESKTOP:
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ MY ASSIGNED PROJECTS Filter ▼ Search │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌────────────────────────────────────────────────────────────┐ │
|
||
|
|
│ │ Title │ Team │ Status │ Actions │ │
|
||
|
|
│ ├────────────────────────────────────────────────────────────┤ │
|
||
|
|
│ │ Ocean Cleanup AI │ BlueWave │ ✅ Done │ View │ │
|
||
|
|
│ │ Coral Restoration │ ReefGuard │ 📝 Draft │ Continue │ │
|
||
|
|
│ │ Plastic Tracker │ CleanSeas │ ⏳ Pending│ Start │ │
|
||
|
|
│ │ ... │ │ │ │ │
|
||
|
|
│ └────────────────────────────────────────────────────────────┘ │
|
||
|
|
│ │
|
||
|
|
│ Showing 1-10 of 15 < 1 2 > │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
MOBILE (Card View):
|
||
|
|
┌─────────────────────────────────────┐
|
||
|
|
│ MY PROJECTS (15) 🔍 Filter │
|
||
|
|
├─────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────┐│
|
||
|
|
│ │ Ocean Cleanup AI ││
|
||
|
|
│ │ Team: BlueWave ││
|
||
|
|
│ │ ✅ Completed ││
|
||
|
|
│ │ [View →] ││
|
||
|
|
│ └─────────────────────────────────┘│
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────┐│
|
||
|
|
│ │ Coral Restoration ││
|
||
|
|
│ │ Team: ReefGuard ││
|
||
|
|
│ │ 📝 Draft saved ││
|
||
|
|
│ │ [Continue →] ││
|
||
|
|
│ └─────────────────────────────────┘│
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────┐│
|
||
|
|
│ │ Plastic Tracker ││
|
||
|
|
│ │ Team: CleanSeas ││
|
||
|
|
│ │ ⏳ Not started ││
|
||
|
|
│ │ [Start →] ││
|
||
|
|
│ └─────────────────────────────────┘│
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
### Evaluation Form
|
||
|
|
|
||
|
|
```
|
||
|
|
DESKTOP (Side Panel):
|
||
|
|
┌─────────────────────────────────────────────────────────────────┐
|
||
|
|
│ PROJECT DETAILS │ EVALUATION FORM │
|
||
|
|
├─────────────────────────────────────────────────────────────────┤
|
||
|
|
│ │ │
|
||
|
|
│ Ocean Cleanup AI │ Need Clarity │
|
||
|
|
│ Team: BlueWave Tech │ ○ 1 ○ 2 ○ 3 ● 4 ○ 5 │
|
||
|
|
│ │ │
|
||
|
|
│ [📄 Exec Summary] [📊 Deck] │ Solution Relevance │
|
||
|
|
│ [🎬 Video] │ ○ 1 ○ 2 ● 3 ○ 4 ○ 5 │
|
||
|
|
│ │ │
|
||
|
|
│ Description: │ Gap Analysis │
|
||
|
|
│ Our AI-powered system uses │ ○ 1 ○ 2 ○ 3 ○ 4 ● 5 │
|
||
|
|
│ machine learning to identify │ │
|
||
|
|
│ ocean plastic concentrations... │ Target Customers │
|
||
|
|
│ │ ○ 1 ○ 2 ○ 3 ● 4 ○ 5 │
|
||
|
|
│ Tags: AI, Plastic, Monitoring │ │
|
||
|
|
│ │ Ocean Impact │
|
||
|
|
│ │ ○ 1 ○ 2 ○ 3 ○ 4 ● 5 │
|
||
|
|
│ │ │
|
||
|
|
│ │ Global Score (1-10) │
|
||
|
|
│ │ [ 8 ] │
|
||
|
|
│ │ │
|
||
|
|
│ │ Semi-finalist? │
|
||
|
|
│ │ (●) Yes ( ) No │
|
||
|
|
│ │ │
|
||
|
|
│ │ Feedback │
|
||
|
|
│ │ ┌────────────────────┐ │
|
||
|
|
│ │ │ Strong technical │ │
|
||
|
|
│ │ │ approach with... │ │
|
||
|
|
│ │ └────────────────────┘ │
|
||
|
|
│ │ │
|
||
|
|
│ │ Autosaved 2s ago │
|
||
|
|
│ │ [Submit Evaluation] │
|
||
|
|
│ │ │
|
||
|
|
└─────────────────────────────────────────────────────────────────┘
|
||
|
|
|
||
|
|
MOBILE (Full Screen Wizard):
|
||
|
|
┌─────────────────────────────────────┐
|
||
|
|
│ ← Ocean Cleanup AI Step 3/7 │
|
||
|
|
├─────────────────────────────────────┤
|
||
|
|
│ │
|
||
|
|
│ Gap Analysis │
|
||
|
|
│ │
|
||
|
|
│ How well does the project │
|
||
|
|
│ analyze market gaps? │
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────┐│
|
||
|
|
│ │ ││
|
||
|
|
│ │ 1 2 3 4 5 ││
|
||
|
|
│ │ (○) (○) (○) (○) (●) ││
|
||
|
|
│ │ Poor Excellent ││
|
||
|
|
│ │ ││
|
||
|
|
│ └─────────────────────────────────┘│
|
||
|
|
│ │
|
||
|
|
│ │
|
||
|
|
│ │
|
||
|
|
│ │
|
||
|
|
│ │
|
||
|
|
│ ┌─────────────────────────────────┐│
|
||
|
|
│ │ ○ ○ ● ○ ○ ○ ○ ││
|
||
|
|
│ └─────────────────────────────────┘│
|
||
|
|
│ │
|
||
|
|
│ [← Previous] [Next →] │
|
||
|
|
│ │
|
||
|
|
└─────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
## Design System
|
||
|
|
|
||
|
|
### Color Palette (MOPC Brand)
|
||
|
|
|
||
|
|
```css
|
||
|
|
/* CSS Variables in tailwind.config.ts - MOPC Brand Colors */
|
||
|
|
:root {
|
||
|
|
/* Brand Colors */
|
||
|
|
--color-primary: 354 90% 47%; /* #de0f1e - Primary Red */
|
||
|
|
--color-secondary: 198 85% 18%; /* #053d57 - Dark Blue */
|
||
|
|
--color-accent: 194 25% 44%; /* #557f8c - Teal */
|
||
|
|
|
||
|
|
/* shadcn/ui mapped to MOPC brand */
|
||
|
|
--background: 0 0% 100%; /* #fefefe */
|
||
|
|
--foreground: 198 85% 18%; /* Dark Blue for text */
|
||
|
|
--card: 0 0% 100%;
|
||
|
|
--card-foreground: 198 85% 18%;
|
||
|
|
--popover: 0 0% 100%;
|
||
|
|
--popover-foreground: 198 85% 18%;
|
||
|
|
--primary: 354 90% 47%; /* Primary Red - main actions */
|
||
|
|
--primary-foreground: 0 0% 100%;
|
||
|
|
--secondary: 30 6% 96%; /* Warm gray */
|
||
|
|
--secondary-foreground: 198 85% 18%;
|
||
|
|
--muted: 30 6% 96%;
|
||
|
|
--muted-foreground: 30 8% 45%;
|
||
|
|
--accent: 194 25% 44%; /* Teal */
|
||
|
|
--accent-foreground: 0 0% 100%;
|
||
|
|
--destructive: 0 84.2% 60.2%;
|
||
|
|
--destructive-foreground: 0 0% 100%;
|
||
|
|
--border: 30 6% 91%;
|
||
|
|
--input: 30 6% 91%;
|
||
|
|
--ring: 354 90% 47%; /* Primary Red for focus */
|
||
|
|
--radius: 0.5rem;
|
||
|
|
|
||
|
|
/* Semantic colors */
|
||
|
|
--success: 142.1 76.2% 36.3%;
|
||
|
|
--warning: 38 92% 50%;
|
||
|
|
--info: 194 25% 44%; /* Teal */
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Typography (Montserrat)
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// tailwind.config.ts
|
||
|
|
const config = {
|
||
|
|
theme: {
|
||
|
|
extend: {
|
||
|
|
fontFamily: {
|
||
|
|
sans: ['Montserrat', 'system-ui', 'sans-serif'],
|
||
|
|
mono: ['JetBrains Mono', 'monospace'],
|
||
|
|
},
|
||
|
|
fontWeight: {
|
||
|
|
light: '300',
|
||
|
|
normal: '400',
|
||
|
|
semibold: '600',
|
||
|
|
bold: '700',
|
||
|
|
},
|
||
|
|
fontSize: {
|
||
|
|
'display-lg': ['3rem', { lineHeight: '1.1', fontWeight: '700' }],
|
||
|
|
'display': ['2.25rem', { lineHeight: '1.2', fontWeight: '700' }],
|
||
|
|
'heading': ['1.5rem', { lineHeight: '1.3', fontWeight: '600' }],
|
||
|
|
'subheading': ['1.125rem', { lineHeight: '1.4', fontWeight: '600' }],
|
||
|
|
'body': ['1rem', { lineHeight: '1.5', fontWeight: '400' }],
|
||
|
|
'small': ['0.875rem', { lineHeight: '1.5', fontWeight: '400' }],
|
||
|
|
'tiny': ['0.75rem', { lineHeight: '1.5', fontWeight: '400' }],
|
||
|
|
},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Spacing System
|
||
|
|
|
||
|
|
```
|
||
|
|
Base unit: 4px
|
||
|
|
|
||
|
|
0 = 0px
|
||
|
|
1 = 4px
|
||
|
|
2 = 8px
|
||
|
|
3 = 12px
|
||
|
|
4 = 16px
|
||
|
|
5 = 20px
|
||
|
|
6 = 24px
|
||
|
|
8 = 32px
|
||
|
|
10 = 40px
|
||
|
|
12 = 48px
|
||
|
|
16 = 64px
|
||
|
|
20 = 80px
|
||
|
|
24 = 96px
|
||
|
|
```
|
||
|
|
|
||
|
|
## Component Patterns
|
||
|
|
|
||
|
|
### Loading States
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Always show skeleton while loading
|
||
|
|
function ProjectList() {
|
||
|
|
const { data, isLoading } = trpc.project.list.useQuery({ roundId })
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<div className="space-y-4">
|
||
|
|
{Array.from({ length: 5 }).map((_, i) => (
|
||
|
|
<Skeleton key={i} className="h-20 w-full" />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
return <ProjectTable projects={data.projects} />
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Error States
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Consistent error display
|
||
|
|
function ErrorState({
|
||
|
|
title = 'Something went wrong',
|
||
|
|
message,
|
||
|
|
onRetry,
|
||
|
|
}: {
|
||
|
|
title?: string
|
||
|
|
message: string
|
||
|
|
onRetry?: () => void
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||
|
|
<AlertCircle className="h-12 w-12 text-destructive" />
|
||
|
|
<h3 className="mt-4 text-lg font-semibold">{title}</h3>
|
||
|
|
<p className="mt-2 text-muted-foreground">{message}</p>
|
||
|
|
{onRetry && (
|
||
|
|
<Button onClick={onRetry} className="mt-4">
|
||
|
|
Try Again
|
||
|
|
</Button>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Empty States
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function EmptyState({
|
||
|
|
icon: Icon,
|
||
|
|
title,
|
||
|
|
description,
|
||
|
|
action,
|
||
|
|
}: {
|
||
|
|
icon: React.ComponentType<{ className?: string }>
|
||
|
|
title: string
|
||
|
|
description: string
|
||
|
|
action?: React.ReactNode
|
||
|
|
}) {
|
||
|
|
return (
|
||
|
|
<div className="flex flex-col items-center justify-center p-8 text-center">
|
||
|
|
<Icon className="h-12 w-12 text-muted-foreground" />
|
||
|
|
<h3 className="mt-4 text-lg font-semibold">{title}</h3>
|
||
|
|
<p className="mt-2 text-muted-foreground">{description}</p>
|
||
|
|
{action && <div className="mt-4">{action}</div>}
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Responsive Patterns
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Table on desktop, cards on mobile
|
||
|
|
function ProjectDisplay({ projects }: { projects: Project[] }) {
|
||
|
|
return (
|
||
|
|
<>
|
||
|
|
{/* Desktop: Table */}
|
||
|
|
<div className="hidden md:block">
|
||
|
|
<ProjectTable projects={projects} />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Mobile: Cards */}
|
||
|
|
<div className="md:hidden space-y-4">
|
||
|
|
{projects.map((project) => (
|
||
|
|
<ProjectCard key={project.id} project={project} />
|
||
|
|
))}
|
||
|
|
</div>
|
||
|
|
</>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Touch Targets
|
||
|
|
|
||
|
|
All interactive elements must have a minimum touch target of 44x44px on mobile:
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// Good: Large touch target
|
||
|
|
<Button className="min-h-[44px] min-w-[44px] px-4">
|
||
|
|
Click me
|
||
|
|
</Button>
|
||
|
|
|
||
|
|
// Good: Icon button with padding
|
||
|
|
<Button variant="ghost" size="icon" className="h-11 w-11">
|
||
|
|
<Menu className="h-5 w-5" />
|
||
|
|
</Button>
|
||
|
|
```
|
||
|
|
|
||
|
|
## Form Patterns
|
||
|
|
|
||
|
|
### Autosave with Debounce
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
function EvaluationForm({ evaluation }: { evaluation: Evaluation }) {
|
||
|
|
const utils = trpc.useUtils()
|
||
|
|
|
||
|
|
const autosave = trpc.evaluation.autosave.useMutation({
|
||
|
|
onSuccess: () => {
|
||
|
|
utils.evaluation.get.invalidate({ assignmentId: evaluation.assignmentId })
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
const debouncedSave = useMemo(
|
||
|
|
() => debounce((data: FormData) => autosave.mutate(data), 1000),
|
||
|
|
[autosave]
|
||
|
|
)
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Form
|
||
|
|
onChange={(data) => {
|
||
|
|
debouncedSave(data)
|
||
|
|
}}
|
||
|
|
>
|
||
|
|
{/* Form fields */}
|
||
|
|
<div className="text-sm text-muted-foreground">
|
||
|
|
{autosave.isPending ? 'Saving...' : 'Autosaved'}
|
||
|
|
</div>
|
||
|
|
</Form>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Form Validation
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
const evaluationSchema = z.object({
|
||
|
|
criterionScores: z.record(z.number().min(1).max(5)),
|
||
|
|
globalScore: z.number().min(1).max(10),
|
||
|
|
binaryDecision: z.boolean(),
|
||
|
|
feedbackText: z.string().min(10, 'Please provide at least 10 characters'),
|
||
|
|
})
|
||
|
|
|
||
|
|
function EvaluationForm() {
|
||
|
|
const form = useForm<z.infer<typeof evaluationSchema>>({
|
||
|
|
resolver: zodResolver(evaluationSchema),
|
||
|
|
})
|
||
|
|
|
||
|
|
return (
|
||
|
|
<Form {...form}>
|
||
|
|
<FormField
|
||
|
|
control={form.control}
|
||
|
|
name="feedbackText"
|
||
|
|
render={({ field }) => (
|
||
|
|
<FormItem>
|
||
|
|
<FormLabel>Feedback</FormLabel>
|
||
|
|
<FormControl>
|
||
|
|
<Textarea {...field} />
|
||
|
|
</FormControl>
|
||
|
|
<FormMessage /> {/* Shows validation error */}
|
||
|
|
</FormItem>
|
||
|
|
)}
|
||
|
|
/>
|
||
|
|
</Form>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
## Accessibility Checklist
|
||
|
|
|
||
|
|
- [ ] All images have alt text
|
||
|
|
- [ ] Form inputs have labels
|
||
|
|
- [ ] Color is not the only indicator (icons + text)
|
||
|
|
- [ ] Focus states are visible
|
||
|
|
- [ ] Skip links for main content
|
||
|
|
- [ ] Keyboard navigation works
|
||
|
|
- [ ] Screen reader tested
|
||
|
|
- [ ] Reduced motion respected
|
||
|
|
- [ ] Sufficient color contrast (4.5:1 for text)
|
||
|
|
|
||
|
|
## Animation Patterns
|
||
|
|
|
||
|
|
### Page Transitions (Motion)
|
||
|
|
```typescript
|
||
|
|
const pageVariants = {
|
||
|
|
initial: { opacity: 0, y: 20 },
|
||
|
|
enter: { opacity: 1, y: 0, transition: { duration: 0.3, ease: [0.25, 0.1, 0.25, 1] } },
|
||
|
|
exit: { opacity: 0, y: -10, transition: { duration: 0.2 } }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### List Stagger (Items enter one by one)
|
||
|
|
```typescript
|
||
|
|
const listVariants = {
|
||
|
|
visible: { transition: { staggerChildren: 0.05 } }
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### Spring Physics (Natural movement)
|
||
|
|
```typescript
|
||
|
|
const springConfig = { type: "spring", stiffness: 400, damping: 30 }
|
||
|
|
```
|
||
|
|
|
||
|
|
## Mobile-Specific UX Patterns
|
||
|
|
|
||
|
|
| Pattern | Implementation |
|
||
|
|
|---------|----------------|
|
||
|
|
| Bottom sheets instead of modals | Vaul drawer, thumb-reachable |
|
||
|
|
| Swipe gestures | Motion drag handlers |
|
||
|
|
| Pull-to-refresh | Custom spring animation |
|
||
|
|
| Haptic feedback hints | Visual bounce on limits |
|
||
|
|
| Large touch targets | Min 44x44px, generous spacing |
|
||
|
|
| Thumb-zone navigation | Bottom nav, not hamburger menu |
|
||
|
|
| Native-feeling scrolls | CSS scroll-snap, momentum |
|
||
|
|
|
||
|
|
## Performance Targets
|
||
|
|
|
||
|
|
| Metric | Target | How |
|
||
|
|
|--------|--------|-----|
|
||
|
|
| First Contentful Paint | < 1.5s | SSR, optimized fonts |
|
||
|
|
| Largest Contentful Paint | < 2.5s | Image optimization, lazy loading |
|
||
|
|
| Time to Interactive | < 3.5s | Code splitting, minimal JS |
|
||
|
|
| Cumulative Layout Shift | < 0.1 | Reserved space, skeleton loaders |
|
||
|
|
| Touch response | < 100ms | Optimistic UI, spring animations |
|
||
|
|
| Scroll performance | 60fps | CSS transforms, will-change |
|
||
|
|
|
||
|
|
## Component Design Rules
|
||
|
|
|
||
|
|
### Buttons
|
||
|
|
- Primary: Solid brand red (#de0f1e), 12px radius, subtle shadow
|
||
|
|
- Secondary: Ghost/outline, same radius
|
||
|
|
- Hover: Scale 1.02, slight lift shadow
|
||
|
|
- Active: Scale 0.98, pressed feel
|
||
|
|
- Loading: Spinner replaces text, same width
|
||
|
|
|
||
|
|
### Tables (for data density)
|
||
|
|
- Zebra striping: Subtle, not harsh
|
||
|
|
- Row hover: Slight highlight, not full color change
|
||
|
|
- Sortable headers: Subtle indicator, not loud
|
||
|
|
- Mobile: Horizontal scroll with sticky first column
|
||
|
|
|
||
|
|
### Forms
|
||
|
|
- Labels above inputs (not placeholder-as-label)
|
||
|
|
- Clear focus states (brand color ring)
|
||
|
|
- Inline validation (not modal alerts)
|
||
|
|
- Autosave indicator: Subtle, top-right
|
||
|
|
|
||
|
|
### Empty States
|
||
|
|
- Illustration + helpful text
|
||
|
|
- Clear CTA to fix the empty state
|
||
|
|
- Not just "No data found"
|
||
|
|
|
||
|
|
## Related Documentation
|
||
|
|
|
||
|
|
- [Architecture Overview](./README.md) - System design
|
||
|
|
- [API Design](./api.md) - tRPC endpoints
|