MOPC-App/docs/architecture/ui.md

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