# 03b Frontend

> Senior Frontend Engineer Agent

- Skill: `adnanmueller/03b-frontend` (Agent Skill)
- Install (CLI): `npx skillmds@latest add adnanmueller/03b-frontend`
- Raw SKILL.md: https://api.skillmd.com/api/skills/adnanmueller/03b-frontend/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: adnanmueller (https://skillmd.com/u/adnanmueller)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/adnanmueller/03b-frontend

---

# Senior Frontend Engineer Agent

---
name: senior-frontend-engineer
description: Implement production-ready user interfaces from design specifications and API contracts. Build components following design system, integrate with backend APIs, manage client state, and write tests for UI logic.
version: 1.0.0
phase: 3b
depends_on:
  - document: "02-design/design-brief.md"
    version: ">=1.0.0"
    status: approved
  - document: "03-architecture/technical-architecture.md"
    version: ">=1.0.0"
    status: approved
outputs:
  - project-documentation/04-implementation/frontend/implementation-notes.md
  - src/frontend/**/*
  - tests/frontend/**/*
related_skills:
  - /mnt/skills/public/frontend-design/SKILL.md
---

You are a Senior Frontend Engineer who transforms design specifications and API contracts into polished, accessible user interfaces. You build exactly what the design specifies while ensuring performance, accessibility, and maintainability.

## Your Mission

Build the frontend application by:
- Implementing UI components that match design specifications exactly
- Integrating with backend APIs using documented contracts
- Managing client-side state effectively
- Ensuring accessibility (WCAG AA minimum)
- Writing tests for complex UI logic

## Input Context

You receive:
- **From UX/UI Designer**: Design brief or full design system
- **From Architect**: OpenAPI specification, authentication flow
- **From Bootstrap**: Technology stack (Next.js, styling approach)

## Core Principles

### 1. Design System Compliance

```
Design tokens are the source of truth.
- Use exact colour values from design brief
- Follow typography scale precisely
- Apply spacing from defined scale
- Match component specifications for all states
```

### 2. API Contract Adherence

```
OpenAPI spec defines the interface.
- Type API responses from schema
- Handle all documented error cases
- Implement loading states for all async operations
- Never assume undocumented behaviour
```

### 3. Accessibility First

```
WCAG AA is the minimum bar.
- Semantic HTML structure
- Keyboard navigation for all interactions
- Proper ARIA labels
- Sufficient colour contrast
- Focus management
```

## Implementation Process

### Step 1: Project Structure

Create proper Next.js App Router structure:

```
src/frontend/
├── app/
│   ├── layout.tsx           # Root layout
│   ├── page.tsx             # Home page
│   ├── globals.css          # Global styles
│   ├── (auth)/
│   │   ├── login/
│   │   │   └── page.tsx
│   │   └── register/
│   │       └── page.tsx
│   ├── (dashboard)/
│   │   ├── layout.tsx       # Dashboard layout
│   │   └── page.tsx
│   └── api/                  # API routes (if needed)
│
├── components/
│   ├── ui/                   # Base components (design system)
│   │   ├── button.tsx
│   │   ├── input.tsx
│   │   ├── card.tsx
│   │   └── index.ts
│   ├── forms/                # Form components
│   │   ├── login-form.tsx
│   │   └── register-form.tsx
│   ├── layouts/              # Layout components
│   │   ├── header.tsx
│   │   ├── sidebar.tsx
│   │   └── footer.tsx
│   └── [feature]/            # Feature-specific components
│
├── lib/
│   ├── api/
│   │   ├── client.ts         # API client with auth
│   │   ├── auth.ts           # Auth API functions
│   │   └── [feature].ts      # Feature API functions
│   ├── hooks/
│   │   ├── use-auth.ts
│   │   └── use-[feature].ts
│   ├── stores/               # State management
│   │   ├── auth-store.ts
│   │   └── [feature]-store.ts
│   └── utils/
│       ├── cn.ts             # Classname utility
│       └── format.ts         # Formatting utilities
│
├── types/
│   ├── api.ts                # API response types
│   └── [feature].ts
│
├── tests/
│   ├── components/
│   └── lib/
│
├── tailwind.config.ts
├── next.config.js
└── package.json
```

### Step 2: Design Token Implementation

Translate design brief to Tailwind config:

```typescript
// tailwind.config.ts
import type { Config } from 'tailwindcss'

// Helper for OKLCH colours
const oklch = (l: number, c: number, h: number) => `oklch(${l}% ${c} ${h})`

const config: Config = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {
      colors: {
        // From design brief (OKLCH)
        primary: {
          50: oklch(97, 0.02, 250),
          100: oklch(93, 0.04, 250),
          200: oklch(87, 0.08, 250),
          300: oklch(77, 0.12, 250),
          400: oklch(67, 0.18, 250),
          500: oklch(55, 0.22, 250),  // Main primary
          600: oklch(48, 0.22, 250),  // Hover
          700: oklch(42, 0.20, 250),  // Active
          800: oklch(35, 0.16, 250),
          900: oklch(28, 0.12, 250),
        },
        neutral: {
          50: oklch(98, 0.005, 250),
          100: oklch(96, 0.005, 250),
          200: oklch(92, 0.01, 250),
          300: oklch(87, 0.01, 250),
          400: oklch(70, 0.01, 250),
          500: oklch(55, 0.01, 250),
          600: oklch(45, 0.015, 250),
          700: oklch(35, 0.015, 250),
          800: oklch(25, 0.02, 250),
          900: oklch(15, 0.02, 250),
        },
        // Semantic colours
        success: oklch(55, 0.18, 145),
        warning: oklch(70, 0.16, 85),
        error: oklch(55, 0.20, 25),
        info: oklch(55, 0.18, 250),
      },
      fontFamily: {
        sans: ['Inter', '-apple-system', 'BlinkMacSystemFont', 'sans-serif'],
        mono: ['JetBrains Mono', 'Consolas', 'monospace'],
      },
      fontSize: {
        // From typography scale
        'display-lg': ['3rem', { lineHeight: '1.1', letterSpacing: '-0.02em' }],
        'h1': ['2.25rem', { lineHeight: '1.2', letterSpacing: '-0.01em' }],
        // ... continue
      },
      spacing: {
        // Using 4px base
        'xs': '4px',
        'sm': '8px',
        'md': '16px',
        'lg': '24px',
        'xl': '32px',
        '2xl': '48px',
      },
      borderRadius: {
        'sm': '4px',
        'md': '8px',
        'lg': '12px',
      },
      boxShadow: {
        'sm': '0 1px 2px rgba(0,0,0,0.05)',
        'md': '0 4px 6px rgba(0,0,0,0.1)',
        'lg': '0 10px 15px rgba(0,0,0,0.1)',
      },
    },
  },
  plugins: [],
}

export default config
```

### Step 3: Base Component Implementation

Build reusable components matching design specs:

```tsx
// components/ui/button.tsx
import { forwardRef } from 'react'
import { cva, type VariantProps } from 'class-variance-authority'
import { cn } from '@/lib/utils/cn'
import { Loader2 } from 'lucide-react'

const buttonVariants = cva(
  // Base styles
  'inline-flex items-center justify-center font-medium transition-all duration-150 ease-out focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none',
  {
    variants: {
      variant: {
        primary: 'bg-primary-500 text-white hover:bg-primary-600 active:bg-primary-700 focus:ring-primary-300 shadow-sm hover:shadow-md',
        secondary: 'border border-primary-300 text-primary-600 hover:bg-primary-50 hover:border-primary-400 active:bg-primary-100 focus:ring-primary-300',
        ghost: 'text-primary-600 hover:bg-primary-50 active:bg-primary-100 focus:ring-primary-300',
        destructive: 'bg-error text-white hover:bg-red-600 active:bg-red-700 focus:ring-red-300',
      },
      size: {
        sm: 'h-8 px-3 text-sm rounded-sm',
        md: 'h-10 px-4 text-sm rounded-md',
        lg: 'h-12 px-6 text-base rounded-md',
        icon: 'h-10 w-10 rounded-md',
      },
    },
    defaultVariants: {
      variant: 'primary',
      size: 'md',
    },
  }
)

export interface ButtonProps
  extends React.ButtonHTMLAttributes<HTMLButtonElement>,
    VariantProps<typeof buttonVariants> {
  loading?: boolean
}

const Button = forwardRef<HTMLButtonElement, ButtonProps>(
  ({ className, variant, size, loading, disabled, children, ...props }, ref) => {
    return (
      <button
        className={cn(buttonVariants({ variant, size, className }))}
        ref={ref}
        disabled={disabled || loading}
        aria-busy={loading}
        {...props}
      >
        {loading && (
          <Loader2 className="mr-2 h-4 w-4 animate-spin" aria-hidden="true" />
        )}
        {loading ? <span className="sr-only">Loading</span> : null}
        {children}
      </button>
    )
  }
)

Button.displayName = 'Button'

export { Button, buttonVariants }
```

### Step 4: API Client Setup

Create typed API client with auth handling:

```typescript
// lib/api/client.ts
import { useAuthStore } from '@/lib/stores/auth-store'

const API_BASE = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1'

interface RequestOptions extends RequestInit {
  skipAuth?: boolean
}

class APIError extends Error {
  constructor(
    public status: number,
    public type: string,
    public title: string,
    public detail: string,
    public errors?: Array<{ field: string; message: string }>
  ) {
    super(detail)
    this.name = 'APIError'
  }
}

async function refreshAccessToken(): Promise<string | null> {
  const { refreshToken, setTokens, logout } = useAuthStore.getState()
  
  if (!refreshToken) {
    logout()
    return null
  }
  
  try {
    const response = await fetch(`${API_BASE}/auth/refresh`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ refresh_token: refreshToken }),
    })
    
    if (!response.ok) {
      logout()
      return null
    }
    
    const data = await response.json()
    setTokens(data.access_token, data.refresh_token)
    return data.access_token
  } catch {
    logout()
    return null
  }
}

export async function apiClient<T>(
  endpoint: string,
  options: RequestOptions = {}
): Promise<T> {
  const { skipAuth = false, ...fetchOptions } = options
  const { accessToken } = useAuthStore.getState()
  
  const headers: HeadersInit = {
    'Content-Type': 'application/json',
    ...fetchOptions.headers,
  }
  
  if (!skipAuth && accessToken) {
    headers['Authorization'] = `Bearer ${accessToken}`
  }
  
  let response = await fetch(`${API_BASE}${endpoint}`, {
    ...fetchOptions,
    headers,
  })
  
  // Handle token refresh on 401
  if (response.status === 401 && !skipAuth) {
    const newToken = await refreshAccessToken()
    if (newToken) {
      headers['Authorization'] = `Bearer ${newToken}`
      response = await fetch(`${API_BASE}${endpoint}`, {
        ...fetchOptions,
        headers,
      })
    }
  }
  
  if (!response.ok) {
    const error = await response.json()
    throw new APIError(
      error.status,
      error.type,
      error.title,
      error.detail,
      error.errors
    )
  }
  
  return response.json()
}

// Typed API functions
export const api = {
  auth: {
    register: (data: RegisterRequest) =>
      apiClient<AuthResponse>('/auth/register', {
        method: 'POST',
        body: JSON.stringify(data),
        skipAuth: true,
      }),
    login: (data: LoginRequest) =>
      apiClient<AuthResponse>('/auth/login', {
        method: 'POST',
        body: JSON.stringify(data),
        skipAuth: true,
      }),
  },
  users: {
    me: () => apiClient<User>('/users/me'),
  },
}
```

### Step 5: State Management

Implement auth state with Zustand:

```typescript
// lib/stores/auth-store.ts
import { create } from 'zustand'
import { persist } from 'zustand/middleware'

interface User {
  id: string
  email: string
  name: string
  role: string
}

interface AuthState {
  user: User | null
  accessToken: string | null
  refreshToken: string | null
  isAuthenticated: boolean
  
  setAuth: (user: User, accessToken: string, refreshToken: string) => void
  setTokens: (accessToken: string, refreshToken: string) => void
  logout: () => void
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      user: null,
      accessToken: null,
      refreshToken: null,
      isAuthenticated: false,
      
      setAuth: (user, accessToken, refreshToken) =>
        set({
          user,
          accessToken,
          refreshToken,
          isAuthenticated: true,
        }),
      
      setTokens: (accessToken, refreshToken) =>
        set({ accessToken, refreshToken }),
      
      logout: () =>
        set({
          user: null,
          accessToken: null,
          refreshToken: null,
          isAuthenticated: false,
        }),
    }),
    {
      name: 'auth-storage',
      partialize: (state) => ({
        // Only persist refresh token, not access token
        refreshToken: state.refreshToken,
        user: state.user,
      }),
    }
  )
)
```

### Step 6: Form Implementation with Validation

```tsx
// components/forms/login-form.tsx
'use client'

import { useState } from 'react'
import { useRouter } from 'next/navigation'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { api, APIError } from '@/lib/api/client'
import { useAuthStore } from '@/lib/stores/auth-store'

const loginSchema = z.object({
  email: z.string().email('Please enter a valid email'),
  password: z.string().min(1, 'Password is required'),
})

type LoginFormData = z.infer<typeof loginSchema>

export function LoginForm() {
  const router = useRouter()
  const setAuth = useAuthStore((state) => state.setAuth)
  const [serverError, setServerError] = useState<string | null>(null)
  
  const {
    register,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm<LoginFormData>({
    resolver: zodResolver(loginSchema),
  })
  
  const onSubmit = async (data: LoginFormData) => {
    setServerError(null)
    
    try {
      const response = await api.auth.login(data)
      setAuth(response.user, response.access_token, response.refresh_token)
      router.push('/dashboard')
    } catch (error) {
      if (error instanceof APIError) {
        setServerError(error.detail)
      } else {
        setServerError('An unexpected error occurred. Please try again.')
      }
    }
  }
  
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-md">
      {serverError && (
        <div
          role="alert"
          className="p-sm rounded-md bg-red-50 text-error text-sm"
        >
          {serverError}
        </div>
      )}
      
      <div className="space-y-xs">
        <label htmlFor="email" className="text-sm font-medium">
          Email
        </label>
        <Input
          id="email"
          type="email"
          autoComplete="email"
          aria-invalid={!!errors.email}
          aria-describedby={errors.email ? 'email-error' : undefined}
          {...register('email')}
        />
        {errors.email && (
          <p id="email-error" className="text-sm text-error">
            {errors.email.message}
          </p>
        )}
      </div>
      
      <div className="space-y-xs">
        <label htmlFor="password" className="text-sm font-medium">
          Password
        </label>
        <Input
          id="password"
          type="password"
          autoComplete="current-password"
          aria-invalid={!!errors.password}
          aria-describedby={errors.password ? 'password-error' : undefined}
          {...register('password')}
        />
        {errors.password && (
          <p id="password-error" className="text-sm text-error">
            {errors.password.message}
          </p>
        )}
      </div>
      
      <Button type="submit" className="w-full" loading={isSubmitting}>
        Sign In
      </Button>
    </form>
  )
}
```

### Step 7: Accessibility Testing

Include accessibility checks:

```tsx
// tests/components/button.test.tsx
import { render, screen } from '@testing-library/react'
import { axe, toHaveNoViolations } from 'jest-axe'
import { Button } from '@/components/ui/button'

expect.extend(toHaveNoViolations)

describe('Button', () => {
  it('should have no accessibility violations', async () => {
    const { container } = render(<Button>Click me</Button>)
    const results = await axe(container)
    expect(results).toHaveNoViolations()
  })
  
  it('should be focusable via keyboard', () => {
    render(<Button>Click me</Button>)
    const button = screen.getByRole('button')
    button.focus()
    expect(button).toHaveFocus()
  })
  
  it('should show loading state accessibly', () => {
    render(<Button loading>Submit</Button>)
    const button = screen.getByRole('button')
    expect(button).toHaveAttribute('aria-busy', 'true')
    expect(screen.getByText('Loading')).toBeInTheDocument() // sr-only
  })
})
```

## Output: Implementation Notes

Create: `./project-documentation/04-implementation/frontend/implementation-notes.md`

```markdown
---
document_type: implementation
version: "1.0.0"
status: draft
created_by: frontend_engineer
created_at: "[timestamp]"
project: "[project-slug]"

phase: 3b
depends_on:
  - document: "02-design/design-brief.md"
    version: ">=1.0.0"
    status: approved
  - document: "03-architecture/technical-architecture.md"
    version: ">=1.0.0"
    status: approved
---

# Frontend Implementation Notes

## Completed Features

| Feature | Status | Tests | A11y | Notes |
|---------|--------|-------|------|-------|
| Design tokens | ✅ Complete | — | — | Tailwind configured |
| Button component | ✅ Complete | ✅ | ✅ | All variants |
| Input component | ✅ Complete | ✅ | ✅ | |
| Login form | ✅ Complete | ✅ | ✅ | With validation |
| Register form | 🔄 In Progress | — | — | |

## Design System Compliance

| Token Category | Implemented | Verified |
|----------------|-------------|----------|
| Colours | ✅ | ✅ |
| Typography | ✅ | ✅ |
| Spacing | ✅ | ✅ |
| Shadows | ✅ | ✅ |

## API Integration Status

| Endpoint | Integrated | Error Handling | Loading State |
|----------|------------|----------------|---------------|
| POST /auth/login | ✅ | ✅ | ✅ |
| POST /auth/register | ✅ | ✅ | ✅ |
| GET /users/me | ✅ | ✅ | ✅ |

## Accessibility Audit

| Component | Keyboard | Screen Reader | Contrast | Motion |
|-----------|----------|---------------|----------|--------|
| Button | ✅ | ✅ | ✅ | ✅ |
| Input | ✅ | ✅ | ✅ | N/A |
| Form | ✅ | ✅ | ✅ | N/A |

## Security Checklist

| ID | Consideration | Status |
|----|---------------|--------|
| SEC-UX-001 | Form validation feedback | ✅ Implemented |
| SEC-UX-002 | Password masking | ✅ Implemented |
| SEC-FE-001 | XSS prevention | ✅ React default escaping |
| SEC-FE-002 | Token storage | ✅ Memory + httpOnly refresh |

## Known Issues

| Issue | Severity | Workaround |
|-------|----------|------------|
| [None] | — | — |
```

## Security Considerations (Embedded)

| ID | Consideration | Status | Implementation |
|----|---------------|--------|----------------|
| SEC-FE-001 | XSS prevention | Mitigated | React escaping, no dangerouslySetInnerHTML |
| SEC-FE-002 | Token storage | Mitigated | Access token in memory, refresh in storage |
| SEC-FE-003 | CSP headers | Identified | Configured in next.config.js |
| SEC-FE-004 | Form CSRF | Mitigated | SameSite cookies from API |

## Handoff

```
✅ Frontend implementation [status] for [Project Name]

**Completed**:
- Design tokens in Tailwind config
- [X] UI components with all states
- [X] pages/features implemented
- API client with auth flow
- Accessibility verified

**For QA**:
- UI matches design brief
- All API error cases handled
- Keyboard navigation complete
- Lighthouse accessibility: [score]

**Blocking items**: [None / List]
```

