diff --git a/.agents/skills/better-auth-best-practices/SKILL.md b/.agents/skills/better-auth-best-practices/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..fabd32f65b3905c2fdbd174f62b2dbd09b47677d --- /dev/null +++ b/.agents/skills/better-auth-best-practices/SKILL.md @@ -0,0 +1,183 @@ +--- +name: better-auth-best-practices +description: Configure Better Auth server and client, set up database adapters, manage sessions, add plugins, and handle environment variables. Use when users mention Better Auth, betterauth, auth.ts, or need to set up TypeScript authentication with email/password, OAuth, or plugin configuration. +--- + +# Better Auth Integration Guide + +**Always consult [better-auth.com/docs](https://better-auth.com/docs) for code examples and latest API.** + +--- + +## Setup Workflow + +1. Install: `npm install better-auth` +2. Set env vars: `BETTER_AUTH_SECRET` and `BETTER_AUTH_URL` +3. Create `auth.ts` with database + config +4. Create route handler for your framework +5. Run `npx @better-auth/cli@latest migrate` +6. Verify: call `GET /api/auth/ok` — should return `{ status: "ok" }` + +--- + +## Quick Reference + +### Environment Variables + +- `BETTER_AUTH_SECRET` - Encryption secret (min 32 chars). Generate: `openssl rand -base64 32` +- `BETTER_AUTH_URL` - Base URL (e.g., `https://example.com`) + +Only define `baseURL`/`secret` in config if env vars are NOT set. + +### File Location + +CLI looks for `auth.ts` in: `./`, `./lib`, `./utils`, or under `./src`. Use `--config` for custom path. + +### CLI Commands + +- `npx @better-auth/cli@latest migrate` - Apply schema (built-in adapter) +- `npx @better-auth/cli@latest generate` - Generate schema for Prisma/Drizzle +- `npx @better-auth/cli mcp --cursor` - Add MCP to AI tools + +**Re-run after adding/changing plugins.** + +--- + +## Core Config Options + +| Option | Notes | +| ------------------ | ---------------------------------------------- | +| `appName` | Optional display name | +| `baseURL` | Only if `BETTER_AUTH_URL` not set | +| `basePath` | Default `/api/auth`. Set `/` for root. | +| `secret` | Only if `BETTER_AUTH_SECRET` not set | +| `database` | Required for most features. See adapters docs. | +| `secondaryStorage` | Redis/KV for sessions & rate limits | +| `emailAndPassword` | `{ enabled: true }` to activate | +| `socialProviders` | `{ google: { clientId, clientSecret }, ... }` | +| `plugins` | Array of plugins | +| `trustedOrigins` | CSRF whitelist | + +--- + +## Database + +**Direct connections:** Pass `pg.Pool`, `mysql2` pool, `better-sqlite3`, or `bun:sqlite` instance. + +**ORM adapters:** Import from `better-auth/adapters/drizzle`, `better-auth/adapters/prisma`, `better-auth/adapters/mongodb`. + +**Critical:** Better Auth uses adapter model names, NOT underlying table names. If Prisma model is `User` mapping to table `users`, use `modelName: "user"` (Prisma reference), not `"users"`. + +--- + +## Session Management + +**Storage priority:** + +1. If `secondaryStorage` defined → sessions go there (not DB) +2. Set `session.storeSessionInDatabase: true` to also persist to DB +3. No database + `cookieCache` → fully stateless mode + +**Cookie cache strategies:** + +- `compact` (default) - Base64url + HMAC. Smallest. +- `jwt` - Standard JWT. Readable but signed. +- `jwe` - Encrypted. Maximum security. + +**Key options:** `session.expiresIn` (default 7 days), `session.updateAge` (refresh interval), `session.cookieCache.maxAge`, `session.cookieCache.version` (change to invalidate all sessions). + +--- + +## User & Account Config + +**User:** `user.modelName`, `user.fields` (column mapping), `user.additionalFields`, `user.changeEmail.enabled` (disabled by default), `user.deleteUser.enabled` (disabled by default). + +**Account:** `account.modelName`, `account.accountLinking.enabled`, `account.storeAccountCookie` (for stateless OAuth). + +**Required for registration:** `email` and `name` fields. + +--- + +## Email Flows + +- `emailVerification.sendVerificationEmail` - Must be defined for verification to work +- `emailVerification.sendOnSignUp` / `sendOnSignIn` - Auto-send triggers +- `emailAndPassword.sendResetPassword` - Password reset email handler + +--- + +## Security + +**In `advanced`:** + +- `useSecureCookies` - Force HTTPS cookies +- `disableCSRFCheck` - ⚠️ Security risk +- `disableOriginCheck` - ⚠️ Security risk +- `crossSubDomainCookies.enabled` - Share cookies across subdomains +- `ipAddress.ipAddressHeaders` - Custom IP headers for proxies +- `database.generateId` - Custom ID generation or `"serial"`/`"uuid"`/`false` + +**Rate limiting:** `rateLimit.enabled`, `rateLimit.window`, `rateLimit.max`, `rateLimit.storage` ("memory" | "database" | "secondary-storage"). + +--- + +## Hooks + +**Endpoint hooks:** `hooks.before` / `hooks.after` - Array of `{ matcher, handler }`. Use `createAuthMiddleware`. Access `ctx.path`, `ctx.context.returned` (after), `ctx.context.session`. + +**Database hooks:** `databaseHooks.user.create.before/after`, same for `session`, `account`. Useful for adding default values or post-creation actions. + +**Hook context (`ctx.context`):** `session`, `secret`, `authCookies`, `password.hash()`/`verify()`, `adapter`, `internalAdapter`, `generateId()`, `tables`, `baseURL`. + +--- + +## Plugins + +**Import from dedicated paths for tree-shaking:** + +``` +import { twoFactor } from "better-auth/plugins/two-factor" +``` + +NOT `from "better-auth/plugins"`. + +**Popular plugins:** `twoFactor`, `organization`, `passkey`, `magicLink`, `emailOtp`, `username`, `phoneNumber`, `admin`, `apiKey`, `bearer`, `jwt`, `multiSession`, `sso`, `oauthProvider`, `oidcProvider`, `openAPI`, `genericOAuth`. + +Client plugins go in `createAuthClient({ plugins: [...] })`. + +--- + +## Client + +Import from: `better-auth/client` (vanilla), `better-auth/react`, `better-auth/vue`, `better-auth/svelte`, `better-auth/solid`. + +Key methods: `signUp.email()`, `signIn.email()`, `signIn.social()`, `signOut()`, `useSession()`, `getSession()`, `revokeSession()`, `revokeSessions()`. + +--- + +## Type Safety + +Infer types: `typeof auth.$Infer.Session`, `typeof auth.$Infer.Session.user`. + +For separate client/server projects: `createAuthClient()`. + +--- + +## Common Gotchas + +1. **Model vs table name** - Config uses ORM model name, not DB table name +2. **Plugin schema** - Re-run CLI after adding plugins +3. **Secondary storage** - Sessions go there by default, not DB +4. **Cookie cache** - Custom session fields NOT cached, always re-fetched +5. **Stateless mode** - No DB = session in cookie only, logout on cache expiry +6. **Change email flow** - Sends to current email first, then new email + +--- + +## Resources + +- [Docs](https://better-auth.com/docs) +- [Options Reference](https://better-auth.com/docs/reference/options) +- [LLMs.txt](https://better-auth.com/llms.txt) +- [GitHub](https://github.com/better-auth/better-auth) +- [Init Options Source](https://github.com/better-auth/better-auth/blob/main/packages/core/src/types/init-options.ts) diff --git a/.agents/skills/hono/SKILL.md b/.agents/skills/hono/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..9f9da4b0078799bfc7d6d39995157f9a987e943b --- /dev/null +++ b/.agents/skills/hono/SKILL.md @@ -0,0 +1,579 @@ +--- +name: hono +description: Use when building Hono web applications or when the user asks about Hono APIs, routing, middleware, JSX, validation, testing, or streaming. TRIGGER when code imports from 'hono' or 'hono/*', or user mentions Hono. Use `npx hono request` to test endpoints. +--- + +# Hono Skill + +Build Hono web applications. This skill provides inline API knowledge for AI. Use `npx hono request` to test endpoints. If the `hono-docs` MCP server is configured, prefer its tools for the latest documentation over the inline reference. + +## Hono CLI Usage + +### Request Testing + +Test endpoints without starting an HTTP server. Uses `app.request()` internally. + +```bash +# GET request +npx hono request [file] -P /path + +# POST request with JSON body +npx hono request [file] -X POST -P /api/users -d '{"name": "test"}' +``` + +**Note:** Do not pass credentials directly in CLI arguments. Use environment variables for sensitive values. `hono request` does not support Cloudflare Workers bindings (KV, D1, R2, etc.). When bindings are required, use `workers-fetch` instead: + +```bash +npx workers-fetch /path +npx workers-fetch -X POST -H "Content-Type:application/json" -d '{"name":"test"}' /api/users +``` + +--- + +## Hono API Reference + +### App Constructor + +```ts +import { Hono } from "hono"; + +const app = new Hono(); + +// With TypeScript generics +type Env = { + Bindings: { DATABASE: D1Database; KV: KVNamespace }; + Variables: { user: User }; +}; +const app = new Hono(); +``` + +### Routing Methods + +```ts +app.get("/path", handler); +app.post("/path", handler); +app.put("/path", handler); +app.delete("/path", handler); +app.patch("/path", handler); +app.options("/path", handler); +app.all("/path", handler); // all HTTP methods +app.on("PURGE", "/path", handler); // custom method +app.on(["PUT", "DELETE"], "/path", handler); // multiple methods +``` + +### Routing Patterns + +```ts +// Path parameters +app.get("/user/:name", (c) => { + const name = c.req.param("name"); + return c.json({ name }); +}); + +// Multiple params +app.get("/posts/:id/comments/:commentId", (c) => { + const { id, commentId } = c.req.param(); +}); + +// Optional parameters +app.get("/api/animal/:type?", (c) => c.text("Animal!")); + +// Wildcards +app.get("/wild/*/card", (c) => c.text("Wildcard")); + +// Regexp constraints +app.get("/post/:date{[0-9]+}/:title{[a-z]+}", (c) => { + const { date, title } = c.req.param(); +}); + +// Chained routes +app + .get("/endpoint", (c) => c.text("GET")) + .post((c) => c.text("POST")) + .delete((c) => c.text("DELETE")); +``` + +### Route Grouping + +```ts +// Using route() +const api = new Hono(); +api.get("/users", (c) => c.json([])); + +const app = new Hono(); +app.route("/api", api); // mounts at /api/users + +// Using basePath() +const app = new Hono().basePath("/api"); +app.get("/users", (c) => c.json([])); // GET /api/users +``` + +### Error Handling + +```ts +app.notFound((c) => c.json({ message: "Not Found" }, 404)); + +app.onError((err, c) => { + console.error(err); + return c.json({ message: "Internal Server Error" }, 500); +}); +``` + +--- + +## Context (c) + +### Response Methods + +```ts +c.text("Hello"); // text/plain +c.json({ message: "Hello" }); // application/json +c.html("

Hello

"); // text/html +c.redirect("/new-path"); // 302 redirect +c.redirect("/new-path", 301); // 301 redirect +c.body("raw body", 200, headers); // raw response +c.notFound(); // 404 response +``` + +### Headers & Status + +```ts +c.status(201); +c.header("X-Custom", "value"); +c.header("Cache-Control", "no-store"); +``` + +### Variables (request-scoped data) + +```ts +// In middleware +c.set("user", { id: 1, name: "Alice" }); + +// In handler +const user = c.get("user"); +// or +const user = c.var.user; +``` + +### Environment (Cloudflare Workers) + +```ts +const value = await c.env.KV.get("key"); +const db = c.env.DATABASE; +c.executionCtx.waitUntil(promise); +``` + +### Renderer + +```ts +app.use(async (c, next) => { + c.setRenderer((content) => + c.html( + {content} + ) + ) + await next() +}) + +app.get('/', (c) => c.render(

Hello

)) +``` + +--- + +## HonoRequest (c.req) + +```ts +c.req.param("id"); // path parameter +c.req.param(); // all path params as object +c.req.query("page"); // query string parameter +c.req.query(); // all query params as object +c.req.queries("tags"); // multiple values: ?tags=A&tags=B → ['A', 'B'] +c.req.header("Authorization"); // request header +c.req.header(); // all headers (keys are lowercase) + +// Body parsing +await c.req.json(); // parse JSON body +await c.req.text(); // parse text body +await c.req.formData(); // parse as FormData +await c.req.parseBody(); // parse multipart/form-data or urlencoded +await c.req.arrayBuffer(); // parse as ArrayBuffer +await c.req.blob(); // parse as Blob + +// Validated data (used with validator middleware) +c.req.valid("json"); +c.req.valid("query"); +c.req.valid("form"); +c.req.valid("param"); + +// Properties +c.req.url; // full URL string +c.req.path; // pathname +c.req.method; // HTTP method +c.req.raw; // underlying Request object +``` + +--- + +## Middleware + +### Using Built-in Middleware + +```ts +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; +import { basicAuth } from "hono/basic-auth"; +import { prettyJSON } from "hono/pretty-json"; +import { secureHeaders } from "hono/secure-headers"; +import { etag } from "hono/etag"; +import { compress } from "hono/compress"; +import { poweredBy } from "hono/powered-by"; +import { timing } from "hono/timing"; +import { cache } from "hono/cache"; +import { bearerAuth } from "hono/bearer-auth"; +import { jwt } from "hono/jwt"; +import { csrf } from "hono/csrf"; +import { ipRestriction } from "hono/ip-restriction"; +import { bodyLimit } from "hono/body-limit"; +import { requestId } from "hono/request-id"; +import { methodOverride } from "hono/method-override"; +import { trailingSlash, trimTrailingSlash } from "hono/trailing-slash"; + +// Registration +app.use(logger()); // all routes +app.use("/api/*", cors()); // specific path +app.post("/api/*", basicAuth({ username: "admin", password: "secret" })); +``` + +### Custom Middleware + +```ts +// Inline +app.use(async (c, next) => { + const start = Date.now(); + await next(); + const elapsed = Date.now() - start; + c.res.headers.set("X-Response-Time", `${elapsed}ms`); +}); + +// Reusable with createMiddleware +import { createMiddleware } from "hono/factory"; + +const auth = createMiddleware(async (c, next) => { + const token = c.req.header("Authorization"); + if (!token) return c.json({ error: "Unauthorized" }, 401); + await next(); +}); + +app.use("/api/*", auth); +``` + +### Middleware Execution Order + +Middleware executes in registration order. `await next()` calls the next middleware/handler, and code after `next()` runs on the way back: + +``` +Request → mw1 before → mw2 before → handler → mw2 after → mw1 after → Response +``` + +```ts +app.use(async (c, next) => { + // before handler + await next(); + // after handler +}); +``` + +--- + +## Validation + +Validation targets: `json`, `form`, `query`, `header`, `param`, `cookie`. + +### Zod Validator + +```ts +import { zValidator } from "@hono/zod-validator"; +import { z } from "zod"; + +const schema = z.object({ + title: z.string().min(1), + body: z.string(), +}); + +app.post("/posts", zValidator("json", schema), (c) => { + const data = c.req.valid("json"); // fully typed + return c.json(data, 201); +}); +``` + +### Valibot / Standard Schema Validator + +```ts +import { sValidator } from "@hono/standard-validator"; +import * as v from "valibot"; + +const schema = v.object({ name: v.string(), age: v.number() }); + +app.post("/users", sValidator("json", schema), (c) => { + const data = c.req.valid("json"); + return c.json(data, 201); +}); +``` + +--- + +## JSX + +### Setup + +In `tsconfig.json`: + +```json +{ + "compilerOptions": { + "jsx": "react-jsx", + "jsxImportSource": "hono/jsx" + } +} +``` + +Or use pragma: `/** @jsxImportSource hono/jsx */` + +**Important:** Files using JSX must have a `.tsx` extension. Rename `.ts` to `.tsx` or the compiler will fail. + +### Components + +```tsx +import type { PropsWithChildren } from "hono/jsx"; + +const Layout = (props: PropsWithChildren) => ( + + + My App + + {props.children} + +); + +const UserCard = ({ name }: { name: string }) => ( +
+

{name}

+
+); + +app.get("/", (c) => { + return c.html( + + + , + ); +}); +``` + +### jsxRenderer Middleware + +Use `jsxRenderer` middleware for layouts. See `npx hono docs /docs/middleware/builtin/jsx-renderer` for details. + +### Async Components + +```tsx +const UserList = async () => { + const users = await fetchUsers(); + return ( + + ); +}; +``` + +### Fragments + +```tsx +const Items = () => ( + <> +
  • Item 1
  • +
  • Item 2
  • + +); +``` + +--- + +## Streaming + +```ts +import { stream, streamText, streamSSE } from "hono/streaming"; + +// Basic stream +app.get("/stream", (c) => { + return stream(c, async (stream) => { + stream.onAbort(() => console.log("Aborted")); + await stream.write(new Uint8Array([0x48, 0x65])); + await stream.pipe(readableStream); + }); +}); + +// Text stream +app.get("/stream-text", (c) => { + return streamText(c, async (stream) => { + await stream.writeln("Hello"); + await stream.sleep(1000); + await stream.write("World"); + }); +}); + +// Server-Sent Events +app.get("/sse", (c) => { + return streamSSE(c, async (stream) => { + let id = 0; + while (true) { + await stream.writeSSE({ + data: JSON.stringify({ time: new Date().toISOString() }), + event: "time-update", + id: String(id++), + }); + await stream.sleep(1000); + } + }); +}); +``` + +--- + +## Testing with app.request() + +Test endpoints without starting an HTTP server: + +```ts +// GET +const res = await app.request("/posts"); +expect(res.status).toBe(200); +expect(await res.json()).toEqual({ posts: [] }); + +// POST with JSON +const res = await app.request("/posts", { + method: "POST", + body: JSON.stringify({ title: "Hello" }), + headers: { "Content-Type": "application/json" }, +}); + +// POST with FormData +const formData = new FormData(); +formData.append("name", "Alice"); +const res = await app.request("/users", { method: "POST", body: formData }); + +// With mock env (Cloudflare Workers bindings) +const res = await app.request("/api/data", {}, { KV: mockKV, DATABASE: mockDB }); + +// Using Request object +const req = new Request("http://localhost/api", { method: "DELETE" }); +const res = await app.request(req); +``` + +--- + +## Hono Client (RPC) + +Type-safe API client using shared types between server and client. + +**IMPORTANT: Routes MUST be chained for type inference to work. Without chaining, the client cannot infer route types.** + +```ts +// Server: routes MUST be chained to preserve types +const route = app + .post("/posts", zValidator("json", schema), (c) => { + return c.json({ ok: true }, 201); + }) + .get("/posts", (c) => { + return c.json({ posts: [] }); + }); +export type AppType = typeof route; + +// Client: use hc() with the exported type +import { hc } from "hono/client"; +import type { AppType } from "./server"; + +const client = hc("http://localhost:8787/"); +const res = await client.posts.$post({ json: { title: "Hello" } }); +const data = await res.json(); // fully typed +``` + +Type utilities: + +```ts +import type { InferRequestType, InferResponseType } from "hono/client"; + +type ReqType = InferRequestType; +type ResType = InferResponseType; +``` + +--- + +## Helpers + +Helpers are utility functions imported from `hono/`: + +```ts +import { getConnInfo } from "hono/conninfo"; +import { getCookie, setCookie, deleteCookie } from "hono/cookie"; +import { css, Style } from "hono/css"; +import { createFactory } from "hono/factory"; +import { html, raw } from "hono/html"; +import { stream, streamText, streamSSE } from "hono/streaming"; +import { testClient } from "hono/testing"; +import { upgradeWebSocket } from "hono/cloudflare-workers"; // or other adapter +``` + +Available helpers: Accepts, Adapter, ConnInfo, Cookie, css, Dev, Factory, html, JWT, Proxy, Route, SSG, Streaming, Testing, WebSocket. + +For details, use `npx hono docs /docs/helpers/`. + +### Factory + +Use `createFactory` to define `Env` once and share it across app, middleware, and handlers: + +```ts +import { createFactory } from "hono/factory"; + +const factory = createFactory(); + +// Create app (Env type is inherited) +const app = factory.createApp(); + +// Create middleware (Env type is inherited, no need to pass generics) +const mw = factory.createMiddleware(async (c, next) => { + await next(); +}); + +// Create handlers separately (preserves type inference) +const handlers = factory.createHandlers(logger(), (c) => c.json({ message: "Hello" })); +app.get("/api", ...handlers); +``` + +--- + +## Best Practices + +- Write handlers inline in route definitions for proper type inference of path params. +- Use `app.route()` to organize large apps by feature, not Rails-style controllers. +- Use `createFactory()` to share Env type across app, middleware, and handlers. +- Use `c.set()`/`c.get()` to pass data between middleware and handlers. +- Chain validators for multiple request parts (param + query + json). +- Export app type for RPC: `export type AppType = typeof routes` +- Use `app.request()` for testing — no server startup needed. + +## Adapters + +Hono runs on multiple runtimes. The default export works for Cloudflare Workers, Deno, and Bun. For Node.js, use the Node adapter: + +```ts +// Cloudflare Workers / Deno / Bun +export default app; + +// Node.js +import { serve } from "@hono/node-server"; +serve(app); +``` diff --git a/.agents/skills/shadcn/SKILL.md b/.agents/skills/shadcn/SKILL.md new file mode 100644 index 0000000000000000000000000000000000000000..2aab0b0711341aa6e41d0441b5d07c6e6684855e --- /dev/null +++ b/.agents/skills/shadcn/SKILL.md @@ -0,0 +1,250 @@ +--- +name: shadcn +description: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI. Provides project context, component docs, and usage examples. Applies when working with shadcn/ui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for "shadcn init", "create an app with --preset", or "switch to --preset". +user-invocable: false +allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *) +--- + +# shadcn/ui + +A framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI. + +> **IMPORTANT:** Run all CLI commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest` — based on the project's `packageManager`. Examples below use `npx shadcn@latest` but substitute the correct runner for the project. + +## Current Project Context + +```json +!`npx shadcn@latest info --json` +``` + +The JSON above contains the project config and installed components. Use `npx shadcn@latest docs ` to get documentation and example URLs for any component. + +## Principles + +1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too. +2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table. +3. **Use built-in variants before custom styles.** `variant="outline"`, `size="sm"`, etc. +4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`. + +## Critical Rules + +These rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs. + +### Styling & Tailwind → [styling.md](./rules/styling.md) + +- **`className` for layout, not styling.** Never override component colors or typography. +- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`. +- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`. +- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`. +- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`). +- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries. +- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking. + +### Forms & Inputs → [forms.md](./rules/forms.md) + +- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout. +- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`. +- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.** +- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state. +- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading. +- **Field validation uses `data-invalid` + `aria-invalid`.** `data-invalid` on `Field`, `aria-invalid` on the control. For disabled: `data-disabled` on `Field`, `disabled` on the control. + +### Component Structure → [composition.md](./rules/composition.md) + +- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`. +- **Use `asChild` (radix) or `render` (base) for custom triggers.** Check `base` field from `npx shadcn@latest info`. → [base-vs-radix.md](./rules/base-vs-radix.md) +- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className="sr-only"` if visually hidden. +- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`. +- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`. +- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`. +- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load. + +### Use Components, Not Custom Markup → [composition.md](./rules/composition.md) + +- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`. +- **Callouts use `Alert`.** Don't build custom styled divs. +- **Empty states use `Empty`.** Don't build custom empty state markup. +- **Toast via `sonner`.** Use `toast()` from `sonner`. +- **Use `Separator`** instead of `
    ` or `
    `. +- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs. +- **Use `Badge`** instead of custom styled spans. + +### Icons → [icons.md](./rules/icons.md) + +- **Icons in `Button` use `data-icon`.** `data-icon="inline-start"` or `data-icon="inline-end"` on the icon. +- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`. +- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup. + +### CLI + +- **Never decode or fetch preset codes manually.** Pass them directly to `npx shadcn@latest apply --preset ` for existing projects, or `npx shadcn@latest init --preset ` when initializing. + +## Key Patterns + +These are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above. + +```tsx +// Form layout: FieldGroup + Field, not div + Label. + + + Email + + + + +// Validation: data-invalid on Field, aria-invalid on the control. + + Email + + Invalid email. + + +// Icons in buttons: data-icon, no sizing classes. + + +// Spacing: gap-*, not space-y-*. +
    // correct +
    // wrong + +// Equal dimensions: size-*, not w-* h-*. + // correct + // wrong + +// Status colors: Badge variants or semantic tokens, not raw colors. ++20.1% // correct ++20.1% // wrong +``` + +## Component Selection + +| Need | Use | +| -------------------------- | --------------------------------------------------------------------------------------------------- | +| Button/action | `Button` with appropriate variant | +| Form inputs | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` | +| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem` | +| Data display | `Table`, `Card`, `Badge`, `Avatar` | +| Navigation | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination` | +| Overlays | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation) | +| Feedback | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner` | +| Command palette | `Command` inside `Dialog` | +| Charts | `Chart` (wraps Recharts) | +| Layout | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible` | +| Empty states | `Empty` | +| Menus | `DropdownMenu`, `ContextMenu`, `Menubar` | +| Tooltips/info | `Tooltip`, `HoverCard`, `Popover` | + +## Key Fields + +The injected project context contains these key fields: + +- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode. +- **`isRSC`** → when `true`, components using `useState`, `useEffect`, event handlers, or browser APIs need `"use client"` at the top of the file. Always reference this field when advising on the directive. +- **`tailwindVersion`** → `"v4"` uses `@theme inline` blocks; `"v3"` uses `tailwind.config.js`. +- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one. +- **`style`** → component visual treatment (e.g. `nova`, `vega`). +- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props. +- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`. +- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc. +- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA). +- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`). + +See [cli.md — `info` command](./cli.md) for the full field reference. + +## Component Docs, Examples, and Usage + +Run `npx shadcn@latest docs ` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content. + +```bash +npx shadcn@latest docs button dialog select +``` + +**When creating, fixing, debugging, or using a component, always run `npx shadcn@latest docs` and fetch the URLs first.** This ensures you're working with the correct API and usage patterns rather than guessing. + +## Workflow + +1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh. +2. **Check installed components first** — before running `add`, always check the `components` list from project context or list the `resolvedPaths.ui` directory. Don't import components that haven't been added, and don't re-add ones already installed. +3. **Find components** — `npx shadcn@latest search`. +4. **Get docs and examples** — run `npx shadcn@latest docs ` to get URLs, then fetch them. Use `npx shadcn@latest view` to browse registry items you haven't installed. To preview changes to installed components, use `npx shadcn@latest add --diff`. +5. **Install or update** — `npx shadcn@latest add`. When updating existing components, use `--dry-run` and `--diff` to preview changes first (see [Updating Components](#updating-components) below). +6. **Fix imports in third-party components** — After adding components from community registries (e.g. `@bundui`, `@magicui`), check the added non-UI files for hardcoded import paths like `@/components/ui/...`. These won't match the project's actual aliases. Use `npx shadcn@latest info` to get the correct `ui` alias (e.g. `@workspace/ui/components`) and rewrite the imports accordingly. The CLI rewrites imports for its own UI files, but third-party registry components may use default paths that don't match the project. +7. **Review added components** — After adding a component or block from any registry, **always read the added files and verify they are correct**. Check for missing sub-components (e.g. `SelectItem` without `SelectGroup`), missing imports, incorrect composition, or violations of the [Critical Rules](#critical-rules). Also replace any icon imports with the project's `iconLibrary` from the project context (e.g. if the registry item uses `lucide-react` but the project uses `hugeicons`, swap the imports and icon names accordingly). Fix all issues before moving on. +8. **Registry must be explicit** — When the user asks to add a block or component, **do not guess the registry**. If no registry is specified (e.g. user says "add a login block" without specifying `@shadcn`, `@tailark`, etc.), ask which registry to use. Never default to a registry on behalf of the user. +9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**? + - **Overwrite**: `npx shadcn@latest apply --preset `. Overwrites detected components, fonts, and CSS variables. + - **Partial**: `npx shadcn@latest apply --preset --only theme,font`. Updates only the selected preset parts without reinstalling UI components. Supported values are `theme` and `font`; comma-separated combinations are allowed. `icon` is intentionally not supported, because icon changes may require full component reinstall and transforms. + - **Merge**: `npx shadcn@latest init --preset --force --no-reinstall`, then run `npx shadcn@latest info` to list installed components, then for each installed component use `--dry-run` and `--diff` to [smart merge](#updating-components) it individually. + - **Skip**: `npx shadcn@latest init --preset --force --no-reinstall`. Only updates config and CSS, leaves components as-is. + - **Important**: Always run preset commands inside the user's project directory. `apply` only works in an existing project with a `components.json` file. The CLI automatically preserves the current base (`base` vs `radix`) from `components.json`. If you must use a scratch/temp directory (e.g. for `--dry-run` comparisons), pass `--base ` explicitly — preset codes do not encode the base. + +## Updating Components + +When the user asks to update a component from upstream while keeping their local changes, use `--dry-run` and `--diff` to intelligently merge. **NEVER fetch raw files from GitHub manually — always use the CLI.** + +1. Run `npx shadcn@latest add --dry-run` to see all files that would be affected. +2. For each file, run `npx shadcn@latest add --diff ` to see what changed upstream vs local. +3. Decide per file based on the diff: + - No local changes → safe to overwrite. + - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications. + - User says "just update everything" → use `--overwrite`, but confirm first. +4. **Never use `--overwrite` without the user's explicit approval.** + +## Quick Reference + +```bash +# Create a new project. +npx shadcn@latest init --name my-app --preset base-nova +npx shadcn@latest init --name my-app --preset a2r6bw --template vite + +# Create a monorepo project. +npx shadcn@latest init --name my-app --preset base-nova --monorepo +npx shadcn@latest init --name my-app --preset base-nova --template next --monorepo + +# Initialize existing project. +npx shadcn@latest init --preset base-nova +npx shadcn@latest init --defaults # shortcut: --template=next --preset=nova (base style implied) + +# Apply a preset to an existing project. +npx shadcn@latest apply --preset a2r6bw +npx shadcn@latest apply a2r6bw +npx shadcn@latest apply --preset a2r6bw --only theme +npx shadcn@latest apply --preset a2r6bw --only font +npx shadcn@latest apply --preset a2r6bw --only theme,font + +# Add components. +npx shadcn@latest add button card dialog +npx shadcn@latest add @magicui/shimmer-button +npx shadcn@latest add --all + +# Preview changes before adding/updating. +npx shadcn@latest add button --dry-run +npx shadcn@latest add button --diff button.tsx +npx shadcn@latest add @acme/form --view button.tsx + +# Search registries. +npx shadcn@latest search @shadcn -q "sidebar" +npx shadcn@latest search @tailark -q "stats" + +# Get component docs and example URLs. +npx shadcn@latest docs button dialog select + +# View registry item details (for items not yet installed). +npx shadcn@latest view @shadcn/button +``` + +**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma` +**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo) +**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com). + +## Detailed References + +- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states +- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading +- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects +- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index +- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion +- [cli.md](./cli.md) — Commands, flags, presets, templates +- [customization.md](./customization.md) — Theming, CSS variables, extending components diff --git a/.agents/skills/shadcn/agents/openai.yml b/.agents/skills/shadcn/agents/openai.yml new file mode 100644 index 0000000000000000000000000000000000000000..ab636da86b5ee5b8721b0575af67faa88e4bf838 --- /dev/null +++ b/.agents/skills/shadcn/agents/openai.yml @@ -0,0 +1,5 @@ +interface: + display_name: "shadcn/ui" + short_description: "Manages shadcn/ui components — adding, searching, fixing, debugging, styling, and composing UI." + icon_small: "./assets/shadcn-small.png" + icon_large: "./assets/shadcn.png" diff --git a/.agents/skills/shadcn/assets/shadcn-small.png b/.agents/skills/shadcn/assets/shadcn-small.png new file mode 100644 index 0000000000000000000000000000000000000000..ab5277fe5f76f26a29442b9e135e2166aec12b1d --- /dev/null +++ b/.agents/skills/shadcn/assets/shadcn-small.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ecc62d682727f68fc4937eed19960c706714fa084d44f63028d611ae02bf3d7 +size 1049 diff --git a/.agents/skills/shadcn/assets/shadcn.png b/.agents/skills/shadcn/assets/shadcn.png new file mode 100644 index 0000000000000000000000000000000000000000..019d4d5fd00d3ce5b17e96458bdeb91204914556 --- /dev/null +++ b/.agents/skills/shadcn/assets/shadcn.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7d60ad6fec4d89a0d44ba5a3c9283d2fea0047af5467f9227040e20d927d39b7 +size 3852 diff --git a/.agents/skills/shadcn/cli.md b/.agents/skills/shadcn/cli.md new file mode 100644 index 0000000000000000000000000000000000000000..c3a0f0aa74816e6099b7a9f6143caf10b3333ae1 --- /dev/null +++ b/.agents/skills/shadcn/cli.md @@ -0,0 +1,276 @@ +# shadcn CLI Reference + +Configuration is read from `components.json`. + +> **IMPORTANT:** Always run commands using the project's package runner: `npx shadcn@latest`, `pnpm dlx shadcn@latest`, or `bunx --bun shadcn@latest`. Check `packageManager` from project context to choose the right one. Examples below use `npx shadcn@latest` but substitute the correct runner for the project. + +> **IMPORTANT:** Only use the flags documented below. Do not invent or guess flags — if a flag isn't listed here, it doesn't exist. The CLI auto-detects the package manager from the project's lockfile; there is no `--package-manager` flag. + +## Contents + +- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build +- Templates: next, vite, start, react-router, astro +- Presets: named, code, URL formats and fields +- Switching presets + +--- + +## Commands + +### `init` — Initialize or create a project + +```bash +npx shadcn@latest init [components...] [options] +``` + +Initializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step. + +| Flag | Short | Description | Default | +| ----------------------- | ----- | --------------------------------------------------------- | ------- | +| `--template