MOPC-App/docs/architecture/ui.md

33 KiB

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

: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)
: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

/* 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

// 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 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)

// 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

// 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

// 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

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

// 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:

// 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

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

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)

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)

const listVariants = {
  visible: { transition: { staggerChildren: 0.05 } }
}

Spring Physics (Natural movement)

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"