doc updates and test fixin
This commit is contained in:
410
docs/subagents/FRONTEND-GUIDE.md
Normal file
410
docs/subagents/FRONTEND-GUIDE.md
Normal file
@@ -0,0 +1,410 @@
|
||||
# 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`:
|
||||
|
||||
```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
|
||||
- [../DESIGN_TOKENS.md](../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
|
||||
Reference in New Issue
Block a user