```
```tsx
// Don't: External fonts (unreliable)
fontFamily: "'Custom Font', sans-serif"
// Do: System font stack
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
```
```tsx
// Don't: Dark mode CSS (limited support)
@media (prefers-color-scheme: dark)
// Do: Light theme only, or use Resend's dark mode support
```
================================================================================
## integrations/openrouter.md
================================================================================
# OpenRouter Integration
OpenRouter provides access to multiple AI models (Claude, GPT-4, Gemini, Llama, etc.) through a single API, with automatic fallbacks and cost optimization.
## Default Model
**Gemini 2.5 Flash** (`google/gemini-2.5-flash`) is the default for new projects:
- Pricing: $0.30/M input, $2.50/M output
- Context: 1,048,576 tokens (1M)
- Multimodal: text, image, audio, video, file input
- Built-in reasoning/thinking capabilities
Model choice is project-specific—switch based on requirements (coding, reasoning, speed, cost).
## Installation
```bash
pnpm add @openrouter/ai-sdk-provider ai
```
## Basic Setup
### Environment Variables
```bash
OPENROUTER_API_KEY=sk-or-v1-...
```
### Provider Configuration
```ts
// lib/ai.ts
import { createOpenRouter } from "@openrouter/ai-sdk-provider";
export const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
});
```
## Usage with Vercel AI SDK
### Text Generation
```ts
import { generateText } from "ai";
import { openrouter } from "@/lib/ai";
const { text } = await generateText({
model: openrouter("google/gemini-2.5-flash"),
prompt: "Explain quantum computing in simple terms",
});
```
### Streaming
```ts
import { streamText } from "ai";
import { openrouter } from "@/lib/ai";
const result = await streamText({
model: openrouter("google/gemini-2.5-flash"),
messages: [
{ role: "user", content: "Write a haiku about programming" }
],
});
for await (const chunk of result.textStream) {
process.stdout.write(chunk);
}
```
### With Tools
```ts
import { generateText, tool } from "ai";
import { openrouter } from "@/lib/ai";
import { z } from "zod";
const { text, toolCalls } = await generateText({
model: openrouter("google/gemini-2.5-flash"),
tools: {
weather: tool({
description: "Get the current weather",
parameters: z.object({
location: z.string(),
}),
execute: async ({ location }) => {
return { temperature: 72, conditions: "sunny" };
},
}),
},
prompt: "What's the weather in San Francisco?",
});
```
## Next.js API Route Example
```ts
// app/api/chat/route.ts
import { streamText } from "ai";
import { openrouter } from "@/lib/ai";
export async function POST(req: Request) {
const { messages } = await req.json();
const result = await streamText({
model: openrouter("google/gemini-2.5-flash"),
messages,
});
return result.toDataStreamResponse();
}
```
## React Hook Usage
```tsx
"use client";
import { useChat } from "ai/react";
export function Chat() {
const { messages, input, handleInputChange, handleSubmit } = useChat({
api: "/api/chat",
});
return (
{messages.map((m) => (
{m.role}: {m.content}
))}
);
}
```
## Cost Optimization
OpenRouter automatically routes to the cheapest available model that matches your requirements. You can also set budget limits:
```ts
const openrouter = createOpenRouter({
apiKey: process.env.OPENROUTER_API_KEY,
headers: {
"HTTP-Referer": "https://yoursite.com",
"X-Title": "Your App Name",
},
});
```
## Fallback Models
OpenRouter handles fallbacks automatically, but you can specify preferences:
```ts
// Will try Gemini first, then fall back to Claude
const result = await generateText({
model: openrouter("google/gemini-2.5-flash", {
fallbacks: ["anthropic/claude-sonnet-4"],
}),
prompt: "Hello",
});
```
================================================================================
## integrations/fal-ai.md
================================================================================
# fal.ai Image Generation
fal.ai provides fast image generation APIs.
## Default Models
**GPT-Image 1.5** (`fal-ai/gpt-image-1.5`) - OpenAI's text-to-image model:
- Pricing: $0.009 (low) to $0.133 (high) per image
- Supports text-to-image and image editing
- Sizes: 1024x1024, 1024x1536, 1536x1024
**Gemini 2.5 Flash Image** (`fal-ai/gemini-25-flash-image`) - Google's image generation:
- Pricing: $0.039/image
- Aspect ratios: 1:1, 16:9, 4:3, etc.
## Installation
```bash
pnpm add @fal-ai/client
```
## Environment Variables
```bash
FAL_KEY=...
```
## Setup
```ts
// lib/fal.ts
import { fal } from "@fal-ai/client";
fal.config({
credentials: process.env.FAL_KEY,
});
export { fal };
```
## Basic Image Generation
### GPT-Image 1.5
```ts
import { fal } from "@/lib/fal";
const result = await fal.subscribe("fal-ai/gpt-image-1.5", {
input: {
prompt: "A serene mountain landscape at sunset",
image_size: "1024x1024",
num_images: 1,
},
});
const imageUrl = result.data.images[0].url;
```
### Gemini 2.5 Flash Image
```ts
const result = await fal.subscribe("fal-ai/gemini-25-flash-image", {
input: {
prompt: "Professional headshot of a business executive",
aspect_ratio: "1:1",
},
});
```
## Image Sizes
| Size | Dimensions | Use Case |
|------|------------|----------|
| `square` | 1024x1024 | Avatars, icons |
| `square_hd` | 1536x1536 | High-res squares |
| `portrait_4_3` | 768x1024 | Portraits |
| `portrait_16_9` | 576x1024 | Tall portraits |
| `landscape_4_3` | 1024x768 | Standard landscape |
| `landscape_16_9` | 1024x576 | Wide landscape |
## Advanced Options
```ts
const result = await fal.subscribe("fal-ai/gpt-image-1.5", {
input: {
prompt: "A cyberpunk city at night",
image_size: "1536x1024",
num_images: 4,
quality: "high", // "low", "medium", "high"
},
});
```
## Image Editing
GPT-Image 1.5 supports image editing:
```ts
const result = await fal.subscribe("fal-ai/gpt-image-1.5", {
input: {
prompt: "Add a cat sitting on the chair",
image_url: "https://example.com/room.jpg",
},
});
```
## Polling vs Subscription
```ts
// Subscription (recommended) - auto-polls until complete
const result = await fal.subscribe("fal-ai/gpt-image-1.5", {
input: { prompt: "..." },
logs: true,
onQueueUpdate: (update) => {
if (update.status === "IN_PROGRESS") {
console.log("Processing...", update.logs);
}
},
});
// Manual queue for long operations
const { request_id } = await fal.queue.submit("fal-ai/gpt-image-1.5", {
input: { prompt: "..." },
});
// Check status later
const status = await fal.queue.status("fal-ai/gpt-image-1.5", {
requestId: request_id,
});
// Get result when done
const result = await fal.queue.result("fal-ai/gpt-image-1.5", {
requestId: request_id,
});
```
## Next.js Server Action
```ts
// actions/generate-image.ts
"use server";
import { fal } from "@/lib/fal";
export async function generateImage(prompt: string) {
const result = await fal.subscribe("fal-ai/gpt-image-1.5", {
input: {
prompt,
image_size: "1024x1024",
num_images: 1,
},
});
return result.data.images[0].url;
}
```
## Rate Limiting
fal.ai has per-model rate limits. For production:
```ts
async function generateWithRetry(prompt: string, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fal.subscribe("fal-ai/gpt-image-1.5", {
input: { prompt },
});
} catch (error: any) {
if (error.status === 429 && i < maxRetries - 1) {
await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
continue;
}
throw error;
}
}
}
```
================================================================================
## integrations/voyage-embeddings.md
================================================================================
# Voyage AI Embeddings
Voyage AI provides state-of-the-art text embeddings optimized for semantic search and RAG applications.
## Installation
```bash
pnpm add voyage-ai-provider voyageai
```
## Environment Variables
```bash
VOYAGE_API_KEY=pa-...
```
## Basic Setup
### Provider Configuration
```ts
// lib/embeddings.ts
import { createVoyage } from "voyage-ai-provider";
export const voyage = createVoyage({
apiKey: process.env.VOYAGE_API_KEY,
});
```
### Direct Client (Alternative)
```ts
import VoyageAI from "voyageai";
const voyage = new VoyageAI({
apiKey: process.env.VOYAGE_API_KEY,
});
```
## Generating Embeddings
### With AI SDK Provider
```ts
import { embed, embedMany } from "ai";
import { voyage } from "@/lib/embeddings";
// Single text
const { embedding } = await embed({
model: voyage.textEmbeddingModel("voyage-3.5"),
value: "What is machine learning?",
});
// Multiple texts
const { embeddings } = await embedMany({
model: voyage.textEmbeddingModel("voyage-3.5"),
values: [
"What is machine learning?",
"How does neural networks work?",
"Explain deep learning",
],
});
```
### With Direct Client
```ts
const result = await voyage.embed({
input: ["Text to embed", "Another text"],
model: "voyage-3.5",
});
const embeddings = result.data.map((d) => d.embedding);
```
## Text Embedding Models
| Model | Dimensions | Best For |
|-------|------------|----------|
| `voyage-3-large` | 2048 | SOTA text, 9.74% better than OpenAI v3 large |
| `voyage-3.5` | 1024 | General purpose text |
| `voyage-3.5-lite` | 512 | Faster, lower cost |
| `voyage-code-3` | 1024 | Code retrieval |
| `voyage-finance-2` | 1024 | Financial documents |
| `voyage-law-2` | 1024 | Legal documents |
All models support dimension reduction (2048, 1024, 512, 256) via the `output_dimension` parameter.
## Multimodal Embeddings
For image and video search:
| Model | Best For |
|-------|----------|
| `voyage-multimodal-3` | Text + images, 41% better than CLIP on table/figure retrieval |
| `voyage-multimodal-3.5` | Text + images + video |
Multimodal models process interleaved text + images (unlike CLIP which separates them). 200M text tokens + 150B pixels free per account.
```ts
// Multimodal embedding with image
const { embedding } = await embed({
model: voyage.textEmbeddingModel("voyage-multimodal-3"),
value: {
content: [
{ type: "text", text: "Product description" },
{ type: "image_url", image_url: { url: "https://..." } },
],
},
});
```
## Database Storage (Neon + pgvector)
### Schema
```ts
// db/schema.ts
import { pgTable, text, vector, serial } from "drizzle-orm/pg-core";
export const documents = pgTable("documents", {
id: serial("id").primaryKey(),
content: text("content").notNull(),
embedding: vector("embedding", { dimensions: 1024 }),
});
```
### Enable pgvector
```sql
CREATE EXTENSION IF NOT EXISTS vector;
```
### Insert with Embedding
```ts
import { db } from "@/db";
import { documents } from "@/db/schema";
import { embed } from "ai";
import { voyage } from "@/lib/embeddings";
async function indexDocument(content: string) {
const { embedding } = await embed({
model: voyage.textEmbeddingModel("voyage-3.5"),
value: content,
});
await db.insert(documents).values({
content,
embedding,
});
}
```
### Semantic Search
```ts
import { sql } from "drizzle-orm";
import { db } from "@/db";
import { documents } from "@/db/schema";
import { embed } from "ai";
import { voyage } from "@/lib/embeddings";
async function search(query: string, limit = 10) {
const { embedding } = await embed({
model: voyage.textEmbeddingModel("voyage-3.5"),
value: query,
});
const results = await db
.select({
id: documents.id,
content: documents.content,
similarity: sql`1 - (${documents.embedding} <=> ${embedding})`,
})
.from(documents)
.orderBy(sql`${documents.embedding} <=> ${embedding}`)
.limit(limit);
return results;
}
```
## RAG Pattern
```ts
import { generateText } from "ai";
import { openrouter } from "@/lib/ai";
async function ragQuery(question: string) {
// 1. Find relevant documents
const relevantDocs = await search(question, 5);
// 2. Build context
const context = relevantDocs
.map((doc) => doc.content)
.join("\n\n---\n\n");
// 3. Generate answer with context
const { text } = await generateText({
model: openrouter("anthropic/claude-sonnet-4"),
system: `Answer questions based on the provided context. If the context doesn't contain relevant information, say so.`,
prompt: `Context:\n${context}\n\nQuestion: ${question}`,
});
return text;
}
```
## Batch Processing
For large datasets, batch embeddings efficiently:
```ts
async function batchEmbed(texts: string[], batchSize = 100) {
const allEmbeddings: number[][] = [];
for (let i = 0; i < texts.length; i += batchSize) {
const batch = texts.slice(i, i + batchSize);
const { embeddings } = await embedMany({
model: voyage.textEmbeddingModel("voyage-3.5"),
values: batch,
});
allEmbeddings.push(...embeddings);
// Rate limiting
if (i + batchSize < texts.length) {
await new Promise((r) => setTimeout(r, 100));
}
}
return allEmbeddings;
}
```
================================================================================
## integrations/uploadthing.md
================================================================================
# UploadThing
UploadThing provides simple, type-safe file uploads for Next.js with built-in CDN hosting.
## Installation
```bash
pnpm add uploadthing @uploadthing/react
```
## Environment Variables
```bash
UPLOADTHING_TOKEN=...
```
Get your token from [uploadthing.com](https://uploadthing.com).
## Server Setup
### File Router
```ts
// app/api/uploadthing/core.ts
import { createUploadthing, type FileRouter } from "uploadthing/next";
import { auth } from "@clerk/nextjs/server";
const f = createUploadthing();
export const ourFileRouter = {
// Image uploader with auth
imageUploader: f({ image: { maxFileSize: "4MB", maxFileCount: 4 } })
.middleware(async () => {
const { userId } = await auth();
if (!userId) throw new Error("Unauthorized");
return { userId };
})
.onUploadComplete(async ({ metadata, file }) => {
console.log("Upload complete for user:", metadata.userId);
console.log("File URL:", file.url);
return { uploadedBy: metadata.userId };
}),
// PDF uploader
pdfUploader: f({ pdf: { maxFileSize: "16MB" } })
.middleware(async () => {
const { userId } = await auth();
if (!userId) throw new Error("Unauthorized");
return { userId };
})
.onUploadComplete(async ({ file }) => {
return { url: file.url };
}),
// Any file type
fileUploader: f(["image", "pdf", "text"])
.middleware(async () => ({ userId: "anon" }))
.onUploadComplete(async ({ file }) => {
return { url: file.url };
}),
} satisfies FileRouter;
export type OurFileRouter = typeof ourFileRouter;
```
### Route Handler
```ts
// app/api/uploadthing/route.ts
import { createRouteHandler } from "uploadthing/next";
import { ourFileRouter } from "./core";
export const { GET, POST } = createRouteHandler({
router: ourFileRouter,
});
```
## Client Setup
### Generate Components
```ts
// lib/uploadthing.ts
import {
generateUploadButton,
generateUploadDropzone,
generateReactHelpers,
} from "@uploadthing/react";
import type { OurFileRouter } from "@/app/api/uploadthing/core";
export const UploadButton = generateUploadButton();
export const UploadDropzone = generateUploadDropzone();
export const { useUploadThing, uploadFiles } = generateReactHelpers();
```
## Usage
### Upload Button
```tsx
"use client";
import { UploadButton } from "@/lib/uploadthing";
export function ImageUpload() {
return (
{
console.log("Files:", res);
const urls = res.map((file) => file.url);
// Handle uploaded URLs
}}
onUploadError={(error) => {
console.error("Error:", error);
}}
/>
);
}
```
### Upload Dropzone
```tsx
"use client";
import { UploadDropzone } from "@/lib/uploadthing";
export function FileDropzone() {
return (
{
console.log("Uploaded:", res);
}}
onUploadError={(error) => {
console.error("Error:", error);
}}
appearance={{
container: "border-2 border-dashed border-gray-300 rounded-lg p-8",
uploadIcon: "text-gray-400",
label: "text-gray-600",
allowedContent: "text-gray-400 text-sm",
}}
/>
);
}
```
### Programmatic Upload
```tsx
"use client";
import { useUploadThing } from "@/lib/uploadthing";
export function CustomUpload() {
const { startUpload, isUploading } = useUploadThing("imageUploader", {
onClientUploadComplete: (res) => {
console.log("Uploaded:", res);
},
onUploadError: (error) => {
console.error("Error:", error);
},
});
const handleFileChange = async (e: React.ChangeEvent) => {
const files = e.target.files;
if (!files) return;
const result = await startUpload(Array.from(files));
console.log("Result:", result);
};
return (
{isUploading && Uploading... }
);
}
```
### Upload from Server Action
```ts
// lib/uploadthing-server.ts
import { UTApi } from "uploadthing/server";
export const utapi = new UTApi();
```
```ts
// actions/upload.ts
"use server";
import { utapi } from "@/lib/uploadthing-server";
export async function uploadFromUrl(url: string) {
const response = await utapi.uploadFilesFromUrl(url);
return response.data?.url;
}
export async function deleteFile(fileKey: string) {
await utapi.deleteFiles(fileKey);
}
```
## File Types Reference
```ts
// Common configurations
f({ image: { maxFileSize: "4MB" } }) // Images only
f({ pdf: { maxFileSize: "16MB" } }) // PDFs only
f({ video: { maxFileSize: "256MB" } }) // Videos only
f({ audio: { maxFileSize: "32MB" } }) // Audio only
f({ blob: { maxFileSize: "8MB" } }) // Any file type
f(["image", "pdf"]) // Multiple types
// With count limits
f({ image: { maxFileSize: "4MB", maxFileCount: 10 } })
// Custom mime types
f({ "image/png": { maxFileSize: "4MB" } })
```
## Styling
### Tailwind CSS
```tsx
```
### Hide Default Button Text
```tsx
```
================================================================================
## integrations/biome-ultracite.md
================================================================================
# Biome + Ultracite
Ultracite is a zero-config Biome preset that enforces strict code quality standards. Biome is the underlying Rust-based formatter and linter (replaces ESLint + Prettier).
## Why Biome?
| Feature | ESLint + Prettier | Biome |
|---------|-------------------|-------|
| Speed | ~5-10 seconds | ~50-200ms |
| Config files | 2+ files | 1 file |
| Dependencies | 10+ packages | 1 package |
| Language | JavaScript | Rust |
## Installation
```bash
pnpm add -D @biomejs/biome ultracite
```
## Configuration
### Option 1: Extend Ultracite (Recommended)
Create `biome.json`:
```json
{
"$schema": "https://biomejs.dev/schemas/2.0.5/schema.json",
"extends": ["ultracite"]
}
```
### Option 2: Initialize Fresh
```bash
pnpm exec ultracite init
```
This creates a `biome.json` with Ultracite defaults.
## Commands
### Ultracite CLI
```bash
# Check for issues (no changes)
pnpm exec ultracite check
# Fix all issues
pnpm exec ultracite fix
# Fix including unsafe changes
pnpm exec ultracite fix --unsafe
# Diagnose configuration
pnpm exec ultracite doctor
```
### Direct Biome CLI
```bash
# Lint and fix
pnpm exec biome check src --write
# Format only
pnpm exec biome format src --write
# Lint only (no format)
pnpm exec biome lint src --write
```
## Package.json Scripts
### Single App
```json
{
"scripts": {
"lint": "biome check src --write",
"format": "biome format src --write"
}
}
```
### Monorepo
```json
{
"scripts": {
"lint": "turbo run lint && biome check .",
"format": "ultracite fix"
}
}
```
## Key Rules Enforced
### Formatting
- Double quotes for strings
- Semicolons always
- Trailing commas (ES5 style)
- 2-space indentation
- 80-character line width (soft)
### TypeScript
- No `any` type
- Use `interface` for objects, `type` for unions
- Use `import type` for type-only imports
- No non-null assertions (`!`)
- No `as any` casts
### React
- Hooks called at top level only
- All dependencies in hook arrays
- Keys required in iterables (no index keys)
- No component definitions inside components
- Accessible components (ARIA, semantic HTML)
### Modern JavaScript
- `for...of` over `.forEach()`
- Template literals over concatenation
- Optional chaining (`?.`) and nullish coalescing (`??`)
- `const` by default, `let` when needed, never `var`
### Imports
- Organized: external → internal → relative
- Node builtins prefixed: `node:fs`, `node:path`
- No namespace imports (`import * as`)
## Git Hooks Integration
### When to Use Pre-commit Hooks
| Project Size | Recommendation |
|--------------|----------------|
| New/Small (<50 files) | Lint + typecheck on commit |
| Medium (50-200 files) | Lint on commit, typecheck on push |
| Large (200+ files) | Lint staged only, typecheck in CI |
For new projects, always start with full checks on commit. You can relax later if it becomes slow.
### Setup Husky + lint-staged
```bash
pnpm add -D husky lint-staged
pnpm exec husky init
```
### New Projects: Lint + Typecheck on Commit
#### package.json
```json
{
"scripts": {
"prepare": "husky",
"typecheck": "tsc --noEmit"
},
"lint-staged": {
"*.{js,jsx,ts,tsx}": [
"pnpm exec ultracite fix"
],
"*.{json,jsonc,css,md,mdx}": [
"pnpm exec biome format --write"
]
}
}
```
#### .husky/pre-commit
```bash
#!/bin/sh
pnpm lint-staged
pnpm typecheck
```
### Large Projects: Staged Lint Only
For larger codebases where full typecheck is slow (>5 seconds):
#### .husky/pre-commit
```bash
#!/bin/sh
pnpm lint-staged
```
#### .husky/pre-push (typecheck before push)
```bash
#!/bin/sh
pnpm typecheck
```
Create pre-push hook:
```bash
echo '#!/bin/sh\npnpm typecheck' > .husky/pre-push
chmod +x .husky/pre-push
```
### Monorepo Setup
For Turborepo projects, scope checks to affected packages:
#### .husky/pre-commit
```bash
#!/bin/sh
pnpm lint-staged
pnpm turbo typecheck --filter='...[HEAD^]'
```
This only typechecks packages affected by the current commit.
### Skipping Hooks (Escape Hatch)
When you need to commit WIP code:
```bash
git commit --no-verify -m "WIP: work in progress"
```
Use sparingly. CI should still catch issues.
## VS Code Integration
Install the Biome extension, then add to `.vscode/settings.json`:
```json
{
"editor.defaultFormatter": "biomejs.biome",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"quickfix.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
},
"[typescriptreact]": {
"editor.defaultFormatter": "biomejs.biome"
}
}
```
## Ignoring Files
In `biome.json`:
```json
{
"extends": ["ultracite"],
"files": {
"ignore": [
"node_modules",
".next",
"dist",
"drizzle"
]
}
}
```
## Custom Overrides
```json
{
"extends": ["ultracite"],
"linter": {
"rules": {
"complexity": {
"noForEach": "warn"
}
}
},
"formatter": {
"lineWidth": 100
}
}
```
## Migration from ESLint + Prettier
1. Remove old dependencies:
```bash
pnpm remove eslint prettier eslint-config-next @typescript-eslint/eslint-plugin @typescript-eslint/parser
```
2. Remove old config files:
```bash
rm .eslintrc* .prettierrc* eslint.config.* prettier.config.*
```
3. Install Biome:
```bash
pnpm add -D @biomejs/biome ultracite
pnpm exec ultracite init
```
4. Update scripts:
```json
{
"lint": "biome check src --write",
"format": "biome format src --write"
}
```
5. Run initial fix:
```bash
pnpm exec ultracite fix
```
## Troubleshooting
### "Cannot find configuration"
```bash
pnpm exec ultracite doctor
```
### Conflicts with other formatters
Disable other formatters in VS Code:
```json
{
"prettier.enable": false,
"eslint.enable": false
}
```
### Specific file issues
Check with verbose output:
```bash
pnpm exec biome check src/problem-file.ts --verbose
```
### Tailwind v4 CSS Directives
Biome 2.3+ supports parsing Tailwind v4 CSS directives (`@theme`, `@source`, etc.):
```bash
pnpm exec biome check src --css-parse-tailwind-directives
```
Or in `biome.json`:
```json
{
"css": {
"parser": {
"tailwindCssDirectives": true
}
}
}
```
================================================================================
## rules/code-style.md
================================================================================
# Code Style
## Formatting & Syntax
- MUST: Use Biome as the single source of truth for formatting.
- MUST: Use double quotes for strings and include semicolons.
- MUST: Include trailing commas (ES5: objects, arrays, function params).
- SHOULD: Wrap all arrow function parameters in parentheses.
- SHOULD: Prefer `for...of` over `Array.forEach` and index-based `for` loops.
- SHOULD: Prefer object spread (`{...obj}`) over `Object.assign()`.
- SHOULD: Prefer template literals over string concatenation.
- MUST: Use strict equality: `===` and `!==` exclusively.
- MUST: Use kebab-case ASCII filenames. Components: `component-name.tsx`; utilities: `util-name.ts`.
- SHOULD: Order imports external → internal → relative, and MUST use `node:` for Node builtins (Biome organizes imports).
## Naming & Comments
- MUST: Prefer clear function/variable names over inline comments.
- SHOULD: Include brief inline comments only when behavior is non-obvious.
- SHOULD: Use JSDoc for exported functions/components when additional context improves DX.
- MUST: Keep comments minimal and focused on "why" rather than "what".
- NEVER: Create `.md` documentation files for application code (README.md files for packages and the `.ruler` system are allowed).
- NEVER: Use emojis in code or commit messages.
## Canonical Code Principle
- MUST: Code is always canonical. No backward compatibility layers, deprecation warnings, or legacy patterns.
- MUST: When updating patterns or APIs, update ALL usage sites immediately. No migration periods.
- NEVER: Leave comments about "old way" vs "new way". The current code IS the way.
- NEVER: Add compatibility shims or adapters for old patterns. Remove old patterns entirely.
- MUST: Refactor fearlessly. The codebase reflects current best practices only.
## Patterns
- SHOULD: Use regex literals over `RegExp` constructor.
- MUST: Use `indexOf`/`lastIndexOf` for simple value lookups (not `findIndex`/`findLastIndex`).
- MUST: Use `.flatMap()` over `map().flat()`.
## Cleanup
- SHOULD: Use `knip` to find and remove unused code when making large changes.
================================================================================
## rules/typescript.md
================================================================================
# TypeScript Rules
## Type Definitions
- MUST: Use `interface` over `type` for object type definitions.
- MUST: Use `type` for unions, mapped types, and conditional types.
- MUST: Use `as const` for literal type assertions.
- MUST: Use `import type` and `export type` for type-only imports/exports.
- NEVER: Use `unknown` as a generic constraint (e.g., `T extends unknown`).
## Type Safety
- NEVER: Use `any`. Prefer precise types or `unknown` with narrowing.
- NEVER: Cast to `any` (`as any`, `as unknown as`). Fix types at the source.
- NEVER: Use unsafe `as` casts to bypass errors. Wrong types = wrong code.
- NEVER: Use non-null assertions (`!`). Fix types at source.
- NEVER: Shadow variables from outer scope.
## Error Handling
- NEVER: Add unnecessary `try`/`catch`. Let errors propagate.
- SHOULD: Only catch when you can recover, transform, or report.
## Simplicity
- SHOULD: Only create abstractions when actually needed. Inline is fine.
- SHOULD: Avoid helper functions when a simple inline expression suffices.
## Style
- SHOULD: Use numeric separators in large number literals (e.g., `1_000_000`).
================================================================================
## rules/react.md
================================================================================
# React Rules
Scope: All apps and packages.
## Component Design
- MUST: Each component does one thing well (single responsibility). Keep it minimal and "dumb" by default (rendering-focused).
- MUST: When logic grows, extract business logic into custom hooks. Components focus on composition and render.
- MUST: Avoid massive JSX blocks. Compose smaller, focused components instead.
- SHOULD: Colocate code that changes together (component + hook + types in same folder).
- SHOULD: Organize complex components in a folder:
- `components/complex-component/complex-component-root.tsx`
- `components/complex-component/complex-component-item.tsx`
- `components/complex-component/index.ts`
## Component Naming
- MUST: Use specific, descriptive names that convey purpose. Avoid generic suffixes like `-content`, `-wrapper`, `-container`, `-component`.
- NEVER: Create "god components" with vague names like `PageContent`, `MainWrapper`, `ComponentContainer`.
- SHOULD: Name components by their domain role: `FolderThumbnail`, `ProductCard`, `UserAvatar` — not `ItemContent`, `CardWrapper`.
- SHOULD: Part components should describe their role: `FolderActionMenu`, `DialogHeader`, `FormFieldError` — not just `Actions`, `Header`, `Error`.
## Design System First
- MUST: Check for the existence of design system primitives (`Stack`, `Grid`, `Container`, `Text`, `Heading`) in the project before using them.
- MUST: IF primitives exist: Use them for layout and typography instead of raw HTML.
- MUST: IF primitives DO NOT exist: Use raw HTML (`div`, `h1`, `p`) with utility classes.
- SHOULD: When missing a primitive, prefer defining it over one-off `className` usage if the pattern repeats.
- SHOULD: Use `Button` component variants instead of raw ` |