skills/frontend-patterns/SKILL.md
Frontend development patterns for React, Next.js, state management, performance optimization, and UI best practices.
npx skillsauth add rubicanjr/FinCognis frontend-patternsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Modern frontend patterns for React, Next.js, and performant user interfaces.
// ✅ GOOD: Component composition
interface CardProps {
children: React.ReactNode
variant?: 'default' | 'outlined'
}
export function Card({ children, variant = 'default' }: CardProps) {
return <div className={`card card-${variant}`}>{children}</div>
}
export function CardHeader({ children }: { children: React.ReactNode }) {
return <div className="card-header">{children}</div>
}
export function CardBody({ children }: { children: React.ReactNode }) {
return <div className="card-body">{children}</div>
}
// Usage
<Card>
<CardHeader>Title</CardHeader>
<CardBody>Content</CardBody>
</Card>
interface TabsContextValue {
activeTab: string
setActiveTab: (tab: string) => void
}
const TabsContext = createContext<TabsContextValue | undefined>(undefined)
export function Tabs({ children, defaultTab }: {
children: React.ReactNode
defaultTab: string
}) {
const [activeTab, setActiveTab] = useState(defaultTab)
return (
<TabsContext.Provider value={{ activeTab, setActiveTab }}>
{children}
</TabsContext.Provider>
)
}
export function TabList({ children }: { children: React.ReactNode }) {
return <div className="tab-list">{children}</div>
}
export function Tab({ id, children }: { id: string, children: React.ReactNode }) {
const context = useContext(TabsContext)
if (!context) throw new Error('Tab must be used within Tabs')
return (
<button
className={context.activeTab === id ? 'active' : ''}
onClick={() => context.setActiveTab(id)}
>
{children}
</button>
)
}
// Usage
<Tabs defaultTab="overview">
<TabList>
<Tab id="overview">Overview</Tab>
<Tab id="details">Details</Tab>
</TabList>
</Tabs>
interface DataLoaderProps<T> {
url: string
children: (data: T | null, loading: boolean, error: Error | null) => React.ReactNode
}
export function DataLoader<T>({ url, children }: DataLoaderProps<T>) {
const [data, setData] = useState<T | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<Error | null>(null)
useEffect(() => {
fetch(url)
.then(res => res.json())
.then(setData)
.catch(setError)
.finally(() => setLoading(false))
}, [url])
return <>{children(data, loading, error)}</>
}
// Usage
<DataLoader<Market[]> url="/api/markets">
{(markets, loading, error) => {
if (loading) return <Spinner />
if (error) return <Error error={error} />
return <MarketList markets={markets!} />
}}
</DataLoader>
export function useToggle(initialValue = false): [boolean, () => void] {
const [value, setValue] = useState(initialValue)
const toggle = useCallback(() => {
setValue(v => !v)
}, [])
return [value, toggle]
}
// Usage
const [isOpen, toggleOpen] = useToggle()
interface UseQueryOptions<T> {
onSuccess?: (data: T) => void
onError?: (error: Error) => void
enabled?: boolean
}
export function useQuery<T>(
key: string,
fetcher: () => Promise<T>,
options?: UseQueryOptions<T>
) {
const [data, setData] = useState<T | null>(null)
const [error, setError] = useState<Error | null>(null)
const [loading, setLoading] = useState(false)
const refetch = useCallback(async () => {
setLoading(true)
setError(null)
try {
const result = await fetcher()
setData(result)
options?.onSuccess?.(result)
} catch (err) {
const error = err as Error
setError(error)
options?.onError?.(error)
} finally {
setLoading(false)
}
}, [fetcher, options])
useEffect(() => {
if (options?.enabled !== false) {
refetch()
}
}, [key, refetch, options?.enabled])
return { data, error, loading, refetch }
}
// Usage
const { data: markets, loading, error, refetch } = useQuery(
'markets',
() => fetch('/api/markets').then(r => r.json()),
{
onSuccess: data => console.log('Fetched', data.length, 'markets'),
onError: err => console.error('Failed:', err)
}
)
export function useDebounce<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState<T>(value)
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value)
}, delay)
return () => clearTimeout(handler)
}, [value, delay])
return debouncedValue
}
// Usage
const [searchQuery, setSearchQuery] = useState('')
const debouncedQuery = useDebounce(searchQuery, 500)
useEffect(() => {
if (debouncedQuery) {
performSearch(debouncedQuery)
}
}, [debouncedQuery])
interface State {
markets: Market[]
selectedMarket: Market | null
loading: boolean
}
type Action =
| { type: 'SET_MARKETS'; payload: Market[] }
| { type: 'SELECT_MARKET'; payload: Market }
| { type: 'SET_LOADING'; payload: boolean }
function reducer(state: State, action: Action): State {
switch (action.type) {
case 'SET_MARKETS':
return { ...state, markets: action.payload }
case 'SELECT_MARKET':
return { ...state, selectedMarket: action.payload }
case 'SET_LOADING':
return { ...state, loading: action.payload }
default:
return state
}
}
const MarketContext = createContext<{
state: State
dispatch: Dispatch<Action>
} | undefined>(undefined)
export function MarketProvider({ children }: { children: React.ReactNode }) {
const [state, dispatch] = useReducer(reducer, {
markets: [],
selectedMarket: null,
loading: false
})
return (
<MarketContext.Provider value={{ state, dispatch }}>
{children}
</MarketContext.Provider>
)
}
export function useMarkets() {
const context = useContext(MarketContext)
if (!context) throw new Error('useMarkets must be used within MarketProvider')
return context
}
// ✅ useMemo for expensive computations
const sortedMarkets = useMemo(() => {
return markets.sort((a, b) => b.volume - a.volume)
}, [markets])
// ✅ useCallback for functions passed to children
const handleSearch = useCallback((query: string) => {
setSearchQuery(query)
}, [])
// ✅ React.memo for pure components
export const MarketCard = React.memo<MarketCardProps>(({ market }) => {
return (
<div className="market-card">
<h3>{market.name}</h3>
<p>{market.description}</p>
</div>
)
})
import { lazy, Suspense } from 'react'
// ✅ Lazy load heavy components
const HeavyChart = lazy(() => import('./HeavyChart'))
const ThreeJsBackground = lazy(() => import('./ThreeJsBackground'))
export function Dashboard() {
return (
<div>
<Suspense fallback={<ChartSkeleton />}>
<HeavyChart data={data} />
</Suspense>
<Suspense fallback={null}>
<ThreeJsBackground />
</Suspense>
</div>
)
}
import { useVirtualizer } from '@tanstack/react-virtual'
export function VirtualMarketList({ markets }: { markets: Market[] }) {
const parentRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer({
count: markets.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 100, // Estimated row height
overscan: 5 // Extra items to render
})
return (
<div ref={parentRef} style={{ height: '600px', overflow: 'auto' }}>
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative'
}}
>
{virtualizer.getVirtualItems().map(virtualRow => (
<div
key={virtualRow.index}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualRow.size}px`,
transform: `translateY(${virtualRow.start}px)`
}}
>
<MarketCard market={markets[virtualRow.index]} />
</div>
))}
</div>
</div>
)
}
interface FormData {
name: string
description: string
endDate: string
}
interface FormErrors {
name?: string
description?: string
endDate?: string
}
export function CreateMarketForm() {
const [formData, setFormData] = useState<FormData>({
name: '',
description: '',
endDate: ''
})
const [errors, setErrors] = useState<FormErrors>({})
const validate = (): boolean => {
const newErrors: FormErrors = {}
if (!formData.name.trim()) {
newErrors.name = 'Name is required'
} else if (formData.name.length > 200) {
newErrors.name = 'Name must be under 200 characters'
}
if (!formData.description.trim()) {
newErrors.description = 'Description is required'
}
if (!formData.endDate) {
newErrors.endDate = 'End date is required'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
try {
await createMarket(formData)
// Success handling
} catch (error) {
// Error handling
}
}
return (
<form onSubmit={handleSubmit}>
<input
value={formData.name}
onChange={e => setFormData(prev => ({ ...prev, name: e.target.value }))}
placeholder="Market name"
/>
{errors.name && <span className="error">{errors.name}</span>}
{/* Other fields */}
<button type="submit">Create Market</button>
</form>
)
}
interface ErrorBoundaryState {
hasError: boolean
error: Error | null
}
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
ErrorBoundaryState
> {
state: ErrorBoundaryState = {
hasError: false,
error: null
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
console.error('Error boundary caught:', error, errorInfo)
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>Something went wrong</h2>
<p>{this.state.error?.message}</p>
<button onClick={() => this.setState({ hasError: false })}>
Try again
</button>
</div>
)
}
return this.props.children
}
}
// Usage
<ErrorBoundary>
<App />
</ErrorBoundary>
import { motion, AnimatePresence } from 'framer-motion'
// ✅ List animations
export function AnimatedMarketList({ markets }: { markets: Market[] }) {
return (
<AnimatePresence>
{markets.map(market => (
<motion.div
key={market.id}
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -20 }}
transition={{ duration: 0.3 }}
>
<MarketCard market={market} />
</motion.div>
))}
</AnimatePresence>
)
}
// ✅ Modal animations
export function Modal({ isOpen, onClose, children }: ModalProps) {
return (
<AnimatePresence>
{isOpen && (
<>
<motion.div
className="modal-overlay"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={onClose}
/>
<motion.div
className="modal-content"
initial={{ opacity: 0, scale: 0.9, y: 20 }}
animate={{ opacity: 1, scale: 1, y: 0 }}
exit={{ opacity: 0, scale: 0.9, y: 20 }}
>
{children}
</motion.div>
</>
)}
</AnimatePresence>
)
}
export function Dropdown({ options, onSelect }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false)
const [activeIndex, setActiveIndex] = useState(0)
const handleKeyDown = (e: React.KeyboardEvent) => {
switch (e.key) {
case 'ArrowDown':
e.preventDefault()
setActiveIndex(i => Math.min(i + 1, options.length - 1))
break
case 'ArrowUp':
e.preventDefault()
setActiveIndex(i => Math.max(i - 1, 0))
break
case 'Enter':
e.preventDefault()
onSelect(options[activeIndex])
setIsOpen(false)
break
case 'Escape':
setIsOpen(false)
break
}
}
return (
<div
role="combobox"
aria-expanded={isOpen}
aria-haspopup="listbox"
onKeyDown={handleKeyDown}
>
{/* Dropdown implementation */}
</div>
)
}
export function Modal({ isOpen, onClose, children }: ModalProps) {
const modalRef = useRef<HTMLDivElement>(null)
const previousFocusRef = useRef<HTMLElement | null>(null)
useEffect(() => {
if (isOpen) {
// Save currently focused element
previousFocusRef.current = document.activeElement as HTMLElement
// Focus modal
modalRef.current?.focus()
} else {
// Restore focus when closing
previousFocusRef.current?.focus()
}
}, [isOpen])
return isOpen ? (
<div
ref={modalRef}
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={e => e.key === 'Escape' && onClose()}
>
{children}
</div>
) : null
}
:root {
/* Color — semantic naming, not "blue-500" */
--color-surface: #ffffff;
--color-surface-raised: #f8fafc;
--color-surface-sunken: #f1f5f9;
--color-border: #e2e8f0;
--color-border-strong: #cbd5e1;
--color-text: #0f172a;
--color-text-secondary: #475569;
--color-text-muted: #94a3b8;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-primary-subtle: #eff6ff;
--color-danger: #dc2626;
--color-danger-subtle: #fef2f2;
--color-success: #16a34a;
--color-success-subtle: #f0fdf4;
--color-warning: #d97706;
/* Spacing — 4px base, t-shirt sizes */
--space-1: 0.25rem; /* 4px */
--space-2: 0.5rem; /* 8px */
--space-3: 0.75rem; /* 12px */
--space-4: 1rem; /* 16px */
--space-6: 1.5rem; /* 24px */
--space-8: 2rem; /* 32px */
--space-12: 3rem; /* 48px */
--space-16: 4rem; /* 64px */
/* Typography */
--font-sans: 'Inter', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-base: 1rem;
--text-lg: 1.125rem;
--text-xl: 1.25rem;
--text-2xl: 1.5rem;
--text-3xl: 1.875rem;
--leading-tight: 1.25;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
/* Radius */
--radius-sm: 0.25rem;
--radius-md: 0.375rem;
--radius-lg: 0.5rem;
--radius-xl: 0.75rem;
--radius-full: 9999px;
/* Shadow — depth levels */
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
--shadow-focus: 0 0 0 3px rgb(37 99 235 / 0.2);
/* Transitions */
--duration-fast: 100ms;
--duration-normal: 200ms;
--duration-slow: 300ms;
--ease-out: cubic-bezier(0.16, 1, 0.3, 1);
}
[data-theme="dark"] {
--color-surface: #0f172a;
--color-surface-raised: #1e293b;
--color-surface-sunken: #020617;
--color-border: #334155;
--color-border-strong: #475569;
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-text-muted: #64748b;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-primary-subtle: #172554;
--color-danger-subtle: #450a0a;
--color-success-subtle: #052e16;
}
/* mobile-first: base → sm → md → lg → xl */
/* 640px → sm (landscape phone) */
/* 768px → md (tablet) */
/* 1024px → lg (laptop) */
/* 1280px → xl (desktop) */
/* 1536px → 2xl (wide) */
.grid-auto {
display: grid;
gap: var(--space-4);
grid-template-columns: 1fr;
}
@media (min-width: 640px) { .grid-auto { grid-template-columns: repeat(2, 1fr); } }
@media (min-width: 1024px) { .grid-auto { grid-template-columns: repeat(3, 1fr); } }
@media (min-width: 1280px) { .grid-auto { grid-template-columns: repeat(4, 1fr); } }
/* focus-visible > focus — keyboard only, not mouse clicks */
.btn:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
/* minimum 44x44px touch target */
.btn {
min-height: 44px;
min-width: 44px;
padding: var(--space-2) var(--space-4);
transition: all var(--duration-normal) var(--ease-out);
}
/* disabled state — opacity, not color change */
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
pointer-events: none;
}
// app/dashboard/page.tsx — server component by default
// DB query here, no "use client", no useState, no useEffect
import { db } from '@/lib/db'
export default async function DashboardPage() {
const stats = await db.query.stats.findMany()
return (
<div>
<h1>Dashboard</h1>
<StatGrid stats={stats} />
<Suspense fallback={<TableSkeleton rows={10} />}>
<RecentActivity />
</Suspense>
</div>
)
}
// async child — streams in when ready
async function RecentActivity() {
const activity = await db.query.activity.findMany({
orderBy: (a, { desc }) => [desc(a.createdAt)],
limit: 20,
})
return <ActivityTable rows={activity} />
}
'use client'
// push "use client" as LOW as possible in the tree
// wrong: entire page is client
// right: only the interactive widget
import { useState, useTransition } from 'react'
export function SearchFilter({ onSearch }: { onSearch: (q: string) => void }) {
const [query, setQuery] = useState('')
const [isPending, startTransition] = useTransition()
return (
<input
value={query}
onChange={e => {
setQuery(e.target.value)
startTransition(() => onSearch(e.target.value))
}}
placeholder="Search..."
aria-label="Search"
className={isPending ? 'opacity-60' : ''}
/>
)
}
// app/actions.ts
'use server'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
import { z } from 'zod'
const CreateTaskSchema = z.object({
title: z.string().min(1).max(200),
description: z.string().max(2000).optional(),
priority: z.enum(['low', 'medium', 'high']),
})
export async function createTask(formData: FormData) {
const parsed = CreateTaskSchema.safeParse({
title: formData.get('title'),
description: formData.get('description'),
priority: formData.get('priority'),
})
if (!parsed.success) {
return { error: parsed.error.flatten().fieldErrors }
}
await db.insert(tasks).values(parsed.data)
revalidatePath('/tasks')
redirect('/tasks')
}
app/
dashboard/
@stats/
page.tsx ← parallel: stats panel
loading.tsx ← skeleton while stats load
@activity/
page.tsx ← parallel: activity feed
loading.tsx
layout.tsx ← renders both slots
page.tsx
// app/dashboard/layout.tsx
export default function DashboardLayout({
children,
stats,
activity,
}: {
children: React.ReactNode
stats: React.ReactNode
activity: React.ReactNode
}) {
return (
<div className="grid grid-cols-1 lg:grid-cols-[1fr_320px] gap-6">
<div>
{children}
{activity}
</div>
<aside>{stats}</aside>
</div>
)
}
// app/api/tasks/route.ts
import { NextRequest, NextResponse } from 'next/server'
export async function GET(request: NextRequest) {
const { searchParams } = request.nextUrl
const page = parseInt(searchParams.get('page') ?? '1', 10)
const limit = Math.min(parseInt(searchParams.get('limit') ?? '20', 10), 100)
const tasks = await db.query.tasks.findMany({
offset: (page - 1) * limit,
limit,
orderBy: (t, { desc }) => [desc(t.createdAt)],
})
return NextResponse.json({
data: tasks,
meta: { page, limit, total: await db.select({ count: count() }).from(tasks) },
})
}
export async function POST(request: NextRequest) {
const body = await request.json()
// validate with zod, then insert
}
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { TaskForm } from './TaskForm'
describe('TaskForm', () => {
const user = userEvent.setup()
it('submits with valid data', async () => {
const onSubmit = vi.fn()
render(<TaskForm onSubmit={onSubmit} />)
await user.type(screen.getByLabelText(/title/i), 'Fix login bug')
await user.selectOptions(screen.getByLabelText(/priority/i), 'high')
await user.click(screen.getByRole('button', { name: /create/i }))
expect(onSubmit).toHaveBeenCalledWith({
title: 'Fix login bug',
priority: 'high',
})
})
it('shows validation error on empty title', async () => {
render(<TaskForm onSubmit={vi.fn()} />)
await user.click(screen.getByRole('button', { name: /create/i }))
expect(screen.getByText(/title is required/i)).toBeInTheDocument()
})
it('disables submit while loading', async () => {
render(<TaskForm onSubmit={() => new Promise(() => {})} />)
await user.type(screen.getByLabelText(/title/i), 'Test')
await user.click(screen.getByRole('button', { name: /create/i }))
expect(screen.getByRole('button', { name: /create/i })).toBeDisabled()
})
})
import { renderHook, act } from '@testing-library/react'
import { useDebounce } from './useDebounce'
describe('useDebounce', () => {
beforeEach(() => vi.useFakeTimers())
afterEach(() => vi.useRealTimers())
it('returns initial value immediately', () => {
const { result } = renderHook(() => useDebounce('hello', 500))
expect(result.current).toBe('hello')
})
it('debounces value changes', () => {
const { result, rerender } = renderHook(
({ value, delay }) => useDebounce(value, delay),
{ initialProps: { value: 'hello', delay: 500 } }
)
rerender({ value: 'world', delay: 500 })
expect(result.current).toBe('hello')
act(() => vi.advanceTimersByTime(500))
expect(result.current).toBe('world')
})
})
import { http, HttpResponse } from 'msw'
import { setupServer } from 'msw/node'
const handlers = [
http.get('/api/tasks', () => {
return HttpResponse.json({
data: [
{ id: '1', title: 'Task 1', priority: 'high' },
{ id: '2', title: 'Task 2', priority: 'low' },
],
meta: { total: 2, page: 1 },
})
}),
http.post('/api/tasks', async ({ request }) => {
const body = await request.json()
return HttpResponse.json({ id: '3', ...body }, { status: 201 })
}),
]
const server = setupServer(...handlers)
beforeAll(() => server.listen())
afterEach(() => server.resetHandlers())
afterAll(() => server.close())
it('renders task list from API', async () => {
render(<TaskList />)
expect(await screen.findByText('Task 1')).toBeInTheDocument()
expect(screen.getByText('Task 2')).toBeInTheDocument()
})
it('handles API error', async () => {
server.use(
http.get('/api/tasks', () => HttpResponse.json(null, { status: 500 }))
)
render(<TaskList />)
expect(await screen.findByText(/something went wrong/i)).toBeInTheDocument()
})
// snapshots break on every CSS change — test behavior instead
// WRONG: expect(component).toMatchSnapshot()
// RIGHT: test what the user sees and does
it('shows empty state when no tasks', () => {
render(<TaskList tasks={[]} />)
expect(screen.getByText(/no tasks yet/i)).toBeInTheDocument()
expect(screen.getByRole('link', { name: /create/i })).toHaveAttribute('href', '/tasks/new')
})
// lib/utils.ts
import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge'
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// usage
<div className={cn(
'rounded-lg border p-4',
variant === 'danger' && 'border-red-300 bg-red-50',
variant === 'success' && 'border-green-300 bg-green-50',
className
)} />
// mobile-first: stack → 2col → 3col
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map(item => <Card key={item.id} {...item} />)}
</div>
// sidebar layout: full-width mobile, sidebar on desktop
<div className="flex flex-col lg:flex-row lg:gap-8">
<main className="flex-1 min-w-0">{children}</main>
<aside className="w-full lg:w-80 lg:shrink-0">{sidebar}</aside>
</div>
import { cva, type VariantProps } from 'class-variance-authority'
const buttonVariants = cva(
'inline-flex items-center justify-center rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
{
variants: {
variant: {
default: 'bg-blue-600 text-white hover:bg-blue-700',
outline: 'border border-gray-300 bg-white hover:bg-gray-50',
ghost: 'hover:bg-gray-100',
danger: 'bg-red-600 text-white hover:bg-red-700',
},
size: {
sm: 'h-8 px-3 text-xs',
md: 'h-10 px-4',
lg: 'h-12 px-6 text-base',
},
},
defaultVariants: {
variant: 'default',
size: 'md',
},
}
)
interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {}
export function Button({ variant, size, className, ...props }: ButtonProps) {
return <button className={cn(buttonVariants({ variant, size }), className)} {...props} />
}
// tailwind.config.ts
export default {
darkMode: 'class',
// ...
}
// toggle
<button onClick={() => document.documentElement.classList.toggle('dark')}>
Toggle
</button>
// usage — always pair light/dark
<div className="bg-white dark:bg-gray-900 text-gray-900 dark:text-gray-100">
<p className="text-gray-600 dark:text-gray-400">Secondary text</p>
</div>
// WRONG: string interpolation — Tailwind can't detect at build time
<div className={`text-${color}-500`} />
// RIGHT: map to full class names
const colorMap = {
red: 'text-red-500',
blue: 'text-blue-500',
green: 'text-green-500',
} as const
<div className={colorMap[color]} />
// WRONG: @apply everywhere — defeats the purpose
// RIGHT: @apply only in base layer for truly repeated patterns
@layer base {
h1 { @apply text-3xl font-bold tracking-tight; }
}
// WRONG: arbitrary values for things that should be tokens
<div className="p-[13px] text-[#3b82f6]" />
// RIGHT: extend theme if you need custom values
// tailwind.config.ts → theme.extend.spacing / colors
development
Goal-based workflow orchestration - routes tasks to specialist agents based on user goals
tools
Wiring Verification
development
Connection management, room patterns, reconnection strategies, message buffering, and binary protocol design.
development
Screenshot comparison QA for frontend development. Takes a screenshot of the current implementation, scores it across multiple visual dimensions, and returns a structured PASS/REVISE/FAIL verdict with concrete fixes. Use when implementing UI from a design reference or verifying visual correctness.