
Oma Frontend
- 125 installs
- 41 repo stars
- Updated August 4, 2026
- gracefullight/stock-checker
Build and verify stock-checker UI pages, watchlist components, charts, and responsive layouts with accessible patterns and frontend quality checks.
About
Oma-frontend skill guides agents building stock-checker client interfaces: composable components, market data visualization, responsive layouts, accessibility, and verification so price screens and alerts render correctly across devices.
- Dashboard UI
- Market charts
- Responsive layout
- Client state
- Visual verification
Oma Frontend by the numbers
- 125 all-time installs (skills.sh)
- Ranked #1,005 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/gracefullight/stock-checker --skill oma-frontendAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 125 |
|---|---|
| repo stars | ★ 41 |
| Last updated | August 4, 2026 |
| Repository | gracefullight/stock-checker ↗ |
What it does
Build and verify stock-checker UI pages, watchlist components, charts, and responsive layouts with accessible patterns and frontend quality checks.
Files
Frontend Agent - UI/UX Specialist
Scheduling
Goal
Build, modify, and verify React/Next.js/TypeScript user interfaces that follow project architecture, design-system constraints, accessibility expectations, and existing frontend conventions.
Intent signature
- User asks for UI, component, page, layout, CSS, Tailwind, shadcn, form, interaction, client state, or frontend API integration work.
- User needs browser-facing implementation in a React/Next.js TypeScript codebase.
When to use
- Building user interfaces and components
- Client-side logic and state management
- Styling and responsive design
- Form validation and user interactions
- Integrating with backend APIs
When NOT to use
- Backend API implementation → use Backend Agent
- Database access, migrations, or ORM setup → use Backend Agent
- Auth server setup (better-auth server library, DB adapters) → use Backend Agent
- Native mobile development → use Mobile Agent
Expected inputs
- Target page, component, flow, or UI behavior
- Existing app structure, design tokens, component library, i18n files, and API contracts
- Acceptance criteria and target responsive states
Expected outputs
- Frontend code changes in pages, components, hooks, styles, tests, or wrappers
- UI that respects project tokens, i18n, server/client boundaries, and accessibility expectations
- Verification results from relevant lint, typecheck, tests, or browser checks
Dependencies
- React, Next.js, TypeScript, TailwindCSS v4, and
shadcn/ui - Project sources of truth such as
packages/design-tokens,packages/i18n, and shared utilities resources/execution-protocol.md,resources/checklist.md, examples, snippets, and Tailwind rules
Control-flow features
- Branches by server/client component boundary, responsive state, component library availability, and i18n/token requirements
- Reads and writes frontend codebase files
- May call shadcn registry tools or local verification commands
Structural Flow
Entry
1. Identify target route, component, state boundary, and design-system constraints. 2. Read existing patterns before adding components or utilities. 3. Determine whether work belongs in Server Components, Client Components, wrappers, hooks, or styles.
Scenes
1. PREPARE: Load relevant project conventions, UI requirements, and acceptance criteria. 2. ACQUIRE: Inspect existing components, tokens, i18n keys, APIs, and shadcn availability. 3. ACT: Implement UI, state, styles, validation, and integration. 4. VERIFY: Run checklist, automated checks, and browser/responsive validation when applicable. 5. FINALIZE: Summarize changed UI behavior and verification.
Transitions
- If a strict shadcn primitive exists, use or wrap it before creating generic markup.
- If UI text is user-facing and i18n exists, add strings through the i18n source of truth.
- If interaction or hooks are needed, mark the boundary as Client Component.
- If backend contracts are missing, coordinate with backend/API planning.
Failure and recovery
- If design tokens or i18n sources are missing, state assumptions and follow existing local patterns.
- If verification fails, fix before handoff or report the blocker.
- If required shadcn registry access fails, use existing local components or document fallback.
Exit
- Success: UI works across target responsive states and passes relevant checks.
- Partial success: missing assets, backend contracts, or verification gaps are explicit.
Logical Operations
Actions
| Action | SSL primitive | Evidence |
|---|---|---|
| Inspect existing frontend patterns | READ | Components, routes, hooks, styles |
| Select component and state approach | SELECT | Server/client and shadcn workflow |
| Implement UI code | WRITE | TSX, CSS, hooks, wrappers |
| Validate form/data contracts | VALIDATE | Zod/forms/API schemas |
| Call shadcn or verification tools | CALL_TOOL | Registry, lint, typecheck, tests |
| Compare responsive states | COMPARE | Desktop/mobile behavior |
| Report result | NOTIFY | Final summary |
Tools and instruments
- React, Next.js, TypeScript, TailwindCSS v4, shadcn/ui
ahooks,es-toolkit,nuqs, TanStack Query, Jotai, TanStack React Form,zod- Lint, typecheck, tests, and browser inspection when applicable
Canonical workflow path
rg --files
rg "components/ui|shadcn|use client|generateMetadata|useQuery|i18n|design-tokens" .Then run the project's frontend verification commands, typically lint, typecheck, tests, and browser/responsive checks when the UI changes.
Resource scope
| Scope | Resource target |
|---|---|
CODEBASE | Frontend routes, components, styles, hooks, tests |
LOCAL_FS | Design tokens, i18n files, resource references |
PROCESS | Build, lint, typecheck, test, browser commands |
NETWORK | Backend APIs or registry tools when required |
Preconditions
- Target UI behavior and affected frontend area are identifiable.
- Required design tokens, i18n, and API contracts are available or assumptions are stated.
Effects and side effects
- Mutates frontend source, styles, tests, and possibly i18n keys.
- May add dependencies or shadcn components only when justified by project conventions.
- Does not edit
components/ui/*directly.
Guardrails
1. Follow the existing React, Next.js, TypeScript, and FSD-lite architecture in the target project. 2. Use shadcn/ui primitives and wrappers for UI work; treat components/ui/* as read-only. 3. Keep server/client boundaries explicit: Server Components for static/layout work, Client Components for interaction and hooks. 4. Use project sources of truth for design tokens, i18n strings, and shared utilities before adding local alternatives. 5. Run the execution checklist before handoff and include relevant verification results. 6. Self-describing file names: every new file follows the File Naming convention in ../../rules/frontend.md §Naming Conventions — domain + role readable from the basename alone (order-summary-card.tsx, use-order-polling.ts, cart.atoms.ts). Grab-bag names (utils.ts, helpers.ts, misc.ts) and version suffixes (*-v2, *-final) are banned. 7. Next.js 16 `proxy.ts` is mandatory; `middleware.ts` is BANNED: this project is Next.js 16+. middleware.ts is NOT "deprecated"; it is forbidden, touch it and you die. The canonical request-proxy / auth-gate file is proxy.ts (root or src/) exporting a proxy function. NEVER create, recommend, suggest, or "restore" middleware.ts. NEVER flag proxy.ts as dead code, unused, or not-wired. Any such finding is a fatal self-error: retract it immediately and write proxy.ts.
Libraries
| Category | Library |
|---|---|
| Framework | next@16+ (App Router) + react@19+; next < 16 is BANNED |
| Date | luxon |
| Styling | TailwindCSS v4 + shadcn/ui |
| Hooks | ahooks (pre-made hooks preferred) |
| Utils | es-toolkit (first choice) |
| State (URL) | nuqs |
| State (Server) | TanStack Query |
| State (Client) | Jotai (minimize use) |
| Forms | @tanstack/react-form + zod |
| Auth | better-auth (client SDK only; never import server library or database adapters) |
| Animation | motion; import from motion/react. framer-motion (legacy package name) is BANNED. |
Shadcn Workflow
1. Search: shadcn_search_items_in_registries 2. Review: shadcn_get_item_examples_from_registries 3. Install: shadcn_get_add_command_for_items
Server vs Client Components
- Server Components: Layouts, marketing pages, SEO metadata (
generateMetadata,sitemap) - Client Components: Interactive features and
useQueryhooks
UI Implementation (Shadcn/UI)
- Usage: Prefer strict shadcn primitives (
Card,Sheet,Typography,Table) overdivor generic classes. - Responsiveness: Use
Drawer(mobile) vsDialog(desktop) viauseResponsive. - Customization: Treat
components/ui/*as read-only. Create wrappers (e.g.,components/common/ProductButton.tsx) or usecvacomposition. Never editcomponents/ui/button.tsxdirectly.
Sources of Truth
- DESIGN.md (project root): visual system source of truth; read Section 9 (Agent Prompt Guide) verbatim for component prompts when present
- Design Tokens:
packages/design-tokens(OKLCH); never hardcode colors - i18n strings:
packages/i18n; never hardcode UI text - Custom utilities: check
es-toolkitfirst; if implementing custom logic, >90% unit test coverage is mandatory
Designer Collaboration
- Sync: Map code variables to Figma layer names
- UX: Ensure key actions are visible "Above the Fold"
Stack Reference
Project stack conventions live in dedicated files. Read these before coding; they are not optional appendix material.
| File | Owns |
|---|---|
resources/tech-stack.md | Framework versions, Next.js 16 proxy.ts conventions, Serena shortcuts |
resources/tailwind-rules.md | Design tokens, focus states, Tailwind v4 @theme syntax |
resources/snippets.md | React 19 hook patterns, TanStack Query/Form, a11y card |
To extend: add resources/<name>.md and append a row above.
References
1. Follow resources/execution-protocol.md step by step. 2. Before submitting, run resources/checklist.md. Vendor-specific execution protocols are injected automatically by oma agent:spawn. Source files live under ../_shared/runtime/execution-protocols/{vendor}.md.
- Project frontend rules (MUST load before review/implementation):
../../rules/frontend.md - Execution steps:
resources/execution-protocol.md - Checklist:
resources/checklist.md - Error recovery:
resources/error-playbook.md - Context loading:
../_shared/core/context-loading.md - Reasoning templates:
../_shared/core/reasoning-templates.md - Clarification:
../_shared/core/clarification-protocol.md - Context budget:
../_shared/core/context-budget.md - Lessons learned:
../_shared/core/lessons-learned.md - Observability handoff:
../oma-observability/SKILL.md§Integrations — Core Web Vitals, SSR→client trace propagation, INP profiling
[!IMPORTANT]
Treat components/ui/* as read-only. Create wrappers for customization.Frontend Agent - Self-Verification Checklist
Run through every item before submitting your work.
Next.js 16 Conventions
- [ ] Request-proxy/auth-gate file is named
proxy.ts(NOTmiddleware.ts) and exportsproxy(NOTmiddleware) - [ ] Do NOT flag
src/proxy.tsas dead code or recommend renaming tomiddleware.ts;proxy.tsis the canonical Next.js 16+ convention - [ ] Config flags use the
Proxyform (e.g.skipProxyUrlNormalize), not the legacyMiddlewareform
TypeScript
- [ ] Strict mode, no
anytypes - [ ] Explicit interfaces for all component props
- [ ] No TypeScript errors (
npx tsc --noEmit)
Styling
- [ ] Tailwind CSS only (no inline styles, no CSS modules)
- [ ] Responsive at 320px, 768px, 1024px, 1440px
- [ ] Dark mode supported (if project uses it)
- [ ] No hardcoded colors (use Tailwind theme tokens)
Accessibility (WCAG 2.2 AA)
- [ ] Semantic HTML elements (
<nav>,<main>,<button>) - [ ] All images have alt text
- [ ] Color contrast >= 4.5:1 (normal text), >= 3:1 (large text)
- [ ] Keyboard navigation works for all interactive elements
- [ ] ARIA labels on non-obvious interactive elements
- [ ] Focus indicators visible
UX States
- [ ] Loading state (skeleton or spinner)
- [ ] Error state (user-friendly message + retry action)
- [ ] Empty state (helpful message + CTA)
- [ ] Optimistic updates where appropriate
Deferred / Stub Markers (transparency)
- [ ] Every deferred external integration is wrapped in
// TODO(oma-deferred): <vendor or feature> - [ ] Every mock data block has
// MVP: replace with <real-source>on top - [ ] Every save/persist UI has a working storage adapter (localStorage, IndexedDB, or API). Empty
onClick={() => console.log("save")}does NOT count; wire actual persistence, or mark explicitly:// TODO(oma-deferred): persist <state> to <storage> - [ ] Reload-after-save round-trips state for any save action surfaced in the UI
React 19 hook patterns (catch what eslint catches)
React 19 + recent eslint-plugin-react-hooks flag these as hard errors. They aren't style nits; they break runtime behavior. Avoid them at write-time, not "fix later".
- [ ] Don't put `setState` synchronously inside `useEffect` (
react-hooks/set-state-in-effect). Cascading renders trash performance and, in graphics contexts, can exhaust GPU resources. Replace with: derive in render (const count = items.length), lazyuseStateinitializer (useState(() => computeOnce())), move the update into the event handler that triggered it, oruseSyncExternalStorefor external subscriptions. - [ ] Don't read `ref.current` during render (
react-hooks/refs). Refs are null on first render and don't trigger re-renders when they attach, so any JSX gated onref.currentis wrong. Read refs only inside event handlers or effects. When a child component needs an instance, pass the ref object itself (or use a callback ref); let the child read.currentfrom inside its own effect/handler. - [ ] `useEffect` deps complete and stable. List every referenced reactive value. Wrap object/array literals in
useMemoso the dep identity doesn't change every render. - [ ] No prop-mirroring via effect.
useEffect(() => setX(derive(props)))is always a bug; computexin the render body or memoize.
react-three-fiber specifics
- [ ] Heavy components (
Canvas, drei helpers, anything that toucheswindow/WebGL) load vianext/dynamicwithssr: false. - [ ] Effects, helpers, and post-process passes mount unconditionally on the relevant state; don't gate them on
ref.currenttruthiness in JSX. - [ ] Background and lighting work without remote assets when the project doesn't ship a CDN HDR. Solid
<color attach="background">or local textures beat a<Environment preset>that depends on network. - [ ] Shadow map sizes scale to the number of lights; default to 1024 unless the scene needs more. Multiple 2048-shadowed lights drain GPU memory on integrated graphics.
Performance
- [ ] No unnecessary re-renders (check with React DevTools Profiler)
- [ ] Code splitting for route-level components
- [ ] Images optimized and lazy-loaded
Testing
- [ ] Unit tests for components with logic
- [ ] User interactions tested (click, type, submit)
- [ ] Async behavior tested (loading -> data -> display)
/**
* Component Template for Frontend Agent
*
* This is a reference template for creating new components.
* Follow this structure for consistency.
*/
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
// 1. Type Definitions
interface ComponentNameProps {
className?: string;
children: ReactNode;
// Add specific props here
variant?: "default" | "primary" | "secondary";
size?: "sm" | "md" | "lg";
onClick?: () => void;
disabled?: boolean;
}
// 2. Main Component
export function ComponentName({
className,
children,
variant = "default",
size = "md",
onClick,
disabled = false,
}: ComponentNameProps) {
// 3. Hooks (if needed)
// const [state, setState] = useState();
// 4. Effects (if needed)
// useEffect(() => {}, []);
// 5. Event Handlers
const handleClick = () => {
if (disabled) return;
onClick?.();
};
// 6. Computed Values
const classes = cn(
// Base styles
"inline-flex items-center justify-center rounded-md font-medium transition-colors",
// Variants
{
"bg-primary text-primary-foreground hover:bg-primary/90":
variant === "primary",
"bg-secondary text-secondary-foreground hover:bg-secondary/80":
variant === "secondary",
"bg-background text-foreground hover:bg-accent": variant === "default",
},
// Sizes
{
"h-8 px-3 text-sm": size === "sm",
"h-10 px-4 text-base": size === "md",
"h-12 px-6 text-lg": size === "lg",
},
// States
{
"opacity-50 cursor-not-allowed": disabled,
},
className,
);
// 7. Render
return (
<button
type="button"
className={classes}
onClick={handleClick}
disabled={disabled}
aria-disabled={disabled}
>
{children}
</button>
);
}
// 8. Sub-components (if needed)
ComponentName.Slot = function ComponentNameSlot({
className,
children,
}: ComponentNameProps) {
return (
<div className={cn("flex items-center gap-2", className)}>{children}</div>
);
};
// 9. Display Name (for debugging)
ComponentName.displayName = "ComponentName";
Frontend Agent - Error Recovery Playbook
When you encounter a failure, find the matching scenario and follow the recovery steps. Do NOT stop or ask for help until you have exhausted the playbook.
---
False Positive: proxy.ts flagged as dead code or middleware.ts demanded
Symptoms: Reviewer claims src/proxy.ts won't be picked up, demands rename to src/middleware.ts, or flags the auth gate as not wired.
1. STOP. This is a fatal self-error. Next.js 16+ renamed middleware.ts → proxy.ts. 2. The canonical request-proxy / auth-gate file is proxy.ts (root or src/) exporting a proxy function. 3. middleware.ts is BANNED in this project, not "deprecated", BANNED. Touch it and you die. 4. Verify by checking package.json for next@16+; proxy.ts is the only correct answer. 5. Reference: https://nextjs.org/docs/messages/middleware-to-proxy 6. Retract the finding. Do NOT raise this as critical / security / medium / low at any severity.
---
TypeScript Compilation Error
Symptoms: TS2322, TS2345, Type X is not assignable to type Y
1. Read the error: which file, which line, which types conflict 2. Check: is the interface/type definition correct? 3. Check: is the API response type matching the expected shape? 4. If API mismatch: update the type to match actual response (don't cast with as any) 5. If generic issue: use explicit type parameter <Type> instead of inference 6. NEVER do this: @ts-ignore, as any (hides type issues without resolving them)
---
Build Error
Symptoms: next build fails, Module not found, SyntaxError
1. Read the full error: which module, which file 2. If missing dependency: note in result as "requires npm install X"; do NOT install yourself 3. If import path wrong: use search_for_pattern("export.*ComponentName") to find actual path 4. If dynamic import issue: ensure component is client-side ('use client') 5. Re-run build after fix to confirm
---
Test Failure
Symptoms: vitest FAILED, expect(X).toBe(Y) assertion errors
1. Read the error: expected vs received, which test file 2. find_symbol("ComponentName") to check current implementation 3. Determine: test outdated or implementation wrong?
- Test expects old behavior → update test
- Component bug → fix component
4. Re-run the specific test: npx vitest run path/to/test.ts 5. After 3 failures: Try a different approach. Record in progress
---
Hydration Mismatch (Next.js)
Symptoms: Hydration failed, Text content does not match server-rendered HTML
1. Find the component that renders differently on server vs client 2. Common causes:
Date.now()orMath.random()in render- Browser-only APIs (
window,localStorage) withoutuseEffect - Conditional rendering based on client-only state
3. Fix: wrap client-only code in useEffect + state, or use 'use client' 4. If third-party component: wrap with dynamic(() => import(...), { ssr: false })
---
API Integration Error
Symptoms: Network Error, CORS, 401 Unauthorized, wrong data shape
1. CORS: Check backend CORS config; is frontend origin allowed? 2. 401: Check token; is it in the header? is it expired? 3. Wrong data: Log response.data and compare with expected type 4. Network Error: Is the backend running? Correct port? 5. If backend isn't your responsibility: document the expected API contract in result
---
Styling / Layout Broken
Symptoms: Component renders but looks wrong, responsive breakpoint fails
1. Check Tailwind classes: typo? wrong breakpoint prefix? 2. Check parent container: is it blocking layout? (overflow-hidden, fixed width) 3. Test at specific breakpoints: 320px, 768px, 1024px, 1440px 4. Use browser DevTools to inspect computed styles 5. If dark mode issue: check dark: variants applied
---
Rate Limit / Quota Error (Gemini API)
Symptoms: 429, RESOURCE_EXHAUSTED, rate limit exceeded
1. Stop immediately: do not make additional API calls 2. Save current work to progress-{agent-id}[-{sessionId}].md 3. Record Status: quota_exceeded in result-{agent-id}[-{sessionId}].md 4. Specify remaining tasks
---
Serena Memory Unavailable
1. Retry once 2. If 2 consecutive failures: use local file /tmp/progress-{agent-id}[-{sessionId}].md 3. Add memory_fallback: true flag to result
---
General Principles
- After 3 failures: If same approach fails 3 times, must try a different method
- Blocked: If no progress after 5 turns, save current state and record
Status: blocked - Out of scope: If you find backend issues, only record in result; do not modify directly
Frontend Agent - Examples
Example 1: Feature Component
Input: "Create a todo list page with add, toggle, and delete"
Output:
## Task: Todo List Page
### Implementation
- Components: TodoList, TodoItem, AddTodoForm
- Routes: /todos (page)
- State: TanStack Query for server state, Jotai for client state
### Files Created/Modified
- src/types/todo.ts (NEW) - Todo interface
- src/lib/api/todos.ts (NEW) - API client with TanStack Query hooks
- src/components/todo/todo-item.tsx (NEW) - Single todo display
- src/components/todo/add-todo-form.tsx (NEW) - Form with validation
- src/components/todo/todo-list.tsx (NEW) - List container
- src/app/todos/page.tsx (NEW) - Page component
- tests/components/todo-list.test.tsx (NEW)
### Testing
- Unit tests: 5 passing
- Keyboard navigation: verified
- Responsive: tested at 4 breakpointsExample 2: Form with Validation
Input: "Create a user profile edit form"
Output:
## Task: Profile Edit Form
### Implementation
- Components: ProfileForm (TanStack Form + Zod)
- Validation: name (required, 2-50 chars), email (valid format), bio (optional, max 500)
- API: PATCH /api/users/me via TanStack Query mutation
- UX: optimistic update, error toast, success redirect
### Files Created
- src/types/user.ts
- src/lib/api/users.ts
- src/components/profile/profile-form.tsx
- src/app/profile/edit/page.tsx
- tests/components/profile-form.test.tsxExample 3: Responsive Dashboard
Input: "Build a dashboard with stats cards and a chart"
Output:
## Task: Dashboard Page
### Implementation
- Components: StatCard, StatsGrid, ActivityChart
- Layout: CSS Grid - 1 col (mobile), 2 col (tablet), 4 col (desktop)
- Chart: recharts with responsive container
- Data: TanStack Query with 30s refetch interval
### Accessibility
- [x] All cards have aria-label with stat value
- [x] Chart has aria-describedby with text summary
- [x] Color contrast 4.5:1 on all text
### Files Created
- src/components/dashboard/stat-card.tsx
- src/components/dashboard/stats-grid.tsx
- src/components/dashboard/activity-chart.tsx
- src/app/dashboard/page.tsxFrontend Agent - Execution Protocol
Step 0: Prepare
1. Assess difficulty: see ../../_shared/core/difficulty-guide.md
- Simple: Skip to Step 3 | Medium: All 4 steps | Complex: All steps + checkpoints
2. Check lessons: read your domain section in ../../_shared/core/lessons-learned.md 3. Clarify requirements: follow ../../_shared/core/clarification-protocol.md
- Check Uncertainty Triggers: business logic, security/auth, existing code conflicts?
- Determine level: LOW → proceed | MEDIUM → present options | HIGH → ask immediately
4. Budget context: follow ../../_shared/core/context-budget.md (read symbols, not whole files)
Intelligent Escalation: When uncertain, escalate early. Don't blindly proceed.
Follow these steps in order (adjust depth by difficulty).
Step 1: Analyze
- Read the task requirements carefully
- Read
DESIGN.mdif present at the project root; treat Section 9 (Agent Prompt Guide) as authoritative component spec - Identify which components, pages, and hooks are needed
- Check existing code with Serena:
get_symbols_overview("src/components"),find_symbol("ComponentName") - Review existing patterns:
find_referencing_symbols("Button")to understand usage conventions - List assumptions; ask if unclear
Step 2: Plan
- Decide on component structure (which are new, which extend existing)
- Define props interfaces with TypeScript
- Plan state management approach (Jotai for client, nuqs for URL, TanStack Query for server)
- Identify API integration points (TanStack Query hooks)
- Plan responsive breakpoints and accessibility requirements
Step 3: Implement
- Create/modify files in this order:
1. TypeScript types/interfaces 2. API client hooks (TanStack Query) 3. Reusable UI components (shadcn/ui based) 4. Feature components (compose UI + logic) 5. Page components (route-level) 6. Tests (unit + integration)
- Follow
resources/tailwind-rules.mdfor styling
Step 4: Verify
- Run
resources/checklist.mditems - Run
../../_shared/core/common-checklist.mditems - Check TypeScript strict mode: no errors
- Verify responsive design at 320px, 768px, 1024px, 1440px
- Test keyboard navigation and screen reader compatibility
On Error
See resources/error-playbook.md for recovery steps.
Frontend Agent - Code Snippets
Copy-paste ready patterns. Use these as starting points, adapt to the specific task.
---
Next.js 16 framework canonicals (use these, never the legacy alternatives)
// Internal nav: <Link>, never <a href="/...">
import Link from "next/link";
<Link href="/gallery" className="...">View gallery</Link>
// Custom font: next/font, never <link rel="stylesheet">
import { Inter } from "next/font/google";
const inter = Inter({ subsets: ["latin"] });
<body className={inter.className}>...</body>
// Images: next/image, never raw <img>
import Image from "next/image";
<Image src="/hero.png" alt="Hero scene" width={1200} height={600} priority />
// Imports: only what you use. After refactoring, remove orphans.
// useCallback / useEffect deps: list every referenced symbol exactly.---
Accessible Card (focus ring + semantic + keyboard)
Baseline for an interactive surface. Adjust colors via theme tokens; verify the resulting contrast ratio against the actual --card / --foreground / --muted-foreground values (theme tokens alone do NOT guarantee 4.5:1; the designer / token system has to make them so).
interface CardProps {
title: string;
description?: string;
onClick?: () => void;
}
export function Card({ title, description, onClick }: CardProps) {
return (
<button
type="button"
onClick={onClick}
className={[
"rounded-lg border bg-card p-4 text-left shadow-sm transition-colors",
"hover:bg-accent",
// visible focus indicator (WCAG 2.4.7 Focus Visible, 2.4.11 Focus Not Obscured)
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
].join(" ")}
>
{/* Visible text inside the button is the accessible name —
do NOT add aria-label here, that would override and hide the
description from screen readers. */}
<span className="block text-lg font-semibold text-foreground">{title}</span>
{description && (
<span className="mt-1 block text-sm text-muted-foreground">
{description}
</span>
)}
</button>
);
}Accessibility checklist for interactive surfaces:
- Semantic element:
<button>,<a>,<Link>. Never<div onClick>. - Keyboard reachable: implicit when using semantic elements.
- Visible focus:
focus-visible:ring-2 ring-offset-2(or equivalent). Never strip the outline without replacing it. - Accessible name: visible text inside the element IS the name. Add
aria-labelONLY for icon-only buttons (e.g.,<button aria-label="Close">×</button>); when visible text exists,aria-labeloverrides it and is an anti-pattern. - Contrast: verify the actual color values against background reach 4.5:1 (normal text) or 3:1 (large text >= 18pt or 14pt bold). Run an axe/Lighthouse pass; theme tokens are not a proof.
- Heading semantics: keep heading tags (
<h1>-<h6>) outside interactive elements. Inside a button, use<span>with type-scale classes; promote to a heading at the surrounding section level.
---
React 19 hook patterns
// Derive in render — no state, no effect
function ItemCount({ items }: { items: Item[] }) {
const count = items.length;
return <span>{count}</span>;
}
// Initialize once with a lazy initializer
const [id] = useState(() => crypto.randomUUID());
// Pass the ref OBJECT (not its current value) to children that need an instance.
// The child reads .current inside its own effect or event handler, never in render.
function Selectable({ targetRef }: { targetRef: React.RefObject<THREE.Object3D> }) {
// ...
}
// Never: useEffect that calls setState to mirror props/state
// eslint: react-hooks/set-state-in-effect
// useEffect(() => { setCount(items.length); }, [items]);
// Never: gate JSX on ref.current — it is null on first render, and refs
// do not trigger re-renders when they attach
// eslint: react-hooks/refs
// return ref.current ? <Child target={ref.current} /> : null;TanStack Query Hook
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
interface Todo {
id: string;
title: string;
completed: boolean;
}
export function useTodos() {
return useQuery<Todo[]>({
queryKey: ["todos"],
queryFn: async () => {
const res = await fetch("/api/todos", {
headers: { Authorization: `Bearer ${getToken()}` },
});
if (!res.ok) throw new Error("Failed to fetch");
return res.json();
},
});
}
export function useCreateTodo() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: { title: string }) => {
const res = await fetch("/api/todos", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${getToken()}`,
},
body: JSON.stringify(data),
});
if (!res.ok) throw new Error("Failed to create");
return res.json();
},
onSuccess: () => queryClient.invalidateQueries({ queryKey: ["todos"] }),
});
}---
Form with TanStack Form + Zod
"use client";
import { useForm } from "@tanstack/react-form";
import { zodValidator } from "@tanstack/zod-form-adapter";
import { z } from "zod";
const schema = z.object({
email: z.string().email("Invalid email"),
password: z.string().min(8, "At least 8 characters"),
});
export function LoginForm({ onSubmit }: { onSubmit: (data: z.infer<typeof schema>) => void }) {
const form = useForm({
defaultValues: { email: "", password: "" },
validatorAdapter: zodValidator(),
validators: { onChange: schema },
onSubmit: async ({ value }) => onSubmit(value),
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
className="space-y-4"
>
<form.Field name="email">
{(field) => (
<div>
<label htmlFor="email" className="text-sm font-medium">
Email
</label>
<input
id="email"
type="email"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className="mt-1 w-full rounded-md border px-3 py-2"
aria-invalid={field.state.meta.errors.length > 0}
/>
{field.state.meta.errors.length > 0 && (
<p className="mt-1 text-sm text-destructive">{field.state.meta.errors[0]}</p>
)}
</div>
)}
</form.Field>
<form.Field name="password">
{(field) => (
<div>
<label htmlFor="password" className="text-sm font-medium">
Password
</label>
<input
id="password"
type="password"
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
onBlur={field.handleBlur}
className="mt-1 w-full rounded-md border px-3 py-2"
aria-invalid={field.state.meta.errors.length > 0}
/>
{field.state.meta.errors.length > 0 && (
<p className="mt-1 text-sm text-destructive">{field.state.meta.errors[0]}</p>
)}
</div>
)}
</form.Field>
<form.Subscribe selector={(state) => [state.canSubmit, state.isSubmitting]}>
{([canSubmit, isSubmitting]) => (
<button
type="submit"
disabled={!canSubmit}
className="w-full rounded-md bg-primary px-4 py-2 text-primary-foreground disabled:opacity-50"
>
{isSubmitting ? "Signing in..." : "Sign in"}
</button>
)}
</form.Subscribe>
</form>
);
}---
Loading / Error / Empty States
interface AsyncStateProps<T> {
data: T | undefined;
isLoading: boolean;
error: Error | null;
empty: React.ReactNode;
children: (data: T) => React.ReactNode;
}
export function AsyncState<T>({ data, isLoading, error, empty, children }: AsyncStateProps<T>) {
if (isLoading) return <div className="flex justify-center p-8"><Spinner /></div>;
if (error) return <ErrorCard message={error.message} />;
if (!data || (Array.isArray(data) && data.length === 0)) return <>{empty}</>;
return <>{children(data)}</>;
}---
Responsive Grid Layout
export function StatsGrid({ children }: { children: React.ReactNode }) {
return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
{children}
</div>
);
}---
Vitest Component Test
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi } from "vitest";
import { Card } from "./Card";
describe("Card", () => {
it("renders title and description", () => {
render(<Card title="Test" description="Desc" />);
expect(screen.getByText("Test")).toBeInTheDocument();
expect(screen.getByText("Desc")).toBeInTheDocument();
});
it("calls onClick when clicked", async () => {
const onClick = vi.fn();
render(<Card title="Test" onClick={onClick} />);
await userEvent.click(screen.getByText("Test"));
expect(onClick).toHaveBeenCalledOnce();
});
});Tailwind CSS Rules
Basic Tailwind syntax (spacing scale, flex/grid, breakpoints) is assumed knowledge. This file only covers project-specific rules and non-obvious patterns. See snippets.md for ready-made component snippets.Core Rules
1. No inline styles: Tailwind classes only 2. Mobile-first: base classes target mobile, scale up with sm: md: lg: 3. No hardcoded colors: always use semantic tokens (see below) 4. Dark mode aware: every color token must work under dark: variant 5. Arbitrary values only with justification: w-[789px] needs a design reason; otherwise use the scale
Semantic Color Tokens
Map all colors to design tokens in packages/design-tokens. Never use raw colors like bg-white or text-gray-900.
// Good
<div className="bg-background text-foreground border-border">
<button className="bg-primary text-primary-foreground">
// Bad
<div className="bg-white text-black border-gray-300">Available tokens:
background/foregroundprimary/primary-foregroundsecondary/secondary-foregroundmuted/muted-foregroundaccent/accent-foregrounddestructive/destructive-foregroundborder/input/ring
cn() Utility
Use cn() (clsx + tailwind-merge) whenever classes are conditional or need to be overridable from a prop:
import { cn } from "@/lib/utils";
<div className={cn(
"base-class",
condition && "conditional-class",
{ "variant-a": variant === "a" },
className, // prop override must come last
)} />Focus States (A11y)
Every interactive element needs a visible focus state:
className="focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2"Screen-reader-only text: <span className="sr-only">...</span>.
Tailwind v4 Theme
Tailwind v4 uses CSS-based configuration. tailwind.config.js is deprecated. Add custom tokens via @theme in app/globals.css:
@import "tailwindcss";
@theme {
--spacing-18: 4.5rem;
--color-brand-50: #...;
--color-brand-900: #...;
}Anti-Patterns
- `@apply` in CSS files: defeats utility-first intent. Use classes in JSX, or extract a component.
- Hardcoded hex/rgb: always go through the token system.
- Desktop-first breakpoints:
grid-cols-3 sm:grid-cols-1reverses the mobile-first flow. - Nested overrides: if you find yourself writing
lg:!bg-primary, the component API is wrong; add a variant instead.
Formatting
Follow whatever class-sorting convention the project's formatter enforces (Prettier + prettier-plugin-tailwindcss, Biome's useSortedClasses, or similar). Do not hand-reorder against the formatter.
Frontend Agent - Tech Stack Reference
Core Framework
- Framework: Next.js 16+ (App Router), React 19+
- Language: TypeScript (strict mode)
- Testing: Vitest, React Testing Library, Playwright
Next.js 16 Conventions
Proxy replaces Middleware
middleware.ts is BANNED in this project. It is NOT merely deprecated; touch it and you die. No exceptions.
- File:
middleware.ts→proxy.ts(root orsrc/) - Exported function:
middleware→proxy - Config flags:
skipMiddlewareUrlNormalize→skipProxyUrlNormalize, etc. src/proxy.tsis the canonical request-proxy / auth-gate location
Forbidden actions (any of these is a fatal self-error; retract immediately):
- Creating a new
middleware.ts - Suggesting a rename of
proxy.tsback tomiddleware.ts - Flagging
proxy.tsas dead code, unused, or not-wired
Reference: https://nextjs.org/docs/messages/middleware-to-proxy
Serena MCP Shortcuts
find_symbol("ComponentName"): locate existing componentget_symbols_overview("src/components"): list all componentsfind_referencing_symbols("Button"): find usages before changes