Some checks failed
Deploy to Test Environment / deploy-to-test (push) Failing after 2m15s
425 lines
11 KiB
Markdown
425 lines
11 KiB
Markdown
# 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`:
|
|
|
|
```css
|
|
/* 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:**
|
|
|
|
```tsx
|
|
<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:**
|
|
|
|
```tsx
|
|
<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)
|
|
|
|
```tsx
|
|
// 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)
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// Lazy load heavy components
|
|
const PdfViewer = lazy(() => import('./PdfViewer'));
|
|
|
|
function FlyerPage() {
|
|
return (
|
|
<Suspense fallback={<LoadingSpinner />}>
|
|
<PdfViewer />
|
|
</Suspense>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Image Optimization
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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](./OVERVIEW.md) - Subagent system overview
|
|
- [CODER-GUIDE.md](./CODER-GUIDE.md) - For implementing features
|
|
- [TESTER-GUIDE.md](./TESTER-GUIDE.md) - Component testing patterns
|
|
- [../development/DESIGN_TOKENS.md](../development/DESIGN_TOKENS.md) - Design token reference
|
|
- [../adr/0012-frontend-component-library-and-design-system.md](../adr/0012-frontend-component-library-and-design-system.md) - Design system ADR
|
|
- [../adr/0005-frontend-state-management-and-server-cache-strategy.md](../adr/0005-frontend-state-management-and-server-cache-strategy.md) - State management ADR
|
|
- [../adr/0044-frontend-feature-organization.md](../adr/0044-frontend-feature-organization.md) - Feature organization
|