SKILL.md
UI Engineering Stack Rules
Consistency is what makes a codebase read as authored instead of generated. These rules pick one way to do each thing. Load them before writing React; follow the repo's existing choices when they differ, these are defaults, not a mandate to rewrite.
The stack
| Layer | Default |
|---|---|
| Framework | Next.js App Router, React 19, Server Components by default |
| Language | TypeScript, strict: true, no any |
| Styling | Tailwind with CSS-variable design tokens, cn() for composition |
| Components | shadcn/ui (owned source in components/ui/) on Radix, cva variants |
| Motion | motion (import from motion/react), reduced-motion respected |
| Data | Server Components and server actions; client store only for real client state |
| Package manager | bun (bunx shadcn@latest add ...) |
Server vs client components
- Default to Server Components. They fetch data and ship no JS.
- Add
"use client"only for interactivity: hooks, event handlers, browser APIs, Motion, R3F. - Push the client boundary down the tree. The page stays server; the leaf button goes client. Never mark a page client because one element needs
onClick. - Fetch in Server Components or server actions. No
useEffectfetching for what the server can render. - Secrets stay server-side. Never import server-only modules into client files.
Files and naming
- One component per file,
PascalCase.tsx, helpers colocated. - Hooks:
useThing.tsinhooks/. Utilities:lib/(lib/utils.tsholdscn()). - Routes follow App Router:
app/segment/page.tsx,layout.tsx,loading.tsx,error.tsx. - Directories kebab-case, types PascalCase, constants UPPER_SNAKE_CASE.
- Named exports; default-export only where the router requires it.
TypeScript
- No
any. Useunknownplus narrowing, generics, or a real type. - Props get an explicit
interfaceortype; derive variant props viaVariantProps<typeof variants>. - Validate domain data with zod at the boundary; infer types with
z.infer. - Discriminated unions over boolean-flag soup for component states.
Styling
- Tokens only:
bg-background,text-foreground,border-border. Never raw palette classes likebg-slate-900. Restyling happens in tokens, not components. - Compose through
cn(); spread incomingclassNamelast so callers can override. - New looks are new
cvavariants on the existing component, never a forked copy. - Mobile-first: base styles, then
sm:/md:/lg:. Check ~375px and ~1440px. - Density is a decision. Real products are not uniformly
p-8 gap-6.
Avoiding the generic AI look
- No unmotivated purple gradients, oversized glassy cards, or centered-everything heroes.
- One strong accent from the token set beats a rainbow.
- A real type scale and one consistent
--radiusread as designed; random sizes read as generated. - One signature motion or 3D moment per section, not effect soup (premium-motion-ui covers the craft).
Good vs bad
Bad: A dashboard page marked "use client" at the top, data fetched in useEffect, cards styled with bg-slate-800 rounded-2xl p-8, a copied ButtonSecondary.tsx that is Button with different colors.
Good: Server page fetches via a server action, one "use client" leaf for the filter dropdown, cards on bg-card border-border, the secondary look added as a variant: "secondary" entry in Button's cva map.
Verification
Run bunx tsc --noEmit && bun run lint && bun run build. Expect all three to exit 0 with no new warnings. Then grep for drift: grep -rn "bg-slate-\|bg-gray-\|text-white " app/ components/ | grep -v components/ui should return nothing, and grep -rln '"use client"' app/**/page.tsx should return nothing. Any hit: fix the offender before calling the surface done, then hand off to browser-visual-qa for the responsive and theme matrix.
Completion checklist
- [ ] Server Components by default; client boundary pushed to leaves
- [ ] No
any; props typed; zod at data boundaries - [ ] Semantic tokens only;
classNamemerged last viacn() - [ ] Variants in
cva, no forked components - [ ] Naming and file layout match the repo
- [ ] Empty, loading, and error states exist and look intentional
- [ ] tsc, lint, build clean; a11y and browser QA handed off
Any box unchecked: not done. Fix or say so.
Footguns
- The page-level `"use client"`. One interactive widget drags the whole route to client rendering and kills the server data path. Extract the widget instead.
- Raw palette classes "just this once". The first
bg-slate-900breaks theme switching for that surface and invites the second. Tokens or nothing. - Forking a component to restyle it. Two sources of truth drift immediately. Add a variant; if the component cannot express it, fix the component (shadcn-component-architecture).
- Repo mismatch. Applying these defaults to a Vite or npm repo creates a second convention. The repo's existing choices win; these rules fill the gaps.