All checks were successful
Deploy to Test Environment / deploy-to-test (push) Successful in 19m11s
10 KiB
10 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
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);
}, []);
Related Documentation
- OVERVIEW.md - Subagent system overview
- CODER-GUIDE.md - For implementing features
- ../DESIGN_TOKENS.md - Design token reference
- ../adr/0012-frontend-component-library-and-design-system.md - Design system ADR
- ../adr/0005-frontend-state-management-and-server-cache-strategy.md - State management ADR
- ../adr/0044-frontend-feature-organization.md - Feature organization