Course navigationPhase 20
Volume 06 · Phase 20

Production Application UI

Build a production application shell and reusable UI states instead of isolated demo components.

16lessons + labs
5klesson words
25source references
What you will ship

Phase outcome and project increment

Learning outcome

Build a production application shell and reusable UI states instead of isolated demo components.

Concrete deliverable

TaskFlow responsive shell with navigation, workspace context, task screens, loading/empty/error states, and accessible interactions.

Figure 20.1 — Where this phase lives in TaskFlow
route/layout
server data boundary
page composition
client interaction islands
responsive/accessibility states
Trace the flow left-to-right for implementation and right-to-left for debugging. At every arrow, ask what data crosses the boundary and what proves it is trustworthy.
Instructor mental model

Principles to keep while coding

01

Build production UI from reusable layout and interaction patterns, not a collection of one-off pages.

02

Represent loading, empty, error, success, and permission states explicitly.

03

Accessibility and responsive behavior are requirements of the component, not a final polishing step.

04

Keep UI composition separate from server-state ownership so components remain understandable.

Code-along path

Guided phase lab

Do these in order. Each step should end with evidence before the next one starts.

  1. 01

    Design the application shell first: sidebar/topbar/main content/mobile navigation.

  2. 02

    Create reusable primitives for page header, panel/card, empty state, loading skeleton, and error state.

  3. 03

    Build workspace/board/task navigation with active state derived from routing rather than duplicated local state.

  4. 04

    Compose Server and Client Components so interactive islands remain small.

  5. 05

    Test keyboard focus, labels, landmarks, reduced-motion expectations, and narrow widths.

  6. 06

    Exercise all non-happy states by forcing no data, slow data, error, and forbidden responses.

Debugging studio

Failures you should know how to diagnose

Failure drill 1

Only styling the success state

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 2

Global client component wrapping the whole app

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 3

Clickable divs without keyboard semantics

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Failure drill 4

Mobile layout treated as desktop squeezed smaller

Predict the symptom, capture evidence at the nearest boundary, change one variable, and verify the same reproduction after the fix.

Watch-order style

Lessons

The source chapters come first. Every phase now ends with a guided build, a debugging workshop, and an independent challenge so reading turns into transferable skill.

01

Production UI Architecture and Composition

Production UI Architecture & Composition Define responsibilities for layouts, pages, features and interactive islands Phase 20 - Production Application UI Volume 06 - Professional Frontend A

2 min
02

Protected Route Groups and App Layout

Protected Route Groups & App Layout Organize authenticated application routes without changing public URLs Phase 20 - Production Application UI Volume 06 - Professional Frontend Architecture

2 min
03

App Shell Navigation and Workspace Context

App Shell, Navigation & Workspace Context Create persistent orientation with semantic landmarks and accessible navigation Phase 20 - Production Application UI Volume 06 - Professional Fronte

2 min
04

Task Table and Domain Presentation

Task Table & Domain Presentation Present operational data with semantic, responsive and scannable UI Phase 20 - Production Application UI Volume 06 - Professional Frontend Architecture miles

2 min
05

Visual System CSS Modules and Design Tokens

Visual System, CSS Modules & Design Tokens Build consistent styling without premature design-system dependencies Phase 20 - Production Application UI Volume 06 - Professional Frontend Archit

2 min
06

Loading Empty Error and NotFound States

Loading, Empty, Error & Not-Found States Design explicit states for every route outcome Phase 20 - Production Application UI Volume 06 - Professional Frontend Architecture milestone Request

2 min
07

Accessibility Keyboard and Focus Architecture

Accessibility, Keyboard & Focus Architecture Build WCAG 2.2 thinking into component behavior and semantics Phase 20 - Production Application UI Volume 06 - Professional Frontend Architecture

2 min
08

Responsive and Adaptive UI Design

Responsive & Adaptive UI Design Adapt the application shell and domain surfaces across real viewport pressure points Phase 20 - Production Application UI Volume 06 - Professional Frontend Ar

2 min
09

Server and Client Component Composition

Server & Client Component Composition Keep Server Components as the default and hydrate only interaction islands Phase 20 - Production Application UI Volume 06 - Professional Frontend Archit

2 min
10

Permission Aware UI without Client Side Trust

Permission-Aware UI Without Client-Side Trust Improve UX with capabilities while keeping authorization authoritative on the API Phase 20 - Production Application UI Volume 06 - Professional

2 min
11

Build the TaskFlow Production UI

Build the TaskFlow Production UI Assemble the protected shell, real Task data and polished route states Phase 20 - Production Application UI Volume 06 - Professional Frontend Architecture mi

2 min
12

Hands On Lab and Phase Exit

Hands-On Lab & Phase Exit Verify the product surface across keyboard, responsive, error and authorization scenarios Phase 20 - Production Application UI Volume 06 - Professional Frontend Arc

2 min
LAB

Guided Build Lab

Purpose Build a production application shell and reusable UI states instead of isolated demo components.

3 min
LAB

Debugging Workshop

Goal Learn to diagnose this phase's failures systematically instead of reinstalling packages, changing random configuration, or copying a fix without understanding it.

2 min
LAB

Phase Synthesis & Independent Challenge

Mission Rebuild the capability of Phase 20 without following the exact code line-by-line. The goal is transfer: can you use the ideas when the variable names, UI, or route shape changes?

2 min
LAB

Mastery Capstone Lab

Integrate Phase 20 without following a transcript.

2 min
Practice ladder

Exercises that increase independence

Guided

Design the application shell first: sidebar/topbar/main content/mobile navigation.

Practice

Build workspace/board/task navigation with active state derived from routing rather than duplicated local state.

Debug

reproduce and diagnose — Only styling the success state.

Independent

deliver TaskFlow responsive shell with navigation, workspace context, task screens, loading/empty/error states, and accessible interactions.

Explain-back

Can compose a shell from reusable patterns and Can model all UI states.

Mastery gate

Do not leave the phase only because you reached the bottom

Phase exit criteria

Knowledge check

Scenario and concept quiz

These questions test engineering choices rather than whether you remember a lesson title.

0/0 correct · 0/8 answered
01Which practice best matches Phase 20's engineering goal?
02Which practice best matches Phase 20's engineering goal?
03Which practice best matches Phase 20's engineering goal?
04Which practice best matches Phase 20's engineering goal?
05You are working through the Phase 20 lab. Which next action is the most disciplined step?
06You are working through the Phase 20 lab. Which next action is the most disciplined step?
07You are working through the Phase 20 lab. Which next action is the most disciplined step?
08You are working through the Phase 20 lab. Which next action is the most disciplined step?
Source snapshot

Reference files

Attempt the work first. Use snapshots to compare a checkpoint, not as a substitute for understanding.

reference/TaskFlow_Production_UI_Snapshot/ACCESSIBILITY_CHECKLIST.md
# Phase 20 accessibility checklist

- Semantic landmarks: header, nav, main, aside where appropriate.
- One meaningful page h1.
- Skip link reaches main content.
- All interactive controls work by keyboard.
- Focus indicator remains visible and not obscured.
- Target size aims for at least 44px for primary controls and never relies on tiny adjacent click targets.
- Tables use table/th/scope semantics when data is truly tabular.
- Horizontal-scroll table region is keyboard focusable and labeled.
- Color never carries task state alone; text labels accompany status colors.
- Empty, loading, error, and not-found states provide text, not only visuals.
- Reduced-motion preferences should be respected if animation is added later.
reference/TaskFlow_Production_UI_Snapshot/RESPONSIVE_TEST_MATRIX.md
# Responsive verification matrix

Test at minimum:
- 320px narrow mobile
- 390px common mobile
- 768px tablet portrait
- 900px shell breakpoint
- 1024px laptop/tablet landscape
- 1366px common desktop
- 1920px wide desktop

Verify:
- no horizontal page overflow,
- task table uses contained horizontal scroll when needed,
- focus is not hidden under sticky UI,
- action buttons remain reachable,
- navigation remains understandable,
- text remains readable at 200% zoom,
- touch targets are not crowded.
reference/TaskFlow_Production_UI_Snapshot/SOURCE_NOTES.txt
Official references used for Phase 20 verification (September 18, 2026):

Next.js - Server and Client Components
https://nextjs.org/docs/app/getting-started/server-and-client-components

Next.js - App Router / Project Structure / File Conventions
https://nextjs.org/docs/app
https://nextjs.org/docs/app/getting-started/project-structure
https://nextjs.org/docs/app/api-reference/file-conventions

Next.js - error.js
https://nextjs.org/docs/app/api-reference/file-conventions/error

Next.js - not-found.js
https://nextjs.org/docs/app/api-reference/file-conventions/not-found

Next.js - CSS styling
https://nextjs.org/learn/dashboard-app/css-styling

WCAG 2.2
reference/TaskFlow_Production_UI_Snapshot/UI_ARCHITECTURE.md
# Phase 20 UI architecture

The production UI uses the App Router with a protected `(app)` route group.

Server Components own:
- authenticated layout checks,
- initial page data fetching,
- metadata and route composition,
- permission-derived server rendering.

Client Components are kept as islands for:
- mobile menu toggle,
- local disclosure controls,
- browser-only interactions.

The UI does not become the security boundary. Hidden controls improve experience, but NestJS Phase 19 authorization remains authoritative.
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/(app)/layout.tsx
import type { ReactNode } from 'react'
import { redirect } from 'next/navigation'
import { createClient } from '@/lib/supabase/server'
import { AppShell } from '@/components/app-shell/app-shell'

export default async function AppLayout({ children }: { children: ReactNode }) {
  const supabase = await createClient()
  const { data, error } = await supabase.auth.getClaims()

  if (error || !data?.claims?.sub) redirect('/signin')

  return (
    <>
      <a className="skipLink" href="#main-content">Skip to main content</a>
      <AppShell
        user={{
          id: data.claims.sub,
          email: typeof data.claims.email === 'string' ? data.claims.email : undefined,
        }}
      >
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/(app)/tasks/error.tsx
'use client'

export default function TasksError({ reset }: { reset: () => void }) {
  return (
    <section role="alert">
      <h2>We could not load tasks</h2>
      <p>The request failed. Try again, or contact support if the problem continues.</p>
      <button type="button" onClick={() => reset()}>Try again</button>
    </section>
  )
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/(app)/tasks/loading.tsx
export default function LoadingTasks() {
  return (
    <section aria-busy="true" aria-live="polite">
      <p>Loading tasks...</p>
    </section>
  )
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/(app)/tasks/not-found.tsx
import Link from 'next/link'

export default function TaskNotFound() {
  return (
    <section>
      <h1>Task not found</h1>
      <p>The task may not exist or may not be available in your current workspace scope.</p>
      <Link href="/tasks">Back to tasks</Link>
    </section>
  )
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/(app)/tasks/page.tsx
import { getAuthenticatedAccessToken } from '@/lib/auth/get-authenticated-access-token'
import { fetchTasks } from '@/lib/api/tasks'
import { TaskTable } from '@/components/tasks/task-table'
import { EmptyState } from '@/components/ui/empty-state'
import styles from './tasks.module.css'

export default async function TasksPage({
  searchParams,
}: {
  searchParams: Promise<Record<string, string | string[] | undefined>>
}) {
  const params = await searchParams
  const token = await getAuthenticatedAccessToken()
  const result = await fetchTasks({ params, token })

  return (
    <section aria-labelledby="tasks-heading">
      <div className={styles.pageHeader}>
        <div>
          <p className={styles.eyebrow}>Work management</p>
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/(app)/tasks/tasks.module.css
.pageHeader {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  gap: 20px;
  margin-bottom: 20px;
}
.pageHeader h1 { margin: 0; font-size: clamp(28px, 4vw, 40px); line-height: 1.1; }
.eyebrow { margin: 0 0 4px; color: var(--brand); font-weight: 700; }
.description { margin: 8px 0 0; color: var(--text-secondary); }
.primaryAction {
  display: inline-flex;
  min-height: 44px;
  align-items: center;
  padding: 0 16px;
  background: var(--brand);
  color: white;
  border-radius: 10px;
  text-decoration: none;
  font-weight: 700;
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/app/globals.css
:root {
  --surface-canvas: #f5f7fb;
  --surface-panel: #ffffff;
  --surface-muted: #eef2f7;
  --text-primary: #172033;
  --text-secondary: #627086;
  --border-subtle: #dbe2ea;
  --brand: #166b72;
  --brand-strong: #0e5359;
  --danger: #b42318;
  --warning: #a15c00;
  --success: #137333;

  --radius-sm: 8px;
  --radius-md: 12px;
  --radius-lg: 16px;

  --space-1: 4px;
  --space-2: 8px;
  --space-3: 12px;
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/app-shell/app-header.module.css
.header {
  min-height: 64px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 0 24px;
  background: var(--surface-panel);
  border-bottom: 1px solid var(--border-subtle);
}
.muted { margin-left: 8px; color: var(--text-secondary); }
.header a { color: var(--brand-strong); }
@media (max-width: 900px) { .header { padding: 0 16px; } }
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/app-shell/app-header.tsx
import Link from 'next/link'
import styles from './app-header.module.css'

export function AppHeader({ user }: { user: { email?: string } }) {
  return (
    <header className={styles.header}>
      <div>
        <strong>Workspace</strong>
        <span className={styles.muted}>Acme Operations</span>
      </div>
      <nav aria-label="Account navigation">
        <Link href="/account">{user.email ?? 'Account'}</Link>
      </nav>
    </header>
  )
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/app-shell/app-shell.module.css
.shell {
  min-height: 100dvh;
  display: grid;
  grid-template-columns: 260px minmax(0, 1fr);
}
.contentColumn { min-width: 0; }
.main {
  width: min(1440px, 100%);
  margin: 0 auto;
  padding: 24px;
}

@media (max-width: 900px) {
  .shell { grid-template-columns: 1fr; }
  .main { padding: 16px; }
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/app-shell/app-shell.tsx
import type { ReactNode } from 'react'
import { Sidebar } from './sidebar'
import { AppHeader } from './app-header'
import styles from './app-shell.module.css'

type AppShellProps = {
  user: { id: string; email?: string }
  children: ReactNode
}

export function AppShell({ user, children }: AppShellProps) {
  return (
    <div className={styles.shell}>
      <Sidebar />
      <div className={styles.contentColumn}>
        <AppHeader user={user} />
        <main id="main-content" className={styles.main} tabIndex={-1}>
          {children}
        </main>
      </div>
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/app-shell/sidebar.module.css
.sidebar {
  position: sticky;
  top: 0;
  height: 100dvh;
  border-right: 1px solid var(--border-subtle);
  background: var(--surface-panel);
  padding: 20px 16px;
}
.brand { font-weight: 800; font-size: 20px; margin-bottom: 24px; }
.list { list-style: none; margin: 0; padding: 0; display: grid; gap: 6px; }
.list a {
  display: block;
  min-height: 44px;
  padding: 10px 12px;
  border-radius: 10px;
  color: var(--text-primary);
  text-decoration: none;
}
.list a:hover { background: var(--surface-muted); }
@media (max-width: 900px) { .sidebar { display: none; } }
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/app-shell/sidebar.tsx
import Link from 'next/link'
import styles from './sidebar.module.css'

const nav = [
  { href: '/dashboard', label: 'Dashboard' },
  { href: '/projects', label: 'Projects' },
  { href: '/tasks', label: 'Tasks' },
]

export function Sidebar() {
  return (
    <aside className={styles.sidebar} aria-label="Primary navigation">
      <div className={styles.brand}>TaskFlow</div>
      <nav>
        <ul className={styles.list}>
          {nav.map((item) => (
            <li key={item.href}>
              <Link href={item.href}>{item.label}</Link>
            </li>
          ))}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/mobile/mobile-nav.module.css
.wrapper { display: none; }
@media (max-width: 900px) {
  .wrapper { display: block; }
  .wrapper button { min-height: 44px; min-width: 44px; }
  .wrapper nav { display: grid; gap: 8px; padding: 12px 0; }
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/mobile/mobile-nav.tsx
'use client'

import { useState } from 'react'
import Link from 'next/link'
import styles from './mobile-nav.module.css'

export function MobileNav() {
  const [open, setOpen] = useState(false)

  return (
    <div className={styles.wrapper}>
      <button
        type="button"
        aria-expanded={open}
        aria-controls="mobile-navigation"
        onClick={() => setOpen((value) => !value)}
      >
        Menu
      </button>
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/tasks/status-badge.module.css
.badge { display: inline-flex; align-items: center; min-height: 28px; padding: 4px 9px; border-radius: 999px; font-size: 12px; font-weight: 700; }
.TODO { background: #eef2f7; color: #344054; }
.IN_PROGRESS { background: #e8f3ff; color: #175cd3; }
.DONE { background: #e9f7ee; color: #137333; }
.CANCELED { background: #fcecec; color: #b42318; }
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/tasks/status-badge.tsx
import styles from './status-badge.module.css'

const label = {
  TODO: 'To do',
  IN_PROGRESS: 'In progress',
  DONE: 'Done',
  CANCELED: 'Canceled',
} as const

export function StatusBadge({ status }: { status: keyof typeof label }) {
  return <span className={`${styles.badge} ${styles[status]}`}>{label[status]}</span>
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/tasks/task-table.module.css
.tableRegion {
  overflow-x: auto;
  border: 1px solid var(--border-subtle);
  border-radius: var(--radius-md);
  background: var(--surface-panel);
  box-shadow: var(--shadow-panel);
}
.table { width: 100%; border-collapse: collapse; min-width: 780px; }
.table th, .table td { padding: 14px 16px; border-bottom: 1px solid var(--border-subtle); text-align: left; }
.table thead th { color: var(--text-secondary); font-size: 13px; }
.table tbody th { font-weight: 650; }
.table tbody tr:hover { background: #fafbfd; }
.table a { color: var(--brand-strong); text-decoration-thickness: 1px; text-underline-offset: 3px; }
.srOnly {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0;
}
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/tasks/task-table.tsx
import Link from 'next/link'
import { StatusBadge } from './status-badge'
import styles from './task-table.module.css'

type TaskRow = {
  id: string
  title: string
  status: 'TODO' | 'IN_PROGRESS' | 'DONE' | 'CANCELED'
  priority: 'LOW' | 'MEDIUM' | 'HIGH'
  assignee?: { displayName?: string | null; email: string } | null
  dueAt?: string | null
}

export function TaskTable({ tasks }: { tasks: TaskRow[] }) {
  return (
    <div className={styles.tableRegion} tabIndex={0} aria-label="Tasks table, scroll horizontally on small screens">
      <table className={styles.table}>
        <caption className={styles.srOnly}>Tasks visible in your current authorized workspace scope</caption>
        <thead>
          <tr>
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/ui/empty-state.module.css
.empty {
  padding: 48px 24px;
  text-align: center;
  background: var(--surface-panel);
  border: 1px dashed var(--border-subtle);
  border-radius: var(--radius-md);
}
.empty h2 { margin: 0 0 8px; font-size: 20px; }
.empty p { margin: 0; color: var(--text-secondary); }
reference/TaskFlow_Production_UI_Snapshot/apps/web/src/components/ui/empty-state.tsx
import styles from './empty-state.module.css'

export function EmptyState({ title, description }: { title: string; description: string }) {
  return (
    <section className={styles.empty} aria-labelledby="empty-state-title">
      <h2 id="empty-state-title">{title}</h2>
      <p>{description}</p>
    </section>
  )
}