Files
flyer-crawler.projectium.com/docs/subagents/FRONTEND-GUIDE.md
Torben Sorensen 45ac4fccf5
Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
comprehensive documentation review + test fixes
2026-01-28 16:35:38 -08:00

11 KiB

Frontend Subagent Guide

This guide covers frontend-focused subagents:

  • frontend-specialist: UI components, Neo-Brutalism, Core Web Vitals, accessibility
  • uiux-designer: UI/UX decisions, component design, user experience

Quick Reference

Aspect frontend-specialist uiux-designer
Primary Use React components, performance, accessibility Design decisions, user flows
Key Files src/components/, src/features/ Design specs, mockups
Key ADRs ADR-012 (Design System), ADR-044 (Feature Org) ADR-012 (Design System)
Design System Neo-Brutalism (bold borders, high contrast) Same
State Mgmt TanStack Query (server), Zustand (client) N/A
Delegate To coder (backend), tester (test coverage) frontend-specialist (implementation)

The frontend-specialist Subagent

When to Use

Use the frontend-specialist subagent when you need to:

  • Build new React components
  • Fix CSS/styling issues
  • Improve Core Web Vitals performance
  • Implement accessibility features
  • Debug React rendering issues
  • Optimize bundle size

What frontend-specialist Knows

The frontend-specialist subagent understands:

  • React 18+ patterns and hooks
  • TanStack Query for server state
  • Zustand for client state
  • Tailwind CSS with custom design tokens
  • Neo-Brutalism design system
  • Accessibility standards (WCAG)
  • Performance optimization

Design System: Neo-Brutalism

The project uses a Neo-Brutalism design aesthetic characterized by:

  • Bold, black borders
  • High contrast colors
  • Shadow offsets for depth
  • Raw, honest UI elements
  • Playful but functional

Design Tokens

Located in src/styles/ and documented in docs/DESIGN_TOKENS.md:

/* Core colors */
--color-primary: #ff6b35;
--color-secondary: #004e89;
--color-accent: #f7c548;
--color-background: #fffdf7;
--color-text: #1a1a1a;

/* Borders */
--border-width: 3px;
--border-color: #1a1a1a;

/* Shadows (offset style) */
--shadow-sm: 2px 2px 0 0 #1a1a1a;
--shadow-md: 4px 4px 0 0 #1a1a1a;
--shadow-lg: 6px 6px 0 0 #1a1a1a;

Component Patterns

Brutal Card:

<div className="border-3 border-black bg-white p-4 shadow-[4px_4px_0_0_#1A1A1A] hover:shadow-[6px_6px_0_0_#1A1A1A] hover:translate-x-[-2px] hover:translate-y-[-2px] transition-all">
  {children}
</div>

Brutal Button:

<button className="border-3 border-black bg-primary px-4 py-2 font-bold shadow-[4px_4px_0_0_#1A1A1A] hover:shadow-[2px_2px_0_0_#1A1A1A] hover:translate-x-[2px] hover:translate-y-[2px] active:shadow-none active:translate-x-[4px] active:translate-y-[4px] transition-all">
  Click Me
</button>

Example Requests

Building New Components

"Use frontend-specialist to create a PriceTag component that
displays the current price and original price (if discounted)
in the Neo-Brutalism style with a 'SALE' badge when applicable."

Performance Optimization

"Use frontend-specialist to optimize the deals list page.
It's showing poor Largest Contentful Paint scores and the
initial load feels sluggish."

Accessibility Fix

"Use frontend-specialist to audit and fix accessibility issues
on the shopping list page. Screen reader users report that
the checkbox states aren't being announced correctly."

Responsive Design

"Use frontend-specialist to make the store search component
work better on mobile. The dropdown menu is getting cut off
on smaller screens."

State Management

Server State (TanStack Query)

// Fetching data with caching
const {
  data: deals,
  isLoading,
  error,
} = useQuery({
  queryKey: ['deals', storeId],
  queryFn: () => dealsApi.getByStore(storeId),
  staleTime: 5 * 60 * 1000, // 5 minutes
});

// Mutations with optimistic updates
const mutation = useMutation({
  mutationFn: dealsApi.favorite,
  onMutate: async (dealId) => {
    await queryClient.cancelQueries(['deals']);
    const previous = queryClient.getQueryData(['deals']);
    queryClient.setQueryData(['deals'], (old) =>
      old.map((d) => (d.id === dealId ? { ...d, isFavorite: true } : d)),
    );
    return { previous };
  },
  onError: (err, dealId, context) => {
    queryClient.setQueryData(['deals'], context.previous);
  },
});

Client State (Zustand)

// Simple client-only state
const useUIStore = create((set) => ({
  sidebarOpen: false,
  toggleSidebar: () => set((s) => ({ sidebarOpen: !s.sidebarOpen })),
}));

The uiux-designer Subagent

When to Use

Use the uiux-designer subagent when you need to:

  • Make design decisions for new features
  • Improve user flows
  • Design component layouts
  • Choose appropriate UI patterns
  • Plan information architecture

Example Requests

Design new feature:

"Use uiux-designer to design the user flow for adding items
to a shopping list from the deals page. Consider both desktop
and mobile experiences."

Improve existing UX:

"Use uiux-designer to improve the flyer upload experience.
Users are confused about which file types are supported and
don't understand the processing status."

Component design:

"Use uiux-designer to design a price comparison component
that shows the same item across multiple stores."

Component Structure

Feature-Based Organization

src/
├── components/           # Shared UI components
│   ├── ui/              # Basic UI primitives
│   │   ├── Button.tsx
│   │   ├── Card.tsx
│   │   └── Input.tsx
│   ├── layout/          # Layout components
│   │   ├── Header.tsx
│   │   └── Sidebar.tsx
│   └── shared/          # Complex shared components
│       └── PriceDisplay.tsx
├── features/            # Feature-specific components
│   ├── deals/
│   │   ├── components/
│   │   ├── hooks/
│   │   └── api/
│   └── shopping-list/
│       ├── components/
│       ├── hooks/
│       └── api/
└── pages/               # Route page components
    ├── DealsPage.tsx
    └── ShoppingListPage.tsx

Component Pattern

// src/components/PriceTag.tsx
import { cn } from '@/utils/cn';

interface PriceTagProps {
  currentPrice: number;
  originalPrice?: number;
  currency?: string;
  className?: string;
}

export function PriceTag({
  currentPrice,
  originalPrice,
  currency = '$',
  className,
}: PriceTagProps) {
  const isOnSale = originalPrice && originalPrice > currentPrice;
  const discount = isOnSale ? Math.round((1 - currentPrice / originalPrice) * 100) : 0;

  return (
    <div className={cn('flex items-baseline gap-2', className)}>
      <span className="text-2xl font-bold text-primary">
        {currency}
        {currentPrice.toFixed(2)}
      </span>
      {isOnSale && (
        <>
          <span className="text-sm text-gray-500 line-through">
            {currency}
            {originalPrice.toFixed(2)}
          </span>
          <span className="border-2 border-black bg-accent px-1 text-xs font-bold">
            -{discount}%
          </span>
        </>
      )}
    </div>
  );
}

Testing React Components

Component Test Pattern

import { describe, it, expect, vi } from 'vitest';
import { renderWithProviders, screen } from '@/tests/utils/renderWithProviders';
import userEvent from '@testing-library/user-event';
import { PriceTag } from './PriceTag';

describe('PriceTag', () => {
  it('displays current price', () => {
    renderWithProviders(<PriceTag currentPrice={9.99} />);
    expect(screen.getByText('$9.99')).toBeInTheDocument();
  });

  it('shows discount when original price is higher', () => {
    renderWithProviders(<PriceTag currentPrice={7.99} originalPrice={9.99} />);
    expect(screen.getByText('$7.99')).toBeInTheDocument();
    expect(screen.getByText('$9.99')).toBeInTheDocument();
    expect(screen.getByText('-20%')).toBeInTheDocument();
  });
});

Hook Test Pattern

import { renderHook, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useDeals } from './useDeals';

describe('useDeals', () => {
  it('fetches deals for store', async () => {
    const queryClient = new QueryClient({
      defaultOptions: { queries: { retry: false } },
    });

    const { result } = renderHook(() => useDeals('store-123'), {
      wrapper: ({ children }) => (
        <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
      ),
    });

    await waitFor(() => expect(result.current.isSuccess).toBe(true));
    expect(result.current.data).toHaveLength(10);
  });
});

Accessibility Guidelines

ARIA Patterns

// Proper button with loading state
<button
  aria-busy={isLoading}
  aria-label={isLoading ? 'Loading...' : 'Add to cart'}
  disabled={isLoading}
>
  {isLoading ? <Spinner /> : 'Add to Cart'}
</button>

// Proper form field
<label htmlFor="email">Email Address</label>
<input
  id="email"
  type="email"
  aria-describedby="email-error"
  aria-invalid={!!errors.email}
/>
{errors.email && (
  <span id="email-error" role="alert">
    {errors.email}
  </span>
)}

Keyboard Navigation

  • All interactive elements must be focusable
  • Focus order should be logical
  • Focus traps for modals
  • Skip links for main content

Color Contrast

  • Normal text: minimum 4.5:1 contrast ratio
  • Large text: minimum 3:1 contrast ratio
  • Use the Neo-Brutalism palette which is designed for high contrast

Performance Optimization

Code Splitting

// Lazy load heavy components
const PdfViewer = lazy(() => import('./PdfViewer'));

function FlyerPage() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <PdfViewer />
    </Suspense>
  );
}

Image Optimization

// Use appropriate sizes and formats
<img
  src={imageUrl}
  srcSet={`${imageUrl}?w=400 400w, ${imageUrl}?w=800 800w`}
  sizes="(max-width: 600px) 400px, 800px"
  loading="lazy"
  alt={itemName}
/>

Memoization

// Memoize expensive computations
const sortedDeals = useMemo(() => deals.slice().sort((a, b) => a.price - b.price), [deals]);

// Memoize callbacks passed to children
const handleSelect = useCallback((id: string) => {
  setSelectedId(id);
}, []);