{"owner":"Kiranism","repo":"next-shadcn-dashboard-starter","hasSkills":true,"totalSkillsCount":234,"totalTokensCount":197706,"categories":["anthropic-skill","root-instruction","claude-rule"],"hasMcp":false,"mcpConfig":null,"found":[".agents/skills/kiranism-shadcn-dashboard/SKILL.md",".agents/skills/kiranism-shadcn-dashboard/references/charts-guide.md",".agents/skills/kiranism-shadcn-dashboard/references/forms-guide.md",".agents/skills/kiranism-shadcn-dashboard/references/mock-api-guide.md",".agents/skills/kiranism-shadcn-dashboard/references/query-abstractions.md",".agents/skills/kiranism-shadcn-dashboard/references/theming-guide.md",".agents/skills/next-best-practices/SKILL.md",".agents/skills/next-best-practices/async-patterns.md",".agents/skills/next-best-practices/bundling.md",".agents/skills/next-best-practices/data-patterns.md",".agents/skills/next-best-practices/debug-tricks.md",".agents/skills/next-best-practices/directives.md",".agents/skills/next-best-practices/error-handling.md",".agents/skills/next-best-practices/file-conventions.md",".agents/skills/next-best-practices/font.md",".agents/skills/next-best-practices/functions.md",".agents/skills/next-best-practices/hydration-error.md",".agents/skills/next-best-practices/image.md",".agents/skills/next-best-practices/metadata.md",".agents/skills/next-best-practices/parallel-routes.md",".agents/skills/next-best-practices/route-handlers.md",".agents/skills/next-best-practices/rsc-boundaries.md",".agents/skills/next-best-practices/runtime-selection.md",".agents/skills/next-best-practices/scripts.md",".agents/skills/next-best-practices/self-hosting.md",".agents/skills/next-best-practices/suspense-boundaries.md",".agents/skills/shadcn/SKILL.md",".agents/skills/shadcn/cli.md",".agents/skills/shadcn/customization.md",".agents/skills/shadcn/evals/evals.json",".agents/skills/shadcn/mcp.md",".agents/skills/shadcn/registry.md",".agents/skills/shadcn/rules/base-vs-radix.md",".agents/skills/shadcn/rules/chat.md",".agents/skills/shadcn/rules/composition.md",".agents/skills/shadcn/rules/forms.md",".agents/skills/shadcn/rules/icons.md",".agents/skills/shadcn/rules/styling.md",".agents/skills/tanstack-form/SKILL.md",".agents/skills/tanstack-query/SKILL.md",".agents/skills/tanstack-query/resources/cache-strategies.md",".agents/skills/tanstack-query/resources/data-fetching.md",".agents/skills/tanstack-query/resources/mutation-patterns.md",".agents/skills/tanstack-query/skill-rules-fragment.json",".agents/skills/vercel-composition-patterns/AGENTS.md",".agents/skills/vercel-composition-patterns/README.md",".agents/skills/vercel-composition-patterns/SKILL.md",".agents/skills/vercel-composition-patterns/rules/_sections.md",".agents/skills/vercel-composition-patterns/rules/_template.md",".agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md",".agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md",".agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md",".agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md",".agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md",".agents/skills/vercel-composition-patterns/rules/state-context-interface.md",".agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md",".agents/skills/vercel-composition-patterns/rules/state-lift-state.md",".agents/skills/vercel-react-best-practices/AGENTS.md",".agents/skills/vercel-react-best-practices/README.md",".agents/skills/vercel-react-best-practices/SKILL.md",".agents/skills/vercel-react-best-practices/rules/_sections.md",".agents/skills/vercel-react-best-practices/rules/_template.md",".agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md",".agents/skills/vercel-react-best-practices/rules/advanced-init-once.md",".agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md",".agents/skills/vercel-react-best-practices/rules/async-api-routes.md",".agents/skills/vercel-react-best-practices/rules/async-defer-await.md",".agents/skills/vercel-react-best-practices/rules/async-dependencies.md",".agents/skills/vercel-react-best-practices/rules/async-parallel.md",".agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md",".agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md",".agents/skills/vercel-react-best-practices/rules/bundle-conditional.md",".agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md",".agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md",".agents/skills/vercel-react-best-practices/rules/bundle-preload.md",".agents/skills/vercel-react-best-practices/rules/client-event-listeners.md",".agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md",".agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md",".agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md",".agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md",".agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md",".agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md",".agents/skills/vercel-react-best-practices/rules/js-cache-storage.md",".agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md",".agents/skills/vercel-react-best-practices/rules/js-early-exit.md",".agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md",".agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md",".agents/skills/vercel-react-best-practices/rules/js-index-maps.md",".agents/skills/vercel-react-best-practices/rules/js-length-check-first.md",".agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md",".agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md",".agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md",".agents/skills/vercel-react-best-practices/rules/rendering-activity.md",".agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md",".agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md",".agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md",".agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md",".agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md",".agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md",".agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md",".agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md",".agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md",".agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md",".agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md",".agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md",".agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md",".agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md",".agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md",".agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md",".agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md",".agents/skills/vercel-react-best-practices/rules/rerender-memo.md",".agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md",".agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md",".agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md",".agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md",".agents/skills/vercel-react-best-practices/rules/rerender-transitions.md",".agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md",".agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md",".agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md",".agents/skills/vercel-react-best-practices/rules/server-auth-actions.md",".agents/skills/vercel-react-best-practices/rules/server-cache-lru.md",".agents/skills/vercel-react-best-practices/rules/server-cache-react.md",".agents/skills/vercel-react-best-practices/rules/server-dedup-props.md",".agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md",".agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md",".agents/skills/vercel-react-best-practices/rules/server-serialization.md",".agents/skills/web-design-guidelines/SKILL.md",".claude/skills/kiranism-shadcn-dashboard/SKILL.md",".claude/skills/kiranism-shadcn-dashboard/references/theming-guide.md",".claude/skills/next-best-practices/SKILL.md",".claude/skills/next-best-practices/async-patterns.md",".claude/skills/next-best-practices/bundling.md",".claude/skills/next-best-practices/data-patterns.md",".claude/skills/next-best-practices/debug-tricks.md",".claude/skills/next-best-practices/directives.md",".claude/skills/next-best-practices/error-handling.md",".claude/skills/next-best-practices/file-conventions.md",".claude/skills/next-best-practices/font.md",".claude/skills/next-best-practices/functions.md",".claude/skills/next-best-practices/hydration-error.md",".claude/skills/next-best-practices/image.md",".claude/skills/next-best-practices/metadata.md",".claude/skills/next-best-practices/parallel-routes.md",".claude/skills/next-best-practices/route-handlers.md",".claude/skills/next-best-practices/rsc-boundaries.md",".claude/skills/next-best-practices/runtime-selection.md",".claude/skills/next-best-practices/scripts.md",".claude/skills/next-best-practices/self-hosting.md",".claude/skills/next-best-practices/suspense-boundaries.md",".claude/skills/vercel-composition-patterns/AGENTS.md",".claude/skills/vercel-composition-patterns/README.md",".claude/skills/vercel-composition-patterns/SKILL.md",".claude/skills/vercel-composition-patterns/rules/_sections.md",".claude/skills/vercel-composition-patterns/rules/_template.md",".claude/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md",".claude/skills/vercel-composition-patterns/rules/architecture-compound-components.md",".claude/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md",".claude/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md",".claude/skills/vercel-composition-patterns/rules/react19-no-forwardref.md",".claude/skills/vercel-composition-patterns/rules/state-context-interface.md",".claude/skills/vercel-composition-patterns/rules/state-decouple-implementation.md",".claude/skills/vercel-composition-patterns/rules/state-lift-state.md",".claude/skills/vercel-react-best-practices/AGENTS.md",".claude/skills/vercel-react-best-practices/README.md",".claude/skills/vercel-react-best-practices/SKILL.md",".claude/skills/vercel-react-best-practices/rules/_sections.md",".claude/skills/vercel-react-best-practices/rules/_template.md",".claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md",".claude/skills/vercel-react-best-practices/rules/advanced-init-once.md",".claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md",".claude/skills/vercel-react-best-practices/rules/async-api-routes.md",".claude/skills/vercel-react-best-practices/rules/async-defer-await.md",".claude/skills/vercel-react-best-practices/rules/async-dependencies.md",".claude/skills/vercel-react-best-practices/rules/async-parallel.md",".claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md",".claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md",".claude/skills/vercel-react-best-practices/rules/bundle-conditional.md",".claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md",".claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md",".claude/skills/vercel-react-best-practices/rules/bundle-preload.md",".claude/skills/vercel-react-best-practices/rules/client-event-listeners.md",".claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md",".claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md",".claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md",".claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md",".claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md",".claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md",".claude/skills/vercel-react-best-practices/rules/js-cache-storage.md",".claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md",".claude/skills/vercel-react-best-practices/rules/js-early-exit.md",".claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md",".claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md",".claude/skills/vercel-react-best-practices/rules/js-index-maps.md",".claude/skills/vercel-react-best-practices/rules/js-length-check-first.md",".claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md",".claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md",".claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md",".claude/skills/vercel-react-best-practices/rules/rendering-activity.md",".claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md",".claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md",".claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md",".claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md",".claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md",".claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md",".claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md",".claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md",".claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md",".claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md",".claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md",".claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md",".claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md",".claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md",".claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md",".claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md",".claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md",".claude/skills/vercel-react-best-practices/rules/rerender-memo.md",".claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md",".claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md",".claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md",".claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md",".claude/skills/vercel-react-best-practices/rules/rerender-transitions.md",".claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md",".claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md",".claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md",".claude/skills/vercel-react-best-practices/rules/server-auth-actions.md",".claude/skills/vercel-react-best-practices/rules/server-cache-lru.md",".claude/skills/vercel-react-best-practices/rules/server-cache-react.md",".claude/skills/vercel-react-best-practices/rules/server-dedup-props.md",".claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md",".claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md",".claude/skills/vercel-react-best-practices/rules/server-serialization.md",".claude/skills/web-design-guidelines/SKILL.md","AGENTS.md","CLAUDE.md"],"skills":{".agents/skills/kiranism-shadcn-dashboard/SKILL.md":"---\nname: kiranism-shadcn-dashboard\ndescription: |\n  Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention \"dashboard\" explicitly — if they're adding UI, pages, or features to this project, use this skill.\n---\n\n# Dashboard Development Guide\n\nThis skill encodes the exact patterns and conventions used in this Next.js 16 + shadcn/ui admin dashboard template.\n\n## Quick Reference: What Goes Where\n\n| Task                  | Location                                |\n| --------------------- | --------------------------------------- |\n| New page              | `src/app/dashboard/<name>/page.tsx`     |\n| New feature module    | `src/features/<name>/`                  |\n| Feature components    | `src/features/<name>/components/`       |\n| API types             | `src/features/<name>/api/types.ts`      |\n| Service layer         | `src/features/<name>/api/service.ts`    |\n| Query options         | `src/features/<name>/api/queries.ts`    |\n| Mutation options      | `src/features/<name>/api/mutations.ts`  |\n| Zod schemas           | `src/features/<name>/schemas/<name>.ts` |\n| Filter/select options | `src/features/<name>/constants/`        |\n| Nav config            | `src/config/nav-config.ts`              |\n| Types                 | `src/types/index.ts`                    |\n| Mock data             | `src/constants/mock-api-<name>.ts`      |\n| Search params         | `src/lib/searchparams.ts`               |\n| Query client          | `src/lib/query-client.ts`               |\n| Theme CSS             | `src/styles/themes/<name>.css`          |\n| Theme registry        | `src/components/themes/theme.config.ts` |\n| Custom hook           | `src/hooks/`                            |\n| Icons registry        | `src/components/icons.tsx`              |\n\n---\n\n## Adding a New Feature (End-to-End)\n\nWhen a user asks to add a feature (e.g., \"add an orders page\"), follow these steps in order. Each step below shows the minimal pattern — see reference files for full templates.\n\n### Step 1: Mock API (`src/constants/mock-api-<name>.ts`)\n\nSee [references/mock-api-guide.md](references/mock-api-guide.md) for the complete template. Key structure:\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\nexport type Order = {\n  id: number;\n  customer: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\nexport const fakeOrders = {\n  records: [] as Order[],\n  initialize() {\n    /* generate with faker */\n  },\n  async getOrders({ page, limit, search, sort }) {\n    /* filter, sort, paginate, return { items, total_items } */\n  },\n  async getOrderById(id: number) {\n    /* find by id */\n  },\n  async createOrder(data) {\n    /* push to records */\n  },\n  async updateOrder(id, data) {\n    /* merge into record */\n  },\n  async deleteOrder(id) {\n    /* filter out */\n  }\n};\nfakeOrders.initialize();\n```\n\nEvery method should call `await delay(800)` to simulate network latency. Use `matchSorter` for search. Return `{ items, total_items }` from list methods.\n\n### Step 2: API Layer (`src/features/<name>/api/`)\n\nEach feature has 4 API files: **types** → **service** → **queries** → **mutations**.\n\n**Types** (`api/types.ts`) — re-export the entity type from mock API, plus filter/response/payload types:\n\n```tsx\nexport type { Order } from '@/constants/mock-api-orders';\nexport type OrderFilters = { page?: number; limit?: number; search?: string; sort?: string };\nexport type OrdersResponse = { items: Order[]; total_items: number };\nexport type OrderMutationPayload = { customer: string; status: string; total: number };\n```\n\n**Service** (`api/service.ts`) — data access layer. One exported function per operation:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse, OrderMutationPayload } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function getOrderById(id: number) {\n  return fakeOrders.getOrderById(id);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\nexport async function updateOrder(id: number, data: OrderMutationPayload) {\n  return fakeOrders.updateOrder(id, data);\n}\nexport async function deleteOrder(id: number) {\n  return fakeOrders.deleteOrder(id);\n}\n```\n\n**Queries** (`api/queries.ts`) — query key factory + query options:\n\n```tsx\nimport { queryOptions } from '@tanstack/react-query';\nimport { getOrders, getOrderById } from './service';\nimport type { Order, OrderFilters } from './types';\n\nexport type { Order };\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({\n    queryKey: orderKeys.list(filters),\n    queryFn: () => getOrders(filters)\n  });\n\nexport const orderByIdOptions = (id: number) =>\n  queryOptions({\n    queryKey: orderKeys.detail(id),\n    queryFn: () => getOrderById(id)\n  });\n```\n\n**Mutations** (`api/mutations.ts`) — use `mutationOptions` + `getQueryClient()` (not custom hooks with `useQueryClient()`):\n\n```tsx\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { createOrder, updateOrder, deleteOrder } from './service';\nimport { orderKeys } from './queries';\nimport type { OrderMutationPayload } from './types';\n\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data: OrderMutationPayload) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const updateOrderMutation = mutationOptions({\n  mutationFn: ({ id, values }: { id: number; values: OrderMutationPayload }) =>\n    updateOrder(id, values),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const deleteOrderMutation = mutationOptions({\n  mutationFn: (id: number) => deleteOrder(id),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n```\n\n`mutationOptions` is the right abstraction because it works outside React (event handlers, tests, utilities), composes via spread at the call site, and uses `getQueryClient()` which handles both SSR (fresh per request) and client (singleton) correctly. See [references/query-abstractions.md](references/query-abstractions.md) for the full rationale.\n\n### Step 3: Zod Schema (`src/features/<name>/schemas/<name>.ts`)\n\n```tsx\nimport { z } from 'zod';\n\nexport const orderSchema = z.object({\n  customer: z.string().min(2, 'Customer name must be at least 2 characters'),\n  status: z.string().min(1, 'Please select a status'),\n  total: z.number({ message: 'Total is required' })\n});\n\nexport type OrderFormValues = z.infer<typeof orderSchema>;\n```\n\n### Step 4: Feature Components\n\nCreate `src/features/<name>/components/` with:\n\n**Listing page** (server component — `<name>-listing.tsx`):\n\n```tsx\nimport { HydrationBoundary, dehydrate } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport { ordersQueryOptions } from '../api/queries';\nimport { OrderTable, OrderTableSkeleton } from './orders-table';\nimport { Suspense } from 'react';\n\nexport default function OrderListingPage() {\n  const page = searchParamsCache.get('page');\n  const search = searchParamsCache.get('name');\n  const pageLimit = searchParamsCache.get('perPage');\n  const sort = searchParamsCache.get('sort');\n\n  const filters = {\n    page,\n    limit: pageLimit,\n    ...(search && { search }),\n    ...(sort.length > 0 && { sort: JSON.stringify(sort) })\n  };\n\n  const queryClient = getQueryClient();\n  void queryClient.prefetchQuery(ordersQueryOptions(filters));\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      <Suspense fallback={<OrderTableSkeleton />}>\n        <OrderTable />\n      </Suspense>\n    </HydrationBoundary>\n  );\n}\n```\n\n**Table + skeleton** (client component — `orders-table/index.tsx`):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';\nimport { getSortingStateParser } from '@/lib/data-table-parsers';\nimport { useDataTable } from '@/hooks/use-data-table';\nimport { DataTable } from '@/components/data-table/data-table';\nimport { DataTableToolbar } from '@/components/data-table/data-table-toolbar';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { ordersQueryOptions } from '../../api/queries';\nimport { columns } from './columns';\n\nexport function OrderTable() {\n  const [params] = useQueryStates({\n    page: parseAsInteger.withDefault(1),\n    perPage: parseAsInteger.withDefault(10),\n    name: parseAsString,\n    sort: getSortingStateParser().withDefault([]) // same parser as searchParamsCache\n  });\n\n  const filters = {\n    page: params.page,\n    limit: params.perPage,\n    ...(params.name && { search: params.name }),\n    ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })\n  };\n\n  const { data } = useSuspenseQuery(ordersQueryOptions(filters));\n\n  const { table } = useDataTable({\n    data: data.items,\n    columns,\n    pageCount: Math.ceil(data.total_items / params.perPage),\n    shallow: true,\n    debounceMs: 500,\n    initialState: { columnPinning: { start: [], end: ['actions'] } }\n  });\n\n  return (\n    <DataTable table={table}>\n      <DataTableToolbar table={table} />\n    </DataTable>\n  );\n}\n\nexport function OrderTableSkeleton() {\n  return (\n    <div className='space-y-4 p-4'>\n      <Skeleton className='h-10 w-full' />\n      <Skeleton className='h-96 w-full' />\n    </div>\n  );\n}\n```\n\n**Column definitions** (`orders-table/columns.tsx`):\n\nEach column needs `id`, `accessorKey` (or `accessorFn`), `header` with `DataTableColumnHeader`, and optionally `meta` for filtering + `enableColumnFilter: true`. Type columns as `ColumnDef<DataTableFeatures, Order>[]` (TanStack Table v9; `DataTableFeatures` from `@/lib/data-table-features` types `meta`).\n\n```tsx\nexport const columns: ColumnDef<DataTableFeatures, Order>[] = [\n  {\n    id: 'customer',\n    accessorKey: 'customer',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Customer' />,\n    meta: { label: 'Customer', placeholder: 'Search...', variant: 'text', icon: Icons.text },\n    enableColumnFilter: true\n  },\n  {\n    id: 'status',\n    accessorKey: 'status',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Status' />,\n    cell: ({ cell }) => (\n      <Badge variant='outline' className='capitalize'>\n        {cell.getValue<string>()}\n      </Badge>\n    ),\n    enableColumnFilter: true,\n    meta: { label: 'Status', variant: 'multiSelect', options: STATUS_OPTIONS }\n  },\n  { id: 'actions', cell: ({ row }) => <CellAction data={row.original} /> }\n];\n```\n\nFilter `meta.variant` options: `text`, `number`, `range`, `date`, `dateRange`, `select`, `multiSelect`, `boolean`. For multiSelect, provide `options: { value, label, icon? }[]`.\n\n**Cell actions** (`orders-table/cell-action.tsx`):\n\nPattern: `DropdownMenu` with edit/delete items + `AlertModal` for delete confirmation + `useMutation` for the delete API call.\n\n```tsx\nimport { deleteOrderMutation } from '../../api/mutations';\n\nexport const CellAction: React.FC<{ data: Order }> = ({ data }) => {\n  const [deleteOpen, setDeleteOpen] = useState(false);\n  const deleteMutation = useMutation({\n    ...deleteOrderMutation,\n    onSuccess: () => {\n      toast.success('Deleted');\n      setDeleteOpen(false);\n    }\n  });\n  return (\n    <>\n      <AlertModal\n        isOpen={deleteOpen}\n        onClose={() => setDeleteOpen(false)}\n        onConfirm={() => deleteMutation.mutate(data.id)}\n        loading={deleteMutation.isPending}\n      />\n      <DropdownMenu modal={false}>\n        <DropdownMenuTrigger asChild>\n          <Button variant='ghost' className='h-8 w-8 p-0'>\n            <Icons.ellipsis className='h-4 w-4' />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align='end'>\n          <DropdownMenuLabel>Actions</DropdownMenuLabel>\n          <DropdownMenuItem onClick={() => router.push(`/dashboard/orders/${data.id}`)}>\n            <Icons.edit className='mr-2 h-4 w-4' /> Edit\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setDeleteOpen(true)}>\n            <Icons.trash className='mr-2 h-4 w-4' /> Delete\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </>\n  );\n};\n```\n\nFor **sheet-based editing** (like Users), replace `router.push` with opening a `<FormSheet>` — see the Forms section below.\n\n### Step 5: Page Route (`src/app/dashboard/<name>/page.tsx`)\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderListingPage from '@/features/orders/components/order-listing';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport type { SearchParams } from 'nuqs/server';\n\nexport const metadata = { title: 'Dashboard: Orders' };\ntype PageProps = { searchParams: Promise<SearchParams> };\n\nexport default async function Page(props: PageProps) {\n  const searchParams = await props.searchParams;\n  searchParamsCache.parse(searchParams);\n\n  return (\n    <PageContainer\n      scrollable={false}\n      pageTitle='Orders'\n      pageDescription='Manage your orders.'\n      pageHeaderAction={/* Add button — Link or SheetTrigger */}\n    >\n      <OrderListingPage />\n    </PageContainer>\n  );\n}\n```\n\n**PageContainer props**: `scrollable`, `pageTitle`, `pageDescription`, `pageHeaderAction` (React node for the top-right button), `infoContent` (help sidebar), `access` + `accessFallback` (RBAC gating).\n\n**Detail/Edit page** (`src/app/dashboard/<name>/[id]/page.tsx`):\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderViewPage from '@/features/orders/components/order-view-page';\n\nexport const metadata = { title: 'Dashboard: Order Details' };\ntype PageProps = { params: Promise<{ id: string }> };\n\nexport default async function Page(props: PageProps) {\n  const { id } = await props.params;\n  return (\n    <PageContainer scrollable pageTitle='Order Details'>\n      <OrderViewPage orderId={id} />\n    </PageContainer>\n  );\n}\n```\n\n**View page component** (client — handles new vs edit):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { notFound } from 'next/navigation';\nimport { orderByIdOptions } from '../api/queries';\nimport OrderForm from './order-form';\n\nexport default function OrderViewPage({ orderId }: { orderId: string }) {\n  if (orderId === 'new') return <OrderForm initialData={null} pageTitle='Create Order' />;\n  const { data } = useSuspenseQuery(orderByIdOptions(Number(orderId)));\n  if (!data) notFound();\n  return <OrderForm initialData={data} pageTitle='Edit Order' />;\n}\n```\n\n### Step 6: Search Params (`src/lib/searchparams.ts`)\n\nAdd any new filter keys. Existing params: `page`, `perPage`, `name`, `gender`, `category`, `role`, `sort`.\n\n### Step 7: Navigation (`src/config/nav-config.ts`)\n\n```tsx\n{ title: 'Orders', url: '/dashboard/orders', icon: 'product', items: [] }\n```\n\n### Step 8: Icons (`src/components/icons.tsx`)\n\nTo register a new icon, import from `@tabler/icons-react` and add to the `Icons` object:\n\n```tsx\nimport { IconShoppingCart } from '@tabler/icons-react';\nexport const Icons = { /* ...existing */ cart: IconShoppingCart };\n```\n\nNever import `@tabler/icons-react` anywhere else. Always use `Icons.keyName`.\n\n**Existing icon keys** (partial): `dashboard`, `product`, `kanban`, `chat`, `forms`, `user`, `teams`, `billing`, `settings`, `add`, `edit`, `trash`, `search`, `check`, `close`, `clock`, `ellipsis`, `text`, `calendar`, `upload`, `spinner`, `chevronDown/Left/Right/Up`, `sun`, `moon`, `palette`, `pro`, `workspace`, `notification`.\n\n---\n\n## Forms\n\nForms use **TanStack Form + Zod** with `useAppForm` + `useFormFields<T>()` and `useMutation` for submission. See [references/forms-guide.md](references/forms-guide.md) for all field types, validation strategies, multi-step forms, and advanced patterns.\n\n### Page Form (Create/Edit on a dedicated route)\n\nThe full pattern is shown in Steps 1-4 above. The key structure:\n\n1. **Schema** — Zod schema + inferred type in `schemas/<name>.ts`\n2. **Form component** — `useAppForm({ defaultValues, validators: { onSubmit: schema }, onSubmit })` + `useFormFields<T>()` for typed fields\n3. **Mutations** — `useMutation({ ...createOrderMutation, onSuccess: () => { toast(); router.push() } })`, spread shared mutation options from `api/mutations.ts` and layer on UI callbacks\n4. **View page** — client component that checks `id === 'new'` for create vs `useSuspenseQuery(byIdOptions)` for edit\n\n### Sheet Form (Inline create/edit in a side panel)\n\nFor features where a separate page is overkill (like Users). The sheet manages open state; the form uses a `form` attribute to connect to the sheet footer's submit button.\n\n```tsx\n'use client';\nimport { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';\n\nexport function OrderFormSheet({\n  order,\n  open,\n  onOpenChange\n}: {\n  order?: Order;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}) {\n  const isEdit = !!order;\n  const mutation = useMutation({\n    ...(isEdit ? updateOrderMutation : createOrderMutation),\n    onSuccess: () => {\n      onOpenChange(false);\n    }\n  });\n  const form = useAppForm({\n    defaultValues: { customer: order?.customer ?? '' /* ... */ } as OrderFormValues,\n    validators: { onSubmit: orderSchema },\n    onSubmit: async ({ value }) => {\n      await mutation.mutateAsync(value);\n    }\n  });\n  const { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n  return (\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className='flex flex-col'>\n        <SheetHeader>\n          <SheetTitle>{isEdit ? 'Edit' : 'New'} Order</SheetTitle>\n        </SheetHeader>\n        <div className='flex-1 overflow-auto'>\n          <form.AppForm>\n            <form.Form id='order-sheet-form' className='space-y-4'>\n              <FormTextField name='customer' label='Customer' required />\n              <FormSelectField name='status' label='Status' required options={STATUS_OPTIONS} />\n            </form.Form>\n          </form.AppForm>\n        </div>\n        <SheetFooter>\n          <Button variant='outline' onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button type='submit' form='order-sheet-form' disabled={mutation.isPending}>\n            {mutation.isPending ? 'Saving...' : isEdit ? 'Update' : 'Create'}\n          </Button>\n        </SheetFooter>\n      </SheetContent>\n    </Sheet>\n  );\n}\n```\n\nFor cell actions, add `const [editOpen, setEditOpen] = useState(false)` and render `<OrderFormSheet order={data} open={editOpen} onOpenChange={setEditOpen} />` with a `<DropdownMenuItem onClick={() => setEditOpen(true)}>`. For the page header \"Add\" button, create a trigger component that manages `open` state and renders the sheet.\n\n**Available field components** from `useFormFields<T>()`: `FormTextField`, `FormTextareaField`, `FormSelectField`, `FormCheckboxField`, `FormSwitchField`, `FormRadioGroupField`, `FormSliderField`, `FormFileUploadField`.\n\n---\n\n## Data Fetching with React Query\n\nThe pattern is: server prefetch → HydrationBoundary → client useSuspenseQuery.\n\n1. **Server**: `void queryClient.prefetchQuery(options)` — fire-and-forget during SSR streaming\n2. **Client**: `useSuspenseQuery(options)` — picks up dehydrated data, suspends until resolved\n3. **HydrationBoundary + dehydrate**: bridges server cache → client cache\n4. **Suspense fallback**: skeleton shown while data streams\n\n**Why `useSuspenseQuery` not `useQuery`:** `useQuery` doesn't integrate with Suspense — it shows loading even when data is prefetched. `useSuspenseQuery` picks up the dehydrated pending query. Once cached (within `staleTime: 60s`), subsequent visits are instant.\n\n**Mutations** use `mutationOptions` + `getQueryClient()` in `mutations.ts`, composed via spread at the call site:\n\n```tsx\n// In mutations.ts — shared config\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\n// In component — spread + layer UI callbacks\nconst mutation = useMutation({\n  ...createOrderMutation,\n  onSuccess: () => toast.success('Created')\n});\n```\n\nSee [references/query-abstractions.md](references/query-abstractions.md) for why `mutationOptions`/`queryOptions` are the right abstraction over custom hooks.\n\n---\n\n## Navigation & RBAC\n\nConfigure in `src/config/nav-config.ts`. Items are filtered client-side in `src/hooks/use-nav.ts` using Clerk.\n\n**Access control properties** on nav items:\n\n- `requireOrg: boolean` — requires active Clerk organization\n- `permission: string` — requires specific Clerk permission\n- `role: string` — requires specific Clerk role\n- `plan: string` — requires subscription plan (server-side)\n- `feature: string` — requires feature flag (server-side)\n\nItems without `access` are visible to everyone. All client-side checks are synchronous — no loading states.\n\n---\n\n## Themes\n\nSee [references/theming-guide.md](references/theming-guide.md) for the complete guide. Quick steps:\n\n1. Create `src/styles/themes/<name>.css` with OKLCH color tokens + `@theme inline` block\n2. Import in `src/styles/theme.css`\n3. Register in `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add Google Fonts in `src/components/themes/font.config.ts`\n\n---\n\n## Code Conventions\n\n- **`cn()`** for class merging — never concatenate className strings\n- **Server components by default** — only add `'use client'` when needed\n- **React Query** — `void prefetchQuery()` on server + `useSuspenseQuery` on client\n- **API layer** — `types.ts` → `service.ts` → `queries.ts` → `mutations.ts` per feature; `queryOptions`/`mutationOptions` as base abstractions (not custom hooks); `getQueryClient()` in mutations (not `useQueryClient()`); key factories (`entityKeys.all/list/detail`); components never import mock APIs directly\n- **nuqs** — `searchParamsCache` on server, `useQueryStates` on client with `shallow: true`\n- **Icons** — only from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` + `useFormFields<T>()` from `@/components/ui/tanstack-form`\n- **Page headers** — `PageContainer` props, never import `<Heading>` manually\n- **Sort parser** — use `getSortingStateParser` from `@/lib/data-table-parsers` (same parser as `useDataTable`)\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n",".agents/skills/kiranism-shadcn-dashboard/references/charts-guide.md":"# Charts & Analytics Guide\n\n## Table of Contents\n\n1. [Overview Architecture](#overview-architecture)\n2. [Parallel Routes Pattern](#parallel-routes-pattern)\n3. [Chart Components](#chart-components)\n4. [Stats Cards](#stats-cards)\n5. [Skeleton Loading](#skeleton-loading)\n6. [Adding a New Chart Section](#adding-a-new-chart-section)\n\n---\n\n## Overview Architecture\n\nThe analytics dashboard at `/dashboard/overview` uses **Next.js parallel routes** to load multiple chart sections independently. Each chart slot streams in as its data becomes ready — no waterfall, no blocking.\n\n**File structure:**\n\n```\nsrc/app/dashboard/overview/\n├── layout.tsx              # Composes all slots into a grid\n├── @area_stats/\n│   ├── page.tsx            # Async server component (fetches data)\n│   ├── loading.tsx         # Skeleton shown while streaming\n│   └── error.tsx           # Error boundary if fetch fails\n├── @bar_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n├── @pie_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n└── @sales/\n    ├── page.tsx\n    ├── loading.tsx\n    └── error.tsx\n\nsrc/features/overview/components/\n├── area-graph.tsx          # Client chart component\n├── area-graph-skeleton.tsx # Matching skeleton\n├── bar-graph.tsx\n├── bar-graph-skeleton.tsx\n├── pie-graph.tsx\n├── pie-graph-skeleton.tsx\n├── recent-sales.tsx\n└── recent-sales-skeleton.tsx\n```\n\n---\n\n## Parallel Routes Pattern\n\n### Layout (`layout.tsx`)\n\nThe layout receives each parallel route as a prop and arranges them in a grid:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats\n}: {\n  sales: React.ReactNode;\n  pie_stats: React.ReactNode;\n  bar_stats: React.ReactNode;\n  area_stats: React.ReactNode;\n}) {\n  return (\n    <PageContainer pageTitle='Dashboard' pageDescription='Overview analytics.'>\n      {/* Stats cards row */}\n      <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>\n        <Card>\n          <CardHeader className='flex flex-row items-center justify-between pb-2'>\n            <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n            <Icons.billing className='h-4 w-4 text-muted-foreground' />\n          </CardHeader>\n          <CardContent>\n            <div className='text-2xl font-bold'>$45,231.89</div>\n            <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n          </CardContent>\n        </Card>\n        {/* ...more stat cards */}\n      </div>\n\n      {/* Charts grid — each slot loads independently */}\n      <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n        <div className='col-span-4'>{area_stats}</div>\n        <div className='col-span-3'>{sales}</div>\n        <div className='col-span-4'>{bar_stats}</div>\n        <div className='col-span-3'>{pie_stats}</div>\n      </div>\n    </PageContainer>\n  );\n}\n```\n\n### Slot Page (`@area_stats/page.tsx`)\n\nEach slot is an async server component that fetches data then renders the chart:\n\n```tsx\nimport { delay } from '@/constants/mock-api';\nimport { AreaGraph } from '@/features/overview/components/area-graph';\n\nexport default async function AreaStatsPage() {\n  await delay(2000); // Simulates API fetch\n  return <AreaGraph />;\n}\n```\n\n### Slot Loading (`@area_stats/loading.tsx`)\n\n```tsx\nimport { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';\n\nexport default function Loading() {\n  return <AreaGraphSkeleton />;\n}\n```\n\n### Slot Error (`@area_stats/error.tsx`)\n\n```tsx\n'use client';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Icons } from '@/components/icons';\n\nexport default function AreaStatsError({ error }: { error: Error }) {\n  return (\n    <Alert variant='destructive'>\n      <Icons.alertCircle className='h-4 w-4' />\n      <AlertTitle>Error</AlertTitle>\n      <AlertDescription>Failed to load area stats: {error.message}</AlertDescription>\n    </Alert>\n  );\n}\n```\n\nEach slot can fail independently without affecting others.\n\n---\n\n## Chart Components\n\nAll chart components are `'use client'` and use **Recharts** wrapped in shadcn's `ChartContainer`.\n\n### Chart Config\n\nEvery chart defines a config object mapping data keys to labels and theme colors:\n\n```tsx\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n```\n\nTheme colors `--chart-1` through `--chart-5` are defined in each theme's CSS file and automatically adapt to light/dark mode.\n\n### Area Chart Example\n\n```tsx\n'use client';\nimport { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Icons } from '@/components/icons';\n\nconst chartData = [\n  { month: 'January', desktop: 186, mobile: 80 },\n  { month: 'February', desktop: 305, mobile: 200 }\n  // ...more months\n];\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n\nexport function AreaGraph() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <CardTitle>Area Chart - Stacked</CardTitle>\n        <Badge variant='outline'>\n          <Icons.trendingUp className='mr-1 h-3 w-3' /> +12.5%\n        </Badge>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <AreaChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey='month'\n              tickLine={false}\n              axisLine={false}\n              tickFormatter={(value) => value.slice(0, 3)}\n            />\n            <ChartTooltip content={<ChartTooltipContent indicator='dot' />} />\n            <Area\n              dataKey='mobile'\n              type='natural'\n              fill='var(--color-mobile)'\n              stroke='var(--color-mobile)'\n              stackId='a'\n            />\n            <Area\n              dataKey='desktop'\n              type='natural'\n              fill='var(--color-desktop)'\n              stroke='var(--color-desktop)'\n              stackId='a'\n            />\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### Bar Chart Pattern\n\nSame structure, using `BarChart` + `Bar`:\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <BarChart data={chartData}>\n    <CartesianGrid vertical={false} />\n    <XAxis dataKey='month' tickLine={false} axisLine={false} />\n    <ChartTooltip content={<ChartTooltipContent />} />\n    <Bar dataKey='desktop' fill='var(--color-desktop)' radius={4} />\n    <Bar dataKey='mobile' fill='var(--color-mobile)' radius={4} />\n  </BarChart>\n</ChartContainer>\n```\n\n### Pie/Donut Chart Pattern\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <PieChart>\n    <ChartTooltip content={<ChartTooltipContent hideLabel />} />\n    <Pie data={chartData} dataKey='visitors' nameKey='browser' innerRadius={30}>\n      <LabelList dataKey='visitors' className='fill-background' />\n    </Pie>\n  </PieChart>\n</ChartContainer>\n```\n\n---\n\n## Stats Cards\n\nStats cards are simple server-rendered `Card` components at the top of the layout — no parallel routes needed since they render instantly:\n\n```tsx\n<Card>\n  <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>\n    <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n    <Icons.billing className='h-4 w-4 text-muted-foreground' />\n  </CardHeader>\n  <CardContent>\n    <div className='text-2xl font-bold'>$45,231.89</div>\n    <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n  </CardContent>\n</Card>\n```\n\nFor dynamic stats that need data fetching, wrap in their own Suspense boundary or parallel route slot.\n\n---\n\n## Skeleton Loading\n\nEach chart has a matching skeleton component. Pattern:\n\n```tsx\nimport { Card, CardContent, CardHeader } from '@/components/ui/card';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport function AreaGraphSkeleton() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <Skeleton className='h-5 w-[140px]' />\n        <Skeleton className='h-4 w-[80px]' />\n      </CardHeader>\n      <CardContent>\n        <Skeleton className='h-[250px] w-full rounded-md' />\n      </CardContent>\n    </Card>\n  );\n}\n```\n\nMatch the skeleton dimensions to the actual chart for smooth visual transitions.\n\n---\n\n## Adding a New Chart Section\n\nTo add a new chart (e.g., line chart for user growth):\n\n### 1. Create the chart component\n\n`src/features/overview/components/line-graph.tsx`:\n\n```tsx\n'use client';\nimport { Line, LineChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  users: { label: 'Users', color: 'var(--chart-3)' }\n} satisfies ChartConfig;\n\nconst chartData = [\n  /* monthly user data */\n];\n\nexport function LineGraph() {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>User Growth</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <LineChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis dataKey='month' tickLine={false} axisLine={false} />\n            <ChartTooltip content={<ChartTooltipContent />} />\n            <Line dataKey='users' type='monotone' stroke='var(--color-users)' strokeWidth={2} />\n          </LineChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### 2. Create matching skeleton\n\n`src/features/overview/components/line-graph-skeleton.tsx`\n\n### 3. Create parallel route slot\n\n```\nsrc/app/dashboard/overview/@line_stats/\n├── page.tsx     → async, fetches data, returns <LineGraph />\n├── loading.tsx  → returns <LineGraphSkeleton />\n├── error.tsx    → error alert\n└── default.tsx  → return null (fallback when route doesn't match)\n```\n\n`default.tsx` is required for parallel routes — return `null` or a fallback:\n\n```tsx\nexport default function Default() {\n  return null;\n}\n```\n\n### 4. Add slot to layout\n\nUpdate `src/app/dashboard/overview/layout.tsx`:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats,\n  line_stats // ← add new slot\n}: {\n  /* ...types */\n}) {\n  return (\n    <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n      {/* existing charts */}\n      <div className='col-span-4'>{line_stats}</div>\n    </div>\n  );\n}\n```\n\n### Available Recharts Components\n\nCommon chart types to use with `ChartContainer`:\n\n- `AreaChart` + `Area` — filled area charts (stacked or standalone)\n- `BarChart` + `Bar` — vertical/horizontal bars\n- `LineChart` + `Line` — line/trend charts\n- `PieChart` + `Pie` — pie/donut charts\n- `RadarChart` + `Radar` — radar/spider charts\n- `RadialBarChart` + `RadialBar` — radial progress bars\n\nAll support `ChartTooltip`, `ChartLegend`, and theme-aware colors via `var(--chart-N)`.\n",".agents/skills/kiranism-shadcn-dashboard/references/forms-guide.md":"# Forms Guide\n\n## Table of Contents\n\n1. [Architecture](#architecture)\n2. [Field Types](#field-types)\n3. [Usage Patterns](#usage-patterns)\n4. [Validation Strategies](#validation-strategies)\n5. [Sheet/Dialog Forms](#sheetdialog-forms)\n6. [Multi-Step Forms](#multi-step-forms)\n7. [Advanced Patterns](#advanced-patterns)\n\n---\n\n## Architecture\n\nThe form system is built on **TanStack Form + Zod** with a composable field layer.\n\n**Key files:**\n\n- `src/components/ui/tanstack-form.tsx` — exports `useAppForm`, `useFormFields<T>()`, composed fields\n- `src/components/ui/form-context.tsx` — contexts, `createFormField`, structural components\n- `src/components/forms/fields/*.tsx` — 8 field type implementations\n\n**Key exports:**\n\n```tsx\nimport { useAppForm, useFormFields } from '@/components/ui/tanstack-form';\n```\n\n- `useAppForm(config)` — creates a form instance with `defaultValues`, `validators`, `onSubmit`\n- `useFormFields<T>()` — returns all 8 typed field components with name autocomplete from `T`\n- `form.AppForm` — context provider wrapper\n- `form.Form` — `<form>` element that handles submit\n- `form.SubmitButton` — auto-disabled when form is invalid or submitting\n- `form.AppField` — low-level render prop for custom fields\n\n---\n\n## Field Types\n\nAll fields accept: `name`, `label`, `description`, `required`, `disabled`, `validators`, `listeners`, `className`.\n\n| Component             | Props                                                                                         | Notes                                   |\n| --------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |\n| `FormTextField`       | `type` (text/email/number/password/tel/url), `placeholder`, `min`, `max`, `step`, `maxLength` | For numbers use `type='number'`         |\n| `FormTextareaField`   | `placeholder`, `rows`, `maxLength`                                                            | Multiline text                          |\n| `FormSelectField`     | `options: {value, label}[]`, `placeholder`                                                    | Single select dropdown                  |\n| `FormCheckboxField`   | `options?: {value, label}[]`                                                                  | Single checkbox or multi-checkbox group |\n| `FormSwitchField`     | —                                                                                             | Toggle switch                           |\n| `FormRadioGroupField` | `options: {value, label}[]`, `orientation`                                                    | Radio button group                      |\n| `FormSliderField`     | `min`, `max`, `step`                                                                          | Range slider                            |\n| `FormFileUploadField` | `maxSize`, `maxFiles`, `accept`                                                               | Drag-and-drop with preview              |\n\n---\n\n## Usage Patterns\n\n### Pattern 1: `useFormFields<T>()` (Recommended)\n\nType-safe field components with name autocomplete:\n\n```tsx\nconst { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n<FormTextField name='customer' label='Customer' required placeholder='Name'\n  validators={{ onBlur: z.string().min(2) }} />\n\n<FormSelectField name='status' label='Status' required options={STATUS_OPTIONS}\n  validators={{ onBlur: z.string().min(1) }} />\n```\n\n### Pattern 2: `form.AppField` render prop\n\nFull control for custom field rendering:\n\n```tsx\n<form.AppField name='framework'>\n  {(field) => (\n    <field.FieldSet>\n      <field.Field>\n        <field.TextField label='Framework' />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Pattern 3: Direct import (no type safety)\n\nFor quick prototyping:\n\n```tsx\nimport { FormTextField } from '@/components/ui/tanstack-form';\n<FormTextField name='name' label='Name' />;\n```\n\n---\n\n## Validation Strategies\n\n### Field-level (recommended for UX)\n\n```tsx\n<FormTextField\n  name='email'\n  label='Email'\n  validators={{\n    onBlur: z.string().email('Invalid email') // Validates when field loses focus\n  }}\n/>\n```\n\n### Form-level (catch-all on submit)\n\n```tsx\nconst form = useAppForm({\n  validators: { onSubmit: orderSchema }, // Validates entire form on submit\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n### Async validation (server-side checks)\n\n```tsx\n<FormTextField\n  name='username'\n  label='Username'\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const exists = await checkUsername(value);\n      return exists ? 'Username taken' : undefined;\n    }\n  }}\n  asyncDebounceMs={500}\n/>\n```\n\n### Linked field validation\n\nFor dependent fields (e.g., confirm password):\n\n```tsx\n<FormTextField\n  name='confirmPassword'\n  label='Confirm Password'\n  validators={{\n    onChangeListenTo: ['password'],\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      return value !== password ? 'Passwords must match' : undefined;\n    }\n  }}\n/>\n```\n\n---\n\n## Sheet/Dialog Forms\n\nThe key pattern for forms inside sheets or dialogs: give the `<form.Form>` an `id`, and use that `id` on the submit button's `form` attribute. This allows the submit button to live outside the form element (e.g., in `SheetFooter`).\n\n```tsx\n<form.AppForm>\n  <form.Form id='my-sheet-form' className='space-y-4'>\n    {/* fields */}\n  </form.Form>\n</form.AppForm>;\n\n{\n  /* In SheetFooter — button is outside the <form> but still submits it */\n}\n<SheetFooter>\n  <Button type='submit' form='my-sheet-form'>\n    Save\n  </Button>\n</SheetFooter>;\n```\n\nOn success, call `onOpenChange(false)` to close the sheet and `form.reset()` for create forms.\n\n---\n\n## Multi-Step Forms\n\nUse `withFieldGroup` + `useAppForm` with `StepButton`:\n\n```tsx\n// Define field groups for each step\nconst Step1 = withFieldGroup({\n  fields: ['name', 'email'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='name' label='Name' />\n        <FormTextField name='email' label='Email' />\n        <form.StepButton direction='next' label='Next' />\n      </>\n    );\n  }\n});\n\nconst Step2 = withFieldGroup({\n  fields: ['address', 'city'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='address' label='Address' />\n        <FormTextField name='city' label='City' />\n        <form.StepButton direction='prev' label='Back' />\n        <form.SubmitButton label='Submit' />\n      </>\n    );\n  }\n});\n```\n\nUse the `useStepper` hook from `src/hooks/use-stepper.tsx` to manage step state.\n\n---\n\n## Advanced Patterns\n\n### Nested objects (dot notation)\n\n```tsx\n<FormTextField name='address.street' label='Street' />\n<FormTextField name='address.city' label='City' />\n```\n\n### Dynamic array rows\n\n```tsx\n<form.AppField name='items' mode='array'>\n  {(field) => (\n    <>\n      {field.state.value.map((_, i) => (\n        <form.AppField key={i} name={`items[${i}].name`}>\n          {(subField) => <subField.TextField label={`Item ${i + 1}`} />}\n        </form.AppField>\n      ))}\n      <Button onClick={() => field.pushValue({ name: '' })}>Add Row</Button>\n    </>\n  )}\n</form.AppField>\n```\n\n### Side effects with listeners\n\n```tsx\n<FormSelectField\n  name='country'\n  label='Country'\n  options={countryOptions}\n  listeners={{\n    onChange: ({ value }) => {\n      // Reset city when country changes\n      form.setFieldValue('city', '');\n    }\n  }}\n/>\n```\n\n### Custom field with `form.AppField`\n\nFor fields not covered by the built-in 8 types:\n\n```tsx\n<form.AppField name='color'>\n  {(field) => (\n    <field.FieldSet>\n      <Label>Pick a color</Label>\n      <field.Field>\n        <input\n          type='color'\n          value={field.state.value}\n          onChange={(e) => field.handleChange(e.target.value)}\n        />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Form-level errors\n\nDisplay errors that apply to the whole form (e.g., server errors):\n\n```tsx\nimport { FormErrors } from '@/components/ui/form-context';\n\n<form.AppForm>\n  <form.Form>\n    <FormErrors /> {/* Renders form-level validation errors */}\n    {/* fields... */}\n  </form.Form>\n</form.AppForm>;\n```\n",".agents/skills/kiranism-shadcn-dashboard/references/mock-api-guide.md":"# Mock API Guide\n\n## Table of Contents\n\n1. [Structure](#structure)\n2. [Full Template](#full-template)\n3. [Key Patterns](#key-patterns)\n4. [Integrating with React Query](#integrating-with-react-query)\n\n---\n\n## Structure\n\nEach mock API file lives in `src/constants/mock-api-<name>.ts` and is a self-contained in-memory database. It uses:\n\n- **faker** for generating sample data\n- **match-sorter** for fuzzy search across fields\n- **delay** (from `./mock-api`) to simulate network latency\n\nThe `delay` function is exported from `src/constants/mock-api.ts`:\n\n```tsx\nexport async function delay(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n```\n\n---\n\n## Full Template\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\n// 1. Define the entity type\nexport type Order = {\n  id: number;\n  customer: string;\n  email: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\n// 2. Create the fake database object\nexport const fakeOrders = {\n  records: [] as Order[],\n\n  // 3. Initialize with faker data\n  initialize() {\n    const statuses = ['pending', 'processing', 'completed', 'cancelled'];\n    for (let i = 1; i <= 20; i++) {\n      this.records.push({\n        id: i,\n        customer: faker.person.fullName(),\n        email: faker.internet.email(),\n        status: faker.helpers.arrayElement(statuses),\n        total: parseFloat(faker.commerce.price({ min: 10, max: 500 })),\n        created_at: faker.date.between({ from: '2023-01-01', to: Date.now() }).toISOString(),\n        updated_at: faker.date.recent().toISOString()\n      });\n    }\n  },\n\n  // 4. Get all with optional search (used internally)\n  async getAll({ search }: { search?: string } = {}) {\n    let items = [...this.records];\n    if (search) {\n      items = matchSorter(items, search, {\n        keys: ['customer', 'email']\n      });\n    }\n    return items;\n  },\n\n  // 5. Paginated list with filtering and sorting\n  async getOrders(params: {\n    page?: number;\n    limit?: number;\n    search?: string;\n    statuses?: string;\n    sort?: string;\n  }) {\n    await delay(800);\n    const { page = 1, limit = 10, search, statuses, sort } = params;\n\n    let items = await this.getAll({ search });\n\n    // Filter by comma-separated values\n    if (statuses) {\n      const statusList = statuses.split('.');\n      items = items.filter((item) => statusList.includes(item.status));\n    }\n\n    // Sort by column\n    if (sort) {\n      const parsedSort = JSON.parse(sort) as { id: string; desc: boolean }[];\n      if (parsedSort.length > 0) {\n        const { id, desc } = parsedSort[0];\n        items.sort((a, b) => {\n          const aVal = a[id as keyof Order];\n          const bVal = b[id as keyof Order];\n          if (aVal < bVal) return desc ? 1 : -1;\n          if (aVal > bVal) return desc ? -1 : 1;\n          return 0;\n        });\n      }\n    }\n\n    // Paginate\n    const total_items = items.length;\n    items = items.slice((page - 1) * limit, page * limit);\n\n    return { items, total_items };\n  },\n\n  // 6. Get single record by ID\n  async getOrderById(id: number) {\n    await delay(800);\n    return this.records.find((r) => r.id === id) || null;\n  },\n\n  // 7. Create\n  async createOrder(data: Omit<Order, 'id' | 'created_at' | 'updated_at'>) {\n    await delay(800);\n    const newRecord: Order = {\n      ...data,\n      id: this.records.length + 1,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString()\n    };\n    this.records.push(newRecord);\n    return newRecord;\n  },\n\n  // 8. Update\n  async updateOrder(id: number, data: Partial<Order>) {\n    await delay(800);\n    const idx = this.records.findIndex((r) => r.id === id);\n    if (idx === -1) return null;\n    this.records[idx] = {\n      ...this.records[idx],\n      ...data,\n      updated_at: new Date().toISOString()\n    };\n    return this.records[idx];\n  },\n\n  // 9. Delete\n  async deleteOrder(id: number) {\n    await delay(800);\n    this.records = this.records.filter((r) => r.id !== id);\n    return true;\n  }\n};\n\n// 10. Auto-initialize on import\nfakeOrders.initialize();\n```\n\n---\n\n## Key Patterns\n\n### Search with match-sorter\n\nAlways specify which fields to search across:\n\n```tsx\nmatchSorter(items, search, { keys: ['customer', 'email', 'status'] });\n```\n\n### Comma-separated filter values\n\nFor multi-select filters (roles, statuses), the URL param uses `.` as delimiter:\n\n```tsx\nif (statuses) {\n  const list = statuses.split('.');\n  items = items.filter((item) => list.includes(item.status));\n}\n```\n\n### Computed column sorting\n\nWhen a table has a computed column (e.g., combining first_name + last_name into \"name\"), handle it in the sort logic:\n\n```tsx\nif (id === 'name') {\n  const aName = `${a.first_name} ${a.last_name}`;\n  const bName = `${b.first_name} ${b.last_name}`;\n  return desc ? bName.localeCompare(aName) : aName.localeCompare(bName);\n}\n```\n\n### Return shape\n\nList methods must return `{ items, total_items }` (or `{ products, total }` etc. — match the query option expectations). The total is the count **before** pagination, used for `pageCount` calculation.\n\n---\n\n## Integrating with the API Layer\n\nThe mock API is only imported in `service.ts`. Queries and components import from the service and types files:\n\n```\nmock-api-orders.ts  →  api/service.ts  →  api/queries.ts  →  components\n(data source)          (data access)       (key factory +     (useSuspenseQuery\n                                            queryOptions)       + useMutation)\n```\n\n**service.ts** imports from the mock API:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\n```\n\n**queries.ts** imports from service, uses key factories:\n\n```tsx\nimport { getOrders } from './service';\nimport type { OrderFilters } from './types';\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({ queryKey: orderKeys.list(filters), queryFn: () => getOrders(filters) });\n```\n\n**Mutations** in components use service functions + key factories:\n\n```tsx\nimport { createOrder } from '../api/service';\nimport { orderKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })\n});\n```\n",".agents/skills/kiranism-shadcn-dashboard/references/query-abstractions.md":"# TanStack Query Abstractions (v5)\n\nThe core insight: **`queryOptions` and `mutationOptions` are the right abstraction — not custom hooks.**\n\n---\n\n## Query Abstraction\n\n### The Pattern\n\n```ts\n// queries/invoice.ts\nimport { queryOptions } from '@tanstack/react-query';\n\nexport function invoiceOptions(id: number) {\n  return queryOptions({\n    queryKey: ['invoice', id],\n    queryFn: () => fetchInvoice(id)\n  });\n}\n\nexport function invoiceListOptions(filters: InvoiceFilters) {\n  return queryOptions({\n    queryKey: ['invoices', filters],\n    queryFn: () => fetchInvoices(filters),\n    staleTime: 30_000\n  });\n}\n```\n\n### Usage — always compose at the call site\n\n```ts\n// basic\nconst { data } = useQuery(invoiceOptions(id));\n\n// with suspense — same options, different hook\nconst { data } = useSuspenseQuery(invoiceOptions(id));\n\n// with extra options spread on top — full type inference, no TS pain\nconst { data } = useQuery({\n  ...invoiceOptions(id),\n  select: (invoice) => invoice.createdAt, // data infers as string | undefined\n  enabled: !!id\n});\n\n// prefetch in a route loader (works outside React — this is why hooks are wrong)\nawait queryClient.prefetchQuery(invoiceOptions(id));\n\n// read from cache imperatively — queryKey is typed via DataTag symbol\nconst invoice = queryClient.getQueryData(invoiceOptions(id).queryKey);\n\n// invalidate\nqueryClient.invalidateQueries({ queryKey: invoiceOptions(id).queryKey });\n```\n\n### Why NOT a custom hook\n\nCustom hooks like `useInvoice(id)` have three critical problems:\n\n1. **Hooks only work in components/hooks** — but queries are now used in route loaders, server prefetching, event handlers, and server components. `queryOptions` is just a plain function — works anywhere.\n2. **They share logic, not configuration** — what you actually want to share is the `queryKey` + `queryFn` config. Hooks are the wrong primitive for that.\n3. **They lock you to one hook** — you can't use `useInvoice()` with `useSuspenseQuery`, `useQueries`, or imperative `queryClient` methods.\n\n### Why NOT `UseQueryOptions` type directly\n\n```ts\n// BAD — data becomes unknown\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions>) { ... }\n\n// STILL BAD — select breaks with TS error\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions<Invoice>>) { ... }\n// select: (invoice) => invoice.createdAt\n// Error: Type 'string' is not assignable to type 'Invoice'\n```\n\n`queryOptions` solves this via a `DataTag` symbol on the queryKey — full inference, zero manual generics.\n\n### Custom hooks are still fine on top\n\nIf a component always uses the same composition, a hook is fine — but build it _on top of_ `queryOptions`:\n\n```ts\n// OK — hook built on queryOptions\nfunction useInvoice(id: number) {\n  return useQuery(invoiceOptions(id));\n}\n\n// OK — hook that adds per-feature defaults\nfunction useInvoiceWithSuspense(id: number) {\n  return useSuspenseQuery(invoiceOptions(id));\n}\n```\n\n---\n\n## Mutation Abstraction\n\n### The Pattern\n\n```ts\n// mutations/invoice.ts\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\n\nexport const createInvoiceMutation = mutationOptions({\n  mutationFn: (data: CreateInvoiceInput) => createInvoice(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n\nexport const updateInvoiceMutation = mutationOptions({\n  mutationFn: ({ id, ...data }: UpdateInvoiceInput) => updateInvoice(id, data),\n  onSuccess: (updated) => {\n    const qc = getQueryClient();\n    qc.setQueryData(invoiceOptions(updated.id).queryKey, updated);\n    qc.invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n```\n\n> **Note on queryClient**: Import `getQueryClient()` directly — do NOT pass `queryClient` as a function argument. The `getQueryClient()` pattern handles both SSR (fresh per request) and client (singleton) correctly.\n\n### Usage\n\n```ts\n// basic\nconst { mutate } = useMutation(createInvoiceMutation);\n\n// composed — add per-usage callbacks on top\nconst { mutate } = useMutation({\n  ...createInvoiceMutation,\n  onError: (err) => toast.error(err.message),\n  onSuccess: (data) => {\n    // this runs AFTER the shared onSuccess above\n    router.push(`/invoices/${data.id}`);\n  }\n});\n```\n\n---\n\n## Rules Summary\n\n| Rule                                                          | Reason                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| Use `queryOptions()` not custom hooks as the base abstraction | Works everywhere — loaders, server, imperative calls                   |\n| Keep options factories lean — no extra config params          | Best abstractions are not configurable                                 |\n| Compose extra options at the call site via spread             | Full TS inference without manual generics                              |\n| Import `getQueryClient()` in mutation files                   | Handles SSR/client correctly without prop drilling                     |\n| Co-locate `queryKey` inside `queryOptions`                    | Typed key reuse in `invalidateQueries`, `setQueryData`, `getQueryData` |\n| Custom hooks are fine — but built ON TOP of `queryOptions`    | Hooks for component convenience, `queryOptions` for sharing config     |\n",".agents/skills/kiranism-shadcn-dashboard/references/theming-guide.md":"# Theme Creation Guide\n\n## Table of Contents\n\n1. [Create Theme CSS](#1-create-theme-css)\n2. [Import Theme](#2-import-theme)\n3. [Register Theme](#3-register-theme)\n4. [Add Custom Fonts](#4-add-custom-fonts-optional)\n5. [Set as Default](#5-set-as-default-optional)\n6. [Required Tokens](#required-tokens)\n7. [Color Format Reference](#color-format-reference)\n\n---\n\n## 1. Create Theme CSS\n\nCreate `src/styles/themes/<name>.css`:\n\n```css\n/* Light mode */\n[data-theme='your-theme'] {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(...);\n  --card-foreground: oklch(...);\n  --popover: oklch(...);\n  --popover-foreground: oklch(...);\n  --primary: oklch(...);\n  --primary-foreground: oklch(...);\n  --secondary: oklch(...);\n  --secondary-foreground: oklch(...);\n  --muted: oklch(...);\n  --muted-foreground: oklch(...);\n  --accent: oklch(...);\n  --accent-foreground: oklch(...);\n  --destructive: oklch(...);\n  --destructive-foreground: oklch(...);\n  --border: oklch(...);\n  --input: oklch(...);\n  --ring: oklch(...);\n  --chart-1: oklch(...);\n  --chart-2: oklch(...);\n  --chart-3: oklch(...);\n  --chart-4: oklch(...);\n  --chart-5: oklch(...);\n  --sidebar: oklch(...);\n  --sidebar-foreground: oklch(...);\n  --sidebar-primary: oklch(...);\n  --sidebar-primary-foreground: oklch(...);\n  --sidebar-accent: oklch(...);\n  --sidebar-accent-foreground: oklch(...);\n  --sidebar-border: oklch(...);\n  --sidebar-ring: oklch(...);\n  --font-sans: 'Font Name', sans-serif;\n  --font-mono: 'Mono Font', monospace;\n  --radius: 0.5rem;\n  --spacing: 0.25rem;\n}\n\n/* Dark mode */\n[data-theme='your-theme'].dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  /* ... all tokens with dark values */\n}\n\n/* Tailwind integration (required) */\n[data-theme='your-theme'] {\n  @theme inline {\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-destructive-foreground: var(--destructive-foreground);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n    --font-sans: var(--font-sans);\n    --font-mono: var(--font-mono);\n    --font-serif: var(--font-serif);\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n  }\n}\n```\n\n## 2. Import Theme\n\nAdd to `src/styles/theme.css`:\n\n```css\n@import './themes/your-theme.css';\n```\n\n## 3. Register Theme\n\nAdd to `THEMES` array in `src/components/themes/theme.config.ts`:\n\n```typescript\n{ name: 'Your Theme', value: 'your-theme' }\n```\n\nThe `value` must exactly match the `data-theme` attribute in your CSS.\n\n## 4. Add Custom Fonts (Optional)\n\nOnly if using a Google Font not already loaded.\n\nIn `src/components/themes/font.config.ts`:\n\n```typescript\nimport { Your_Font } from 'next/font/google';\n\nconst fontYourName = Your_Font({\n  subsets: ['latin'],\n  weight: ['400', '500', '700'],\n  variable: '--font-your-name'\n});\n\nexport const fontVariables = cn(\n  // ... existing fonts\n  fontYourName.variable\n);\n```\n\nIn your theme CSS, use the font's **display name** (not the CSS variable):\n\n```css\n--font-sans: 'Your Font', sans-serif;\n```\n\n## 5. Set as Default (Optional)\n\nIn `src/components/themes/theme.config.ts`:\n\n```typescript\nexport const DEFAULT_THEME = 'your-theme';\n```\n\n## Required Tokens\n\nMinimum required: `--background`, `--foreground`, `--card` & `--card-foreground`, `--popover` & `--popover-foreground`, `--primary` & `--primary-foreground`, `--secondary` & `--secondary-foreground`, `--muted` & `--muted-foreground`, `--accent` & `--accent-foreground`, `--destructive` & `--destructive-foreground`, `--border`, `--input`, `--ring`, `--radius`.\n\nOptional: `--chart-*`, `--sidebar-*`, `--font-*`, `--shadow-*`, `--tracking-normal`, `--spacing`.\n\n## Color Format Reference\n\nOKLCH: `oklch(lightness chroma hue)`\n\n- Lightness: 0-1 (0=black, 1=white)\n- Chroma: 0+ (0=gray, higher=saturated)\n- Hue: 0-360 (0=red, 120=green, 240=blue)\n\nSee `src/styles/themes/claude.css` for a complete example.\n",".agents/skills/next-best-practices/SKILL.md":"---\nname: next-best-practices\ndescription: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling\nuser-invocable: false\n---\n\n# Next.js Best Practices\n\nApply these rules when writing or reviewing Next.js code.\n\n## File Conventions\n\nSee [file-conventions.md](./file-conventions.md) for:\n\n- Project structure and special files\n- Route segments (dynamic, catch-all, groups)\n- Parallel and intercepting routes\n- Middleware rename in v16 (middleware → proxy)\n\n## RSC Boundaries\n\nDetect invalid React Server Component patterns.\n\nSee [rsc-boundaries.md](./rsc-boundaries.md) for:\n\n- Async client component detection (invalid)\n- Non-serializable props detection\n- Server Action exceptions\n\n## Async Patterns\n\nNext.js 15+ async API changes.\n\nSee [async-patterns.md](./async-patterns.md) for:\n\n- Async `params` and `searchParams`\n- Async `cookies()` and `headers()`\n- Migration codemod\n\n## Runtime Selection\n\nSee [runtime-selection.md](./runtime-selection.md) for:\n\n- Default to Node.js runtime\n- When Edge runtime is appropriate\n\n## Directives\n\nSee [directives.md](./directives.md) for:\n\n- `'use client'`, `'use server'` (React)\n- `'use cache'` (Next.js)\n\n## Functions\n\nSee [functions.md](./functions.md) for:\n\n- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`\n- Server functions: `cookies`, `headers`, `draftMode`, `after`\n- Generate functions: `generateStaticParams`, `generateMetadata`\n\n## Error Handling\n\nSee [error-handling.md](./error-handling.md) for:\n\n- `error.tsx`, `global-error.tsx`, `not-found.tsx`\n- `redirect`, `permanentRedirect`, `notFound`\n- `forbidden`, `unauthorized` (auth errors)\n- `unstable_rethrow` for catch blocks\n\n## Data Patterns\n\nSee [data-patterns.md](./data-patterns.md) for:\n\n- Server Components vs Server Actions vs Route Handlers\n- Avoiding data waterfalls (`Promise.all`, Suspense, preload)\n- Client component data fetching\n\n## Route Handlers\n\nSee [route-handlers.md](./route-handlers.md) for:\n\n- `route.ts` basics\n- GET handler conflicts with `page.tsx`\n- Environment behavior (no React DOM)\n- When to use vs Server Actions\n\n## Metadata & OG Images\n\nSee [metadata.md](./metadata.md) for:\n\n- Static and dynamic metadata\n- `generateMetadata` function\n- OG image generation with `next/og`\n- File-based metadata conventions\n\n## Image Optimization\n\nSee [image.md](./image.md) for:\n\n- Always use `next/image` over `<img>`\n- Remote images configuration\n- Responsive `sizes` attribute\n- Blur placeholders\n- Priority loading for LCP\n\n## Font Optimization\n\nSee [font.md](./font.md) for:\n\n- `next/font` setup\n- Google Fonts, local fonts\n- Tailwind CSS integration\n- Preloading subsets\n\n## Bundling\n\nSee [bundling.md](./bundling.md) for:\n\n- Server-incompatible packages\n- CSS imports (not link tags)\n- Polyfills (already included)\n- ESM/CommonJS issues\n- Bundle analysis\n\n## Scripts\n\nSee [scripts.md](./scripts.md) for:\n\n- `next/script` vs native script tags\n- Inline scripts need `id`\n- Loading strategies\n- Google Analytics with `@next/third-parties`\n\n## Hydration Errors\n\nSee [hydration-error.md](./hydration-error.md) for:\n\n- Common causes (browser APIs, dates, invalid HTML)\n- Debugging with error overlay\n- Fixes for each cause\n\n## Suspense Boundaries\n\nSee [suspense-boundaries.md](./suspense-boundaries.md) for:\n\n- CSR bailout with `useSearchParams` and `usePathname`\n- Which hooks require Suspense boundaries\n\n## Parallel & Intercepting Routes\n\nSee [parallel-routes.md](./parallel-routes.md) for:\n\n- Modal patterns with `@slot` and `(.)` interceptors\n- `default.tsx` for fallbacks\n- Closing modals correctly with `router.back()`\n\n## Self-Hosting\n\nSee [self-hosting.md](./self-hosting.md) for:\n\n- `output: 'standalone'` for Docker\n- Cache handlers for multi-instance ISR\n- What works vs needs extra setup\n\n## Debug Tricks\n\nSee [debug-tricks.md](./debug-tricks.md) for:\n\n- MCP endpoint for AI-assisted debugging\n- Rebuild specific routes with `--debug-build-paths`\n",".agents/skills/next-best-practices/async-patterns.md":"# Async Patterns\n\nIn Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous.\n\n## Async Params and SearchParams\n\nAlways type them as `Promise<...>` and await them.\n\n### Pages and Layouts\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Page({ params }: Props) {\n  const { slug } = await params;\n}\n```\n\n### Route Handlers\n\n```tsx\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n### SearchParams\n\n```tsx\ntype Props = {\n  params: Promise<{ slug: string }>;\n  searchParams: Promise<{ query?: string }>;\n};\n\nexport default async function Page({ params, searchParams }: Props) {\n  const { slug } = await params;\n  const { query } = await searchParams;\n}\n```\n\n### Synchronous Components\n\nUse `React.use()` for non-async components:\n\n```tsx\nimport { use } from 'react';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default function Page({ params }: Props) {\n  const { slug } = use(params);\n}\n```\n\n### generateMetadata\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  return { title: slug };\n}\n```\n\n## Async Cookies and Headers\n\n```tsx\nimport { cookies, headers } from 'next/headers';\n\nexport default async function Page() {\n  const cookieStore = await cookies();\n  const headersList = await headers();\n\n  const theme = cookieStore.get('theme');\n  const userAgent = headersList.get('user-agent');\n}\n```\n\n## Migration Codemod\n\n```bash\nnpx @next/codemod@latest next-async-request-api .\n```\n",".agents/skills/next-best-practices/bundling.md":"# Bundling\n\nFix common bundling issues with third-party packages.\n\n## Server-Incompatible Packages\n\nSome packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components.\n\n### Error Signs\n\n```\nReferenceError: window is not defined\nReferenceError: document is not defined\nReferenceError: localStorage is not defined\nModule not found: Can't resolve 'fs'\n```\n\n### Solution 1: Mark as Client-Only\n\nIf the package is only needed on client:\n\n```tsx\n// Bad: Fails - package uses window\nimport SomeChart from 'some-chart-library';\n\nexport default function Page() {\n  return <SomeChart />;\n}\n\n// Good: Use dynamic import with ssr: false\nimport dynamic from 'next/dynamic';\n\nconst SomeChart = dynamic(() => import('some-chart-library'), {\n  ssr: false\n});\n\nexport default function Page() {\n  return <SomeChart />;\n}\n```\n\n### Solution 2: Externalize from Server Bundle\n\nFor packages that should run on server but have bundling issues:\n\n```js\n// next.config.js\nmodule.exports = {\n  serverExternalPackages: ['problematic-package']\n};\n```\n\nUse this for:\n\n- Packages with native bindings (sharp, bcrypt)\n- Packages that don't bundle well (some ORMs)\n- Packages with circular dependencies\n\n### Solution 3: Client Component Wrapper\n\nWrap the entire usage in a client component:\n\n```tsx\n// components/ChartWrapper.tsx\n'use client';\n\nimport { Chart } from 'chart-library';\n\nexport function ChartWrapper(props) {\n  return <Chart {...props} />;\n}\n\n// app/page.tsx (server component)\nimport { ChartWrapper } from '@/components/ChartWrapper';\n\nexport default function Page() {\n  return <ChartWrapper data={data} />;\n}\n```\n\n## CSS Imports\n\nImport CSS files instead of using `<link>` tags. Next.js handles bundling and optimization.\n\n```tsx\n// Bad: Manual link tag\n<link rel='stylesheet' href='/styles.css' />;\n\n// Good: Import CSS\nimport './styles.css';\n\n// Good: CSS Modules\nimport styles from './Button.module.css';\n```\n\n## Polyfills\n\nNext.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs.\n\nAlready included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others.\n\n```tsx\n// Bad: Redundant polyfills\n<script src='https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,Array.from' />\n\n// Good: Next.js includes these automatically\n```\n\n## ESM/CommonJS Issues\n\n### Error Signs\n\n```\nSyntaxError: Cannot use import statement outside a module\nError: require() of ES Module\nModule not found: ESM packages need to be imported\n```\n\n### Solution: Transpile Package\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['some-esm-package', 'another-package']\n};\n```\n\n## Common Problematic Packages\n\n| Package         | Issue           | Solution                                                        |\n| --------------- | --------------- | --------------------------------------------------------------- |\n| `sharp`         | Native bindings | `serverExternalPackages: ['sharp']`                             |\n| `bcrypt`        | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs`          |\n| `canvas`        | Native bindings | `serverExternalPackages: ['canvas']`                            |\n| `recharts`      | Uses window     | `dynamic(() => import('recharts'), { ssr: false })`             |\n| `react-quill`   | Uses document   | `dynamic(() => import('react-quill'), { ssr: false })`          |\n| `mapbox-gl`     | Uses window     | `dynamic(() => import('mapbox-gl'), { ssr: false })`            |\n| `monaco-editor` | Uses window     | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |\n| `lottie-web`    | Uses document   | `dynamic(() => import('lottie-react'), { ssr: false })`         |\n\n## Bundle Analysis\n\nAnalyze bundle size with the built-in analyzer (Next.js 16.1+):\n\n```bash\nnext experimental-analyze\n```\n\nThis opens an interactive UI to:\n\n- Filter by route, environment (client/server), and type\n- Inspect module sizes and import chains\n- View treemap visualization\n\nSave output for comparison:\n\n```bash\nnext experimental-analyze --output\n# Output saved to .next/diagnostics/analyze\n```\n\nReference: https://nextjs.org/docs/app/guides/package-bundling\n\n## Migrating from Webpack to Turbopack\n\nTurbopack is the default bundler in Next.js 15+. If you have custom webpack config, migrate to Turbopack-compatible alternatives:\n\n```js\n// next.config.js\nmodule.exports = {\n  // Good: Works with Turbopack\n  serverExternalPackages: ['package'],\n  transpilePackages: ['package'],\n\n  // Bad: Webpack-only - migrate away from this\n  webpack: (config) => {\n    // custom webpack config\n  }\n};\n```\n\nReference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack\n",".agents/skills/next-best-practices/data-patterns.md":"# Data Patterns\n\nChoose the right data fetching pattern for each use case.\n\n## Decision Tree\n\n```\nNeed to fetch data?\n├── From a Server Component?\n│   └── Use: Fetch directly (no API needed)\n│\n├── From a Client Component?\n│   ├── Is it a mutation (POST/PUT/DELETE)?\n│   │   └── Use: Server Action\n│   └── Is it a read (GET)?\n│       └── Use: Route Handler OR pass from Server Component\n│\n├── Need external API access (webhooks, third parties)?\n│   └── Use: Route Handler\n│\n└── Need REST API for mobile app / external clients?\n    └── Use: Route Handler\n```\n\n## Pattern 1: Server Components (Preferred for Reads)\n\nFetch data directly in Server Components - no API layer needed.\n\n```tsx\n// app/users/page.tsx\nasync function UsersPage() {\n  // Direct database access - no API round-trip\n  const users = await db.user.findMany();\n\n  // Or fetch from external API\n  const posts = await fetch('https://api.example.com/posts').then((r) => r.json());\n\n  return (\n    <ul>\n      {users.map((user) => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\n**Benefits**:\n\n- No API to maintain\n- No client-server waterfall\n- Secrets stay on server\n- Direct database access\n\n## Pattern 2: Server Actions (Preferred for Mutations)\n\nServer Actions are the recommended way to handle mutations.\n\n```tsx\n// app/actions.ts\n'use server';\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function createPost(formData: FormData) {\n  const title = formData.get('title') as string;\n\n  await db.post.create({ data: { title } });\n\n  revalidatePath('/posts');\n}\n\nexport async function deletePost(id: string) {\n  await db.post.delete({ where: { id } });\n\n  revalidateTag('posts');\n}\n```\n\n```tsx\n// app/posts/new/page.tsx\nimport { createPost } from '@/app/actions';\n\nexport default function NewPost() {\n  return (\n    <form action={createPost}>\n      <input name='title' required />\n      <button type='submit'>Create</button>\n    </form>\n  );\n}\n```\n\n**Benefits**:\n\n- End-to-end type safety\n- Progressive enhancement (works without JS)\n- Automatic request handling\n- Integrated with React transitions\n\n**Constraints**:\n\n- POST only (no GET caching semantics)\n- Internal use only (no external access)\n- Cannot return non-serializable data\n\n## Pattern 3: Route Handlers (APIs)\n\nUse Route Handlers when you need a REST API.\n\n```tsx\n// app/api/posts/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\n// GET is cacheable\nexport async function GET(request: NextRequest) {\n  const posts = await db.post.findMany();\n  return NextResponse.json(posts);\n}\n\n// POST for mutations\nexport async function POST(request: NextRequest) {\n  const body = await request.json();\n  const post = await db.post.create({ data: body });\n  return NextResponse.json(post, { status: 201 });\n}\n```\n\n**When to use**:\n\n- External API access (mobile apps, third parties)\n- Webhooks from external services\n- GET endpoints that need HTTP caching\n- OpenAPI/Swagger documentation needed\n\n**When NOT to use**:\n\n- Internal data fetching (use Server Components)\n- Mutations from your UI (use Server Actions)\n\n## Avoiding Data Waterfalls\n\n### Problem: Sequential Fetches\n\n```tsx\n// Bad: Sequential waterfalls\nasync function Dashboard() {\n  const user = await getUser(); // Wait...\n  const posts = await getPosts(); // Then wait...\n  const comments = await getComments(); // Then wait...\n\n  return <div>...</div>;\n}\n```\n\n### Solution 1: Parallel Fetching with Promise.all\n\n```tsx\n// Good: Parallel fetching\nasync function Dashboard() {\n  const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);\n\n  return <div>...</div>;\n}\n```\n\n### Solution 2: Streaming with Suspense\n\n```tsx\n// Good: Show content progressively\nimport { Suspense } from 'react';\n\nasync function Dashboard() {\n  return (\n    <div>\n      <Suspense fallback={<UserSkeleton />}>\n        <UserSection />\n      </Suspense>\n      <Suspense fallback={<PostsSkeleton />}>\n        <PostsSection />\n      </Suspense>\n    </div>\n  );\n}\n\nasync function UserSection() {\n  const user = await getUser(); // Fetches independently\n  return <div>{user.name}</div>;\n}\n\nasync function PostsSection() {\n  const posts = await getPosts(); // Fetches independently\n  return <PostList posts={posts} />;\n}\n```\n\n### Solution 3: Preload Pattern\n\n```tsx\n// lib/data.ts\nimport { cache } from 'react';\n\nexport const getUser = cache(async (id: string) => {\n  return db.user.findUnique({ where: { id } });\n});\n\nexport const preloadUser = (id: string) => {\n  void getUser(id); // Fire and forget\n};\n```\n\n```tsx\n// app/user/[id]/page.tsx\nimport { getUser, preloadUser } from '@/lib/data';\n\nexport default async function UserPage({ params }) {\n  const { id } = await params;\n\n  // Start fetching early\n  preloadUser(id);\n\n  // Do other work...\n\n  // Data likely ready by now\n  const user = await getUser(id);\n  return <div>{user.name}</div>;\n}\n```\n\n## Client Component Data Fetching\n\nWhen Client Components need data:\n\n### Option 1: Pass from Server Component (Preferred)\n\n```tsx\n// Server Component\nasync function Page() {\n  const data = await fetchData();\n  return <ClientComponent initialData={data} />;\n}\n\n// Client Component\n('use client');\nfunction ClientComponent({ initialData }) {\n  const [data, setData] = useState(initialData);\n  // ...\n}\n```\n\n### Option 2: Fetch on Mount (When Necessary)\n\n```tsx\n'use client';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    fetch('/api/data')\n      .then((r) => r.json())\n      .then(setData);\n  }, []);\n\n  if (!data) return <Loading />;\n  return <div>{data.value}</div>;\n}\n```\n\n### Option 3: Server Action for Reads (Works But Not Ideal)\n\nServer Actions can be called from Client Components for reads, but this is not their intended purpose:\n\n```tsx\n'use client';\nimport { getData } from './actions';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    getData().then(setData);\n  }, []);\n\n  return <div>{data?.value}</div>;\n}\n```\n\n**Note**: Server Actions always use POST, so no HTTP caching. Prefer Route Handlers for cacheable reads.\n\n## Quick Reference\n\n| Pattern                | Use Case                    | HTTP Method | Caching              |\n| ---------------------- | --------------------------- | ----------- | -------------------- |\n| Server Component fetch | Internal reads              | Any         | Full Next.js caching |\n| Server Action          | Mutations, form submissions | POST only   | No                   |\n| Route Handler          | External APIs, webhooks     | Any         | GET can be cached    |\n| Client fetch to API    | Client-side reads           | Any         | HTTP cache headers   |\n",".agents/skills/next-best-practices/debug-tricks.md":"# Debug Tricks\n\nTricks to speed up debugging Next.js applications.\n\n## MCP Endpoint (Dev Server)\n\nNext.js exposes a `/_next/mcp` endpoint in development for AI-assisted debugging via MCP (Model Context Protocol).\n\n- **Next.js 16+**: Enabled by default, use `next-devtools-mcp`\n- **Next.js < 16**: Requires `experimental.mcpServer: true` in next.config.js\n\nReference: https://nextjs.org/docs/app/guides/mcp\n\n**Important**: Find the actual port of the running Next.js dev server (check terminal output or `package.json` scripts). Don't assume port 3000.\n\n### Request Format\n\nThe endpoint uses JSON-RPC 2.0 over HTTP POST:\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\",\n    \"id\": \"1\",\n    \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"<tool-name>\",\n      \"arguments\": {}\n    }\n  }'\n```\n\n### Available Tools\n\n#### `get_errors`\n\nGet current errors from dev server (build errors, runtime errors with source-mapped stacks):\n\n```json\n{ \"name\": \"get_errors\", \"arguments\": {} }\n```\n\n#### `get_routes`\n\nDiscover all routes by scanning filesystem:\n\n```json\n{ \"name\": \"get_routes\", \"arguments\": {} }\n// Optional: { \"name\": \"get_routes\", \"arguments\": { \"routerType\": \"app\" } }\n```\n\nReturns: `{ \"appRouter\": [\"/\", \"/api/users/[id]\", ...], \"pagesRouter\": [...] }`\n\n#### `get_project_metadata`\n\nGet project path and dev server URL:\n\n```json\n{ \"name\": \"get_project_metadata\", \"arguments\": {} }\n```\n\nReturns: `{ \"projectPath\": \"/path/to/project\", \"devServerUrl\": \"http://localhost:3000\" }`\n\n#### `get_page_metadata`\n\nGet runtime metadata about current page render (requires active browser session):\n\n```json\n{ \"name\": \"get_page_metadata\", \"arguments\": {} }\n```\n\nReturns segment trie data showing layouts, boundaries, and page components.\n\n#### `get_logs`\n\nGet path to Next.js development log file:\n\n```json\n{ \"name\": \"get_logs\", \"arguments\": {} }\n```\n\nReturns path to `<distDir>/logs/next-development.log`\n\n#### `get_server_action_by_id`\n\nLocate a Server Action by ID:\n\n```json\n{ \"name\": \"get_server_action_by_id\", \"arguments\": { \"actionId\": \"<action-id>\" } }\n```\n\n### Example: Get Errors\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"tools/call\",\"params\":{\"name\":\"get_errors\",\"arguments\":{}}}'\n```\n\n## Rebuild Specific Routes (Next.js 16+)\n\nUse `--debug-build-paths` to rebuild only specific routes instead of the entire app:\n\n```bash\n# Rebuild a specific route\nnext build --debug-build-paths \"/dashboard\"\n\n# Rebuild routes matching a glob\nnext build --debug-build-paths \"/api/*\"\n\n# Dynamic routes\nnext build --debug-build-paths \"/blog/[slug]\"\n```\n\nUse this to:\n\n- Quickly verify a build fix without full rebuild\n- Debug static generation issues for specific pages\n- Iterate faster on build errors\n",".agents/skills/next-best-practices/directives.md":"# Directives\n\n## React Directives\n\nThese are React directives, not Next.js specific.\n\n### `'use client'`\n\nMarks a component as a Client Component. Required for:\n\n- React hooks (`useState`, `useEffect`, etc.)\n- Event handlers (`onClick`, `onChange`)\n- Browser APIs (`window`, `localStorage`)\n\n```tsx\n'use client';\n\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-client\n\n### `'use server'`\n\nMarks a function as a Server Action. Can be passed to Client Components.\n\n```tsx\n'use server';\n\nexport async function submitForm(formData: FormData) {\n  // Runs on server\n}\n```\n\nOr inline within a Server Component:\n\n```tsx\nexport default function Page() {\n  async function submit() {\n    'use server';\n    // Runs on server\n  }\n  return <form action={submit}>...</form>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-server\n\n---\n\n## Next.js Directive\n\n### `'use cache'`\n\nMarks a function or component for caching. Part of Next.js Cache Components.\n\n```tsx\n'use cache';\n\nexport async function getCachedData() {\n  return await fetchData();\n}\n```\n\nRequires `cacheComponents: true` in `next.config.ts`.\n\nFor detailed usage including cache profiles, `cacheLife()`, `cacheTag()`, and `updateTag()`, see the `next-cache-components` skill.\n\nReference: https://nextjs.org/docs/app/api-reference/directives/use-cache\n",".agents/skills/next-best-practices/error-handling.md":"# Error Handling\n\nHandle errors gracefully in Next.js applications.\n\nReference: https://nextjs.org/docs/app/getting-started/error-handling\n\n## Error Boundaries\n\n### `error.tsx`\n\nCatches errors in a route segment and its children:\n\n```tsx\n'use client';\n\nexport default function Error({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <div>\n      <h2>Something went wrong!</h2>\n      <button onClick={() => reset()}>Try again</button>\n    </div>\n  );\n}\n```\n\n**Important:** `error.tsx` must be a Client Component.\n\n### `global-error.tsx`\n\nCatches errors in root layout:\n\n```tsx\n'use client';\n\nexport default function GlobalError({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <html>\n      <body>\n        <h2>Something went wrong!</h2>\n        <button onClick={() => reset()}>Try again</button>\n      </body>\n    </html>\n  );\n}\n```\n\n**Important:** Must include `<html>` and `<body>` tags.\n\n## Server Actions: Navigation API Gotcha\n\n**Do NOT wrap navigation APIs in try-catch.** They throw special errors that Next.js handles internally.\n\nReference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior\n\n```tsx\n'use server'\n\nimport { redirect } from 'next/navigation'\nimport { notFound } from 'next/navigation'\n\n// Bad: try-catch catches the navigation \"error\"\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)  // This throws!\n  } catch (error) {\n    // redirect() throw is caught here - navigation fails!\n    return { error: 'Failed to create post' }\n  }\n}\n\n// Good: Call navigation APIs outside try-catch\nasync function createPost(formData: FormData) {\n  let post\n  try {\n    post = await db.post.create({ ... })\n  } catch (error) {\n    return { error: 'Failed to create post' }\n  }\n  redirect(`/posts/${post.id}`)  // Outside try-catch\n}\n\n// Good: Re-throw navigation errors\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)\n  } catch (error) {\n    if (error instanceof Error && error.message === 'NEXT_REDIRECT') {\n      throw error  // Re-throw navigation errors\n    }\n    return { error: 'Failed to create post' }\n  }\n}\n```\n\nSame applies to:\n\n- `redirect()` - 307 temporary redirect\n- `permanentRedirect()` - 308 permanent redirect\n- `notFound()` - 404 not found\n- `forbidden()` - 403 forbidden\n- `unauthorized()` - 401 unauthorized\n\nUse `unstable_rethrow()` to re-throw these errors in catch blocks:\n\n```tsx\nimport { unstable_rethrow } from 'next/navigation';\n\nasync function action() {\n  try {\n    // ...\n    redirect('/success');\n  } catch (error) {\n    unstable_rethrow(error); // Re-throws Next.js internal errors\n    return { error: 'Something went wrong' };\n  }\n}\n```\n\n## Redirects\n\n```tsx\nimport { redirect, permanentRedirect } from 'next/navigation';\n\n// 307 Temporary - use for most cases\nredirect('/new-path');\n\n// 308 Permanent - use for URL migrations (cached by browsers)\npermanentRedirect('/new-url');\n```\n\n## Auth Errors\n\nTrigger auth-related error pages:\n\n```tsx\nimport { forbidden, unauthorized } from 'next/navigation';\n\nasync function Page() {\n  const session = await getSession();\n\n  if (!session) {\n    unauthorized(); // Renders unauthorized.tsx (401)\n  }\n\n  if (!session.hasAccess) {\n    forbidden(); // Renders forbidden.tsx (403)\n  }\n\n  return <Dashboard />;\n}\n```\n\nCreate corresponding error pages:\n\n```tsx\n// app/forbidden.tsx\nexport default function Forbidden() {\n  return <div>You don't have access to this resource</div>;\n}\n\n// app/unauthorized.tsx\nexport default function Unauthorized() {\n  return <div>Please log in to continue</div>;\n}\n```\n\n## Not Found\n\n### `not-found.tsx`\n\nCustom 404 page for a route segment:\n\n```tsx\nexport default function NotFound() {\n  return (\n    <div>\n      <h2>Not Found</h2>\n      <p>Could not find the requested resource</p>\n    </div>\n  );\n}\n```\n\n### Triggering Not Found\n\n```tsx\nimport { notFound } from 'next/navigation';\n\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const post = await getPost(id);\n\n  if (!post) {\n    notFound(); // Renders closest not-found.tsx\n  }\n\n  return <div>{post.title}</div>;\n}\n```\n\n## Error Hierarchy\n\nErrors bubble up to the nearest error boundary:\n\n```\napp/\n├── error.tsx           # Catches errors from all children\n├── blog/\n│   ├── error.tsx       # Catches errors in /blog/*\n│   └── [slug]/\n│       ├── error.tsx   # Catches errors in /blog/[slug]\n│       └── page.tsx\n└── layout.tsx          # Errors here go to global-error.tsx\n```\n",".agents/skills/next-best-practices/file-conventions.md":"# File Conventions\n\nNext.js App Router uses file-based routing with special file conventions.\n\n## Project Structure\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure\n\n```\napp/\n├── layout.tsx          # Root layout (required)\n├── page.tsx            # Home page (/)\n├── loading.tsx         # Loading UI\n├── error.tsx           # Error UI\n├── not-found.tsx       # 404 UI\n├── global-error.tsx    # Global error UI\n├── route.ts            # API endpoint\n├── template.tsx        # Re-rendered layout\n├── default.tsx         # Parallel route fallback\n├── blog/\n│   ├── page.tsx        # /blog\n│   └── [slug]/\n│       └── page.tsx    # /blog/:slug\n└── (group)/            # Route group (no URL impact)\n    └── page.tsx\n```\n\n## Special Files\n\n| File            | Purpose                                  |\n| --------------- | ---------------------------------------- |\n| `page.tsx`      | UI for a route segment                   |\n| `layout.tsx`    | Shared UI for segment and children       |\n| `loading.tsx`   | Loading UI (Suspense boundary)           |\n| `error.tsx`     | Error UI (Error boundary)                |\n| `not-found.tsx` | 404 UI                                   |\n| `route.ts`      | API endpoint                             |\n| `template.tsx`  | Like layout but re-renders on navigation |\n| `default.tsx`   | Fallback for parallel routes             |\n\n## Route Segments\n\n```\napp/\n├── blog/               # Static segment: /blog\n├── [slug]/             # Dynamic segment: /:slug\n├── [...slug]/          # Catch-all: /a/b/c\n├── [[...slug]]/        # Optional catch-all: / or /a/b/c\n└── (marketing)/        # Route group (ignored in URL)\n```\n\n## Parallel Routes\n\n```\napp/\n├── @analytics/\n│   └── page.tsx\n├── @sidebar/\n│   └── page.tsx\n└── layout.tsx          # Receives { analytics, sidebar } as props\n```\n\n## Intercepting Routes\n\n```\napp/\n├── feed/\n│   └── page.tsx\n├── @modal/\n│   └── (.)photo/[id]/  # Intercepts /photo/[id] from /feed\n│       └── page.tsx\n└── photo/[id]/\n    └── page.tsx\n```\n\nConventions:\n\n- `(.)` - same level\n- `(..)` - one level up\n- `(..)(..)` - two levels up\n- `(...)` - from root\n\n## Private Folders\n\n```\napp/\n├── _components/        # Private folder (not a route)\n│   └── Button.tsx\n└── page.tsx\n```\n\nPrefix with `_` to exclude from routing.\n\n## Middleware / Proxy\n\n### Next.js 14-15: `middleware.ts`\n\n```ts\n// middleware.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  // Auth, redirects, rewrites, etc.\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n### Next.js 16+: `proxy.ts`\n\nRenamed for clarity - same capabilities, different names:\n\n```ts\n// proxy.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function proxy(request: NextRequest) {\n  // Same logic as middleware\n  return NextResponse.next();\n}\n\nexport const proxyConfig = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n| Version | File            | Export         | Config        |\n| ------- | --------------- | -------------- | ------------- |\n| v14-15  | `middleware.ts` | `middleware()` | `config`      |\n| v16+    | `proxy.ts`      | `proxy()`      | `proxyConfig` |\n\n**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.\n\n## File Conventions Reference\n\nReference: https://nextjs.org/docs/app/api-reference/file-conventions\n",".agents/skills/next-best-practices/font.md":"# Font Optimization\n\nUse `next/font` for automatic font optimization with zero layout shift.\n\n## Google Fonts\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={inter.className}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Multiple Fonts\n\n```tsx\nimport { Inter, Roboto_Mono } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nconst robotoMono = Roboto_Mono({\n  subsets: ['latin'],\n  variable: '--font-roboto-mono'\n});\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={`${inter.variable} ${robotoMono.variable}`}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\nUse in CSS:\n\n```css\nbody {\n  font-family: var(--font-inter);\n}\n\ncode {\n  font-family: var(--font-roboto-mono);\n}\n```\n\n## Font Weights and Styles\n\n```tsx\n// Single weight\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: '400'\n});\n\n// Multiple weights\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: ['400', '500', '700']\n});\n\n// Variable font (recommended) - includes all weights\nconst inter = Inter({\n  subsets: ['latin']\n  // No weight needed - variable fonts support all weights\n});\n\n// With italic\nconst inter = Inter({\n  subsets: ['latin'],\n  style: ['normal', 'italic']\n});\n```\n\n## Local Fonts\n\n```tsx\nimport localFont from 'next/font/local';\n\nconst myFont = localFont({\n  src: './fonts/MyFont.woff2'\n});\n\n// Multiple files for different weights\nconst myFont = localFont({\n  src: [\n    {\n      path: './fonts/MyFont-Regular.woff2',\n      weight: '400',\n      style: 'normal'\n    },\n    {\n      path: './fonts/MyFont-Bold.woff2',\n      weight: '700',\n      style: 'normal'\n    }\n  ]\n});\n\n// Variable font\nconst myFont = localFont({\n  src: './fonts/MyFont-Variable.woff2',\n  variable: '--font-my-font'\n});\n```\n\n## Tailwind CSS Integration\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nexport default function RootLayout({ children }) {\n  return (\n    <html lang='en' className={inter.variable}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n```js\n// tailwind.config.js\nmodule.exports = {\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['var(--font-inter)']\n      }\n    }\n  }\n};\n```\n\n## Preloading Subsets\n\nOnly load needed character subsets:\n\n```tsx\n// Latin only (most common)\nconst inter = Inter({ subsets: ['latin'] });\n\n// Multiple subsets\nconst inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });\n```\n\n## Display Strategy\n\nControl font loading behavior:\n\n```tsx\nconst inter = Inter({\n  subsets: ['latin'],\n  display: 'swap' // Default - shows fallback, swaps when loaded\n});\n\n// Options:\n// 'auto' - browser decides\n// 'block' - short block period, then swap\n// 'swap' - immediate fallback, swap when ready (recommended)\n// 'fallback' - short block, short swap, then fallback\n// 'optional' - short block, no swap (use if font is optional)\n```\n\n## Don't Use Manual Font Links\n\nAlways use `next/font` instead of `<link>` tags for Google Fonts.\n\n```tsx\n// Bad: Manual link tag (blocks rendering, no optimization)\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Bad: Missing display and preconnect\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Good: Use next/font (self-hosted, zero layout shift)\nimport { Inter } from 'next/font/google'\n\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Importing font in every component\n// components/Button.tsx\nimport { Inter } from 'next/font/google'\nconst inter = Inter({ subsets: ['latin'] }) // Creates new instance each time!\n\n// Good: Import once in layout, use CSS variable\n// app/layout.tsx\nconst inter = Inter({ subsets: ['latin'], variable: '--font-inter' })\n\n// Bad: Using @import in CSS (blocks rendering)\n/* globals.css */\n@import url('https://fonts.googleapis.com/css2?family=Inter');\n\n// Good: Use next/font (self-hosted, no network request)\nimport { Inter } from 'next/font/google'\n\n// Bad: Loading all weights when only using a few\nconst inter = Inter({ subsets: ['latin'] }) // Loads all weights\n\n// Good: Specify only needed weights (for non-variable fonts)\nconst inter = Inter({ subsets: ['latin'], weight: ['400', '700'] })\n\n// Bad: Missing subset - loads all characters\nconst inter = Inter({})\n\n// Good: Always specify subset\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Font in Specific Components\n\n```tsx\n// For component-specific fonts, export from a shared file\n// lib/fonts.ts\nimport { Inter, Playfair_Display } from 'next/font/google';\n\nexport const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });\nexport const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });\n\n// components/Heading.tsx\nimport { playfair } from '@/lib/fonts';\n\nexport function Heading({ children }) {\n  return <h1 className={playfair.className}>{children}</h1>;\n}\n```\n",".agents/skills/next-best-practices/functions.md":"# Functions\n\nNext.js function APIs.\n\nReference: https://nextjs.org/docs/app/api-reference/functions\n\n## Navigation Hooks (Client)\n\n| Hook                        | Purpose                                                        | Reference                                                                                |\n| --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `useRouter`                 | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router)                   |\n| `usePathname`               | Get current pathname                                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname)                 |\n| `useSearchParams`           | Read URL search parameters                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params)            |\n| `useParams`                 | Access dynamic route parameters                                | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params)                   |\n| `useSelectedLayoutSegment`  | Active child segment (one level)                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment)  |\n| `useSelectedLayoutSegments` | All active segments below layout                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |\n| `useLinkStatus`             | Check link prefetch status                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status)              |\n| `useReportWebVitals`        | Report Core Web Vitals metrics                                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals)        |\n\n## Server Functions\n\n| Function     | Purpose                                      | Reference                                                              |\n| ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |\n| `cookies`    | Read/write cookies                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies)    |\n| `headers`    | Read request headers                         | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers)    |\n| `draftMode`  | Enable preview of unpublished CMS content    | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |\n| `after`      | Run code after response finishes streaming   | [Docs](https://nextjs.org/docs/app/api-reference/functions/after)      |\n| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |\n| `userAgent`  | Parse User-Agent header                      | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent)  |\n\n## Generate Functions\n\n| Function                | Purpose                                 | Reference                                                                           |\n| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |\n| `generateStaticParams`  | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params)  |\n| `generateMetadata`      | Dynamic metadata                        | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata)       |\n| `generateViewport`      | Dynamic viewport config                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport)       |\n| `generateSitemaps`      | Multiple sitemaps for large sites       | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps)       |\n| `generateImageMetadata` | Multiple OG images per route            | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |\n\n## Request/Response\n\n| Function        | Purpose                        | Reference                                                                  |\n| --------------- | ------------------------------ | -------------------------------------------------------------------------- |\n| `NextRequest`   | Extended Request with helpers  | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request)   |\n| `NextResponse`  | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response)  |\n| `ImageResponse` | Generate OG images             | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |\n\n## Common Examples\n\n### Navigation\n\nUse `next/link` for internal navigation instead of `<a>` tags.\n\n```tsx\n// Bad: Plain anchor tag\n<a href='/about'>About</a>;\n\n// Good: Next.js Link\nimport Link from 'next/link';\n\n<Link href='/about'>About</Link>;\n```\n\nActive link styling:\n\n```tsx\n'use client';\n\nimport Link from 'next/link';\nimport { usePathname } from 'next/navigation';\n\nexport function NavLink({ href, children }) {\n  const pathname = usePathname();\n\n  return (\n    <Link href={href} className={pathname === href ? 'active' : ''}>\n      {children}\n    </Link>\n  );\n}\n```\n\n### Static Generation\n\n```tsx\n// app/blog/[slug]/page.tsx\nexport async function generateStaticParams() {\n  const posts = await getPosts();\n  return posts.map((post) => ({ slug: post.slug }));\n}\n```\n\n### After Response\n\n```tsx\nimport { after } from 'next/server';\n\nexport async function POST(request: Request) {\n  const data = await processRequest(request);\n\n  after(async () => {\n    await logAnalytics(data);\n  });\n\n  return Response.json({ success: true });\n}\n```\n",".agents/skills/next-best-practices/hydration-error.md":"# Hydration Errors\n\nDiagnose and fix React hydration mismatch errors.\n\n## Error Signs\n\n- \"Hydration failed because the initial UI does not match\"\n- \"Text content does not match server-rendered HTML\"\n\n## Debugging\n\nIn development, click the hydration error to see the server/client diff.\n\n## Common Causes and Fixes\n\n### Browser-only APIs\n\n```tsx\n// Bad: Causes mismatch - window doesn't exist on server\n<div>{window.innerWidth}</div>;\n\n// Good: Use client component with mounted check\n('use client');\nimport { useState, useEffect } from 'react';\n\nexport function ClientOnly({ children }: { children: React.ReactNode }) {\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted ? children : null;\n}\n```\n\n### Date/Time Rendering\n\nServer and client may be in different timezones:\n\n```tsx\n// Bad: Causes mismatch\n<span>{new Date().toLocaleString()}</span>;\n\n// Good: Render on client only\n('use client');\nconst [time, setTime] = useState<string>();\nuseEffect(() => setTime(new Date().toLocaleString()), []);\n```\n\n### Random Values or IDs\n\n```tsx\n// Bad: Random values differ between server and client\n<div id={Math.random().toString()}>\n\n// Good: Use useId hook\nimport { useId } from 'react'\n\nfunction Input() {\n  const id = useId()\n  return <input id={id} />\n}\n```\n\n### Invalid HTML Nesting\n\n```tsx\n// Bad: Invalid - div inside p\n<p><div>Content</div></p>\n\n// Bad: Invalid - p inside p\n<p><p>Nested</p></p>\n\n// Good: Valid nesting\n<div><p>Content</p></div>\n```\n\n### Third-party Scripts\n\nScripts that modify DOM during hydration.\n\n```tsx\n// Good: Use next/script with afterInteractive\nimport Script from 'next/script';\n\nexport default function Page() {\n  return <Script src='https://example.com/script.js' strategy='afterInteractive' />;\n}\n```\n",".agents/skills/next-best-practices/image.md":"# Image Optimization\n\nUse `next/image` for automatic image optimization.\n\n## Always Use next/image\n\n```tsx\n// Bad: Avoid native img\n<img src='/hero.png' alt='Hero' />;\n\n// Good: Use next/image\nimport Image from 'next/image';\n<Image src='/hero.png' alt='Hero' width={800} height={400} />;\n```\n\n## Required Props\n\nImages need explicit dimensions to prevent layout shift:\n\n```tsx\n// Local images - dimensions inferred automatically\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" />\n\n// Remote images - must specify width/height\n<Image src=\"https://example.com/image.jpg\" alt=\"Hero\" width={800} height={400} />\n\n// Or use fill for parent-relative sizing\n<div style={{ position: 'relative', width: '100%', height: 400 }}>\n  <Image src=\"/hero.png\" alt=\"Hero\" fill style={{ objectFit: 'cover' }} />\n</div>\n```\n\n## Remote Images Configuration\n\nRemote domains must be configured in `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'example.com',\n        pathname: '/images/**'\n      },\n      {\n        protocol: 'https',\n        hostname: '*.cdn.com' // Wildcard subdomain\n      }\n    ]\n  }\n};\n```\n\n## Responsive Images\n\nUse `sizes` to tell the browser which size to download:\n\n```tsx\n// Full-width hero\n<Image\n  src=\"/hero.png\"\n  alt=\"Hero\"\n  fill\n  sizes=\"100vw\"\n/>\n\n// Responsive grid (3 columns on desktop, 1 on mobile)\n<Image\n  src=\"/card.png\"\n  alt=\"Card\"\n  fill\n  sizes=\"(max-width: 768px) 100vw, 33vw\"\n/>\n\n// Fixed sidebar image\n<Image\n  src=\"/avatar.png\"\n  alt=\"Avatar\"\n  width={200}\n  height={200}\n  sizes=\"200px\"\n/>\n```\n\n## Blur Placeholder\n\nPrevent layout shift with placeholders:\n\n```tsx\n// Local images - automatic blur hash\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" placeholder=\"blur\" />\n\n// Remote images - provide blurDataURL\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"blur\"\n  blurDataURL=\"data:image/jpeg;base64,/9j/4AAQSkZJRg...\"\n/>\n\n// Or use color placeholder\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"empty\"\n  style={{ backgroundColor: '#e0e0e0' }}\n/>\n```\n\n## Priority Loading\n\nUse `priority` for above-the-fold images (LCP):\n\n```tsx\n// Hero image - loads immediately\n<Image src=\"/hero.png\" alt=\"Hero\" fill priority />\n\n// Below-fold images - lazy loaded by default (no priority needed)\n<Image src=\"/card.png\" alt=\"Card\" width={400} height={300} />\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Missing sizes with fill - downloads largest image\n<Image src=\"/hero.png\" alt=\"Hero\" fill />\n\n// Good: Add sizes for proper responsive behavior\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" />\n\n// Bad: Using width/height for aspect ratio only\n<Image src=\"/hero.png\" alt=\"Hero\" width={16} height={9} />\n\n// Good: Use actual display dimensions or fill with sizes\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" style={{ objectFit: 'cover' }} />\n\n// Bad: Remote image without config\n<Image src=\"https://untrusted.com/image.jpg\" alt=\"Image\" width={400} height={300} />\n// Error: Invalid src prop, hostname not configured\n\n// Good: Add hostname to next.config.js remotePatterns\n```\n\n## Static Export\n\nWhen using `output: 'export'`, use `unoptimized` or custom loader:\n\n```tsx\n// Option 1: Disable optimization\n<Image src='/hero.png' alt='Hero' width={800} height={400} unoptimized />;\n\n// Option 2: Global config\n// next.config.js\nmodule.exports = {\n  output: 'export',\n  images: { unoptimized: true }\n};\n\n// Option 3: Custom loader (Cloudinary, Imgix, etc.)\nconst cloudinaryLoader = ({ src, width, quality }) => {\n  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;\n};\n\n<Image loader={cloudinaryLoader} src='sample.jpg' alt='Sample' width={800} height={400} />;\n```\n",".agents/skills/next-best-practices/metadata.md":"# Metadata\n\nAdd SEO metadata to Next.js pages using the Metadata API.\n\n## Important: Server Components Only\n\nThe `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.\n\nIf the target page has `'use client'`:\n\n1. Remove `'use client'` if possible, move client logic to child components\n2. Or extract metadata to a parent Server Component layout\n3. Or split the file: Server Component with metadata imports Client Components\n\n## Static Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n  title: 'Page Title',\n  description: 'Page description for search engines'\n};\n```\n\n## Dynamic Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  const post = await getPost(slug);\n  return { title: post.title, description: post.description };\n}\n```\n\n## Avoid Duplicate Fetches\n\nUse React `cache()` when the same data is needed for both metadata and page:\n\n```tsx\nimport { cache } from 'react';\n\nexport const getPost = cache(async (slug: string) => {\n  return await db.posts.findFirst({ where: { slug } });\n});\n```\n\n## Viewport\n\nSeparate from metadata for streaming support:\n\n```tsx\nimport type { Viewport } from 'next';\n\nexport const viewport: Viewport = {\n  width: 'device-width',\n  initialScale: 1,\n  themeColor: '#000000'\n};\n\n// Or dynamic\nexport function generateViewport({ params }): Viewport {\n  return { themeColor: getThemeColor(params) };\n}\n```\n\n## Title Templates\n\nIn root layout for consistent naming:\n\n```tsx\nexport const metadata: Metadata = {\n  title: { default: 'Site Name', template: '%s | Site Name' }\n};\n```\n\n## Metadata File Conventions\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions\n\nPlace these files in `app/` directory (or route segments):\n\n| File                            | Purpose                                       |\n| ------------------------------- | --------------------------------------------- |\n| `favicon.ico`                   | Favicon                                       |\n| `icon.png` / `icon.svg`         | App icon                                      |\n| `apple-icon.png`                | Apple app icon                                |\n| `opengraph-image.png`           | OG image                                      |\n| `twitter-image.png`             | Twitter card image                            |\n| `sitemap.ts` / `sitemap.xml`    | Sitemap (use `generateSitemaps` for multiple) |\n| `robots.ts` / `robots.txt`      | Robots directives                             |\n| `manifest.ts` / `manifest.json` | Web app manifest                              |\n\n## SEO Best Practice: Static Files Are Often Enough\n\nFor most sites, **static metadata files provide excellent SEO coverage**:\n\n```\napp/\n├── favicon.ico\n├── opengraph-image.png     # Works for both OG and Twitter\n├── sitemap.ts\n├── robots.ts\n└── layout.tsx              # With title/description metadata\n```\n\n**Tips:**\n\n- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)\n- Static `title` and `description` in layout metadata is sufficient for most pages\n- Only use dynamic `generateMetadata` when content varies per page\n\n---\n\n# OG Image Generation\n\nGenerate dynamic Open Graph images using `next/og`.\n\n## Important Rules\n\n1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)\n2. **No searchParams** - OG images can't access search params, use route params instead\n3. **Avoid Edge runtime** - Use default Node.js runtime\n\n```tsx\n// Good\nimport { ImageResponse } from 'next/og';\n\n// Bad\n// import { ImageResponse } from '@vercel/og'\n// export const runtime = 'edge'\n```\n\n## Basic OG Image\n\n```tsx\n// app/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Site Name';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\nexport default function Image() {\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 128,\n        background: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center'\n      }}\n    >\n      Hello World\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Dynamic OG Image\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Blog Post';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Image({ params }: Props) {\n  const { slug } = await params;\n  const post = await getPost(slug);\n\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 48,\n        background: 'linear-gradient(to bottom, #1a1a1a, #333)',\n        color: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: 48\n      }}\n    >\n      <div style={{ fontSize: 64, fontWeight: 'bold' }}>{post.title}</div>\n      <div style={{ marginTop: 24, opacity: 0.8 }}>{post.description}</div>\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Custom Fonts\n\n```tsx\nimport { ImageResponse } from 'next/og';\nimport { join } from 'path';\nimport { readFile } from 'fs/promises';\n\nexport default async function Image() {\n  const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');\n  const fontData = await readFile(fontPath);\n\n  return new ImageResponse(\n    <div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [{ name: 'Inter', data: fontData, style: 'normal' }]\n    }\n  );\n}\n```\n\n## File Naming\n\n- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)\n- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)\n\n## Styling Notes\n\nImageResponse uses Flexbox layout:\n\n- Use `display: 'flex'`\n- No CSS Grid support\n- Styles must be inline objects\n\n## Multiple OG Images\n\nUse `generateImageMetadata` for multiple images per route:\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport async function generateImageMetadata({ params }) {\n  const images = await getPostImages(params.slug);\n  return images.map((img, idx) => ({\n    id: idx,\n    alt: img.alt,\n    size: { width: 1200, height: 630 },\n    contentType: 'image/png'\n  }));\n}\n\nexport default async function Image({ params, id }) {\n  const images = await getPostImages(params.slug);\n  const image = images[id];\n  return new ImageResponse(/* ... */);\n}\n```\n\n## Multiple Sitemaps\n\nUse `generateSitemaps` for large sites:\n\n```tsx\n// app/sitemap.ts\nimport type { MetadataRoute } from 'next';\n\nexport async function generateSitemaps() {\n  // Return array of sitemap IDs\n  return [{ id: 0 }, { id: 1 }, { id: 2 }];\n}\n\nexport default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {\n  const start = id * 50000;\n  const end = start + 50000;\n  const products = await getProducts(start, end);\n\n  return products.map((product) => ({\n    url: `https://example.com/product/${product.id}`,\n    lastModified: product.updatedAt\n  }));\n}\n```\n\nGenerates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.\n",".agents/skills/next-best-practices/parallel-routes.md":"# Parallel & Intercepting Routes\n\nParallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.\n\n## File Structure\n\n```\napp/\n├── @modal/                    # Parallel route slot\n│   ├── default.tsx            # Required! Returns null\n│   ├── (.)photos/             # Intercepts /photos/*\n│   │   └── [id]/\n│   │       └── page.tsx       # Modal content\n│   └── [...]catchall/         # Optional: catch unmatched\n│       └── page.tsx\n├── photos/\n│   └── [id]/\n│       └── page.tsx           # Full page (direct access)\n├── layout.tsx                 # Renders both children and @modal\n└── page.tsx\n```\n\n## Step 1: Root Layout with Slot\n\n```tsx\n// app/layout.tsx\nexport default function RootLayout({\n  children,\n  modal\n}: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\n    <html>\n      <body>\n        {children}\n        {modal}\n      </body>\n    </html>\n  );\n}\n```\n\n## Step 2: Default File (Critical!)\n\n**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.\n\n```tsx\n// app/@modal/default.tsx\nexport default function Default() {\n  return null;\n}\n```\n\nWithout this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.\n\n## Step 3: Intercepting Route (Modal)\n\nThe `(.)` prefix intercepts routes at the same level.\n\n```tsx\n// app/@modal/(.)photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Step 4: Full Page (Direct Access)\n\n```tsx\n// app/photos/[id]/page.tsx\nexport default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <div className='full-page'>\n      <img src={photo.url} alt={photo.title} />\n      <h1>{photo.title}</h1>\n    </div>\n  );\n}\n```\n\n## Step 5: Modal Component with Correct Closing\n\n**Critical: Use `router.back()` to close modals, NOT `router.push()` or `<Link>`.**\n\n```tsx\n// components/modal.tsx\n'use client';\n\nimport { useRouter } from 'next/navigation';\nimport { useCallback, useEffect, useRef } from 'react';\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter();\n  const overlayRef = useRef<HTMLDivElement>(null);\n\n  // Close on escape key\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') {\n        router.back(); // Correct\n      }\n    }\n    document.addEventListener('keydown', onKeyDown);\n    return () => document.removeEventListener('keydown', onKeyDown);\n  }, [router]);\n\n  // Close on overlay click\n  const handleOverlayClick = useCallback(\n    (e: React.MouseEvent) => {\n      if (e.target === overlayRef.current) {\n        router.back(); // Correct\n      }\n    },\n    [router]\n  );\n\n  return (\n    <div\n      ref={overlayRef}\n      onClick={handleOverlayClick}\n      className='fixed inset-0 bg-black/50 flex items-center justify-center z-50'\n    >\n      <div className='bg-white rounded-lg p-6 max-w-2xl w-full mx-4'>\n        <button\n          onClick={() => router.back()} // Correct!\n          className='absolute top-4 right-4'\n        >\n          Close\n        </button>\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n### Why NOT `router.push('/')` or `<Link href=\"/\">`?\n\nUsing `push` or `Link` to \"close\" a modal:\n\n1. Adds a new history entry (back button shows modal again)\n2. Doesn't properly clear the intercepted route\n3. Can cause the modal to flash or persist unexpectedly\n\n`router.back()` correctly:\n\n1. Removes the intercepted route from history\n2. Returns to the previous page\n3. Properly unmounts the modal\n\n## Route Matcher Reference\n\nMatchers match **route segments**, not filesystem paths:\n\n| Matcher    | Matches       | Example                                                               |\n| ---------- | ------------- | --------------------------------------------------------------------- |\n| `(.)`      | Same level    | `@modal/(.)photos` intercepts `/photos`                               |\n| `(..)`     | One level up  | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |\n| `(..)(..)` | Two levels up | Rarely used                                                           |\n| `(...)`    | From root     | `@modal/(...)photos` intercepts `/photos` from anywhere               |\n\n**Common mistake**: Thinking `(..)` means \"parent folder\" - it means \"parent route segment\".\n\n## Handling Hard Navigation\n\nWhen users directly visit `/photos/123` (bookmark, refresh, shared link):\n\n- The intercepting route is bypassed\n- The full `photos/[id]/page.tsx` renders\n- Modal doesn't appear (expected behavior)\n\nIf you want the modal to appear on direct access too, you need additional logic:\n\n```tsx\n// app/photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoPage({ params }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  // Option: Render as modal on direct access too\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Common Gotchas\n\n### 1. Missing `default.tsx` → 404 on Refresh\n\nEvery `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).\n\n### 2. Modal Persists After Navigation\n\nYou're using `router.push()` instead of `router.back()`.\n\n### 3. Nested Parallel Routes Need Defaults Too\n\nIf you have `@modal` inside a route group, each level needs its own `default.tsx`:\n\n```\napp/\n├── (marketing)/\n│   ├── @modal/\n│   │   └── default.tsx     # Needed!\n│   └── layout.tsx\n└── layout.tsx\n```\n\n### 4. Intercepted Route Shows Wrong Content\n\nCheck your matcher:\n\n- `(.)photos` intercepts `/photos` from the same route level\n- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`\n\n### 5. TypeScript Errors with `params`\n\nIn Next.js 15+, `params` is a Promise:\n\n```tsx\n// Correct\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n## Complete Example: Photo Gallery Modal\n\n```\napp/\n├── @modal/\n│   ├── default.tsx\n│   └── (.)photos/\n│       └── [id]/\n│           └── page.tsx\n├── photos/\n│   ├── page.tsx           # Gallery grid\n│   └── [id]/\n│       └── page.tsx       # Full photo page\n├── layout.tsx\n└── page.tsx\n```\n\nLinks in the gallery:\n\n```tsx\n// app/photos/page.tsx\nimport Link from 'next/link';\n\nexport default async function Gallery() {\n  const photos = await getPhotos();\n\n  return (\n    <div className='grid grid-cols-3 gap-4'>\n      {photos.map((photo) => (\n        <Link key={photo.id} href={`/photos/${photo.id}`}>\n          <img src={photo.thumbnail} alt={photo.title} />\n        </Link>\n      ))}\n    </div>\n  );\n}\n```\n\nClicking a photo → Modal opens (intercepted)\nDirect URL → Full page renders\nRefresh while modal open → Full page renders\n",".agents/skills/next-best-practices/route-handlers.md":"# Route Handlers\n\nCreate API endpoints with `route.ts` files.\n\n## Basic Usage\n\n```tsx\n// app/api/users/route.ts\nexport async function GET() {\n  const users = await getUsers();\n  return Response.json(users);\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  const user = await createUser(body);\n  return Response.json(user, { status: 201 });\n}\n```\n\n## Supported Methods\n\n`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`\n\n## GET Handler Conflicts with page.tsx\n\n**A `route.ts` and `page.tsx` cannot coexist in the same folder.**\n\n```\napp/\n├── api/\n│   └── users/\n│       └── route.ts    # /api/users\n└── users/\n    ├── page.tsx        # /users (page)\n    └── route.ts        # Warning: Conflicts with page.tsx!\n```\n\nIf you need both a page and an API at the same path, use different paths:\n\n```\napp/\n├── users/\n│   └── page.tsx        # /users (page)\n└── api/\n    └── users/\n        └── route.ts    # /api/users (API)\n```\n\n## Environment Behavior\n\nRoute handlers run in a **Server Component-like environment**:\n\n- Yes: Can use `async/await`\n- Yes: Can access `cookies()`, `headers()`\n- Yes: Can use Node.js APIs\n- No: Cannot use React hooks\n- No: Cannot use React DOM APIs\n- No: Cannot use browser APIs\n\n```tsx\n// Bad: This won't work - no React DOM in route handlers\nimport { renderToString } from 'react-dom/server';\n\nexport async function GET() {\n  const html = renderToString(<Component />); // Error!\n  return new Response(html);\n}\n```\n\n## Dynamic Route Handlers\n\n```tsx\n// app/api/users/[id]/route.ts\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const user = await getUser(id);\n\n  if (!user) {\n    return Response.json({ error: 'Not found' }, { status: 404 });\n  }\n\n  return Response.json(user);\n}\n```\n\n## Request Helpers\n\n```tsx\nexport async function GET(request: Request) {\n  // URL and search params\n  const { searchParams } = new URL(request.url);\n  const query = searchParams.get('q');\n\n  // Headers\n  const authHeader = request.headers.get('authorization');\n\n  // Cookies (Next.js helper)\n  const cookieStore = await cookies();\n  const token = cookieStore.get('token');\n\n  return Response.json({ query, token });\n}\n```\n\n## Response Helpers\n\n```tsx\n// JSON response\nreturn Response.json({ data });\n\n// With status\nreturn Response.json({ error: 'Not found' }, { status: 404 });\n\n// With headers\nreturn Response.json(data, {\n  headers: {\n    'Cache-Control': 'max-age=3600'\n  }\n});\n\n// Redirect\nreturn Response.redirect(new URL('/login', request.url));\n\n// Stream\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/event-stream' }\n});\n```\n\n## When to Use Route Handlers vs Server Actions\n\n| Use Case                 | Route Handlers | Server Actions |\n| ------------------------ | -------------- | -------------- |\n| Form submissions         | No             | Yes            |\n| Data mutations from UI   | No             | Yes            |\n| Third-party webhooks     | Yes            | No             |\n| External API consumption | Yes            | No             |\n| Public REST API          | Yes            | No             |\n| File uploads             | Both work      | Both work      |\n\n**Prefer Server Actions** for mutations triggered from your UI.\n**Use Route Handlers** for external integrations and public APIs.\n",".agents/skills/next-best-practices/rsc-boundaries.md":"# RSC Boundaries\n\nDetect and prevent invalid patterns when crossing Server/Client component boundaries.\n\n## Detection Rules\n\n### 1. Async Client Components Are Invalid\n\nClient components **cannot** be async functions. Only Server Components can be async.\n\n**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`\n\n```tsx\n// Bad: async client component\n'use client';\nexport default async function UserProfile() {\n  const user = await getUser(); // Cannot await in client component\n  return <div>{user.name}</div>;\n}\n\n// Good: Remove async, fetch data in parent server component\n// page.tsx (server component - no 'use client')\nexport default async function Page() {\n  const user = await getUser();\n  return <UserProfile user={user} />;\n}\n\n// UserProfile.tsx (client component)\n('use client');\nexport function UserProfile({ user }: { user: User }) {\n  return <div>{user.name}</div>;\n}\n```\n\n```tsx\n// Bad: async arrow function client component\n'use client';\nconst Dashboard = async () => {\n  const data = await fetchDashboard();\n  return <div>{data}</div>;\n};\n\n// Good: Fetch in server component, pass data down\n```\n\n### 2. Non-Serializable Props to Client Components\n\nProps passed from Server → Client must be JSON-serializable.\n\n**Detect:** Server component passes these to a client component:\n\n- Functions (except Server Actions with `'use server'`)\n- `Date` objects\n- `Map`, `Set`, `WeakMap`, `WeakSet`\n- Class instances\n- `Symbol` (unless globally registered)\n- Circular references\n\n```tsx\n// Bad: Function prop\n// page.tsx (server)\nexport default function Page() {\n  const handleClick = () => console.log('clicked');\n  return <ClientButton onClick={handleClick} />;\n}\n\n// Good: Define function inside client component\n// ClientButton.tsx\n('use client');\nexport function ClientButton() {\n  const handleClick = () => console.log('clicked');\n  return <button onClick={handleClick}>Click</button>;\n}\n```\n\n```tsx\n// Bad: Date object (silently becomes string, then crashes)\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt} />; // Date object\n}\n\n// PostCard.tsx (client) - will crash on .getFullYear()\n('use client');\nexport function PostCard({ createdAt }: { createdAt: Date }) {\n  return <span>{createdAt.getFullYear()}</span>; // Runtime error!\n}\n\n// Good: Serialize to string on server\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt.toISOString()} />;\n}\n\n// PostCard.tsx (client)\n('use client');\nexport function PostCard({ createdAt }: { createdAt: string }) {\n  const date = new Date(createdAt);\n  return <span>{date.getFullYear()}</span>;\n}\n```\n\n```tsx\n// Bad: Class instance\nconst user = new UserModel(data)\n<ClientProfile user={user} /> // Methods will be stripped\n\n// Good: Pass plain object\nconst user = await getUser()\n<ClientProfile user={{ id: user.id, name: user.name }} />\n```\n\n```tsx\n// Bad: Map/Set\n<ClientComponent items={new Map([['a', 1]])} />\n\n// Good: Convert to array/object\n<ClientComponent items={Object.fromEntries(map)} />\n<ClientComponent items={Array.from(set)} />\n```\n\n### 3. Server Actions Are the Exception\n\nFunctions marked with `'use server'` CAN be passed to client components.\n\n```tsx\n// Valid: Server Action can be passed\n// actions.ts\n'use server';\nexport async function submitForm(formData: FormData) {\n  // server-side logic\n}\n\n// page.tsx (server)\nimport { submitForm } from './actions';\nexport default function Page() {\n  return <ClientForm onSubmit={submitForm} />; // OK!\n}\n\n// ClientForm.tsx (client)\n('use client');\nexport function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {\n  return <form action={onSubmit}>...</form>;\n}\n```\n\n## Quick Reference\n\n| Pattern                           | Valid? | Fix                                   |\n| --------------------------------- | ------ | ------------------------------------- |\n| `'use client'` + `async function` | No     | Fetch in server parent, pass data     |\n| Pass `() => {}` to client         | No     | Define in client or use server action |\n| Pass `new Date()` to client       | No     | Use `.toISOString()`                  |\n| Pass `new Map()` to client        | No     | Convert to object/array               |\n| Pass class instance to client     | No     | Pass plain object                     |\n| Pass server action to client      | Yes    | -                                     |\n| Pass `string/number/boolean`      | Yes    | -                                     |\n| Pass plain object/array           | Yes    | -                                     |\n",".agents/skills/next-best-practices/runtime-selection.md":"# Runtime Selection\n\n## Use Node.js Runtime by Default\n\nUse the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.\n\n```tsx\n// Good: Default - no runtime config needed (uses Node.js)\nexport default function Page() { ... }\n\n// Caution: Only if already used in project or specifically required\nexport const runtime = 'edge'\n```\n\n## When to Use Each\n\n### Node.js Runtime (Default)\n\n- Full Node.js API support\n- File system access (`fs`)\n- Full `crypto` support\n- Database connections\n- Most npm packages work\n\n### Edge Runtime\n\n- Only for specific edge-location latency requirements\n- Limited API (no `fs`, limited `crypto`)\n- Smaller cold start\n- Geographic distribution needs\n\n## Detection\n\n**Before adding `runtime = 'edge'`**, check:\n\n1. Does the project already use Edge runtime?\n2. Is there a specific latency requirement?\n3. Are all dependencies Edge-compatible?\n\nIf unsure, use Node.js runtime.\n",".agents/skills/next-best-practices/scripts.md":"# Scripts\n\nLoading third-party scripts in Next.js.\n\n## Use next/script\n\nAlways use `next/script` instead of native `<script>` tags for better performance.\n\n```tsx\n// Bad: Native script tag\n<script src='https://example.com/script.js'></script>;\n\n// Good: Next.js Script component\nimport Script from 'next/script';\n\n<Script src='https://example.com/script.js' />;\n```\n\n## Inline Scripts Need ID\n\nInline scripts require an `id` attribute for Next.js to track them.\n\n```tsx\n// Bad: Missing id\n<Script dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Has id\n<Script id=\"my-script\" dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Inline with id\n<Script id=\"show-banner\">\n  {`document.getElementById('banner').classList.remove('hidden')`}\n</Script>\n```\n\n## Don't Put Script in Head\n\n`next/script` should not be placed inside `next/head`. It handles its own positioning.\n\n```tsx\n// Bad: Script inside Head\nimport Head from 'next/head'\nimport Script from 'next/script'\n\n<Head>\n  <Script src=\"/analytics.js\" />\n</Head>\n\n// Good: Script outside Head\n<Head>\n  <title>Page</title>\n</Head>\n<Script src=\"/analytics.js\" />\n```\n\n## Loading Strategies\n\n```tsx\n// afterInteractive (default) - Load after page is interactive\n<Script src=\"/analytics.js\" strategy=\"afterInteractive\" />\n\n// lazyOnload - Load during idle time\n<Script src=\"/widget.js\" strategy=\"lazyOnload\" />\n\n// beforeInteractive - Load before page is interactive (use sparingly)\n// Only works in app/layout.tsx or pages/_document.js\n<Script src=\"/critical.js\" strategy=\"beforeInteractive\" />\n\n// worker - Load in web worker (experimental)\n<Script src=\"/heavy.js\" strategy=\"worker\" />\n```\n\n## Google Analytics\n\nUse `@next/third-parties` instead of inline GA scripts.\n\n```tsx\n// Bad: Inline GA script\n<Script src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\" />\n<Script id=\"ga-init\">\n  {`window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n    gtag('config', 'G-XXXXX');`}\n</Script>\n\n// Good: Next.js component\nimport { GoogleAnalytics } from '@next/third-parties/google'\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <body>{children}</body>\n      <GoogleAnalytics gaId=\"G-XXXXX\" />\n    </html>\n  )\n}\n```\n\n## Google Tag Manager\n\n```tsx\nimport { GoogleTagManager } from '@next/third-parties/google';\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <GoogleTagManager gtmId='GTM-XXXXX' />\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Other Third-Party Scripts\n\n```tsx\n// YouTube embed\nimport { YouTubeEmbed } from '@next/third-parties/google';\n\n<YouTubeEmbed videoid='dQw4w9WgXcQ' />;\n\n// Google Maps\nimport { GoogleMapsEmbed } from '@next/third-parties/google';\n\n<GoogleMapsEmbed apiKey='YOUR_API_KEY' mode='place' q='Brooklyn+Bridge,New+York,NY' />;\n```\n\n## Quick Reference\n\n| Pattern                                       | Issue                      | Fix                       |\n| --------------------------------------------- | -------------------------- | ------------------------- |\n| `<script src=\"...\">`                          | No optimization            | Use `next/script`         |\n| `<Script>` without id                         | Can't track inline scripts | Add `id` attribute        |\n| `<Script>` inside `<Head>`                    | Wrong placement            | Move outside Head         |\n| Inline GA/GTM scripts                         | No optimization            | Use `@next/third-parties` |\n| `strategy=\"beforeInteractive\"` outside layout | Won't work                 | Only use in root layout   |\n",".agents/skills/next-best-practices/self-hosting.md":"# Self-Hosting Next.js\n\nDeploy Next.js outside of Vercel with confidence.\n\n## Quick Start: Standalone Output\n\nFor Docker or any containerized deployment, use standalone output:\n\n```js\n// next.config.js\nmodule.exports = {\n  output: 'standalone'\n};\n```\n\nThis creates a minimal `standalone` folder with only production dependencies:\n\n```\n.next/\n├── standalone/\n│   ├── server.js          # Entry point\n│   ├── node_modules/      # Only production deps\n│   └── .next/             # Build output\n└── static/                # Must be copied separately\n```\n\n## Docker Deployment\n\n### Dockerfile\n\n```dockerfile\nFROM node:20-alpine AS base\n\n# Install dependencies\nFROM base AS deps\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci\n\n# Build\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nRUN npm run build\n\n# Production\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\n\n# Create non-root user\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\n# Copy standalone output\nCOPY --from=builder /app/.next/standalone ./\nCOPY --from=builder /app/.next/static ./.next/static\nCOPY --from=builder /app/public ./public\n\nUSER nextjs\n\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n```\n\n### Docker Compose\n\n```yaml\nversion: '3.8'\n\nservices:\n  web:\n    build: .\n    ports:\n      - '3000:3000'\n    environment:\n      - NODE_ENV=production\n    restart: unless-stopped\n    healthcheck:\n      test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']\n      interval: 30s\n      timeout: 10s\n      retries: 3\n```\n\n## PM2 Deployment\n\nFor traditional server deployments:\n\n```js\n// ecosystem.config.js\nmodule.exports = {\n  apps: [\n    {\n      name: 'nextjs',\n      script: '.next/standalone/server.js',\n      instances: 'max',\n      exec_mode: 'cluster',\n      env: {\n        NODE_ENV: 'production',\n        PORT: 3000\n      }\n    }\n  ]\n};\n```\n\n```bash\nnpm run build\npm2 start ecosystem.config.js\n```\n\n## ISR and Cache Handlers\n\n### The Problem\n\nISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:\n\n- Instance A regenerates page → saves to its local disk\n- Instance B serves stale page → doesn't see Instance A's cache\n- Load balancer sends users to random instances → inconsistent content\n\n### Solution: Custom Cache Handler\n\nNext.js 14+ supports custom cache handlers for shared storage:\n\n```js\n// next.config.js\nmodule.exports = {\n  cacheHandler: require.resolve('./cache-handler.js'),\n  cacheMaxMemorySize: 0 // Disable in-memory cache\n};\n```\n\n#### Redis Cache Handler Example\n\n```js\n// cache-handler.js\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL);\nconst CACHE_PREFIX = 'nextjs:';\n\nmodule.exports = class CacheHandler {\n  constructor(options) {\n    this.options = options;\n  }\n\n  async get(key) {\n    const data = await redis.get(CACHE_PREFIX + key);\n    if (!data) return null;\n\n    const parsed = JSON.parse(data);\n    return {\n      value: parsed.value,\n      lastModified: parsed.lastModified\n    };\n  }\n\n  async set(key, data, ctx) {\n    const cacheData = {\n      value: data,\n      lastModified: Date.now()\n    };\n\n    // Set TTL based on revalidate option\n    if (ctx?.revalidate) {\n      await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));\n    } else {\n      await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));\n    }\n  }\n\n  async revalidateTag(tags) {\n    // Implement tag-based invalidation\n    // This requires tracking which keys have which tags\n  }\n};\n```\n\n#### S3 Cache Handler Example\n\n```js\n// cache-handler.js\nconst { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');\n\nconst s3 = new S3Client({ region: process.env.AWS_REGION });\nconst BUCKET = process.env.CACHE_BUCKET;\n\nmodule.exports = class CacheHandler {\n  async get(key) {\n    try {\n      const response = await s3.send(\n        new GetObjectCommand({\n          Bucket: BUCKET,\n          Key: `cache/${key}`\n        })\n      );\n      const body = await response.Body.transformToString();\n      return JSON.parse(body);\n    } catch (err) {\n      if (err.name === 'NoSuchKey') return null;\n      throw err;\n    }\n  }\n\n  async set(key, data, ctx) {\n    await s3.send(\n      new PutObjectCommand({\n        Bucket: BUCKET,\n        Key: `cache/${key}`,\n        Body: JSON.stringify({\n          value: data,\n          lastModified: Date.now()\n        }),\n        ContentType: 'application/json'\n      })\n    );\n  }\n};\n```\n\n## What Works vs What Needs Setup\n\n| Feature              | Single Instance | Multi-Instance      | Notes                       |\n| -------------------- | --------------- | ------------------- | --------------------------- |\n| SSR                  | Yes             | Yes                 | No special setup            |\n| SSG                  | Yes             | Yes                 | Built at deploy time        |\n| ISR                  | Yes             | Needs cache handler | Filesystem cache breaks     |\n| Image Optimization   | Yes             | Yes                 | CPU-intensive, consider CDN |\n| Middleware           | Yes             | Yes                 | Runs on Node.js             |\n| Edge Runtime         | Limited         | Limited             | Some features Node-only     |\n| `revalidatePath/Tag` | Yes             | Needs cache handler | Must share cache            |\n| `next/font`          | Yes             | Yes                 | Fonts bundled at build      |\n| Draft Mode           | Yes             | Yes                 | Cookie-based                |\n\n## Image Optimization\n\nNext.js Image Optimization works out of the box but is CPU-intensive.\n\n### Option 1: Built-in (Simple)\n\nWorks automatically, but consider:\n\n- Set `deviceSizes` and `imageSizes` in config to limit variants\n- Use `minimumCacheTTL` to reduce regeneration\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    minimumCacheTTL: 60 * 60 * 24, // 24 hours\n    deviceSizes: [640, 750, 1080, 1920] // Limit sizes\n  }\n};\n```\n\n### Option 2: External Loader (Recommended for Scale)\n\nOffload to Cloudinary, Imgix, or similar:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    loader: 'custom',\n    loaderFile: './lib/image-loader.js'\n  }\n};\n```\n\n```js\n// lib/image-loader.js\nexport default function cloudinaryLoader({ src, width, quality }) {\n  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];\n  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;\n}\n```\n\n## Environment Variables\n\n### Build-time vs Runtime\n\n```js\n// Available at build time only (baked into bundle)\nNEXT_PUBLIC_API_URL=https://api.example.com\n\n// Available at runtime (server-side only)\nDATABASE_URL=postgresql://...\nAPI_SECRET=...\n```\n\n### Runtime Configuration\n\nFor truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:\n\n```tsx\n// app/api/config/route.ts\nexport async function GET() {\n  return Response.json({\n    apiUrl: process.env.API_URL,\n    features: process.env.FEATURES?.split(',')\n  });\n}\n```\n\n## OpenNext: Serverless Without Vercel\n\n[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.\n\n```bash\nnpx create-sst@latest\n# or\nnpx @opennextjs/aws build\n```\n\nSupports:\n\n- AWS Lambda + CloudFront\n- Cloudflare Workers\n- Netlify Functions\n- Deno Deploy\n\n## Health Check Endpoint\n\nAlways include a health check for load balancers:\n\n```tsx\n// app/api/health/route.ts\nexport async function GET() {\n  try {\n    // Optional: check database connection\n    // await db.$queryRaw`SELECT 1`;\n\n    return Response.json({ status: 'healthy' }, { status: 200 });\n  } catch (error) {\n    return Response.json({ status: 'unhealthy' }, { status: 503 });\n  }\n}\n```\n\n## Pre-Deployment Checklist\n\n1. **Build locally first**: `npm run build` - catch errors before deploy\n2. **Test standalone output**: `node .next/standalone/server.js`\n3. **Set `output: 'standalone'`** for Docker\n4. **Configure cache handler** for multi-instance ISR\n5. **Set `HOSTNAME=\"0.0.0.0\"`** for containers\n6. **Copy `public/` and `.next/static/`** - not included in standalone\n7. **Add health check endpoint**\n8. **Test ISR revalidation** after deployment\n9. **Monitor memory usage** - Node.js defaults may need tuning\n\n## Testing Cache Handler\n\n**Critical**: Test your cache handler on every Next.js upgrade:\n\n```bash\n# Start multiple instances\nPORT=3001 node .next/standalone/server.js &\nPORT=3002 node .next/standalone/server.js &\n\n# Trigger ISR revalidation\ncurl http://localhost:3001/api/revalidate?path=/posts\n\n# Verify both instances see the update\ncurl http://localhost:3001/posts\ncurl http://localhost:3002/posts\n# Should return identical content\n```\n",".agents/skills/next-best-practices/suspense-boundaries.md":"# Suspense Boundaries\n\nClient hooks that cause CSR bailout without Suspense boundaries.\n\n## useSearchParams\n\nAlways requires Suspense boundary in static routes. Without it, the entire page becomes client-side rendered.\n\n```tsx\n// Bad: Entire page becomes CSR\n'use client';\n\nimport { useSearchParams } from 'next/navigation';\n\nexport default function SearchBar() {\n  const searchParams = useSearchParams();\n  return <div>Query: {searchParams.get('q')}</div>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\nimport { Suspense } from 'react';\nimport SearchBar from './search-bar';\n\nexport default function Page() {\n  return (\n    <Suspense fallback={<div>Loading...</div>}>\n      <SearchBar />\n    </Suspense>\n  );\n}\n```\n\n## usePathname\n\nRequires Suspense boundary when route has dynamic parameters.\n\n```tsx\n// In dynamic route [slug]\n// Bad: No Suspense\n'use client';\nimport { usePathname } from 'next/navigation';\n\nexport function Breadcrumb() {\n  const pathname = usePathname();\n  return <nav>{pathname}</nav>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\n<Suspense fallback={<BreadcrumbSkeleton />}>\n  <Breadcrumb />\n</Suspense>\n```\n\nIf you use `generateStaticParams`, Suspense is optional.\n\n## Quick Reference\n\n| Hook                | Suspense Required    |\n| ------------------- | -------------------- |\n| `useSearchParams()` | Yes                  |\n| `usePathname()`     | Yes (dynamic routes) |\n| `useParams()`       | No                   |\n| `useRouter()`       | No                   |\n",".agents/skills/shadcn/SKILL.md":"---\nname: shadcn\ndescription: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. 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\".\nuser-invocable: false\nallowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)\n---\n\n# shadcn/ui\n\nA framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI.\n\n> **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.\n\n## Current Project Context\n\n```json\n!`npx shadcn@latest info --json`\n```\n\nThe JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component.\n\n## Principles\n\n1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too.\n2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table.\n3. **Use built-in variants before custom styles.** `variant=\"outline\"`, `size=\"sm\"`, etc.\n4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`.\n\n## Critical Rules\n\nThese rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs.\n\n### Styling & Tailwind → [styling.md](./rules/styling.md)\n\n- **`className` for layout, not styling.** Never override component colors or typography.\n- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`.\n- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`.\n- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`.\n- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`).\n- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries.\n- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking.\n\n### Forms & Inputs → [forms.md](./rules/forms.md)\n\n- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout.\n- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`.\n- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.**\n- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state.\n- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading.\n- **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.\n\n### Component Structure → [composition.md](./rules/composition.md)\n\n- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`.\n- **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)\n- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className=\"sr-only\"` if visually hidden.\n- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`.\n- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`.\n- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`.\n- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load.\n\n### Use Components, Not Custom Markup → [composition.md](./rules/composition.md)\n\n- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.\n- **Callouts use `Alert`.** Don't build custom styled divs.\n- **Empty states use `Empty`.** Don't build custom empty state markup.\n- **Toast via `sonner`.** Use `toast()` from `sonner`.\n- **Use `Separator`** instead of `<hr>` or `<div className=\"border-t\">`.\n- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.\n- **Use `Badge`** instead of custom styled spans.\n\n### Icons → [icons.md](./rules/icons.md)\n\n- **Icons in `Button` use `data-icon`.** `data-icon=\"inline-start\"` or `data-icon=\"inline-end\"` on the icon.\n- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.\n- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.\n\n### Chat & Messaging → [chat.md](./rules/chat.md)\n\n- **Chat UI composes the chat primitives.** Conversations use `MessageScroller`, rows use `Message`, surfaces use `Bubble`. Never hand-rolled bubble `div`s or a raw scroll container.\n- **`MessageScroller` owns scroll behavior.** Streaming follow, anchoring, and jump-to-latest (`MessageScrollerButton`) are built in. Don't write a `useStickToBottom`/`ResizeObserver` hook.\n- **Attachments use `Attachment`; system notes and dividers use `Marker`.** Not `Item` cards or `Separator` + a label.\n\n### CLI\n\n- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`.\n- **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply <code>` for existing projects, or `npx shadcn@latest init --preset <code>` when initializing.\n\n## Key Patterns\n\nThese are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above.\n\n```tsx\n// Form layout: FieldGroup + Field, not div + Label.\n<FieldGroup>\n  <Field>\n    <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n    <Input id=\"email\" />\n  </Field>\n</FieldGroup>\n\n// Validation: data-invalid on Field, aria-invalid on the control.\n<Field data-invalid>\n  <FieldLabel>Email</FieldLabel>\n  <Input aria-invalid />\n  <FieldDescription>Invalid email.</FieldDescription>\n</Field>\n\n// Icons in buttons: data-icon, no sizing classes.\n<Button>\n  <SearchIcon data-icon=\"inline-start\" />\n  Search\n</Button>\n\n// Spacing: gap-*, not space-y-*.\n<div className=\"flex flex-col gap-4\">  // correct\n<div className=\"space-y-4\">           // wrong\n\n// Equal dimensions: size-*, not w-* h-*.\n<Avatar className=\"size-10\">   // correct\n<Avatar className=\"w-10 h-10\"> // wrong\n\n// Status colors: Badge variants or semantic tokens, not raw colors.\n<Badge variant=\"secondary\">+20.1%</Badge>    // correct\n<span className=\"text-emerald-600\">+20.1%</span> // wrong\n```\n\n## Component Selection\n\n| Need                       | Use                                                                                                 |\n| -------------------------- | --------------------------------------------------------------------------------------------------- |\n| Button/action              | `Button` with appropriate variant                                                                   |\n| Form inputs                | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` |\n| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem`                                                                   |\n| Data display               | `Table`, `Card`, `Badge`, `Avatar`                                                                  |\n| Navigation                 | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination`                                     |\n| Overlays                   | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation)       |\n| Feedback                   | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner`                                        |\n| Command palette            | `Command` inside `Dialog`                                                                           |\n| Charts                     | `Chart` (wraps Recharts)                                                                            |\n| Layout                     | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible`                          |\n| Empty states               | `Empty`                                                                                             |\n| Menus                      | `DropdownMenu`, `ContextMenu`, `Menubar`                                                            |\n| Tooltips/info              | `Tooltip`, `HoverCard`, `Popover`                                                                   |\n| Chat / conversation UI     | `MessageScroller`, `Message`, `Bubble`, `Attachment`, `Marker`                                      |\n\n## Key Fields\n\nThe injected project context contains these key fields:\n\n- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode.\n- **`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.\n- **`tailwindVersion`** → `\"v4\"` uses `@theme inline` blocks; `\"v3\"` uses `tailwind.config.js`.\n- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one.\n- **`style`** → component visual treatment (e.g. `nova`, `vega`).\n- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props.\n- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`.\n- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc.\n- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA).\n- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`).\n- **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information.\n\nSee [cli.md — `info` command](./cli.md) for the full field reference.\n\n## Component Docs, Examples, and Usage\n\nRun `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content.\n\n```bash\nnpx shadcn@latest docs button dialog select\n```\n\n**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.\n\n## Workflow\n\n1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh.\n2. **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.\n3. **Find components** — `npx shadcn@latest search`.\n4. **Get docs and examples** — run `npx shadcn@latest docs <component>` 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`.\n5. **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).\n6. **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.\n7. **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.\n8. **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`, `owner/repo`, etc.), ask which registry to use. Never default to a registry on behalf of the user.\n9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?\n   - **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values.\n   - **Inspect incoming preset**: `npx shadcn@latest preset decode <code>`. Use `preset url <code>` or `preset open <code>` to share or open the preset builder.\n   - **Overwrite**: `npx shadcn@latest apply <code>`. Overwrites detected components, fonts, and CSS variables.\n   - **Partial**: `npx shadcn@latest apply <code> --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.\n   - **Merge**: `npx shadcn@latest init --preset <code> --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.\n   - **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.\n   - **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 <current-base>` explicitly — preset codes do not encode the base.\n\n## Updating Components\n\nWhen 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.**\n\n1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected.\n2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local.\n3. Decide per file based on the diff:\n   - No local changes → safe to overwrite.\n   - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications.\n   - User says \"just update everything\" → use `--overwrite`, but confirm first.\n4. **Never use `--overwrite` without the user's explicit approval.**\n\n## Quick Reference\n\n```bash\n# Create a new project.\nnpx shadcn@latest init --name my-app --preset base-nova\nnpx shadcn@latest init --name my-app --preset a2r6bw --template vite\n\n# Create a monorepo project.\nnpx shadcn@latest init --name my-app --preset base-nova --monorepo\nnpx shadcn@latest init --name my-app --preset base-nova --template next --monorepo\n\n# Initialize existing project.\nnpx shadcn@latest init --preset base-nova\nnpx shadcn@latest init --defaults  # shortcut: --template=next --preset=nova (base style implied)\n\n# Apply a preset to an existing project.\nnpx shadcn@latest apply a2r6bw\nnpx shadcn@latest apply a2r6bw --only theme\nnpx shadcn@latest apply a2r6bw --only font\nnpx shadcn@latest apply a2r6bw --only theme,font\n\n# Inspect preset codes and project preset state.\nnpx shadcn@latest preset decode a2r6bw\nnpx shadcn@latest preset url a2r6bw\nnpx shadcn@latest preset open a2r6bw\nnpx shadcn@latest preset resolve\nnpx shadcn@latest preset resolve --json\n\n# Add components.\nnpx shadcn@latest add button card dialog\nnpx shadcn@latest add @magicui/shimmer-button\nnpx shadcn@latest add owner/repo/item\nnpx shadcn@latest add --all\n\n# Preview changes before adding/updating.\nnpx shadcn@latest add button --dry-run\nnpx shadcn@latest add button --diff button.tsx\nnpx shadcn@latest add @acme/form --view button.tsx\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# Search registries.\nnpx shadcn@latest search @shadcn -q \"sidebar\"\nnpx shadcn@latest search @tailark -q \"stats\"\nnpx shadcn@latest search owner/repo -q \"login\"\nnpx shadcn@latest search                          # all configured registries\nnpx shadcn@latest search @shadcn -q \"menu\" -t ui  # filter by item type\n\n# Get component docs and example URLs.\nnpx shadcn@latest docs button dialog select\n\n# View registry item details (for items not yet installed).\nnpx shadcn@latest view @shadcn/button\nnpx shadcn@latest view owner/repo/item\n```\n\n**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma`\n**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo)\n**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com).\n\n## Detailed References\n\n- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states\n- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading\n- [rules/chat.md](./rules/chat.md) — MessageScroller, Message, Bubble, Attachment, Marker; streaming, anchoring, jump-to-latest\n- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects\n- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index\n- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion\n- [cli.md](./cli.md) — Commands, flags, presets, templates\n- [registry.md](./registry.md) — Authoring source registries, `include`, item definitions, dependencies, GitHub registry rules\n- [customization.md](./customization.md) — Theming, CSS variables, extending components\n",".agents/skills/shadcn/cli.md":"# shadcn CLI Reference\n\nConfiguration is read from `components.json`.\n\n> **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.\n\n> **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.\n\n## Contents\n\n- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build\n- Templates: next, vite, start, react-router, astro\n- Presets: named, code, URL formats and fields\n- Switching presets\n\n---\n\n## Commands\n\n### `init` — Initialize or create a project\n\n```bash\nnpx shadcn@latest init [components...] [options]\n```\n\nInitializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.\n\n| Flag                    | Short | Description                                               | Default |\n| ----------------------- | ----- | --------------------------------------------------------- | ------- |\n| `--template <template>` | `-t`  | Template (next, start, vite, next-monorepo, react-router) | —       |\n| `--preset [name]`       | `-p`  | Preset configuration (named, code, or URL)                | —       |\n| `--yes`                 | `-y`  | Skip confirmation prompt                                  | `true`  |\n| `--defaults`            | `-d`  | Use defaults (`--template=next --preset=base-nova`)       | `false` |\n| `--force`               | `-f`  | Force overwrite existing configuration                    | `false` |\n| `--cwd <cwd>`           | `-c`  | Working directory                                         | current |\n| `--name <name>`         | `-n`  | Name for new project                                      | —       |\n| `--silent`              | `-s`  | Mute output                                               | `false` |\n| `--rtl`                 |       | Enable RTL support                                        | —       |\n| `--reinstall`           |       | Re-install existing UI components                         | `false` |\n| `--monorepo`            |       | Scaffold a monorepo project                               | —       |\n| `--no-monorepo`         |       | Skip the monorepo prompt                                  | —       |\n\n`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.\n\n### `apply` — Apply a preset to an existing project\n\n```bash\nnpx shadcn@latest apply [preset] [options]\n```\n\nApplies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components.\n\n| Flag                | Short | Description                                | Default |\n| ------------------- | ----- | ------------------------------------------ | ------- |\n| `--preset <preset>` | —     | Preset configuration (named, code, or URL) | —       |\n| `--yes`             | `-y`  | Skip confirmation prompt                   | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                          | current |\n| `--silent`          | `-s`  | Mute output                                | `false` |\n\n`[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match.\nIf no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`.\n\n### `add` — Add components\n\n> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.\n\n```bash\nnpx shadcn@latest add [components...] [options]\n```\n\nAccepts component names, registry-prefixed names (`@magicui/shimmer-button`),\nGitHub item addresses (`owner/repo/item`), URLs, or local paths.\n\n| Flag            | Short | Description                                                                                                          | Default |\n| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |\n| `--yes`         | `-y`  | Skip confirmation prompt                                                                                             | `false` |\n| `--overwrite`   | `-o`  | Overwrite existing files                                                                                             | `false` |\n| `--cwd <cwd>`   | `-c`  | Working directory                                                                                                    | current |\n| `--all`         | `-a`  | Add all available components                                                                                         | `false` |\n| `--path <path>` | `-p`  | Target path for the component                                                                                        | —       |\n| `--silent`      | `-s`  | Mute output                                                                                                          | `false` |\n| `--dry-run`     |       | Preview all changes without writing files                                                                            | `false` |\n| `--diff [path]` |       | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`)         | —       |\n| `--view [path]` |       | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | —       |\n\n#### Dry-Run Mode\n\nUse `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.\n\n```bash\n# Preview all changes.\nnpx shadcn@latest add button --dry-run\n\n# Show diffs for all files (top 5).\nnpx shadcn@latest add button --diff\n\n# Show the diff for a specific file.\nnpx shadcn@latest add button --diff button.tsx\n\n# Show contents for all files (top 5).\nnpx shadcn@latest add button --view\n\n# Show the full content of a specific file.\nnpx shadcn@latest add button --view button.tsx\n\n# Works with URLs too.\nnpx shadcn@latest add https://api.npoint.io/abc123 --dry-run\n\n# Works with public GitHub registries too.\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# CSS diffs.\nnpx shadcn@latest add button --diff globals.css\n```\n\n**When to use dry-run:**\n\n- When the user asks \"what files will this add?\" or \"what will this change?\" — use `--dry-run`.\n- Before overwriting existing components — use `--diff` to preview the changes first.\n- When the user wants to inspect component source code without installing — use `--view`.\n- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.\n- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.\n\n> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.\n\n#### Smart Merge from Upstream\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow.\n\n### `search` — Search registries\n\n```bash\nnpx shadcn@latest search [registries...] [options]\n```\n\nFuzzy search across registries. Also aliased as `npx shadcn@latest list`.\nSupports namespaces (`@acme`), public GitHub registry sources (`owner/repo`),\nand registry catalog URLs. Without `-q`, lists all items. When no registries are\npassed, searches every registry configured in `components.json`.\n\n| Flag                | Short | Description                                       | Default |\n| ------------------- | ----- | ------------------------------------------------- | ------- |\n| `--query <query>`   | `-q`  | Search query                                      | —       |\n| `--type <type>`     | `-t`  | Filter by item type (e.g. `ui`, `block`, `hook`); comma-separated | —       |\n| `--limit <number>`  | `-l`  | Max items to display                              | `100`   |\n| `--offset <number>` | `-o`  | Items to skip                                     | `0`     |\n| `--json`            |       | Output as JSON                                    | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                                 | current |\n\n### `view` — View item details\n\n```bash\nnpx shadcn@latest view <items...> [options]\n```\n\nDisplays item info including file contents. Examples:\n`npx shadcn@latest view @shadcn/button`,\n`npx shadcn@latest view owner/repo/item`.\n\n### `docs` — Get component documentation URLs\n\n```bash\nnpx shadcn@latest docs <components...> [options]\n```\n\nOutputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.\n\nExample output for `npx shadcn@latest docs input button`:\n\n```\nbase  radix\n\ninput\n  docs      https://ui.shadcn.com/docs/components/radix/input\n  examples  https://raw.githubusercontent.com/.../examples/input-example.tsx\n\nbutton\n  docs      https://ui.shadcn.com/docs/components/radix/button\n  examples  https://raw.githubusercontent.com/.../examples/button-example.tsx\n```\n\nSome components include an `api` link to the underlying library (e.g. `cmdk` for the command component).\n\n### `diff` — Check for updates\n\nDo not use this command. Use `npx shadcn@latest add --diff` instead.\n\n### `info` — Project information\n\n```bash\nnpx shadcn@latest info [options]\n```\n\nDisplays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.\n\n| Flag          | Short | Description       | Default |\n| ------------- | ----- | ----------------- | ------- |\n| `--cwd <cwd>` | `-c`  | Working directory | current |\n\n**Project Info fields:**\n\n| Field                | Type      | Meaning                                                            |\n| -------------------- | --------- | ------------------------------------------------------------------ |\n| `framework`          | `string`  | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |\n| `frameworkVersion`   | `string`  | Framework version (e.g. `15.2.4`)                                  |\n| `isSrcDir`           | `boolean` | Whether the project uses a `src/` directory                        |\n| `isRSC`              | `boolean` | Whether React Server Components are enabled                        |\n| `isTsx`              | `boolean` | Whether the project uses TypeScript                                |\n| `tailwindVersion`    | `string`  | `\"v3\"` or `\"v4\"`                                                   |\n| `tailwindConfigFile` | `string`  | Path to the Tailwind config file                                   |\n| `tailwindCssFile`    | `string`  | Path to the global CSS file                                        |\n| `aliasPrefix`        | `string`  | Import alias prefix (e.g. `@`, `~`, `@/`)                          |\n| `packageManager`     | `string`  | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`)            |\n\n**Components.json fields:**\n\n| Field                | Type      | Meaning                                                                                    |\n| -------------------- | --------- | ------------------------------------------------------------------------------------------ |\n| `base`               | `string`  | Primitive library (`radix` or `base`) — determines component APIs and available props      |\n| `style`              | `string`  | Visual style (e.g. `nova`, `vega`)                                                         |\n| `rsc`                | `boolean` | RSC flag from config                                                                       |\n| `tsx`                | `boolean` | TypeScript flag                                                                            |\n| `tailwind.config`    | `string`  | Tailwind config path                                                                       |\n| `tailwind.css`       | `string`  | Global CSS path — this is where custom CSS variables go                                    |\n| `iconLibrary`        | `string`  | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |\n| `aliases.components` | `string`  | Component import alias (e.g. `@/components`)                                               |\n| `aliases.utils`      | `string`  | Utils import alias (e.g. `@/lib/utils`)                                                    |\n| `aliases.ui`         | `string`  | UI component alias (e.g. `@/components/ui`)                                                |\n| `aliases.lib`        | `string`  | Lib alias (e.g. `@/lib`)                                                                   |\n| `aliases.hooks`      | `string`  | Hooks alias (e.g. `@/hooks`)                                                               |\n| `resolvedPaths`      | `object`  | Absolute file-system paths for each alias                                                  |\n| `registries`         | `object`  | Configured custom registries                                                               |\n\n**Links fields:**\n\nThe `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.\n\n### `build` — Build a custom registry\n\n```bash\nnpx shadcn@latest build [registry] [options]\n```\n\nBuilds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.\n\nFor authoring rules, `include`, item definitions, `registryDependencies`, and\nGitHub registry behavior, see [registry.md](./registry.md).\n\n| Flag              | Short | Description       | Default      |\n| ----------------- | ----- | ----------------- | ------------ |\n| `--output <path>` | `-o`  | Output directory  | `./public/r` |\n| `--cwd <cwd>`     | `-c`  | Working directory | current      |\n\n---\n\n## Templates\n\n| Value          | Framework      | Monorepo support |\n| -------------- | -------------- | ---------------- |\n| `next`         | Next.js        | Yes              |\n| `vite`         | Vite           | Yes              |\n| `start`        | TanStack Start | Yes              |\n| `react-router` | React Router   | Yes              |\n| `astro`        | Astro          | Yes              |\n| `laravel`      | Laravel        | No               |\n\nAll templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.\n\n---\n\n## Presets\n\nThree ways to specify a preset via `--preset`:\n\n1. **Named:** `--preset nova` or `--preset lyra`\n2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`)\n3. **URL:** `--preset \"https://ui.shadcn.com/init?base=radix&style=nova&...\"`\n\n> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.\n> Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset.\n\n## Switching Presets\n\nAsk the user first: **overwrite**, **merge**, or **skip** existing components?\n\n- **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components.\n- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.\n- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.\n\nAlways 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 <current-base>` explicitly — preset codes do not encode the base.\n",".agents/skills/shadcn/customization.md":"# Customization & Theming\n\nComponents reference semantic CSS variable tokens. Change the variables to change every component.\n\n## Contents\n\n- How it works (CSS variables → Tailwind utilities → components)\n- Color variables and OKLCH format\n- Dark mode setup\n- Changing the theme (presets, CSS variables)\n- Adding custom colors (Tailwind v3 and v4)\n- Border radius\n- Customizing components (variants, className, wrappers)\n- Checking for updates\n\n---\n\n## How It Works\n\n1. CSS variables defined in `:root` (light) and `.dark` (dark mode).\n2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.\n3. Components use these utilities — changing a variable changes all components that reference it.\n\n---\n\n## Color Variables\n\nEvery color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.\n\n| Variable                                     | Purpose                          |\n| -------------------------------------------- | -------------------------------- |\n| `--background` / `--foreground`              | Page background and default text |\n| `--card` / `--card-foreground`               | Card surfaces                    |\n| `--primary` / `--primary-foreground`         | Primary buttons and actions      |\n| `--secondary` / `--secondary-foreground`     | Secondary actions                |\n| `--muted` / `--muted-foreground`             | Muted/disabled states            |\n| `--accent` / `--accent-foreground`           | Hover and accent states          |\n| `--destructive` / `--destructive-foreground` | Error and destructive actions    |\n| `--border`                                   | Default border color             |\n| `--input`                                    | Form input borders               |\n| `--ring`                                     | Focus ring color                 |\n| `--chart-1` through `--chart-5`              | Chart/data visualization         |\n| `--sidebar-*`                                | Sidebar-specific colors          |\n| `--surface` / `--surface-foreground`         | Secondary surface                |\n\nColors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).\n\n---\n\n## Dark Mode\n\nClass-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:\n\n```tsx\nimport { ThemeProvider } from \"next-themes\"\n\n<ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n  {children}\n</ThemeProvider>\n```\n\n---\n\n## Changing the Theme\n\n```bash\n# Apply a preset code from ui.shadcn.com.\nnpx shadcn@latest apply --preset a2r6bw\n\n# Positional shorthand also works.\nnpx shadcn@latest apply a2r6bw\n\n# Switch to a named preset and overwrite existing components.\nnpx shadcn@latest apply --preset nova\n\n# Preserve existing components instead.\nnpx shadcn@latest init --preset nova --force --no-reinstall\n\n# Use a custom theme URL.\nnpx shadcn@latest apply --preset \"https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&...\"\n```\n\nOr edit CSS variables directly in `globals.css`.\n\n---\n\n## Adding Custom Colors\n\nAdd variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.\n\n```css\n/* 1. Define in the global CSS file. */\n:root {\n  --warning: oklch(0.84 0.16 84);\n  --warning-foreground: oklch(0.28 0.07 46);\n}\n.dark {\n  --warning: oklch(0.41 0.11 46);\n  --warning-foreground: oklch(0.99 0.02 95);\n}\n```\n\n```css\n/* 2a. Register with Tailwind v4 (@theme inline). */\n@theme inline {\n  --color-warning: var(--warning);\n  --color-warning-foreground: var(--warning-foreground);\n}\n```\n\nWhen `tailwindVersion` is `\"v3\"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:\n\n```js\n// 2b. Register with Tailwind v3 (tailwind.config.js).\nmodule.exports = {\n  theme: {\n    extend: {\n      colors: {\n        warning: \"oklch(var(--warning) / <alpha-value>)\",\n        \"warning-foreground\":\n          \"oklch(var(--warning-foreground) / <alpha-value>)\",\n      },\n    },\n  },\n}\n```\n\n```tsx\n// 3. Use in components.\n<div className=\"bg-warning text-warning-foreground\">Warning</div>\n```\n\n---\n\n## Border Radius\n\n`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).\n\n---\n\n## Customizing Components\n\nSee also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.\n\nPrefer these approaches in order:\n\n### 1. Built-in variants\n\n```tsx\n<Button variant=\"outline\" size=\"sm\">\n  Click\n</Button>\n```\n\n### 2. Tailwind classes via `className`\n\n```tsx\n<Card className=\"mx-auto max-w-md\">...</Card>\n```\n\n### 3. Add a new variant\n\nEdit the component source to add a variant via `cva`:\n\n```tsx\n// components/ui/button.tsx\nwarning: \"bg-warning text-warning-foreground hover:bg-warning/90\",\n```\n\n### 4. Wrapper components\n\nCompose shadcn/ui primitives into higher-level components:\n\n```tsx\nexport function ConfirmDialog({ title, description, onConfirm, children }) {\n  return (\n    <AlertDialog>\n      <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{description}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n```\n\n---\n\n## Checking for Updates\n\n```bash\nnpx shadcn@latest add button --diff\n```\n\nTo preview exactly what would change before updating, use `--dry-run` and `--diff`:\n\n```bash\nnpx shadcn@latest add button --dry-run        # see all affected files\nnpx shadcn@latest add button --diff button.tsx # see the diff for a specific file\n```\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.\n",".agents/skills/shadcn/evals/evals.json":"{\n  \"skill_name\": \"shadcn\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.\",\n      \"expected_output\": \"A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses FieldGroup and Field components for form layout instead of raw div with space-y\",\n        \"Uses Switch for independent on/off notification toggles (not looping Button with manual active state)\",\n        \"Uses data-invalid on Field and aria-invalid on the input control for validation states\",\n        \"Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing\",\n        \"Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500\",\n        \"No manual dark: color overrides\"\n      ]\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.\",\n      \"expected_output\": \"A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Includes DialogTitle for accessibility (visible or with sr-only class)\",\n        \"Avatar component includes AvatarFallback\",\n        \"Icons on buttons use the data-icon attribute (data-icon=\\\"inline-start\\\" or data-icon=\\\"inline-end\\\")\",\n        \"No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)\",\n        \"Uses tabler icons (@tabler/icons-react) instead of lucide-react\",\n        \"Uses asChild for custom triggers (radix preset)\"\n      ]\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.\",\n      \"expected_output\": \"A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)\",\n        \"Uses Skeleton component for loading placeholders instead of custom animate-pulse divs\",\n        \"Uses Badge component for percentage change instead of custom styled spans\",\n        \"Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600\",\n        \"Uses gap-* instead of space-y-* or space-x-* for spacing\",\n        \"Uses size-* when width and height are equal instead of separate w-* h-*\"\n      ]\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Build a chat conversation view: a scrollable thread of messages from two different people, each with an avatar, sender name, timestamp, and message bubble. A couple of messages include an image attachment and a PDF file attachment, and there's a 'Today' divider separating the days.\",\n      \"expected_output\": \"A React component composing MessageScroller, Message, Bubble, Attachment, and Marker from the registry instead of hand-rolled bubble/divider/attachment markup.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses MessageScroller (MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem) for the scrollable thread instead of a raw overflow-y-auto div or ScrollArea\",\n        \"Wraps each row in MessageScrollerItem inside MessageScrollerContent\",\n        \"Uses Message with MessageAvatar/MessageContent/MessageHeader for row layout instead of custom flex divs\",\n        \"Uses Bubble + BubbleContent for the message surface instead of a styled div with bg-muted/bg-primary\",\n        \"Uses Attachment (AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription) for the file and image attachments instead of Item or a custom card\",\n        \"Uses Marker (variant=\\\"separator\\\") for the 'Today' divider instead of Separator plus a centered label\",\n        \"Uses semantic color tokens and gap-* spacing; no raw colors like bg-emerald-500 and no space-y-*\",\n        \"Includes \\\"use client\\\" when the component uses state or event handlers (isRSC)\"\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"Using shadcn/ui (base-nova preset, lucide icons), build a streaming AI chat UI. The assistant's reply streams in while it generates, the view auto-scrolls to follow the latest content but stops following if the user scrolls up to read earlier messages, a 'jump to latest' button appears when the user has scrolled away from the bottom, and a subtle 'thinking…' shimmer shows while the model is generating.\",\n      \"expected_output\": \"A React component that delegates scroll/anchor behavior to MessageScroller and uses MessageScrollerButton for jump-to-latest and the shimmer utility for the thinking indicator — no hand-rolled scroll logic or custom shimmer keyframes.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses MessageScroller with MessageScrollerProvider (autoScroll) and scrollAnchor on message items for the stick-to-bottom/follow behavior instead of a custom useStickToBottom hook or ResizeObserver/scrollTop wiring\",\n        \"Uses MessageScrollerButton for the jump-to-latest control instead of a hand-built conditional button driven by manual scroll-position state\",\n        \"Uses the shimmer utility class for the 'thinking…' indicator instead of a custom @keyframes or bg-clip-text gradient animation\",\n        \"Wraps each message row in MessageScrollerItem inside MessageScrollerContent\",\n        \"Uses Message + Bubble + BubbleContent for the conversation rows instead of hand-rolled bubble divs\",\n        \"Uses semantic color tokens and gap-* spacing; includes \\\"use client\\\" (isRSC)\"\n      ]\n    }\n  ]\n}\n",".agents/skills/shadcn/mcp.md":"# shadcn MCP Server\n\nThe CLI includes an MCP server that lets AI assistants search, browse, view, and install items from registries.\n\n---\n\n## Setup\n\n```bash\nshadcn mcp        # start the MCP server (stdio)\nshadcn mcp init   # write config for your editor\n```\n\nEditor config files:\n\n| Editor      | Config file                     |\n| ----------- | ------------------------------- |\n| Claude Code | `.mcp.json`                     |\n| Cursor      | `.cursor/mcp.json`              |\n| VS Code     | `.vscode/mcp.json`              |\n| OpenCode    | `opencode.json`                 |\n| Codex       | `~/.codex/config.toml` (manual) |\n\n---\n\n## Tools\n\n> **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent.\n\n### `shadcn:get_project_registries`\n\nReturns registry names from `components.json`. Errors if no `components.json` exists.\n\n**Input:** none\n\n### `shadcn:list_items_in_registries`\n\nLists all items from one or more registries. Registries can be configured\nnamespaces such as `@acme`, public GitHub sources such as `owner/repo`, or\nregistry catalog URLs. Omit `registries` to list from every registry configured\nin `components.json`.\n\n**Input:** `registries` (string[], optional — omit for all configured), `types` (string[], optional — e.g. `[\"ui\", \"block\"]`), `limit` (number, optional, defaults to 100), `offset` (number, optional)\n\n### `shadcn:search_items_in_registries`\n\nFuzzy search across registries. Registries can be configured namespaces, public\nGitHub sources, or registry catalog URLs. Omit `registries` to search every\nregistry configured in `components.json` — e.g. \"find me a hero\" across all\nconfigured registries.\n\n**Input:** `registries` (string[], optional — omit for all configured), `query` (string), `types` (string[], optional — e.g. `[\"ui\", \"block\"]`), `limit` (number, optional, defaults to 100), `offset` (number, optional)\n\n### `shadcn:view_items_in_registries`\n\nView item details including full file contents.\n\n**Input:** `items` (string[]) — e.g.\n`[\"@shadcn/button\", \"@shadcn/card\", \"owner/repo/item\"]`\n\n### `shadcn:get_item_examples_from_registries`\n\nFind usage examples and demos with source code. Omit `registries` to search\nevery registry configured in `components.json`.\n\n**Input:** `registries` (string[], optional — omit for all configured), `query` (string) — e.g. `\"accordion-demo\"`, `\"button example\"`\n\n### `shadcn:get_add_command_for_items`\n\nReturns the CLI install command.\n\n**Input:** `items` (string[]) — e.g. `[\"@shadcn/button\"]`\n\n### `shadcn:get_audit_checklist`\n\nReturns a checklist for verifying components (imports, deps, lint, TypeScript).\n\n**Input:** none\n\n---\n\n## Configuring Registries\n\nNamespaced and authenticated registries are set in `components.json`. The\n`@shadcn` registry is always built-in. Public GitHub registries can also be used\ndirectly as `owner/repo` registry sources when the repository has a root\n`registry.json`; they do not need `components.json` configuration.\n\n```json\n{\n  \"registries\": {\n    \"@acme\": \"https://acme.com/r/{name}.json\",\n    \"@private\": {\n      \"url\": \"https://private.com/r/{name}.json\",\n      \"headers\": { \"Authorization\": \"Bearer ${MY_TOKEN}\" }\n    }\n  }\n}\n```\n\n- Names must start with `@`.\n- URLs must contain `{name}`.\n- `${VAR}` references are resolved from environment variables.\n\nCommunity registry index: `https://ui.shadcn.com/r/registries.json`\n",".agents/skills/shadcn/registry.md":"# Registry Authoring and Addresses\n\nUse this reference when the user wants to create, fix, publish, or reason about\na shadcn registry.\n\n## Mental Model\n\nA registry has two forms:\n\n- **Source registry**: an authored `registry.json` in a project or repository.\n  It may use `include` and file paths that point at source files.\n- **Built registry**: generated JSON files served to CLI consumers, usually\n  from `public/r`. Use `npx shadcn@latest build` to create this form.\n\nThe CLI installer consumes registry item payloads. A source registry is a way to\nauthor those payloads from real files.\n\nRegistry items are not limited to React components. They can distribute\ncomponents, hooks, utilities, design tokens, pages, config files, docs, rules,\nworkflows, templates, MCP files, and other project files.\n\n## Root `registry.json`\n\nThe root registry file should define registry metadata and either `items` or\n`include`.\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"acme\",\n  \"homepage\": \"https://acme.com\",\n  \"items\": [\n    {\n      \"name\": \"absolute-url\",\n      \"type\": \"registry:lib\",\n      \"title\": \"Absolute URL\",\n      \"description\": \"A utility to turn any path into an absolute URL.\",\n      \"files\": [\n        {\n          \"path\": \"lib/absolute-url.ts\",\n          \"type\": \"registry:lib\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nRoot registry rules:\n\n- Root `registry.json` must include `name` and `homepage`.\n- `items` is an array of registry item definitions.\n- `include` may be used to split the source registry into multiple files.\n- Included registry files may omit `name` and `homepage`.\n\n## Include\n\nUse `include` to keep large registries modular.\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"acme\",\n  \"homepage\": \"https://acme.com\",\n  \"include\": [\"registry/ui/registry.json\", \"registry/blocks/registry.json\"]\n}\n```\n\nInclude rules:\n\n- Include paths are relative to the `registry.json` that declares them.\n- Include paths must explicitly point to a `registry.json` file.\n- Do not use remote URLs, absolute paths, or parent traversal (`..`).\n- Item file paths are relative to the registry file that declares the item.\n- Duplicate item names fail across the resolved registry.\n\nExample included file:\n\n```json\n{\n  \"items\": [\n    {\n      \"name\": \"button\",\n      \"type\": \"registry:ui\",\n      \"files\": [\n        {\n          \"path\": \"button.tsx\",\n          \"type\": \"registry:ui\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nIf this file is at `registry/ui/registry.json`, then `button.tsx` is read from\n`registry/ui/button.tsx`, and the built item path is emitted relative to the\nroot registry.\n\n## Item Definitions\n\nCommon item fields:\n\n```json\n{\n  \"name\": \"login-form\",\n  \"type\": \"registry:block\",\n  \"title\": \"Login Form\",\n  \"description\": \"A login form with email and password fields.\",\n  \"dependencies\": [\"zod\"],\n  \"registryDependencies\": [\"button\", \"input\", \"label\"],\n  \"files\": [\n    {\n      \"path\": \"blocks/login-form.tsx\",\n      \"type\": \"registry:block\"\n    }\n  ],\n  \"cssVars\": {\n    \"light\": {\n      \"brand\": \"oklch(0.62 0.18 250)\"\n    },\n    \"dark\": {\n      \"brand\": \"oklch(0.72 0.16 250)\"\n    }\n  }\n}\n```\n\nImportant fields:\n\n- `name`: the installable item name. It is not necessarily a file path.\n- `type`: one of the registry item types, such as `registry:ui`,\n  `registry:block`, `registry:lib`, `registry:hook`, `registry:file`,\n  `registry:page`, `registry:theme`, `registry:style`, `registry:font`, or\n  `registry:item`.\n- `files`: source files copied or generated by the item.\n- `dependencies`: npm runtime dependencies.\n- `devDependencies`: npm development dependencies.\n- `registryDependencies`: other registry items required by this item.\n- `cssVars`, `css`, `tailwind`, `envVars`, and `docs`: optional install-time\n  additions.\n\nFile rules:\n\n- File paths are relative to the declaring `registry.json`.\n- `registry:file` and `registry:page` files require a `target`.\n- Do not use remote file URLs in source registry file paths.\n- Keep source files copy-pasteable: no hidden app-only imports.\n\n## Registry Dependencies\n\n`registryDependencies` entries are item addresses, not file paths.\n\n```json\n{\n  \"name\": \"login-form\",\n  \"type\": \"registry:block\",\n  \"registryDependencies\": [\"button\", \"@acme/input\", \"acme/ui/card#v1.2.0\"],\n  \"files\": [\n    {\n      \"path\": \"blocks/login-form.tsx\",\n      \"type\": \"registry:block\"\n    }\n  ]\n}\n```\n\nDependency rules:\n\n- Bare names such as `\"button\"` mean official shadcn items.\n- Bare names never mean same-registry or same-repository items.\n- Namespaced dependencies use `@namespace/item-name`.\n- GitHub dependencies use `owner/repo/item-name`.\n- Pin GitHub dependencies with `owner/repo/item-name#ref` when needed.\n- Refs are not inherited. If `owner/repo/foo#v2` depends on `bar` from the same\n  repo at `v2`, write `owner/repo/bar#v2`.\n- Do not use relative dependencies such as `\"./bar\"`.\n\n## Address Schemes\n\nWhen reasoning about a registry item string, classify it first.\n\n| Address                             | Scheme    | Meaning                                                      |\n| ----------------------------------- | --------- | ------------------------------------------------------------ |\n| `button`                            | shadcn    | Official shadcn item named `button`.                         |\n| `@acme/button`                      | namespace | Item `button` from configured registry `@acme`.              |\n| `@acme/ui/button`                   | namespace | Item `ui/button` from configured registry `@acme`.           |\n| `https://example.com/r/button.json` | url       | Built registry item JSON at that URL.                        |\n| `./button.json`                     | file      | Built registry item JSON on disk.                            |\n| `acme/ui/button`                    | github    | Item `button` from GitHub repo `acme/ui`.                    |\n| `acme/ui/forms/login#main`          | github    | Item `forms/login` from GitHub repo `acme/ui` at ref `main`. |\n\nFor namespace and GitHub addresses, slashful item names are allowed and are item\nnames, not file paths. Addresses ending in `.json` keep file-address\nprecedence, so `acme/ui/data/schema.json` is treated as a file path, not a\nGitHub item address.\n\n## GitHub Registries\n\nA public GitHub repository can act as a source registry when it has a root\n`registry.json`.\n\n```txt\nowner/repo/item-name[#ref]\n```\n\nRules:\n\n- The first two path segments are GitHub owner and repo.\n- All remaining path segments are the registry item name.\n- The source entrypoint is always root `registry.json`.\n- GitHub registries are source registries consumed directly by the CLI. They do\n  not require `shadcn build` or generated item JSON files.\n- `include` follows the same source-registry rules as local registries.\n- Currently, GitHub addresses support public `github.com` repositories only.\n- Private repos and GitHub Enterprise require explicit product decisions.\n\nWhen implementing GitHub registry fetching, resolve refs to a commit SHA before\nreading source files. Do not read moving refs directly from\n`raw.githubusercontent.com`, because branch-like refs can be cached for several\nminutes.\n\nPreferred flow:\n\n```txt\nowner/repo[#ref]\n  -> resolve ref with git ls-remote\n  -> commit SHA\n  -> read https://raw.githubusercontent.com/{owner}/{repo}/{sha}/registry.json\n  -> read includes and item files from the same SHA\n```\n\nThis keeps a command on one consistent repository snapshot.\n\nFull 40-character commit SHAs are already stable and can be used directly.\nBranches, tags, and short refs require Git so the CLI can resolve them to a\ncommit SHA first.\n\n## Build and Verify\n\nUse the CLI to build source registries:\n\n```bash\nnpx shadcn@latest build\nnpx shadcn@latest build registry.json --output public/r\n```\n\nUse CLI commands to inspect the result:\n\n```bash\nnpx shadcn@latest list @acme\nnpx shadcn@latest search @acme -q \"login\"\nnpx shadcn@latest view @acme/login-form\nnpx shadcn@latest add @acme/login-form --dry-run\nnpx shadcn@latest registry validate ./registry.json\n```\n\nUse GitHub addresses directly for public GitHub registries:\n\n```bash\nnpx shadcn@latest list owner/repo\nnpx shadcn@latest search owner/repo -q \"login\"\nnpx shadcn@latest view owner/repo/item\nnpx shadcn@latest add owner/repo/item --dry-run\nnpx shadcn@latest registry validate owner/repo\n```\n\nWhen working on registry implementation in the shadcn/ui codebase:\n\n- Keep address parsing pure and testable.\n- Do not add side effects to validators.\n- Preserve existing behavior for official shadcn, namespace, URL, and file\n  schemes.\n- Add tests for address parsing, source loading, dependency resolution, list,\n  search, view, and add paths.\n- Prefer small source-reader abstractions over a plugin system until there are\n  multiple real providers.\n",".agents/skills/shadcn/rules/base-vs-radix.md":"# Base vs Radix\n\nAPI differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.\n\n## Contents\n\n- Composition: asChild vs render\n- Button / trigger as non-button element\n- Select (items prop, placeholder, positioning, multiple, object values)\n- ToggleGroup (type vs multiple)\n- Slider (scalar vs array)\n- Accordion (type and defaultValue)\n\n---\n\n## Composition: asChild (radix) vs render (base)\n\nRadix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.\n\n**Incorrect:**\n\n```tsx\n<DialogTrigger>\n  <div>\n    <Button>Open</Button>\n  </div>\n</DialogTrigger>\n```\n\n**Correct (radix):**\n\n```tsx\n<DialogTrigger asChild>\n  <Button>Open</Button>\n</DialogTrigger>\n```\n\n**Correct (base):**\n\n```tsx\n<DialogTrigger render={<Button />}>Open</DialogTrigger>\n```\n\nThis applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.\n\n---\n\n## Button / trigger as non-button element (base only)\n\nWhen `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.\n\n**Incorrect (base):** missing `nativeButton={false}`.\n\n```tsx\n<Button render={<a href=\"/docs\" />}>Read the docs</Button>\n```\n\n**Correct (base):**\n\n```tsx\n<Button render={<a href=\"/docs\" />} nativeButton={false}>\n  Read the docs\n</Button>\n```\n\n**Correct (radix):**\n\n```tsx\n<Button asChild>\n  <a href=\"/docs\">Read the docs</a>\n</Button>\n```\n\nSame for triggers whose `render` is not a `Button`:\n\n```tsx\n// base.\n<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>\n  Pick date\n</PopoverTrigger>\n```\n\n---\n\n## Select\n\n**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.\n\n**Incorrect (base):**\n\n```tsx\n<Select>\n  <SelectTrigger><SelectValue placeholder=\"Select a fruit\" /></SelectTrigger>\n</Select>\n```\n\n**Correct (base):**\n\n```tsx\nconst items = [\n  { label: \"Select a fruit\", value: null },\n  { label: \"Apple\", value: \"apple\" },\n  { label: \"Banana\", value: \"banana\" },\n]\n\n<Select items={items}>\n  <SelectTrigger>\n    <SelectValue />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      {items.map((item) => (\n        <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>\n      ))}\n    </SelectGroup>\n  </SelectContent>\n</Select>\n```\n\n**Correct (radix):**\n\n```tsx\n<Select>\n  <SelectTrigger>\n    <SelectValue placeholder=\"Select a fruit\" />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      <SelectItem value=\"apple\">Apple</SelectItem>\n      <SelectItem value=\"banana\">Banana</SelectItem>\n    </SelectGroup>\n  </SelectContent>\n</Select>\n```\n\n**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder=\"...\">`.\n\n**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.\n\n```tsx\n// base.\n<SelectContent alignItemWithTrigger={false} side=\"bottom\">\n\n// radix.\n<SelectContent position=\"popper\">\n```\n\n---\n\n## Select — multiple selection and object values (base only)\n\nBase supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.\n\n**Correct (base — multiple selection):**\n\n```tsx\n<Select items={items} multiple defaultValue={[]}>\n  <SelectTrigger>\n    <SelectValue>\n      {(value: string[]) => value.length === 0 ? \"Select fruits\" : `${value.length} selected`}\n    </SelectValue>\n  </SelectTrigger>\n  ...\n</Select>\n```\n\n**Correct (base — object values):**\n\n```tsx\n<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>\n  <SelectTrigger>\n    <SelectValue>{(value) => value.name}</SelectValue>\n  </SelectTrigger>\n  ...\n</Select>\n```\n\n---\n\n## ToggleGroup\n\nBase uses a `multiple` boolean prop. Radix uses `type=\"single\"` or `type=\"multiple\"`.\n\n**Incorrect (base):**\n\n```tsx\n<ToggleGroup type=\"single\" defaultValue=\"daily\">\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Correct (base):**\n\n```tsx\n// Single (no prop needed), defaultValue is always an array.\n<ToggleGroup defaultValue={[\"daily\"]} spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n</ToggleGroup>\n\n// Multi-selection.\n<ToggleGroup multiple>\n  <ToggleGroupItem value=\"bold\">Bold</ToggleGroupItem>\n  <ToggleGroupItem value=\"italic\">Italic</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Correct (radix):**\n\n```tsx\n// Single, defaultValue is a string.\n<ToggleGroup type=\"single\" defaultValue=\"daily\" spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n</ToggleGroup>\n\n// Multi-selection.\n<ToggleGroup type=\"multiple\">\n  <ToggleGroupItem value=\"bold\">Bold</ToggleGroupItem>\n  <ToggleGroupItem value=\"italic\">Italic</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Controlled single value:**\n\n```tsx\n// base — wrap/unwrap arrays.\nconst [value, setValue] = React.useState(\"normal\")\n<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>\n\n// radix — plain string.\nconst [value, setValue] = React.useState(\"normal\")\n<ToggleGroup type=\"single\" value={value} onValueChange={setValue}>\n```\n\n---\n\n## Slider\n\nBase accepts a plain number for a single thumb. Radix always requires an array.\n\n**Incorrect (base):**\n\n```tsx\n<Slider defaultValue={[50]} max={100} step={1} />\n```\n\n**Correct (base):**\n\n```tsx\n<Slider defaultValue={50} max={100} step={1} />\n```\n\n**Correct (radix):**\n\n```tsx\n<Slider defaultValue={[50]} max={100} step={1} />\n```\n\nBoth use arrays for range sliders. Controlled `onValueChange` in base may need a cast:\n\n```tsx\n// base.\nconst [value, setValue] = React.useState([0.3, 0.7])\n<Slider value={value} onValueChange={(v) => setValue(v as number[])} />\n\n// radix.\nconst [value, setValue] = React.useState([0.3, 0.7])\n<Slider value={value} onValueChange={setValue} />\n```\n\n---\n\n## Accordion\n\nRadix requires `type=\"single\"` or `type=\"multiple\"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.\n\n**Incorrect (base):**\n\n```tsx\n<Accordion type=\"single\" collapsible defaultValue=\"item-1\">\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n```\n\n**Correct (base):**\n\n```tsx\n<Accordion defaultValue={[\"item-1\"]}>\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n\n// Multi-select.\n<Accordion multiple defaultValue={[\"item-1\", \"item-2\"]}>\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n  <AccordionItem value=\"item-2\">...</AccordionItem>\n</Accordion>\n```\n\n**Correct (radix):**\n\n```tsx\n<Accordion type=\"single\" collapsible defaultValue=\"item-1\">\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n```\n",".agents/skills/shadcn/rules/chat.md":"# Chat & Messaging\n\nComponents for conversation and chat UI. Compose these instead of hand-rolling\nbubbles, scroll containers, dividers, or attachment cards.\n\nInstall: `npx shadcn@latest add message-scroller message bubble attachment marker`\n\nThe same component names and props ship for both `base` and `radix`; only\ncomposition differs (`render` vs `asChild`). See [base-vs-radix.md](./base-vs-radix.md).\n\n## Contents\n\n- Scrollable threads use MessageScroller\n- Message rows use Message\n- Message surfaces use Bubble\n- Attachments use Attachment\n- System notes and dividers use Marker\n- Streaming, anchoring, and jump-to-latest are built in\n- Escape hatch: the scroller hooks\n\n---\n\n## Scrollable threads use MessageScroller\n\nA conversation that scrolls, follows new messages, restores position, or jumps\nto a message uses `MessageScroller`. Don't build a raw overflow container with\nmanual scroll wiring, and don't reach for `ScrollArea`.\n\nThe parts nest in a fixed order. Every direct child of the content is wrapped in\na `MessageScrollerItem` so the scroller can measure, anchor, preserve position,\ntrack visibility, and jump to it. `MessageScrollerButton` sits inside\n`MessageScroller`, after the viewport.\n\n**Incorrect:**\n\n```tsx\n// Hand-rolled scroll container with manual stick-to-bottom logic.\n<div ref={scrollRef} onScroll={handleScroll} className=\"flex-1 overflow-y-auto\">\n  <div className=\"flex flex-col gap-6 p-4\">\n    {messages.map((m) => (\n      <ChatMessage key={m.id} message={m} />\n    ))}\n  </div>\n</div>\n```\n\n**Correct:**\n\n```tsx\n<MessageScrollerProvider autoScroll>\n  <MessageScroller>\n    <MessageScrollerViewport>\n      <MessageScrollerContent>\n        {messages.map((message) => (\n          <MessageScrollerItem\n            key={message.id}\n            messageId={message.id}\n            scrollAnchor={message.role === \"user\"}\n          >\n            <Message align={message.role === \"user\" ? \"end\" : \"start\"}>\n              {/* ...message content... */}\n            </Message>\n          </MessageScrollerItem>\n        ))}\n      </MessageScrollerContent>\n    </MessageScrollerViewport>\n    <MessageScrollerButton />\n  </MessageScroller>\n</MessageScrollerProvider>\n```\n\n---\n\n## Message rows use Message\n\n`Message` lays out a single row: avatar, header, content, footer, with\nalignment. Group consecutive rows from one sender with `MessageGroup`. Don't\nrebuild the row from flex divs.\n\n`align=\"end\"` is the current user's side; `align=\"start\"` is everyone else.\n\n```tsx\n<Message align=\"start\">\n  <MessageAvatar>\n    <Avatar>\n      <AvatarImage src={sender.avatar} alt={sender.name} />\n      <AvatarFallback>{initials}</AvatarFallback>\n    </Avatar>\n  </MessageAvatar>\n  <MessageContent>\n    <MessageHeader>{sender.name}</MessageHeader>\n    <Bubble>\n      <BubbleContent>{text}</BubbleContent>\n    </Bubble>\n    <MessageFooter>{time}</MessageFooter>\n  </MessageContent>\n</Message>\n```\n\n---\n\n## Message surfaces use Bubble\n\nThe colored message surface is `Bubble` + `BubbleContent`, never a styled `div`\nwith `bg-muted` / `bg-primary` and hand-managed corners.\n\n- `variant`: `default`, `secondary`, `muted`, `tinted`, `outline`, `ghost`, `destructive`.\n- `align`: `start` or `end` (matches the `Message` side).\n\n`BubbleReactions` renders the reaction cluster. `side` (`top` | `bottom`) and\n`align` (`start` | `end`) position it against the bubble. Don't lay reactions out\nwith absolutely-positioned `Badge`s.\n\n**Incorrect:**\n\n```tsx\n<div className=\"w-fit rounded-2xl bg-primary px-3 py-2 text-primary-foreground\">\n  {text}\n</div>\n```\n\n**Correct:**\n\n```tsx\n<Bubble variant=\"default\" align=\"end\">\n  <BubbleContent>{text}</BubbleContent>\n  <BubbleReactions side=\"bottom\" align=\"end\">\n    <Badge variant=\"secondary\">👍 2</Badge>\n  </BubbleReactions>\n</Bubble>\n```\n\n---\n\n## Attachments use Attachment\n\nFile and image attachments use `Attachment`, not `Item` or a custom card. It\ncarries upload state, so wire `state` to the real status rather than rendering a\nseparate spinner.\n\n- `state`: `idle`, `uploading`, `processing`, `error`, `done`. `uploading` and\n  `processing` apply the `shimmer` animation to the title automatically.\n- `size`: `default`, `sm`, `xs`. `orientation`: `horizontal`, `vertical`.\n- Use `AttachmentGroup` to lay out several attachments in a scrolling row.\n\n```tsx\n<Attachment state=\"done\">\n  <AttachmentMedia variant=\"icon\">\n    <FileTextIcon />\n  </AttachmentMedia>\n  <AttachmentContent>\n    <AttachmentTitle>homepage-feedback.pdf</AttachmentTitle>\n    <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>\n  </AttachmentContent>\n  <AttachmentActions>\n    <AttachmentAction>\n      <DownloadIcon />\n    </AttachmentAction>\n  </AttachmentActions>\n</Attachment>\n```\n\nFor an image, use `<AttachmentMedia variant=\"image\">` with an `img` child.\n\n---\n\n## System notes and dividers use Marker\n\nStatus lines (\"Sarah joined the conversation\"), date dividers (\"Today\"), and\nlabeled separators are `Marker`, not a `Separator` plus a centered span.\n\n- `variant`: `default` (plain row), `separator` (centered label with rules on\n  each side), `border` (bottom-bordered row).\n- `MarkerIcon` holds a leading icon; `MarkerContent` holds the label.\n\n**Incorrect:**\n\n```tsx\n<div className=\"flex items-center gap-3 py-2\">\n  <Separator className=\"flex-1\" />\n  <span className=\"text-xs text-muted-foreground\">Today</span>\n  <Separator className=\"flex-1\" />\n</div>\n```\n\n**Correct:**\n\n```tsx\n<Marker variant=\"separator\">\n  <MarkerContent>Today</MarkerContent>\n</Marker>\n```\n\n---\n\n## Streaming, anchoring, and jump-to-latest are built in\n\n`MessageScroller` handles the behavior that chat UIs usually reinvent. Don't\nwrite a `useStickToBottom` hook, a `ResizeObserver`, or manual `scrollTop` math.\n\n- **Follow the live edge while streaming.** `MessageScrollerProvider` with\n  `autoScroll` keeps the view pinned to new content and yields the moment the\n  user scrolls up. Streaming token updates that grow the last message are\n  followed automatically.\n- **Anchor a turn.** `scrollAnchor` on a `MessageScrollerItem` marks the row to\n  hold in view (typically the user's message that started the turn).\n- **Jump to latest.** `MessageScrollerButton` appears when the user scrolls away\n  and scrolls back on click. `direction=\"end\"` (default) or `direction=\"start\"`.\n  It is a self-managing control, so don't gate it behind your own scroll-position\n  state.\n\nFor a \"thinking…\" indicator while the model generates, apply the `shimmer`\nutility to text. Don't author a custom keyframe animation. See\n[styling.md](./styling.md).\n\n---\n\n## Escape hatch: the scroller hooks\n\nFor behavior the parts don't expose, read state from the hooks rather than\nre-implementing the scroller: `useMessageScroller`,\n`useMessageScrollerVisibility`, and `useMessageScrollerScrollable`. They come\nfrom the auto-installed `@shadcn/react` dependency, so there's nothing extra to\ninstall. Reach for them only when composition can't express what you need.\n",".agents/skills/shadcn/rules/composition.md":"# Component Composition\n\n## Contents\n\n- Items always inside their Group component\n- Callouts use Alert\n- Empty states use Empty component\n- Toast notifications use sonner\n- Choosing between overlay components\n- Dialog, Sheet, and Drawer always need a Title\n- Card structure\n- Button has no isPending or isLoading prop\n- TabsTrigger must be inside TabsList\n- Avatar always needs AvatarFallback\n- Use Separator instead of raw hr or border divs\n- Use Skeleton for loading placeholders\n- Use Badge instead of custom styled spans\n\n---\n\n## Items always inside their Group component\n\nNever render items directly inside the content container.\n\n**Incorrect:**\n\n```tsx\n<SelectContent>\n  <SelectItem value=\"apple\">Apple</SelectItem>\n  <SelectItem value=\"banana\">Banana</SelectItem>\n</SelectContent>\n```\n\n**Correct:**\n\n```tsx\n<SelectContent>\n  <SelectGroup>\n    <SelectItem value=\"apple\">Apple</SelectItem>\n    <SelectItem value=\"banana\">Banana</SelectItem>\n  </SelectGroup>\n</SelectContent>\n```\n\nThis applies to all group-based components:\n\n| Item | Group |\n|------|-------|\n| `SelectItem`, `SelectLabel` | `SelectGroup` |\n| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |\n| `MenubarItem` | `MenubarGroup` |\n| `ContextMenuItem` | `ContextMenuGroup` |\n| `CommandItem` | `CommandGroup` |\n| `MessageScrollerItem` | `MessageScrollerContent` |\n| `Message` (consecutive, same sender) | `MessageGroup` |\n| `Bubble` (stacked) | `BubbleGroup` |\n| `Attachment` (in a row) | `AttachmentGroup` |\n\nChat components nest in a fixed order (`MessageScrollerProvider` → `MessageScroller` → `MessageScrollerViewport` → `MessageScrollerContent` → `MessageScrollerItem`). See [chat.md](./chat.md).\n\n---\n\n## Callouts use Alert\n\n```tsx\n<Alert>\n  <AlertTitle>Warning</AlertTitle>\n  <AlertDescription>Something needs attention.</AlertDescription>\n</Alert>\n```\n\n---\n\n## Empty states use Empty component\n\n```tsx\n<Empty>\n  <EmptyHeader>\n    <EmptyMedia variant=\"icon\"><FolderIcon /></EmptyMedia>\n    <EmptyTitle>No projects yet</EmptyTitle>\n    <EmptyDescription>Get started by creating a new project.</EmptyDescription>\n  </EmptyHeader>\n  <EmptyContent>\n    <Button>Create Project</Button>\n  </EmptyContent>\n</Empty>\n```\n\n---\n\n## Toast notifications use sonner\n\n```tsx\nimport { toast } from \"sonner\"\n\ntoast.success(\"Changes saved.\")\ntoast.error(\"Something went wrong.\")\ntoast(\"File deleted.\", {\n  action: { label: \"Undo\", onClick: () => undoDelete() },\n})\n```\n\n---\n\n## Choosing between overlay components\n\n| Use case | Component |\n|----------|-----------|\n| Focused task that requires input | `Dialog` |\n| Destructive action confirmation | `AlertDialog` |\n| Side panel with details or filters | `Sheet` |\n| Mobile-first bottom panel | `Drawer` |\n| Quick info on hover | `HoverCard` |\n| Small contextual content on click | `Popover` |\n\n---\n\n## Dialog, Sheet, and Drawer always need a Title\n\n`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className=\"sr-only\"` if visually hidden.\n\n```tsx\n<DialogContent>\n  <DialogHeader>\n    <DialogTitle>Edit Profile</DialogTitle>\n    <DialogDescription>Update your profile.</DialogDescription>\n  </DialogHeader>\n  ...\n</DialogContent>\n```\n\n---\n\n## Card structure\n\nUse full composition — don't dump everything into `CardContent`:\n\n```tsx\n<Card>\n  <CardHeader>\n    <CardTitle>Team Members</CardTitle>\n    <CardDescription>Manage your team.</CardDescription>\n  </CardHeader>\n  <CardContent>...</CardContent>\n  <CardFooter>\n    <Button>Invite</Button>\n  </CardFooter>\n</Card>\n```\n\n---\n\n## Button has no isPending or isLoading prop\n\nCompose with `Spinner` + `data-icon` + `disabled`:\n\n```tsx\n<Button disabled>\n  <Spinner data-icon=\"inline-start\" />\n  Saving...\n</Button>\n```\n\n---\n\n## TabsTrigger must be inside TabsList\n\nNever render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:\n\n```tsx\n<Tabs defaultValue=\"account\">\n  <TabsList>\n    <TabsTrigger value=\"account\">Account</TabsTrigger>\n    <TabsTrigger value=\"password\">Password</TabsTrigger>\n  </TabsList>\n  <TabsContent value=\"account\">...</TabsContent>\n</Tabs>\n```\n\n---\n\n## Avatar always needs AvatarFallback\n\nAlways include `AvatarFallback` for when the image fails to load:\n\n```tsx\n<Avatar>\n  <AvatarImage src=\"/avatar.png\" alt=\"User\" />\n  <AvatarFallback>JD</AvatarFallback>\n</Avatar>\n```\n\n---\n\n## Use existing components instead of custom markup\n\n| Instead of | Use |\n|---|---|\n| `<hr>` or `<div className=\"border-t\">` | `<Separator />` |\n| `<div className=\"animate-pulse\">` with styled divs | `<Skeleton className=\"h-4 w-3/4\" />` |\n| `<span className=\"rounded-full bg-green-100 ...\">` | `<Badge variant=\"secondary\">` |\n",".agents/skills/shadcn/rules/forms.md":"# Forms & Inputs\n\n## Contents\n\n- Forms use FieldGroup + Field\n- InputGroup requires InputGroupInput/InputGroupTextarea\n- Buttons inside inputs use InputGroup + InputGroupAddon\n- Option sets (2–7 choices) use ToggleGroup\n- FieldSet + FieldLegend for grouping related fields\n- Field validation and disabled states\n\n---\n\n## Forms use FieldGroup + Field\n\nAlways use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:\n\n```tsx\n<FieldGroup>\n  <Field>\n    <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n    <Input id=\"email\" type=\"email\" />\n  </Field>\n  <Field>\n    <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n    <Input id=\"password\" type=\"password\" />\n  </Field>\n</FieldGroup>\n```\n\nUse `Field orientation=\"horizontal\"` for settings pages. Use `FieldLabel className=\"sr-only\"` for visually hidden labels.\n\n**Choosing form controls:**\n\n- Simple text input → `Input`\n- Dropdown with predefined options → `Select`\n- Searchable dropdown → `Combobox`\n- Native HTML select (no JS) → `native-select`\n- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)\n- Single choice from few options → `RadioGroup`\n- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`\n- OTP/verification code → `InputOTP`\n- Multi-line text → `Textarea`\n\n---\n\n## InputGroup requires InputGroupInput/InputGroupTextarea\n\nNever use raw `Input` or `Textarea` inside an `InputGroup`.\n\n**Incorrect:**\n\n```tsx\n<InputGroup>\n  <Input placeholder=\"Search...\" />\n</InputGroup>\n```\n\n**Correct:**\n\n```tsx\nimport { InputGroup, InputGroupInput } from \"@/components/ui/input-group\"\n\n<InputGroup>\n  <InputGroupInput placeholder=\"Search...\" />\n</InputGroup>\n```\n\n---\n\n## Buttons inside inputs use InputGroup + InputGroupAddon\n\nNever place a `Button` directly inside or adjacent to an `Input` with custom positioning.\n\n**Incorrect:**\n\n```tsx\n<div className=\"relative\">\n  <Input placeholder=\"Search...\" className=\"pr-10\" />\n  <Button className=\"absolute right-0 top-0\" size=\"icon\">\n    <SearchIcon />\n  </Button>\n</div>\n```\n\n**Correct:**\n\n```tsx\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"@/components/ui/input-group\"\n\n<InputGroup>\n  <InputGroupInput placeholder=\"Search...\" />\n  <InputGroupAddon>\n    <Button size=\"icon\">\n      <SearchIcon data-icon=\"inline-start\" />\n    </Button>\n  </InputGroupAddon>\n</InputGroup>\n```\n\n---\n\n## Option sets (2–7 choices) use ToggleGroup\n\nDon't manually loop `Button` components with active state.\n\n**Incorrect:**\n\n```tsx\nconst [selected, setSelected] = useState(\"daily\")\n\n<div className=\"flex gap-2\">\n  {[\"daily\", \"weekly\", \"monthly\"].map((option) => (\n    <Button\n      key={option}\n      variant={selected === option ? \"default\" : \"outline\"}\n      onClick={() => setSelected(option)}\n    >\n      {option}\n    </Button>\n  ))}\n</div>\n```\n\n**Correct:**\n\n```tsx\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\n\n<ToggleGroup spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n  <ToggleGroupItem value=\"monthly\">Monthly</ToggleGroupItem>\n</ToggleGroup>\n```\n\nCombine with `Field` for labelled toggle groups:\n\n```tsx\n<Field orientation=\"horizontal\">\n  <FieldTitle id=\"theme-label\">Theme</FieldTitle>\n  <ToggleGroup aria-labelledby=\"theme-label\" spacing={2}>\n    <ToggleGroupItem value=\"light\">Light</ToggleGroupItem>\n    <ToggleGroupItem value=\"dark\">Dark</ToggleGroupItem>\n    <ToggleGroupItem value=\"system\">System</ToggleGroupItem>\n  </ToggleGroup>\n</Field>\n```\n\n> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).\n\n---\n\n## FieldSet + FieldLegend for grouping related fields\n\nUse `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:\n\n```tsx\n<FieldSet>\n  <FieldLegend variant=\"label\">Preferences</FieldLegend>\n  <FieldDescription>Select all that apply.</FieldDescription>\n  <FieldGroup className=\"gap-3\">\n    <Field orientation=\"horizontal\">\n      <Checkbox id=\"dark\" />\n      <FieldLabel htmlFor=\"dark\" className=\"font-normal\">Dark mode</FieldLabel>\n    </Field>\n  </FieldGroup>\n</FieldSet>\n```\n\n---\n\n## Field validation and disabled states\n\nBoth attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.\n\n```tsx\n// Invalid.\n<Field data-invalid>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" aria-invalid />\n  <FieldDescription>Invalid email address.</FieldDescription>\n</Field>\n\n// Disabled.\n<Field data-disabled>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" disabled />\n</Field>\n```\n\nWorks for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.\n",".agents/skills/shadcn/rules/icons.md":"# Icons\n\n**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`.\n\n---\n\n## Icons in Button use data-icon attribute\n\nAdd `data-icon=\"inline-start\"` (prefix) or `data-icon=\"inline-end\"` (suffix) to the icon. No sizing classes on the icon.\n\n**Incorrect:**\n\n```tsx\n<Button>\n  <SearchIcon className=\"mr-2 size-4\" />\n  Search\n</Button>\n```\n\n**Correct:**\n\n```tsx\n<Button>\n  <SearchIcon data-icon=\"inline-start\"/>\n  Search\n</Button>\n\n<Button>\n  Next\n  <ArrowRightIcon data-icon=\"inline-end\"/>\n</Button>\n```\n\n---\n\n## No sizing classes on icons inside components\n\nComponents handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes.\n\n**Incorrect:**\n\n```tsx\n<Button>\n  <SearchIcon className=\"size-4\" data-icon=\"inline-start\" />\n  Search\n</Button>\n\n<DropdownMenuItem>\n  <SettingsIcon className=\"mr-2 size-4\" />\n  Settings\n</DropdownMenuItem>\n```\n\n**Correct:**\n\n```tsx\n<Button>\n  <SearchIcon data-icon=\"inline-start\" />\n  Search\n</Button>\n\n<DropdownMenuItem>\n  <SettingsIcon />\n  Settings\n</DropdownMenuItem>\n```\n\n---\n\n## Pass icons as component objects, not string keys\n\nUse `icon={CheckIcon}`, not a string key to a lookup map.\n\n**Incorrect:**\n\n```tsx\nconst iconMap = {\n  check: CheckIcon,\n  alert: AlertIcon,\n}\n\nfunction StatusBadge({ icon }: { icon: string }) {\n  const Icon = iconMap[icon]\n  return <Icon />\n}\n\n<StatusBadge icon=\"check\" />\n```\n\n**Correct:**\n\n```tsx\n// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).\nimport { CheckIcon } from \"lucide-react\"\n\nfunction StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {\n  return <Icon />\n}\n\n<StatusBadge icon={CheckIcon} />\n```\n",".agents/skills/shadcn/rules/styling.md":"# Styling & Customization\n\nSee [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.\n\n## Contents\n\n- Semantic colors\n- Built-in variants first\n- className for layout only\n- No space-x-* / space-y-*\n- Prefer size-* over w-* h-* when equal\n- Prefer truncate shorthand\n- No manual dark: color overrides\n- Use cn() for conditional classes\n- No manual z-index on overlay components\n- Use shimmer / scroll-fade utilities, not custom animations\n\n---\n\n## Semantic colors\n\n**Incorrect:**\n\n```tsx\n<div className=\"bg-blue-500 text-white\">\n  <p className=\"text-gray-600\">Secondary text</p>\n</div>\n```\n\n**Correct:**\n\n```tsx\n<div className=\"bg-primary text-primary-foreground\">\n  <p className=\"text-muted-foreground\">Secondary text</p>\n</div>\n```\n\n---\n\n## No raw color values for status/state indicators\n\nFor positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.\n\n**Incorrect:**\n\n```tsx\n<span className=\"text-emerald-600\">+20.1%</span>\n<span className=\"text-green-500\">Active</span>\n<span className=\"text-red-600\">-3.2%</span>\n```\n\n**Correct:**\n\n```tsx\n<Badge variant=\"secondary\">+20.1%</Badge>\n<Badge>Active</Badge>\n<span className=\"text-destructive\">-3.2%</span>\n```\n\nIf you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).\n\n---\n\n## Built-in variants first\n\n**Incorrect:**\n\n```tsx\n<Button className=\"border border-input bg-transparent hover:bg-accent\">\n  Click me\n</Button>\n```\n\n**Correct:**\n\n```tsx\n<Button variant=\"outline\">Click me</Button>\n```\n\n---\n\n## className for layout only\n\nUse `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.\n\n**Incorrect:**\n\n```tsx\n<Card className=\"bg-blue-100 text-blue-900 font-bold\">\n  <CardContent>Dashboard</CardContent>\n</Card>\n```\n\n**Correct:**\n\n```tsx\n<Card className=\"max-w-md mx-auto\">\n  <CardContent>Dashboard</CardContent>\n</Card>\n```\n\nTo customize a component's appearance, prefer these approaches in order:\n1. **Built-in variants** — `variant=\"outline\"`, `variant=\"destructive\"`, etc.\n2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.\n3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).\n\n---\n\n## No space-x-* / space-y-*\n\nUse `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.\n\n```tsx\n<div className=\"flex flex-col gap-4\">\n  <Input />\n  <Input />\n  <Button>Submit</Button>\n</div>\n```\n\n---\n\n## Prefer size-* over w-* h-* when equal\n\n`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.\n\n---\n\n## Prefer truncate shorthand\n\n`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.\n\n---\n\n## No manual dark: color overrides\n\nUse semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.\n\n---\n\n## Use cn() for conditional classes\n\nUse the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings.\n\n**Incorrect:**\n\n```tsx\n<div className={`flex items-center ${isActive ? \"bg-primary text-primary-foreground\" : \"bg-muted\"}`}>\n```\n\n**Correct:**\n\n```tsx\nimport { cn } from \"@/lib/utils\"\n\n<div className={cn(\"flex items-center\", isActive ? \"bg-primary text-primary-foreground\" : \"bg-muted\")}>\n```\n\n---\n\n## No manual z-index on overlay components\n\n`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.\n\n---\n\n## Use shimmer / scroll-fade utilities, not custom animations\n\nFor a live \"thinking…\" or loading-text shimmer, apply the `shimmer` utility. Don't author a custom `@keyframes` or a `bg-clip-text` gradient sweep.\n\nFor scroll-aware edge fading on a scroll container, use `scroll-fade` (and the axis variants `scroll-fade-x` / `scroll-fade-b`). Don't hand-roll mask gradients. The chat components already apply these internally: `Attachment` shimmers its title during upload, and `MessageScrollerViewport` fades its edges.\n\n**Incorrect:**\n\n```tsx\n<span className=\"animate-pulse bg-gradient-to-r from-muted-foreground/40 via-foreground/70 to-muted-foreground/40 bg-clip-text text-transparent [animation:shimmer_1.6s_infinite]\">\n  Thinking…\n</span>\n```\n\n**Correct:**\n\n```tsx\n<span className=\"shimmer\">Thinking…</span>\n```\n",".agents/skills/tanstack-form/SKILL.md":"---\nname: tanstack-form\ndescription: Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.\n---\n\n## Overview\n\nTanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).\n\n**Package:** `@tanstack/react-form`\n**Adapters:** `@tanstack/zod-form-adapter`, `@tanstack/valibot-form-adapter`\n**Status:** Stable (v1)\n\n## Installation\n\n```bash\nnpm install @tanstack/react-form\n# Optional schema adapters:\nnpm install @tanstack/zod-form-adapter zod\nnpm install @tanstack/valibot-form-adapter valibot\n```\n\n## Core: useForm\n\n```tsx\nimport { useForm } from '@tanstack/react-form';\n\nfunction MyForm() {\n  const form = useForm({\n    defaultValues: {\n      firstName: '',\n      lastName: '',\n      email: '',\n      age: 0\n    },\n    onSubmit: async ({ value }) => {\n      // value is fully typed\n      await submitToServer(value);\n    },\n    onSubmitInvalid: ({ value, formApi }) => {\n      console.log('Validation failed:', formApi.state.errors);\n    }\n  });\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n        form.handleSubmit();\n      }}\n    >\n      {/* Fields */}\n      <form.Subscribe\n        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}\n        children={({ canSubmit, isSubmitting }) => (\n          <button type='submit' disabled={!canSubmit}>\n            {isSubmitting ? 'Submitting...' : 'Submit'}\n          </button>\n        )}\n      />\n    </form>\n  );\n}\n```\n\n## Fields (form.Field)\n\n```tsx\n<form.Field\n  name=\"firstName\"\n  validators={{\n    onChange: ({ value }) =>\n      value.length < 3 ? 'Must be at least 3 characters' : undefined,\n  }}\n  children={(field) => (\n    <div>\n      <label htmlFor={field.name}>First Name</label>\n      <input\n        id={field.name}\n        name={field.name}\n        value={field.state.value}\n        onBlur={field.handleBlur}\n        onChange={(e) => field.handleChange(e.target.value)}\n      />\n      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (\n        <em>{field.state.meta.errors.join(', ')}</em>\n      )}\n    </div>\n  )}\n/>\n\n<!-- Nested fields use dot notation -->\n<form.Field name=\"address.city\">\n  {(field) => (\n    <input\n      value={field.state.value}\n      onChange={(e) => field.handleChange(e.target.value)}\n      onBlur={field.handleBlur}\n    />\n  )}\n</form.Field>\n```\n\n## Validation\n\n### Validation Timing\n\n| Cause      | When                     |\n| ---------- | ------------------------ |\n| `onChange` | After every value change |\n| `onBlur`   | When field loses focus   |\n| `onSubmit` | During submission        |\n| `onMount`  | When field mounts        |\n\n### Synchronous Validation\n\n```tsx\n<form.Field\n  name='age'\n  validators={{\n    onChange: ({ value }) => {\n      if (value < 18) return 'Must be 18 or older';\n      return undefined; // undefined = valid\n    },\n    onBlur: ({ value }) => {\n      if (!value) return 'Required';\n      return undefined;\n    }\n  }}\n/>\n```\n\n### Asynchronous Validation\n\n```tsx\n<form.Field\n  name='username'\n  asyncDebounceMs={500}\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const res = await fetch(`/api/check-username?q=${value}`);\n      const { available } = await res.json();\n      if (!available) return 'Username taken';\n      return undefined;\n    }\n  }}\n>\n  {(field) => (\n    <>\n      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />\n      {field.state.meta.isValidating && <span>Checking...</span>}\n    </>\n  )}\n</form.Field>\n```\n\n### Schema Validation (Zod)\n\n```tsx\nimport { zodValidator } from '@tanstack/zod-form-adapter'\nimport { z } from 'zod'\n\nconst form = useForm({\n  defaultValues: { email: '', age: 0 },\n  validatorAdapter: zodValidator(),\n  onSubmit: async ({ value }) => { /* ... */ },\n})\n\n<form.Field\n  name=\"email\"\n  validators={{\n    onChange: z.string().email('Invalid email'),\n    onBlur: z.string().min(1, 'Required'),\n  }}\n/>\n\n<form.Field\n  name=\"age\"\n  validators={{\n    onChange: z.number().min(18, 'Must be 18+'),\n  }}\n/>\n```\n\n### Form-Level Validation\n\n```tsx\nconst form = useForm({\n  defaultValues: { password: '', confirmPassword: '' },\n  validators: {\n    onChange: ({ value }) => {\n      if (value.password !== value.confirmPassword) {\n        return 'Passwords do not match';\n      }\n      return undefined;\n    }\n  }\n});\n```\n\n### Linked/Dependent Fields\n\n```tsx\n<form.Field\n  name='confirmPassword'\n  validators={{\n    onChangeListenTo: ['password'], // Re-validate when password changes\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      if (value !== password) return 'Passwords do not match';\n      return undefined;\n    }\n  }}\n/>\n```\n\n## Array Fields\n\n```tsx\n<form.Field name='people' mode='array'>\n  {(field) => (\n    <div>\n      {field.state.value.map((_, index) => (\n        <div key={index}>\n          <form.Field name={`people[${index}].name`}>\n            {(subField) => (\n              <input\n                value={subField.state.value}\n                onChange={(e) => subField.handleChange(e.target.value)}\n              />\n            )}\n          </form.Field>\n          <button type='button' onClick={() => field.removeValue(index)}>\n            Remove\n          </button>\n        </div>\n      ))}\n      <button type='button' onClick={() => field.pushValue({ name: '', age: 0 })}>\n        Add Person\n      </button>\n    </div>\n  )}\n</form.Field>\n```\n\n### Array Methods\n\n```typescript\nfield.pushValue(item); // Add to end\nfield.insertValue(index, item); // Insert at index\nfield.replaceValue(index, item); // Replace at index\nfield.removeValue(index); // Remove at index\nfield.swapValues(indexA, indexB); // Swap positions\nfield.moveValue(from, to); // Move position\n```\n\n## Listeners (Side Effects)\n\n```tsx\n<form.Field\n  name='country'\n  listeners={{\n    onChange: ({ value }) => {\n      // Side effect: reset dependent fields\n      form.setFieldValue('state', '');\n      form.setFieldValue('postalCode', '');\n    }\n  }}\n/>\n```\n\n## Reactivity (form.Subscribe & useStore)\n\n```tsx\n// Render-prop subscription (fine-grained)\n<form.Subscribe\n  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}\n  children={({ canSubmit, isDirty }) => (\n    <div>\n      {isDirty && <span>Unsaved changes</span>}\n      <button disabled={!canSubmit}>Save</button>\n    </div>\n  )}\n/>;\n\n// Hook-based subscription\nfunction FormStatus() {\n  const isValid = form.useStore((s) => s.isValid);\n  return isValid ? null : <p>Fix errors</p>;\n}\n```\n\n## Form State\n\n```typescript\ninterface FormState {\n  values: TFormData;\n  errors: ValidationError[];\n  errorMap: Record<string, ValidationError>;\n  isFormValid: boolean;\n  isFieldsValid: boolean;\n  isValid: boolean; // isFormValid && isFieldsValid\n  isTouched: boolean;\n  isPristine: boolean;\n  isDirty: boolean;\n  isSubmitting: boolean;\n  isSubmitted: boolean;\n  isSubmitSuccessful: boolean;\n  submissionAttempts: number;\n  canSubmit: boolean; // isValid && !isSubmitting\n}\n```\n\n## Field State\n\n```typescript\ninterface FieldState<TData> {\n  value: TData;\n  meta: {\n    isTouched: boolean;\n    isDirty: boolean;\n    isPristine: boolean;\n    isValidating: boolean;\n    errors: ValidationError[];\n    errorMap: Record<ValidationCause, ValidationError>;\n  };\n}\n```\n\n## FormApi Methods\n\n```typescript\nform.handleSubmit();\nform.reset();\nform.getFieldValue(field);\nform.setFieldValue(field, value);\nform.getFieldMeta(field);\nform.setFieldMeta(field, updater);\nform.validateAllFields(cause);\nform.validateField(field, cause);\nform.deleteField(field);\n```\n\n## Shared Form Options (formOptions)\n\n```tsx\nimport { formOptions } from '@tanstack/react-form';\n\nconst sharedOpts = formOptions({\n  defaultValues: { firstName: '', lastName: '' }\n});\n\n// Reuse across components\nconst form = useForm({\n  ...sharedOpts,\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n## Server-Side Validation\n\n```tsx\n// TanStack Start / Next.js server action\nimport { ServerValidateError } from '@tanstack/react-form/nextjs';\n\nexport async function validateForm(data: FormData) {\n  const email = data.get('email') as string;\n  if (await checkEmailExists(email)) {\n    throw new ServerValidateError({\n      form: 'Submission failed',\n      fields: { email: 'Email already registered' }\n    });\n  }\n}\n```\n\n## TypeScript Integration\n\n```tsx\n// Type-safe field paths with DeepKeys\ninterface UserForm {\n  name: string\n  address: { street: string; city: string }\n  tags: string[]\n  contacts: Array<{ name: string; phone: string }>\n}\n\n// TypeScript auto-completes all valid paths:\n// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'\n<form.Field name=\"address.city\" />     // OK\n<form.Field name=\"nonexistent\" />       // Type Error!\n```\n\n## Best Practices\n\n1. **Always call `e.preventDefault()` and `e.stopPropagation()`** on form submit\n2. **Always attach `onBlur={field.handleBlur}`** for blur validation and isTouched tracking\n3. **Use `mode=\"array\"`** for array fields to get array methods\n4. **Return `undefined`** (not null/false) for valid validators\n5. **Use `asyncDebounceMs`** for async validators to prevent API spam\n6. **Check `isTouched` before showing errors** for better UX\n7. **Use `form.Subscribe` with selectors** to minimize re-renders\n8. **Use `formOptions`** for shared configuration across components\n9. **Use schema validators** (Zod/Valibot) for complex validation rules\n10. **Use `onChangeListenTo`** for cross-field validation dependencies\n\n## Common Pitfalls\n\n- Forgetting `e.preventDefault()` on form submit (causes page reload)\n- Not attaching `onBlur` to inputs (breaks blur validation and isTouched)\n- Returning `null` or `false` instead of `undefined` for valid fields\n- Using `mode=\"array\"` incorrectly (only needed on the array field itself, not sub-fields)\n- Subscribing to entire form state instead of using selectors (unnecessary re-renders)\n- Not using `asyncDebounceMs` with async validators (fires on every keystroke)\n",".agents/skills/tanstack-query/SKILL.md":"---\nname: tanstack-query\ndescription: TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration. Use when fetching data, managing server state, or working with TanStack Query hooks.\n---\n\n# TanStack Query Patterns\n\n## Purpose\n\nModern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.\n\n**Note**: v5 (released October 2023) has breaking changes from v4:\n\n- `isLoading` → `isPending` for status\n- `cacheTime` → `gcTime` (garbage collection time)\n- React 18.0+ required\n- Callbacks removed from useQuery (onError, onSuccess, onSettled)\n- `keepPreviousData` replaced with `placeholderData` function\n\n## When to Use This Skill\n\n- Fetching data with TanStack Query\n- Using useSuspenseQuery or useQuery\n- Managing mutations\n- Cache invalidation and updates\n- API service patterns\n\n---\n\n## Quick Start\n\n### Primary Pattern: useSuspenseQuery\n\nFor **all new components**, use `useSuspenseQuery`:\n\n```typescript\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { postsApi } from '~/features/posts/api/postsApi';\n\nfunction PostList() {\n  const { data: posts } = useSuspenseQuery({\n    queryKey: ['posts'],\n    queryFn: postsApi.getAll,\n  });\n\n  return (\n    <div>\n      {posts.map(post => (\n        <PostCard key={post.id} post={post} />\n      ))}\n    </div>\n  );\n}\n\n// Wrap with Suspense\n<Suspense fallback={<PostsSkeleton />}>\n  <PostList />\n</Suspense>\n```\n\n**Benefits:**\n\n- No `isLoading` checks needed\n- Integrates with Suspense boundaries\n- Cleaner component code\n- Consistent loading UX\n\n---\n\n## useSuspenseQuery Patterns\n\n### Basic Usage\n\n```typescript\nconst { data } = useSuspenseQuery({\n  queryKey: ['user', userId],\n  queryFn: () => userApi.get(userId),\n});\n\n// data is never undefined - guaranteed by Suspense\nreturn <div>{data.name}</div>;\n```\n\n### With Parameters\n\n```typescript\nfunction UserPosts({ userId }: { userId: string }) {\n  const { data: posts } = useSuspenseQuery({\n    queryKey: ['users', userId, 'posts'],\n    queryFn: () => postsApi.getByUser(userId),\n  });\n\n  return <div>{posts.length} posts</div>;\n}\n```\n\n### Dependent Queries\n\n```typescript\nfunction PostDetails({ postId }: { postId: string }) {\n  // First query\n  const { data: post } = useSuspenseQuery({\n    queryKey: ['posts', postId],\n    queryFn: () => postsApi.get(postId),\n  });\n\n  // Second query depends on first\n  const { data: author } = useSuspenseQuery({\n    queryKey: ['users', post.authorId],\n    queryFn: () => userApi.get(post.authorId),\n  });\n\n  return <div>{author.name} wrote {post.title}</div>;\n}\n```\n\n---\n\n## useQuery (Legacy Pattern)\n\nUse `useQuery` only when you need loading/error states in the component:\n\n```typescript\nimport { useQuery } from '@tanstack/react-query';\n\nfunction Component() {\n  const { data, isPending, error } = useQuery({\n    queryKey: ['posts'],\n    queryFn: postsApi.getAll,\n  });\n\n  if (isPending) return <Spinner />;\n  if (error) return <Error error={error} />;\n\n  return <div>{data.map(...)}</div>;\n}\n```\n\n**When to use `useQuery` vs `useSuspenseQuery`:**\n\n- Use `useSuspenseQuery` by default (preferred)\n- Use `useQuery` only when you need component-level loading states\n- Most cases should use `useSuspenseQuery` + Suspense boundaries\n\n---\n\n## Mutations\n\n### Basic Mutation\n\n```typescript\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\n\nfunction CreatePostButton() {\n  const queryClient = useQueryClient();\n\n  const mutation = useMutation({\n    mutationFn: postsApi.create,\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: ['posts'] });\n    },\n  });\n\n  const handleCreate = () => {\n    mutation.mutate({\n      title: 'New Post',\n      content: 'Content here',\n    });\n  };\n\n  return (\n    <button onClick={handleCreate} disabled={mutation.isPending}>\n      {mutation.isPending ? 'Creating...' : 'Create Post'}\n    </button>\n  );\n}\n```\n\n### Optimistic Updates\n\n```typescript\nconst mutation = useMutation({\n  mutationFn: postsApi.update,\n  onMutate: async (updatedPost) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });\n\n    // Snapshot previous value\n    const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['posts', updatedPost.id], updatedPost);\n\n    // Return context with snapshot\n    return { previousPost };\n  },\n  onError: (err, updatedPost, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['posts', updatedPost.id], context.previousPost);\n  },\n  onSettled: (data, error, variables) => {\n    // Refetch after mutation\n    queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });\n  }\n});\n```\n\n---\n\n## Cache Management\n\n### Invalidation\n\n```typescript\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\n// Invalidate all posts queries\nqueryClient.invalidateQueries({ queryKey: ['posts'] });\n\n// Invalidate specific post\nqueryClient.invalidateQueries({ queryKey: ['posts', postId] });\n\n// Invalidate all queries\nqueryClient.invalidateQueries();\n```\n\n### Manual Updates\n\n```typescript\n// Update cache directly\nqueryClient.setQueryData(['posts', postId], newPost);\n\n// Update with function\nqueryClient.setQueryData(['posts'], (oldPosts) => [...oldPosts, newPost]);\n```\n\n### Prefetching\n\n```typescript\n// Prefetch data\nawait queryClient.prefetchQuery({\n  queryKey: ['posts', postId],\n  queryFn: () => postsApi.get(postId),\n});\n\n// In a component\nconst prefetchPost = (postId: string) => {\n  queryClient.prefetchQuery({\n    queryKey: ['posts', postId],\n    queryFn: () => postsApi.get(postId),\n  });\n};\n\n<Link\n  to={`/posts/${post.id}`}\n  onMouseEnter={() => prefetchPost(post.id)}\n>\n  {post.title}\n</Link>\n```\n\n---\n\n## API Service Pattern\n\n### Centralized API Service\n\n```typescript\n// features/posts/api/postsApi.ts\nimport { apiClient } from '@/lib/apiClient';\nimport type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';\n\nexport const postsApi = {\n  getAll: async (): Promise<Post[]> => {\n    const response = await apiClient.get('/posts');\n    return response.data;\n  },\n\n  get: async (id: string): Promise<Post> => {\n    const response = await apiClient.get(`/posts/${id}`);\n    return response.data;\n  },\n\n  create: async (data: CreatePostDto): Promise<Post> => {\n    const response = await apiClient.post('/posts', data);\n    return response.data;\n  },\n\n  update: async (id: string, data: UpdatePostDto): Promise<Post> => {\n    const response = await apiClient.put(`/posts/${id}`, data);\n    return response.data;\n  },\n\n  delete: async (id: string): Promise<void> => {\n    await apiClient.delete(`/posts/${id}`);\n  },\n\n  getByUser: async (userId: string): Promise<Post[]> => {\n    const response = await apiClient.get(`/users/${userId}/posts`);\n    return response.data;\n  }\n};\n```\n\n### Usage in Components\n\n```typescript\nimport { postsApi } from '~/features/posts/api/postsApi';\n\n// In query\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll\n});\n\n// In mutation\nconst mutation = useMutation({\n  mutationFn: postsApi.create\n});\n```\n\n---\n\n## Query Keys\n\n### Key Structure\n\n```typescript\n// List queries\n['posts'][('posts', { status: 'published' })][ // All posts // Filtered posts\n  // Detail queries\n  ('posts', postId)\n][('posts', postId, 'comments')][ // Single post // Post comments\n  // Nested resources\n  ('users', userId, 'posts')\n][('users', userId, 'posts', postId)]; // User's posts // Specific user post\n```\n\n### Key Factories\n\n```typescript\n// features/posts/api/postKeys.ts\nexport const postKeys = {\n  all: ['posts'] as const,\n  lists: () => [...postKeys.all, 'list'] as const,\n  list: (filters: string) => [...postKeys.lists(), { filters }] as const,\n  details: () => [...postKeys.all, 'detail'] as const,\n  detail: (id: string) => [...postKeys.details(), id] as const,\n  comments: (id: string) => [...postKeys.detail(id), 'comments'] as const\n};\n\n// Usage\nconst { data } = useSuspenseQuery({\n  queryKey: postKeys.detail(postId),\n  queryFn: () => postsApi.get(postId)\n});\n\n// Invalidate all post lists\nqueryClient.invalidateQueries({ queryKey: postKeys.lists() });\n```\n\n---\n\n## Error Handling\n\n### With Error Boundaries\n\n```typescript\nimport { ErrorBoundary } from 'react-error-boundary';\n\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <Suspense fallback={<Loading />}>\n    <DataComponent />\n  </Suspense>\n</ErrorBoundary>\n\n// In component\nfunction DataComponent() {\n  const { data } = useSuspenseQuery({\n    queryKey: ['data'],\n    queryFn: fetchData,\n    // Errors automatically caught by ErrorBoundary\n  });\n\n  return <div>{data}</div>;\n}\n```\n\n### Retry and Cache Configuration\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll,\n  retry: 3, // Retry 3 times\n  retryDelay: 1000, // Wait 1s between retries\n  gcTime: 5 * 60 * 1000 // Garbage collection time: 5 minutes (v5: was 'cacheTime')\n});\n```\n\n---\n\n## Best Practices\n\n### 1. Use Suspense by Default\n\n```typescript\n// ✅ Good: useSuspenseQuery + Suspense\n<Suspense fallback={<Skeleton />}>\n  <DataComponent />\n</Suspense>\n\nfunction DataComponent() {\n  const { data } = useSuspenseQuery({...});\n  return <div>{data}</div>;\n}\n\n// ❌ Avoid: useQuery with manual loading\nfunction DataComponent() {\n  const { data, isPending } = useQuery({...});\n  if (isPending) return <Spinner />;\n  return <div>{data}</div>;\n}\n```\n\n### 2. Consistent Query Keys\n\n```typescript\n// ✅ Good: Use key factories\nconst { data } = useSuspenseQuery({\n  queryKey: postKeys.detail(id),\n  queryFn: () => postsApi.get(id)\n});\n\n// ❌ Avoid: Inconsistent keys\nconst { data } = useSuspenseQuery({\n  queryKey: ['post', id], // Different format\n  queryFn: () => postsApi.get(id)\n});\n```\n\n### 3. Centralized API Services\n\n```typescript\n// ✅ Good: API service\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll\n});\n\n// ❌ Avoid: Inline fetching\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: async () => {\n    const res = await fetch('/api/posts');\n    return res.json();\n  }\n});\n```\n\n---\n\n## Additional Resources\n\nFor more patterns, see:\n\n- [data-fetching.md](resources/data-fetching.md) - Advanced patterns\n- [cache-strategies.md](resources/cache-strategies.md) - Cache management\n- [mutation-patterns.md](resources/mutation-patterns.md) - Complex mutations\n",".agents/skills/tanstack-query/resources/cache-strategies.md":"# Cache Management Strategies\n\n## Cache Time Configuration\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes\n  gcTime: 10 * 60 * 1000 // Keep in cache for 10 minutes (formerly cacheTime)\n});\n```\n\n## Cache Invalidation\n\n### Invalidate Specific Queries\n\n```typescript\nconst queryClient = useQueryClient();\n\n// Invalidate all post queries\nqueryClient.invalidateQueries({ queryKey: ['posts'] });\n\n// Invalidate specific post\nqueryClient.invalidateQueries({ queryKey: ['post', postId] });\n\n// Invalidate with exact match\nqueryClient.invalidateQueries({\n  queryKey: ['posts'],\n  exact: true // Only ['posts'], not ['posts', 'list']\n});\n```\n\n### Invalidate on Mutation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n  }\n});\n```\n\n## Manual Cache Updates\n\n### Set Query Data\n\n```typescript\n// Update cache directly\nqueryClient.setQueryData(['post', postId], (oldData) => ({\n  ...oldData,\n  title: 'New Title'\n}));\n\n// Set new data\nqueryClient.setQueryData(['post', postId], newPost);\n```\n\n### Get Query Data\n\n```typescript\n// Read from cache\nconst cachedPost = queryClient.getQueryData(['post', postId]);\n\n// Use in initialData\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  initialData: () => queryClient.getQueryData(['posts'])?.find((p) => p.id === postId)\n});\n```\n\n## Refetch Strategies\n\n### Refetch on Window Focus\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  refetchOnWindowFocus: true // Refetch when tab regains focus\n});\n```\n\n### Refetch on Reconnect\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  refetchOnReconnect: true // Refetch when internet reconnects\n});\n```\n\n### Refetch Intervals\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['live-data'],\n  queryFn: fetchLiveData,\n  refetchInterval: 5000, // Refetch every 5 seconds\n  refetchIntervalInBackground: false // Pause when tab not active\n});\n```\n\n## Cache Persistence\n\n### Persist to localStorage\n\n```typescript\nimport { QueryClient } from '@tanstack/react-query';\nimport { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';\nimport { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      gcTime: 1000 * 60 * 60 * 24  // 24 hours\n    }\n  }\n});\n\nconst persister = createSyncStoragePersister({\n  storage: window.localStorage\n});\n\n<PersistQueryClientProvider\n  client={queryClient}\n  persister={persister}\n>\n  <App />\n</PersistQueryClientProvider>\n```\n\n## Cache Deduplication\n\n### Automatic Request Deduplication\n\n```typescript\n// Both components will share the same request\nfunction Component1() {\n  const { data } = useQuery({\n    queryKey: ['posts'],\n    queryFn: fetchPosts\n  });\n}\n\nfunction Component2() {\n  const { data } = useQuery({\n    queryKey: ['posts'], // Same key = same request\n    queryFn: fetchPosts\n  });\n}\n```\n\n## Cache Preloading\n\n### Prefetch Queries\n\n```typescript\nconst queryClient = useQueryClient();\n\n// Prefetch before navigation\nconst handleMouseEnter = () => {\n  queryClient.prefetchQuery({\n    queryKey: ['post', postId],\n    queryFn: () => fetchPost(postId)\n  });\n};\n\n// Prefetch in loader\nrouter.beforeEach(async (to, from, next) => {\n  await queryClient.prefetchQuery({\n    queryKey: ['user', to.params.userId],\n    queryFn: () => fetchUser(to.params.userId)\n  });\n  next();\n});\n```\n\n### Ensure Query Data\n\n```typescript\n// Fetch if not in cache, otherwise use cached\nawait queryClient.ensureQueryData({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId)\n});\n```\n\n## Selective Cache Updates\n\n### Update Nested Data\n\n```typescript\nqueryClient.setQueryData(['posts'], (oldPosts) => {\n  return oldPosts.map((post) => (post.id === updatedPost.id ? updatedPost : post));\n});\n```\n\n### Add to List Cache\n\n```typescript\n// After creating a post\nqueryClient.setQueryData(['posts'], (oldPosts = []) => {\n  return [newPost, ...oldPosts];\n});\n```\n\n### Remove from List Cache\n\n```typescript\n// After deleting a post\nqueryClient.setQueryData(['posts'], (oldPosts) => {\n  return oldPosts.filter((post) => post.id !== deletedPostId);\n});\n```\n\n## Cache Debugging\n\n### React Query Devtools\n\n```typescript\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\n\n<QueryClientProvider client={queryClient}>\n  <App />\n  <ReactQueryDevtools initialIsOpen={false} />\n</QueryClientProvider>\n```\n\n### Query Cache Events\n\n```typescript\nconst queryCache = queryClient.getQueryCache();\n\nqueryCache.subscribe((event) => {\n  console.log('Query cache event:', event.type, event.query.queryKey);\n});\n```\n\n## Best Practices\n\n1. **Set Appropriate staleTime** - Balance freshness vs performance\n2. **Use Invalidation Over Refetch** - Let queries refetch when needed\n3. **Prefetch Predictably** - Preload data on hover/intent\n4. **Update Cache on Mutations** - Keep UI in sync\n5. **Use Devtools** - Debug cache issues visually\n6. **Persist Important Data** - Save to localStorage for offline support\n7. **Deduplicate Requests** - Rely on automatic deduplication\n",".agents/skills/tanstack-query/resources/data-fetching.md":"# Advanced Data Fetching Patterns with TanStack Query\n\n## Dependent Queries\n\nQueries that depend on data from other queries:\n\n```typescript\n// First query - Get user ID\nconst { data: user } = useQuery({\n  queryKey: ['user'],\n  queryFn: fetchCurrentUser\n});\n\n// Second query - Depends on user ID\nconst { data: posts } = useQuery({\n  queryKey: ['posts', user?.id],\n  queryFn: () => fetchUserPosts(user!.id),\n  enabled: !!user // Only run when user is available\n});\n```\n\n## Parallel Queries\n\nFetch multiple independent queries simultaneously:\n\n```typescript\nfunction Dashboard() {\n  const queries = useQueries({\n    queries: [\n      { queryKey: ['stats'], queryFn: fetchStats },\n      { queryKey: ['recentPosts'], queryFn: fetchRecentPosts },\n      { queryKey: ['notifications'], queryFn: fetchNotifications }\n    ]\n  });\n\n  const [statsQuery, postsQuery, notificationsQuery] = queries;\n\n  if (queries.some(q => q.isLoading)) return <Loading />;\n\n  return <Dashboard\n    stats={statsQuery.data}\n    posts={postsQuery.data}\n    notifications={notificationsQuery.data}\n  />;\n}\n```\n\n## Infinite Queries\n\nFor pagination and infinite scroll:\n\n```typescript\nconst {\n  data,\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage\n} = useInfiniteQuery({\n  queryKey: ['posts'],\n  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),\n  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,\n  initialPageParam: 1\n});\n\n// Flatten pages\nconst allPosts = data?.pages.flatMap(page => page.posts) ?? [];\n\nreturn (\n  <div>\n    {allPosts.map(post => <PostCard key={post.id} post={post} />)}\n    {hasNextPage && (\n      <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>\n        {isFetchingNextPage ? 'Loading...' : 'Load More'}\n      </button>\n    )}\n  </div>\n);\n```\n\n## Prefetching\n\nPreload data before it's needed:\n\n```typescript\nimport { useQueryClient } from '@tanstack/react-query';\n\nfunction PostLink({ postId }: { postId: string }) {\n  const queryClient = useQueryClient();\n\n  const handleMouseEnter = () => {\n    queryClient.prefetchQuery({\n      queryKey: ['post', postId],\n      queryFn: () => fetchPost(postId)\n    });\n  };\n\n  return (\n    <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>\n      View Post\n    </Link>\n  );\n}\n```\n\n## Suspense Mode\n\nUse with React Suspense:\n\n```typescript\nimport { useSuspenseQuery } from '@tanstack/react-query';\n\nfunction PostDetails({ postId }: { postId: string }) {\n  // Throws promise on loading, error on error\n  const { data: post } = useSuspenseQuery({\n    queryKey: ['post', postId],\n    queryFn: () => fetchPost(postId)\n  });\n\n  return <div>{post.title}</div>;\n}\n\n// Wrap with Suspense\n<Suspense fallback={<Loading />}>\n  <PostDetails postId=\"123\" />\n</Suspense>\n```\n\n## Query Cancellation\n\nCancel queries when component unmounts:\n\n```typescript\nconst { data, isLoading } = useQuery({\n  queryKey: ['search', searchTerm],\n  queryFn: async ({ signal }) => {\n    const response = await fetch(`/api/search?q=${searchTerm}`, { signal });\n    return response.json();\n  }\n});\n```\n\n## Initial Data\n\nProvide initial data to avoid loading state:\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  initialData: () => {\n    // Get from cache or other source\n    return queryClient.getQueryData(['posts'])?.find((post) => post.id === postId);\n  }\n});\n```\n\n## Placeholder Data\n\nShow placeholder while loading:\n\n```typescript\nconst { data, isPlaceholderData } = useQuery({\n  queryKey: ['posts', page],\n  queryFn: () => fetchPosts(page),\n  placeholderData: (previousData) => previousData  // Keep previous page while loading\n});\n\n// Or provide static placeholder\nplaceholderData: { posts: [], total: 0 }\n```\n\n## Optimistic Updates with Queries\n\nUpdate UI immediately, rollback on error:\n\n```typescript\nconst queryClient = useQueryClient();\n\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (newPost) => {\n    // Cancel outgoing queries\n    await queryClient.cancelQueries({ queryKey: ['post', newPost.id] });\n\n    // Snapshot current value\n    const previousPost = queryClient.getQueryData(['post', newPost.id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['post', newPost.id], newPost);\n\n    return { previousPost };\n  },\n  onError: (err, newPost, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['post', newPost.id], context?.previousPost);\n  },\n  onSettled: (newPost) => {\n    // Refetch after success or error\n    queryClient.invalidateQueries({ queryKey: ['post', newPost.id] });\n  }\n});\n```\n\n## Query Retries\n\nConfigure retry behavior:\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  retry: 3, // Retry 3 times\n  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff\n});\n```\n\n## Error Handling\n\nHandle query errors:\n\n```typescript\nconst { data, error, isError } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  throwOnError: false  // Don't throw, just set error\n});\n\nif (isError) {\n  return <ErrorMessage error={error} />;\n}\n```\n\n## Best Practices\n\n1. **Use Suspense** - Better loading UX with React Suspense\n2. **Prefetch on Intent** - Preload data on hover/focus\n3. **Enable Queries Conditionally** - Use `enabled` option\n4. **Cancel on Unmount** - Use abort signals\n5. **Handle Errors Gracefully** - Show error states\n6. **Optimize with Placeholders** - Show previous data while loading\n",".agents/skills/tanstack-query/resources/mutation-patterns.md":"# Complex Mutation Patterns\n\n## Basic Mutations\n\n```typescript\nconst { mutate, isPending, isError, error } = useMutation({\n  mutationFn: (newPost: CreatePostDto) => createPost(newPost),\n  onSuccess: (data) => {\n    console.log('Post created:', data);\n  },\n  onError: (error) => {\n    console.error('Failed to create post:', error);\n  }\n});\n\n// Trigger mutation\nmutate({ title: 'New Post', content: '...' });\n```\n\n## Optimistic Updates\n\nUpdate UI immediately, rollback on error:\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (newPost) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['posts'] });\n\n    // Snapshot previous value\n    const previousPosts = queryClient.getQueryData(['posts']);\n\n    // Optimistically update to the new value\n    queryClient.setQueryData(['posts'], (old) =>\n      old.map((post) => (post.id === newPost.id ? newPost : post))\n    );\n\n    // Return context with snapshot\n    return { previousPosts };\n  },\n  onError: (err, newPost, context) => {\n    // Rollback to previous value\n    queryClient.setQueryData(['posts'], context.previousPosts);\n  },\n  onSettled: () => {\n    // Always refetch after error or success\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n  }\n});\n```\n\n## Sequential Mutations\n\nRun mutations in sequence:\n\n```typescript\nconst createAndPublish = async (postData) => {\n  // Create post\n  const post = await createPostMutation.mutateAsync(postData);\n\n  // Publish post\n  const published = await publishPostMutation.mutateAsync(post.id);\n\n  return published;\n};\n```\n\n## Parallel Mutations\n\nRun multiple mutations simultaneously:\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: async (updates) => {\n    const results = await Promise.all([\n      updateProfile(updates.profile),\n      updateSettings(updates.settings),\n      updatePreferences(updates.preferences)\n    ]);\n    return results;\n  }\n});\n```\n\n## Mutation with Invalidation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n\n    // Or update cache directly\n    queryClient.setQueryData(['posts'], (old) => [newPost, ...old]);\n  }\n});\n```\n\n## Mutation with Multiple Cache Updates\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: deletePost,\n  onSuccess: (_, deletedPostId) => {\n    // Update posts list\n    queryClient.setQueryData(['posts'], (old) => old.filter((post) => post.id !== deletedPostId));\n\n    // Update post count\n    queryClient.setQueryData(['postsCount'], (old) => old - 1);\n\n    // Invalidate related queries\n    queryClient.invalidateQueries({ queryKey: ['user', 'stats'] });\n  }\n});\n```\n\n## Error Handling\n\n```typescript\nconst { mutate, isError, error, reset } = useMutation({\n  mutationFn: createPost,\n  onError: (error) => {\n    if (error.code === 'VALIDATION_ERROR') {\n      setFormErrors(error.fields);\n    } else if (error.code === 'NETWORK_ERROR') {\n      showRetryDialog();\n    } else {\n      showGenericError();\n    }\n  }\n});\n\n// Clear error state\nreset();\n```\n\n## Retry Failed Mutations\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  retry: 3, // Retry 3 times on failure\n  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff\n});\n```\n\n## Mutation with Loading State\n\n```typescript\nfunction CreatePostForm() {\n  const { mutate, isPending } = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      navigate('/posts');\n    }\n  });\n\n  const handleSubmit = (data) => {\n    mutate(data);\n  };\n\n  return (\n    <form onSubmit={handleSubmit}>\n      {/* form fields */}\n      <button type=\"submit\" disabled={isPending}>\n        {isPending ? 'Creating...' : 'Create Post'}\n      </button>\n    </form>\n  );\n}\n```\n\n## Mutation with Variables\n\n```typescript\nconst { mutate, variables } = useMutation({\n  mutationFn: updatePost\n});\n\n// Access last mutation variables\nconsole.log('Last updated post:', variables);\n```\n\n## Mutation Callbacks\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onMutate: (variables) => {\n    console.log('Starting mutation with:', variables);\n  },\n  onSuccess: (data, variables, context) => {\n    console.log('Success!', data);\n  },\n  onError: (error, variables, context) => {\n    console.error('Error!', error);\n  },\n  onSettled: (data, error, variables, context) => {\n    console.log('Mutation finished (success or error)');\n  }\n});\n```\n\n## Mutation with Form Integration\n\n```typescript\nimport { useForm } from 'react-hook-form';\n\nfunction CreatePostForm() {\n  const { register, handleSubmit, reset } = useForm();\n\n  const { mutate, isPending, isError, error } = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      reset();  // Clear form\n      toast.success('Post created!');\n    },\n    onError: (error) => {\n      toast.error(error.message);\n    }\n  });\n\n  const onSubmit = (data) => {\n    mutate(data);\n  };\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <input {...register('title')} />\n      <textarea {...register('content')} />\n      <button type=\"submit\" disabled={isPending}>\n        Submit\n      </button>\n      {isError && <ErrorMessage error={error} />}\n    </form>\n  );\n}\n```\n\n## Mutation State Reset\n\n```typescript\nconst { mutate, data, error, reset } = useMutation({\n  mutationFn: createPost\n});\n\n// Clear mutation state\nconst handleReset = () => {\n  reset(); // Clears data, error, status, etc.\n};\n```\n\n## Global Mutation Configuration\n\n```typescript\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    mutations: {\n      retry: 3,\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n      onError: (error) => {\n        // Global error handler\n        console.error('Mutation error:', error);\n      }\n    }\n  }\n});\n```\n\n## Mutation Lifecycle\n\n```\n┌─────────────┐\n│ idle        │\n└──────┬──────┘\n       │ mutate()\n       ▼\n┌─────────────┐\n│ pending     │ ─── onMutate()\n└──────┬──────┘\n       │\n       ├─ success ──► onSuccess() ──┐\n       │                            │\n       └─ error ────► onError() ────┤\n                                    │\n                                    ▼\n                              onSettled()\n```\n\n## Best Practices\n\n1. **Use Optimistic Updates** - Better UX for fast operations\n2. **Always Handle Errors** - Show clear error messages\n3. **Invalidate Related Queries** - Keep cache in sync\n4. **Use onSettled** - For cleanup that runs regardless of success/error\n5. **Reset on Unmount** - Clear mutation state when component unmounts\n6. **Retry Network Errors** - Configure retry for transient failures\n7. **Show Loading States** - Disable buttons during mutation\n8. **Rollback on Error** - Revert optimistic updates if mutation fails\n\n## Common Patterns\n\n### Create with Redirect\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: (newPost) => {\n    navigate(`/posts/${newPost.id}`);\n  }\n});\n```\n\n### Update with Toast\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onSuccess: () => {\n    toast.success('Post updated!');\n  },\n  onError: () => {\n    toast.error('Failed to update post');\n  }\n});\n```\n\n### Delete with Confirmation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: deletePost,\n  onMutate: async () => {\n    const confirmed = await confirm('Are you sure?');\n    if (!confirmed) throw new Error('Cancelled');\n  },\n  onSuccess: () => {\n    toast.success('Post deleted');\n    navigate('/posts');\n  }\n});\n```\n",".agents/skills/tanstack-query/skill-rules-fragment.json":"{\n  \"tanstack-query\": {\n    \"type\": \"domain\",\n    \"enforcement\": \"suggest\",\n    \"priority\": \"high\",\n    \"promptTriggers\": {\n      \"keywords\": [\n        \"tanstack query\",\n        \"react query\",\n        \"@tanstack/react-query\",\n        \"useQuery\",\n        \"useMutation\",\n        \"useSuspenseQuery\",\n        \"useInfiniteQuery\",\n        \"useQueries\",\n        \"useQueryClient\",\n        \"useIsFetching\",\n        \"useIsMutating\",\n        \"useMutationState\",\n        \"QueryClient\",\n        \"QueryClientProvider\",\n        \"queryKey\",\n        \"queryFn\",\n        \"mutationFn\",\n        \"invalidateQueries\",\n        \"setQueryData\",\n        \"getQueryData\",\n        \"removeQueries\",\n        \"resetQueries\",\n        \"staleTime\",\n        \"cacheTime\",\n        \"refetchInterval\",\n        \"refetchOnWindowFocus\",\n        \"refetchOnReconnect\",\n        \"isLoading\",\n        \"isError\",\n        \"isFetching\",\n        \"isSuccess\",\n        \"queryCache\",\n        \"mutationCache\"\n      ],\n      \"intentPatterns\": [\n        \"use.*tanstack.*query\",\n        \"use.*react.*query\",\n        \"create.*tanstack.*(query|mutation)\",\n        \"invalidate.*query.*cache\",\n        \"invalidate.*tanstack.*queries\",\n        \"prefetch.*query\",\n        \"setup.*query.*client\",\n        \"configure.*tanstack.*query\",\n        \"use.*(useQuery|useMutation|useInfiniteQuery)\",\n        \"implement.*query.*invalidation\",\n        \"add.*query.*key\",\n        \"set.*query.*data\"\n      ]\n    },\n    \"fileTriggers\": {\n      \"pathPatterns\": [\n        \"**/api/**/*.ts\",\n        \"**/api/**/*.tsx\",\n        \"**/queries/**/*.ts\",\n        \"**/queries/**/*.tsx\",\n        \"**/hooks/**/*.ts\",\n        \"**/hooks/**/*.tsx\",\n        \"**/mutations/**/*.ts\",\n        \"**/mutations/**/*.tsx\"\n      ],\n      \"contentPatterns\": [\n        \"useQuery\\\\(\",\n        \"useMutation\\\\(\",\n        \"useSuspenseQuery\\\\(\",\n        \"useInfiniteQuery\\\\(\",\n        \"useQueryClient\\\\(\",\n        \"queryClient\\\\.\",\n        \"import.*@tanstack/react-query\",\n        \"from '@tanstack/react-query'\",\n        \"QueryClientProvider\",\n        \"new QueryClient\\\\(\",\n        \"invalidateQueries\\\\(\",\n        \"setQueryData\\\\(\",\n        \"queryKey:\",\n        \"queryFn:\"\n      ]\n    }\n  }\n}\n",".agents/skills/vercel-composition-patterns/AGENTS.md":"# React Composition Patterns\n\n**Version 1.0.0**  \nEngineering  \nJanuary 2026\n\n> **Note:**  \n> This document is mainly for agents and LLMs to follow when maintaining,  \n> generating, or refactoring React codebases using composition. Humans  \n> may also find it useful, but guidance here is optimized for automation  \n> and consistency by AI-assisted workflows.\n\n---\n\n## Abstract\n\nComposition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.\n\n---\n\n## Table of Contents\n\n1. [Component Architecture](#1-component-architecture) — **HIGH**\n   - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)\n   - 1.2 [Use Compound Components](#12-use-compound-components)\n2. [State Management](#2-state-management) — **MEDIUM**\n   - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)\n   - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)\n   - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)\n3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**\n   - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)\n   - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)\n4. [React 19 APIs](#4-react-19-apis) — **MEDIUM**\n   - 4.1 [React 19 API Changes](#41-react-19-api-changes)\n\n---\n\n## 1. Component Architecture\n\n**Impact: HIGH**\n\nFundamental patterns for structuring components to avoid prop\nproliferation and enable flexible composition.\n\n### 1.1 Avoid Boolean Prop Proliferation\n\n**Impact: CRITICAL (prevents unmaintainable component variants)**\n\nDon't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize\n\ncomponent behavior. Each boolean doubles possible states and creates\n\nunmaintainable conditional logic. Use composition instead.\n\n**Incorrect: boolean props create exponential complexity**\n\n```tsx\nfunction Composer({\n  onSubmit,\n  isThread,\n  channelId,\n  isDMThread,\n  dmId,\n  isEditing,\n  isForwarding\n}: Props) {\n  return (\n    <form>\n      <Header />\n      <Input />\n      {isDMThread ? (\n        <AlsoSendToDMField id={dmId} />\n      ) : isThread ? (\n        <AlsoSendToChannelField id={channelId} />\n      ) : null}\n      {isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}\n      <Footer onSubmit={onSubmit} />\n    </form>\n  );\n}\n```\n\n**Correct: composition eliminates conditionals**\n\n```tsx\n// Channel composer\nfunction ChannelComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Attachments />\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Thread composer - adds \"also send to channel\" field\nfunction ThreadComposer({ channelId }: { channelId: string }) {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <AlsoSendToChannelField id={channelId} />\n      <Composer.Footer>\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Edit composer - different footer actions\nfunction EditComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.CancelEdit />\n        <Composer.SaveEdit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n```\n\nEach variant is explicit about what it renders. We can share internals without\n\nsharing a single monolithic parent.\n\n### 1.2 Use Compound Components\n\n**Impact: HIGH (enables flexible composition without prop drilling)**\n\nStructure complex components as compound components with a shared context. Each\n\nsubcomponent accesses shared state via context, not props. Consumers compose the\n\npieces they need.\n\n**Incorrect: monolithic component with render props**\n\n```tsx\nfunction Composer({\n  renderHeader,\n  renderFooter,\n  renderActions,\n  showAttachments,\n  showFormatting,\n  showEmojis\n}: Props) {\n  return (\n    <form>\n      {renderHeader?.()}\n      <Input />\n      {showAttachments && <Attachments />}\n      {renderFooter ? (\n        renderFooter()\n      ) : (\n        <Footer>\n          {showFormatting && <Formatting />}\n          {showEmojis && <Emojis />}\n          {renderActions?.()}\n        </Footer>\n      )}\n    </form>\n  );\n}\n```\n\n**Correct: compound components with shared context**\n\n```tsx\nconst ComposerContext = createContext<ComposerContextValue | null>(null);\n\nfunction ComposerProvider({ children, state, actions, meta }: ProviderProps) {\n  return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;\n}\n\nfunction ComposerFrame({ children }: { children: React.ReactNode }) {\n  return <form>{children}</form>;\n}\n\nfunction ComposerInput() {\n  const {\n    state,\n    actions: { update },\n    meta: { inputRef }\n  } = use(ComposerContext);\n  return (\n    <TextInput\n      ref={inputRef}\n      value={state.input}\n      onChangeText={(text) => update((s) => ({ ...s, input: text }))}\n    />\n  );\n}\n\nfunction ComposerSubmit() {\n  const {\n    actions: { submit }\n  } = use(ComposerContext);\n  return <Button onPress={submit}>Send</Button>;\n}\n\n// Export as compound component\nconst Composer = {\n  Provider: ComposerProvider,\n  Frame: ComposerFrame,\n  Input: ComposerInput,\n  Submit: ComposerSubmit,\n  Header: ComposerHeader,\n  Footer: ComposerFooter,\n  Attachments: ComposerAttachments,\n  Formatting: ComposerFormatting,\n  Emojis: ComposerEmojis\n};\n```\n\n**Usage:**\n\n```tsx\n<Composer.Provider state={state} actions={actions} meta={meta}>\n  <Composer.Frame>\n    <Composer.Header />\n    <Composer.Input />\n    <Composer.Footer>\n      <Composer.Formatting />\n      <Composer.Submit />\n    </Composer.Footer>\n  </Composer.Frame>\n</Composer.Provider>\n```\n\nConsumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.\n\n---\n\n## 2. State Management\n\n**Impact: MEDIUM**\n\nPatterns for lifting state and managing shared context across\ncomposed components.\n\n### 2.1 Decouple State Management from UI\n\n**Impact: MEDIUM (enables swapping state implementations without changing UI)**\n\nThe provider component should be the only place that knows how state is managed.\n\nUI components consume the context interface—they don't know if state comes from\n\nuseState, Zustand, or a server sync.\n\n**Incorrect: UI coupled to state implementation**\n\n```tsx\nfunction ChannelComposer({ channelId }: { channelId: string }) {\n  // UI component knows about global state implementation\n  const state = useGlobalChannelState(channelId);\n  const { submit, updateInput } = useChannelSync(channelId);\n\n  return (\n    <Composer.Frame>\n      <Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />\n      <Composer.Submit onPress={() => sync.submit()} />\n    </Composer.Frame>\n  );\n}\n```\n\n**Correct: state management isolated in provider**\n\n```tsx\n// Provider handles all state management details\nfunction ChannelProvider({\n  channelId,\n  children\n}: {\n  channelId: string;\n  children: React.ReactNode;\n}) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n  const inputRef = useRef(null);\n\n  return (\n    <Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n\n// UI component only knows about the context interface\nfunction ChannelComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Usage\nfunction Channel({ channelId }: { channelId: string }) {\n  return (\n    <ChannelProvider channelId={channelId}>\n      <ChannelComposer />\n    </ChannelProvider>\n  );\n}\n```\n\n**Different providers, same UI:**\n\n```tsx\n// Local state for ephemeral forms\nfunction ForwardMessageProvider({ children }) {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n\n  return (\n    <Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n\n// Global synced state for channels\nfunction ChannelProvider({ channelId, children }) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n\n  return (\n    <Composer.Provider state={state} actions={{ update, submit }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n```\n\nThe same `Composer.Input` component works with both providers because it only\n\ndepends on the context interface, not the implementation.\n\n### 2.2 Define Generic Context Interfaces for Dependency Injection\n\n**Impact: HIGH (enables dependency-injectable state across use-cases)**\n\nDefine a **generic interface** for your component context with three parts:\n\n`state`, `actions`, and `meta`. This interface is a contract that any provider\n\ncan implement—enabling the same UI components to work with completely different\n\nstate implementations.\n\n**Core principle:** Lift state, compose internals, make state\n\ndependency-injectable.\n\n**Incorrect: UI coupled to specific state implementation**\n\n```tsx\nfunction ComposerInput() {\n  // Tightly coupled to a specific hook\n  const { input, setInput } = useChannelComposerState();\n  return <TextInput value={input} onChangeText={setInput} />;\n}\n```\n\n**Correct: generic interface enables dependency injection**\n\n```tsx\n// Define a GENERIC interface that any provider can implement\ninterface ComposerState {\n  input: string;\n  attachments: Attachment[];\n  isSubmitting: boolean;\n}\n\ninterface ComposerActions {\n  update: (updater: (state: ComposerState) => ComposerState) => void;\n  submit: () => void;\n}\n\ninterface ComposerMeta {\n  inputRef: React.RefObject<TextInput>;\n}\n\ninterface ComposerContextValue {\n  state: ComposerState;\n  actions: ComposerActions;\n  meta: ComposerMeta;\n}\n\nconst ComposerContext = createContext<ComposerContextValue | null>(null);\n```\n\n**UI components consume the interface, not the implementation:**\n\n```tsx\nfunction ComposerInput() {\n  const {\n    state,\n    actions: { update },\n    meta\n  } = use(ComposerContext);\n\n  // This component works with ANY provider that implements the interface\n  return (\n    <TextInput\n      ref={meta.inputRef}\n      value={state.input}\n      onChangeText={(text) => update((s) => ({ ...s, input: text }))}\n    />\n  );\n}\n```\n\n**Different providers implement the same interface:**\n\n```tsx\n// Provider A: Local state for ephemeral forms\nfunction ForwardMessageProvider({ children }: { children: React.ReactNode }) {\n  const [state, setState] = useState(initialState);\n  const inputRef = useRef(null);\n  const submit = useForwardMessage();\n\n  return (\n    <ComposerContext\n      value={{\n        state,\n        actions: { update: setState, submit },\n        meta: { inputRef }\n      }}\n    >\n      {children}\n    </ComposerContext>\n  );\n}\n\n// Provider B: Global synced state for channels\nfunction ChannelProvider({ channelId, children }: Props) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n  const inputRef = useRef(null);\n\n  return (\n    <ComposerContext\n      value={{\n        state,\n        actions: { update, submit },\n        meta: { inputRef }\n      }}\n    >\n      {children}\n    </ComposerContext>\n  );\n}\n```\n\n**The same composed UI works with both:**\n\n```tsx\n// Works with ForwardMessageProvider (local state)\n<ForwardMessageProvider>\n  <Composer.Frame>\n    <Composer.Input />\n    <Composer.Submit />\n  </Composer.Frame>\n</ForwardMessageProvider>\n\n// Works with ChannelProvider (global synced state)\n<ChannelProvider channelId=\"abc\">\n  <Composer.Frame>\n    <Composer.Input />\n    <Composer.Submit />\n  </Composer.Frame>\n</ChannelProvider>\n```\n\n**Custom UI outside the component can access state and actions:**\n\n```tsx\nfunction ForwardMessageDialog() {\n  return (\n    <ForwardMessageProvider>\n      <Dialog>\n        {/* The composer UI */}\n        <Composer.Frame>\n          <Composer.Input placeholder=\"Add a message, if you'd like.\" />\n          <Composer.Footer>\n            <Composer.Formatting />\n            <Composer.Emojis />\n          </Composer.Footer>\n        </Composer.Frame>\n\n        {/* Custom UI OUTSIDE the composer, but INSIDE the provider */}\n        <MessagePreview />\n\n        {/* Actions at the bottom of the dialog */}\n        <DialogActions>\n          <CancelButton />\n          <ForwardButton />\n        </DialogActions>\n      </Dialog>\n    </ForwardMessageProvider>\n  );\n}\n\n// This button lives OUTSIDE Composer.Frame but can still submit based on its context!\nfunction ForwardButton() {\n  const {\n    actions: { submit }\n  } = use(ComposerContext);\n  return <Button onPress={submit}>Forward</Button>;\n}\n\n// This preview lives OUTSIDE Composer.Frame but can read composer's state!\nfunction MessagePreview() {\n  const { state } = use(ComposerContext);\n  return <Preview message={state.input} attachments={state.attachments} />;\n}\n```\n\nThe provider boundary is what matters—not the visual nesting. Components that\n\nneed shared state don't have to be inside the `Composer.Frame`. They just need\n\nto be within the provider.\n\nThe `ForwardButton` and `MessagePreview` are not visually inside the composer\n\nbox, but they can still access its state and actions. This is the power of\n\nlifting state into providers.\n\nThe UI is reusable bits you compose together. The state is dependency-injected\n\nby the provider. Swap the provider, keep the UI.\n\n### 2.3 Lift State into Provider Components\n\n**Impact: HIGH (enables state sharing outside component boundaries)**\n\nMove state management into dedicated provider components. This allows sibling\n\ncomponents outside the main UI to access and modify state without prop drilling\n\nor awkward refs.\n\n**Incorrect: state trapped inside component**\n\n```tsx\nfunction ForwardMessageComposer() {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n\n  return (\n    <Composer.Frame>\n      <Composer.Input />\n      <Composer.Footer />\n    </Composer.Frame>\n  );\n}\n\n// Problem: How does this button access composer state?\nfunction ForwardMessageDialog() {\n  return (\n    <Dialog>\n      <ForwardMessageComposer />\n      <MessagePreview /> {/* Needs composer state */}\n      <DialogActions>\n        <CancelButton />\n        <ForwardButton /> {/* Needs to call submit */}\n      </DialogActions>\n    </Dialog>\n  );\n}\n```\n\n**Incorrect: useEffect to sync state up**\n\n```tsx\nfunction ForwardMessageDialog() {\n  const [input, setInput] = useState('');\n  return (\n    <Dialog>\n      <ForwardMessageComposer onInputChange={setInput} />\n      <MessagePreview input={input} />\n    </Dialog>\n  );\n}\n\nfunction ForwardMessageComposer({ onInputChange }) {\n  const [state, setState] = useState(initialState);\n  useEffect(() => {\n    onInputChange(state.input); // Sync on every change 😬\n  }, [state.input]);\n}\n```\n\n**Incorrect: reading state from ref on submit**\n\n```tsx\nfunction ForwardMessageDialog() {\n  const stateRef = useRef(null);\n  return (\n    <Dialog>\n      <ForwardMessageComposer stateRef={stateRef} />\n      <ForwardButton onPress={() => submit(stateRef.current)} />\n    </Dialog>\n  );\n}\n```\n\n**Correct: state lifted to provider**\n\n```tsx\nfunction ForwardMessageProvider({ children }: { children: React.ReactNode }) {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n  const inputRef = useRef(null);\n\n  return (\n    <Composer.Provider\n      state={state}\n      actions={{ update: setState, submit: forwardMessage }}\n      meta={{ inputRef }}\n    >\n      {children}\n    </Composer.Provider>\n  );\n}\n\nfunction ForwardMessageDialog() {\n  return (\n    <ForwardMessageProvider>\n      <Dialog>\n        <ForwardMessageComposer />\n        <MessagePreview /> {/* Custom components can access state and actions */}\n        <DialogActions>\n          <CancelButton />\n          <ForwardButton /> {/* Custom components can access state and actions */}\n        </DialogActions>\n      </Dialog>\n    </ForwardMessageProvider>\n  );\n}\n\nfunction ForwardButton() {\n  const { actions } = use(Composer.Context);\n  return <Button onPress={actions.submit}>Forward</Button>;\n}\n```\n\nThe ForwardButton lives outside the Composer.Frame but still has access to the\n\nsubmit action because it's within the provider. Even though it's a one-off\n\ncomponent, it can still access the composer's state and actions from outside the\n\nUI itself.\n\n**Key insight:** Components that need shared state don't have to be visually\n\nnested inside each other—they just need to be within the same provider.\n\n---\n\n## 3. Implementation Patterns\n\n**Impact: MEDIUM**\n\nSpecific techniques for implementing compound components and\ncontext providers.\n\n### 3.1 Create Explicit Component Variants\n\n**Impact: MEDIUM (self-documenting code, no hidden conditionals)**\n\nInstead of one component with many boolean props, create explicit variant\n\ncomponents. Each variant composes the pieces it needs. The code documents\n\nitself.\n\n**Incorrect: one component, many modes**\n\n```tsx\n// What does this component actually render?\n<Composer isThread isEditing={false} channelId='abc' showAttachments showFormatting={false} />\n```\n\n**Correct: explicit variants**\n\n```tsx\n// Immediately clear what this renders\n<ThreadComposer channelId=\"abc\" />\n\n// Or\n<EditMessageComposer messageId=\"xyz\" />\n\n// Or\n<ForwardMessageComposer messageId=\"123\" />\n```\n\nEach implementation is unique, explicit and self-contained. Yet they can each\n\nuse shared parts.\n\n**Implementation:**\n\n```tsx\nfunction ThreadComposer({ channelId }: { channelId: string }) {\n  return (\n    <ThreadProvider channelId={channelId}>\n      <Composer.Frame>\n        <Composer.Input />\n        <AlsoSendToChannelField channelId={channelId} />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.Submit />\n        </Composer.Footer>\n      </Composer.Frame>\n    </ThreadProvider>\n  );\n}\n\nfunction EditMessageComposer({ messageId }: { messageId: string }) {\n  return (\n    <EditMessageProvider messageId={messageId}>\n      <Composer.Frame>\n        <Composer.Input />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.CancelEdit />\n          <Composer.SaveEdit />\n        </Composer.Footer>\n      </Composer.Frame>\n    </EditMessageProvider>\n  );\n}\n\nfunction ForwardMessageComposer({ messageId }: { messageId: string }) {\n  return (\n    <ForwardMessageProvider messageId={messageId}>\n      <Composer.Frame>\n        <Composer.Input placeholder=\"Add a message, if you'd like.\" />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.Mentions />\n        </Composer.Footer>\n      </Composer.Frame>\n    </ForwardMessageProvider>\n  );\n}\n```\n\nEach variant is explicit about:\n\n- What provider/state it uses\n\n- What UI elements it includes\n\n- What actions are available\n\nNo boolean prop combinations to reason about. No impossible states.\n\n### 3.2 Prefer Composing Children Over Render Props\n\n**Impact: MEDIUM (cleaner composition, better readability)**\n\nUse `children` for composition instead of `renderX` props. Children are more\n\nreadable, compose naturally, and don't require understanding callback\n\nsignatures.\n\n**Incorrect: render props**\n\n```tsx\nfunction Composer({\n  renderHeader,\n  renderFooter,\n  renderActions\n}: {\n  renderHeader?: () => React.ReactNode;\n  renderFooter?: () => React.ReactNode;\n  renderActions?: () => React.ReactNode;\n}) {\n  return (\n    <form>\n      {renderHeader?.()}\n      <Input />\n      {renderFooter ? renderFooter() : <DefaultFooter />}\n      {renderActions?.()}\n    </form>\n  );\n}\n\n// Usage is awkward and inflexible\nreturn (\n  <Composer\n    renderHeader={() => <CustomHeader />}\n    renderFooter={() => (\n      <>\n        <Formatting />\n        <Emojis />\n      </>\n    )}\n    renderActions={() => <SubmitButton />}\n  />\n);\n```\n\n**Correct: compound components with children**\n\n```tsx\nfunction ComposerFrame({ children }: { children: React.ReactNode }) {\n  return <form>{children}</form>;\n}\n\nfunction ComposerFooter({ children }: { children: React.ReactNode }) {\n  return <footer className='flex'>{children}</footer>;\n}\n\n// Usage is flexible\nreturn (\n  <Composer.Frame>\n    <CustomHeader />\n    <Composer.Input />\n    <Composer.Footer>\n      <Composer.Formatting />\n      <Composer.Emojis />\n      <SubmitButton />\n    </Composer.Footer>\n  </Composer.Frame>\n);\n```\n\n**When render props are appropriate:**\n\n```tsx\n// Render props work well when you need to pass data back\n<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />\n```\n\nUse render props when the parent needs to provide data or state to the child.\n\nUse children when composing static structure.\n\n---\n\n## 4. React 19 APIs\n\n**Impact: MEDIUM**\n\nReact 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.\n\n### 4.1 React 19 API Changes\n\n**Impact: MEDIUM (cleaner component definitions and context usage)**\n\n> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.\n\nIn React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.\n\n**Incorrect: forwardRef in React 19**\n\n```tsx\nconst ComposerInput = forwardRef<TextInput, Props>((props, ref) => {\n  return <TextInput ref={ref} {...props} />;\n});\n```\n\n**Correct: ref as a regular prop**\n\n```tsx\nfunction ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {\n  return <TextInput ref={ref} {...props} />;\n}\n```\n\n**Incorrect: useContext in React 19**\n\n```tsx\nconst value = useContext(MyContext);\n```\n\n**Correct: use instead of useContext**\n\n```tsx\nconst value = use(MyContext);\n```\n\n`use()` can also be called conditionally, unlike `useContext()`.\n\n---\n\n## References\n\n1. [https://react.dev](https://react.dev)\n2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)\n3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)\n",".agents/skills/vercel-composition-patterns/README.md":"# React Composition Patterns\n\nA structured repository for React composition patterns that scale. These\npatterns help avoid boolean prop proliferation by using compound components,\nlifting state, and composing internals.\n\n## Structure\n\n- `rules/` - Individual rule files (one per rule)\n  - `_sections.md` - Section metadata (titles, impacts, descriptions)\n  - `_template.md` - Template for creating new rules\n  - `area-description.md` - Individual rule files\n- `metadata.json` - Document metadata (version, organization, abstract)\n- **`AGENTS.md`** - Compiled output (generated)\n\n## Rules\n\n### Component Architecture (CRITICAL)\n\n- `architecture-avoid-boolean-props.md` - Don't add boolean props to customize\n  behavior\n- `architecture-compound-components.md` - Structure as compound components with\n  shared context\n\n### State Management (HIGH)\n\n- `state-lift-state.md` - Lift state into provider components\n- `state-context-interface.md` - Define clear context interfaces\n  (state/actions/meta)\n- `state-decouple-implementation.md` - Decouple state management from UI\n\n### Implementation Patterns (MEDIUM)\n\n- `patterns-children-over-render-props.md` - Prefer children over renderX props\n- `patterns-explicit-variants.md` - Create explicit component variants\n\n## Core Principles\n\n1. **Composition over configuration** — Instead of adding props, let consumers\n   compose\n2. **Lift your state** — State in providers, not trapped in components\n3. **Compose your internals** — Subcomponents access context, not props\n4. **Explicit variants** — Create ThreadComposer, EditComposer, not Composer\n   with isThread\n\n## Creating a New Rule\n\n1. Copy `rules/_template.md` to `rules/area-description.md`\n2. Choose the appropriate area prefix:\n   - `architecture-` for Component Architecture\n   - `state-` for State Management\n   - `patterns-` for Implementation Patterns\n3. Fill in the frontmatter and content\n4. Ensure you have clear examples with explanations\n\n## Impact Levels\n\n- `CRITICAL` - Foundational patterns, prevents unmaintainable code\n- `HIGH` - Significant maintainability improvements\n- `MEDIUM` - Good practices for cleaner code\n",".agents/skills/vercel-composition-patterns/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2FSKILL.md -->",".agents/skills/vercel-composition-patterns/rules/_sections.md":"# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parentheses) is the filename prefix used to group rules.\n\n---\n\n## 1. Component Architecture (architecture)\n\n**Impact:** HIGH  \n**Description:** Fundamental patterns for structuring components to avoid prop\nproliferation and enable flexible composition.\n\n## 2. State Management (state)\n\n**Impact:** MEDIUM  \n**Description:** Patterns for lifting state and managing shared context across\ncomposed components.\n\n## 3. Implementation Patterns (patterns)\n\n**Impact:** MEDIUM  \n**Description:** Specific techniques for implementing compound components and\ncontext providers.\n\n## 4. React 19 APIs (react19)\n\n**Impact:** MEDIUM  \n**Description:** React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.\n",".agents/skills/vercel-composition-patterns/rules/_template.md":"---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: brief description of impact\ntags: composition, components\n---\n\n## Rule Title Here\n\nBrief explanation of the rule and why it matters.\n\n**Incorrect:**\n\n```tsx\n// Bad code example\n```\n\n**Correct:**\n\n```tsx\n// Good code example\n```\n\nReference: [Link](https://example.com)\n",".agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-avoid-boolean-props.md -->",".agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-compound-components.md -->",".agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-children-over-render-props.md -->",".agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-explicit-variants.md -->",".agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Freact19-no-forwardref.md -->",".agents/skills/vercel-composition-patterns/rules/state-context-interface.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-context-interface.md -->",".agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-decouple-implementation.md -->",".agents/skills/vercel-composition-patterns/rules/state-lift-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-lift-state.md -->",".agents/skills/vercel-react-best-practices/AGENTS.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FAGENTS.md -->",".agents/skills/vercel-react-best-practices/README.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FREADME.md -->",".agents/skills/vercel-react-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FSKILL.md -->",".agents/skills/vercel-react-best-practices/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2F_sections.md -->",".agents/skills/vercel-react-best-practices/rules/_template.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2F_template.md -->",".agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-event-handler-refs.md -->",".agents/skills/vercel-react-best-practices/rules/advanced-init-once.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-init-once.md -->",".agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-use-latest.md -->",".agents/skills/vercel-react-best-practices/rules/async-api-routes.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-api-routes.md -->",".agents/skills/vercel-react-best-practices/rules/async-defer-await.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-defer-await.md -->",".agents/skills/vercel-react-best-practices/rules/async-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-dependencies.md -->",".agents/skills/vercel-react-best-practices/rules/async-parallel.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-parallel.md -->",".agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-suspense-boundaries.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-barrel-imports.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-conditional.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-conditional.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-defer-third-party.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-dynamic-imports.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-preload.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-preload.md -->",".agents/skills/vercel-react-best-practices/rules/client-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-event-listeners.md -->",".agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-localstorage-schema.md -->",".agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-passive-event-listeners.md -->",".agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-swr-dedup.md -->",".agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-batch-dom-css.md -->",".agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-function-results.md -->",".agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-property-access.md -->",".agents/skills/vercel-react-best-practices/rules/js-cache-storage.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-storage.md -->",".agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-combine-iterations.md -->",".agents/skills/vercel-react-best-practices/rules/js-early-exit.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-early-exit.md -->",".agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-flatmap-filter.md -->",".agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-hoist-regexp.md -->",".agents/skills/vercel-react-best-practices/rules/js-index-maps.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-index-maps.md -->",".agents/skills/vercel-react-best-practices/rules/js-length-check-first.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-length-check-first.md -->",".agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-min-max-loop.md -->",".agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-set-map-lookups.md -->",".agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-tosorted-immutable.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-activity.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-activity.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-animate-svg-wrapper.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-conditional-render.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-content-visibility.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hoist-jsx.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-no-flicker.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-suppress-warning.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-resource-hints.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-script-defer-async.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-svg-precision.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-usetransition-loading.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-defer-reads.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-dependencies.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state-no-effect.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-functional-setstate.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-lazy-state-init.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo-with-default-value.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-move-effect-to-event.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-no-inline-components.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-split-combined-hooks.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-transitions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-transitions.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-deferred-value.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-ref-transient-values.md -->",".agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-after-nonblocking.md -->",".agents/skills/vercel-react-best-practices/rules/server-auth-actions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-auth-actions.md -->",".agents/skills/vercel-react-best-practices/rules/server-cache-lru.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-lru.md -->",".agents/skills/vercel-react-best-practices/rules/server-cache-react.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-react.md -->",".agents/skills/vercel-react-best-practices/rules/server-dedup-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-dedup-props.md -->",".agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-hoist-static-io.md -->",".agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-parallel-fetching.md -->",".agents/skills/vercel-react-best-practices/rules/server-serialization.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-serialization.md -->",".agents/skills/web-design-guidelines/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->",".claude/skills/kiranism-shadcn-dashboard/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fkiranism-shadcn-dashboard%2FSKILL.md -->",".claude/skills/kiranism-shadcn-dashboard/references/theming-guide.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fkiranism-shadcn-dashboard%2Freferences%2Ftheming-guide.md -->",".claude/skills/next-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2FSKILL.md -->",".claude/skills/next-best-practices/async-patterns.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fasync-patterns.md -->",".claude/skills/next-best-practices/bundling.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fbundling.md -->",".claude/skills/next-best-practices/data-patterns.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdata-patterns.md -->",".claude/skills/next-best-practices/debug-tricks.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdebug-tricks.md -->",".claude/skills/next-best-practices/directives.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdirectives.md -->",".claude/skills/next-best-practices/error-handling.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ferror-handling.md -->",".claude/skills/next-best-practices/file-conventions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffile-conventions.md -->",".claude/skills/next-best-practices/font.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffont.md -->",".claude/skills/next-best-practices/functions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffunctions.md -->",".claude/skills/next-best-practices/hydration-error.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fhydration-error.md -->",".claude/skills/next-best-practices/image.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fimage.md -->",".claude/skills/next-best-practices/metadata.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fmetadata.md -->",".claude/skills/next-best-practices/parallel-routes.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fparallel-routes.md -->",".claude/skills/next-best-practices/route-handlers.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Froute-handlers.md -->",".claude/skills/next-best-practices/rsc-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Frsc-boundaries.md -->",".claude/skills/next-best-practices/runtime-selection.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fruntime-selection.md -->",".claude/skills/next-best-practices/scripts.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fscripts.md -->",".claude/skills/next-best-practices/self-hosting.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fself-hosting.md -->",".claude/skills/next-best-practices/suspense-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fsuspense-boundaries.md -->",".claude/skills/vercel-composition-patterns/AGENTS.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FAGENTS.md -->",".claude/skills/vercel-composition-patterns/README.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FREADME.md -->",".claude/skills/vercel-composition-patterns/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FSKILL.md -->",".claude/skills/vercel-composition-patterns/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2F_sections.md -->",".claude/skills/vercel-composition-patterns/rules/_template.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2F_template.md -->",".claude/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-avoid-boolean-props.md -->",".claude/skills/vercel-composition-patterns/rules/architecture-compound-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-compound-components.md -->",".claude/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-children-over-render-props.md -->",".claude/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-explicit-variants.md -->",".claude/skills/vercel-composition-patterns/rules/react19-no-forwardref.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Freact19-no-forwardref.md -->",".claude/skills/vercel-composition-patterns/rules/state-context-interface.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-context-interface.md -->",".claude/skills/vercel-composition-patterns/rules/state-decouple-implementation.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-decouple-implementation.md -->",".claude/skills/vercel-composition-patterns/rules/state-lift-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-lift-state.md -->",".claude/skills/vercel-react-best-practices/AGENTS.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FAGENTS.md -->",".claude/skills/vercel-react-best-practices/README.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FREADME.md -->",".claude/skills/vercel-react-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FSKILL.md -->",".claude/skills/vercel-react-best-practices/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2F_sections.md -->",".claude/skills/vercel-react-best-practices/rules/_template.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2F_template.md -->",".claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-event-handler-refs.md -->",".claude/skills/vercel-react-best-practices/rules/advanced-init-once.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-init-once.md -->",".claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-use-latest.md -->",".claude/skills/vercel-react-best-practices/rules/async-api-routes.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-api-routes.md -->",".claude/skills/vercel-react-best-practices/rules/async-defer-await.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-defer-await.md -->",".claude/skills/vercel-react-best-practices/rules/async-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-dependencies.md -->",".claude/skills/vercel-react-best-practices/rules/async-parallel.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-parallel.md -->",".claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-suspense-boundaries.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-barrel-imports.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-conditional.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-conditional.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-defer-third-party.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-dynamic-imports.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-preload.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-preload.md -->",".claude/skills/vercel-react-best-practices/rules/client-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-event-listeners.md -->",".claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-localstorage-schema.md -->",".claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-passive-event-listeners.md -->",".claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-swr-dedup.md -->",".claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-batch-dom-css.md -->",".claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-function-results.md -->",".claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-property-access.md -->",".claude/skills/vercel-react-best-practices/rules/js-cache-storage.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-storage.md -->",".claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-combine-iterations.md -->",".claude/skills/vercel-react-best-practices/rules/js-early-exit.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-early-exit.md -->",".claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-flatmap-filter.md -->",".claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-hoist-regexp.md -->",".claude/skills/vercel-react-best-practices/rules/js-index-maps.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-index-maps.md -->",".claude/skills/vercel-react-best-practices/rules/js-length-check-first.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-length-check-first.md -->",".claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-min-max-loop.md -->",".claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-set-map-lookups.md -->",".claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-tosorted-immutable.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-activity.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-activity.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-animate-svg-wrapper.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-conditional-render.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-content-visibility.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hoist-jsx.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-no-flicker.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-suppress-warning.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-resource-hints.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-script-defer-async.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-svg-precision.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-usetransition-loading.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-defer-reads.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-dependencies.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state-no-effect.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-functional-setstate.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-lazy-state-init.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo-with-default-value.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-move-effect-to-event.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-no-inline-components.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-split-combined-hooks.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-transitions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-transitions.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-deferred-value.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-ref-transient-values.md -->",".claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-after-nonblocking.md -->",".claude/skills/vercel-react-best-practices/rules/server-auth-actions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-auth-actions.md -->",".claude/skills/vercel-react-best-practices/rules/server-cache-lru.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-lru.md -->",".claude/skills/vercel-react-best-practices/rules/server-cache-react.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-react.md -->",".claude/skills/vercel-react-best-practices/rules/server-dedup-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-dedup-props.md -->",".claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-hoist-static-io.md -->",".claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-parallel-fetching.md -->",".claude/skills/vercel-react-best-practices/rules/server-serialization.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-serialization.md -->",".claude/skills/web-design-guidelines/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","AGENTS.md":"# AGENTS.md - AI Coding Agent Reference\n\nThis file provides essential information for AI coding agents working on this project. It contains project-specific details, conventions, and guidelines that complement the README.\n\n---\n\n## Project Overview\n\n**Next.js Admin Dashboard Starter** is a production-ready admin dashboard template built with:\n\n- **Framework**: Next.js 16 (App Router)\n- **Language**: TypeScript 7\n- **Styling**: Tailwind CSS v4\n- **UI Components**: shadcn/ui (base-nova style, Base UI primitives)\n- **Authentication**: Clerk (with Organizations/Billing support)\n- **Error Tracking**: Sentry\n- **Charts**: Recharts\n- **Containerization**: Docker (Node.js & Bun Dockerfiles)\n- **Package Manager**: Bun (preferred) or npm\n\nThe project follows a feature-based folder structure designed for scalability in SaaS applications, internal tools, and admin panels.\n\n---\n\n## Technology Stack Details\n\n### Core Framework & Runtime\n\n- Next.js 16.4 with App Router\n- React 19.3\n- TypeScript 7 (native compiler) with strict mode enabled\n\n### Styling & UI\n\n- Tailwind CSS v4 (using `@import 'tailwindcss'` syntax)\n- PostCSS with `@tailwindcss/postcss` plugin\n- shadcn/ui component library (Base UI primitives)\n- CSS custom properties for theming (OKLCH color format)\n\n### State Management\n\n- Zustand 5.x for local UI state in the stateful demo features\n- Nuqs for URL search params state management\n- TanStack Form + Zod for form handling (`createFormHook` + shadcn `Field`-anatomy components)\n\n### Data Fetching & Caching\n\n- TanStack React Query for data fetching, caching, and mutations\n- Server-side prefetching with `HydrationBoundary` + `dehydrate`\n- Client-side `useQuery` + nuqs `shallow: true` for tables (no RSC round-trips on pagination/filter)\n- `useMutation` + `invalidateQueries` for form submissions\n- Query client singleton in `src/lib/query-client.ts`\n\n### Authentication & Authorization\n\n- Clerk for authentication and user management\n- Clerk Organizations for multi-tenant workspaces\n- Clerk Billing for subscription management (B2B)\n- Client-side RBAC for navigation visibility\n\n### Data & APIs\n\n- TanStack Table for data tables\n- TanStack React Query for data fetching and mutations\n- Recharts for analytics/charts\n- Service layer per feature (`api/types.ts` → `api/service.ts` → `api/queries.ts`)\n- Route handlers at `src/app/api/` (for Route Handler or BFF patterns)\n- Mock data in `src/constants/mock-api*.ts` (default, swap via service layer)\n- API client utility in `src/lib/api-client.ts` (for fetch-based patterns)\n\n### Development Tools\n\n- ESLint 8.x with Next.js core-web-vitals config\n- Prettier 3.x with prettier-plugin-tailwindcss\n- Husky for git hooks\n- lint-staged for pre-commit formatting\n\n---\n\n## Project Structure\n\n```\n/src\n├── app/                    # Next.js App Router\n│   ├── auth/              # Authentication routes (sign-in, sign-up)\n│   ├── dashboard/         # Dashboard routes\n│   │   ├── overview/      # Parallel routes (@area_stats, @bar_stats, etc.)\n│   │   ├── product/       # Product management pages\n│   │   ├── kanban/        # Kanban board page\n│   │   ├── chat/          # Messaging page\n│   │   ├── ai-chat/       # AI chat streaming demo\n│   │   ├── notifications/ # Notifications page\n│   │   ├── workspaces/    # Organization management\n│   │   ├── billing/       # Subscription billing\n│   │   ├── exclusive/     # Pro plan feature example\n│   │   └── profile/       # User profile\n│   ├── api/               # API routes (if any)\n│   ├── layout.tsx         # Root layout with providers\n│   ├── page.tsx           # Landing page\n│   ├── global-error.tsx   # Global error boundary\n│   └── not-found.tsx      # 404 page\n│\n├── components/\n│   ├── ui/                # shadcn/ui components (50+ components)\n│   ├── layout/            # Layout components (sidebar, header, etc.)\n│   ├── forms/             # Field components (shadcn TanStack Form anatomy) + demos\n│   ├── themes/            # Theme system components\n│   ├── kbar/              # Command+K search bar\n│   ├── icons.tsx          # Icon registry\n│   └── ...\n│\n├── features/              # Feature-based modules\n│   ├── auth/              # Authentication components\n│   ├── overview/          # Dashboard analytics\n│   ├── products/          # Product management (React Query + nuqs)\n│   │   ├── api/\n│   │   │   ├── types.ts   # Type contract (response shapes, filters, payloads)\n│   │   │   ├── service.ts # Data access layer (swap for your backend)\n│   │   │   └── queries.ts # React Query options + key factories\n│   │   ├── components/    # Listing, form, table components\n│   │   ├── schemas/       # Zod schemas\n│   │   └── constants/     # Filter options\n│   ├── users/             # User management (React Query + nuqs)\n│   │   ├── api/           # Same pattern: types.ts → service.ts → queries.ts\n│   │   └── components/    # Listing, table components\n│   ├── react-query-demo/  # React Query showcase (Pokemon API)\n│   ├── kanban/            # Kanban board with dnd-kit\n│   ├── chat/              # Messaging UI (conversations, bubbles, composer)\n│   ├── ai-chat/           # Scripted useChat streaming demo\n│   ├── notifications/     # Notification center & store\n│   └── profile/           # Profile management\n│\n├── config/                # Configuration files\n│   ├── nav-config.ts      # Navigation with RBAC\n│   └── ...\n│\n├── hooks/                 # Custom React hooks\n│   ├── use-nav.ts         # RBAC navigation filtering\n│   ├── use-data-table.ts  # Data table state\n│   └── ...\n│\n├── lib/                   # Utility functions\n│   ├── utils.ts           # cn() and formatters\n│   ├── searchparams.ts    # Search param utilities\n│   └── ...\n│\n├── types/                 # TypeScript type definitions\n│   └── index.ts           # Core types (NavItem, etc.)\n│\n└── styles/                # Global styles\n    ├── globals.css        # Tailwind imports + view transitions\n    ├── theme.css          # Theme imports\n    └── themes/            # Individual theme files\n\n/docs                      # Documentation\n│   ├── clerk_setup.md     # Clerk configuration guide\n│   ├── nav-rbac.md        # Navigation RBAC documentation\n│   └── themes.md          # Theme customization guide\n\n/scripts                   # Dev tooling\n    ├── cleanup.js         # Feature removal, run via `bun run cleanup` (templates in cleanup-templates/, typechecked)\n    └── cleanup-templates/ # Replacement files cleanup.js copies into the repo\n\nDockerfile                 # Node.js production Dockerfile\nDockerfile.bun             # Bun production Dockerfile\n.dockerignore              # Docker build exclusions\n```\n\n---\n\n## Build & Development Commands\n\n```bash\n# Install dependencies\nbun install\n\n# Development server\nbun run dev          # Starts at http://localhost:3000\n\n# Build for production\nbun run build\n\n# Start production server\nbun run start\n\n# Linting\nbun run lint         # Run ESLint\nbun run lint:fix     # Fix ESLint issues and format\nbun run lint:strict  # Zero warnings tolerance\n\nbun run typecheck    # tsc --noEmit\n\n# Formatting\nbun run format       # Format with Prettier\nbun run format:check # Check formatting\n\n# Git hooks\nbun run prepare      # Install Husky hooks\n```\n\n---\n\n## Environment Configuration\n\nCopy `env.example.txt` to `.env.local` and configure:\n\n### Required for Authentication (Clerk)\n\n```env\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...\nCLERK_SECRET_KEY=sk_...\n\n# Redirect URLs\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=\"/auth/sign-in\"\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=\"/auth/sign-up\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=\"/dashboard/overview\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=\"/dashboard/overview\"\n```\n\n### Optional for Error Tracking (Sentry)\n\n```env\nNEXT_PUBLIC_SENTRY_DSN=https://...@....ingest.sentry.io/...\nNEXT_PUBLIC_SENTRY_ORG=your-org\nNEXT_PUBLIC_SENTRY_PROJECT=your-project\nSENTRY_AUTH_TOKEN=sntrys_...\nNEXT_PUBLIC_SENTRY_DISABLED=\"false\"  # Set to \"true\" to disable in dev\n```\n\n**Note**: Clerk supports \"keyless mode\" — run `npx clerk@latest init` to provision a development instance in seconds (no account needed; keys are written to `.env.local`).\n\n---\n\n## Code Style Guidelines\n\n### TypeScript\n\n- Strict mode enabled\n- Use explicit return types for public functions\n- Prefer interface over type for object definitions\n- Use `@/*` alias for imports from src\n\n### Formatting (Prettier)\n\n```json\n{\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"semi\": true,\n  \"trailingComma\": \"none\",\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\"\n}\n```\n\n### ESLint Rules\n\n- `@typescript-eslint/no-unused-vars`: warn\n- `no-console`: warn\n- `react-hooks/exhaustive-deps`: warn\n- `import/no-unresolved`: off (handled by TypeScript)\n\n### Component Conventions\n\n- Use function declarations for components: `function ComponentName() {}`\n- Props interface named `{ComponentName}Props`\n- shadcn/ui components use `cn()` utility for class merging\n- Server components by default, `'use client'` only when needed\n\n---\n\n## Theming System\n\nThe project uses a sophisticated multi-theme system with 10 built-in themes:\n\n- `vercel` (default)\n- `claude`\n- `discord`\n- `supabase`\n- `mono`\n- `notebook`\n- `light-green`\n- `zen`\n- `astro-vista`\n- `whatsapp`\n\n### Theme Files\n\n- CSS files: `src/styles/themes/{theme-name}.css`\n- Theme registry: `src/components/themes/theme.config.ts`\n- Font config: `src/components/themes/font.config.ts`\n- Active theme provider: `src/components/themes/active-theme.tsx`\n\n### Adding a New Theme\n\n1. Create `src/styles/themes/your-theme.css` with `[data-theme='your-theme']` selector\n2. Import in `src/styles/theme.css`\n3. Add to `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add fonts in `font.config.ts`\n5. (Optional) Set as default in `theme.config.ts`\n\nSee `docs/themes.md` for detailed theming guide.\n\n---\n\n## Navigation & RBAC System\n\n### Navigation Configuration\n\nNavigation is organized into groups in `src/config/nav-config.ts`:\n\n```typescript\nimport { NavGroup } from '@/types';\n\nexport const navGroups: NavGroup[] = [\n  {\n    label: 'Overview',\n    items: [\n      {\n        title: 'Dashboard',\n        url: '/dashboard/overview',\n        icon: 'dashboard',\n        shortcut: ['d', 'd'],\n        items: [],\n        access: { requireOrg: true } // RBAC check\n      }\n    ]\n  }\n];\n```\n\n### Access Control Properties\n\n- `requireOrg: boolean` - Requires active organization\n- `permission: string` - Requires specific permission\n- `role: string` - Requires specific role\n- `plan: string` - Requires specific subscription plan\n- `feature: string` - Requires specific feature\n\n### Client-Side Filtering\n\nThe `useFilteredNavItems()` hook in `src/hooks/use-nav.ts` filters navigation client-side using Clerk's `useOrganization()` and `useUser()` hooks. This is for UX only - actual security checks must happen server-side.\n\n---\n\n## Authentication Patterns\n\n### Protected Routes\n\nRoute protection is resource-based (Clerk's recommended pattern): `src/proxy.ts` runs a bare `clerkMiddleware()` to attach the auth context, and every dashboard `layout`/`page` and API route handler performs its own check.\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nexport default async function Page() {\n  await auth.protect(); // redirects signed-out users to sign-in\n  // ...\n}\n```\n\nAPI route handlers return 401 instead of redirecting:\n\n```tsx\nconst { userId } = await auth();\nif (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n```\n\nPages that require an organization:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\nimport { redirect } from 'next/navigation';\n\nexport default async function Page() {\n  await auth.protect();\n  const { orgId } = await auth();\n  if (!orgId) redirect('/dashboard/workspaces');\n  // ...\n}\n```\n\n### Plan/Feature Protection\n\nUse Clerk's `<Show>` component for client-side:\n\n```tsx\nimport { Show } from '@clerk/nextjs';\n\n<Show when={{ plan: 'pro' }} fallback={<UpgradePrompt />}>\n  <PremiumContent />\n</Show>;\n```\n\nUse `has()` function for server-side checks:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nconst { has } = await auth();\nconst hasFeature = has({ feature: 'premium_access' });\n```\n\n---\n\n## Data Fetching Patterns\n\n### Service Layer Architecture\n\nEach feature has a three-file API layer:\n\n```\nsrc/features/<name>/api/\n  types.ts      ← Type contract (response shapes, filters, payloads)\n  service.ts    ← Data access functions (the ONE file to swap for your backend)\n  queries.ts    ← React Query options + query key factories (stable, never changes)\n```\n\n**`service.ts` is the only file you modify when connecting to a real backend.** Queries and components import from it — they never change.\n\n#### Backend Patterns\n\n| Pattern                                            | How to implement                                                                            |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |\n| **Server Actions + ORM** (Prisma/Drizzle/Supabase) | Add `'use server'` at top of `service.ts`, call ORM directly                                |\n| **Route Handlers + ORM**                           | `service.ts` calls `/api/` routes via `apiClient`, route handlers call ORM                  |\n| **BFF** (Next.js proxies to Laravel/Go/etc.)       | `service.ts` calls `/api/` routes via `apiClient`, route handlers proxy to external backend |\n| **Direct external API** (frontend-only)            | `service.ts` calls external URL via `fetch()`                                               |\n| **Mock** (default)                                 | `service.ts` calls in-memory fake data stores                                               |\n\nRoute handlers at `src/app/api/` are ready for patterns 2 and 3. `src/lib/api-client.ts` provides a typed `fetch` wrapper.\n\n### Query Key Factories\n\nEach feature defines a key factory in `queries.ts` for type-safe, hierarchical cache invalidation:\n\n```tsx\nexport const entityKeys = {\n  all: ['entities'] as const,\n  list: (filters: EntityFilters) => [...entityKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...entityKeys.all, 'detail', id] as const\n};\n\n// Usage in queryOptions\nqueryKey: entityKeys.list(filters);\n\n// Usage in mutations — invalidate all entity queries\nqueryClient.invalidateQueries({ queryKey: entityKeys.all });\n```\n\n### React Query (Default for all new pages)\n\nThe project uses TanStack React Query with server-side prefetching and client-side cache management:\n\n1. **Query options** defined in `queries.ts` — shared between server prefetch and client hooks\n2. **Server prefetch** using `void queryClient.prefetchQuery()` + `HydrationBoundary` + `dehydrate` — `void` (fire-and-forget) is the standard TanStack pattern for Next.js App Router\n3. **Client fetch** using `useSuspenseQuery()` — integrates with React Suspense so prefetched data streams in without showing a loading skeleton on first load\n4. **Suspense boundary** wraps the client component — shows a fallback skeleton only on subsequent client-side navigations when cache is empty\n\n```tsx\n// Server component: prefetch + dehydrate\nconst queryClient = getQueryClient();\nvoid queryClient.prefetchQuery(entitiesQueryOptions(filters)); // void, not await\n\nreturn (\n  <HydrationBoundary state={dehydrate(queryClient)}>\n    <Suspense fallback={<Skeleton />}>\n      <EntityTable />\n    </Suspense>\n  </HydrationBoundary>\n);\n\n// Client component: useSuspenseQuery (not useQuery)\nconst { data } = useSuspenseQuery(entitiesQueryOptions(filters));\n```\n\n**Why `void` + `useSuspenseQuery`:**\n\n- `void` fires the prefetch without blocking the server component\n- `useSuspenseQuery` integrates with React Suspense — the pending query streams in via Next.js streaming SSR\n- With `<Suspense fallback={<Skeleton />}>`: skeleton shows immediately while data streams in — this is expected behavior, the skeleton IS the Suspense fallback during streaming\n- Without `<Suspense>` wrapper: no skeleton, but the previous page stays visible until data fully resolves (feels like a slow navigation)\n- Once data is cached (within `staleTime`), subsequent visits are instant — no skeleton\n\n**Why NOT `useQuery`:**\n\n- `useQuery` doesn't integrate with Suspense — returns `isLoading: true` and you must handle loading state manually\n- Hydrated pending queries from `void` prefetch won't prevent the loading state\n- Results in skeleton flash even when data is prefetched\n\n### Mutations\n\nComponents import service functions for mutations. Use query key factories for invalidation:\n\n```tsx\nimport { createEntity } from '../api/service';\nimport { entityKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createEntity(data),\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: entityKeys.all });\n    toast.success('Created');\n  }\n});\n```\n\n### URL State Management\n\nUse `nuqs` for search params state:\n\n- `searchParamsCache` (server) — reads params in server components\n- `useQueryState` (client) — reads/writes params in client components with `shallow: true`\n\n### Data Tables\n\nTables use TanStack Table v9 (DiceUI data table) with React Query:\n\n- Query options in `features/*/api/queries.ts`\n- Column definitions in `features/*/components/*-tables/columns.tsx`, typed `ColumnDef<DataTableFeatures, T>[]`\n- Table component in `src/components/data-table/data-table.tsx`\n- Feature set + `DataTableFeatures` type in `src/lib/data-table-features.ts`; URL parsers in `src/lib/data-table-parsers.ts`\n- Column pinning via `initialState.columnPinning` (`{ start: [], end: ['actions'] }`) in `useDataTable`\n\n---\n\n## Error Handling & Monitoring\n\n### Sentry Integration\n\nSentry is configured for both client and server:\n\n- Client config: `src/instrumentation-client.ts`\n- Server config: `src/instrumentation.ts`\n- Global error: `src/app/global-error.tsx`\n\nTo disable Sentry in development:\n\n```env\nNEXT_PUBLIC_SENTRY_DISABLED=\"true\"\n```\n\n### Error Boundaries\n\n- `global-error.tsx` - Catches all errors, reports to Sentry\n- Parallel route `error.tsx` files for specific sections\n\n---\n\n## Testing Strategy\n\n**Note**: This project does not include a test suite by default. Consider adding:\n\n- **Unit tests**: Vitest or Jest for utilities and hooks\n- **Component tests**: React Testing Library for UI components\n- **E2E tests**: Playwright for critical user flows\n\nRecommended test locations:\n\n```\n/src\n  /__tests__           # Unit tests\n  /features/*/tests    # Feature tests\n/e2e                   # Playwright tests\n```\n\n---\n\n## Deployment\n\nCanonical guide: [docs/deployment.md](./docs/deployment.md) (Vercel, production environment variables, Docker).\n\n### Vercel (Recommended)\n\n1. Connect repository to Vercel\n2. Add environment variables in dashboard\n3. Deploy\n\n### Environment Variables for Production\n\nEnsure these are set in your deployment platform:\n\n- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`\n- `CLERK_SECRET_KEY`\n- All `NEXT_PUBLIC_*` variables for client-side access\n- `SENTRY_*` variables if using error tracking\n\n### Docker\n\nProduction-ready Dockerfiles are included:\n\n- `Dockerfile` — Node.js-based\n- `Dockerfile.bun` — Bun-based\n\nBoth use `output: 'standalone'` in `next.config.ts`. Pass `NEXT_PUBLIC_*` vars as `--build-arg` at build time, and runtime secrets via `-e` at run time.\n\n### Build Considerations\n\n- Output: `standalone` (optimized for Docker/self-hosting)\n- Images: Configured for `api.slingacademy.com`, `img.clerk.com`, `clerk.com`\n- Sentry source maps uploaded automatically in CI\n\n---\n\n## Feature Cleanup System\n\nA single `scripts/cleanup.js` file handles removal of optional features:\n\n```bash\n# Interactive mode — prompts for each feature\nnode scripts/cleanup.js --interactive\n\n# Remove specific features\nnode scripts/cleanup.js clerk           # Remove auth/org/billing\nnode scripts/cleanup.js kanban          # Remove kanban board\nnode scripts/cleanup.js chat            # Remove messaging UI\nnode scripts/cleanup.js ai-chat         # Remove AI chat demo\nnode scripts/cleanup.js notifications   # Remove notification center\nnode scripts/cleanup.js themes          # Keep one theme, remove rest\nnode scripts/cleanup.js sentry          # Remove error tracking\n\n# Remove multiple at once\nnode scripts/cleanup.js kanban chat notifications\n\n# Preview without changing files\nnode scripts/cleanup.js --dry-run kanban\n\n# List all features\nnode scripts/cleanup.js --list\n```\n\n**Safety**: Script requires git repository with at least one commit. Use `--force` to skip.\n\nReplacement files live in `scripts/cleanup-templates/` as real `.ts`/`.tsx` files typechecked by `tsc` and `next build`, so template rot fails loudly instead of shipping broken code.\n\nAfter cleanup, delete `scripts/cleanup.js` and `scripts/cleanup-templates/` — the dev server message auto-cleans on next start.\n\n---\n\n## Icon System\n\n**All icons come from a single source: `src/components/icons.tsx`.**\n\nThe project uses `@tabler/icons-react` as the sole icon package. Every icon is re-exported through a centralized `Icons` object — **never import directly from `@tabler/icons-react` or any other icon package**.\n\n### Usage\n\n```tsx\nimport { Icons } from '@/components/icons';\n\n// In JSX\n<Icons.search className='h-4 w-4' />\n<Icons.chevronRight className='h-4 w-4' />\n\n// Passing as a prop\nicon={Icons.check}\n```\n\n### Adding a New Icon\n\n1. Import the tabler icon in `src/components/icons.tsx`\n2. Add a semantic key to the `Icons` object\n3. Use `Icons.yourKey` everywhere — never the raw import\n\n```tsx\n// In src/components/icons.tsx\nimport { IconNewIcon } from '@tabler/icons-react';\n\nexport const Icons = {\n  // ...existing icons\n  newIcon: IconNewIcon\n};\n```\n\n### Available Icon Categories\n\n| Category        | Example Keys                                                                  |\n| --------------- | ----------------------------------------------------------------------------- |\n| General         | `check`, `close`, `search`, `settings`, `trash`, `spinner`, `info`, `warning` |\n| Navigation      | `chevronDown`, `chevronLeft`, `chevronRight`, `chevronUp`, `chevronsUpDown`   |\n| Layout          | `dashboard`, `kanban`, `panelLeft`                                            |\n| User            | `user`, `account`, `profile`, `teams`                                         |\n| Communication   | `chat`, `notification`, `phone`, `video`, `send`                              |\n| Files           | `page`, `post`, `media`, `fileTypePdf`, `fileTypeDoc`                         |\n| Actions         | `add`, `edit`, `upload`, `share`, `login`, `logout`                           |\n| Theme           | `sun`, `moon`, `brightness`, `laptop`, `palette`                              |\n| Text formatting | `bold`, `italic`, `underline`, `text`                                         |\n| Data / Charts   | `trendingUp`, `trendingDown`, `eyeOff`, `adjustments`                         |\n\n### Icon Showcase Page\n\nBrowse all available icons at `/dashboard/elements/icons` — a searchable grid of every icon in the registry.\n\n### Why This Pattern?\n\n- **Single source of truth** — swap icon packages by editing one file\n- **Semantic naming** — `Icons.trash` is clearer than `IconTrash` scattered across files\n- **Discoverability** — autocomplete on `Icons.` shows every available icon\n- **No direct dependencies** — components never couple to a specific icon package\n\n---\n\n## Common Development Tasks\n\n### Adding a New Feature (End-to-End)\n\n1. Create `src/features/<name>/api/types.ts` — response types, filter types, mutation payloads\n2. Create `src/features/<name>/api/service.ts` — data access functions (mock by default)\n3. Create `src/features/<name>/api/queries.ts` — query key factory + `queryOptions`\n4. Create page route: `src/app/dashboard/<name>/page.tsx`\n5. Create feature components in `src/features/<name>/components/`\n6. Add navigation item in `src/config/nav-config.ts`\n7. (Optional) Add route handlers in `src/app/api/<name>/` for REST API patterns\n8. (Optional) Register new icon in `src/components/icons.tsx`\n\n### Adding a New API Route\n\n1. Create: `src/app/api/my-route/route.ts`\n2. Export HTTP method handlers: `GET`, `POST`, etc.\n3. For BFF pattern: proxy requests to your external backend\n\n### Adding a shadcn Component\n\n```bash\nnpx shadcn add component-name\n```\n\n### Adding a New Theme\n\nSee \"Theming System\" section above or `docs/themes.md`.\n\n---\n\n## Troubleshooting\n\n### Common Issues\n\n**Build fails with Tailwind errors**\n\n- Ensure using Tailwind CSS v4 syntax (`@import 'tailwindcss'`)\n- Check `postcss.config.js` uses `@tailwindcss/postcss`\n\n**Clerk keyless mode popup**\n\n- Run `npx clerk@latest init` to provision a dev instance in seconds (no account needed)\n- It writes keys to `.env.local`; later you can claim application or set env variables\n\n**Theme not applying**\n\n- Check theme name matches in CSS `[data-theme]` and `theme.config.ts`\n- Verify theme CSS is imported in `theme.css`\n\n**Navigation items not showing**\n\n- Check `access` property in nav config\n- Verify user has required org/permission/role\n\n---\n\n## External Documentation\n\n- [Next.js App Router](https://nextjs.org/docs/app)\n- [Clerk Next.js SDK](https://clerk.com/docs/references/nextjs)\n- [shadcn/ui](https://ui.shadcn.com/docs)\n- [Tailwind CSS v4](https://tailwindcss.com/docs)\n- [TanStack Table](https://tanstack.com/table/latest)\n- [Sentry Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/)\n\n---\n\n## Notes for AI Agents\n\n1. **Always use `cn()` for className merging** - never concatenate strings manually\n2. **Respect the feature-based structure** - put new feature code in `src/features/`\n3. **Server components by default** - only add `'use client'` when using browser APIs or React hooks\n4. **Type safety first** - avoid `any`, prefer explicit types\n5. **Follow existing patterns** - look at similar components before creating new ones\n6. **Environment variables** - prefix with `NEXT_PUBLIC_` for client-side access\n7. **shadcn components** - don't modify files in `src/components/ui/` directly; extend them instead\n8. **Icons** - NEVER import icons directly from `@tabler/icons-react` or any other icon package. All icons must be registered in `src/components/icons.tsx` and imported as `import { Icons } from '@/components/icons'`. To add a new icon: add the tabler import to `icons.tsx`, add a semantic key to the `Icons` object, then use `Icons.keyName` in your component.\n9. **Page headers** - Always use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`) for page headers. Never import `<Heading>` manually in pages — `PageContainer` handles that internally.\n10. **Forms** - Use `useAppForm` from `@/lib/form` with `form.AppField` rendering the shared field components (`field.TextField`, `field.SelectField`, …) from `@/components/forms/fields`. Each component follows the official shadcn TanStack Form anatomy; drop down to raw `form.Field` render props for one-off custom fields. Never use `useState` inside a render prop — extract stateful controls into components.\n11. **Button loading** - Use `<Button isLoading={isPending}>` for loading states. Uses CSS Grid overlap trick for zero layout shift. When `isLoading` is not passed, button behaves as default shadcn. `SubmitButton` in forms handles this automatically via form `isSubmitting` state.\n12. **Data layer** - Always go through the service layer: `types.ts` → `service.ts` → `queries.ts`. Components import types from `types.ts`, functions from `service.ts`, query options from `queries.ts`. Never import from `@/constants/mock-api*` directly in components.\n","CLAUDE.md":"# CLAUDE.md\n\nThis is a Next.js 16 + shadcn/ui admin dashboard starter kit.\n\n## Key References\n\n- **[AGENTS.md](./AGENTS.md)** — Full project overview, tech stack, structure, conventions, data fetching patterns, deployment\n- **[docs/forms.md](./docs/forms.md)** — Form system: TanStack Form + Zod, composable fields, validation, multi-step, sheet/dialog forms\n- **[docs/themes.md](./docs/themes.md)** — Theme system: OKLCH colors, adding themes, font config\n- **[docs/nav-rbac.md](./docs/nav-rbac.md)** — Navigation RBAC: access control, Clerk integration\n- **[docs/clerk_setup.md](./docs/clerk_setup.md)** — Clerk auth setup: organizations, billing, environment variables\n- **[docs/deployment.md](./docs/deployment.md)** — Deployment: Vercel, production environment variables, Docker\n\n## Critical Conventions\n\n- **React Query** for all data fetching — `void prefetchQuery()` on server + `useSuspenseQuery` on client (standard TanStack pattern), `useMutation` for forms, `HydrationBoundary` + `dehydrate` for hydration, `<Suspense fallback>` for streaming\n- **API layer** per feature — `api/types.ts` → `api/service.ts` → `api/queries.ts`; queries use key factories (`entityKeys.all/list/detail`); components import from service and queries, never from mock APIs directly\n- **nuqs** for URL search params — `searchParamsCache` on server, `useQueryStates` on client, use `getSortingStateParser` for sort (same parser as `useDataTable`)\n- **Icons** — only import from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` from `@/lib/form` (TanStack `createFormHook`) + `form.AppField` rendering the field components in `@/components/forms/fields` (`field.TextField`, `field.SelectField`, …); each component is the shadcn TanStack Form doc anatomy; raw `form.Field` for one-off custom fields; form-level Zod `onSubmit` validators\n- **Page headers** — use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`), never import `<Heading>` manually\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n"},"files":{".agents/skills/kiranism-shadcn-dashboard/SKILL.md":"---\nname: kiranism-shadcn-dashboard\ndescription: |\n  Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention \"dashboard\" explicitly — if they're adding UI, pages, or features to this project, use this skill.\n---\n\n# Dashboard Development Guide\n\nThis skill encodes the exact patterns and conventions used in this Next.js 16 + shadcn/ui admin dashboard template.\n\n## Quick Reference: What Goes Where\n\n| Task                  | Location                                |\n| --------------------- | --------------------------------------- |\n| New page              | `src/app/dashboard/<name>/page.tsx`     |\n| New feature module    | `src/features/<name>/`                  |\n| Feature components    | `src/features/<name>/components/`       |\n| API types             | `src/features/<name>/api/types.ts`      |\n| Service layer         | `src/features/<name>/api/service.ts`    |\n| Query options         | `src/features/<name>/api/queries.ts`    |\n| Mutation options      | `src/features/<name>/api/mutations.ts`  |\n| Zod schemas           | `src/features/<name>/schemas/<name>.ts` |\n| Filter/select options | `src/features/<name>/constants/`        |\n| Nav config            | `src/config/nav-config.ts`              |\n| Types                 | `src/types/index.ts`                    |\n| Mock data             | `src/constants/mock-api-<name>.ts`      |\n| Search params         | `src/lib/searchparams.ts`               |\n| Query client          | `src/lib/query-client.ts`               |\n| Theme CSS             | `src/styles/themes/<name>.css`          |\n| Theme registry        | `src/components/themes/theme.config.ts` |\n| Custom hook           | `src/hooks/`                            |\n| Icons registry        | `src/components/icons.tsx`              |\n\n---\n\n## Adding a New Feature (End-to-End)\n\nWhen a user asks to add a feature (e.g., \"add an orders page\"), follow these steps in order. Each step below shows the minimal pattern — see reference files for full templates.\n\n### Step 1: Mock API (`src/constants/mock-api-<name>.ts`)\n\nSee [references/mock-api-guide.md](references/mock-api-guide.md) for the complete template. Key structure:\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\nexport type Order = {\n  id: number;\n  customer: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\nexport const fakeOrders = {\n  records: [] as Order[],\n  initialize() {\n    /* generate with faker */\n  },\n  async getOrders({ page, limit, search, sort }) {\n    /* filter, sort, paginate, return { items, total_items } */\n  },\n  async getOrderById(id: number) {\n    /* find by id */\n  },\n  async createOrder(data) {\n    /* push to records */\n  },\n  async updateOrder(id, data) {\n    /* merge into record */\n  },\n  async deleteOrder(id) {\n    /* filter out */\n  }\n};\nfakeOrders.initialize();\n```\n\nEvery method should call `await delay(800)` to simulate network latency. Use `matchSorter` for search. Return `{ items, total_items }` from list methods.\n\n### Step 2: API Layer (`src/features/<name>/api/`)\n\nEach feature has 4 API files: **types** → **service** → **queries** → **mutations**.\n\n**Types** (`api/types.ts`) — re-export the entity type from mock API, plus filter/response/payload types:\n\n```tsx\nexport type { Order } from '@/constants/mock-api-orders';\nexport type OrderFilters = { page?: number; limit?: number; search?: string; sort?: string };\nexport type OrdersResponse = { items: Order[]; total_items: number };\nexport type OrderMutationPayload = { customer: string; status: string; total: number };\n```\n\n**Service** (`api/service.ts`) — data access layer. One exported function per operation:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse, OrderMutationPayload } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function getOrderById(id: number) {\n  return fakeOrders.getOrderById(id);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\nexport async function updateOrder(id: number, data: OrderMutationPayload) {\n  return fakeOrders.updateOrder(id, data);\n}\nexport async function deleteOrder(id: number) {\n  return fakeOrders.deleteOrder(id);\n}\n```\n\n**Queries** (`api/queries.ts`) — query key factory + query options:\n\n```tsx\nimport { queryOptions } from '@tanstack/react-query';\nimport { getOrders, getOrderById } from './service';\nimport type { Order, OrderFilters } from './types';\n\nexport type { Order };\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({\n    queryKey: orderKeys.list(filters),\n    queryFn: () => getOrders(filters)\n  });\n\nexport const orderByIdOptions = (id: number) =>\n  queryOptions({\n    queryKey: orderKeys.detail(id),\n    queryFn: () => getOrderById(id)\n  });\n```\n\n**Mutations** (`api/mutations.ts`) — use `mutationOptions` + `getQueryClient()` (not custom hooks with `useQueryClient()`):\n\n```tsx\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { createOrder, updateOrder, deleteOrder } from './service';\nimport { orderKeys } from './queries';\nimport type { OrderMutationPayload } from './types';\n\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data: OrderMutationPayload) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const updateOrderMutation = mutationOptions({\n  mutationFn: ({ id, values }: { id: number; values: OrderMutationPayload }) =>\n    updateOrder(id, values),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const deleteOrderMutation = mutationOptions({\n  mutationFn: (id: number) => deleteOrder(id),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n```\n\n`mutationOptions` is the right abstraction because it works outside React (event handlers, tests, utilities), composes via spread at the call site, and uses `getQueryClient()` which handles both SSR (fresh per request) and client (singleton) correctly. See [references/query-abstractions.md](references/query-abstractions.md) for the full rationale.\n\n### Step 3: Zod Schema (`src/features/<name>/schemas/<name>.ts`)\n\n```tsx\nimport { z } from 'zod';\n\nexport const orderSchema = z.object({\n  customer: z.string().min(2, 'Customer name must be at least 2 characters'),\n  status: z.string().min(1, 'Please select a status'),\n  total: z.number({ message: 'Total is required' })\n});\n\nexport type OrderFormValues = z.infer<typeof orderSchema>;\n```\n\n### Step 4: Feature Components\n\nCreate `src/features/<name>/components/` with:\n\n**Listing page** (server component — `<name>-listing.tsx`):\n\n```tsx\nimport { HydrationBoundary, dehydrate } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport { ordersQueryOptions } from '../api/queries';\nimport { OrderTable, OrderTableSkeleton } from './orders-table';\nimport { Suspense } from 'react';\n\nexport default function OrderListingPage() {\n  const page = searchParamsCache.get('page');\n  const search = searchParamsCache.get('name');\n  const pageLimit = searchParamsCache.get('perPage');\n  const sort = searchParamsCache.get('sort');\n\n  const filters = {\n    page,\n    limit: pageLimit,\n    ...(search && { search }),\n    ...(sort.length > 0 && { sort: JSON.stringify(sort) })\n  };\n\n  const queryClient = getQueryClient();\n  void queryClient.prefetchQuery(ordersQueryOptions(filters));\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      <Suspense fallback={<OrderTableSkeleton />}>\n        <OrderTable />\n      </Suspense>\n    </HydrationBoundary>\n  );\n}\n```\n\n**Table + skeleton** (client component — `orders-table/index.tsx`):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';\nimport { getSortingStateParser } from '@/lib/data-table-parsers';\nimport { useDataTable } from '@/hooks/use-data-table';\nimport { DataTable } from '@/components/data-table/data-table';\nimport { DataTableToolbar } from '@/components/data-table/data-table-toolbar';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { ordersQueryOptions } from '../../api/queries';\nimport { columns } from './columns';\n\nexport function OrderTable() {\n  const [params] = useQueryStates({\n    page: parseAsInteger.withDefault(1),\n    perPage: parseAsInteger.withDefault(10),\n    name: parseAsString,\n    sort: getSortingStateParser().withDefault([]) // same parser as searchParamsCache\n  });\n\n  const filters = {\n    page: params.page,\n    limit: params.perPage,\n    ...(params.name && { search: params.name }),\n    ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })\n  };\n\n  const { data } = useSuspenseQuery(ordersQueryOptions(filters));\n\n  const { table } = useDataTable({\n    data: data.items,\n    columns,\n    pageCount: Math.ceil(data.total_items / params.perPage),\n    shallow: true,\n    debounceMs: 500,\n    initialState: { columnPinning: { start: [], end: ['actions'] } }\n  });\n\n  return (\n    <DataTable table={table}>\n      <DataTableToolbar table={table} />\n    </DataTable>\n  );\n}\n\nexport function OrderTableSkeleton() {\n  return (\n    <div className='space-y-4 p-4'>\n      <Skeleton className='h-10 w-full' />\n      <Skeleton className='h-96 w-full' />\n    </div>\n  );\n}\n```\n\n**Column definitions** (`orders-table/columns.tsx`):\n\nEach column needs `id`, `accessorKey` (or `accessorFn`), `header` with `DataTableColumnHeader`, and optionally `meta` for filtering + `enableColumnFilter: true`. Type columns as `ColumnDef<DataTableFeatures, Order>[]` (TanStack Table v9; `DataTableFeatures` from `@/lib/data-table-features` types `meta`).\n\n```tsx\nexport const columns: ColumnDef<DataTableFeatures, Order>[] = [\n  {\n    id: 'customer',\n    accessorKey: 'customer',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Customer' />,\n    meta: { label: 'Customer', placeholder: 'Search...', variant: 'text', icon: Icons.text },\n    enableColumnFilter: true\n  },\n  {\n    id: 'status',\n    accessorKey: 'status',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Status' />,\n    cell: ({ cell }) => (\n      <Badge variant='outline' className='capitalize'>\n        {cell.getValue<string>()}\n      </Badge>\n    ),\n    enableColumnFilter: true,\n    meta: { label: 'Status', variant: 'multiSelect', options: STATUS_OPTIONS }\n  },\n  { id: 'actions', cell: ({ row }) => <CellAction data={row.original} /> }\n];\n```\n\nFilter `meta.variant` options: `text`, `number`, `range`, `date`, `dateRange`, `select`, `multiSelect`, `boolean`. For multiSelect, provide `options: { value, label, icon? }[]`.\n\n**Cell actions** (`orders-table/cell-action.tsx`):\n\nPattern: `DropdownMenu` with edit/delete items + `AlertModal` for delete confirmation + `useMutation` for the delete API call.\n\n```tsx\nimport { deleteOrderMutation } from '../../api/mutations';\n\nexport const CellAction: React.FC<{ data: Order }> = ({ data }) => {\n  const [deleteOpen, setDeleteOpen] = useState(false);\n  const deleteMutation = useMutation({\n    ...deleteOrderMutation,\n    onSuccess: () => {\n      toast.success('Deleted');\n      setDeleteOpen(false);\n    }\n  });\n  return (\n    <>\n      <AlertModal\n        isOpen={deleteOpen}\n        onClose={() => setDeleteOpen(false)}\n        onConfirm={() => deleteMutation.mutate(data.id)}\n        loading={deleteMutation.isPending}\n      />\n      <DropdownMenu modal={false}>\n        <DropdownMenuTrigger asChild>\n          <Button variant='ghost' className='h-8 w-8 p-0'>\n            <Icons.ellipsis className='h-4 w-4' />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align='end'>\n          <DropdownMenuLabel>Actions</DropdownMenuLabel>\n          <DropdownMenuItem onClick={() => router.push(`/dashboard/orders/${data.id}`)}>\n            <Icons.edit className='mr-2 h-4 w-4' /> Edit\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setDeleteOpen(true)}>\n            <Icons.trash className='mr-2 h-4 w-4' /> Delete\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </>\n  );\n};\n```\n\nFor **sheet-based editing** (like Users), replace `router.push` with opening a `<FormSheet>` — see the Forms section below.\n\n### Step 5: Page Route (`src/app/dashboard/<name>/page.tsx`)\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderListingPage from '@/features/orders/components/order-listing';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport type { SearchParams } from 'nuqs/server';\n\nexport const metadata = { title: 'Dashboard: Orders' };\ntype PageProps = { searchParams: Promise<SearchParams> };\n\nexport default async function Page(props: PageProps) {\n  const searchParams = await props.searchParams;\n  searchParamsCache.parse(searchParams);\n\n  return (\n    <PageContainer\n      scrollable={false}\n      pageTitle='Orders'\n      pageDescription='Manage your orders.'\n      pageHeaderAction={/* Add button — Link or SheetTrigger */}\n    >\n      <OrderListingPage />\n    </PageContainer>\n  );\n}\n```\n\n**PageContainer props**: `scrollable`, `pageTitle`, `pageDescription`, `pageHeaderAction` (React node for the top-right button), `infoContent` (help sidebar), `access` + `accessFallback` (RBAC gating).\n\n**Detail/Edit page** (`src/app/dashboard/<name>/[id]/page.tsx`):\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderViewPage from '@/features/orders/components/order-view-page';\n\nexport const metadata = { title: 'Dashboard: Order Details' };\ntype PageProps = { params: Promise<{ id: string }> };\n\nexport default async function Page(props: PageProps) {\n  const { id } = await props.params;\n  return (\n    <PageContainer scrollable pageTitle='Order Details'>\n      <OrderViewPage orderId={id} />\n    </PageContainer>\n  );\n}\n```\n\n**View page component** (client — handles new vs edit):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { notFound } from 'next/navigation';\nimport { orderByIdOptions } from '../api/queries';\nimport OrderForm from './order-form';\n\nexport default function OrderViewPage({ orderId }: { orderId: string }) {\n  if (orderId === 'new') return <OrderForm initialData={null} pageTitle='Create Order' />;\n  const { data } = useSuspenseQuery(orderByIdOptions(Number(orderId)));\n  if (!data) notFound();\n  return <OrderForm initialData={data} pageTitle='Edit Order' />;\n}\n```\n\n### Step 6: Search Params (`src/lib/searchparams.ts`)\n\nAdd any new filter keys. Existing params: `page`, `perPage`, `name`, `gender`, `category`, `role`, `sort`.\n\n### Step 7: Navigation (`src/config/nav-config.ts`)\n\n```tsx\n{ title: 'Orders', url: '/dashboard/orders', icon: 'product', items: [] }\n```\n\n### Step 8: Icons (`src/components/icons.tsx`)\n\nTo register a new icon, import from `@tabler/icons-react` and add to the `Icons` object:\n\n```tsx\nimport { IconShoppingCart } from '@tabler/icons-react';\nexport const Icons = { /* ...existing */ cart: IconShoppingCart };\n```\n\nNever import `@tabler/icons-react` anywhere else. Always use `Icons.keyName`.\n\n**Existing icon keys** (partial): `dashboard`, `product`, `kanban`, `chat`, `forms`, `user`, `teams`, `billing`, `settings`, `add`, `edit`, `trash`, `search`, `check`, `close`, `clock`, `ellipsis`, `text`, `calendar`, `upload`, `spinner`, `chevronDown/Left/Right/Up`, `sun`, `moon`, `palette`, `pro`, `workspace`, `notification`.\n\n---\n\n## Forms\n\nForms use **TanStack Form + Zod** with `useAppForm` + `useFormFields<T>()` and `useMutation` for submission. See [references/forms-guide.md](references/forms-guide.md) for all field types, validation strategies, multi-step forms, and advanced patterns.\n\n### Page Form (Create/Edit on a dedicated route)\n\nThe full pattern is shown in Steps 1-4 above. The key structure:\n\n1. **Schema** — Zod schema + inferred type in `schemas/<name>.ts`\n2. **Form component** — `useAppForm({ defaultValues, validators: { onSubmit: schema }, onSubmit })` + `useFormFields<T>()` for typed fields\n3. **Mutations** — `useMutation({ ...createOrderMutation, onSuccess: () => { toast(); router.push() } })`, spread shared mutation options from `api/mutations.ts` and layer on UI callbacks\n4. **View page** — client component that checks `id === 'new'` for create vs `useSuspenseQuery(byIdOptions)` for edit\n\n### Sheet Form (Inline create/edit in a side panel)\n\nFor features where a separate page is overkill (like Users). The sheet manages open state; the form uses a `form` attribute to connect to the sheet footer's submit button.\n\n```tsx\n'use client';\nimport { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';\n\nexport function OrderFormSheet({\n  order,\n  open,\n  onOpenChange\n}: {\n  order?: Order;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}) {\n  const isEdit = !!order;\n  const mutation = useMutation({\n    ...(isEdit ? updateOrderMutation : createOrderMutation),\n    onSuccess: () => {\n      onOpenChange(false);\n    }\n  });\n  const form = useAppForm({\n    defaultValues: { customer: order?.customer ?? '' /* ... */ } as OrderFormValues,\n    validators: { onSubmit: orderSchema },\n    onSubmit: async ({ value }) => {\n      await mutation.mutateAsync(value);\n    }\n  });\n  const { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n  return (\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className='flex flex-col'>\n        <SheetHeader>\n          <SheetTitle>{isEdit ? 'Edit' : 'New'} Order</SheetTitle>\n        </SheetHeader>\n        <div className='flex-1 overflow-auto'>\n          <form.AppForm>\n            <form.Form id='order-sheet-form' className='space-y-4'>\n              <FormTextField name='customer' label='Customer' required />\n              <FormSelectField name='status' label='Status' required options={STATUS_OPTIONS} />\n            </form.Form>\n          </form.AppForm>\n        </div>\n        <SheetFooter>\n          <Button variant='outline' onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button type='submit' form='order-sheet-form' disabled={mutation.isPending}>\n            {mutation.isPending ? 'Saving...' : isEdit ? 'Update' : 'Create'}\n          </Button>\n        </SheetFooter>\n      </SheetContent>\n    </Sheet>\n  );\n}\n```\n\nFor cell actions, add `const [editOpen, setEditOpen] = useState(false)` and render `<OrderFormSheet order={data} open={editOpen} onOpenChange={setEditOpen} />` with a `<DropdownMenuItem onClick={() => setEditOpen(true)}>`. For the page header \"Add\" button, create a trigger component that manages `open` state and renders the sheet.\n\n**Available field components** from `useFormFields<T>()`: `FormTextField`, `FormTextareaField`, `FormSelectField`, `FormCheckboxField`, `FormSwitchField`, `FormRadioGroupField`, `FormSliderField`, `FormFileUploadField`.\n\n---\n\n## Data Fetching with React Query\n\nThe pattern is: server prefetch → HydrationBoundary → client useSuspenseQuery.\n\n1. **Server**: `void queryClient.prefetchQuery(options)` — fire-and-forget during SSR streaming\n2. **Client**: `useSuspenseQuery(options)` — picks up dehydrated data, suspends until resolved\n3. **HydrationBoundary + dehydrate**: bridges server cache → client cache\n4. **Suspense fallback**: skeleton shown while data streams\n\n**Why `useSuspenseQuery` not `useQuery`:** `useQuery` doesn't integrate with Suspense — it shows loading even when data is prefetched. `useSuspenseQuery` picks up the dehydrated pending query. Once cached (within `staleTime: 60s`), subsequent visits are instant.\n\n**Mutations** use `mutationOptions` + `getQueryClient()` in `mutations.ts`, composed via spread at the call site:\n\n```tsx\n// In mutations.ts — shared config\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\n// In component — spread + layer UI callbacks\nconst mutation = useMutation({\n  ...createOrderMutation,\n  onSuccess: () => toast.success('Created')\n});\n```\n\nSee [references/query-abstractions.md](references/query-abstractions.md) for why `mutationOptions`/`queryOptions` are the right abstraction over custom hooks.\n\n---\n\n## Navigation & RBAC\n\nConfigure in `src/config/nav-config.ts`. Items are filtered client-side in `src/hooks/use-nav.ts` using Clerk.\n\n**Access control properties** on nav items:\n\n- `requireOrg: boolean` — requires active Clerk organization\n- `permission: string` — requires specific Clerk permission\n- `role: string` — requires specific Clerk role\n- `plan: string` — requires subscription plan (server-side)\n- `feature: string` — requires feature flag (server-side)\n\nItems without `access` are visible to everyone. All client-side checks are synchronous — no loading states.\n\n---\n\n## Themes\n\nSee [references/theming-guide.md](references/theming-guide.md) for the complete guide. Quick steps:\n\n1. Create `src/styles/themes/<name>.css` with OKLCH color tokens + `@theme inline` block\n2. Import in `src/styles/theme.css`\n3. Register in `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add Google Fonts in `src/components/themes/font.config.ts`\n\n---\n\n## Code Conventions\n\n- **`cn()`** for class merging — never concatenate className strings\n- **Server components by default** — only add `'use client'` when needed\n- **React Query** — `void prefetchQuery()` on server + `useSuspenseQuery` on client\n- **API layer** — `types.ts` → `service.ts` → `queries.ts` → `mutations.ts` per feature; `queryOptions`/`mutationOptions` as base abstractions (not custom hooks); `getQueryClient()` in mutations (not `useQueryClient()`); key factories (`entityKeys.all/list/detail`); components never import mock APIs directly\n- **nuqs** — `searchParamsCache` on server, `useQueryStates` on client with `shallow: true`\n- **Icons** — only from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` + `useFormFields<T>()` from `@/components/ui/tanstack-form`\n- **Page headers** — `PageContainer` props, never import `<Heading>` manually\n- **Sort parser** — use `getSortingStateParser` from `@/lib/data-table-parsers` (same parser as `useDataTable`)\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n",".agents/skills/kiranism-shadcn-dashboard/references/charts-guide.md":"# Charts & Analytics Guide\n\n## Table of Contents\n\n1. [Overview Architecture](#overview-architecture)\n2. [Parallel Routes Pattern](#parallel-routes-pattern)\n3. [Chart Components](#chart-components)\n4. [Stats Cards](#stats-cards)\n5. [Skeleton Loading](#skeleton-loading)\n6. [Adding a New Chart Section](#adding-a-new-chart-section)\n\n---\n\n## Overview Architecture\n\nThe analytics dashboard at `/dashboard/overview` uses **Next.js parallel routes** to load multiple chart sections independently. Each chart slot streams in as its data becomes ready — no waterfall, no blocking.\n\n**File structure:**\n\n```\nsrc/app/dashboard/overview/\n├── layout.tsx              # Composes all slots into a grid\n├── @area_stats/\n│   ├── page.tsx            # Async server component (fetches data)\n│   ├── loading.tsx         # Skeleton shown while streaming\n│   └── error.tsx           # Error boundary if fetch fails\n├── @bar_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n├── @pie_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n└── @sales/\n    ├── page.tsx\n    ├── loading.tsx\n    └── error.tsx\n\nsrc/features/overview/components/\n├── area-graph.tsx          # Client chart component\n├── area-graph-skeleton.tsx # Matching skeleton\n├── bar-graph.tsx\n├── bar-graph-skeleton.tsx\n├── pie-graph.tsx\n├── pie-graph-skeleton.tsx\n├── recent-sales.tsx\n└── recent-sales-skeleton.tsx\n```\n\n---\n\n## Parallel Routes Pattern\n\n### Layout (`layout.tsx`)\n\nThe layout receives each parallel route as a prop and arranges them in a grid:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats\n}: {\n  sales: React.ReactNode;\n  pie_stats: React.ReactNode;\n  bar_stats: React.ReactNode;\n  area_stats: React.ReactNode;\n}) {\n  return (\n    <PageContainer pageTitle='Dashboard' pageDescription='Overview analytics.'>\n      {/* Stats cards row */}\n      <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>\n        <Card>\n          <CardHeader className='flex flex-row items-center justify-between pb-2'>\n            <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n            <Icons.billing className='h-4 w-4 text-muted-foreground' />\n          </CardHeader>\n          <CardContent>\n            <div className='text-2xl font-bold'>$45,231.89</div>\n            <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n          </CardContent>\n        </Card>\n        {/* ...more stat cards */}\n      </div>\n\n      {/* Charts grid — each slot loads independently */}\n      <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n        <div className='col-span-4'>{area_stats}</div>\n        <div className='col-span-3'>{sales}</div>\n        <div className='col-span-4'>{bar_stats}</div>\n        <div className='col-span-3'>{pie_stats}</div>\n      </div>\n    </PageContainer>\n  );\n}\n```\n\n### Slot Page (`@area_stats/page.tsx`)\n\nEach slot is an async server component that fetches data then renders the chart:\n\n```tsx\nimport { delay } from '@/constants/mock-api';\nimport { AreaGraph } from '@/features/overview/components/area-graph';\n\nexport default async function AreaStatsPage() {\n  await delay(2000); // Simulates API fetch\n  return <AreaGraph />;\n}\n```\n\n### Slot Loading (`@area_stats/loading.tsx`)\n\n```tsx\nimport { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';\n\nexport default function Loading() {\n  return <AreaGraphSkeleton />;\n}\n```\n\n### Slot Error (`@area_stats/error.tsx`)\n\n```tsx\n'use client';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Icons } from '@/components/icons';\n\nexport default function AreaStatsError({ error }: { error: Error }) {\n  return (\n    <Alert variant='destructive'>\n      <Icons.alertCircle className='h-4 w-4' />\n      <AlertTitle>Error</AlertTitle>\n      <AlertDescription>Failed to load area stats: {error.message}</AlertDescription>\n    </Alert>\n  );\n}\n```\n\nEach slot can fail independently without affecting others.\n\n---\n\n## Chart Components\n\nAll chart components are `'use client'` and use **Recharts** wrapped in shadcn's `ChartContainer`.\n\n### Chart Config\n\nEvery chart defines a config object mapping data keys to labels and theme colors:\n\n```tsx\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n```\n\nTheme colors `--chart-1` through `--chart-5` are defined in each theme's CSS file and automatically adapt to light/dark mode.\n\n### Area Chart Example\n\n```tsx\n'use client';\nimport { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Icons } from '@/components/icons';\n\nconst chartData = [\n  { month: 'January', desktop: 186, mobile: 80 },\n  { month: 'February', desktop: 305, mobile: 200 }\n  // ...more months\n];\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n\nexport function AreaGraph() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <CardTitle>Area Chart - Stacked</CardTitle>\n        <Badge variant='outline'>\n          <Icons.trendingUp className='mr-1 h-3 w-3' /> +12.5%\n        </Badge>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <AreaChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey='month'\n              tickLine={false}\n              axisLine={false}\n              tickFormatter={(value) => value.slice(0, 3)}\n            />\n            <ChartTooltip content={<ChartTooltipContent indicator='dot' />} />\n            <Area\n              dataKey='mobile'\n              type='natural'\n              fill='var(--color-mobile)'\n              stroke='var(--color-mobile)'\n              stackId='a'\n            />\n            <Area\n              dataKey='desktop'\n              type='natural'\n              fill='var(--color-desktop)'\n              stroke='var(--color-desktop)'\n              stackId='a'\n            />\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### Bar Chart Pattern\n\nSame structure, using `BarChart` + `Bar`:\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <BarChart data={chartData}>\n    <CartesianGrid vertical={false} />\n    <XAxis dataKey='month' tickLine={false} axisLine={false} />\n    <ChartTooltip content={<ChartTooltipContent />} />\n    <Bar dataKey='desktop' fill='var(--color-desktop)' radius={4} />\n    <Bar dataKey='mobile' fill='var(--color-mobile)' radius={4} />\n  </BarChart>\n</ChartContainer>\n```\n\n### Pie/Donut Chart Pattern\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <PieChart>\n    <ChartTooltip content={<ChartTooltipContent hideLabel />} />\n    <Pie data={chartData} dataKey='visitors' nameKey='browser' innerRadius={30}>\n      <LabelList dataKey='visitors' className='fill-background' />\n    </Pie>\n  </PieChart>\n</ChartContainer>\n```\n\n---\n\n## Stats Cards\n\nStats cards are simple server-rendered `Card` components at the top of the layout — no parallel routes needed since they render instantly:\n\n```tsx\n<Card>\n  <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>\n    <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n    <Icons.billing className='h-4 w-4 text-muted-foreground' />\n  </CardHeader>\n  <CardContent>\n    <div className='text-2xl font-bold'>$45,231.89</div>\n    <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n  </CardContent>\n</Card>\n```\n\nFor dynamic stats that need data fetching, wrap in their own Suspense boundary or parallel route slot.\n\n---\n\n## Skeleton Loading\n\nEach chart has a matching skeleton component. Pattern:\n\n```tsx\nimport { Card, CardContent, CardHeader } from '@/components/ui/card';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport function AreaGraphSkeleton() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <Skeleton className='h-5 w-[140px]' />\n        <Skeleton className='h-4 w-[80px]' />\n      </CardHeader>\n      <CardContent>\n        <Skeleton className='h-[250px] w-full rounded-md' />\n      </CardContent>\n    </Card>\n  );\n}\n```\n\nMatch the skeleton dimensions to the actual chart for smooth visual transitions.\n\n---\n\n## Adding a New Chart Section\n\nTo add a new chart (e.g., line chart for user growth):\n\n### 1. Create the chart component\n\n`src/features/overview/components/line-graph.tsx`:\n\n```tsx\n'use client';\nimport { Line, LineChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  users: { label: 'Users', color: 'var(--chart-3)' }\n} satisfies ChartConfig;\n\nconst chartData = [\n  /* monthly user data */\n];\n\nexport function LineGraph() {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>User Growth</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <LineChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis dataKey='month' tickLine={false} axisLine={false} />\n            <ChartTooltip content={<ChartTooltipContent />} />\n            <Line dataKey='users' type='monotone' stroke='var(--color-users)' strokeWidth={2} />\n          </LineChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### 2. Create matching skeleton\n\n`src/features/overview/components/line-graph-skeleton.tsx`\n\n### 3. Create parallel route slot\n\n```\nsrc/app/dashboard/overview/@line_stats/\n├── page.tsx     → async, fetches data, returns <LineGraph />\n├── loading.tsx  → returns <LineGraphSkeleton />\n├── error.tsx    → error alert\n└── default.tsx  → return null (fallback when route doesn't match)\n```\n\n`default.tsx` is required for parallel routes — return `null` or a fallback:\n\n```tsx\nexport default function Default() {\n  return null;\n}\n```\n\n### 4. Add slot to layout\n\nUpdate `src/app/dashboard/overview/layout.tsx`:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats,\n  line_stats // ← add new slot\n}: {\n  /* ...types */\n}) {\n  return (\n    <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n      {/* existing charts */}\n      <div className='col-span-4'>{line_stats}</div>\n    </div>\n  );\n}\n```\n\n### Available Recharts Components\n\nCommon chart types to use with `ChartContainer`:\n\n- `AreaChart` + `Area` — filled area charts (stacked or standalone)\n- `BarChart` + `Bar` — vertical/horizontal bars\n- `LineChart` + `Line` — line/trend charts\n- `PieChart` + `Pie` — pie/donut charts\n- `RadarChart` + `Radar` — radar/spider charts\n- `RadialBarChart` + `RadialBar` — radial progress bars\n\nAll support `ChartTooltip`, `ChartLegend`, and theme-aware colors via `var(--chart-N)`.\n",".agents/skills/kiranism-shadcn-dashboard/references/forms-guide.md":"# Forms Guide\n\n## Table of Contents\n\n1. [Architecture](#architecture)\n2. [Field Types](#field-types)\n3. [Usage Patterns](#usage-patterns)\n4. [Validation Strategies](#validation-strategies)\n5. [Sheet/Dialog Forms](#sheetdialog-forms)\n6. [Multi-Step Forms](#multi-step-forms)\n7. [Advanced Patterns](#advanced-patterns)\n\n---\n\n## Architecture\n\nThe form system is built on **TanStack Form + Zod** with a composable field layer.\n\n**Key files:**\n\n- `src/components/ui/tanstack-form.tsx` — exports `useAppForm`, `useFormFields<T>()`, composed fields\n- `src/components/ui/form-context.tsx` — contexts, `createFormField`, structural components\n- `src/components/forms/fields/*.tsx` — 8 field type implementations\n\n**Key exports:**\n\n```tsx\nimport { useAppForm, useFormFields } from '@/components/ui/tanstack-form';\n```\n\n- `useAppForm(config)` — creates a form instance with `defaultValues`, `validators`, `onSubmit`\n- `useFormFields<T>()` — returns all 8 typed field components with name autocomplete from `T`\n- `form.AppForm` — context provider wrapper\n- `form.Form` — `<form>` element that handles submit\n- `form.SubmitButton` — auto-disabled when form is invalid or submitting\n- `form.AppField` — low-level render prop for custom fields\n\n---\n\n## Field Types\n\nAll fields accept: `name`, `label`, `description`, `required`, `disabled`, `validators`, `listeners`, `className`.\n\n| Component             | Props                                                                                         | Notes                                   |\n| --------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |\n| `FormTextField`       | `type` (text/email/number/password/tel/url), `placeholder`, `min`, `max`, `step`, `maxLength` | For numbers use `type='number'`         |\n| `FormTextareaField`   | `placeholder`, `rows`, `maxLength`                                                            | Multiline text                          |\n| `FormSelectField`     | `options: {value, label}[]`, `placeholder`                                                    | Single select dropdown                  |\n| `FormCheckboxField`   | `options?: {value, label}[]`                                                                  | Single checkbox or multi-checkbox group |\n| `FormSwitchField`     | —                                                                                             | Toggle switch                           |\n| `FormRadioGroupField` | `options: {value, label}[]`, `orientation`                                                    | Radio button group                      |\n| `FormSliderField`     | `min`, `max`, `step`                                                                          | Range slider                            |\n| `FormFileUploadField` | `maxSize`, `maxFiles`, `accept`                                                               | Drag-and-drop with preview              |\n\n---\n\n## Usage Patterns\n\n### Pattern 1: `useFormFields<T>()` (Recommended)\n\nType-safe field components with name autocomplete:\n\n```tsx\nconst { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n<FormTextField name='customer' label='Customer' required placeholder='Name'\n  validators={{ onBlur: z.string().min(2) }} />\n\n<FormSelectField name='status' label='Status' required options={STATUS_OPTIONS}\n  validators={{ onBlur: z.string().min(1) }} />\n```\n\n### Pattern 2: `form.AppField` render prop\n\nFull control for custom field rendering:\n\n```tsx\n<form.AppField name='framework'>\n  {(field) => (\n    <field.FieldSet>\n      <field.Field>\n        <field.TextField label='Framework' />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Pattern 3: Direct import (no type safety)\n\nFor quick prototyping:\n\n```tsx\nimport { FormTextField } from '@/components/ui/tanstack-form';\n<FormTextField name='name' label='Name' />;\n```\n\n---\n\n## Validation Strategies\n\n### Field-level (recommended for UX)\n\n```tsx\n<FormTextField\n  name='email'\n  label='Email'\n  validators={{\n    onBlur: z.string().email('Invalid email') // Validates when field loses focus\n  }}\n/>\n```\n\n### Form-level (catch-all on submit)\n\n```tsx\nconst form = useAppForm({\n  validators: { onSubmit: orderSchema }, // Validates entire form on submit\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n### Async validation (server-side checks)\n\n```tsx\n<FormTextField\n  name='username'\n  label='Username'\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const exists = await checkUsername(value);\n      return exists ? 'Username taken' : undefined;\n    }\n  }}\n  asyncDebounceMs={500}\n/>\n```\n\n### Linked field validation\n\nFor dependent fields (e.g., confirm password):\n\n```tsx\n<FormTextField\n  name='confirmPassword'\n  label='Confirm Password'\n  validators={{\n    onChangeListenTo: ['password'],\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      return value !== password ? 'Passwords must match' : undefined;\n    }\n  }}\n/>\n```\n\n---\n\n## Sheet/Dialog Forms\n\nThe key pattern for forms inside sheets or dialogs: give the `<form.Form>` an `id`, and use that `id` on the submit button's `form` attribute. This allows the submit button to live outside the form element (e.g., in `SheetFooter`).\n\n```tsx\n<form.AppForm>\n  <form.Form id='my-sheet-form' className='space-y-4'>\n    {/* fields */}\n  </form.Form>\n</form.AppForm>;\n\n{\n  /* In SheetFooter — button is outside the <form> but still submits it */\n}\n<SheetFooter>\n  <Button type='submit' form='my-sheet-form'>\n    Save\n  </Button>\n</SheetFooter>;\n```\n\nOn success, call `onOpenChange(false)` to close the sheet and `form.reset()` for create forms.\n\n---\n\n## Multi-Step Forms\n\nUse `withFieldGroup` + `useAppForm` with `StepButton`:\n\n```tsx\n// Define field groups for each step\nconst Step1 = withFieldGroup({\n  fields: ['name', 'email'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='name' label='Name' />\n        <FormTextField name='email' label='Email' />\n        <form.StepButton direction='next' label='Next' />\n      </>\n    );\n  }\n});\n\nconst Step2 = withFieldGroup({\n  fields: ['address', 'city'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='address' label='Address' />\n        <FormTextField name='city' label='City' />\n        <form.StepButton direction='prev' label='Back' />\n        <form.SubmitButton label='Submit' />\n      </>\n    );\n  }\n});\n```\n\nUse the `useStepper` hook from `src/hooks/use-stepper.tsx` to manage step state.\n\n---\n\n## Advanced Patterns\n\n### Nested objects (dot notation)\n\n```tsx\n<FormTextField name='address.street' label='Street' />\n<FormTextField name='address.city' label='City' />\n```\n\n### Dynamic array rows\n\n```tsx\n<form.AppField name='items' mode='array'>\n  {(field) => (\n    <>\n      {field.state.value.map((_, i) => (\n        <form.AppField key={i} name={`items[${i}].name`}>\n          {(subField) => <subField.TextField label={`Item ${i + 1}`} />}\n        </form.AppField>\n      ))}\n      <Button onClick={() => field.pushValue({ name: '' })}>Add Row</Button>\n    </>\n  )}\n</form.AppField>\n```\n\n### Side effects with listeners\n\n```tsx\n<FormSelectField\n  name='country'\n  label='Country'\n  options={countryOptions}\n  listeners={{\n    onChange: ({ value }) => {\n      // Reset city when country changes\n      form.setFieldValue('city', '');\n    }\n  }}\n/>\n```\n\n### Custom field with `form.AppField`\n\nFor fields not covered by the built-in 8 types:\n\n```tsx\n<form.AppField name='color'>\n  {(field) => (\n    <field.FieldSet>\n      <Label>Pick a color</Label>\n      <field.Field>\n        <input\n          type='color'\n          value={field.state.value}\n          onChange={(e) => field.handleChange(e.target.value)}\n        />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Form-level errors\n\nDisplay errors that apply to the whole form (e.g., server errors):\n\n```tsx\nimport { FormErrors } from '@/components/ui/form-context';\n\n<form.AppForm>\n  <form.Form>\n    <FormErrors /> {/* Renders form-level validation errors */}\n    {/* fields... */}\n  </form.Form>\n</form.AppForm>;\n```\n",".agents/skills/kiranism-shadcn-dashboard/references/mock-api-guide.md":"# Mock API Guide\n\n## Table of Contents\n\n1. [Structure](#structure)\n2. [Full Template](#full-template)\n3. [Key Patterns](#key-patterns)\n4. [Integrating with React Query](#integrating-with-react-query)\n\n---\n\n## Structure\n\nEach mock API file lives in `src/constants/mock-api-<name>.ts` and is a self-contained in-memory database. It uses:\n\n- **faker** for generating sample data\n- **match-sorter** for fuzzy search across fields\n- **delay** (from `./mock-api`) to simulate network latency\n\nThe `delay` function is exported from `src/constants/mock-api.ts`:\n\n```tsx\nexport async function delay(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n```\n\n---\n\n## Full Template\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\n// 1. Define the entity type\nexport type Order = {\n  id: number;\n  customer: string;\n  email: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\n// 2. Create the fake database object\nexport const fakeOrders = {\n  records: [] as Order[],\n\n  // 3. Initialize with faker data\n  initialize() {\n    const statuses = ['pending', 'processing', 'completed', 'cancelled'];\n    for (let i = 1; i <= 20; i++) {\n      this.records.push({\n        id: i,\n        customer: faker.person.fullName(),\n        email: faker.internet.email(),\n        status: faker.helpers.arrayElement(statuses),\n        total: parseFloat(faker.commerce.price({ min: 10, max: 500 })),\n        created_at: faker.date.between({ from: '2023-01-01', to: Date.now() }).toISOString(),\n        updated_at: faker.date.recent().toISOString()\n      });\n    }\n  },\n\n  // 4. Get all with optional search (used internally)\n  async getAll({ search }: { search?: string } = {}) {\n    let items = [...this.records];\n    if (search) {\n      items = matchSorter(items, search, {\n        keys: ['customer', 'email']\n      });\n    }\n    return items;\n  },\n\n  // 5. Paginated list with filtering and sorting\n  async getOrders(params: {\n    page?: number;\n    limit?: number;\n    search?: string;\n    statuses?: string;\n    sort?: string;\n  }) {\n    await delay(800);\n    const { page = 1, limit = 10, search, statuses, sort } = params;\n\n    let items = await this.getAll({ search });\n\n    // Filter by comma-separated values\n    if (statuses) {\n      const statusList = statuses.split('.');\n      items = items.filter((item) => statusList.includes(item.status));\n    }\n\n    // Sort by column\n    if (sort) {\n      const parsedSort = JSON.parse(sort) as { id: string; desc: boolean }[];\n      if (parsedSort.length > 0) {\n        const { id, desc } = parsedSort[0];\n        items.sort((a, b) => {\n          const aVal = a[id as keyof Order];\n          const bVal = b[id as keyof Order];\n          if (aVal < bVal) return desc ? 1 : -1;\n          if (aVal > bVal) return desc ? -1 : 1;\n          return 0;\n        });\n      }\n    }\n\n    // Paginate\n    const total_items = items.length;\n    items = items.slice((page - 1) * limit, page * limit);\n\n    return { items, total_items };\n  },\n\n  // 6. Get single record by ID\n  async getOrderById(id: number) {\n    await delay(800);\n    return this.records.find((r) => r.id === id) || null;\n  },\n\n  // 7. Create\n  async createOrder(data: Omit<Order, 'id' | 'created_at' | 'updated_at'>) {\n    await delay(800);\n    const newRecord: Order = {\n      ...data,\n      id: this.records.length + 1,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString()\n    };\n    this.records.push(newRecord);\n    return newRecord;\n  },\n\n  // 8. Update\n  async updateOrder(id: number, data: Partial<Order>) {\n    await delay(800);\n    const idx = this.records.findIndex((r) => r.id === id);\n    if (idx === -1) return null;\n    this.records[idx] = {\n      ...this.records[idx],\n      ...data,\n      updated_at: new Date().toISOString()\n    };\n    return this.records[idx];\n  },\n\n  // 9. Delete\n  async deleteOrder(id: number) {\n    await delay(800);\n    this.records = this.records.filter((r) => r.id !== id);\n    return true;\n  }\n};\n\n// 10. Auto-initialize on import\nfakeOrders.initialize();\n```\n\n---\n\n## Key Patterns\n\n### Search with match-sorter\n\nAlways specify which fields to search across:\n\n```tsx\nmatchSorter(items, search, { keys: ['customer', 'email', 'status'] });\n```\n\n### Comma-separated filter values\n\nFor multi-select filters (roles, statuses), the URL param uses `.` as delimiter:\n\n```tsx\nif (statuses) {\n  const list = statuses.split('.');\n  items = items.filter((item) => list.includes(item.status));\n}\n```\n\n### Computed column sorting\n\nWhen a table has a computed column (e.g., combining first_name + last_name into \"name\"), handle it in the sort logic:\n\n```tsx\nif (id === 'name') {\n  const aName = `${a.first_name} ${a.last_name}`;\n  const bName = `${b.first_name} ${b.last_name}`;\n  return desc ? bName.localeCompare(aName) : aName.localeCompare(bName);\n}\n```\n\n### Return shape\n\nList methods must return `{ items, total_items }` (or `{ products, total }` etc. — match the query option expectations). The total is the count **before** pagination, used for `pageCount` calculation.\n\n---\n\n## Integrating with the API Layer\n\nThe mock API is only imported in `service.ts`. Queries and components import from the service and types files:\n\n```\nmock-api-orders.ts  →  api/service.ts  →  api/queries.ts  →  components\n(data source)          (data access)       (key factory +     (useSuspenseQuery\n                                            queryOptions)       + useMutation)\n```\n\n**service.ts** imports from the mock API:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\n```\n\n**queries.ts** imports from service, uses key factories:\n\n```tsx\nimport { getOrders } from './service';\nimport type { OrderFilters } from './types';\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({ queryKey: orderKeys.list(filters), queryFn: () => getOrders(filters) });\n```\n\n**Mutations** in components use service functions + key factories:\n\n```tsx\nimport { createOrder } from '../api/service';\nimport { orderKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })\n});\n```\n",".agents/skills/kiranism-shadcn-dashboard/references/query-abstractions.md":"# TanStack Query Abstractions (v5)\n\nThe core insight: **`queryOptions` and `mutationOptions` are the right abstraction — not custom hooks.**\n\n---\n\n## Query Abstraction\n\n### The Pattern\n\n```ts\n// queries/invoice.ts\nimport { queryOptions } from '@tanstack/react-query';\n\nexport function invoiceOptions(id: number) {\n  return queryOptions({\n    queryKey: ['invoice', id],\n    queryFn: () => fetchInvoice(id)\n  });\n}\n\nexport function invoiceListOptions(filters: InvoiceFilters) {\n  return queryOptions({\n    queryKey: ['invoices', filters],\n    queryFn: () => fetchInvoices(filters),\n    staleTime: 30_000\n  });\n}\n```\n\n### Usage — always compose at the call site\n\n```ts\n// basic\nconst { data } = useQuery(invoiceOptions(id));\n\n// with suspense — same options, different hook\nconst { data } = useSuspenseQuery(invoiceOptions(id));\n\n// with extra options spread on top — full type inference, no TS pain\nconst { data } = useQuery({\n  ...invoiceOptions(id),\n  select: (invoice) => invoice.createdAt, // data infers as string | undefined\n  enabled: !!id\n});\n\n// prefetch in a route loader (works outside React — this is why hooks are wrong)\nawait queryClient.prefetchQuery(invoiceOptions(id));\n\n// read from cache imperatively — queryKey is typed via DataTag symbol\nconst invoice = queryClient.getQueryData(invoiceOptions(id).queryKey);\n\n// invalidate\nqueryClient.invalidateQueries({ queryKey: invoiceOptions(id).queryKey });\n```\n\n### Why NOT a custom hook\n\nCustom hooks like `useInvoice(id)` have three critical problems:\n\n1. **Hooks only work in components/hooks** — but queries are now used in route loaders, server prefetching, event handlers, and server components. `queryOptions` is just a plain function — works anywhere.\n2. **They share logic, not configuration** — what you actually want to share is the `queryKey` + `queryFn` config. Hooks are the wrong primitive for that.\n3. **They lock you to one hook** — you can't use `useInvoice()` with `useSuspenseQuery`, `useQueries`, or imperative `queryClient` methods.\n\n### Why NOT `UseQueryOptions` type directly\n\n```ts\n// BAD — data becomes unknown\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions>) { ... }\n\n// STILL BAD — select breaks with TS error\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions<Invoice>>) { ... }\n// select: (invoice) => invoice.createdAt\n// Error: Type 'string' is not assignable to type 'Invoice'\n```\n\n`queryOptions` solves this via a `DataTag` symbol on the queryKey — full inference, zero manual generics.\n\n### Custom hooks are still fine on top\n\nIf a component always uses the same composition, a hook is fine — but build it _on top of_ `queryOptions`:\n\n```ts\n// OK — hook built on queryOptions\nfunction useInvoice(id: number) {\n  return useQuery(invoiceOptions(id));\n}\n\n// OK — hook that adds per-feature defaults\nfunction useInvoiceWithSuspense(id: number) {\n  return useSuspenseQuery(invoiceOptions(id));\n}\n```\n\n---\n\n## Mutation Abstraction\n\n### The Pattern\n\n```ts\n// mutations/invoice.ts\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\n\nexport const createInvoiceMutation = mutationOptions({\n  mutationFn: (data: CreateInvoiceInput) => createInvoice(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n\nexport const updateInvoiceMutation = mutationOptions({\n  mutationFn: ({ id, ...data }: UpdateInvoiceInput) => updateInvoice(id, data),\n  onSuccess: (updated) => {\n    const qc = getQueryClient();\n    qc.setQueryData(invoiceOptions(updated.id).queryKey, updated);\n    qc.invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n```\n\n> **Note on queryClient**: Import `getQueryClient()` directly — do NOT pass `queryClient` as a function argument. The `getQueryClient()` pattern handles both SSR (fresh per request) and client (singleton) correctly.\n\n### Usage\n\n```ts\n// basic\nconst { mutate } = useMutation(createInvoiceMutation);\n\n// composed — add per-usage callbacks on top\nconst { mutate } = useMutation({\n  ...createInvoiceMutation,\n  onError: (err) => toast.error(err.message),\n  onSuccess: (data) => {\n    // this runs AFTER the shared onSuccess above\n    router.push(`/invoices/${data.id}`);\n  }\n});\n```\n\n---\n\n## Rules Summary\n\n| Rule                                                          | Reason                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| Use `queryOptions()` not custom hooks as the base abstraction | Works everywhere — loaders, server, imperative calls                   |\n| Keep options factories lean — no extra config params          | Best abstractions are not configurable                                 |\n| Compose extra options at the call site via spread             | Full TS inference without manual generics                              |\n| Import `getQueryClient()` in mutation files                   | Handles SSR/client correctly without prop drilling                     |\n| Co-locate `queryKey` inside `queryOptions`                    | Typed key reuse in `invalidateQueries`, `setQueryData`, `getQueryData` |\n| Custom hooks are fine — but built ON TOP of `queryOptions`    | Hooks for component convenience, `queryOptions` for sharing config     |\n",".agents/skills/kiranism-shadcn-dashboard/references/theming-guide.md":"# Theme Creation Guide\n\n## Table of Contents\n\n1. [Create Theme CSS](#1-create-theme-css)\n2. [Import Theme](#2-import-theme)\n3. [Register Theme](#3-register-theme)\n4. [Add Custom Fonts](#4-add-custom-fonts-optional)\n5. [Set as Default](#5-set-as-default-optional)\n6. [Required Tokens](#required-tokens)\n7. [Color Format Reference](#color-format-reference)\n\n---\n\n## 1. Create Theme CSS\n\nCreate `src/styles/themes/<name>.css`:\n\n```css\n/* Light mode */\n[data-theme='your-theme'] {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(...);\n  --card-foreground: oklch(...);\n  --popover: oklch(...);\n  --popover-foreground: oklch(...);\n  --primary: oklch(...);\n  --primary-foreground: oklch(...);\n  --secondary: oklch(...);\n  --secondary-foreground: oklch(...);\n  --muted: oklch(...);\n  --muted-foreground: oklch(...);\n  --accent: oklch(...);\n  --accent-foreground: oklch(...);\n  --destructive: oklch(...);\n  --destructive-foreground: oklch(...);\n  --border: oklch(...);\n  --input: oklch(...);\n  --ring: oklch(...);\n  --chart-1: oklch(...);\n  --chart-2: oklch(...);\n  --chart-3: oklch(...);\n  --chart-4: oklch(...);\n  --chart-5: oklch(...);\n  --sidebar: oklch(...);\n  --sidebar-foreground: oklch(...);\n  --sidebar-primary: oklch(...);\n  --sidebar-primary-foreground: oklch(...);\n  --sidebar-accent: oklch(...);\n  --sidebar-accent-foreground: oklch(...);\n  --sidebar-border: oklch(...);\n  --sidebar-ring: oklch(...);\n  --font-sans: 'Font Name', sans-serif;\n  --font-mono: 'Mono Font', monospace;\n  --radius: 0.5rem;\n  --spacing: 0.25rem;\n}\n\n/* Dark mode */\n[data-theme='your-theme'].dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  /* ... all tokens with dark values */\n}\n\n/* Tailwind integration (required) */\n[data-theme='your-theme'] {\n  @theme inline {\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-destructive-foreground: var(--destructive-foreground);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n    --font-sans: var(--font-sans);\n    --font-mono: var(--font-mono);\n    --font-serif: var(--font-serif);\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n  }\n}\n```\n\n## 2. Import Theme\n\nAdd to `src/styles/theme.css`:\n\n```css\n@import './themes/your-theme.css';\n```\n\n## 3. Register Theme\n\nAdd to `THEMES` array in `src/components/themes/theme.config.ts`:\n\n```typescript\n{ name: 'Your Theme', value: 'your-theme' }\n```\n\nThe `value` must exactly match the `data-theme` attribute in your CSS.\n\n## 4. Add Custom Fonts (Optional)\n\nOnly if using a Google Font not already loaded.\n\nIn `src/components/themes/font.config.ts`:\n\n```typescript\nimport { Your_Font } from 'next/font/google';\n\nconst fontYourName = Your_Font({\n  subsets: ['latin'],\n  weight: ['400', '500', '700'],\n  variable: '--font-your-name'\n});\n\nexport const fontVariables = cn(\n  // ... existing fonts\n  fontYourName.variable\n);\n```\n\nIn your theme CSS, use the font's **display name** (not the CSS variable):\n\n```css\n--font-sans: 'Your Font', sans-serif;\n```\n\n## 5. Set as Default (Optional)\n\nIn `src/components/themes/theme.config.ts`:\n\n```typescript\nexport const DEFAULT_THEME = 'your-theme';\n```\n\n## Required Tokens\n\nMinimum required: `--background`, `--foreground`, `--card` & `--card-foreground`, `--popover` & `--popover-foreground`, `--primary` & `--primary-foreground`, `--secondary` & `--secondary-foreground`, `--muted` & `--muted-foreground`, `--accent` & `--accent-foreground`, `--destructive` & `--destructive-foreground`, `--border`, `--input`, `--ring`, `--radius`.\n\nOptional: `--chart-*`, `--sidebar-*`, `--font-*`, `--shadow-*`, `--tracking-normal`, `--spacing`.\n\n## Color Format Reference\n\nOKLCH: `oklch(lightness chroma hue)`\n\n- Lightness: 0-1 (0=black, 1=white)\n- Chroma: 0+ (0=gray, higher=saturated)\n- Hue: 0-360 (0=red, 120=green, 240=blue)\n\nSee `src/styles/themes/claude.css` for a complete example.\n",".agents/skills/next-best-practices/SKILL.md":"---\nname: next-best-practices\ndescription: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling\nuser-invocable: false\n---\n\n# Next.js Best Practices\n\nApply these rules when writing or reviewing Next.js code.\n\n## File Conventions\n\nSee [file-conventions.md](./file-conventions.md) for:\n\n- Project structure and special files\n- Route segments (dynamic, catch-all, groups)\n- Parallel and intercepting routes\n- Middleware rename in v16 (middleware → proxy)\n\n## RSC Boundaries\n\nDetect invalid React Server Component patterns.\n\nSee [rsc-boundaries.md](./rsc-boundaries.md) for:\n\n- Async client component detection (invalid)\n- Non-serializable props detection\n- Server Action exceptions\n\n## Async Patterns\n\nNext.js 15+ async API changes.\n\nSee [async-patterns.md](./async-patterns.md) for:\n\n- Async `params` and `searchParams`\n- Async `cookies()` and `headers()`\n- Migration codemod\n\n## Runtime Selection\n\nSee [runtime-selection.md](./runtime-selection.md) for:\n\n- Default to Node.js runtime\n- When Edge runtime is appropriate\n\n## Directives\n\nSee [directives.md](./directives.md) for:\n\n- `'use client'`, `'use server'` (React)\n- `'use cache'` (Next.js)\n\n## Functions\n\nSee [functions.md](./functions.md) for:\n\n- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`\n- Server functions: `cookies`, `headers`, `draftMode`, `after`\n- Generate functions: `generateStaticParams`, `generateMetadata`\n\n## Error Handling\n\nSee [error-handling.md](./error-handling.md) for:\n\n- `error.tsx`, `global-error.tsx`, `not-found.tsx`\n- `redirect`, `permanentRedirect`, `notFound`\n- `forbidden`, `unauthorized` (auth errors)\n- `unstable_rethrow` for catch blocks\n\n## Data Patterns\n\nSee [data-patterns.md](./data-patterns.md) for:\n\n- Server Components vs Server Actions vs Route Handlers\n- Avoiding data waterfalls (`Promise.all`, Suspense, preload)\n- Client component data fetching\n\n## Route Handlers\n\nSee [route-handlers.md](./route-handlers.md) for:\n\n- `route.ts` basics\n- GET handler conflicts with `page.tsx`\n- Environment behavior (no React DOM)\n- When to use vs Server Actions\n\n## Metadata & OG Images\n\nSee [metadata.md](./metadata.md) for:\n\n- Static and dynamic metadata\n- `generateMetadata` function\n- OG image generation with `next/og`\n- File-based metadata conventions\n\n## Image Optimization\n\nSee [image.md](./image.md) for:\n\n- Always use `next/image` over `<img>`\n- Remote images configuration\n- Responsive `sizes` attribute\n- Blur placeholders\n- Priority loading for LCP\n\n## Font Optimization\n\nSee [font.md](./font.md) for:\n\n- `next/font` setup\n- Google Fonts, local fonts\n- Tailwind CSS integration\n- Preloading subsets\n\n## Bundling\n\nSee [bundling.md](./bundling.md) for:\n\n- Server-incompatible packages\n- CSS imports (not link tags)\n- Polyfills (already included)\n- ESM/CommonJS issues\n- Bundle analysis\n\n## Scripts\n\nSee [scripts.md](./scripts.md) for:\n\n- `next/script` vs native script tags\n- Inline scripts need `id`\n- Loading strategies\n- Google Analytics with `@next/third-parties`\n\n## Hydration Errors\n\nSee [hydration-error.md](./hydration-error.md) for:\n\n- Common causes (browser APIs, dates, invalid HTML)\n- Debugging with error overlay\n- Fixes for each cause\n\n## Suspense Boundaries\n\nSee [suspense-boundaries.md](./suspense-boundaries.md) for:\n\n- CSR bailout with `useSearchParams` and `usePathname`\n- Which hooks require Suspense boundaries\n\n## Parallel & Intercepting Routes\n\nSee [parallel-routes.md](./parallel-routes.md) for:\n\n- Modal patterns with `@slot` and `(.)` interceptors\n- `default.tsx` for fallbacks\n- Closing modals correctly with `router.back()`\n\n## Self-Hosting\n\nSee [self-hosting.md](./self-hosting.md) for:\n\n- `output: 'standalone'` for Docker\n- Cache handlers for multi-instance ISR\n- What works vs needs extra setup\n\n## Debug Tricks\n\nSee [debug-tricks.md](./debug-tricks.md) for:\n\n- MCP endpoint for AI-assisted debugging\n- Rebuild specific routes with `--debug-build-paths`\n",".agents/skills/next-best-practices/async-patterns.md":"# Async Patterns\n\nIn Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous.\n\n## Async Params and SearchParams\n\nAlways type them as `Promise<...>` and await them.\n\n### Pages and Layouts\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Page({ params }: Props) {\n  const { slug } = await params;\n}\n```\n\n### Route Handlers\n\n```tsx\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n### SearchParams\n\n```tsx\ntype Props = {\n  params: Promise<{ slug: string }>;\n  searchParams: Promise<{ query?: string }>;\n};\n\nexport default async function Page({ params, searchParams }: Props) {\n  const { slug } = await params;\n  const { query } = await searchParams;\n}\n```\n\n### Synchronous Components\n\nUse `React.use()` for non-async components:\n\n```tsx\nimport { use } from 'react';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default function Page({ params }: Props) {\n  const { slug } = use(params);\n}\n```\n\n### generateMetadata\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  return { title: slug };\n}\n```\n\n## Async Cookies and Headers\n\n```tsx\nimport { cookies, headers } from 'next/headers';\n\nexport default async function Page() {\n  const cookieStore = await cookies();\n  const headersList = await headers();\n\n  const theme = cookieStore.get('theme');\n  const userAgent = headersList.get('user-agent');\n}\n```\n\n## Migration Codemod\n\n```bash\nnpx @next/codemod@latest next-async-request-api .\n```\n",".agents/skills/next-best-practices/bundling.md":"# Bundling\n\nFix common bundling issues with third-party packages.\n\n## Server-Incompatible Packages\n\nSome packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components.\n\n### Error Signs\n\n```\nReferenceError: window is not defined\nReferenceError: document is not defined\nReferenceError: localStorage is not defined\nModule not found: Can't resolve 'fs'\n```\n\n### Solution 1: Mark as Client-Only\n\nIf the package is only needed on client:\n\n```tsx\n// Bad: Fails - package uses window\nimport SomeChart from 'some-chart-library';\n\nexport default function Page() {\n  return <SomeChart />;\n}\n\n// Good: Use dynamic import with ssr: false\nimport dynamic from 'next/dynamic';\n\nconst SomeChart = dynamic(() => import('some-chart-library'), {\n  ssr: false\n});\n\nexport default function Page() {\n  return <SomeChart />;\n}\n```\n\n### Solution 2: Externalize from Server Bundle\n\nFor packages that should run on server but have bundling issues:\n\n```js\n// next.config.js\nmodule.exports = {\n  serverExternalPackages: ['problematic-package']\n};\n```\n\nUse this for:\n\n- Packages with native bindings (sharp, bcrypt)\n- Packages that don't bundle well (some ORMs)\n- Packages with circular dependencies\n\n### Solution 3: Client Component Wrapper\n\nWrap the entire usage in a client component:\n\n```tsx\n// components/ChartWrapper.tsx\n'use client';\n\nimport { Chart } from 'chart-library';\n\nexport function ChartWrapper(props) {\n  return <Chart {...props} />;\n}\n\n// app/page.tsx (server component)\nimport { ChartWrapper } from '@/components/ChartWrapper';\n\nexport default function Page() {\n  return <ChartWrapper data={data} />;\n}\n```\n\n## CSS Imports\n\nImport CSS files instead of using `<link>` tags. Next.js handles bundling and optimization.\n\n```tsx\n// Bad: Manual link tag\n<link rel='stylesheet' href='/styles.css' />;\n\n// Good: Import CSS\nimport './styles.css';\n\n// Good: CSS Modules\nimport styles from './Button.module.css';\n```\n\n## Polyfills\n\nNext.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs.\n\nAlready included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others.\n\n```tsx\n// Bad: Redundant polyfills\n<script src='https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,Array.from' />\n\n// Good: Next.js includes these automatically\n```\n\n## ESM/CommonJS Issues\n\n### Error Signs\n\n```\nSyntaxError: Cannot use import statement outside a module\nError: require() of ES Module\nModule not found: ESM packages need to be imported\n```\n\n### Solution: Transpile Package\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['some-esm-package', 'another-package']\n};\n```\n\n## Common Problematic Packages\n\n| Package         | Issue           | Solution                                                        |\n| --------------- | --------------- | --------------------------------------------------------------- |\n| `sharp`         | Native bindings | `serverExternalPackages: ['sharp']`                             |\n| `bcrypt`        | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs`          |\n| `canvas`        | Native bindings | `serverExternalPackages: ['canvas']`                            |\n| `recharts`      | Uses window     | `dynamic(() => import('recharts'), { ssr: false })`             |\n| `react-quill`   | Uses document   | `dynamic(() => import('react-quill'), { ssr: false })`          |\n| `mapbox-gl`     | Uses window     | `dynamic(() => import('mapbox-gl'), { ssr: false })`            |\n| `monaco-editor` | Uses window     | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |\n| `lottie-web`    | Uses document   | `dynamic(() => import('lottie-react'), { ssr: false })`         |\n\n## Bundle Analysis\n\nAnalyze bundle size with the built-in analyzer (Next.js 16.1+):\n\n```bash\nnext experimental-analyze\n```\n\nThis opens an interactive UI to:\n\n- Filter by route, environment (client/server), and type\n- Inspect module sizes and import chains\n- View treemap visualization\n\nSave output for comparison:\n\n```bash\nnext experimental-analyze --output\n# Output saved to .next/diagnostics/analyze\n```\n\nReference: https://nextjs.org/docs/app/guides/package-bundling\n\n## Migrating from Webpack to Turbopack\n\nTurbopack is the default bundler in Next.js 15+. If you have custom webpack config, migrate to Turbopack-compatible alternatives:\n\n```js\n// next.config.js\nmodule.exports = {\n  // Good: Works with Turbopack\n  serverExternalPackages: ['package'],\n  transpilePackages: ['package'],\n\n  // Bad: Webpack-only - migrate away from this\n  webpack: (config) => {\n    // custom webpack config\n  }\n};\n```\n\nReference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack\n",".agents/skills/next-best-practices/data-patterns.md":"# Data Patterns\n\nChoose the right data fetching pattern for each use case.\n\n## Decision Tree\n\n```\nNeed to fetch data?\n├── From a Server Component?\n│   └── Use: Fetch directly (no API needed)\n│\n├── From a Client Component?\n│   ├── Is it a mutation (POST/PUT/DELETE)?\n│   │   └── Use: Server Action\n│   └── Is it a read (GET)?\n│       └── Use: Route Handler OR pass from Server Component\n│\n├── Need external API access (webhooks, third parties)?\n│   └── Use: Route Handler\n│\n└── Need REST API for mobile app / external clients?\n    └── Use: Route Handler\n```\n\n## Pattern 1: Server Components (Preferred for Reads)\n\nFetch data directly in Server Components - no API layer needed.\n\n```tsx\n// app/users/page.tsx\nasync function UsersPage() {\n  // Direct database access - no API round-trip\n  const users = await db.user.findMany();\n\n  // Or fetch from external API\n  const posts = await fetch('https://api.example.com/posts').then((r) => r.json());\n\n  return (\n    <ul>\n      {users.map((user) => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\n**Benefits**:\n\n- No API to maintain\n- No client-server waterfall\n- Secrets stay on server\n- Direct database access\n\n## Pattern 2: Server Actions (Preferred for Mutations)\n\nServer Actions are the recommended way to handle mutations.\n\n```tsx\n// app/actions.ts\n'use server';\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function createPost(formData: FormData) {\n  const title = formData.get('title') as string;\n\n  await db.post.create({ data: { title } });\n\n  revalidatePath('/posts');\n}\n\nexport async function deletePost(id: string) {\n  await db.post.delete({ where: { id } });\n\n  revalidateTag('posts');\n}\n```\n\n```tsx\n// app/posts/new/page.tsx\nimport { createPost } from '@/app/actions';\n\nexport default function NewPost() {\n  return (\n    <form action={createPost}>\n      <input name='title' required />\n      <button type='submit'>Create</button>\n    </form>\n  );\n}\n```\n\n**Benefits**:\n\n- End-to-end type safety\n- Progressive enhancement (works without JS)\n- Automatic request handling\n- Integrated with React transitions\n\n**Constraints**:\n\n- POST only (no GET caching semantics)\n- Internal use only (no external access)\n- Cannot return non-serializable data\n\n## Pattern 3: Route Handlers (APIs)\n\nUse Route Handlers when you need a REST API.\n\n```tsx\n// app/api/posts/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\n// GET is cacheable\nexport async function GET(request: NextRequest) {\n  const posts = await db.post.findMany();\n  return NextResponse.json(posts);\n}\n\n// POST for mutations\nexport async function POST(request: NextRequest) {\n  const body = await request.json();\n  const post = await db.post.create({ data: body });\n  return NextResponse.json(post, { status: 201 });\n}\n```\n\n**When to use**:\n\n- External API access (mobile apps, third parties)\n- Webhooks from external services\n- GET endpoints that need HTTP caching\n- OpenAPI/Swagger documentation needed\n\n**When NOT to use**:\n\n- Internal data fetching (use Server Components)\n- Mutations from your UI (use Server Actions)\n\n## Avoiding Data Waterfalls\n\n### Problem: Sequential Fetches\n\n```tsx\n// Bad: Sequential waterfalls\nasync function Dashboard() {\n  const user = await getUser(); // Wait...\n  const posts = await getPosts(); // Then wait...\n  const comments = await getComments(); // Then wait...\n\n  return <div>...</div>;\n}\n```\n\n### Solution 1: Parallel Fetching with Promise.all\n\n```tsx\n// Good: Parallel fetching\nasync function Dashboard() {\n  const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);\n\n  return <div>...</div>;\n}\n```\n\n### Solution 2: Streaming with Suspense\n\n```tsx\n// Good: Show content progressively\nimport { Suspense } from 'react';\n\nasync function Dashboard() {\n  return (\n    <div>\n      <Suspense fallback={<UserSkeleton />}>\n        <UserSection />\n      </Suspense>\n      <Suspense fallback={<PostsSkeleton />}>\n        <PostsSection />\n      </Suspense>\n    </div>\n  );\n}\n\nasync function UserSection() {\n  const user = await getUser(); // Fetches independently\n  return <div>{user.name}</div>;\n}\n\nasync function PostsSection() {\n  const posts = await getPosts(); // Fetches independently\n  return <PostList posts={posts} />;\n}\n```\n\n### Solution 3: Preload Pattern\n\n```tsx\n// lib/data.ts\nimport { cache } from 'react';\n\nexport const getUser = cache(async (id: string) => {\n  return db.user.findUnique({ where: { id } });\n});\n\nexport const preloadUser = (id: string) => {\n  void getUser(id); // Fire and forget\n};\n```\n\n```tsx\n// app/user/[id]/page.tsx\nimport { getUser, preloadUser } from '@/lib/data';\n\nexport default async function UserPage({ params }) {\n  const { id } = await params;\n\n  // Start fetching early\n  preloadUser(id);\n\n  // Do other work...\n\n  // Data likely ready by now\n  const user = await getUser(id);\n  return <div>{user.name}</div>;\n}\n```\n\n## Client Component Data Fetching\n\nWhen Client Components need data:\n\n### Option 1: Pass from Server Component (Preferred)\n\n```tsx\n// Server Component\nasync function Page() {\n  const data = await fetchData();\n  return <ClientComponent initialData={data} />;\n}\n\n// Client Component\n('use client');\nfunction ClientComponent({ initialData }) {\n  const [data, setData] = useState(initialData);\n  // ...\n}\n```\n\n### Option 2: Fetch on Mount (When Necessary)\n\n```tsx\n'use client';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    fetch('/api/data')\n      .then((r) => r.json())\n      .then(setData);\n  }, []);\n\n  if (!data) return <Loading />;\n  return <div>{data.value}</div>;\n}\n```\n\n### Option 3: Server Action for Reads (Works But Not Ideal)\n\nServer Actions can be called from Client Components for reads, but this is not their intended purpose:\n\n```tsx\n'use client';\nimport { getData } from './actions';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    getData().then(setData);\n  }, []);\n\n  return <div>{data?.value}</div>;\n}\n```\n\n**Note**: Server Actions always use POST, so no HTTP caching. Prefer Route Handlers for cacheable reads.\n\n## Quick Reference\n\n| Pattern                | Use Case                    | HTTP Method | Caching              |\n| ---------------------- | --------------------------- | ----------- | -------------------- |\n| Server Component fetch | Internal reads              | Any         | Full Next.js caching |\n| Server Action          | Mutations, form submissions | POST only   | No                   |\n| Route Handler          | External APIs, webhooks     | Any         | GET can be cached    |\n| Client fetch to API    | Client-side reads           | Any         | HTTP cache headers   |\n",".agents/skills/next-best-practices/debug-tricks.md":"# Debug Tricks\n\nTricks to speed up debugging Next.js applications.\n\n## MCP Endpoint (Dev Server)\n\nNext.js exposes a `/_next/mcp` endpoint in development for AI-assisted debugging via MCP (Model Context Protocol).\n\n- **Next.js 16+**: Enabled by default, use `next-devtools-mcp`\n- **Next.js < 16**: Requires `experimental.mcpServer: true` in next.config.js\n\nReference: https://nextjs.org/docs/app/guides/mcp\n\n**Important**: Find the actual port of the running Next.js dev server (check terminal output or `package.json` scripts). Don't assume port 3000.\n\n### Request Format\n\nThe endpoint uses JSON-RPC 2.0 over HTTP POST:\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\",\n    \"id\": \"1\",\n    \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"<tool-name>\",\n      \"arguments\": {}\n    }\n  }'\n```\n\n### Available Tools\n\n#### `get_errors`\n\nGet current errors from dev server (build errors, runtime errors with source-mapped stacks):\n\n```json\n{ \"name\": \"get_errors\", \"arguments\": {} }\n```\n\n#### `get_routes`\n\nDiscover all routes by scanning filesystem:\n\n```json\n{ \"name\": \"get_routes\", \"arguments\": {} }\n// Optional: { \"name\": \"get_routes\", \"arguments\": { \"routerType\": \"app\" } }\n```\n\nReturns: `{ \"appRouter\": [\"/\", \"/api/users/[id]\", ...], \"pagesRouter\": [...] }`\n\n#### `get_project_metadata`\n\nGet project path and dev server URL:\n\n```json\n{ \"name\": \"get_project_metadata\", \"arguments\": {} }\n```\n\nReturns: `{ \"projectPath\": \"/path/to/project\", \"devServerUrl\": \"http://localhost:3000\" }`\n\n#### `get_page_metadata`\n\nGet runtime metadata about current page render (requires active browser session):\n\n```json\n{ \"name\": \"get_page_metadata\", \"arguments\": {} }\n```\n\nReturns segment trie data showing layouts, boundaries, and page components.\n\n#### `get_logs`\n\nGet path to Next.js development log file:\n\n```json\n{ \"name\": \"get_logs\", \"arguments\": {} }\n```\n\nReturns path to `<distDir>/logs/next-development.log`\n\n#### `get_server_action_by_id`\n\nLocate a Server Action by ID:\n\n```json\n{ \"name\": \"get_server_action_by_id\", \"arguments\": { \"actionId\": \"<action-id>\" } }\n```\n\n### Example: Get Errors\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"tools/call\",\"params\":{\"name\":\"get_errors\",\"arguments\":{}}}'\n```\n\n## Rebuild Specific Routes (Next.js 16+)\n\nUse `--debug-build-paths` to rebuild only specific routes instead of the entire app:\n\n```bash\n# Rebuild a specific route\nnext build --debug-build-paths \"/dashboard\"\n\n# Rebuild routes matching a glob\nnext build --debug-build-paths \"/api/*\"\n\n# Dynamic routes\nnext build --debug-build-paths \"/blog/[slug]\"\n```\n\nUse this to:\n\n- Quickly verify a build fix without full rebuild\n- Debug static generation issues for specific pages\n- Iterate faster on build errors\n",".agents/skills/next-best-practices/directives.md":"# Directives\n\n## React Directives\n\nThese are React directives, not Next.js specific.\n\n### `'use client'`\n\nMarks a component as a Client Component. Required for:\n\n- React hooks (`useState`, `useEffect`, etc.)\n- Event handlers (`onClick`, `onChange`)\n- Browser APIs (`window`, `localStorage`)\n\n```tsx\n'use client';\n\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-client\n\n### `'use server'`\n\nMarks a function as a Server Action. Can be passed to Client Components.\n\n```tsx\n'use server';\n\nexport async function submitForm(formData: FormData) {\n  // Runs on server\n}\n```\n\nOr inline within a Server Component:\n\n```tsx\nexport default function Page() {\n  async function submit() {\n    'use server';\n    // Runs on server\n  }\n  return <form action={submit}>...</form>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-server\n\n---\n\n## Next.js Directive\n\n### `'use cache'`\n\nMarks a function or component for caching. Part of Next.js Cache Components.\n\n```tsx\n'use cache';\n\nexport async function getCachedData() {\n  return await fetchData();\n}\n```\n\nRequires `cacheComponents: true` in `next.config.ts`.\n\nFor detailed usage including cache profiles, `cacheLife()`, `cacheTag()`, and `updateTag()`, see the `next-cache-components` skill.\n\nReference: https://nextjs.org/docs/app/api-reference/directives/use-cache\n",".agents/skills/next-best-practices/error-handling.md":"# Error Handling\n\nHandle errors gracefully in Next.js applications.\n\nReference: https://nextjs.org/docs/app/getting-started/error-handling\n\n## Error Boundaries\n\n### `error.tsx`\n\nCatches errors in a route segment and its children:\n\n```tsx\n'use client';\n\nexport default function Error({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <div>\n      <h2>Something went wrong!</h2>\n      <button onClick={() => reset()}>Try again</button>\n    </div>\n  );\n}\n```\n\n**Important:** `error.tsx` must be a Client Component.\n\n### `global-error.tsx`\n\nCatches errors in root layout:\n\n```tsx\n'use client';\n\nexport default function GlobalError({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <html>\n      <body>\n        <h2>Something went wrong!</h2>\n        <button onClick={() => reset()}>Try again</button>\n      </body>\n    </html>\n  );\n}\n```\n\n**Important:** Must include `<html>` and `<body>` tags.\n\n## Server Actions: Navigation API Gotcha\n\n**Do NOT wrap navigation APIs in try-catch.** They throw special errors that Next.js handles internally.\n\nReference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior\n\n```tsx\n'use server'\n\nimport { redirect } from 'next/navigation'\nimport { notFound } from 'next/navigation'\n\n// Bad: try-catch catches the navigation \"error\"\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)  // This throws!\n  } catch (error) {\n    // redirect() throw is caught here - navigation fails!\n    return { error: 'Failed to create post' }\n  }\n}\n\n// Good: Call navigation APIs outside try-catch\nasync function createPost(formData: FormData) {\n  let post\n  try {\n    post = await db.post.create({ ... })\n  } catch (error) {\n    return { error: 'Failed to create post' }\n  }\n  redirect(`/posts/${post.id}`)  // Outside try-catch\n}\n\n// Good: Re-throw navigation errors\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)\n  } catch (error) {\n    if (error instanceof Error && error.message === 'NEXT_REDIRECT') {\n      throw error  // Re-throw navigation errors\n    }\n    return { error: 'Failed to create post' }\n  }\n}\n```\n\nSame applies to:\n\n- `redirect()` - 307 temporary redirect\n- `permanentRedirect()` - 308 permanent redirect\n- `notFound()` - 404 not found\n- `forbidden()` - 403 forbidden\n- `unauthorized()` - 401 unauthorized\n\nUse `unstable_rethrow()` to re-throw these errors in catch blocks:\n\n```tsx\nimport { unstable_rethrow } from 'next/navigation';\n\nasync function action() {\n  try {\n    // ...\n    redirect('/success');\n  } catch (error) {\n    unstable_rethrow(error); // Re-throws Next.js internal errors\n    return { error: 'Something went wrong' };\n  }\n}\n```\n\n## Redirects\n\n```tsx\nimport { redirect, permanentRedirect } from 'next/navigation';\n\n// 307 Temporary - use for most cases\nredirect('/new-path');\n\n// 308 Permanent - use for URL migrations (cached by browsers)\npermanentRedirect('/new-url');\n```\n\n## Auth Errors\n\nTrigger auth-related error pages:\n\n```tsx\nimport { forbidden, unauthorized } from 'next/navigation';\n\nasync function Page() {\n  const session = await getSession();\n\n  if (!session) {\n    unauthorized(); // Renders unauthorized.tsx (401)\n  }\n\n  if (!session.hasAccess) {\n    forbidden(); // Renders forbidden.tsx (403)\n  }\n\n  return <Dashboard />;\n}\n```\n\nCreate corresponding error pages:\n\n```tsx\n// app/forbidden.tsx\nexport default function Forbidden() {\n  return <div>You don't have access to this resource</div>;\n}\n\n// app/unauthorized.tsx\nexport default function Unauthorized() {\n  return <div>Please log in to continue</div>;\n}\n```\n\n## Not Found\n\n### `not-found.tsx`\n\nCustom 404 page for a route segment:\n\n```tsx\nexport default function NotFound() {\n  return (\n    <div>\n      <h2>Not Found</h2>\n      <p>Could not find the requested resource</p>\n    </div>\n  );\n}\n```\n\n### Triggering Not Found\n\n```tsx\nimport { notFound } from 'next/navigation';\n\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const post = await getPost(id);\n\n  if (!post) {\n    notFound(); // Renders closest not-found.tsx\n  }\n\n  return <div>{post.title}</div>;\n}\n```\n\n## Error Hierarchy\n\nErrors bubble up to the nearest error boundary:\n\n```\napp/\n├── error.tsx           # Catches errors from all children\n├── blog/\n│   ├── error.tsx       # Catches errors in /blog/*\n│   └── [slug]/\n│       ├── error.tsx   # Catches errors in /blog/[slug]\n│       └── page.tsx\n└── layout.tsx          # Errors here go to global-error.tsx\n```\n",".agents/skills/next-best-practices/file-conventions.md":"# File Conventions\n\nNext.js App Router uses file-based routing with special file conventions.\n\n## Project Structure\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure\n\n```\napp/\n├── layout.tsx          # Root layout (required)\n├── page.tsx            # Home page (/)\n├── loading.tsx         # Loading UI\n├── error.tsx           # Error UI\n├── not-found.tsx       # 404 UI\n├── global-error.tsx    # Global error UI\n├── route.ts            # API endpoint\n├── template.tsx        # Re-rendered layout\n├── default.tsx         # Parallel route fallback\n├── blog/\n│   ├── page.tsx        # /blog\n│   └── [slug]/\n│       └── page.tsx    # /blog/:slug\n└── (group)/            # Route group (no URL impact)\n    └── page.tsx\n```\n\n## Special Files\n\n| File            | Purpose                                  |\n| --------------- | ---------------------------------------- |\n| `page.tsx`      | UI for a route segment                   |\n| `layout.tsx`    | Shared UI for segment and children       |\n| `loading.tsx`   | Loading UI (Suspense boundary)           |\n| `error.tsx`     | Error UI (Error boundary)                |\n| `not-found.tsx` | 404 UI                                   |\n| `route.ts`      | API endpoint                             |\n| `template.tsx`  | Like layout but re-renders on navigation |\n| `default.tsx`   | Fallback for parallel routes             |\n\n## Route Segments\n\n```\napp/\n├── blog/               # Static segment: /blog\n├── [slug]/             # Dynamic segment: /:slug\n├── [...slug]/          # Catch-all: /a/b/c\n├── [[...slug]]/        # Optional catch-all: / or /a/b/c\n└── (marketing)/        # Route group (ignored in URL)\n```\n\n## Parallel Routes\n\n```\napp/\n├── @analytics/\n│   └── page.tsx\n├── @sidebar/\n│   └── page.tsx\n└── layout.tsx          # Receives { analytics, sidebar } as props\n```\n\n## Intercepting Routes\n\n```\napp/\n├── feed/\n│   └── page.tsx\n├── @modal/\n│   └── (.)photo/[id]/  # Intercepts /photo/[id] from /feed\n│       └── page.tsx\n└── photo/[id]/\n    └── page.tsx\n```\n\nConventions:\n\n- `(.)` - same level\n- `(..)` - one level up\n- `(..)(..)` - two levels up\n- `(...)` - from root\n\n## Private Folders\n\n```\napp/\n├── _components/        # Private folder (not a route)\n│   └── Button.tsx\n└── page.tsx\n```\n\nPrefix with `_` to exclude from routing.\n\n## Middleware / Proxy\n\n### Next.js 14-15: `middleware.ts`\n\n```ts\n// middleware.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  // Auth, redirects, rewrites, etc.\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n### Next.js 16+: `proxy.ts`\n\nRenamed for clarity - same capabilities, different names:\n\n```ts\n// proxy.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function proxy(request: NextRequest) {\n  // Same logic as middleware\n  return NextResponse.next();\n}\n\nexport const proxyConfig = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n| Version | File            | Export         | Config        |\n| ------- | --------------- | -------------- | ------------- |\n| v14-15  | `middleware.ts` | `middleware()` | `config`      |\n| v16+    | `proxy.ts`      | `proxy()`      | `proxyConfig` |\n\n**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.\n\n## File Conventions Reference\n\nReference: https://nextjs.org/docs/app/api-reference/file-conventions\n",".agents/skills/next-best-practices/font.md":"# Font Optimization\n\nUse `next/font` for automatic font optimization with zero layout shift.\n\n## Google Fonts\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={inter.className}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Multiple Fonts\n\n```tsx\nimport { Inter, Roboto_Mono } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nconst robotoMono = Roboto_Mono({\n  subsets: ['latin'],\n  variable: '--font-roboto-mono'\n});\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={`${inter.variable} ${robotoMono.variable}`}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\nUse in CSS:\n\n```css\nbody {\n  font-family: var(--font-inter);\n}\n\ncode {\n  font-family: var(--font-roboto-mono);\n}\n```\n\n## Font Weights and Styles\n\n```tsx\n// Single weight\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: '400'\n});\n\n// Multiple weights\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: ['400', '500', '700']\n});\n\n// Variable font (recommended) - includes all weights\nconst inter = Inter({\n  subsets: ['latin']\n  // No weight needed - variable fonts support all weights\n});\n\n// With italic\nconst inter = Inter({\n  subsets: ['latin'],\n  style: ['normal', 'italic']\n});\n```\n\n## Local Fonts\n\n```tsx\nimport localFont from 'next/font/local';\n\nconst myFont = localFont({\n  src: './fonts/MyFont.woff2'\n});\n\n// Multiple files for different weights\nconst myFont = localFont({\n  src: [\n    {\n      path: './fonts/MyFont-Regular.woff2',\n      weight: '400',\n      style: 'normal'\n    },\n    {\n      path: './fonts/MyFont-Bold.woff2',\n      weight: '700',\n      style: 'normal'\n    }\n  ]\n});\n\n// Variable font\nconst myFont = localFont({\n  src: './fonts/MyFont-Variable.woff2',\n  variable: '--font-my-font'\n});\n```\n\n## Tailwind CSS Integration\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nexport default function RootLayout({ children }) {\n  return (\n    <html lang='en' className={inter.variable}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n```js\n// tailwind.config.js\nmodule.exports = {\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['var(--font-inter)']\n      }\n    }\n  }\n};\n```\n\n## Preloading Subsets\n\nOnly load needed character subsets:\n\n```tsx\n// Latin only (most common)\nconst inter = Inter({ subsets: ['latin'] });\n\n// Multiple subsets\nconst inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });\n```\n\n## Display Strategy\n\nControl font loading behavior:\n\n```tsx\nconst inter = Inter({\n  subsets: ['latin'],\n  display: 'swap' // Default - shows fallback, swaps when loaded\n});\n\n// Options:\n// 'auto' - browser decides\n// 'block' - short block period, then swap\n// 'swap' - immediate fallback, swap when ready (recommended)\n// 'fallback' - short block, short swap, then fallback\n// 'optional' - short block, no swap (use if font is optional)\n```\n\n## Don't Use Manual Font Links\n\nAlways use `next/font` instead of `<link>` tags for Google Fonts.\n\n```tsx\n// Bad: Manual link tag (blocks rendering, no optimization)\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Bad: Missing display and preconnect\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Good: Use next/font (self-hosted, zero layout shift)\nimport { Inter } from 'next/font/google'\n\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Importing font in every component\n// components/Button.tsx\nimport { Inter } from 'next/font/google'\nconst inter = Inter({ subsets: ['latin'] }) // Creates new instance each time!\n\n// Good: Import once in layout, use CSS variable\n// app/layout.tsx\nconst inter = Inter({ subsets: ['latin'], variable: '--font-inter' })\n\n// Bad: Using @import in CSS (blocks rendering)\n/* globals.css */\n@import url('https://fonts.googleapis.com/css2?family=Inter');\n\n// Good: Use next/font (self-hosted, no network request)\nimport { Inter } from 'next/font/google'\n\n// Bad: Loading all weights when only using a few\nconst inter = Inter({ subsets: ['latin'] }) // Loads all weights\n\n// Good: Specify only needed weights (for non-variable fonts)\nconst inter = Inter({ subsets: ['latin'], weight: ['400', '700'] })\n\n// Bad: Missing subset - loads all characters\nconst inter = Inter({})\n\n// Good: Always specify subset\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Font in Specific Components\n\n```tsx\n// For component-specific fonts, export from a shared file\n// lib/fonts.ts\nimport { Inter, Playfair_Display } from 'next/font/google';\n\nexport const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });\nexport const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });\n\n// components/Heading.tsx\nimport { playfair } from '@/lib/fonts';\n\nexport function Heading({ children }) {\n  return <h1 className={playfair.className}>{children}</h1>;\n}\n```\n",".agents/skills/next-best-practices/functions.md":"# Functions\n\nNext.js function APIs.\n\nReference: https://nextjs.org/docs/app/api-reference/functions\n\n## Navigation Hooks (Client)\n\n| Hook                        | Purpose                                                        | Reference                                                                                |\n| --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `useRouter`                 | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router)                   |\n| `usePathname`               | Get current pathname                                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname)                 |\n| `useSearchParams`           | Read URL search parameters                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params)            |\n| `useParams`                 | Access dynamic route parameters                                | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params)                   |\n| `useSelectedLayoutSegment`  | Active child segment (one level)                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment)  |\n| `useSelectedLayoutSegments` | All active segments below layout                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |\n| `useLinkStatus`             | Check link prefetch status                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status)              |\n| `useReportWebVitals`        | Report Core Web Vitals metrics                                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals)        |\n\n## Server Functions\n\n| Function     | Purpose                                      | Reference                                                              |\n| ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |\n| `cookies`    | Read/write cookies                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies)    |\n| `headers`    | Read request headers                         | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers)    |\n| `draftMode`  | Enable preview of unpublished CMS content    | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |\n| `after`      | Run code after response finishes streaming   | [Docs](https://nextjs.org/docs/app/api-reference/functions/after)      |\n| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |\n| `userAgent`  | Parse User-Agent header                      | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent)  |\n\n## Generate Functions\n\n| Function                | Purpose                                 | Reference                                                                           |\n| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |\n| `generateStaticParams`  | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params)  |\n| `generateMetadata`      | Dynamic metadata                        | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata)       |\n| `generateViewport`      | Dynamic viewport config                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport)       |\n| `generateSitemaps`      | Multiple sitemaps for large sites       | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps)       |\n| `generateImageMetadata` | Multiple OG images per route            | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |\n\n## Request/Response\n\n| Function        | Purpose                        | Reference                                                                  |\n| --------------- | ------------------------------ | -------------------------------------------------------------------------- |\n| `NextRequest`   | Extended Request with helpers  | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request)   |\n| `NextResponse`  | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response)  |\n| `ImageResponse` | Generate OG images             | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |\n\n## Common Examples\n\n### Navigation\n\nUse `next/link` for internal navigation instead of `<a>` tags.\n\n```tsx\n// Bad: Plain anchor tag\n<a href='/about'>About</a>;\n\n// Good: Next.js Link\nimport Link from 'next/link';\n\n<Link href='/about'>About</Link>;\n```\n\nActive link styling:\n\n```tsx\n'use client';\n\nimport Link from 'next/link';\nimport { usePathname } from 'next/navigation';\n\nexport function NavLink({ href, children }) {\n  const pathname = usePathname();\n\n  return (\n    <Link href={href} className={pathname === href ? 'active' : ''}>\n      {children}\n    </Link>\n  );\n}\n```\n\n### Static Generation\n\n```tsx\n// app/blog/[slug]/page.tsx\nexport async function generateStaticParams() {\n  const posts = await getPosts();\n  return posts.map((post) => ({ slug: post.slug }));\n}\n```\n\n### After Response\n\n```tsx\nimport { after } from 'next/server';\n\nexport async function POST(request: Request) {\n  const data = await processRequest(request);\n\n  after(async () => {\n    await logAnalytics(data);\n  });\n\n  return Response.json({ success: true });\n}\n```\n",".agents/skills/next-best-practices/hydration-error.md":"# Hydration Errors\n\nDiagnose and fix React hydration mismatch errors.\n\n## Error Signs\n\n- \"Hydration failed because the initial UI does not match\"\n- \"Text content does not match server-rendered HTML\"\n\n## Debugging\n\nIn development, click the hydration error to see the server/client diff.\n\n## Common Causes and Fixes\n\n### Browser-only APIs\n\n```tsx\n// Bad: Causes mismatch - window doesn't exist on server\n<div>{window.innerWidth}</div>;\n\n// Good: Use client component with mounted check\n('use client');\nimport { useState, useEffect } from 'react';\n\nexport function ClientOnly({ children }: { children: React.ReactNode }) {\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted ? children : null;\n}\n```\n\n### Date/Time Rendering\n\nServer and client may be in different timezones:\n\n```tsx\n// Bad: Causes mismatch\n<span>{new Date().toLocaleString()}</span>;\n\n// Good: Render on client only\n('use client');\nconst [time, setTime] = useState<string>();\nuseEffect(() => setTime(new Date().toLocaleString()), []);\n```\n\n### Random Values or IDs\n\n```tsx\n// Bad: Random values differ between server and client\n<div id={Math.random().toString()}>\n\n// Good: Use useId hook\nimport { useId } from 'react'\n\nfunction Input() {\n  const id = useId()\n  return <input id={id} />\n}\n```\n\n### Invalid HTML Nesting\n\n```tsx\n// Bad: Invalid - div inside p\n<p><div>Content</div></p>\n\n// Bad: Invalid - p inside p\n<p><p>Nested</p></p>\n\n// Good: Valid nesting\n<div><p>Content</p></div>\n```\n\n### Third-party Scripts\n\nScripts that modify DOM during hydration.\n\n```tsx\n// Good: Use next/script with afterInteractive\nimport Script from 'next/script';\n\nexport default function Page() {\n  return <Script src='https://example.com/script.js' strategy='afterInteractive' />;\n}\n```\n",".agents/skills/next-best-practices/image.md":"# Image Optimization\n\nUse `next/image` for automatic image optimization.\n\n## Always Use next/image\n\n```tsx\n// Bad: Avoid native img\n<img src='/hero.png' alt='Hero' />;\n\n// Good: Use next/image\nimport Image from 'next/image';\n<Image src='/hero.png' alt='Hero' width={800} height={400} />;\n```\n\n## Required Props\n\nImages need explicit dimensions to prevent layout shift:\n\n```tsx\n// Local images - dimensions inferred automatically\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" />\n\n// Remote images - must specify width/height\n<Image src=\"https://example.com/image.jpg\" alt=\"Hero\" width={800} height={400} />\n\n// Or use fill for parent-relative sizing\n<div style={{ position: 'relative', width: '100%', height: 400 }}>\n  <Image src=\"/hero.png\" alt=\"Hero\" fill style={{ objectFit: 'cover' }} />\n</div>\n```\n\n## Remote Images Configuration\n\nRemote domains must be configured in `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'example.com',\n        pathname: '/images/**'\n      },\n      {\n        protocol: 'https',\n        hostname: '*.cdn.com' // Wildcard subdomain\n      }\n    ]\n  }\n};\n```\n\n## Responsive Images\n\nUse `sizes` to tell the browser which size to download:\n\n```tsx\n// Full-width hero\n<Image\n  src=\"/hero.png\"\n  alt=\"Hero\"\n  fill\n  sizes=\"100vw\"\n/>\n\n// Responsive grid (3 columns on desktop, 1 on mobile)\n<Image\n  src=\"/card.png\"\n  alt=\"Card\"\n  fill\n  sizes=\"(max-width: 768px) 100vw, 33vw\"\n/>\n\n// Fixed sidebar image\n<Image\n  src=\"/avatar.png\"\n  alt=\"Avatar\"\n  width={200}\n  height={200}\n  sizes=\"200px\"\n/>\n```\n\n## Blur Placeholder\n\nPrevent layout shift with placeholders:\n\n```tsx\n// Local images - automatic blur hash\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" placeholder=\"blur\" />\n\n// Remote images - provide blurDataURL\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"blur\"\n  blurDataURL=\"data:image/jpeg;base64,/9j/4AAQSkZJRg...\"\n/>\n\n// Or use color placeholder\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"empty\"\n  style={{ backgroundColor: '#e0e0e0' }}\n/>\n```\n\n## Priority Loading\n\nUse `priority` for above-the-fold images (LCP):\n\n```tsx\n// Hero image - loads immediately\n<Image src=\"/hero.png\" alt=\"Hero\" fill priority />\n\n// Below-fold images - lazy loaded by default (no priority needed)\n<Image src=\"/card.png\" alt=\"Card\" width={400} height={300} />\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Missing sizes with fill - downloads largest image\n<Image src=\"/hero.png\" alt=\"Hero\" fill />\n\n// Good: Add sizes for proper responsive behavior\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" />\n\n// Bad: Using width/height for aspect ratio only\n<Image src=\"/hero.png\" alt=\"Hero\" width={16} height={9} />\n\n// Good: Use actual display dimensions or fill with sizes\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" style={{ objectFit: 'cover' }} />\n\n// Bad: Remote image without config\n<Image src=\"https://untrusted.com/image.jpg\" alt=\"Image\" width={400} height={300} />\n// Error: Invalid src prop, hostname not configured\n\n// Good: Add hostname to next.config.js remotePatterns\n```\n\n## Static Export\n\nWhen using `output: 'export'`, use `unoptimized` or custom loader:\n\n```tsx\n// Option 1: Disable optimization\n<Image src='/hero.png' alt='Hero' width={800} height={400} unoptimized />;\n\n// Option 2: Global config\n// next.config.js\nmodule.exports = {\n  output: 'export',\n  images: { unoptimized: true }\n};\n\n// Option 3: Custom loader (Cloudinary, Imgix, etc.)\nconst cloudinaryLoader = ({ src, width, quality }) => {\n  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;\n};\n\n<Image loader={cloudinaryLoader} src='sample.jpg' alt='Sample' width={800} height={400} />;\n```\n",".agents/skills/next-best-practices/metadata.md":"# Metadata\n\nAdd SEO metadata to Next.js pages using the Metadata API.\n\n## Important: Server Components Only\n\nThe `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.\n\nIf the target page has `'use client'`:\n\n1. Remove `'use client'` if possible, move client logic to child components\n2. Or extract metadata to a parent Server Component layout\n3. Or split the file: Server Component with metadata imports Client Components\n\n## Static Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n  title: 'Page Title',\n  description: 'Page description for search engines'\n};\n```\n\n## Dynamic Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  const post = await getPost(slug);\n  return { title: post.title, description: post.description };\n}\n```\n\n## Avoid Duplicate Fetches\n\nUse React `cache()` when the same data is needed for both metadata and page:\n\n```tsx\nimport { cache } from 'react';\n\nexport const getPost = cache(async (slug: string) => {\n  return await db.posts.findFirst({ where: { slug } });\n});\n```\n\n## Viewport\n\nSeparate from metadata for streaming support:\n\n```tsx\nimport type { Viewport } from 'next';\n\nexport const viewport: Viewport = {\n  width: 'device-width',\n  initialScale: 1,\n  themeColor: '#000000'\n};\n\n// Or dynamic\nexport function generateViewport({ params }): Viewport {\n  return { themeColor: getThemeColor(params) };\n}\n```\n\n## Title Templates\n\nIn root layout for consistent naming:\n\n```tsx\nexport const metadata: Metadata = {\n  title: { default: 'Site Name', template: '%s | Site Name' }\n};\n```\n\n## Metadata File Conventions\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions\n\nPlace these files in `app/` directory (or route segments):\n\n| File                            | Purpose                                       |\n| ------------------------------- | --------------------------------------------- |\n| `favicon.ico`                   | Favicon                                       |\n| `icon.png` / `icon.svg`         | App icon                                      |\n| `apple-icon.png`                | Apple app icon                                |\n| `opengraph-image.png`           | OG image                                      |\n| `twitter-image.png`             | Twitter card image                            |\n| `sitemap.ts` / `sitemap.xml`    | Sitemap (use `generateSitemaps` for multiple) |\n| `robots.ts` / `robots.txt`      | Robots directives                             |\n| `manifest.ts` / `manifest.json` | Web app manifest                              |\n\n## SEO Best Practice: Static Files Are Often Enough\n\nFor most sites, **static metadata files provide excellent SEO coverage**:\n\n```\napp/\n├── favicon.ico\n├── opengraph-image.png     # Works for both OG and Twitter\n├── sitemap.ts\n├── robots.ts\n└── layout.tsx              # With title/description metadata\n```\n\n**Tips:**\n\n- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)\n- Static `title` and `description` in layout metadata is sufficient for most pages\n- Only use dynamic `generateMetadata` when content varies per page\n\n---\n\n# OG Image Generation\n\nGenerate dynamic Open Graph images using `next/og`.\n\n## Important Rules\n\n1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)\n2. **No searchParams** - OG images can't access search params, use route params instead\n3. **Avoid Edge runtime** - Use default Node.js runtime\n\n```tsx\n// Good\nimport { ImageResponse } from 'next/og';\n\n// Bad\n// import { ImageResponse } from '@vercel/og'\n// export const runtime = 'edge'\n```\n\n## Basic OG Image\n\n```tsx\n// app/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Site Name';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\nexport default function Image() {\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 128,\n        background: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center'\n      }}\n    >\n      Hello World\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Dynamic OG Image\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Blog Post';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Image({ params }: Props) {\n  const { slug } = await params;\n  const post = await getPost(slug);\n\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 48,\n        background: 'linear-gradient(to bottom, #1a1a1a, #333)',\n        color: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: 48\n      }}\n    >\n      <div style={{ fontSize: 64, fontWeight: 'bold' }}>{post.title}</div>\n      <div style={{ marginTop: 24, opacity: 0.8 }}>{post.description}</div>\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Custom Fonts\n\n```tsx\nimport { ImageResponse } from 'next/og';\nimport { join } from 'path';\nimport { readFile } from 'fs/promises';\n\nexport default async function Image() {\n  const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');\n  const fontData = await readFile(fontPath);\n\n  return new ImageResponse(\n    <div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [{ name: 'Inter', data: fontData, style: 'normal' }]\n    }\n  );\n}\n```\n\n## File Naming\n\n- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)\n- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)\n\n## Styling Notes\n\nImageResponse uses Flexbox layout:\n\n- Use `display: 'flex'`\n- No CSS Grid support\n- Styles must be inline objects\n\n## Multiple OG Images\n\nUse `generateImageMetadata` for multiple images per route:\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport async function generateImageMetadata({ params }) {\n  const images = await getPostImages(params.slug);\n  return images.map((img, idx) => ({\n    id: idx,\n    alt: img.alt,\n    size: { width: 1200, height: 630 },\n    contentType: 'image/png'\n  }));\n}\n\nexport default async function Image({ params, id }) {\n  const images = await getPostImages(params.slug);\n  const image = images[id];\n  return new ImageResponse(/* ... */);\n}\n```\n\n## Multiple Sitemaps\n\nUse `generateSitemaps` for large sites:\n\n```tsx\n// app/sitemap.ts\nimport type { MetadataRoute } from 'next';\n\nexport async function generateSitemaps() {\n  // Return array of sitemap IDs\n  return [{ id: 0 }, { id: 1 }, { id: 2 }];\n}\n\nexport default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {\n  const start = id * 50000;\n  const end = start + 50000;\n  const products = await getProducts(start, end);\n\n  return products.map((product) => ({\n    url: `https://example.com/product/${product.id}`,\n    lastModified: product.updatedAt\n  }));\n}\n```\n\nGenerates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.\n",".agents/skills/next-best-practices/parallel-routes.md":"# Parallel & Intercepting Routes\n\nParallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.\n\n## File Structure\n\n```\napp/\n├── @modal/                    # Parallel route slot\n│   ├── default.tsx            # Required! Returns null\n│   ├── (.)photos/             # Intercepts /photos/*\n│   │   └── [id]/\n│   │       └── page.tsx       # Modal content\n│   └── [...]catchall/         # Optional: catch unmatched\n│       └── page.tsx\n├── photos/\n│   └── [id]/\n│       └── page.tsx           # Full page (direct access)\n├── layout.tsx                 # Renders both children and @modal\n└── page.tsx\n```\n\n## Step 1: Root Layout with Slot\n\n```tsx\n// app/layout.tsx\nexport default function RootLayout({\n  children,\n  modal\n}: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\n    <html>\n      <body>\n        {children}\n        {modal}\n      </body>\n    </html>\n  );\n}\n```\n\n## Step 2: Default File (Critical!)\n\n**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.\n\n```tsx\n// app/@modal/default.tsx\nexport default function Default() {\n  return null;\n}\n```\n\nWithout this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.\n\n## Step 3: Intercepting Route (Modal)\n\nThe `(.)` prefix intercepts routes at the same level.\n\n```tsx\n// app/@modal/(.)photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Step 4: Full Page (Direct Access)\n\n```tsx\n// app/photos/[id]/page.tsx\nexport default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <div className='full-page'>\n      <img src={photo.url} alt={photo.title} />\n      <h1>{photo.title}</h1>\n    </div>\n  );\n}\n```\n\n## Step 5: Modal Component with Correct Closing\n\n**Critical: Use `router.back()` to close modals, NOT `router.push()` or `<Link>`.**\n\n```tsx\n// components/modal.tsx\n'use client';\n\nimport { useRouter } from 'next/navigation';\nimport { useCallback, useEffect, useRef } from 'react';\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter();\n  const overlayRef = useRef<HTMLDivElement>(null);\n\n  // Close on escape key\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') {\n        router.back(); // Correct\n      }\n    }\n    document.addEventListener('keydown', onKeyDown);\n    return () => document.removeEventListener('keydown', onKeyDown);\n  }, [router]);\n\n  // Close on overlay click\n  const handleOverlayClick = useCallback(\n    (e: React.MouseEvent) => {\n      if (e.target === overlayRef.current) {\n        router.back(); // Correct\n      }\n    },\n    [router]\n  );\n\n  return (\n    <div\n      ref={overlayRef}\n      onClick={handleOverlayClick}\n      className='fixed inset-0 bg-black/50 flex items-center justify-center z-50'\n    >\n      <div className='bg-white rounded-lg p-6 max-w-2xl w-full mx-4'>\n        <button\n          onClick={() => router.back()} // Correct!\n          className='absolute top-4 right-4'\n        >\n          Close\n        </button>\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n### Why NOT `router.push('/')` or `<Link href=\"/\">`?\n\nUsing `push` or `Link` to \"close\" a modal:\n\n1. Adds a new history entry (back button shows modal again)\n2. Doesn't properly clear the intercepted route\n3. Can cause the modal to flash or persist unexpectedly\n\n`router.back()` correctly:\n\n1. Removes the intercepted route from history\n2. Returns to the previous page\n3. Properly unmounts the modal\n\n## Route Matcher Reference\n\nMatchers match **route segments**, not filesystem paths:\n\n| Matcher    | Matches       | Example                                                               |\n| ---------- | ------------- | --------------------------------------------------------------------- |\n| `(.)`      | Same level    | `@modal/(.)photos` intercepts `/photos`                               |\n| `(..)`     | One level up  | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |\n| `(..)(..)` | Two levels up | Rarely used                                                           |\n| `(...)`    | From root     | `@modal/(...)photos` intercepts `/photos` from anywhere               |\n\n**Common mistake**: Thinking `(..)` means \"parent folder\" - it means \"parent route segment\".\n\n## Handling Hard Navigation\n\nWhen users directly visit `/photos/123` (bookmark, refresh, shared link):\n\n- The intercepting route is bypassed\n- The full `photos/[id]/page.tsx` renders\n- Modal doesn't appear (expected behavior)\n\nIf you want the modal to appear on direct access too, you need additional logic:\n\n```tsx\n// app/photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoPage({ params }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  // Option: Render as modal on direct access too\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Common Gotchas\n\n### 1. Missing `default.tsx` → 404 on Refresh\n\nEvery `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).\n\n### 2. Modal Persists After Navigation\n\nYou're using `router.push()` instead of `router.back()`.\n\n### 3. Nested Parallel Routes Need Defaults Too\n\nIf you have `@modal` inside a route group, each level needs its own `default.tsx`:\n\n```\napp/\n├── (marketing)/\n│   ├── @modal/\n│   │   └── default.tsx     # Needed!\n│   └── layout.tsx\n└── layout.tsx\n```\n\n### 4. Intercepted Route Shows Wrong Content\n\nCheck your matcher:\n\n- `(.)photos` intercepts `/photos` from the same route level\n- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`\n\n### 5. TypeScript Errors with `params`\n\nIn Next.js 15+, `params` is a Promise:\n\n```tsx\n// Correct\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n## Complete Example: Photo Gallery Modal\n\n```\napp/\n├── @modal/\n│   ├── default.tsx\n│   └── (.)photos/\n│       └── [id]/\n│           └── page.tsx\n├── photos/\n│   ├── page.tsx           # Gallery grid\n│   └── [id]/\n│       └── page.tsx       # Full photo page\n├── layout.tsx\n└── page.tsx\n```\n\nLinks in the gallery:\n\n```tsx\n// app/photos/page.tsx\nimport Link from 'next/link';\n\nexport default async function Gallery() {\n  const photos = await getPhotos();\n\n  return (\n    <div className='grid grid-cols-3 gap-4'>\n      {photos.map((photo) => (\n        <Link key={photo.id} href={`/photos/${photo.id}`}>\n          <img src={photo.thumbnail} alt={photo.title} />\n        </Link>\n      ))}\n    </div>\n  );\n}\n```\n\nClicking a photo → Modal opens (intercepted)\nDirect URL → Full page renders\nRefresh while modal open → Full page renders\n",".agents/skills/next-best-practices/route-handlers.md":"# Route Handlers\n\nCreate API endpoints with `route.ts` files.\n\n## Basic Usage\n\n```tsx\n// app/api/users/route.ts\nexport async function GET() {\n  const users = await getUsers();\n  return Response.json(users);\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  const user = await createUser(body);\n  return Response.json(user, { status: 201 });\n}\n```\n\n## Supported Methods\n\n`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`\n\n## GET Handler Conflicts with page.tsx\n\n**A `route.ts` and `page.tsx` cannot coexist in the same folder.**\n\n```\napp/\n├── api/\n│   └── users/\n│       └── route.ts    # /api/users\n└── users/\n    ├── page.tsx        # /users (page)\n    └── route.ts        # Warning: Conflicts with page.tsx!\n```\n\nIf you need both a page and an API at the same path, use different paths:\n\n```\napp/\n├── users/\n│   └── page.tsx        # /users (page)\n└── api/\n    └── users/\n        └── route.ts    # /api/users (API)\n```\n\n## Environment Behavior\n\nRoute handlers run in a **Server Component-like environment**:\n\n- Yes: Can use `async/await`\n- Yes: Can access `cookies()`, `headers()`\n- Yes: Can use Node.js APIs\n- No: Cannot use React hooks\n- No: Cannot use React DOM APIs\n- No: Cannot use browser APIs\n\n```tsx\n// Bad: This won't work - no React DOM in route handlers\nimport { renderToString } from 'react-dom/server';\n\nexport async function GET() {\n  const html = renderToString(<Component />); // Error!\n  return new Response(html);\n}\n```\n\n## Dynamic Route Handlers\n\n```tsx\n// app/api/users/[id]/route.ts\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const user = await getUser(id);\n\n  if (!user) {\n    return Response.json({ error: 'Not found' }, { status: 404 });\n  }\n\n  return Response.json(user);\n}\n```\n\n## Request Helpers\n\n```tsx\nexport async function GET(request: Request) {\n  // URL and search params\n  const { searchParams } = new URL(request.url);\n  const query = searchParams.get('q');\n\n  // Headers\n  const authHeader = request.headers.get('authorization');\n\n  // Cookies (Next.js helper)\n  const cookieStore = await cookies();\n  const token = cookieStore.get('token');\n\n  return Response.json({ query, token });\n}\n```\n\n## Response Helpers\n\n```tsx\n// JSON response\nreturn Response.json({ data });\n\n// With status\nreturn Response.json({ error: 'Not found' }, { status: 404 });\n\n// With headers\nreturn Response.json(data, {\n  headers: {\n    'Cache-Control': 'max-age=3600'\n  }\n});\n\n// Redirect\nreturn Response.redirect(new URL('/login', request.url));\n\n// Stream\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/event-stream' }\n});\n```\n\n## When to Use Route Handlers vs Server Actions\n\n| Use Case                 | Route Handlers | Server Actions |\n| ------------------------ | -------------- | -------------- |\n| Form submissions         | No             | Yes            |\n| Data mutations from UI   | No             | Yes            |\n| Third-party webhooks     | Yes            | No             |\n| External API consumption | Yes            | No             |\n| Public REST API          | Yes            | No             |\n| File uploads             | Both work      | Both work      |\n\n**Prefer Server Actions** for mutations triggered from your UI.\n**Use Route Handlers** for external integrations and public APIs.\n",".agents/skills/next-best-practices/rsc-boundaries.md":"# RSC Boundaries\n\nDetect and prevent invalid patterns when crossing Server/Client component boundaries.\n\n## Detection Rules\n\n### 1. Async Client Components Are Invalid\n\nClient components **cannot** be async functions. Only Server Components can be async.\n\n**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`\n\n```tsx\n// Bad: async client component\n'use client';\nexport default async function UserProfile() {\n  const user = await getUser(); // Cannot await in client component\n  return <div>{user.name}</div>;\n}\n\n// Good: Remove async, fetch data in parent server component\n// page.tsx (server component - no 'use client')\nexport default async function Page() {\n  const user = await getUser();\n  return <UserProfile user={user} />;\n}\n\n// UserProfile.tsx (client component)\n('use client');\nexport function UserProfile({ user }: { user: User }) {\n  return <div>{user.name}</div>;\n}\n```\n\n```tsx\n// Bad: async arrow function client component\n'use client';\nconst Dashboard = async () => {\n  const data = await fetchDashboard();\n  return <div>{data}</div>;\n};\n\n// Good: Fetch in server component, pass data down\n```\n\n### 2. Non-Serializable Props to Client Components\n\nProps passed from Server → Client must be JSON-serializable.\n\n**Detect:** Server component passes these to a client component:\n\n- Functions (except Server Actions with `'use server'`)\n- `Date` objects\n- `Map`, `Set`, `WeakMap`, `WeakSet`\n- Class instances\n- `Symbol` (unless globally registered)\n- Circular references\n\n```tsx\n// Bad: Function prop\n// page.tsx (server)\nexport default function Page() {\n  const handleClick = () => console.log('clicked');\n  return <ClientButton onClick={handleClick} />;\n}\n\n// Good: Define function inside client component\n// ClientButton.tsx\n('use client');\nexport function ClientButton() {\n  const handleClick = () => console.log('clicked');\n  return <button onClick={handleClick}>Click</button>;\n}\n```\n\n```tsx\n// Bad: Date object (silently becomes string, then crashes)\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt} />; // Date object\n}\n\n// PostCard.tsx (client) - will crash on .getFullYear()\n('use client');\nexport function PostCard({ createdAt }: { createdAt: Date }) {\n  return <span>{createdAt.getFullYear()}</span>; // Runtime error!\n}\n\n// Good: Serialize to string on server\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt.toISOString()} />;\n}\n\n// PostCard.tsx (client)\n('use client');\nexport function PostCard({ createdAt }: { createdAt: string }) {\n  const date = new Date(createdAt);\n  return <span>{date.getFullYear()}</span>;\n}\n```\n\n```tsx\n// Bad: Class instance\nconst user = new UserModel(data)\n<ClientProfile user={user} /> // Methods will be stripped\n\n// Good: Pass plain object\nconst user = await getUser()\n<ClientProfile user={{ id: user.id, name: user.name }} />\n```\n\n```tsx\n// Bad: Map/Set\n<ClientComponent items={new Map([['a', 1]])} />\n\n// Good: Convert to array/object\n<ClientComponent items={Object.fromEntries(map)} />\n<ClientComponent items={Array.from(set)} />\n```\n\n### 3. Server Actions Are the Exception\n\nFunctions marked with `'use server'` CAN be passed to client components.\n\n```tsx\n// Valid: Server Action can be passed\n// actions.ts\n'use server';\nexport async function submitForm(formData: FormData) {\n  // server-side logic\n}\n\n// page.tsx (server)\nimport { submitForm } from './actions';\nexport default function Page() {\n  return <ClientForm onSubmit={submitForm} />; // OK!\n}\n\n// ClientForm.tsx (client)\n('use client');\nexport function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {\n  return <form action={onSubmit}>...</form>;\n}\n```\n\n## Quick Reference\n\n| Pattern                           | Valid? | Fix                                   |\n| --------------------------------- | ------ | ------------------------------------- |\n| `'use client'` + `async function` | No     | Fetch in server parent, pass data     |\n| Pass `() => {}` to client         | No     | Define in client or use server action |\n| Pass `new Date()` to client       | No     | Use `.toISOString()`                  |\n| Pass `new Map()` to client        | No     | Convert to object/array               |\n| Pass class instance to client     | No     | Pass plain object                     |\n| Pass server action to client      | Yes    | -                                     |\n| Pass `string/number/boolean`      | Yes    | -                                     |\n| Pass plain object/array           | Yes    | -                                     |\n",".agents/skills/next-best-practices/runtime-selection.md":"# Runtime Selection\n\n## Use Node.js Runtime by Default\n\nUse the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.\n\n```tsx\n// Good: Default - no runtime config needed (uses Node.js)\nexport default function Page() { ... }\n\n// Caution: Only if already used in project or specifically required\nexport const runtime = 'edge'\n```\n\n## When to Use Each\n\n### Node.js Runtime (Default)\n\n- Full Node.js API support\n- File system access (`fs`)\n- Full `crypto` support\n- Database connections\n- Most npm packages work\n\n### Edge Runtime\n\n- Only for specific edge-location latency requirements\n- Limited API (no `fs`, limited `crypto`)\n- Smaller cold start\n- Geographic distribution needs\n\n## Detection\n\n**Before adding `runtime = 'edge'`**, check:\n\n1. Does the project already use Edge runtime?\n2. Is there a specific latency requirement?\n3. Are all dependencies Edge-compatible?\n\nIf unsure, use Node.js runtime.\n",".agents/skills/next-best-practices/scripts.md":"# Scripts\n\nLoading third-party scripts in Next.js.\n\n## Use next/script\n\nAlways use `next/script` instead of native `<script>` tags for better performance.\n\n```tsx\n// Bad: Native script tag\n<script src='https://example.com/script.js'></script>;\n\n// Good: Next.js Script component\nimport Script from 'next/script';\n\n<Script src='https://example.com/script.js' />;\n```\n\n## Inline Scripts Need ID\n\nInline scripts require an `id` attribute for Next.js to track them.\n\n```tsx\n// Bad: Missing id\n<Script dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Has id\n<Script id=\"my-script\" dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Inline with id\n<Script id=\"show-banner\">\n  {`document.getElementById('banner').classList.remove('hidden')`}\n</Script>\n```\n\n## Don't Put Script in Head\n\n`next/script` should not be placed inside `next/head`. It handles its own positioning.\n\n```tsx\n// Bad: Script inside Head\nimport Head from 'next/head'\nimport Script from 'next/script'\n\n<Head>\n  <Script src=\"/analytics.js\" />\n</Head>\n\n// Good: Script outside Head\n<Head>\n  <title>Page</title>\n</Head>\n<Script src=\"/analytics.js\" />\n```\n\n## Loading Strategies\n\n```tsx\n// afterInteractive (default) - Load after page is interactive\n<Script src=\"/analytics.js\" strategy=\"afterInteractive\" />\n\n// lazyOnload - Load during idle time\n<Script src=\"/widget.js\" strategy=\"lazyOnload\" />\n\n// beforeInteractive - Load before page is interactive (use sparingly)\n// Only works in app/layout.tsx or pages/_document.js\n<Script src=\"/critical.js\" strategy=\"beforeInteractive\" />\n\n// worker - Load in web worker (experimental)\n<Script src=\"/heavy.js\" strategy=\"worker\" />\n```\n\n## Google Analytics\n\nUse `@next/third-parties` instead of inline GA scripts.\n\n```tsx\n// Bad: Inline GA script\n<Script src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\" />\n<Script id=\"ga-init\">\n  {`window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n    gtag('config', 'G-XXXXX');`}\n</Script>\n\n// Good: Next.js component\nimport { GoogleAnalytics } from '@next/third-parties/google'\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <body>{children}</body>\n      <GoogleAnalytics gaId=\"G-XXXXX\" />\n    </html>\n  )\n}\n```\n\n## Google Tag Manager\n\n```tsx\nimport { GoogleTagManager } from '@next/third-parties/google';\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <GoogleTagManager gtmId='GTM-XXXXX' />\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Other Third-Party Scripts\n\n```tsx\n// YouTube embed\nimport { YouTubeEmbed } from '@next/third-parties/google';\n\n<YouTubeEmbed videoid='dQw4w9WgXcQ' />;\n\n// Google Maps\nimport { GoogleMapsEmbed } from '@next/third-parties/google';\n\n<GoogleMapsEmbed apiKey='YOUR_API_KEY' mode='place' q='Brooklyn+Bridge,New+York,NY' />;\n```\n\n## Quick Reference\n\n| Pattern                                       | Issue                      | Fix                       |\n| --------------------------------------------- | -------------------------- | ------------------------- |\n| `<script src=\"...\">`                          | No optimization            | Use `next/script`         |\n| `<Script>` without id                         | Can't track inline scripts | Add `id` attribute        |\n| `<Script>` inside `<Head>`                    | Wrong placement            | Move outside Head         |\n| Inline GA/GTM scripts                         | No optimization            | Use `@next/third-parties` |\n| `strategy=\"beforeInteractive\"` outside layout | Won't work                 | Only use in root layout   |\n",".agents/skills/next-best-practices/self-hosting.md":"# Self-Hosting Next.js\n\nDeploy Next.js outside of Vercel with confidence.\n\n## Quick Start: Standalone Output\n\nFor Docker or any containerized deployment, use standalone output:\n\n```js\n// next.config.js\nmodule.exports = {\n  output: 'standalone'\n};\n```\n\nThis creates a minimal `standalone` folder with only production dependencies:\n\n```\n.next/\n├── standalone/\n│   ├── server.js          # Entry point\n│   ├── node_modules/      # Only production deps\n│   └── .next/             # Build output\n└── static/                # Must be copied separately\n```\n\n## Docker Deployment\n\n### Dockerfile\n\n```dockerfile\nFROM node:20-alpine AS base\n\n# Install dependencies\nFROM base AS deps\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci\n\n# Build\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nRUN npm run build\n\n# Production\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\n\n# Create non-root user\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\n# Copy standalone output\nCOPY --from=builder /app/.next/standalone ./\nCOPY --from=builder /app/.next/static ./.next/static\nCOPY --from=builder /app/public ./public\n\nUSER nextjs\n\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n```\n\n### Docker Compose\n\n```yaml\nversion: '3.8'\n\nservices:\n  web:\n    build: .\n    ports:\n      - '3000:3000'\n    environment:\n      - NODE_ENV=production\n    restart: unless-stopped\n    healthcheck:\n      test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']\n      interval: 30s\n      timeout: 10s\n      retries: 3\n```\n\n## PM2 Deployment\n\nFor traditional server deployments:\n\n```js\n// ecosystem.config.js\nmodule.exports = {\n  apps: [\n    {\n      name: 'nextjs',\n      script: '.next/standalone/server.js',\n      instances: 'max',\n      exec_mode: 'cluster',\n      env: {\n        NODE_ENV: 'production',\n        PORT: 3000\n      }\n    }\n  ]\n};\n```\n\n```bash\nnpm run build\npm2 start ecosystem.config.js\n```\n\n## ISR and Cache Handlers\n\n### The Problem\n\nISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:\n\n- Instance A regenerates page → saves to its local disk\n- Instance B serves stale page → doesn't see Instance A's cache\n- Load balancer sends users to random instances → inconsistent content\n\n### Solution: Custom Cache Handler\n\nNext.js 14+ supports custom cache handlers for shared storage:\n\n```js\n// next.config.js\nmodule.exports = {\n  cacheHandler: require.resolve('./cache-handler.js'),\n  cacheMaxMemorySize: 0 // Disable in-memory cache\n};\n```\n\n#### Redis Cache Handler Example\n\n```js\n// cache-handler.js\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL);\nconst CACHE_PREFIX = 'nextjs:';\n\nmodule.exports = class CacheHandler {\n  constructor(options) {\n    this.options = options;\n  }\n\n  async get(key) {\n    const data = await redis.get(CACHE_PREFIX + key);\n    if (!data) return null;\n\n    const parsed = JSON.parse(data);\n    return {\n      value: parsed.value,\n      lastModified: parsed.lastModified\n    };\n  }\n\n  async set(key, data, ctx) {\n    const cacheData = {\n      value: data,\n      lastModified: Date.now()\n    };\n\n    // Set TTL based on revalidate option\n    if (ctx?.revalidate) {\n      await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));\n    } else {\n      await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));\n    }\n  }\n\n  async revalidateTag(tags) {\n    // Implement tag-based invalidation\n    // This requires tracking which keys have which tags\n  }\n};\n```\n\n#### S3 Cache Handler Example\n\n```js\n// cache-handler.js\nconst { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');\n\nconst s3 = new S3Client({ region: process.env.AWS_REGION });\nconst BUCKET = process.env.CACHE_BUCKET;\n\nmodule.exports = class CacheHandler {\n  async get(key) {\n    try {\n      const response = await s3.send(\n        new GetObjectCommand({\n          Bucket: BUCKET,\n          Key: `cache/${key}`\n        })\n      );\n      const body = await response.Body.transformToString();\n      return JSON.parse(body);\n    } catch (err) {\n      if (err.name === 'NoSuchKey') return null;\n      throw err;\n    }\n  }\n\n  async set(key, data, ctx) {\n    await s3.send(\n      new PutObjectCommand({\n        Bucket: BUCKET,\n        Key: `cache/${key}`,\n        Body: JSON.stringify({\n          value: data,\n          lastModified: Date.now()\n        }),\n        ContentType: 'application/json'\n      })\n    );\n  }\n};\n```\n\n## What Works vs What Needs Setup\n\n| Feature              | Single Instance | Multi-Instance      | Notes                       |\n| -------------------- | --------------- | ------------------- | --------------------------- |\n| SSR                  | Yes             | Yes                 | No special setup            |\n| SSG                  | Yes             | Yes                 | Built at deploy time        |\n| ISR                  | Yes             | Needs cache handler | Filesystem cache breaks     |\n| Image Optimization   | Yes             | Yes                 | CPU-intensive, consider CDN |\n| Middleware           | Yes             | Yes                 | Runs on Node.js             |\n| Edge Runtime         | Limited         | Limited             | Some features Node-only     |\n| `revalidatePath/Tag` | Yes             | Needs cache handler | Must share cache            |\n| `next/font`          | Yes             | Yes                 | Fonts bundled at build      |\n| Draft Mode           | Yes             | Yes                 | Cookie-based                |\n\n## Image Optimization\n\nNext.js Image Optimization works out of the box but is CPU-intensive.\n\n### Option 1: Built-in (Simple)\n\nWorks automatically, but consider:\n\n- Set `deviceSizes` and `imageSizes` in config to limit variants\n- Use `minimumCacheTTL` to reduce regeneration\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    minimumCacheTTL: 60 * 60 * 24, // 24 hours\n    deviceSizes: [640, 750, 1080, 1920] // Limit sizes\n  }\n};\n```\n\n### Option 2: External Loader (Recommended for Scale)\n\nOffload to Cloudinary, Imgix, or similar:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    loader: 'custom',\n    loaderFile: './lib/image-loader.js'\n  }\n};\n```\n\n```js\n// lib/image-loader.js\nexport default function cloudinaryLoader({ src, width, quality }) {\n  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];\n  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;\n}\n```\n\n## Environment Variables\n\n### Build-time vs Runtime\n\n```js\n// Available at build time only (baked into bundle)\nNEXT_PUBLIC_API_URL=https://api.example.com\n\n// Available at runtime (server-side only)\nDATABASE_URL=postgresql://...\nAPI_SECRET=...\n```\n\n### Runtime Configuration\n\nFor truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:\n\n```tsx\n// app/api/config/route.ts\nexport async function GET() {\n  return Response.json({\n    apiUrl: process.env.API_URL,\n    features: process.env.FEATURES?.split(',')\n  });\n}\n```\n\n## OpenNext: Serverless Without Vercel\n\n[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.\n\n```bash\nnpx create-sst@latest\n# or\nnpx @opennextjs/aws build\n```\n\nSupports:\n\n- AWS Lambda + CloudFront\n- Cloudflare Workers\n- Netlify Functions\n- Deno Deploy\n\n## Health Check Endpoint\n\nAlways include a health check for load balancers:\n\n```tsx\n// app/api/health/route.ts\nexport async function GET() {\n  try {\n    // Optional: check database connection\n    // await db.$queryRaw`SELECT 1`;\n\n    return Response.json({ status: 'healthy' }, { status: 200 });\n  } catch (error) {\n    return Response.json({ status: 'unhealthy' }, { status: 503 });\n  }\n}\n```\n\n## Pre-Deployment Checklist\n\n1. **Build locally first**: `npm run build` - catch errors before deploy\n2. **Test standalone output**: `node .next/standalone/server.js`\n3. **Set `output: 'standalone'`** for Docker\n4. **Configure cache handler** for multi-instance ISR\n5. **Set `HOSTNAME=\"0.0.0.0\"`** for containers\n6. **Copy `public/` and `.next/static/`** - not included in standalone\n7. **Add health check endpoint**\n8. **Test ISR revalidation** after deployment\n9. **Monitor memory usage** - Node.js defaults may need tuning\n\n## Testing Cache Handler\n\n**Critical**: Test your cache handler on every Next.js upgrade:\n\n```bash\n# Start multiple instances\nPORT=3001 node .next/standalone/server.js &\nPORT=3002 node .next/standalone/server.js &\n\n# Trigger ISR revalidation\ncurl http://localhost:3001/api/revalidate?path=/posts\n\n# Verify both instances see the update\ncurl http://localhost:3001/posts\ncurl http://localhost:3002/posts\n# Should return identical content\n```\n",".agents/skills/next-best-practices/suspense-boundaries.md":"# Suspense Boundaries\n\nClient hooks that cause CSR bailout without Suspense boundaries.\n\n## useSearchParams\n\nAlways requires Suspense boundary in static routes. Without it, the entire page becomes client-side rendered.\n\n```tsx\n// Bad: Entire page becomes CSR\n'use client';\n\nimport { useSearchParams } from 'next/navigation';\n\nexport default function SearchBar() {\n  const searchParams = useSearchParams();\n  return <div>Query: {searchParams.get('q')}</div>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\nimport { Suspense } from 'react';\nimport SearchBar from './search-bar';\n\nexport default function Page() {\n  return (\n    <Suspense fallback={<div>Loading...</div>}>\n      <SearchBar />\n    </Suspense>\n  );\n}\n```\n\n## usePathname\n\nRequires Suspense boundary when route has dynamic parameters.\n\n```tsx\n// In dynamic route [slug]\n// Bad: No Suspense\n'use client';\nimport { usePathname } from 'next/navigation';\n\nexport function Breadcrumb() {\n  const pathname = usePathname();\n  return <nav>{pathname}</nav>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\n<Suspense fallback={<BreadcrumbSkeleton />}>\n  <Breadcrumb />\n</Suspense>\n```\n\nIf you use `generateStaticParams`, Suspense is optional.\n\n## Quick Reference\n\n| Hook                | Suspense Required    |\n| ------------------- | -------------------- |\n| `useSearchParams()` | Yes                  |\n| `usePathname()`     | Yes (dynamic routes) |\n| `useParams()`       | No                   |\n| `useRouter()`       | No                   |\n",".agents/skills/shadcn/SKILL.md":"---\nname: shadcn\ndescription: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. 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\".\nuser-invocable: false\nallowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)\n---\n\n# shadcn/ui\n\nA framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI.\n\n> **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.\n\n## Current Project Context\n\n```json\n!`npx shadcn@latest info --json`\n```\n\nThe JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component.\n\n## Principles\n\n1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too.\n2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table.\n3. **Use built-in variants before custom styles.** `variant=\"outline\"`, `size=\"sm\"`, etc.\n4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`.\n\n## Critical Rules\n\nThese rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs.\n\n### Styling & Tailwind → [styling.md](./rules/styling.md)\n\n- **`className` for layout, not styling.** Never override component colors or typography.\n- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`.\n- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`.\n- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`.\n- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`).\n- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries.\n- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking.\n\n### Forms & Inputs → [forms.md](./rules/forms.md)\n\n- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout.\n- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`.\n- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.**\n- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state.\n- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading.\n- **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.\n\n### Component Structure → [composition.md](./rules/composition.md)\n\n- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`.\n- **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)\n- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className=\"sr-only\"` if visually hidden.\n- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`.\n- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`.\n- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`.\n- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load.\n\n### Use Components, Not Custom Markup → [composition.md](./rules/composition.md)\n\n- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.\n- **Callouts use `Alert`.** Don't build custom styled divs.\n- **Empty states use `Empty`.** Don't build custom empty state markup.\n- **Toast via `sonner`.** Use `toast()` from `sonner`.\n- **Use `Separator`** instead of `<hr>` or `<div className=\"border-t\">`.\n- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.\n- **Use `Badge`** instead of custom styled spans.\n\n### Icons → [icons.md](./rules/icons.md)\n\n- **Icons in `Button` use `data-icon`.** `data-icon=\"inline-start\"` or `data-icon=\"inline-end\"` on the icon.\n- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.\n- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.\n\n### Chat & Messaging → [chat.md](./rules/chat.md)\n\n- **Chat UI composes the chat primitives.** Conversations use `MessageScroller`, rows use `Message`, surfaces use `Bubble`. Never hand-rolled bubble `div`s or a raw scroll container.\n- **`MessageScroller` owns scroll behavior.** Streaming follow, anchoring, and jump-to-latest (`MessageScrollerButton`) are built in. Don't write a `useStickToBottom`/`ResizeObserver` hook.\n- **Attachments use `Attachment`; system notes and dividers use `Marker`.** Not `Item` cards or `Separator` + a label.\n\n### CLI\n\n- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`.\n- **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply <code>` for existing projects, or `npx shadcn@latest init --preset <code>` when initializing.\n\n## Key Patterns\n\nThese are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above.\n\n```tsx\n// Form layout: FieldGroup + Field, not div + Label.\n<FieldGroup>\n  <Field>\n    <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n    <Input id=\"email\" />\n  </Field>\n</FieldGroup>\n\n// Validation: data-invalid on Field, aria-invalid on the control.\n<Field data-invalid>\n  <FieldLabel>Email</FieldLabel>\n  <Input aria-invalid />\n  <FieldDescription>Invalid email.</FieldDescription>\n</Field>\n\n// Icons in buttons: data-icon, no sizing classes.\n<Button>\n  <SearchIcon data-icon=\"inline-start\" />\n  Search\n</Button>\n\n// Spacing: gap-*, not space-y-*.\n<div className=\"flex flex-col gap-4\">  // correct\n<div className=\"space-y-4\">           // wrong\n\n// Equal dimensions: size-*, not w-* h-*.\n<Avatar className=\"size-10\">   // correct\n<Avatar className=\"w-10 h-10\"> // wrong\n\n// Status colors: Badge variants or semantic tokens, not raw colors.\n<Badge variant=\"secondary\">+20.1%</Badge>    // correct\n<span className=\"text-emerald-600\">+20.1%</span> // wrong\n```\n\n## Component Selection\n\n| Need                       | Use                                                                                                 |\n| -------------------------- | --------------------------------------------------------------------------------------------------- |\n| Button/action              | `Button` with appropriate variant                                                                   |\n| Form inputs                | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` |\n| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem`                                                                   |\n| Data display               | `Table`, `Card`, `Badge`, `Avatar`                                                                  |\n| Navigation                 | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination`                                     |\n| Overlays                   | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation)       |\n| Feedback                   | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner`                                        |\n| Command palette            | `Command` inside `Dialog`                                                                           |\n| Charts                     | `Chart` (wraps Recharts)                                                                            |\n| Layout                     | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible`                          |\n| Empty states               | `Empty`                                                                                             |\n| Menus                      | `DropdownMenu`, `ContextMenu`, `Menubar`                                                            |\n| Tooltips/info              | `Tooltip`, `HoverCard`, `Popover`                                                                   |\n| Chat / conversation UI     | `MessageScroller`, `Message`, `Bubble`, `Attachment`, `Marker`                                      |\n\n## Key Fields\n\nThe injected project context contains these key fields:\n\n- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode.\n- **`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.\n- **`tailwindVersion`** → `\"v4\"` uses `@theme inline` blocks; `\"v3\"` uses `tailwind.config.js`.\n- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one.\n- **`style`** → component visual treatment (e.g. `nova`, `vega`).\n- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props.\n- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`.\n- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc.\n- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA).\n- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`).\n- **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information.\n\nSee [cli.md — `info` command](./cli.md) for the full field reference.\n\n## Component Docs, Examples, and Usage\n\nRun `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content.\n\n```bash\nnpx shadcn@latest docs button dialog select\n```\n\n**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.\n\n## Workflow\n\n1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh.\n2. **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.\n3. **Find components** — `npx shadcn@latest search`.\n4. **Get docs and examples** — run `npx shadcn@latest docs <component>` 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`.\n5. **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).\n6. **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.\n7. **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.\n8. **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`, `owner/repo`, etc.), ask which registry to use. Never default to a registry on behalf of the user.\n9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?\n   - **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values.\n   - **Inspect incoming preset**: `npx shadcn@latest preset decode <code>`. Use `preset url <code>` or `preset open <code>` to share or open the preset builder.\n   - **Overwrite**: `npx shadcn@latest apply <code>`. Overwrites detected components, fonts, and CSS variables.\n   - **Partial**: `npx shadcn@latest apply <code> --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.\n   - **Merge**: `npx shadcn@latest init --preset <code> --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.\n   - **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.\n   - **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 <current-base>` explicitly — preset codes do not encode the base.\n\n## Updating Components\n\nWhen 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.**\n\n1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected.\n2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local.\n3. Decide per file based on the diff:\n   - No local changes → safe to overwrite.\n   - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications.\n   - User says \"just update everything\" → use `--overwrite`, but confirm first.\n4. **Never use `--overwrite` without the user's explicit approval.**\n\n## Quick Reference\n\n```bash\n# Create a new project.\nnpx shadcn@latest init --name my-app --preset base-nova\nnpx shadcn@latest init --name my-app --preset a2r6bw --template vite\n\n# Create a monorepo project.\nnpx shadcn@latest init --name my-app --preset base-nova --monorepo\nnpx shadcn@latest init --name my-app --preset base-nova --template next --monorepo\n\n# Initialize existing project.\nnpx shadcn@latest init --preset base-nova\nnpx shadcn@latest init --defaults  # shortcut: --template=next --preset=nova (base style implied)\n\n# Apply a preset to an existing project.\nnpx shadcn@latest apply a2r6bw\nnpx shadcn@latest apply a2r6bw --only theme\nnpx shadcn@latest apply a2r6bw --only font\nnpx shadcn@latest apply a2r6bw --only theme,font\n\n# Inspect preset codes and project preset state.\nnpx shadcn@latest preset decode a2r6bw\nnpx shadcn@latest preset url a2r6bw\nnpx shadcn@latest preset open a2r6bw\nnpx shadcn@latest preset resolve\nnpx shadcn@latest preset resolve --json\n\n# Add components.\nnpx shadcn@latest add button card dialog\nnpx shadcn@latest add @magicui/shimmer-button\nnpx shadcn@latest add owner/repo/item\nnpx shadcn@latest add --all\n\n# Preview changes before adding/updating.\nnpx shadcn@latest add button --dry-run\nnpx shadcn@latest add button --diff button.tsx\nnpx shadcn@latest add @acme/form --view button.tsx\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# Search registries.\nnpx shadcn@latest search @shadcn -q \"sidebar\"\nnpx shadcn@latest search @tailark -q \"stats\"\nnpx shadcn@latest search owner/repo -q \"login\"\nnpx shadcn@latest search                          # all configured registries\nnpx shadcn@latest search @shadcn -q \"menu\" -t ui  # filter by item type\n\n# Get component docs and example URLs.\nnpx shadcn@latest docs button dialog select\n\n# View registry item details (for items not yet installed).\nnpx shadcn@latest view @shadcn/button\nnpx shadcn@latest view owner/repo/item\n```\n\n**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma`\n**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo)\n**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com).\n\n## Detailed References\n\n- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states\n- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading\n- [rules/chat.md](./rules/chat.md) — MessageScroller, Message, Bubble, Attachment, Marker; streaming, anchoring, jump-to-latest\n- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects\n- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index\n- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion\n- [cli.md](./cli.md) — Commands, flags, presets, templates\n- [registry.md](./registry.md) — Authoring source registries, `include`, item definitions, dependencies, GitHub registry rules\n- [customization.md](./customization.md) — Theming, CSS variables, extending components\n",".agents/skills/shadcn/cli.md":"# shadcn CLI Reference\n\nConfiguration is read from `components.json`.\n\n> **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.\n\n> **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.\n\n## Contents\n\n- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build\n- Templates: next, vite, start, react-router, astro\n- Presets: named, code, URL formats and fields\n- Switching presets\n\n---\n\n## Commands\n\n### `init` — Initialize or create a project\n\n```bash\nnpx shadcn@latest init [components...] [options]\n```\n\nInitializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.\n\n| Flag                    | Short | Description                                               | Default |\n| ----------------------- | ----- | --------------------------------------------------------- | ------- |\n| `--template <template>` | `-t`  | Template (next, start, vite, next-monorepo, react-router) | —       |\n| `--preset [name]`       | `-p`  | Preset configuration (named, code, or URL)                | —       |\n| `--yes`                 | `-y`  | Skip confirmation prompt                                  | `true`  |\n| `--defaults`            | `-d`  | Use defaults (`--template=next --preset=base-nova`)       | `false` |\n| `--force`               | `-f`  | Force overwrite existing configuration                    | `false` |\n| `--cwd <cwd>`           | `-c`  | Working directory                                         | current |\n| `--name <name>`         | `-n`  | Name for new project                                      | —       |\n| `--silent`              | `-s`  | Mute output                                               | `false` |\n| `--rtl`                 |       | Enable RTL support                                        | —       |\n| `--reinstall`           |       | Re-install existing UI components                         | `false` |\n| `--monorepo`            |       | Scaffold a monorepo project                               | —       |\n| `--no-monorepo`         |       | Skip the monorepo prompt                                  | —       |\n\n`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.\n\n### `apply` — Apply a preset to an existing project\n\n```bash\nnpx shadcn@latest apply [preset] [options]\n```\n\nApplies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components.\n\n| Flag                | Short | Description                                | Default |\n| ------------------- | ----- | ------------------------------------------ | ------- |\n| `--preset <preset>` | —     | Preset configuration (named, code, or URL) | —       |\n| `--yes`             | `-y`  | Skip confirmation prompt                   | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                          | current |\n| `--silent`          | `-s`  | Mute output                                | `false` |\n\n`[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match.\nIf no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`.\n\n### `add` — Add components\n\n> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.\n\n```bash\nnpx shadcn@latest add [components...] [options]\n```\n\nAccepts component names, registry-prefixed names (`@magicui/shimmer-button`),\nGitHub item addresses (`owner/repo/item`), URLs, or local paths.\n\n| Flag            | Short | Description                                                                                                          | Default |\n| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |\n| `--yes`         | `-y`  | Skip confirmation prompt                                                                                             | `false` |\n| `--overwrite`   | `-o`  | Overwrite existing files                                                                                             | `false` |\n| `--cwd <cwd>`   | `-c`  | Working directory                                                                                                    | current |\n| `--all`         | `-a`  | Add all available components                                                                                         | `false` |\n| `--path <path>` | `-p`  | Target path for the component                                                                                        | —       |\n| `--silent`      | `-s`  | Mute output                                                                                                          | `false` |\n| `--dry-run`     |       | Preview all changes without writing files                                                                            | `false` |\n| `--diff [path]` |       | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`)         | —       |\n| `--view [path]` |       | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | —       |\n\n#### Dry-Run Mode\n\nUse `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.\n\n```bash\n# Preview all changes.\nnpx shadcn@latest add button --dry-run\n\n# Show diffs for all files (top 5).\nnpx shadcn@latest add button --diff\n\n# Show the diff for a specific file.\nnpx shadcn@latest add button --diff button.tsx\n\n# Show contents for all files (top 5).\nnpx shadcn@latest add button --view\n\n# Show the full content of a specific file.\nnpx shadcn@latest add button --view button.tsx\n\n# Works with URLs too.\nnpx shadcn@latest add https://api.npoint.io/abc123 --dry-run\n\n# Works with public GitHub registries too.\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# CSS diffs.\nnpx shadcn@latest add button --diff globals.css\n```\n\n**When to use dry-run:**\n\n- When the user asks \"what files will this add?\" or \"what will this change?\" — use `--dry-run`.\n- Before overwriting existing components — use `--diff` to preview the changes first.\n- When the user wants to inspect component source code without installing — use `--view`.\n- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.\n- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.\n\n> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.\n\n#### Smart Merge from Upstream\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow.\n\n### `search` — Search registries\n\n```bash\nnpx shadcn@latest search [registries...] [options]\n```\n\nFuzzy search across registries. Also aliased as `npx shadcn@latest list`.\nSupports namespaces (`@acme`), public GitHub registry sources (`owner/repo`),\nand registry catalog URLs. Without `-q`, lists all items. When no registries are\npassed, searches every registry configured in `components.json`.\n\n| Flag                | Short | Description                                       | Default |\n| ------------------- | ----- | ------------------------------------------------- | ------- |\n| `--query <query>`   | `-q`  | Search query                                      | —       |\n| `--type <type>`     | `-t`  | Filter by item type (e.g. `ui`, `block`, `hook`); comma-separated | —       |\n| `--limit <number>`  | `-l`  | Max items to display                              | `100`   |\n| `--offset <number>` | `-o`  | Items to skip                                     | `0`     |\n| `--json`            |       | Output as JSON                                    | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                                 | current |\n\n### `view` — View item details\n\n```bash\nnpx shadcn@latest view <items...> [options]\n```\n\nDisplays item info including file contents. Examples:\n`npx shadcn@latest view @shadcn/button`,\n`npx shadcn@latest view owner/repo/item`.\n\n### `docs` — Get component documentation URLs\n\n```bash\nnpx shadcn@latest docs <components...> [options]\n```\n\nOutputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.\n\nExample output for `npx shadcn@latest docs input button`:\n\n```\nbase  radix\n\ninput\n  docs      https://ui.shadcn.com/docs/components/radix/input\n  examples  https://raw.githubusercontent.com/.../examples/input-example.tsx\n\nbutton\n  docs      https://ui.shadcn.com/docs/components/radix/button\n  examples  https://raw.githubusercontent.com/.../examples/button-example.tsx\n```\n\nSome components include an `api` link to the underlying library (e.g. `cmdk` for the command component).\n\n### `diff` — Check for updates\n\nDo not use this command. Use `npx shadcn@latest add --diff` instead.\n\n### `info` — Project information\n\n```bash\nnpx shadcn@latest info [options]\n```\n\nDisplays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.\n\n| Flag          | Short | Description       | Default |\n| ------------- | ----- | ----------------- | ------- |\n| `--cwd <cwd>` | `-c`  | Working directory | current |\n\n**Project Info fields:**\n\n| Field                | Type      | Meaning                                                            |\n| -------------------- | --------- | ------------------------------------------------------------------ |\n| `framework`          | `string`  | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |\n| `frameworkVersion`   | `string`  | Framework version (e.g. `15.2.4`)                                  |\n| `isSrcDir`           | `boolean` | Whether the project uses a `src/` directory                        |\n| `isRSC`              | `boolean` | Whether React Server Components are enabled                        |\n| `isTsx`              | `boolean` | Whether the project uses TypeScript                                |\n| `tailwindVersion`    | `string`  | `\"v3\"` or `\"v4\"`                                                   |\n| `tailwindConfigFile` | `string`  | Path to the Tailwind config file                                   |\n| `tailwindCssFile`    | `string`  | Path to the global CSS file                                        |\n| `aliasPrefix`        | `string`  | Import alias prefix (e.g. `@`, `~`, `@/`)                          |\n| `packageManager`     | `string`  | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`)            |\n\n**Components.json fields:**\n\n| Field                | Type      | Meaning                                                                                    |\n| -------------------- | --------- | ------------------------------------------------------------------------------------------ |\n| `base`               | `string`  | Primitive library (`radix` or `base`) — determines component APIs and available props      |\n| `style`              | `string`  | Visual style (e.g. `nova`, `vega`)                                                         |\n| `rsc`                | `boolean` | RSC flag from config                                                                       |\n| `tsx`                | `boolean` | TypeScript flag                                                                            |\n| `tailwind.config`    | `string`  | Tailwind config path                                                                       |\n| `tailwind.css`       | `string`  | Global CSS path — this is where custom CSS variables go                                    |\n| `iconLibrary`        | `string`  | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |\n| `aliases.components` | `string`  | Component import alias (e.g. `@/components`)                                               |\n| `aliases.utils`      | `string`  | Utils import alias (e.g. `@/lib/utils`)                                                    |\n| `aliases.ui`         | `string`  | UI component alias (e.g. `@/components/ui`)                                                |\n| `aliases.lib`        | `string`  | Lib alias (e.g. `@/lib`)                                                                   |\n| `aliases.hooks`      | `string`  | Hooks alias (e.g. `@/hooks`)                                                               |\n| `resolvedPaths`      | `object`  | Absolute file-system paths for each alias                                                  |\n| `registries`         | `object`  | Configured custom registries                                                               |\n\n**Links fields:**\n\nThe `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.\n\n### `build` — Build a custom registry\n\n```bash\nnpx shadcn@latest build [registry] [options]\n```\n\nBuilds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.\n\nFor authoring rules, `include`, item definitions, `registryDependencies`, and\nGitHub registry behavior, see [registry.md](./registry.md).\n\n| Flag              | Short | Description       | Default      |\n| ----------------- | ----- | ----------------- | ------------ |\n| `--output <path>` | `-o`  | Output directory  | `./public/r` |\n| `--cwd <cwd>`     | `-c`  | Working directory | current      |\n\n---\n\n## Templates\n\n| Value          | Framework      | Monorepo support |\n| -------------- | -------------- | ---------------- |\n| `next`         | Next.js        | Yes              |\n| `vite`         | Vite           | Yes              |\n| `start`        | TanStack Start | Yes              |\n| `react-router` | React Router   | Yes              |\n| `astro`        | Astro          | Yes              |\n| `laravel`      | Laravel        | No               |\n\nAll templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.\n\n---\n\n## Presets\n\nThree ways to specify a preset via `--preset`:\n\n1. **Named:** `--preset nova` or `--preset lyra`\n2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`)\n3. **URL:** `--preset \"https://ui.shadcn.com/init?base=radix&style=nova&...\"`\n\n> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.\n> Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset.\n\n## Switching Presets\n\nAsk the user first: **overwrite**, **merge**, or **skip** existing components?\n\n- **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components.\n- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.\n- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.\n\nAlways 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 <current-base>` explicitly — preset codes do not encode the base.\n",".agents/skills/shadcn/customization.md":"# Customization & Theming\n\nComponents reference semantic CSS variable tokens. Change the variables to change every component.\n\n## Contents\n\n- How it works (CSS variables → Tailwind utilities → components)\n- Color variables and OKLCH format\n- Dark mode setup\n- Changing the theme (presets, CSS variables)\n- Adding custom colors (Tailwind v3 and v4)\n- Border radius\n- Customizing components (variants, className, wrappers)\n- Checking for updates\n\n---\n\n## How It Works\n\n1. CSS variables defined in `:root` (light) and `.dark` (dark mode).\n2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.\n3. Components use these utilities — changing a variable changes all components that reference it.\n\n---\n\n## Color Variables\n\nEvery color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.\n\n| Variable                                     | Purpose                          |\n| -------------------------------------------- | -------------------------------- |\n| `--background` / `--foreground`              | Page background and default text |\n| `--card` / `--card-foreground`               | Card surfaces                    |\n| `--primary` / `--primary-foreground`         | Primary buttons and actions      |\n| `--secondary` / `--secondary-foreground`     | Secondary actions                |\n| `--muted` / `--muted-foreground`             | Muted/disabled states            |\n| `--accent` / `--accent-foreground`           | Hover and accent states          |\n| `--destructive` / `--destructive-foreground` | Error and destructive actions    |\n| `--border`                                   | Default border color             |\n| `--input`                                    | Form input borders               |\n| `--ring`                                     | Focus ring color                 |\n| `--chart-1` through `--chart-5`              | Chart/data visualization         |\n| `--sidebar-*`                                | Sidebar-specific colors          |\n| `--surface` / `--surface-foreground`         | Secondary surface                |\n\nColors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).\n\n---\n\n## Dark Mode\n\nClass-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:\n\n```tsx\nimport { ThemeProvider } from \"next-themes\"\n\n<ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n  {children}\n</ThemeProvider>\n```\n\n---\n\n## Changing the Theme\n\n```bash\n# Apply a preset code from ui.shadcn.com.\nnpx shadcn@latest apply --preset a2r6bw\n\n# Positional shorthand also works.\nnpx shadcn@latest apply a2r6bw\n\n# Switch to a named preset and overwrite existing components.\nnpx shadcn@latest apply --preset nova\n\n# Preserve existing components instead.\nnpx shadcn@latest init --preset nova --force --no-reinstall\n\n# Use a custom theme URL.\nnpx shadcn@latest apply --preset \"https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&...\"\n```\n\nOr edit CSS variables directly in `globals.css`.\n\n---\n\n## Adding Custom Colors\n\nAdd variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.\n\n```css\n/* 1. Define in the global CSS file. */\n:root {\n  --warning: oklch(0.84 0.16 84);\n  --warning-foreground: oklch(0.28 0.07 46);\n}\n.dark {\n  --warning: oklch(0.41 0.11 46);\n  --warning-foreground: oklch(0.99 0.02 95);\n}\n```\n\n```css\n/* 2a. Register with Tailwind v4 (@theme inline). */\n@theme inline {\n  --color-warning: var(--warning);\n  --color-warning-foreground: var(--warning-foreground);\n}\n```\n\nWhen `tailwindVersion` is `\"v3\"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:\n\n```js\n// 2b. Register with Tailwind v3 (tailwind.config.js).\nmodule.exports = {\n  theme: {\n    extend: {\n      colors: {\n        warning: \"oklch(var(--warning) / <alpha-value>)\",\n        \"warning-foreground\":\n          \"oklch(var(--warning-foreground) / <alpha-value>)\",\n      },\n    },\n  },\n}\n```\n\n```tsx\n// 3. Use in components.\n<div className=\"bg-warning text-warning-foreground\">Warning</div>\n```\n\n---\n\n## Border Radius\n\n`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).\n\n---\n\n## Customizing Components\n\nSee also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.\n\nPrefer these approaches in order:\n\n### 1. Built-in variants\n\n```tsx\n<Button variant=\"outline\" size=\"sm\">\n  Click\n</Button>\n```\n\n### 2. Tailwind classes via `className`\n\n```tsx\n<Card className=\"mx-auto max-w-md\">...</Card>\n```\n\n### 3. Add a new variant\n\nEdit the component source to add a variant via `cva`:\n\n```tsx\n// components/ui/button.tsx\nwarning: \"bg-warning text-warning-foreground hover:bg-warning/90\",\n```\n\n### 4. Wrapper components\n\nCompose shadcn/ui primitives into higher-level components:\n\n```tsx\nexport function ConfirmDialog({ title, description, onConfirm, children }) {\n  return (\n    <AlertDialog>\n      <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{description}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n```\n\n---\n\n## Checking for Updates\n\n```bash\nnpx shadcn@latest add button --diff\n```\n\nTo preview exactly what would change before updating, use `--dry-run` and `--diff`:\n\n```bash\nnpx shadcn@latest add button --dry-run        # see all affected files\nnpx shadcn@latest add button --diff button.tsx # see the diff for a specific file\n```\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.\n",".agents/skills/shadcn/evals/evals.json":"{\n  \"skill_name\": \"shadcn\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.\",\n      \"expected_output\": \"A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses FieldGroup and Field components for form layout instead of raw div with space-y\",\n        \"Uses Switch for independent on/off notification toggles (not looping Button with manual active state)\",\n        \"Uses data-invalid on Field and aria-invalid on the input control for validation states\",\n        \"Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing\",\n        \"Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500\",\n        \"No manual dark: color overrides\"\n      ]\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.\",\n      \"expected_output\": \"A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Includes DialogTitle for accessibility (visible or with sr-only class)\",\n        \"Avatar component includes AvatarFallback\",\n        \"Icons on buttons use the data-icon attribute (data-icon=\\\"inline-start\\\" or data-icon=\\\"inline-end\\\")\",\n        \"No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)\",\n        \"Uses tabler icons (@tabler/icons-react) instead of lucide-react\",\n        \"Uses asChild for custom triggers (radix preset)\"\n      ]\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.\",\n      \"expected_output\": \"A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)\",\n        \"Uses Skeleton component for loading placeholders instead of custom animate-pulse divs\",\n        \"Uses Badge component for percentage change instead of custom styled spans\",\n        \"Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600\",\n        \"Uses gap-* instead of space-y-* or space-x-* for spacing\",\n        \"Uses size-* when width and height are equal instead of separate w-* h-*\"\n      ]\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Build a chat conversation view: a scrollable thread of messages from two different people, each with an avatar, sender name, timestamp, and message bubble. A couple of messages include an image attachment and a PDF file attachment, and there's a 'Today' divider separating the days.\",\n      \"expected_output\": \"A React component composing MessageScroller, Message, Bubble, Attachment, and Marker from the registry instead of hand-rolled bubble/divider/attachment markup.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses MessageScroller (MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem) for the scrollable thread instead of a raw overflow-y-auto div or ScrollArea\",\n        \"Wraps each row in MessageScrollerItem inside MessageScrollerContent\",\n        \"Uses Message with MessageAvatar/MessageContent/MessageHeader for row layout instead of custom flex divs\",\n        \"Uses Bubble + BubbleContent for the message surface instead of a styled div with bg-muted/bg-primary\",\n        \"Uses Attachment (AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription) for the file and image attachments instead of Item or a custom card\",\n        \"Uses Marker (variant=\\\"separator\\\") for the 'Today' divider instead of Separator plus a centered label\",\n        \"Uses semantic color tokens and gap-* spacing; no raw colors like bg-emerald-500 and no space-y-*\",\n        \"Includes \\\"use client\\\" when the component uses state or event handlers (isRSC)\"\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"Using shadcn/ui (base-nova preset, lucide icons), build a streaming AI chat UI. The assistant's reply streams in while it generates, the view auto-scrolls to follow the latest content but stops following if the user scrolls up to read earlier messages, a 'jump to latest' button appears when the user has scrolled away from the bottom, and a subtle 'thinking…' shimmer shows while the model is generating.\",\n      \"expected_output\": \"A React component that delegates scroll/anchor behavior to MessageScroller and uses MessageScrollerButton for jump-to-latest and the shimmer utility for the thinking indicator — no hand-rolled scroll logic or custom shimmer keyframes.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses MessageScroller with MessageScrollerProvider (autoScroll) and scrollAnchor on message items for the stick-to-bottom/follow behavior instead of a custom useStickToBottom hook or ResizeObserver/scrollTop wiring\",\n        \"Uses MessageScrollerButton for the jump-to-latest control instead of a hand-built conditional button driven by manual scroll-position state\",\n        \"Uses the shimmer utility class for the 'thinking…' indicator instead of a custom @keyframes or bg-clip-text gradient animation\",\n        \"Wraps each message row in MessageScrollerItem inside MessageScrollerContent\",\n        \"Uses Message + Bubble + BubbleContent for the conversation rows instead of hand-rolled bubble divs\",\n        \"Uses semantic color tokens and gap-* spacing; includes \\\"use client\\\" (isRSC)\"\n      ]\n    }\n  ]\n}\n",".agents/skills/shadcn/mcp.md":"# shadcn MCP Server\n\nThe CLI includes an MCP server that lets AI assistants search, browse, view, and install items from registries.\n\n---\n\n## Setup\n\n```bash\nshadcn mcp        # start the MCP server (stdio)\nshadcn mcp init   # write config for your editor\n```\n\nEditor config files:\n\n| Editor      | Config file                     |\n| ----------- | ------------------------------- |\n| Claude Code | `.mcp.json`                     |\n| Cursor      | `.cursor/mcp.json`              |\n| VS Code     | `.vscode/mcp.json`              |\n| OpenCode    | `opencode.json`                 |\n| Codex       | `~/.codex/config.toml` (manual) |\n\n---\n\n## Tools\n\n> **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent.\n\n### `shadcn:get_project_registries`\n\nReturns registry names from `components.json`. Errors if no `components.json` exists.\n\n**Input:** none\n\n### `shadcn:list_items_in_registries`\n\nLists all items from one or more registries. Registries can be configured\nnamespaces such as `@acme`, public GitHub sources such as `owner/repo`, or\nregistry catalog URLs. Omit `registries` to list from every registry configured\nin `components.json`.\n\n**Input:** `registries` (string[], optional — omit for all configured), `types` (string[], optional — e.g. `[\"ui\", \"block\"]`), `limit` (number, optional, defaults to 100), `offset` (number, optional)\n\n### `shadcn:search_items_in_registries`\n\nFuzzy search across registries. Registries can be configured namespaces, public\nGitHub sources, or registry catalog URLs. Omit `registries` to search every\nregistry configured in `components.json` — e.g. \"find me a hero\" across all\nconfigured registries.\n\n**Input:** `registries` (string[], optional — omit for all configured), `query` (string), `types` (string[], optional — e.g. `[\"ui\", \"block\"]`), `limit` (number, optional, defaults to 100), `offset` (number, optional)\n\n### `shadcn:view_items_in_registries`\n\nView item details including full file contents.\n\n**Input:** `items` (string[]) — e.g.\n`[\"@shadcn/button\", \"@shadcn/card\", \"owner/repo/item\"]`\n\n### `shadcn:get_item_examples_from_registries`\n\nFind usage examples and demos with source code. Omit `registries` to search\nevery registry configured in `components.json`.\n\n**Input:** `registries` (string[], optional — omit for all configured), `query` (string) — e.g. `\"accordion-demo\"`, `\"button example\"`\n\n### `shadcn:get_add_command_for_items`\n\nReturns the CLI install command.\n\n**Input:** `items` (string[]) — e.g. `[\"@shadcn/button\"]`\n\n### `shadcn:get_audit_checklist`\n\nReturns a checklist for verifying components (imports, deps, lint, TypeScript).\n\n**Input:** none\n\n---\n\n## Configuring Registries\n\nNamespaced and authenticated registries are set in `components.json`. The\n`@shadcn` registry is always built-in. Public GitHub registries can also be used\ndirectly as `owner/repo` registry sources when the repository has a root\n`registry.json`; they do not need `components.json` configuration.\n\n```json\n{\n  \"registries\": {\n    \"@acme\": \"https://acme.com/r/{name}.json\",\n    \"@private\": {\n      \"url\": \"https://private.com/r/{name}.json\",\n      \"headers\": { \"Authorization\": \"Bearer ${MY_TOKEN}\" }\n    }\n  }\n}\n```\n\n- Names must start with `@`.\n- URLs must contain `{name}`.\n- `${VAR}` references are resolved from environment variables.\n\nCommunity registry index: `https://ui.shadcn.com/r/registries.json`\n",".agents/skills/shadcn/registry.md":"# Registry Authoring and Addresses\n\nUse this reference when the user wants to create, fix, publish, or reason about\na shadcn registry.\n\n## Mental Model\n\nA registry has two forms:\n\n- **Source registry**: an authored `registry.json` in a project or repository.\n  It may use `include` and file paths that point at source files.\n- **Built registry**: generated JSON files served to CLI consumers, usually\n  from `public/r`. Use `npx shadcn@latest build` to create this form.\n\nThe CLI installer consumes registry item payloads. A source registry is a way to\nauthor those payloads from real files.\n\nRegistry items are not limited to React components. They can distribute\ncomponents, hooks, utilities, design tokens, pages, config files, docs, rules,\nworkflows, templates, MCP files, and other project files.\n\n## Root `registry.json`\n\nThe root registry file should define registry metadata and either `items` or\n`include`.\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"acme\",\n  \"homepage\": \"https://acme.com\",\n  \"items\": [\n    {\n      \"name\": \"absolute-url\",\n      \"type\": \"registry:lib\",\n      \"title\": \"Absolute URL\",\n      \"description\": \"A utility to turn any path into an absolute URL.\",\n      \"files\": [\n        {\n          \"path\": \"lib/absolute-url.ts\",\n          \"type\": \"registry:lib\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nRoot registry rules:\n\n- Root `registry.json` must include `name` and `homepage`.\n- `items` is an array of registry item definitions.\n- `include` may be used to split the source registry into multiple files.\n- Included registry files may omit `name` and `homepage`.\n\n## Include\n\nUse `include` to keep large registries modular.\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"acme\",\n  \"homepage\": \"https://acme.com\",\n  \"include\": [\"registry/ui/registry.json\", \"registry/blocks/registry.json\"]\n}\n```\n\nInclude rules:\n\n- Include paths are relative to the `registry.json` that declares them.\n- Include paths must explicitly point to a `registry.json` file.\n- Do not use remote URLs, absolute paths, or parent traversal (`..`).\n- Item file paths are relative to the registry file that declares the item.\n- Duplicate item names fail across the resolved registry.\n\nExample included file:\n\n```json\n{\n  \"items\": [\n    {\n      \"name\": \"button\",\n      \"type\": \"registry:ui\",\n      \"files\": [\n        {\n          \"path\": \"button.tsx\",\n          \"type\": \"registry:ui\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nIf this file is at `registry/ui/registry.json`, then `button.tsx` is read from\n`registry/ui/button.tsx`, and the built item path is emitted relative to the\nroot registry.\n\n## Item Definitions\n\nCommon item fields:\n\n```json\n{\n  \"name\": \"login-form\",\n  \"type\": \"registry:block\",\n  \"title\": \"Login Form\",\n  \"description\": \"A login form with email and password fields.\",\n  \"dependencies\": [\"zod\"],\n  \"registryDependencies\": [\"button\", \"input\", \"label\"],\n  \"files\": [\n    {\n      \"path\": \"blocks/login-form.tsx\",\n      \"type\": \"registry:block\"\n    }\n  ],\n  \"cssVars\": {\n    \"light\": {\n      \"brand\": \"oklch(0.62 0.18 250)\"\n    },\n    \"dark\": {\n      \"brand\": \"oklch(0.72 0.16 250)\"\n    }\n  }\n}\n```\n\nImportant fields:\n\n- `name`: the installable item name. It is not necessarily a file path.\n- `type`: one of the registry item types, such as `registry:ui`,\n  `registry:block`, `registry:lib`, `registry:hook`, `registry:file`,\n  `registry:page`, `registry:theme`, `registry:style`, `registry:font`, or\n  `registry:item`.\n- `files`: source files copied or generated by the item.\n- `dependencies`: npm runtime dependencies.\n- `devDependencies`: npm development dependencies.\n- `registryDependencies`: other registry items required by this item.\n- `cssVars`, `css`, `tailwind`, `envVars`, and `docs`: optional install-time\n  additions.\n\nFile rules:\n\n- File paths are relative to the declaring `registry.json`.\n- `registry:file` and `registry:page` files require a `target`.\n- Do not use remote file URLs in source registry file paths.\n- Keep source files copy-pasteable: no hidden app-only imports.\n\n## Registry Dependencies\n\n`registryDependencies` entries are item addresses, not file paths.\n\n```json\n{\n  \"name\": \"login-form\",\n  \"type\": \"registry:block\",\n  \"registryDependencies\": [\"button\", \"@acme/input\", \"acme/ui/card#v1.2.0\"],\n  \"files\": [\n    {\n      \"path\": \"blocks/login-form.tsx\",\n      \"type\": \"registry:block\"\n    }\n  ]\n}\n```\n\nDependency rules:\n\n- Bare names such as `\"button\"` mean official shadcn items.\n- Bare names never mean same-registry or same-repository items.\n- Namespaced dependencies use `@namespace/item-name`.\n- GitHub dependencies use `owner/repo/item-name`.\n- Pin GitHub dependencies with `owner/repo/item-name#ref` when needed.\n- Refs are not inherited. If `owner/repo/foo#v2` depends on `bar` from the same\n  repo at `v2`, write `owner/repo/bar#v2`.\n- Do not use relative dependencies such as `\"./bar\"`.\n\n## Address Schemes\n\nWhen reasoning about a registry item string, classify it first.\n\n| Address                             | Scheme    | Meaning                                                      |\n| ----------------------------------- | --------- | ------------------------------------------------------------ |\n| `button`                            | shadcn    | Official shadcn item named `button`.                         |\n| `@acme/button`                      | namespace | Item `button` from configured registry `@acme`.              |\n| `@acme/ui/button`                   | namespace | Item `ui/button` from configured registry `@acme`.           |\n| `https://example.com/r/button.json` | url       | Built registry item JSON at that URL.                        |\n| `./button.json`                     | file      | Built registry item JSON on disk.                            |\n| `acme/ui/button`                    | github    | Item `button` from GitHub repo `acme/ui`.                    |\n| `acme/ui/forms/login#main`          | github    | Item `forms/login` from GitHub repo `acme/ui` at ref `main`. |\n\nFor namespace and GitHub addresses, slashful item names are allowed and are item\nnames, not file paths. Addresses ending in `.json` keep file-address\nprecedence, so `acme/ui/data/schema.json` is treated as a file path, not a\nGitHub item address.\n\n## GitHub Registries\n\nA public GitHub repository can act as a source registry when it has a root\n`registry.json`.\n\n```txt\nowner/repo/item-name[#ref]\n```\n\nRules:\n\n- The first two path segments are GitHub owner and repo.\n- All remaining path segments are the registry item name.\n- The source entrypoint is always root `registry.json`.\n- GitHub registries are source registries consumed directly by the CLI. They do\n  not require `shadcn build` or generated item JSON files.\n- `include` follows the same source-registry rules as local registries.\n- Currently, GitHub addresses support public `github.com` repositories only.\n- Private repos and GitHub Enterprise require explicit product decisions.\n\nWhen implementing GitHub registry fetching, resolve refs to a commit SHA before\nreading source files. Do not read moving refs directly from\n`raw.githubusercontent.com`, because branch-like refs can be cached for several\nminutes.\n\nPreferred flow:\n\n```txt\nowner/repo[#ref]\n  -> resolve ref with git ls-remote\n  -> commit SHA\n  -> read https://raw.githubusercontent.com/{owner}/{repo}/{sha}/registry.json\n  -> read includes and item files from the same SHA\n```\n\nThis keeps a command on one consistent repository snapshot.\n\nFull 40-character commit SHAs are already stable and can be used directly.\nBranches, tags, and short refs require Git so the CLI can resolve them to a\ncommit SHA first.\n\n## Build and Verify\n\nUse the CLI to build source registries:\n\n```bash\nnpx shadcn@latest build\nnpx shadcn@latest build registry.json --output public/r\n```\n\nUse CLI commands to inspect the result:\n\n```bash\nnpx shadcn@latest list @acme\nnpx shadcn@latest search @acme -q \"login\"\nnpx shadcn@latest view @acme/login-form\nnpx shadcn@latest add @acme/login-form --dry-run\nnpx shadcn@latest registry validate ./registry.json\n```\n\nUse GitHub addresses directly for public GitHub registries:\n\n```bash\nnpx shadcn@latest list owner/repo\nnpx shadcn@latest search owner/repo -q \"login\"\nnpx shadcn@latest view owner/repo/item\nnpx shadcn@latest add owner/repo/item --dry-run\nnpx shadcn@latest registry validate owner/repo\n```\n\nWhen working on registry implementation in the shadcn/ui codebase:\n\n- Keep address parsing pure and testable.\n- Do not add side effects to validators.\n- Preserve existing behavior for official shadcn, namespace, URL, and file\n  schemes.\n- Add tests for address parsing, source loading, dependency resolution, list,\n  search, view, and add paths.\n- Prefer small source-reader abstractions over a plugin system until there are\n  multiple real providers.\n",".agents/skills/shadcn/rules/base-vs-radix.md":"# Base vs Radix\n\nAPI differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.\n\n## Contents\n\n- Composition: asChild vs render\n- Button / trigger as non-button element\n- Select (items prop, placeholder, positioning, multiple, object values)\n- ToggleGroup (type vs multiple)\n- Slider (scalar vs array)\n- Accordion (type and defaultValue)\n\n---\n\n## Composition: asChild (radix) vs render (base)\n\nRadix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.\n\n**Incorrect:**\n\n```tsx\n<DialogTrigger>\n  <div>\n    <Button>Open</Button>\n  </div>\n</DialogTrigger>\n```\n\n**Correct (radix):**\n\n```tsx\n<DialogTrigger asChild>\n  <Button>Open</Button>\n</DialogTrigger>\n```\n\n**Correct (base):**\n\n```tsx\n<DialogTrigger render={<Button />}>Open</DialogTrigger>\n```\n\nThis applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.\n\n---\n\n## Button / trigger as non-button element (base only)\n\nWhen `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.\n\n**Incorrect (base):** missing `nativeButton={false}`.\n\n```tsx\n<Button render={<a href=\"/docs\" />}>Read the docs</Button>\n```\n\n**Correct (base):**\n\n```tsx\n<Button render={<a href=\"/docs\" />} nativeButton={false}>\n  Read the docs\n</Button>\n```\n\n**Correct (radix):**\n\n```tsx\n<Button asChild>\n  <a href=\"/docs\">Read the docs</a>\n</Button>\n```\n\nSame for triggers whose `render` is not a `Button`:\n\n```tsx\n// base.\n<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>\n  Pick date\n</PopoverTrigger>\n```\n\n---\n\n## Select\n\n**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.\n\n**Incorrect (base):**\n\n```tsx\n<Select>\n  <SelectTrigger><SelectValue placeholder=\"Select a fruit\" /></SelectTrigger>\n</Select>\n```\n\n**Correct (base):**\n\n```tsx\nconst items = [\n  { label: \"Select a fruit\", value: null },\n  { label: \"Apple\", value: \"apple\" },\n  { label: \"Banana\", value: \"banana\" },\n]\n\n<Select items={items}>\n  <SelectTrigger>\n    <SelectValue />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      {items.map((item) => (\n        <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>\n      ))}\n    </SelectGroup>\n  </SelectContent>\n</Select>\n```\n\n**Correct (radix):**\n\n```tsx\n<Select>\n  <SelectTrigger>\n    <SelectValue placeholder=\"Select a fruit\" />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      <SelectItem value=\"apple\">Apple</SelectItem>\n      <SelectItem value=\"banana\">Banana</SelectItem>\n    </SelectGroup>\n  </SelectContent>\n</Select>\n```\n\n**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder=\"...\">`.\n\n**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.\n\n```tsx\n// base.\n<SelectContent alignItemWithTrigger={false} side=\"bottom\">\n\n// radix.\n<SelectContent position=\"popper\">\n```\n\n---\n\n## Select — multiple selection and object values (base only)\n\nBase supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.\n\n**Correct (base — multiple selection):**\n\n```tsx\n<Select items={items} multiple defaultValue={[]}>\n  <SelectTrigger>\n    <SelectValue>\n      {(value: string[]) => value.length === 0 ? \"Select fruits\" : `${value.length} selected`}\n    </SelectValue>\n  </SelectTrigger>\n  ...\n</Select>\n```\n\n**Correct (base — object values):**\n\n```tsx\n<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>\n  <SelectTrigger>\n    <SelectValue>{(value) => value.name}</SelectValue>\n  </SelectTrigger>\n  ...\n</Select>\n```\n\n---\n\n## ToggleGroup\n\nBase uses a `multiple` boolean prop. Radix uses `type=\"single\"` or `type=\"multiple\"`.\n\n**Incorrect (base):**\n\n```tsx\n<ToggleGroup type=\"single\" defaultValue=\"daily\">\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Correct (base):**\n\n```tsx\n// Single (no prop needed), defaultValue is always an array.\n<ToggleGroup defaultValue={[\"daily\"]} spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n</ToggleGroup>\n\n// Multi-selection.\n<ToggleGroup multiple>\n  <ToggleGroupItem value=\"bold\">Bold</ToggleGroupItem>\n  <ToggleGroupItem value=\"italic\">Italic</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Correct (radix):**\n\n```tsx\n// Single, defaultValue is a string.\n<ToggleGroup type=\"single\" defaultValue=\"daily\" spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n</ToggleGroup>\n\n// Multi-selection.\n<ToggleGroup type=\"multiple\">\n  <ToggleGroupItem value=\"bold\">Bold</ToggleGroupItem>\n  <ToggleGroupItem value=\"italic\">Italic</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Controlled single value:**\n\n```tsx\n// base — wrap/unwrap arrays.\nconst [value, setValue] = React.useState(\"normal\")\n<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>\n\n// radix — plain string.\nconst [value, setValue] = React.useState(\"normal\")\n<ToggleGroup type=\"single\" value={value} onValueChange={setValue}>\n```\n\n---\n\n## Slider\n\nBase accepts a plain number for a single thumb. Radix always requires an array.\n\n**Incorrect (base):**\n\n```tsx\n<Slider defaultValue={[50]} max={100} step={1} />\n```\n\n**Correct (base):**\n\n```tsx\n<Slider defaultValue={50} max={100} step={1} />\n```\n\n**Correct (radix):**\n\n```tsx\n<Slider defaultValue={[50]} max={100} step={1} />\n```\n\nBoth use arrays for range sliders. Controlled `onValueChange` in base may need a cast:\n\n```tsx\n// base.\nconst [value, setValue] = React.useState([0.3, 0.7])\n<Slider value={value} onValueChange={(v) => setValue(v as number[])} />\n\n// radix.\nconst [value, setValue] = React.useState([0.3, 0.7])\n<Slider value={value} onValueChange={setValue} />\n```\n\n---\n\n## Accordion\n\nRadix requires `type=\"single\"` or `type=\"multiple\"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.\n\n**Incorrect (base):**\n\n```tsx\n<Accordion type=\"single\" collapsible defaultValue=\"item-1\">\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n```\n\n**Correct (base):**\n\n```tsx\n<Accordion defaultValue={[\"item-1\"]}>\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n\n// Multi-select.\n<Accordion multiple defaultValue={[\"item-1\", \"item-2\"]}>\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n  <AccordionItem value=\"item-2\">...</AccordionItem>\n</Accordion>\n```\n\n**Correct (radix):**\n\n```tsx\n<Accordion type=\"single\" collapsible defaultValue=\"item-1\">\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n```\n",".agents/skills/shadcn/rules/chat.md":"# Chat & Messaging\n\nComponents for conversation and chat UI. Compose these instead of hand-rolling\nbubbles, scroll containers, dividers, or attachment cards.\n\nInstall: `npx shadcn@latest add message-scroller message bubble attachment marker`\n\nThe same component names and props ship for both `base` and `radix`; only\ncomposition differs (`render` vs `asChild`). See [base-vs-radix.md](./base-vs-radix.md).\n\n## Contents\n\n- Scrollable threads use MessageScroller\n- Message rows use Message\n- Message surfaces use Bubble\n- Attachments use Attachment\n- System notes and dividers use Marker\n- Streaming, anchoring, and jump-to-latest are built in\n- Escape hatch: the scroller hooks\n\n---\n\n## Scrollable threads use MessageScroller\n\nA conversation that scrolls, follows new messages, restores position, or jumps\nto a message uses `MessageScroller`. Don't build a raw overflow container with\nmanual scroll wiring, and don't reach for `ScrollArea`.\n\nThe parts nest in a fixed order. Every direct child of the content is wrapped in\na `MessageScrollerItem` so the scroller can measure, anchor, preserve position,\ntrack visibility, and jump to it. `MessageScrollerButton` sits inside\n`MessageScroller`, after the viewport.\n\n**Incorrect:**\n\n```tsx\n// Hand-rolled scroll container with manual stick-to-bottom logic.\n<div ref={scrollRef} onScroll={handleScroll} className=\"flex-1 overflow-y-auto\">\n  <div className=\"flex flex-col gap-6 p-4\">\n    {messages.map((m) => (\n      <ChatMessage key={m.id} message={m} />\n    ))}\n  </div>\n</div>\n```\n\n**Correct:**\n\n```tsx\n<MessageScrollerProvider autoScroll>\n  <MessageScroller>\n    <MessageScrollerViewport>\n      <MessageScrollerContent>\n        {messages.map((message) => (\n          <MessageScrollerItem\n            key={message.id}\n            messageId={message.id}\n            scrollAnchor={message.role === \"user\"}\n          >\n            <Message align={message.role === \"user\" ? \"end\" : \"start\"}>\n              {/* ...message content... */}\n            </Message>\n          </MessageScrollerItem>\n        ))}\n      </MessageScrollerContent>\n    </MessageScrollerViewport>\n    <MessageScrollerButton />\n  </MessageScroller>\n</MessageScrollerProvider>\n```\n\n---\n\n## Message rows use Message\n\n`Message` lays out a single row: avatar, header, content, footer, with\nalignment. Group consecutive rows from one sender with `MessageGroup`. Don't\nrebuild the row from flex divs.\n\n`align=\"end\"` is the current user's side; `align=\"start\"` is everyone else.\n\n```tsx\n<Message align=\"start\">\n  <MessageAvatar>\n    <Avatar>\n      <AvatarImage src={sender.avatar} alt={sender.name} />\n      <AvatarFallback>{initials}</AvatarFallback>\n    </Avatar>\n  </MessageAvatar>\n  <MessageContent>\n    <MessageHeader>{sender.name}</MessageHeader>\n    <Bubble>\n      <BubbleContent>{text}</BubbleContent>\n    </Bubble>\n    <MessageFooter>{time}</MessageFooter>\n  </MessageContent>\n</Message>\n```\n\n---\n\n## Message surfaces use Bubble\n\nThe colored message surface is `Bubble` + `BubbleContent`, never a styled `div`\nwith `bg-muted` / `bg-primary` and hand-managed corners.\n\n- `variant`: `default`, `secondary`, `muted`, `tinted`, `outline`, `ghost`, `destructive`.\n- `align`: `start` or `end` (matches the `Message` side).\n\n`BubbleReactions` renders the reaction cluster. `side` (`top` | `bottom`) and\n`align` (`start` | `end`) position it against the bubble. Don't lay reactions out\nwith absolutely-positioned `Badge`s.\n\n**Incorrect:**\n\n```tsx\n<div className=\"w-fit rounded-2xl bg-primary px-3 py-2 text-primary-foreground\">\n  {text}\n</div>\n```\n\n**Correct:**\n\n```tsx\n<Bubble variant=\"default\" align=\"end\">\n  <BubbleContent>{text}</BubbleContent>\n  <BubbleReactions side=\"bottom\" align=\"end\">\n    <Badge variant=\"secondary\">👍 2</Badge>\n  </BubbleReactions>\n</Bubble>\n```\n\n---\n\n## Attachments use Attachment\n\nFile and image attachments use `Attachment`, not `Item` or a custom card. It\ncarries upload state, so wire `state` to the real status rather than rendering a\nseparate spinner.\n\n- `state`: `idle`, `uploading`, `processing`, `error`, `done`. `uploading` and\n  `processing` apply the `shimmer` animation to the title automatically.\n- `size`: `default`, `sm`, `xs`. `orientation`: `horizontal`, `vertical`.\n- Use `AttachmentGroup` to lay out several attachments in a scrolling row.\n\n```tsx\n<Attachment state=\"done\">\n  <AttachmentMedia variant=\"icon\">\n    <FileTextIcon />\n  </AttachmentMedia>\n  <AttachmentContent>\n    <AttachmentTitle>homepage-feedback.pdf</AttachmentTitle>\n    <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>\n  </AttachmentContent>\n  <AttachmentActions>\n    <AttachmentAction>\n      <DownloadIcon />\n    </AttachmentAction>\n  </AttachmentActions>\n</Attachment>\n```\n\nFor an image, use `<AttachmentMedia variant=\"image\">` with an `img` child.\n\n---\n\n## System notes and dividers use Marker\n\nStatus lines (\"Sarah joined the conversation\"), date dividers (\"Today\"), and\nlabeled separators are `Marker`, not a `Separator` plus a centered span.\n\n- `variant`: `default` (plain row), `separator` (centered label with rules on\n  each side), `border` (bottom-bordered row).\n- `MarkerIcon` holds a leading icon; `MarkerContent` holds the label.\n\n**Incorrect:**\n\n```tsx\n<div className=\"flex items-center gap-3 py-2\">\n  <Separator className=\"flex-1\" />\n  <span className=\"text-xs text-muted-foreground\">Today</span>\n  <Separator className=\"flex-1\" />\n</div>\n```\n\n**Correct:**\n\n```tsx\n<Marker variant=\"separator\">\n  <MarkerContent>Today</MarkerContent>\n</Marker>\n```\n\n---\n\n## Streaming, anchoring, and jump-to-latest are built in\n\n`MessageScroller` handles the behavior that chat UIs usually reinvent. Don't\nwrite a `useStickToBottom` hook, a `ResizeObserver`, or manual `scrollTop` math.\n\n- **Follow the live edge while streaming.** `MessageScrollerProvider` with\n  `autoScroll` keeps the view pinned to new content and yields the moment the\n  user scrolls up. Streaming token updates that grow the last message are\n  followed automatically.\n- **Anchor a turn.** `scrollAnchor` on a `MessageScrollerItem` marks the row to\n  hold in view (typically the user's message that started the turn).\n- **Jump to latest.** `MessageScrollerButton` appears when the user scrolls away\n  and scrolls back on click. `direction=\"end\"` (default) or `direction=\"start\"`.\n  It is a self-managing control, so don't gate it behind your own scroll-position\n  state.\n\nFor a \"thinking…\" indicator while the model generates, apply the `shimmer`\nutility to text. Don't author a custom keyframe animation. See\n[styling.md](./styling.md).\n\n---\n\n## Escape hatch: the scroller hooks\n\nFor behavior the parts don't expose, read state from the hooks rather than\nre-implementing the scroller: `useMessageScroller`,\n`useMessageScrollerVisibility`, and `useMessageScrollerScrollable`. They come\nfrom the auto-installed `@shadcn/react` dependency, so there's nothing extra to\ninstall. Reach for them only when composition can't express what you need.\n",".agents/skills/shadcn/rules/composition.md":"# Component Composition\n\n## Contents\n\n- Items always inside their Group component\n- Callouts use Alert\n- Empty states use Empty component\n- Toast notifications use sonner\n- Choosing between overlay components\n- Dialog, Sheet, and Drawer always need a Title\n- Card structure\n- Button has no isPending or isLoading prop\n- TabsTrigger must be inside TabsList\n- Avatar always needs AvatarFallback\n- Use Separator instead of raw hr or border divs\n- Use Skeleton for loading placeholders\n- Use Badge instead of custom styled spans\n\n---\n\n## Items always inside their Group component\n\nNever render items directly inside the content container.\n\n**Incorrect:**\n\n```tsx\n<SelectContent>\n  <SelectItem value=\"apple\">Apple</SelectItem>\n  <SelectItem value=\"banana\">Banana</SelectItem>\n</SelectContent>\n```\n\n**Correct:**\n\n```tsx\n<SelectContent>\n  <SelectGroup>\n    <SelectItem value=\"apple\">Apple</SelectItem>\n    <SelectItem value=\"banana\">Banana</SelectItem>\n  </SelectGroup>\n</SelectContent>\n```\n\nThis applies to all group-based components:\n\n| Item | Group |\n|------|-------|\n| `SelectItem`, `SelectLabel` | `SelectGroup` |\n| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |\n| `MenubarItem` | `MenubarGroup` |\n| `ContextMenuItem` | `ContextMenuGroup` |\n| `CommandItem` | `CommandGroup` |\n| `MessageScrollerItem` | `MessageScrollerContent` |\n| `Message` (consecutive, same sender) | `MessageGroup` |\n| `Bubble` (stacked) | `BubbleGroup` |\n| `Attachment` (in a row) | `AttachmentGroup` |\n\nChat components nest in a fixed order (`MessageScrollerProvider` → `MessageScroller` → `MessageScrollerViewport` → `MessageScrollerContent` → `MessageScrollerItem`). See [chat.md](./chat.md).\n\n---\n\n## Callouts use Alert\n\n```tsx\n<Alert>\n  <AlertTitle>Warning</AlertTitle>\n  <AlertDescription>Something needs attention.</AlertDescription>\n</Alert>\n```\n\n---\n\n## Empty states use Empty component\n\n```tsx\n<Empty>\n  <EmptyHeader>\n    <EmptyMedia variant=\"icon\"><FolderIcon /></EmptyMedia>\n    <EmptyTitle>No projects yet</EmptyTitle>\n    <EmptyDescription>Get started by creating a new project.</EmptyDescription>\n  </EmptyHeader>\n  <EmptyContent>\n    <Button>Create Project</Button>\n  </EmptyContent>\n</Empty>\n```\n\n---\n\n## Toast notifications use sonner\n\n```tsx\nimport { toast } from \"sonner\"\n\ntoast.success(\"Changes saved.\")\ntoast.error(\"Something went wrong.\")\ntoast(\"File deleted.\", {\n  action: { label: \"Undo\", onClick: () => undoDelete() },\n})\n```\n\n---\n\n## Choosing between overlay components\n\n| Use case | Component |\n|----------|-----------|\n| Focused task that requires input | `Dialog` |\n| Destructive action confirmation | `AlertDialog` |\n| Side panel with details or filters | `Sheet` |\n| Mobile-first bottom panel | `Drawer` |\n| Quick info on hover | `HoverCard` |\n| Small contextual content on click | `Popover` |\n\n---\n\n## Dialog, Sheet, and Drawer always need a Title\n\n`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className=\"sr-only\"` if visually hidden.\n\n```tsx\n<DialogContent>\n  <DialogHeader>\n    <DialogTitle>Edit Profile</DialogTitle>\n    <DialogDescription>Update your profile.</DialogDescription>\n  </DialogHeader>\n  ...\n</DialogContent>\n```\n\n---\n\n## Card structure\n\nUse full composition — don't dump everything into `CardContent`:\n\n```tsx\n<Card>\n  <CardHeader>\n    <CardTitle>Team Members</CardTitle>\n    <CardDescription>Manage your team.</CardDescription>\n  </CardHeader>\n  <CardContent>...</CardContent>\n  <CardFooter>\n    <Button>Invite</Button>\n  </CardFooter>\n</Card>\n```\n\n---\n\n## Button has no isPending or isLoading prop\n\nCompose with `Spinner` + `data-icon` + `disabled`:\n\n```tsx\n<Button disabled>\n  <Spinner data-icon=\"inline-start\" />\n  Saving...\n</Button>\n```\n\n---\n\n## TabsTrigger must be inside TabsList\n\nNever render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:\n\n```tsx\n<Tabs defaultValue=\"account\">\n  <TabsList>\n    <TabsTrigger value=\"account\">Account</TabsTrigger>\n    <TabsTrigger value=\"password\">Password</TabsTrigger>\n  </TabsList>\n  <TabsContent value=\"account\">...</TabsContent>\n</Tabs>\n```\n\n---\n\n## Avatar always needs AvatarFallback\n\nAlways include `AvatarFallback` for when the image fails to load:\n\n```tsx\n<Avatar>\n  <AvatarImage src=\"/avatar.png\" alt=\"User\" />\n  <AvatarFallback>JD</AvatarFallback>\n</Avatar>\n```\n\n---\n\n## Use existing components instead of custom markup\n\n| Instead of | Use |\n|---|---|\n| `<hr>` or `<div className=\"border-t\">` | `<Separator />` |\n| `<div className=\"animate-pulse\">` with styled divs | `<Skeleton className=\"h-4 w-3/4\" />` |\n| `<span className=\"rounded-full bg-green-100 ...\">` | `<Badge variant=\"secondary\">` |\n",".agents/skills/shadcn/rules/forms.md":"# Forms & Inputs\n\n## Contents\n\n- Forms use FieldGroup + Field\n- InputGroup requires InputGroupInput/InputGroupTextarea\n- Buttons inside inputs use InputGroup + InputGroupAddon\n- Option sets (2–7 choices) use ToggleGroup\n- FieldSet + FieldLegend for grouping related fields\n- Field validation and disabled states\n\n---\n\n## Forms use FieldGroup + Field\n\nAlways use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:\n\n```tsx\n<FieldGroup>\n  <Field>\n    <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n    <Input id=\"email\" type=\"email\" />\n  </Field>\n  <Field>\n    <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n    <Input id=\"password\" type=\"password\" />\n  </Field>\n</FieldGroup>\n```\n\nUse `Field orientation=\"horizontal\"` for settings pages. Use `FieldLabel className=\"sr-only\"` for visually hidden labels.\n\n**Choosing form controls:**\n\n- Simple text input → `Input`\n- Dropdown with predefined options → `Select`\n- Searchable dropdown → `Combobox`\n- Native HTML select (no JS) → `native-select`\n- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)\n- Single choice from few options → `RadioGroup`\n- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`\n- OTP/verification code → `InputOTP`\n- Multi-line text → `Textarea`\n\n---\n\n## InputGroup requires InputGroupInput/InputGroupTextarea\n\nNever use raw `Input` or `Textarea` inside an `InputGroup`.\n\n**Incorrect:**\n\n```tsx\n<InputGroup>\n  <Input placeholder=\"Search...\" />\n</InputGroup>\n```\n\n**Correct:**\n\n```tsx\nimport { InputGroup, InputGroupInput } from \"@/components/ui/input-group\"\n\n<InputGroup>\n  <InputGroupInput placeholder=\"Search...\" />\n</InputGroup>\n```\n\n---\n\n## Buttons inside inputs use InputGroup + InputGroupAddon\n\nNever place a `Button` directly inside or adjacent to an `Input` with custom positioning.\n\n**Incorrect:**\n\n```tsx\n<div className=\"relative\">\n  <Input placeholder=\"Search...\" className=\"pr-10\" />\n  <Button className=\"absolute right-0 top-0\" size=\"icon\">\n    <SearchIcon />\n  </Button>\n</div>\n```\n\n**Correct:**\n\n```tsx\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"@/components/ui/input-group\"\n\n<InputGroup>\n  <InputGroupInput placeholder=\"Search...\" />\n  <InputGroupAddon>\n    <Button size=\"icon\">\n      <SearchIcon data-icon=\"inline-start\" />\n    </Button>\n  </InputGroupAddon>\n</InputGroup>\n```\n\n---\n\n## Option sets (2–7 choices) use ToggleGroup\n\nDon't manually loop `Button` components with active state.\n\n**Incorrect:**\n\n```tsx\nconst [selected, setSelected] = useState(\"daily\")\n\n<div className=\"flex gap-2\">\n  {[\"daily\", \"weekly\", \"monthly\"].map((option) => (\n    <Button\n      key={option}\n      variant={selected === option ? \"default\" : \"outline\"}\n      onClick={() => setSelected(option)}\n    >\n      {option}\n    </Button>\n  ))}\n</div>\n```\n\n**Correct:**\n\n```tsx\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\n\n<ToggleGroup spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n  <ToggleGroupItem value=\"monthly\">Monthly</ToggleGroupItem>\n</ToggleGroup>\n```\n\nCombine with `Field` for labelled toggle groups:\n\n```tsx\n<Field orientation=\"horizontal\">\n  <FieldTitle id=\"theme-label\">Theme</FieldTitle>\n  <ToggleGroup aria-labelledby=\"theme-label\" spacing={2}>\n    <ToggleGroupItem value=\"light\">Light</ToggleGroupItem>\n    <ToggleGroupItem value=\"dark\">Dark</ToggleGroupItem>\n    <ToggleGroupItem value=\"system\">System</ToggleGroupItem>\n  </ToggleGroup>\n</Field>\n```\n\n> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).\n\n---\n\n## FieldSet + FieldLegend for grouping related fields\n\nUse `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:\n\n```tsx\n<FieldSet>\n  <FieldLegend variant=\"label\">Preferences</FieldLegend>\n  <FieldDescription>Select all that apply.</FieldDescription>\n  <FieldGroup className=\"gap-3\">\n    <Field orientation=\"horizontal\">\n      <Checkbox id=\"dark\" />\n      <FieldLabel htmlFor=\"dark\" className=\"font-normal\">Dark mode</FieldLabel>\n    </Field>\n  </FieldGroup>\n</FieldSet>\n```\n\n---\n\n## Field validation and disabled states\n\nBoth attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.\n\n```tsx\n// Invalid.\n<Field data-invalid>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" aria-invalid />\n  <FieldDescription>Invalid email address.</FieldDescription>\n</Field>\n\n// Disabled.\n<Field data-disabled>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" disabled />\n</Field>\n```\n\nWorks for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.\n",".agents/skills/shadcn/rules/icons.md":"# Icons\n\n**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`.\n\n---\n\n## Icons in Button use data-icon attribute\n\nAdd `data-icon=\"inline-start\"` (prefix) or `data-icon=\"inline-end\"` (suffix) to the icon. No sizing classes on the icon.\n\n**Incorrect:**\n\n```tsx\n<Button>\n  <SearchIcon className=\"mr-2 size-4\" />\n  Search\n</Button>\n```\n\n**Correct:**\n\n```tsx\n<Button>\n  <SearchIcon data-icon=\"inline-start\"/>\n  Search\n</Button>\n\n<Button>\n  Next\n  <ArrowRightIcon data-icon=\"inline-end\"/>\n</Button>\n```\n\n---\n\n## No sizing classes on icons inside components\n\nComponents handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes.\n\n**Incorrect:**\n\n```tsx\n<Button>\n  <SearchIcon className=\"size-4\" data-icon=\"inline-start\" />\n  Search\n</Button>\n\n<DropdownMenuItem>\n  <SettingsIcon className=\"mr-2 size-4\" />\n  Settings\n</DropdownMenuItem>\n```\n\n**Correct:**\n\n```tsx\n<Button>\n  <SearchIcon data-icon=\"inline-start\" />\n  Search\n</Button>\n\n<DropdownMenuItem>\n  <SettingsIcon />\n  Settings\n</DropdownMenuItem>\n```\n\n---\n\n## Pass icons as component objects, not string keys\n\nUse `icon={CheckIcon}`, not a string key to a lookup map.\n\n**Incorrect:**\n\n```tsx\nconst iconMap = {\n  check: CheckIcon,\n  alert: AlertIcon,\n}\n\nfunction StatusBadge({ icon }: { icon: string }) {\n  const Icon = iconMap[icon]\n  return <Icon />\n}\n\n<StatusBadge icon=\"check\" />\n```\n\n**Correct:**\n\n```tsx\n// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).\nimport { CheckIcon } from \"lucide-react\"\n\nfunction StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {\n  return <Icon />\n}\n\n<StatusBadge icon={CheckIcon} />\n```\n",".agents/skills/shadcn/rules/styling.md":"# Styling & Customization\n\nSee [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.\n\n## Contents\n\n- Semantic colors\n- Built-in variants first\n- className for layout only\n- No space-x-* / space-y-*\n- Prefer size-* over w-* h-* when equal\n- Prefer truncate shorthand\n- No manual dark: color overrides\n- Use cn() for conditional classes\n- No manual z-index on overlay components\n- Use shimmer / scroll-fade utilities, not custom animations\n\n---\n\n## Semantic colors\n\n**Incorrect:**\n\n```tsx\n<div className=\"bg-blue-500 text-white\">\n  <p className=\"text-gray-600\">Secondary text</p>\n</div>\n```\n\n**Correct:**\n\n```tsx\n<div className=\"bg-primary text-primary-foreground\">\n  <p className=\"text-muted-foreground\">Secondary text</p>\n</div>\n```\n\n---\n\n## No raw color values for status/state indicators\n\nFor positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.\n\n**Incorrect:**\n\n```tsx\n<span className=\"text-emerald-600\">+20.1%</span>\n<span className=\"text-green-500\">Active</span>\n<span className=\"text-red-600\">-3.2%</span>\n```\n\n**Correct:**\n\n```tsx\n<Badge variant=\"secondary\">+20.1%</Badge>\n<Badge>Active</Badge>\n<span className=\"text-destructive\">-3.2%</span>\n```\n\nIf you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).\n\n---\n\n## Built-in variants first\n\n**Incorrect:**\n\n```tsx\n<Button className=\"border border-input bg-transparent hover:bg-accent\">\n  Click me\n</Button>\n```\n\n**Correct:**\n\n```tsx\n<Button variant=\"outline\">Click me</Button>\n```\n\n---\n\n## className for layout only\n\nUse `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.\n\n**Incorrect:**\n\n```tsx\n<Card className=\"bg-blue-100 text-blue-900 font-bold\">\n  <CardContent>Dashboard</CardContent>\n</Card>\n```\n\n**Correct:**\n\n```tsx\n<Card className=\"max-w-md mx-auto\">\n  <CardContent>Dashboard</CardContent>\n</Card>\n```\n\nTo customize a component's appearance, prefer these approaches in order:\n1. **Built-in variants** — `variant=\"outline\"`, `variant=\"destructive\"`, etc.\n2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.\n3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).\n\n---\n\n## No space-x-* / space-y-*\n\nUse `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.\n\n```tsx\n<div className=\"flex flex-col gap-4\">\n  <Input />\n  <Input />\n  <Button>Submit</Button>\n</div>\n```\n\n---\n\n## Prefer size-* over w-* h-* when equal\n\n`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.\n\n---\n\n## Prefer truncate shorthand\n\n`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.\n\n---\n\n## No manual dark: color overrides\n\nUse semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.\n\n---\n\n## Use cn() for conditional classes\n\nUse the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings.\n\n**Incorrect:**\n\n```tsx\n<div className={`flex items-center ${isActive ? \"bg-primary text-primary-foreground\" : \"bg-muted\"}`}>\n```\n\n**Correct:**\n\n```tsx\nimport { cn } from \"@/lib/utils\"\n\n<div className={cn(\"flex items-center\", isActive ? \"bg-primary text-primary-foreground\" : \"bg-muted\")}>\n```\n\n---\n\n## No manual z-index on overlay components\n\n`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.\n\n---\n\n## Use shimmer / scroll-fade utilities, not custom animations\n\nFor a live \"thinking…\" or loading-text shimmer, apply the `shimmer` utility. Don't author a custom `@keyframes` or a `bg-clip-text` gradient sweep.\n\nFor scroll-aware edge fading on a scroll container, use `scroll-fade` (and the axis variants `scroll-fade-x` / `scroll-fade-b`). Don't hand-roll mask gradients. The chat components already apply these internally: `Attachment` shimmers its title during upload, and `MessageScrollerViewport` fades its edges.\n\n**Incorrect:**\n\n```tsx\n<span className=\"animate-pulse bg-gradient-to-r from-muted-foreground/40 via-foreground/70 to-muted-foreground/40 bg-clip-text text-transparent [animation:shimmer_1.6s_infinite]\">\n  Thinking…\n</span>\n```\n\n**Correct:**\n\n```tsx\n<span className=\"shimmer\">Thinking…</span>\n```\n",".agents/skills/tanstack-form/SKILL.md":"---\nname: tanstack-form\ndescription: Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.\n---\n\n## Overview\n\nTanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).\n\n**Package:** `@tanstack/react-form`\n**Adapters:** `@tanstack/zod-form-adapter`, `@tanstack/valibot-form-adapter`\n**Status:** Stable (v1)\n\n## Installation\n\n```bash\nnpm install @tanstack/react-form\n# Optional schema adapters:\nnpm install @tanstack/zod-form-adapter zod\nnpm install @tanstack/valibot-form-adapter valibot\n```\n\n## Core: useForm\n\n```tsx\nimport { useForm } from '@tanstack/react-form';\n\nfunction MyForm() {\n  const form = useForm({\n    defaultValues: {\n      firstName: '',\n      lastName: '',\n      email: '',\n      age: 0\n    },\n    onSubmit: async ({ value }) => {\n      // value is fully typed\n      await submitToServer(value);\n    },\n    onSubmitInvalid: ({ value, formApi }) => {\n      console.log('Validation failed:', formApi.state.errors);\n    }\n  });\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n        form.handleSubmit();\n      }}\n    >\n      {/* Fields */}\n      <form.Subscribe\n        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}\n        children={({ canSubmit, isSubmitting }) => (\n          <button type='submit' disabled={!canSubmit}>\n            {isSubmitting ? 'Submitting...' : 'Submit'}\n          </button>\n        )}\n      />\n    </form>\n  );\n}\n```\n\n## Fields (form.Field)\n\n```tsx\n<form.Field\n  name=\"firstName\"\n  validators={{\n    onChange: ({ value }) =>\n      value.length < 3 ? 'Must be at least 3 characters' : undefined,\n  }}\n  children={(field) => (\n    <div>\n      <label htmlFor={field.name}>First Name</label>\n      <input\n        id={field.name}\n        name={field.name}\n        value={field.state.value}\n        onBlur={field.handleBlur}\n        onChange={(e) => field.handleChange(e.target.value)}\n      />\n      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (\n        <em>{field.state.meta.errors.join(', ')}</em>\n      )}\n    </div>\n  )}\n/>\n\n<!-- Nested fields use dot notation -->\n<form.Field name=\"address.city\">\n  {(field) => (\n    <input\n      value={field.state.value}\n      onChange={(e) => field.handleChange(e.target.value)}\n      onBlur={field.handleBlur}\n    />\n  )}\n</form.Field>\n```\n\n## Validation\n\n### Validation Timing\n\n| Cause      | When                     |\n| ---------- | ------------------------ |\n| `onChange` | After every value change |\n| `onBlur`   | When field loses focus   |\n| `onSubmit` | During submission        |\n| `onMount`  | When field mounts        |\n\n### Synchronous Validation\n\n```tsx\n<form.Field\n  name='age'\n  validators={{\n    onChange: ({ value }) => {\n      if (value < 18) return 'Must be 18 or older';\n      return undefined; // undefined = valid\n    },\n    onBlur: ({ value }) => {\n      if (!value) return 'Required';\n      return undefined;\n    }\n  }}\n/>\n```\n\n### Asynchronous Validation\n\n```tsx\n<form.Field\n  name='username'\n  asyncDebounceMs={500}\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const res = await fetch(`/api/check-username?q=${value}`);\n      const { available } = await res.json();\n      if (!available) return 'Username taken';\n      return undefined;\n    }\n  }}\n>\n  {(field) => (\n    <>\n      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />\n      {field.state.meta.isValidating && <span>Checking...</span>}\n    </>\n  )}\n</form.Field>\n```\n\n### Schema Validation (Zod)\n\n```tsx\nimport { zodValidator } from '@tanstack/zod-form-adapter'\nimport { z } from 'zod'\n\nconst form = useForm({\n  defaultValues: { email: '', age: 0 },\n  validatorAdapter: zodValidator(),\n  onSubmit: async ({ value }) => { /* ... */ },\n})\n\n<form.Field\n  name=\"email\"\n  validators={{\n    onChange: z.string().email('Invalid email'),\n    onBlur: z.string().min(1, 'Required'),\n  }}\n/>\n\n<form.Field\n  name=\"age\"\n  validators={{\n    onChange: z.number().min(18, 'Must be 18+'),\n  }}\n/>\n```\n\n### Form-Level Validation\n\n```tsx\nconst form = useForm({\n  defaultValues: { password: '', confirmPassword: '' },\n  validators: {\n    onChange: ({ value }) => {\n      if (value.password !== value.confirmPassword) {\n        return 'Passwords do not match';\n      }\n      return undefined;\n    }\n  }\n});\n```\n\n### Linked/Dependent Fields\n\n```tsx\n<form.Field\n  name='confirmPassword'\n  validators={{\n    onChangeListenTo: ['password'], // Re-validate when password changes\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      if (value !== password) return 'Passwords do not match';\n      return undefined;\n    }\n  }}\n/>\n```\n\n## Array Fields\n\n```tsx\n<form.Field name='people' mode='array'>\n  {(field) => (\n    <div>\n      {field.state.value.map((_, index) => (\n        <div key={index}>\n          <form.Field name={`people[${index}].name`}>\n            {(subField) => (\n              <input\n                value={subField.state.value}\n                onChange={(e) => subField.handleChange(e.target.value)}\n              />\n            )}\n          </form.Field>\n          <button type='button' onClick={() => field.removeValue(index)}>\n            Remove\n          </button>\n        </div>\n      ))}\n      <button type='button' onClick={() => field.pushValue({ name: '', age: 0 })}>\n        Add Person\n      </button>\n    </div>\n  )}\n</form.Field>\n```\n\n### Array Methods\n\n```typescript\nfield.pushValue(item); // Add to end\nfield.insertValue(index, item); // Insert at index\nfield.replaceValue(index, item); // Replace at index\nfield.removeValue(index); // Remove at index\nfield.swapValues(indexA, indexB); // Swap positions\nfield.moveValue(from, to); // Move position\n```\n\n## Listeners (Side Effects)\n\n```tsx\n<form.Field\n  name='country'\n  listeners={{\n    onChange: ({ value }) => {\n      // Side effect: reset dependent fields\n      form.setFieldValue('state', '');\n      form.setFieldValue('postalCode', '');\n    }\n  }}\n/>\n```\n\n## Reactivity (form.Subscribe & useStore)\n\n```tsx\n// Render-prop subscription (fine-grained)\n<form.Subscribe\n  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}\n  children={({ canSubmit, isDirty }) => (\n    <div>\n      {isDirty && <span>Unsaved changes</span>}\n      <button disabled={!canSubmit}>Save</button>\n    </div>\n  )}\n/>;\n\n// Hook-based subscription\nfunction FormStatus() {\n  const isValid = form.useStore((s) => s.isValid);\n  return isValid ? null : <p>Fix errors</p>;\n}\n```\n\n## Form State\n\n```typescript\ninterface FormState {\n  values: TFormData;\n  errors: ValidationError[];\n  errorMap: Record<string, ValidationError>;\n  isFormValid: boolean;\n  isFieldsValid: boolean;\n  isValid: boolean; // isFormValid && isFieldsValid\n  isTouched: boolean;\n  isPristine: boolean;\n  isDirty: boolean;\n  isSubmitting: boolean;\n  isSubmitted: boolean;\n  isSubmitSuccessful: boolean;\n  submissionAttempts: number;\n  canSubmit: boolean; // isValid && !isSubmitting\n}\n```\n\n## Field State\n\n```typescript\ninterface FieldState<TData> {\n  value: TData;\n  meta: {\n    isTouched: boolean;\n    isDirty: boolean;\n    isPristine: boolean;\n    isValidating: boolean;\n    errors: ValidationError[];\n    errorMap: Record<ValidationCause, ValidationError>;\n  };\n}\n```\n\n## FormApi Methods\n\n```typescript\nform.handleSubmit();\nform.reset();\nform.getFieldValue(field);\nform.setFieldValue(field, value);\nform.getFieldMeta(field);\nform.setFieldMeta(field, updater);\nform.validateAllFields(cause);\nform.validateField(field, cause);\nform.deleteField(field);\n```\n\n## Shared Form Options (formOptions)\n\n```tsx\nimport { formOptions } from '@tanstack/react-form';\n\nconst sharedOpts = formOptions({\n  defaultValues: { firstName: '', lastName: '' }\n});\n\n// Reuse across components\nconst form = useForm({\n  ...sharedOpts,\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n## Server-Side Validation\n\n```tsx\n// TanStack Start / Next.js server action\nimport { ServerValidateError } from '@tanstack/react-form/nextjs';\n\nexport async function validateForm(data: FormData) {\n  const email = data.get('email') as string;\n  if (await checkEmailExists(email)) {\n    throw new ServerValidateError({\n      form: 'Submission failed',\n      fields: { email: 'Email already registered' }\n    });\n  }\n}\n```\n\n## TypeScript Integration\n\n```tsx\n// Type-safe field paths with DeepKeys\ninterface UserForm {\n  name: string\n  address: { street: string; city: string }\n  tags: string[]\n  contacts: Array<{ name: string; phone: string }>\n}\n\n// TypeScript auto-completes all valid paths:\n// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'\n<form.Field name=\"address.city\" />     // OK\n<form.Field name=\"nonexistent\" />       // Type Error!\n```\n\n## Best Practices\n\n1. **Always call `e.preventDefault()` and `e.stopPropagation()`** on form submit\n2. **Always attach `onBlur={field.handleBlur}`** for blur validation and isTouched tracking\n3. **Use `mode=\"array\"`** for array fields to get array methods\n4. **Return `undefined`** (not null/false) for valid validators\n5. **Use `asyncDebounceMs`** for async validators to prevent API spam\n6. **Check `isTouched` before showing errors** for better UX\n7. **Use `form.Subscribe` with selectors** to minimize re-renders\n8. **Use `formOptions`** for shared configuration across components\n9. **Use schema validators** (Zod/Valibot) for complex validation rules\n10. **Use `onChangeListenTo`** for cross-field validation dependencies\n\n## Common Pitfalls\n\n- Forgetting `e.preventDefault()` on form submit (causes page reload)\n- Not attaching `onBlur` to inputs (breaks blur validation and isTouched)\n- Returning `null` or `false` instead of `undefined` for valid fields\n- Using `mode=\"array\"` incorrectly (only needed on the array field itself, not sub-fields)\n- Subscribing to entire form state instead of using selectors (unnecessary re-renders)\n- Not using `asyncDebounceMs` with async validators (fires on every keystroke)\n",".agents/skills/tanstack-query/SKILL.md":"---\nname: tanstack-query\ndescription: TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration. Use when fetching data, managing server state, or working with TanStack Query hooks.\n---\n\n# TanStack Query Patterns\n\n## Purpose\n\nModern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.\n\n**Note**: v5 (released October 2023) has breaking changes from v4:\n\n- `isLoading` → `isPending` for status\n- `cacheTime` → `gcTime` (garbage collection time)\n- React 18.0+ required\n- Callbacks removed from useQuery (onError, onSuccess, onSettled)\n- `keepPreviousData` replaced with `placeholderData` function\n\n## When to Use This Skill\n\n- Fetching data with TanStack Query\n- Using useSuspenseQuery or useQuery\n- Managing mutations\n- Cache invalidation and updates\n- API service patterns\n\n---\n\n## Quick Start\n\n### Primary Pattern: useSuspenseQuery\n\nFor **all new components**, use `useSuspenseQuery`:\n\n```typescript\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { postsApi } from '~/features/posts/api/postsApi';\n\nfunction PostList() {\n  const { data: posts } = useSuspenseQuery({\n    queryKey: ['posts'],\n    queryFn: postsApi.getAll,\n  });\n\n  return (\n    <div>\n      {posts.map(post => (\n        <PostCard key={post.id} post={post} />\n      ))}\n    </div>\n  );\n}\n\n// Wrap with Suspense\n<Suspense fallback={<PostsSkeleton />}>\n  <PostList />\n</Suspense>\n```\n\n**Benefits:**\n\n- No `isLoading` checks needed\n- Integrates with Suspense boundaries\n- Cleaner component code\n- Consistent loading UX\n\n---\n\n## useSuspenseQuery Patterns\n\n### Basic Usage\n\n```typescript\nconst { data } = useSuspenseQuery({\n  queryKey: ['user', userId],\n  queryFn: () => userApi.get(userId),\n});\n\n// data is never undefined - guaranteed by Suspense\nreturn <div>{data.name}</div>;\n```\n\n### With Parameters\n\n```typescript\nfunction UserPosts({ userId }: { userId: string }) {\n  const { data: posts } = useSuspenseQuery({\n    queryKey: ['users', userId, 'posts'],\n    queryFn: () => postsApi.getByUser(userId),\n  });\n\n  return <div>{posts.length} posts</div>;\n}\n```\n\n### Dependent Queries\n\n```typescript\nfunction PostDetails({ postId }: { postId: string }) {\n  // First query\n  const { data: post } = useSuspenseQuery({\n    queryKey: ['posts', postId],\n    queryFn: () => postsApi.get(postId),\n  });\n\n  // Second query depends on first\n  const { data: author } = useSuspenseQuery({\n    queryKey: ['users', post.authorId],\n    queryFn: () => userApi.get(post.authorId),\n  });\n\n  return <div>{author.name} wrote {post.title}</div>;\n}\n```\n\n---\n\n## useQuery (Legacy Pattern)\n\nUse `useQuery` only when you need loading/error states in the component:\n\n```typescript\nimport { useQuery } from '@tanstack/react-query';\n\nfunction Component() {\n  const { data, isPending, error } = useQuery({\n    queryKey: ['posts'],\n    queryFn: postsApi.getAll,\n  });\n\n  if (isPending) return <Spinner />;\n  if (error) return <Error error={error} />;\n\n  return <div>{data.map(...)}</div>;\n}\n```\n\n**When to use `useQuery` vs `useSuspenseQuery`:**\n\n- Use `useSuspenseQuery` by default (preferred)\n- Use `useQuery` only when you need component-level loading states\n- Most cases should use `useSuspenseQuery` + Suspense boundaries\n\n---\n\n## Mutations\n\n### Basic Mutation\n\n```typescript\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\n\nfunction CreatePostButton() {\n  const queryClient = useQueryClient();\n\n  const mutation = useMutation({\n    mutationFn: postsApi.create,\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: ['posts'] });\n    },\n  });\n\n  const handleCreate = () => {\n    mutation.mutate({\n      title: 'New Post',\n      content: 'Content here',\n    });\n  };\n\n  return (\n    <button onClick={handleCreate} disabled={mutation.isPending}>\n      {mutation.isPending ? 'Creating...' : 'Create Post'}\n    </button>\n  );\n}\n```\n\n### Optimistic Updates\n\n```typescript\nconst mutation = useMutation({\n  mutationFn: postsApi.update,\n  onMutate: async (updatedPost) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });\n\n    // Snapshot previous value\n    const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['posts', updatedPost.id], updatedPost);\n\n    // Return context with snapshot\n    return { previousPost };\n  },\n  onError: (err, updatedPost, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['posts', updatedPost.id], context.previousPost);\n  },\n  onSettled: (data, error, variables) => {\n    // Refetch after mutation\n    queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });\n  }\n});\n```\n\n---\n\n## Cache Management\n\n### Invalidation\n\n```typescript\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\n// Invalidate all posts queries\nqueryClient.invalidateQueries({ queryKey: ['posts'] });\n\n// Invalidate specific post\nqueryClient.invalidateQueries({ queryKey: ['posts', postId] });\n\n// Invalidate all queries\nqueryClient.invalidateQueries();\n```\n\n### Manual Updates\n\n```typescript\n// Update cache directly\nqueryClient.setQueryData(['posts', postId], newPost);\n\n// Update with function\nqueryClient.setQueryData(['posts'], (oldPosts) => [...oldPosts, newPost]);\n```\n\n### Prefetching\n\n```typescript\n// Prefetch data\nawait queryClient.prefetchQuery({\n  queryKey: ['posts', postId],\n  queryFn: () => postsApi.get(postId),\n});\n\n// In a component\nconst prefetchPost = (postId: string) => {\n  queryClient.prefetchQuery({\n    queryKey: ['posts', postId],\n    queryFn: () => postsApi.get(postId),\n  });\n};\n\n<Link\n  to={`/posts/${post.id}`}\n  onMouseEnter={() => prefetchPost(post.id)}\n>\n  {post.title}\n</Link>\n```\n\n---\n\n## API Service Pattern\n\n### Centralized API Service\n\n```typescript\n// features/posts/api/postsApi.ts\nimport { apiClient } from '@/lib/apiClient';\nimport type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';\n\nexport const postsApi = {\n  getAll: async (): Promise<Post[]> => {\n    const response = await apiClient.get('/posts');\n    return response.data;\n  },\n\n  get: async (id: string): Promise<Post> => {\n    const response = await apiClient.get(`/posts/${id}`);\n    return response.data;\n  },\n\n  create: async (data: CreatePostDto): Promise<Post> => {\n    const response = await apiClient.post('/posts', data);\n    return response.data;\n  },\n\n  update: async (id: string, data: UpdatePostDto): Promise<Post> => {\n    const response = await apiClient.put(`/posts/${id}`, data);\n    return response.data;\n  },\n\n  delete: async (id: string): Promise<void> => {\n    await apiClient.delete(`/posts/${id}`);\n  },\n\n  getByUser: async (userId: string): Promise<Post[]> => {\n    const response = await apiClient.get(`/users/${userId}/posts`);\n    return response.data;\n  }\n};\n```\n\n### Usage in Components\n\n```typescript\nimport { postsApi } from '~/features/posts/api/postsApi';\n\n// In query\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll\n});\n\n// In mutation\nconst mutation = useMutation({\n  mutationFn: postsApi.create\n});\n```\n\n---\n\n## Query Keys\n\n### Key Structure\n\n```typescript\n// List queries\n['posts'][('posts', { status: 'published' })][ // All posts // Filtered posts\n  // Detail queries\n  ('posts', postId)\n][('posts', postId, 'comments')][ // Single post // Post comments\n  // Nested resources\n  ('users', userId, 'posts')\n][('users', userId, 'posts', postId)]; // User's posts // Specific user post\n```\n\n### Key Factories\n\n```typescript\n// features/posts/api/postKeys.ts\nexport const postKeys = {\n  all: ['posts'] as const,\n  lists: () => [...postKeys.all, 'list'] as const,\n  list: (filters: string) => [...postKeys.lists(), { filters }] as const,\n  details: () => [...postKeys.all, 'detail'] as const,\n  detail: (id: string) => [...postKeys.details(), id] as const,\n  comments: (id: string) => [...postKeys.detail(id), 'comments'] as const\n};\n\n// Usage\nconst { data } = useSuspenseQuery({\n  queryKey: postKeys.detail(postId),\n  queryFn: () => postsApi.get(postId)\n});\n\n// Invalidate all post lists\nqueryClient.invalidateQueries({ queryKey: postKeys.lists() });\n```\n\n---\n\n## Error Handling\n\n### With Error Boundaries\n\n```typescript\nimport { ErrorBoundary } from 'react-error-boundary';\n\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <Suspense fallback={<Loading />}>\n    <DataComponent />\n  </Suspense>\n</ErrorBoundary>\n\n// In component\nfunction DataComponent() {\n  const { data } = useSuspenseQuery({\n    queryKey: ['data'],\n    queryFn: fetchData,\n    // Errors automatically caught by ErrorBoundary\n  });\n\n  return <div>{data}</div>;\n}\n```\n\n### Retry and Cache Configuration\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll,\n  retry: 3, // Retry 3 times\n  retryDelay: 1000, // Wait 1s between retries\n  gcTime: 5 * 60 * 1000 // Garbage collection time: 5 minutes (v5: was 'cacheTime')\n});\n```\n\n---\n\n## Best Practices\n\n### 1. Use Suspense by Default\n\n```typescript\n// ✅ Good: useSuspenseQuery + Suspense\n<Suspense fallback={<Skeleton />}>\n  <DataComponent />\n</Suspense>\n\nfunction DataComponent() {\n  const { data } = useSuspenseQuery({...});\n  return <div>{data}</div>;\n}\n\n// ❌ Avoid: useQuery with manual loading\nfunction DataComponent() {\n  const { data, isPending } = useQuery({...});\n  if (isPending) return <Spinner />;\n  return <div>{data}</div>;\n}\n```\n\n### 2. Consistent Query Keys\n\n```typescript\n// ✅ Good: Use key factories\nconst { data } = useSuspenseQuery({\n  queryKey: postKeys.detail(id),\n  queryFn: () => postsApi.get(id)\n});\n\n// ❌ Avoid: Inconsistent keys\nconst { data } = useSuspenseQuery({\n  queryKey: ['post', id], // Different format\n  queryFn: () => postsApi.get(id)\n});\n```\n\n### 3. Centralized API Services\n\n```typescript\n// ✅ Good: API service\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll\n});\n\n// ❌ Avoid: Inline fetching\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: async () => {\n    const res = await fetch('/api/posts');\n    return res.json();\n  }\n});\n```\n\n---\n\n## Additional Resources\n\nFor more patterns, see:\n\n- [data-fetching.md](resources/data-fetching.md) - Advanced patterns\n- [cache-strategies.md](resources/cache-strategies.md) - Cache management\n- [mutation-patterns.md](resources/mutation-patterns.md) - Complex mutations\n",".agents/skills/tanstack-query/resources/cache-strategies.md":"# Cache Management Strategies\n\n## Cache Time Configuration\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes\n  gcTime: 10 * 60 * 1000 // Keep in cache for 10 minutes (formerly cacheTime)\n});\n```\n\n## Cache Invalidation\n\n### Invalidate Specific Queries\n\n```typescript\nconst queryClient = useQueryClient();\n\n// Invalidate all post queries\nqueryClient.invalidateQueries({ queryKey: ['posts'] });\n\n// Invalidate specific post\nqueryClient.invalidateQueries({ queryKey: ['post', postId] });\n\n// Invalidate with exact match\nqueryClient.invalidateQueries({\n  queryKey: ['posts'],\n  exact: true // Only ['posts'], not ['posts', 'list']\n});\n```\n\n### Invalidate on Mutation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n  }\n});\n```\n\n## Manual Cache Updates\n\n### Set Query Data\n\n```typescript\n// Update cache directly\nqueryClient.setQueryData(['post', postId], (oldData) => ({\n  ...oldData,\n  title: 'New Title'\n}));\n\n// Set new data\nqueryClient.setQueryData(['post', postId], newPost);\n```\n\n### Get Query Data\n\n```typescript\n// Read from cache\nconst cachedPost = queryClient.getQueryData(['post', postId]);\n\n// Use in initialData\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  initialData: () => queryClient.getQueryData(['posts'])?.find((p) => p.id === postId)\n});\n```\n\n## Refetch Strategies\n\n### Refetch on Window Focus\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  refetchOnWindowFocus: true // Refetch when tab regains focus\n});\n```\n\n### Refetch on Reconnect\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  refetchOnReconnect: true // Refetch when internet reconnects\n});\n```\n\n### Refetch Intervals\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['live-data'],\n  queryFn: fetchLiveData,\n  refetchInterval: 5000, // Refetch every 5 seconds\n  refetchIntervalInBackground: false // Pause when tab not active\n});\n```\n\n## Cache Persistence\n\n### Persist to localStorage\n\n```typescript\nimport { QueryClient } from '@tanstack/react-query';\nimport { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';\nimport { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      gcTime: 1000 * 60 * 60 * 24  // 24 hours\n    }\n  }\n});\n\nconst persister = createSyncStoragePersister({\n  storage: window.localStorage\n});\n\n<PersistQueryClientProvider\n  client={queryClient}\n  persister={persister}\n>\n  <App />\n</PersistQueryClientProvider>\n```\n\n## Cache Deduplication\n\n### Automatic Request Deduplication\n\n```typescript\n// Both components will share the same request\nfunction Component1() {\n  const { data } = useQuery({\n    queryKey: ['posts'],\n    queryFn: fetchPosts\n  });\n}\n\nfunction Component2() {\n  const { data } = useQuery({\n    queryKey: ['posts'], // Same key = same request\n    queryFn: fetchPosts\n  });\n}\n```\n\n## Cache Preloading\n\n### Prefetch Queries\n\n```typescript\nconst queryClient = useQueryClient();\n\n// Prefetch before navigation\nconst handleMouseEnter = () => {\n  queryClient.prefetchQuery({\n    queryKey: ['post', postId],\n    queryFn: () => fetchPost(postId)\n  });\n};\n\n// Prefetch in loader\nrouter.beforeEach(async (to, from, next) => {\n  await queryClient.prefetchQuery({\n    queryKey: ['user', to.params.userId],\n    queryFn: () => fetchUser(to.params.userId)\n  });\n  next();\n});\n```\n\n### Ensure Query Data\n\n```typescript\n// Fetch if not in cache, otherwise use cached\nawait queryClient.ensureQueryData({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId)\n});\n```\n\n## Selective Cache Updates\n\n### Update Nested Data\n\n```typescript\nqueryClient.setQueryData(['posts'], (oldPosts) => {\n  return oldPosts.map((post) => (post.id === updatedPost.id ? updatedPost : post));\n});\n```\n\n### Add to List Cache\n\n```typescript\n// After creating a post\nqueryClient.setQueryData(['posts'], (oldPosts = []) => {\n  return [newPost, ...oldPosts];\n});\n```\n\n### Remove from List Cache\n\n```typescript\n// After deleting a post\nqueryClient.setQueryData(['posts'], (oldPosts) => {\n  return oldPosts.filter((post) => post.id !== deletedPostId);\n});\n```\n\n## Cache Debugging\n\n### React Query Devtools\n\n```typescript\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\n\n<QueryClientProvider client={queryClient}>\n  <App />\n  <ReactQueryDevtools initialIsOpen={false} />\n</QueryClientProvider>\n```\n\n### Query Cache Events\n\n```typescript\nconst queryCache = queryClient.getQueryCache();\n\nqueryCache.subscribe((event) => {\n  console.log('Query cache event:', event.type, event.query.queryKey);\n});\n```\n\n## Best Practices\n\n1. **Set Appropriate staleTime** - Balance freshness vs performance\n2. **Use Invalidation Over Refetch** - Let queries refetch when needed\n3. **Prefetch Predictably** - Preload data on hover/intent\n4. **Update Cache on Mutations** - Keep UI in sync\n5. **Use Devtools** - Debug cache issues visually\n6. **Persist Important Data** - Save to localStorage for offline support\n7. **Deduplicate Requests** - Rely on automatic deduplication\n",".agents/skills/tanstack-query/resources/data-fetching.md":"# Advanced Data Fetching Patterns with TanStack Query\n\n## Dependent Queries\n\nQueries that depend on data from other queries:\n\n```typescript\n// First query - Get user ID\nconst { data: user } = useQuery({\n  queryKey: ['user'],\n  queryFn: fetchCurrentUser\n});\n\n// Second query - Depends on user ID\nconst { data: posts } = useQuery({\n  queryKey: ['posts', user?.id],\n  queryFn: () => fetchUserPosts(user!.id),\n  enabled: !!user // Only run when user is available\n});\n```\n\n## Parallel Queries\n\nFetch multiple independent queries simultaneously:\n\n```typescript\nfunction Dashboard() {\n  const queries = useQueries({\n    queries: [\n      { queryKey: ['stats'], queryFn: fetchStats },\n      { queryKey: ['recentPosts'], queryFn: fetchRecentPosts },\n      { queryKey: ['notifications'], queryFn: fetchNotifications }\n    ]\n  });\n\n  const [statsQuery, postsQuery, notificationsQuery] = queries;\n\n  if (queries.some(q => q.isLoading)) return <Loading />;\n\n  return <Dashboard\n    stats={statsQuery.data}\n    posts={postsQuery.data}\n    notifications={notificationsQuery.data}\n  />;\n}\n```\n\n## Infinite Queries\n\nFor pagination and infinite scroll:\n\n```typescript\nconst {\n  data,\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage\n} = useInfiniteQuery({\n  queryKey: ['posts'],\n  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),\n  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,\n  initialPageParam: 1\n});\n\n// Flatten pages\nconst allPosts = data?.pages.flatMap(page => page.posts) ?? [];\n\nreturn (\n  <div>\n    {allPosts.map(post => <PostCard key={post.id} post={post} />)}\n    {hasNextPage && (\n      <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>\n        {isFetchingNextPage ? 'Loading...' : 'Load More'}\n      </button>\n    )}\n  </div>\n);\n```\n\n## Prefetching\n\nPreload data before it's needed:\n\n```typescript\nimport { useQueryClient } from '@tanstack/react-query';\n\nfunction PostLink({ postId }: { postId: string }) {\n  const queryClient = useQueryClient();\n\n  const handleMouseEnter = () => {\n    queryClient.prefetchQuery({\n      queryKey: ['post', postId],\n      queryFn: () => fetchPost(postId)\n    });\n  };\n\n  return (\n    <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>\n      View Post\n    </Link>\n  );\n}\n```\n\n## Suspense Mode\n\nUse with React Suspense:\n\n```typescript\nimport { useSuspenseQuery } from '@tanstack/react-query';\n\nfunction PostDetails({ postId }: { postId: string }) {\n  // Throws promise on loading, error on error\n  const { data: post } = useSuspenseQuery({\n    queryKey: ['post', postId],\n    queryFn: () => fetchPost(postId)\n  });\n\n  return <div>{post.title}</div>;\n}\n\n// Wrap with Suspense\n<Suspense fallback={<Loading />}>\n  <PostDetails postId=\"123\" />\n</Suspense>\n```\n\n## Query Cancellation\n\nCancel queries when component unmounts:\n\n```typescript\nconst { data, isLoading } = useQuery({\n  queryKey: ['search', searchTerm],\n  queryFn: async ({ signal }) => {\n    const response = await fetch(`/api/search?q=${searchTerm}`, { signal });\n    return response.json();\n  }\n});\n```\n\n## Initial Data\n\nProvide initial data to avoid loading state:\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  initialData: () => {\n    // Get from cache or other source\n    return queryClient.getQueryData(['posts'])?.find((post) => post.id === postId);\n  }\n});\n```\n\n## Placeholder Data\n\nShow placeholder while loading:\n\n```typescript\nconst { data, isPlaceholderData } = useQuery({\n  queryKey: ['posts', page],\n  queryFn: () => fetchPosts(page),\n  placeholderData: (previousData) => previousData  // Keep previous page while loading\n});\n\n// Or provide static placeholder\nplaceholderData: { posts: [], total: 0 }\n```\n\n## Optimistic Updates with Queries\n\nUpdate UI immediately, rollback on error:\n\n```typescript\nconst queryClient = useQueryClient();\n\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (newPost) => {\n    // Cancel outgoing queries\n    await queryClient.cancelQueries({ queryKey: ['post', newPost.id] });\n\n    // Snapshot current value\n    const previousPost = queryClient.getQueryData(['post', newPost.id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['post', newPost.id], newPost);\n\n    return { previousPost };\n  },\n  onError: (err, newPost, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['post', newPost.id], context?.previousPost);\n  },\n  onSettled: (newPost) => {\n    // Refetch after success or error\n    queryClient.invalidateQueries({ queryKey: ['post', newPost.id] });\n  }\n});\n```\n\n## Query Retries\n\nConfigure retry behavior:\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  retry: 3, // Retry 3 times\n  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff\n});\n```\n\n## Error Handling\n\nHandle query errors:\n\n```typescript\nconst { data, error, isError } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  throwOnError: false  // Don't throw, just set error\n});\n\nif (isError) {\n  return <ErrorMessage error={error} />;\n}\n```\n\n## Best Practices\n\n1. **Use Suspense** - Better loading UX with React Suspense\n2. **Prefetch on Intent** - Preload data on hover/focus\n3. **Enable Queries Conditionally** - Use `enabled` option\n4. **Cancel on Unmount** - Use abort signals\n5. **Handle Errors Gracefully** - Show error states\n6. **Optimize with Placeholders** - Show previous data while loading\n",".agents/skills/tanstack-query/resources/mutation-patterns.md":"# Complex Mutation Patterns\n\n## Basic Mutations\n\n```typescript\nconst { mutate, isPending, isError, error } = useMutation({\n  mutationFn: (newPost: CreatePostDto) => createPost(newPost),\n  onSuccess: (data) => {\n    console.log('Post created:', data);\n  },\n  onError: (error) => {\n    console.error('Failed to create post:', error);\n  }\n});\n\n// Trigger mutation\nmutate({ title: 'New Post', content: '...' });\n```\n\n## Optimistic Updates\n\nUpdate UI immediately, rollback on error:\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (newPost) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['posts'] });\n\n    // Snapshot previous value\n    const previousPosts = queryClient.getQueryData(['posts']);\n\n    // Optimistically update to the new value\n    queryClient.setQueryData(['posts'], (old) =>\n      old.map((post) => (post.id === newPost.id ? newPost : post))\n    );\n\n    // Return context with snapshot\n    return { previousPosts };\n  },\n  onError: (err, newPost, context) => {\n    // Rollback to previous value\n    queryClient.setQueryData(['posts'], context.previousPosts);\n  },\n  onSettled: () => {\n    // Always refetch after error or success\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n  }\n});\n```\n\n## Sequential Mutations\n\nRun mutations in sequence:\n\n```typescript\nconst createAndPublish = async (postData) => {\n  // Create post\n  const post = await createPostMutation.mutateAsync(postData);\n\n  // Publish post\n  const published = await publishPostMutation.mutateAsync(post.id);\n\n  return published;\n};\n```\n\n## Parallel Mutations\n\nRun multiple mutations simultaneously:\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: async (updates) => {\n    const results = await Promise.all([\n      updateProfile(updates.profile),\n      updateSettings(updates.settings),\n      updatePreferences(updates.preferences)\n    ]);\n    return results;\n  }\n});\n```\n\n## Mutation with Invalidation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n\n    // Or update cache directly\n    queryClient.setQueryData(['posts'], (old) => [newPost, ...old]);\n  }\n});\n```\n\n## Mutation with Multiple Cache Updates\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: deletePost,\n  onSuccess: (_, deletedPostId) => {\n    // Update posts list\n    queryClient.setQueryData(['posts'], (old) => old.filter((post) => post.id !== deletedPostId));\n\n    // Update post count\n    queryClient.setQueryData(['postsCount'], (old) => old - 1);\n\n    // Invalidate related queries\n    queryClient.invalidateQueries({ queryKey: ['user', 'stats'] });\n  }\n});\n```\n\n## Error Handling\n\n```typescript\nconst { mutate, isError, error, reset } = useMutation({\n  mutationFn: createPost,\n  onError: (error) => {\n    if (error.code === 'VALIDATION_ERROR') {\n      setFormErrors(error.fields);\n    } else if (error.code === 'NETWORK_ERROR') {\n      showRetryDialog();\n    } else {\n      showGenericError();\n    }\n  }\n});\n\n// Clear error state\nreset();\n```\n\n## Retry Failed Mutations\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  retry: 3, // Retry 3 times on failure\n  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff\n});\n```\n\n## Mutation with Loading State\n\n```typescript\nfunction CreatePostForm() {\n  const { mutate, isPending } = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      navigate('/posts');\n    }\n  });\n\n  const handleSubmit = (data) => {\n    mutate(data);\n  };\n\n  return (\n    <form onSubmit={handleSubmit}>\n      {/* form fields */}\n      <button type=\"submit\" disabled={isPending}>\n        {isPending ? 'Creating...' : 'Create Post'}\n      </button>\n    </form>\n  );\n}\n```\n\n## Mutation with Variables\n\n```typescript\nconst { mutate, variables } = useMutation({\n  mutationFn: updatePost\n});\n\n// Access last mutation variables\nconsole.log('Last updated post:', variables);\n```\n\n## Mutation Callbacks\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onMutate: (variables) => {\n    console.log('Starting mutation with:', variables);\n  },\n  onSuccess: (data, variables, context) => {\n    console.log('Success!', data);\n  },\n  onError: (error, variables, context) => {\n    console.error('Error!', error);\n  },\n  onSettled: (data, error, variables, context) => {\n    console.log('Mutation finished (success or error)');\n  }\n});\n```\n\n## Mutation with Form Integration\n\n```typescript\nimport { useForm } from 'react-hook-form';\n\nfunction CreatePostForm() {\n  const { register, handleSubmit, reset } = useForm();\n\n  const { mutate, isPending, isError, error } = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      reset();  // Clear form\n      toast.success('Post created!');\n    },\n    onError: (error) => {\n      toast.error(error.message);\n    }\n  });\n\n  const onSubmit = (data) => {\n    mutate(data);\n  };\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <input {...register('title')} />\n      <textarea {...register('content')} />\n      <button type=\"submit\" disabled={isPending}>\n        Submit\n      </button>\n      {isError && <ErrorMessage error={error} />}\n    </form>\n  );\n}\n```\n\n## Mutation State Reset\n\n```typescript\nconst { mutate, data, error, reset } = useMutation({\n  mutationFn: createPost\n});\n\n// Clear mutation state\nconst handleReset = () => {\n  reset(); // Clears data, error, status, etc.\n};\n```\n\n## Global Mutation Configuration\n\n```typescript\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    mutations: {\n      retry: 3,\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n      onError: (error) => {\n        // Global error handler\n        console.error('Mutation error:', error);\n      }\n    }\n  }\n});\n```\n\n## Mutation Lifecycle\n\n```\n┌─────────────┐\n│ idle        │\n└──────┬──────┘\n       │ mutate()\n       ▼\n┌─────────────┐\n│ pending     │ ─── onMutate()\n└──────┬──────┘\n       │\n       ├─ success ──► onSuccess() ──┐\n       │                            │\n       └─ error ────► onError() ────┤\n                                    │\n                                    ▼\n                              onSettled()\n```\n\n## Best Practices\n\n1. **Use Optimistic Updates** - Better UX for fast operations\n2. **Always Handle Errors** - Show clear error messages\n3. **Invalidate Related Queries** - Keep cache in sync\n4. **Use onSettled** - For cleanup that runs regardless of success/error\n5. **Reset on Unmount** - Clear mutation state when component unmounts\n6. **Retry Network Errors** - Configure retry for transient failures\n7. **Show Loading States** - Disable buttons during mutation\n8. **Rollback on Error** - Revert optimistic updates if mutation fails\n\n## Common Patterns\n\n### Create with Redirect\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: (newPost) => {\n    navigate(`/posts/${newPost.id}`);\n  }\n});\n```\n\n### Update with Toast\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onSuccess: () => {\n    toast.success('Post updated!');\n  },\n  onError: () => {\n    toast.error('Failed to update post');\n  }\n});\n```\n\n### Delete with Confirmation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: deletePost,\n  onMutate: async () => {\n    const confirmed = await confirm('Are you sure?');\n    if (!confirmed) throw new Error('Cancelled');\n  },\n  onSuccess: () => {\n    toast.success('Post deleted');\n    navigate('/posts');\n  }\n});\n```\n",".agents/skills/tanstack-query/skill-rules-fragment.json":"{\n  \"tanstack-query\": {\n    \"type\": \"domain\",\n    \"enforcement\": \"suggest\",\n    \"priority\": \"high\",\n    \"promptTriggers\": {\n      \"keywords\": [\n        \"tanstack query\",\n        \"react query\",\n        \"@tanstack/react-query\",\n        \"useQuery\",\n        \"useMutation\",\n        \"useSuspenseQuery\",\n        \"useInfiniteQuery\",\n        \"useQueries\",\n        \"useQueryClient\",\n        \"useIsFetching\",\n        \"useIsMutating\",\n        \"useMutationState\",\n        \"QueryClient\",\n        \"QueryClientProvider\",\n        \"queryKey\",\n        \"queryFn\",\n        \"mutationFn\",\n        \"invalidateQueries\",\n        \"setQueryData\",\n        \"getQueryData\",\n        \"removeQueries\",\n        \"resetQueries\",\n        \"staleTime\",\n        \"cacheTime\",\n        \"refetchInterval\",\n        \"refetchOnWindowFocus\",\n        \"refetchOnReconnect\",\n        \"isLoading\",\n        \"isError\",\n        \"isFetching\",\n        \"isSuccess\",\n        \"queryCache\",\n        \"mutationCache\"\n      ],\n      \"intentPatterns\": [\n        \"use.*tanstack.*query\",\n        \"use.*react.*query\",\n        \"create.*tanstack.*(query|mutation)\",\n        \"invalidate.*query.*cache\",\n        \"invalidate.*tanstack.*queries\",\n        \"prefetch.*query\",\n        \"setup.*query.*client\",\n        \"configure.*tanstack.*query\",\n        \"use.*(useQuery|useMutation|useInfiniteQuery)\",\n        \"implement.*query.*invalidation\",\n        \"add.*query.*key\",\n        \"set.*query.*data\"\n      ]\n    },\n    \"fileTriggers\": {\n      \"pathPatterns\": [\n        \"**/api/**/*.ts\",\n        \"**/api/**/*.tsx\",\n        \"**/queries/**/*.ts\",\n        \"**/queries/**/*.tsx\",\n        \"**/hooks/**/*.ts\",\n        \"**/hooks/**/*.tsx\",\n        \"**/mutations/**/*.ts\",\n        \"**/mutations/**/*.tsx\"\n      ],\n      \"contentPatterns\": [\n        \"useQuery\\\\(\",\n        \"useMutation\\\\(\",\n        \"useSuspenseQuery\\\\(\",\n        \"useInfiniteQuery\\\\(\",\n        \"useQueryClient\\\\(\",\n        \"queryClient\\\\.\",\n        \"import.*@tanstack/react-query\",\n        \"from '@tanstack/react-query'\",\n        \"QueryClientProvider\",\n        \"new QueryClient\\\\(\",\n        \"invalidateQueries\\\\(\",\n        \"setQueryData\\\\(\",\n        \"queryKey:\",\n        \"queryFn:\"\n      ]\n    }\n  }\n}\n",".agents/skills/vercel-composition-patterns/AGENTS.md":"# React Composition Patterns\n\n**Version 1.0.0**  \nEngineering  \nJanuary 2026\n\n> **Note:**  \n> This document is mainly for agents and LLMs to follow when maintaining,  \n> generating, or refactoring React codebases using composition. Humans  \n> may also find it useful, but guidance here is optimized for automation  \n> and consistency by AI-assisted workflows.\n\n---\n\n## Abstract\n\nComposition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.\n\n---\n\n## Table of Contents\n\n1. [Component Architecture](#1-component-architecture) — **HIGH**\n   - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)\n   - 1.2 [Use Compound Components](#12-use-compound-components)\n2. [State Management](#2-state-management) — **MEDIUM**\n   - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)\n   - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)\n   - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)\n3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**\n   - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)\n   - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)\n4. [React 19 APIs](#4-react-19-apis) — **MEDIUM**\n   - 4.1 [React 19 API Changes](#41-react-19-api-changes)\n\n---\n\n## 1. Component Architecture\n\n**Impact: HIGH**\n\nFundamental patterns for structuring components to avoid prop\nproliferation and enable flexible composition.\n\n### 1.1 Avoid Boolean Prop Proliferation\n\n**Impact: CRITICAL (prevents unmaintainable component variants)**\n\nDon't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize\n\ncomponent behavior. Each boolean doubles possible states and creates\n\nunmaintainable conditional logic. Use composition instead.\n\n**Incorrect: boolean props create exponential complexity**\n\n```tsx\nfunction Composer({\n  onSubmit,\n  isThread,\n  channelId,\n  isDMThread,\n  dmId,\n  isEditing,\n  isForwarding\n}: Props) {\n  return (\n    <form>\n      <Header />\n      <Input />\n      {isDMThread ? (\n        <AlsoSendToDMField id={dmId} />\n      ) : isThread ? (\n        <AlsoSendToChannelField id={channelId} />\n      ) : null}\n      {isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}\n      <Footer onSubmit={onSubmit} />\n    </form>\n  );\n}\n```\n\n**Correct: composition eliminates conditionals**\n\n```tsx\n// Channel composer\nfunction ChannelComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Attachments />\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Thread composer - adds \"also send to channel\" field\nfunction ThreadComposer({ channelId }: { channelId: string }) {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <AlsoSendToChannelField id={channelId} />\n      <Composer.Footer>\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Edit composer - different footer actions\nfunction EditComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.CancelEdit />\n        <Composer.SaveEdit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n```\n\nEach variant is explicit about what it renders. We can share internals without\n\nsharing a single monolithic parent.\n\n### 1.2 Use Compound Components\n\n**Impact: HIGH (enables flexible composition without prop drilling)**\n\nStructure complex components as compound components with a shared context. Each\n\nsubcomponent accesses shared state via context, not props. Consumers compose the\n\npieces they need.\n\n**Incorrect: monolithic component with render props**\n\n```tsx\nfunction Composer({\n  renderHeader,\n  renderFooter,\n  renderActions,\n  showAttachments,\n  showFormatting,\n  showEmojis\n}: Props) {\n  return (\n    <form>\n      {renderHeader?.()}\n      <Input />\n      {showAttachments && <Attachments />}\n      {renderFooter ? (\n        renderFooter()\n      ) : (\n        <Footer>\n          {showFormatting && <Formatting />}\n          {showEmojis && <Emojis />}\n          {renderActions?.()}\n        </Footer>\n      )}\n    </form>\n  );\n}\n```\n\n**Correct: compound components with shared context**\n\n```tsx\nconst ComposerContext = createContext<ComposerContextValue | null>(null);\n\nfunction ComposerProvider({ children, state, actions, meta }: ProviderProps) {\n  return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;\n}\n\nfunction ComposerFrame({ children }: { children: React.ReactNode }) {\n  return <form>{children}</form>;\n}\n\nfunction ComposerInput() {\n  const {\n    state,\n    actions: { update },\n    meta: { inputRef }\n  } = use(ComposerContext);\n  return (\n    <TextInput\n      ref={inputRef}\n      value={state.input}\n      onChangeText={(text) => update((s) => ({ ...s, input: text }))}\n    />\n  );\n}\n\nfunction ComposerSubmit() {\n  const {\n    actions: { submit }\n  } = use(ComposerContext);\n  return <Button onPress={submit}>Send</Button>;\n}\n\n// Export as compound component\nconst Composer = {\n  Provider: ComposerProvider,\n  Frame: ComposerFrame,\n  Input: ComposerInput,\n  Submit: ComposerSubmit,\n  Header: ComposerHeader,\n  Footer: ComposerFooter,\n  Attachments: ComposerAttachments,\n  Formatting: ComposerFormatting,\n  Emojis: ComposerEmojis\n};\n```\n\n**Usage:**\n\n```tsx\n<Composer.Provider state={state} actions={actions} meta={meta}>\n  <Composer.Frame>\n    <Composer.Header />\n    <Composer.Input />\n    <Composer.Footer>\n      <Composer.Formatting />\n      <Composer.Submit />\n    </Composer.Footer>\n  </Composer.Frame>\n</Composer.Provider>\n```\n\nConsumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.\n\n---\n\n## 2. State Management\n\n**Impact: MEDIUM**\n\nPatterns for lifting state and managing shared context across\ncomposed components.\n\n### 2.1 Decouple State Management from UI\n\n**Impact: MEDIUM (enables swapping state implementations without changing UI)**\n\nThe provider component should be the only place that knows how state is managed.\n\nUI components consume the context interface—they don't know if state comes from\n\nuseState, Zustand, or a server sync.\n\n**Incorrect: UI coupled to state implementation**\n\n```tsx\nfunction ChannelComposer({ channelId }: { channelId: string }) {\n  // UI component knows about global state implementation\n  const state = useGlobalChannelState(channelId);\n  const { submit, updateInput } = useChannelSync(channelId);\n\n  return (\n    <Composer.Frame>\n      <Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />\n      <Composer.Submit onPress={() => sync.submit()} />\n    </Composer.Frame>\n  );\n}\n```\n\n**Correct: state management isolated in provider**\n\n```tsx\n// Provider handles all state management details\nfunction ChannelProvider({\n  channelId,\n  children\n}: {\n  channelId: string;\n  children: React.ReactNode;\n}) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n  const inputRef = useRef(null);\n\n  return (\n    <Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n\n// UI component only knows about the context interface\nfunction ChannelComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Usage\nfunction Channel({ channelId }: { channelId: string }) {\n  return (\n    <ChannelProvider channelId={channelId}>\n      <ChannelComposer />\n    </ChannelProvider>\n  );\n}\n```\n\n**Different providers, same UI:**\n\n```tsx\n// Local state for ephemeral forms\nfunction ForwardMessageProvider({ children }) {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n\n  return (\n    <Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n\n// Global synced state for channels\nfunction ChannelProvider({ channelId, children }) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n\n  return (\n    <Composer.Provider state={state} actions={{ update, submit }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n```\n\nThe same `Composer.Input` component works with both providers because it only\n\ndepends on the context interface, not the implementation.\n\n### 2.2 Define Generic Context Interfaces for Dependency Injection\n\n**Impact: HIGH (enables dependency-injectable state across use-cases)**\n\nDefine a **generic interface** for your component context with three parts:\n\n`state`, `actions`, and `meta`. This interface is a contract that any provider\n\ncan implement—enabling the same UI components to work with completely different\n\nstate implementations.\n\n**Core principle:** Lift state, compose internals, make state\n\ndependency-injectable.\n\n**Incorrect: UI coupled to specific state implementation**\n\n```tsx\nfunction ComposerInput() {\n  // Tightly coupled to a specific hook\n  const { input, setInput } = useChannelComposerState();\n  return <TextInput value={input} onChangeText={setInput} />;\n}\n```\n\n**Correct: generic interface enables dependency injection**\n\n```tsx\n// Define a GENERIC interface that any provider can implement\ninterface ComposerState {\n  input: string;\n  attachments: Attachment[];\n  isSubmitting: boolean;\n}\n\ninterface ComposerActions {\n  update: (updater: (state: ComposerState) => ComposerState) => void;\n  submit: () => void;\n}\n\ninterface ComposerMeta {\n  inputRef: React.RefObject<TextInput>;\n}\n\ninterface ComposerContextValue {\n  state: ComposerState;\n  actions: ComposerActions;\n  meta: ComposerMeta;\n}\n\nconst ComposerContext = createContext<ComposerContextValue | null>(null);\n```\n\n**UI components consume the interface, not the implementation:**\n\n```tsx\nfunction ComposerInput() {\n  const {\n    state,\n    actions: { update },\n    meta\n  } = use(ComposerContext);\n\n  // This component works with ANY provider that implements the interface\n  return (\n    <TextInput\n      ref={meta.inputRef}\n      value={state.input}\n      onChangeText={(text) => update((s) => ({ ...s, input: text }))}\n    />\n  );\n}\n```\n\n**Different providers implement the same interface:**\n\n```tsx\n// Provider A: Local state for ephemeral forms\nfunction ForwardMessageProvider({ children }: { children: React.ReactNode }) {\n  const [state, setState] = useState(initialState);\n  const inputRef = useRef(null);\n  const submit = useForwardMessage();\n\n  return (\n    <ComposerContext\n      value={{\n        state,\n        actions: { update: setState, submit },\n        meta: { inputRef }\n      }}\n    >\n      {children}\n    </ComposerContext>\n  );\n}\n\n// Provider B: Global synced state for channels\nfunction ChannelProvider({ channelId, children }: Props) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n  const inputRef = useRef(null);\n\n  return (\n    <ComposerContext\n      value={{\n        state,\n        actions: { update, submit },\n        meta: { inputRef }\n      }}\n    >\n      {children}\n    </ComposerContext>\n  );\n}\n```\n\n**The same composed UI works with both:**\n\n```tsx\n// Works with ForwardMessageProvider (local state)\n<ForwardMessageProvider>\n  <Composer.Frame>\n    <Composer.Input />\n    <Composer.Submit />\n  </Composer.Frame>\n</ForwardMessageProvider>\n\n// Works with ChannelProvider (global synced state)\n<ChannelProvider channelId=\"abc\">\n  <Composer.Frame>\n    <Composer.Input />\n    <Composer.Submit />\n  </Composer.Frame>\n</ChannelProvider>\n```\n\n**Custom UI outside the component can access state and actions:**\n\n```tsx\nfunction ForwardMessageDialog() {\n  return (\n    <ForwardMessageProvider>\n      <Dialog>\n        {/* The composer UI */}\n        <Composer.Frame>\n          <Composer.Input placeholder=\"Add a message, if you'd like.\" />\n          <Composer.Footer>\n            <Composer.Formatting />\n            <Composer.Emojis />\n          </Composer.Footer>\n        </Composer.Frame>\n\n        {/* Custom UI OUTSIDE the composer, but INSIDE the provider */}\n        <MessagePreview />\n\n        {/* Actions at the bottom of the dialog */}\n        <DialogActions>\n          <CancelButton />\n          <ForwardButton />\n        </DialogActions>\n      </Dialog>\n    </ForwardMessageProvider>\n  );\n}\n\n// This button lives OUTSIDE Composer.Frame but can still submit based on its context!\nfunction ForwardButton() {\n  const {\n    actions: { submit }\n  } = use(ComposerContext);\n  return <Button onPress={submit}>Forward</Button>;\n}\n\n// This preview lives OUTSIDE Composer.Frame but can read composer's state!\nfunction MessagePreview() {\n  const { state } = use(ComposerContext);\n  return <Preview message={state.input} attachments={state.attachments} />;\n}\n```\n\nThe provider boundary is what matters—not the visual nesting. Components that\n\nneed shared state don't have to be inside the `Composer.Frame`. They just need\n\nto be within the provider.\n\nThe `ForwardButton` and `MessagePreview` are not visually inside the composer\n\nbox, but they can still access its state and actions. This is the power of\n\nlifting state into providers.\n\nThe UI is reusable bits you compose together. The state is dependency-injected\n\nby the provider. Swap the provider, keep the UI.\n\n### 2.3 Lift State into Provider Components\n\n**Impact: HIGH (enables state sharing outside component boundaries)**\n\nMove state management into dedicated provider components. This allows sibling\n\ncomponents outside the main UI to access and modify state without prop drilling\n\nor awkward refs.\n\n**Incorrect: state trapped inside component**\n\n```tsx\nfunction ForwardMessageComposer() {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n\n  return (\n    <Composer.Frame>\n      <Composer.Input />\n      <Composer.Footer />\n    </Composer.Frame>\n  );\n}\n\n// Problem: How does this button access composer state?\nfunction ForwardMessageDialog() {\n  return (\n    <Dialog>\n      <ForwardMessageComposer />\n      <MessagePreview /> {/* Needs composer state */}\n      <DialogActions>\n        <CancelButton />\n        <ForwardButton /> {/* Needs to call submit */}\n      </DialogActions>\n    </Dialog>\n  );\n}\n```\n\n**Incorrect: useEffect to sync state up**\n\n```tsx\nfunction ForwardMessageDialog() {\n  const [input, setInput] = useState('');\n  return (\n    <Dialog>\n      <ForwardMessageComposer onInputChange={setInput} />\n      <MessagePreview input={input} />\n    </Dialog>\n  );\n}\n\nfunction ForwardMessageComposer({ onInputChange }) {\n  const [state, setState] = useState(initialState);\n  useEffect(() => {\n    onInputChange(state.input); // Sync on every change 😬\n  }, [state.input]);\n}\n```\n\n**Incorrect: reading state from ref on submit**\n\n```tsx\nfunction ForwardMessageDialog() {\n  const stateRef = useRef(null);\n  return (\n    <Dialog>\n      <ForwardMessageComposer stateRef={stateRef} />\n      <ForwardButton onPress={() => submit(stateRef.current)} />\n    </Dialog>\n  );\n}\n```\n\n**Correct: state lifted to provider**\n\n```tsx\nfunction ForwardMessageProvider({ children }: { children: React.ReactNode }) {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n  const inputRef = useRef(null);\n\n  return (\n    <Composer.Provider\n      state={state}\n      actions={{ update: setState, submit: forwardMessage }}\n      meta={{ inputRef }}\n    >\n      {children}\n    </Composer.Provider>\n  );\n}\n\nfunction ForwardMessageDialog() {\n  return (\n    <ForwardMessageProvider>\n      <Dialog>\n        <ForwardMessageComposer />\n        <MessagePreview /> {/* Custom components can access state and actions */}\n        <DialogActions>\n          <CancelButton />\n          <ForwardButton /> {/* Custom components can access state and actions */}\n        </DialogActions>\n      </Dialog>\n    </ForwardMessageProvider>\n  );\n}\n\nfunction ForwardButton() {\n  const { actions } = use(Composer.Context);\n  return <Button onPress={actions.submit}>Forward</Button>;\n}\n```\n\nThe ForwardButton lives outside the Composer.Frame but still has access to the\n\nsubmit action because it's within the provider. Even though it's a one-off\n\ncomponent, it can still access the composer's state and actions from outside the\n\nUI itself.\n\n**Key insight:** Components that need shared state don't have to be visually\n\nnested inside each other—they just need to be within the same provider.\n\n---\n\n## 3. Implementation Patterns\n\n**Impact: MEDIUM**\n\nSpecific techniques for implementing compound components and\ncontext providers.\n\n### 3.1 Create Explicit Component Variants\n\n**Impact: MEDIUM (self-documenting code, no hidden conditionals)**\n\nInstead of one component with many boolean props, create explicit variant\n\ncomponents. Each variant composes the pieces it needs. The code documents\n\nitself.\n\n**Incorrect: one component, many modes**\n\n```tsx\n// What does this component actually render?\n<Composer isThread isEditing={false} channelId='abc' showAttachments showFormatting={false} />\n```\n\n**Correct: explicit variants**\n\n```tsx\n// Immediately clear what this renders\n<ThreadComposer channelId=\"abc\" />\n\n// Or\n<EditMessageComposer messageId=\"xyz\" />\n\n// Or\n<ForwardMessageComposer messageId=\"123\" />\n```\n\nEach implementation is unique, explicit and self-contained. Yet they can each\n\nuse shared parts.\n\n**Implementation:**\n\n```tsx\nfunction ThreadComposer({ channelId }: { channelId: string }) {\n  return (\n    <ThreadProvider channelId={channelId}>\n      <Composer.Frame>\n        <Composer.Input />\n        <AlsoSendToChannelField channelId={channelId} />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.Submit />\n        </Composer.Footer>\n      </Composer.Frame>\n    </ThreadProvider>\n  );\n}\n\nfunction EditMessageComposer({ messageId }: { messageId: string }) {\n  return (\n    <EditMessageProvider messageId={messageId}>\n      <Composer.Frame>\n        <Composer.Input />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.CancelEdit />\n          <Composer.SaveEdit />\n        </Composer.Footer>\n      </Composer.Frame>\n    </EditMessageProvider>\n  );\n}\n\nfunction ForwardMessageComposer({ messageId }: { messageId: string }) {\n  return (\n    <ForwardMessageProvider messageId={messageId}>\n      <Composer.Frame>\n        <Composer.Input placeholder=\"Add a message, if you'd like.\" />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.Mentions />\n        </Composer.Footer>\n      </Composer.Frame>\n    </ForwardMessageProvider>\n  );\n}\n```\n\nEach variant is explicit about:\n\n- What provider/state it uses\n\n- What UI elements it includes\n\n- What actions are available\n\nNo boolean prop combinations to reason about. No impossible states.\n\n### 3.2 Prefer Composing Children Over Render Props\n\n**Impact: MEDIUM (cleaner composition, better readability)**\n\nUse `children` for composition instead of `renderX` props. Children are more\n\nreadable, compose naturally, and don't require understanding callback\n\nsignatures.\n\n**Incorrect: render props**\n\n```tsx\nfunction Composer({\n  renderHeader,\n  renderFooter,\n  renderActions\n}: {\n  renderHeader?: () => React.ReactNode;\n  renderFooter?: () => React.ReactNode;\n  renderActions?: () => React.ReactNode;\n}) {\n  return (\n    <form>\n      {renderHeader?.()}\n      <Input />\n      {renderFooter ? renderFooter() : <DefaultFooter />}\n      {renderActions?.()}\n    </form>\n  );\n}\n\n// Usage is awkward and inflexible\nreturn (\n  <Composer\n    renderHeader={() => <CustomHeader />}\n    renderFooter={() => (\n      <>\n        <Formatting />\n        <Emojis />\n      </>\n    )}\n    renderActions={() => <SubmitButton />}\n  />\n);\n```\n\n**Correct: compound components with children**\n\n```tsx\nfunction ComposerFrame({ children }: { children: React.ReactNode }) {\n  return <form>{children}</form>;\n}\n\nfunction ComposerFooter({ children }: { children: React.ReactNode }) {\n  return <footer className='flex'>{children}</footer>;\n}\n\n// Usage is flexible\nreturn (\n  <Composer.Frame>\n    <CustomHeader />\n    <Composer.Input />\n    <Composer.Footer>\n      <Composer.Formatting />\n      <Composer.Emojis />\n      <SubmitButton />\n    </Composer.Footer>\n  </Composer.Frame>\n);\n```\n\n**When render props are appropriate:**\n\n```tsx\n// Render props work well when you need to pass data back\n<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />\n```\n\nUse render props when the parent needs to provide data or state to the child.\n\nUse children when composing static structure.\n\n---\n\n## 4. React 19 APIs\n\n**Impact: MEDIUM**\n\nReact 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.\n\n### 4.1 React 19 API Changes\n\n**Impact: MEDIUM (cleaner component definitions and context usage)**\n\n> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.\n\nIn React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.\n\n**Incorrect: forwardRef in React 19**\n\n```tsx\nconst ComposerInput = forwardRef<TextInput, Props>((props, ref) => {\n  return <TextInput ref={ref} {...props} />;\n});\n```\n\n**Correct: ref as a regular prop**\n\n```tsx\nfunction ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {\n  return <TextInput ref={ref} {...props} />;\n}\n```\n\n**Incorrect: useContext in React 19**\n\n```tsx\nconst value = useContext(MyContext);\n```\n\n**Correct: use instead of useContext**\n\n```tsx\nconst value = use(MyContext);\n```\n\n`use()` can also be called conditionally, unlike `useContext()`.\n\n---\n\n## References\n\n1. [https://react.dev](https://react.dev)\n2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)\n3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)\n",".agents/skills/vercel-composition-patterns/README.md":"# React Composition Patterns\n\nA structured repository for React composition patterns that scale. These\npatterns help avoid boolean prop proliferation by using compound components,\nlifting state, and composing internals.\n\n## Structure\n\n- `rules/` - Individual rule files (one per rule)\n  - `_sections.md` - Section metadata (titles, impacts, descriptions)\n  - `_template.md` - Template for creating new rules\n  - `area-description.md` - Individual rule files\n- `metadata.json` - Document metadata (version, organization, abstract)\n- **`AGENTS.md`** - Compiled output (generated)\n\n## Rules\n\n### Component Architecture (CRITICAL)\n\n- `architecture-avoid-boolean-props.md` - Don't add boolean props to customize\n  behavior\n- `architecture-compound-components.md` - Structure as compound components with\n  shared context\n\n### State Management (HIGH)\n\n- `state-lift-state.md` - Lift state into provider components\n- `state-context-interface.md` - Define clear context interfaces\n  (state/actions/meta)\n- `state-decouple-implementation.md` - Decouple state management from UI\n\n### Implementation Patterns (MEDIUM)\n\n- `patterns-children-over-render-props.md` - Prefer children over renderX props\n- `patterns-explicit-variants.md` - Create explicit component variants\n\n## Core Principles\n\n1. **Composition over configuration** — Instead of adding props, let consumers\n   compose\n2. **Lift your state** — State in providers, not trapped in components\n3. **Compose your internals** — Subcomponents access context, not props\n4. **Explicit variants** — Create ThreadComposer, EditComposer, not Composer\n   with isThread\n\n## Creating a New Rule\n\n1. Copy `rules/_template.md` to `rules/area-description.md`\n2. Choose the appropriate area prefix:\n   - `architecture-` for Component Architecture\n   - `state-` for State Management\n   - `patterns-` for Implementation Patterns\n3. Fill in the frontmatter and content\n4. Ensure you have clear examples with explanations\n\n## Impact Levels\n\n- `CRITICAL` - Foundational patterns, prevents unmaintainable code\n- `HIGH` - Significant maintainability improvements\n- `MEDIUM` - Good practices for cleaner code\n",".agents/skills/vercel-composition-patterns/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2FSKILL.md -->",".agents/skills/vercel-composition-patterns/rules/_sections.md":"# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parentheses) is the filename prefix used to group rules.\n\n---\n\n## 1. Component Architecture (architecture)\n\n**Impact:** HIGH  \n**Description:** Fundamental patterns for structuring components to avoid prop\nproliferation and enable flexible composition.\n\n## 2. State Management (state)\n\n**Impact:** MEDIUM  \n**Description:** Patterns for lifting state and managing shared context across\ncomposed components.\n\n## 3. Implementation Patterns (patterns)\n\n**Impact:** MEDIUM  \n**Description:** Specific techniques for implementing compound components and\ncontext providers.\n\n## 4. React 19 APIs (react19)\n\n**Impact:** MEDIUM  \n**Description:** React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.\n",".agents/skills/vercel-composition-patterns/rules/_template.md":"---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: brief description of impact\ntags: composition, components\n---\n\n## Rule Title Here\n\nBrief explanation of the rule and why it matters.\n\n**Incorrect:**\n\n```tsx\n// Bad code example\n```\n\n**Correct:**\n\n```tsx\n// Good code example\n```\n\nReference: [Link](https://example.com)\n",".agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-avoid-boolean-props.md -->",".agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-compound-components.md -->",".agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-children-over-render-props.md -->",".agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-explicit-variants.md -->",".agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Freact19-no-forwardref.md -->",".agents/skills/vercel-composition-patterns/rules/state-context-interface.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-context-interface.md -->",".agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-decouple-implementation.md -->",".agents/skills/vercel-composition-patterns/rules/state-lift-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-lift-state.md -->",".agents/skills/vercel-react-best-practices/AGENTS.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FAGENTS.md -->",".agents/skills/vercel-react-best-practices/README.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FREADME.md -->",".agents/skills/vercel-react-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FSKILL.md -->",".agents/skills/vercel-react-best-practices/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2F_sections.md -->",".agents/skills/vercel-react-best-practices/rules/_template.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2F_template.md -->",".agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-event-handler-refs.md -->",".agents/skills/vercel-react-best-practices/rules/advanced-init-once.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-init-once.md -->",".agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-use-latest.md -->",".agents/skills/vercel-react-best-practices/rules/async-api-routes.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-api-routes.md -->",".agents/skills/vercel-react-best-practices/rules/async-defer-await.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-defer-await.md -->",".agents/skills/vercel-react-best-practices/rules/async-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-dependencies.md -->",".agents/skills/vercel-react-best-practices/rules/async-parallel.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-parallel.md -->",".agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-suspense-boundaries.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-barrel-imports.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-conditional.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-conditional.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-defer-third-party.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-dynamic-imports.md -->",".agents/skills/vercel-react-best-practices/rules/bundle-preload.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-preload.md -->",".agents/skills/vercel-react-best-practices/rules/client-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-event-listeners.md -->",".agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-localstorage-schema.md -->",".agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-passive-event-listeners.md -->",".agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-swr-dedup.md -->",".agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-batch-dom-css.md -->",".agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-function-results.md -->",".agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-property-access.md -->",".agents/skills/vercel-react-best-practices/rules/js-cache-storage.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-storage.md -->",".agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-combine-iterations.md -->",".agents/skills/vercel-react-best-practices/rules/js-early-exit.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-early-exit.md -->",".agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-flatmap-filter.md -->",".agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-hoist-regexp.md -->",".agents/skills/vercel-react-best-practices/rules/js-index-maps.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-index-maps.md -->",".agents/skills/vercel-react-best-practices/rules/js-length-check-first.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-length-check-first.md -->",".agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-min-max-loop.md -->",".agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-set-map-lookups.md -->",".agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-tosorted-immutable.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-activity.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-activity.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-animate-svg-wrapper.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-conditional-render.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-content-visibility.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hoist-jsx.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-no-flicker.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-suppress-warning.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-resource-hints.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-script-defer-async.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-svg-precision.md -->",".agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-usetransition-loading.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-defer-reads.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-dependencies.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state-no-effect.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-functional-setstate.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-lazy-state-init.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo-with-default-value.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-move-effect-to-event.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-no-inline-components.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-split-combined-hooks.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-transitions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-transitions.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-deferred-value.md -->",".agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-ref-transient-values.md -->",".agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-after-nonblocking.md -->",".agents/skills/vercel-react-best-practices/rules/server-auth-actions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-auth-actions.md -->",".agents/skills/vercel-react-best-practices/rules/server-cache-lru.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-lru.md -->",".agents/skills/vercel-react-best-practices/rules/server-cache-react.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-react.md -->",".agents/skills/vercel-react-best-practices/rules/server-dedup-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-dedup-props.md -->",".agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-hoist-static-io.md -->",".agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-parallel-fetching.md -->",".agents/skills/vercel-react-best-practices/rules/server-serialization.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-serialization.md -->",".agents/skills/web-design-guidelines/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->",".claude/skills/kiranism-shadcn-dashboard/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fkiranism-shadcn-dashboard%2FSKILL.md -->",".claude/skills/kiranism-shadcn-dashboard/references/theming-guide.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fkiranism-shadcn-dashboard%2Freferences%2Ftheming-guide.md -->",".claude/skills/next-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2FSKILL.md -->",".claude/skills/next-best-practices/async-patterns.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fasync-patterns.md -->",".claude/skills/next-best-practices/bundling.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fbundling.md -->",".claude/skills/next-best-practices/data-patterns.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdata-patterns.md -->",".claude/skills/next-best-practices/debug-tricks.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdebug-tricks.md -->",".claude/skills/next-best-practices/directives.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdirectives.md -->",".claude/skills/next-best-practices/error-handling.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ferror-handling.md -->",".claude/skills/next-best-practices/file-conventions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffile-conventions.md -->",".claude/skills/next-best-practices/font.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffont.md -->",".claude/skills/next-best-practices/functions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffunctions.md -->",".claude/skills/next-best-practices/hydration-error.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fhydration-error.md -->",".claude/skills/next-best-practices/image.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fimage.md -->",".claude/skills/next-best-practices/metadata.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fmetadata.md -->",".claude/skills/next-best-practices/parallel-routes.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fparallel-routes.md -->",".claude/skills/next-best-practices/route-handlers.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Froute-handlers.md -->",".claude/skills/next-best-practices/rsc-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Frsc-boundaries.md -->",".claude/skills/next-best-practices/runtime-selection.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fruntime-selection.md -->",".claude/skills/next-best-practices/scripts.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fscripts.md -->",".claude/skills/next-best-practices/self-hosting.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fself-hosting.md -->",".claude/skills/next-best-practices/suspense-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fsuspense-boundaries.md -->",".claude/skills/vercel-composition-patterns/AGENTS.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FAGENTS.md -->",".claude/skills/vercel-composition-patterns/README.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FREADME.md -->",".claude/skills/vercel-composition-patterns/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FSKILL.md -->",".claude/skills/vercel-composition-patterns/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2F_sections.md -->",".claude/skills/vercel-composition-patterns/rules/_template.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2F_template.md -->",".claude/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-avoid-boolean-props.md -->",".claude/skills/vercel-composition-patterns/rules/architecture-compound-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-compound-components.md -->",".claude/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-children-over-render-props.md -->",".claude/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-explicit-variants.md -->",".claude/skills/vercel-composition-patterns/rules/react19-no-forwardref.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Freact19-no-forwardref.md -->",".claude/skills/vercel-composition-patterns/rules/state-context-interface.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-context-interface.md -->",".claude/skills/vercel-composition-patterns/rules/state-decouple-implementation.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-decouple-implementation.md -->",".claude/skills/vercel-composition-patterns/rules/state-lift-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-lift-state.md -->",".claude/skills/vercel-react-best-practices/AGENTS.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FAGENTS.md -->",".claude/skills/vercel-react-best-practices/README.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FREADME.md -->",".claude/skills/vercel-react-best-practices/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FSKILL.md -->",".claude/skills/vercel-react-best-practices/rules/_sections.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2F_sections.md -->",".claude/skills/vercel-react-best-practices/rules/_template.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2F_template.md -->",".claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-event-handler-refs.md -->",".claude/skills/vercel-react-best-practices/rules/advanced-init-once.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-init-once.md -->",".claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-use-latest.md -->",".claude/skills/vercel-react-best-practices/rules/async-api-routes.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-api-routes.md -->",".claude/skills/vercel-react-best-practices/rules/async-defer-await.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-defer-await.md -->",".claude/skills/vercel-react-best-practices/rules/async-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-dependencies.md -->",".claude/skills/vercel-react-best-practices/rules/async-parallel.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-parallel.md -->",".claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-suspense-boundaries.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-barrel-imports.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-conditional.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-conditional.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-defer-third-party.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-dynamic-imports.md -->",".claude/skills/vercel-react-best-practices/rules/bundle-preload.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-preload.md -->",".claude/skills/vercel-react-best-practices/rules/client-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-event-listeners.md -->",".claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-localstorage-schema.md -->",".claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-passive-event-listeners.md -->",".claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-swr-dedup.md -->",".claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-batch-dom-css.md -->",".claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-function-results.md -->",".claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-property-access.md -->",".claude/skills/vercel-react-best-practices/rules/js-cache-storage.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-storage.md -->",".claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-combine-iterations.md -->",".claude/skills/vercel-react-best-practices/rules/js-early-exit.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-early-exit.md -->",".claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-flatmap-filter.md -->",".claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-hoist-regexp.md -->",".claude/skills/vercel-react-best-practices/rules/js-index-maps.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-index-maps.md -->",".claude/skills/vercel-react-best-practices/rules/js-length-check-first.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-length-check-first.md -->",".claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-min-max-loop.md -->",".claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-set-map-lookups.md -->",".claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-tosorted-immutable.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-activity.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-activity.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-animate-svg-wrapper.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-conditional-render.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-content-visibility.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hoist-jsx.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-no-flicker.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-suppress-warning.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-resource-hints.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-script-defer-async.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-svg-precision.md -->",".claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-usetransition-loading.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-defer-reads.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-dependencies.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state-no-effect.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-functional-setstate.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-lazy-state-init.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo-with-default-value.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-move-effect-to-event.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-no-inline-components.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-split-combined-hooks.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-transitions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-transitions.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-deferred-value.md -->",".claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-ref-transient-values.md -->",".claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-after-nonblocking.md -->",".claude/skills/vercel-react-best-practices/rules/server-auth-actions.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-auth-actions.md -->",".claude/skills/vercel-react-best-practices/rules/server-cache-lru.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-lru.md -->",".claude/skills/vercel-react-best-practices/rules/server-cache-react.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-react.md -->",".claude/skills/vercel-react-best-practices/rules/server-dedup-props.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-dedup-props.md -->",".claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-hoist-static-io.md -->",".claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-parallel-fetching.md -->",".claude/skills/vercel-react-best-practices/rules/server-serialization.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-serialization.md -->",".claude/skills/web-design-guidelines/SKILL.md":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","AGENTS.md":"# AGENTS.md - AI Coding Agent Reference\n\nThis file provides essential information for AI coding agents working on this project. It contains project-specific details, conventions, and guidelines that complement the README.\n\n---\n\n## Project Overview\n\n**Next.js Admin Dashboard Starter** is a production-ready admin dashboard template built with:\n\n- **Framework**: Next.js 16 (App Router)\n- **Language**: TypeScript 7\n- **Styling**: Tailwind CSS v4\n- **UI Components**: shadcn/ui (base-nova style, Base UI primitives)\n- **Authentication**: Clerk (with Organizations/Billing support)\n- **Error Tracking**: Sentry\n- **Charts**: Recharts\n- **Containerization**: Docker (Node.js & Bun Dockerfiles)\n- **Package Manager**: Bun (preferred) or npm\n\nThe project follows a feature-based folder structure designed for scalability in SaaS applications, internal tools, and admin panels.\n\n---\n\n## Technology Stack Details\n\n### Core Framework & Runtime\n\n- Next.js 16.4 with App Router\n- React 19.3\n- TypeScript 7 (native compiler) with strict mode enabled\n\n### Styling & UI\n\n- Tailwind CSS v4 (using `@import 'tailwindcss'` syntax)\n- PostCSS with `@tailwindcss/postcss` plugin\n- shadcn/ui component library (Base UI primitives)\n- CSS custom properties for theming (OKLCH color format)\n\n### State Management\n\n- Zustand 5.x for local UI state in the stateful demo features\n- Nuqs for URL search params state management\n- TanStack Form + Zod for form handling (`createFormHook` + shadcn `Field`-anatomy components)\n\n### Data Fetching & Caching\n\n- TanStack React Query for data fetching, caching, and mutations\n- Server-side prefetching with `HydrationBoundary` + `dehydrate`\n- Client-side `useQuery` + nuqs `shallow: true` for tables (no RSC round-trips on pagination/filter)\n- `useMutation` + `invalidateQueries` for form submissions\n- Query client singleton in `src/lib/query-client.ts`\n\n### Authentication & Authorization\n\n- Clerk for authentication and user management\n- Clerk Organizations for multi-tenant workspaces\n- Clerk Billing for subscription management (B2B)\n- Client-side RBAC for navigation visibility\n\n### Data & APIs\n\n- TanStack Table for data tables\n- TanStack React Query for data fetching and mutations\n- Recharts for analytics/charts\n- Service layer per feature (`api/types.ts` → `api/service.ts` → `api/queries.ts`)\n- Route handlers at `src/app/api/` (for Route Handler or BFF patterns)\n- Mock data in `src/constants/mock-api*.ts` (default, swap via service layer)\n- API client utility in `src/lib/api-client.ts` (for fetch-based patterns)\n\n### Development Tools\n\n- ESLint 8.x with Next.js core-web-vitals config\n- Prettier 3.x with prettier-plugin-tailwindcss\n- Husky for git hooks\n- lint-staged for pre-commit formatting\n\n---\n\n## Project Structure\n\n```\n/src\n├── app/                    # Next.js App Router\n│   ├── auth/              # Authentication routes (sign-in, sign-up)\n│   ├── dashboard/         # Dashboard routes\n│   │   ├── overview/      # Parallel routes (@area_stats, @bar_stats, etc.)\n│   │   ├── product/       # Product management pages\n│   │   ├── kanban/        # Kanban board page\n│   │   ├── chat/          # Messaging page\n│   │   ├── ai-chat/       # AI chat streaming demo\n│   │   ├── notifications/ # Notifications page\n│   │   ├── workspaces/    # Organization management\n│   │   ├── billing/       # Subscription billing\n│   │   ├── exclusive/     # Pro plan feature example\n│   │   └── profile/       # User profile\n│   ├── api/               # API routes (if any)\n│   ├── layout.tsx         # Root layout with providers\n│   ├── page.tsx           # Landing page\n│   ├── global-error.tsx   # Global error boundary\n│   └── not-found.tsx      # 404 page\n│\n├── components/\n│   ├── ui/                # shadcn/ui components (50+ components)\n│   ├── layout/            # Layout components (sidebar, header, etc.)\n│   ├── forms/             # Field components (shadcn TanStack Form anatomy) + demos\n│   ├── themes/            # Theme system components\n│   ├── kbar/              # Command+K search bar\n│   ├── icons.tsx          # Icon registry\n│   └── ...\n│\n├── features/              # Feature-based modules\n│   ├── auth/              # Authentication components\n│   ├── overview/          # Dashboard analytics\n│   ├── products/          # Product management (React Query + nuqs)\n│   │   ├── api/\n│   │   │   ├── types.ts   # Type contract (response shapes, filters, payloads)\n│   │   │   ├── service.ts # Data access layer (swap for your backend)\n│   │   │   └── queries.ts # React Query options + key factories\n│   │   ├── components/    # Listing, form, table components\n│   │   ├── schemas/       # Zod schemas\n│   │   └── constants/     # Filter options\n│   ├── users/             # User management (React Query + nuqs)\n│   │   ├── api/           # Same pattern: types.ts → service.ts → queries.ts\n│   │   └── components/    # Listing, table components\n│   ├── react-query-demo/  # React Query showcase (Pokemon API)\n│   ├── kanban/            # Kanban board with dnd-kit\n│   ├── chat/              # Messaging UI (conversations, bubbles, composer)\n│   ├── ai-chat/           # Scripted useChat streaming demo\n│   ├── notifications/     # Notification center & store\n│   └── profile/           # Profile management\n│\n├── config/                # Configuration files\n│   ├── nav-config.ts      # Navigation with RBAC\n│   └── ...\n│\n├── hooks/                 # Custom React hooks\n│   ├── use-nav.ts         # RBAC navigation filtering\n│   ├── use-data-table.ts  # Data table state\n│   └── ...\n│\n├── lib/                   # Utility functions\n│   ├── utils.ts           # cn() and formatters\n│   ├── searchparams.ts    # Search param utilities\n│   └── ...\n│\n├── types/                 # TypeScript type definitions\n│   └── index.ts           # Core types (NavItem, etc.)\n│\n└── styles/                # Global styles\n    ├── globals.css        # Tailwind imports + view transitions\n    ├── theme.css          # Theme imports\n    └── themes/            # Individual theme files\n\n/docs                      # Documentation\n│   ├── clerk_setup.md     # Clerk configuration guide\n│   ├── nav-rbac.md        # Navigation RBAC documentation\n│   └── themes.md          # Theme customization guide\n\n/scripts                   # Dev tooling\n    ├── cleanup.js         # Feature removal, run via `bun run cleanup` (templates in cleanup-templates/, typechecked)\n    └── cleanup-templates/ # Replacement files cleanup.js copies into the repo\n\nDockerfile                 # Node.js production Dockerfile\nDockerfile.bun             # Bun production Dockerfile\n.dockerignore              # Docker build exclusions\n```\n\n---\n\n## Build & Development Commands\n\n```bash\n# Install dependencies\nbun install\n\n# Development server\nbun run dev          # Starts at http://localhost:3000\n\n# Build for production\nbun run build\n\n# Start production server\nbun run start\n\n# Linting\nbun run lint         # Run ESLint\nbun run lint:fix     # Fix ESLint issues and format\nbun run lint:strict  # Zero warnings tolerance\n\nbun run typecheck    # tsc --noEmit\n\n# Formatting\nbun run format       # Format with Prettier\nbun run format:check # Check formatting\n\n# Git hooks\nbun run prepare      # Install Husky hooks\n```\n\n---\n\n## Environment Configuration\n\nCopy `env.example.txt` to `.env.local` and configure:\n\n### Required for Authentication (Clerk)\n\n```env\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...\nCLERK_SECRET_KEY=sk_...\n\n# Redirect URLs\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=\"/auth/sign-in\"\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=\"/auth/sign-up\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=\"/dashboard/overview\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=\"/dashboard/overview\"\n```\n\n### Optional for Error Tracking (Sentry)\n\n```env\nNEXT_PUBLIC_SENTRY_DSN=https://...@....ingest.sentry.io/...\nNEXT_PUBLIC_SENTRY_ORG=your-org\nNEXT_PUBLIC_SENTRY_PROJECT=your-project\nSENTRY_AUTH_TOKEN=sntrys_...\nNEXT_PUBLIC_SENTRY_DISABLED=\"false\"  # Set to \"true\" to disable in dev\n```\n\n**Note**: Clerk supports \"keyless mode\" — run `npx clerk@latest init` to provision a development instance in seconds (no account needed; keys are written to `.env.local`).\n\n---\n\n## Code Style Guidelines\n\n### TypeScript\n\n- Strict mode enabled\n- Use explicit return types for public functions\n- Prefer interface over type for object definitions\n- Use `@/*` alias for imports from src\n\n### Formatting (Prettier)\n\n```json\n{\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"semi\": true,\n  \"trailingComma\": \"none\",\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\"\n}\n```\n\n### ESLint Rules\n\n- `@typescript-eslint/no-unused-vars`: warn\n- `no-console`: warn\n- `react-hooks/exhaustive-deps`: warn\n- `import/no-unresolved`: off (handled by TypeScript)\n\n### Component Conventions\n\n- Use function declarations for components: `function ComponentName() {}`\n- Props interface named `{ComponentName}Props`\n- shadcn/ui components use `cn()` utility for class merging\n- Server components by default, `'use client'` only when needed\n\n---\n\n## Theming System\n\nThe project uses a sophisticated multi-theme system with 10 built-in themes:\n\n- `vercel` (default)\n- `claude`\n- `discord`\n- `supabase`\n- `mono`\n- `notebook`\n- `light-green`\n- `zen`\n- `astro-vista`\n- `whatsapp`\n\n### Theme Files\n\n- CSS files: `src/styles/themes/{theme-name}.css`\n- Theme registry: `src/components/themes/theme.config.ts`\n- Font config: `src/components/themes/font.config.ts`\n- Active theme provider: `src/components/themes/active-theme.tsx`\n\n### Adding a New Theme\n\n1. Create `src/styles/themes/your-theme.css` with `[data-theme='your-theme']` selector\n2. Import in `src/styles/theme.css`\n3. Add to `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add fonts in `font.config.ts`\n5. (Optional) Set as default in `theme.config.ts`\n\nSee `docs/themes.md` for detailed theming guide.\n\n---\n\n## Navigation & RBAC System\n\n### Navigation Configuration\n\nNavigation is organized into groups in `src/config/nav-config.ts`:\n\n```typescript\nimport { NavGroup } from '@/types';\n\nexport const navGroups: NavGroup[] = [\n  {\n    label: 'Overview',\n    items: [\n      {\n        title: 'Dashboard',\n        url: '/dashboard/overview',\n        icon: 'dashboard',\n        shortcut: ['d', 'd'],\n        items: [],\n        access: { requireOrg: true } // RBAC check\n      }\n    ]\n  }\n];\n```\n\n### Access Control Properties\n\n- `requireOrg: boolean` - Requires active organization\n- `permission: string` - Requires specific permission\n- `role: string` - Requires specific role\n- `plan: string` - Requires specific subscription plan\n- `feature: string` - Requires specific feature\n\n### Client-Side Filtering\n\nThe `useFilteredNavItems()` hook in `src/hooks/use-nav.ts` filters navigation client-side using Clerk's `useOrganization()` and `useUser()` hooks. This is for UX only - actual security checks must happen server-side.\n\n---\n\n## Authentication Patterns\n\n### Protected Routes\n\nRoute protection is resource-based (Clerk's recommended pattern): `src/proxy.ts` runs a bare `clerkMiddleware()` to attach the auth context, and every dashboard `layout`/`page` and API route handler performs its own check.\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nexport default async function Page() {\n  await auth.protect(); // redirects signed-out users to sign-in\n  // ...\n}\n```\n\nAPI route handlers return 401 instead of redirecting:\n\n```tsx\nconst { userId } = await auth();\nif (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n```\n\nPages that require an organization:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\nimport { redirect } from 'next/navigation';\n\nexport default async function Page() {\n  await auth.protect();\n  const { orgId } = await auth();\n  if (!orgId) redirect('/dashboard/workspaces');\n  // ...\n}\n```\n\n### Plan/Feature Protection\n\nUse Clerk's `<Show>` component for client-side:\n\n```tsx\nimport { Show } from '@clerk/nextjs';\n\n<Show when={{ plan: 'pro' }} fallback={<UpgradePrompt />}>\n  <PremiumContent />\n</Show>;\n```\n\nUse `has()` function for server-side checks:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nconst { has } = await auth();\nconst hasFeature = has({ feature: 'premium_access' });\n```\n\n---\n\n## Data Fetching Patterns\n\n### Service Layer Architecture\n\nEach feature has a three-file API layer:\n\n```\nsrc/features/<name>/api/\n  types.ts      ← Type contract (response shapes, filters, payloads)\n  service.ts    ← Data access functions (the ONE file to swap for your backend)\n  queries.ts    ← React Query options + query key factories (stable, never changes)\n```\n\n**`service.ts` is the only file you modify when connecting to a real backend.** Queries and components import from it — they never change.\n\n#### Backend Patterns\n\n| Pattern                                            | How to implement                                                                            |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |\n| **Server Actions + ORM** (Prisma/Drizzle/Supabase) | Add `'use server'` at top of `service.ts`, call ORM directly                                |\n| **Route Handlers + ORM**                           | `service.ts` calls `/api/` routes via `apiClient`, route handlers call ORM                  |\n| **BFF** (Next.js proxies to Laravel/Go/etc.)       | `service.ts` calls `/api/` routes via `apiClient`, route handlers proxy to external backend |\n| **Direct external API** (frontend-only)            | `service.ts` calls external URL via `fetch()`                                               |\n| **Mock** (default)                                 | `service.ts` calls in-memory fake data stores                                               |\n\nRoute handlers at `src/app/api/` are ready for patterns 2 and 3. `src/lib/api-client.ts` provides a typed `fetch` wrapper.\n\n### Query Key Factories\n\nEach feature defines a key factory in `queries.ts` for type-safe, hierarchical cache invalidation:\n\n```tsx\nexport const entityKeys = {\n  all: ['entities'] as const,\n  list: (filters: EntityFilters) => [...entityKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...entityKeys.all, 'detail', id] as const\n};\n\n// Usage in queryOptions\nqueryKey: entityKeys.list(filters);\n\n// Usage in mutations — invalidate all entity queries\nqueryClient.invalidateQueries({ queryKey: entityKeys.all });\n```\n\n### React Query (Default for all new pages)\n\nThe project uses TanStack React Query with server-side prefetching and client-side cache management:\n\n1. **Query options** defined in `queries.ts` — shared between server prefetch and client hooks\n2. **Server prefetch** using `void queryClient.prefetchQuery()` + `HydrationBoundary` + `dehydrate` — `void` (fire-and-forget) is the standard TanStack pattern for Next.js App Router\n3. **Client fetch** using `useSuspenseQuery()` — integrates with React Suspense so prefetched data streams in without showing a loading skeleton on first load\n4. **Suspense boundary** wraps the client component — shows a fallback skeleton only on subsequent client-side navigations when cache is empty\n\n```tsx\n// Server component: prefetch + dehydrate\nconst queryClient = getQueryClient();\nvoid queryClient.prefetchQuery(entitiesQueryOptions(filters)); // void, not await\n\nreturn (\n  <HydrationBoundary state={dehydrate(queryClient)}>\n    <Suspense fallback={<Skeleton />}>\n      <EntityTable />\n    </Suspense>\n  </HydrationBoundary>\n);\n\n// Client component: useSuspenseQuery (not useQuery)\nconst { data } = useSuspenseQuery(entitiesQueryOptions(filters));\n```\n\n**Why `void` + `useSuspenseQuery`:**\n\n- `void` fires the prefetch without blocking the server component\n- `useSuspenseQuery` integrates with React Suspense — the pending query streams in via Next.js streaming SSR\n- With `<Suspense fallback={<Skeleton />}>`: skeleton shows immediately while data streams in — this is expected behavior, the skeleton IS the Suspense fallback during streaming\n- Without `<Suspense>` wrapper: no skeleton, but the previous page stays visible until data fully resolves (feels like a slow navigation)\n- Once data is cached (within `staleTime`), subsequent visits are instant — no skeleton\n\n**Why NOT `useQuery`:**\n\n- `useQuery` doesn't integrate with Suspense — returns `isLoading: true` and you must handle loading state manually\n- Hydrated pending queries from `void` prefetch won't prevent the loading state\n- Results in skeleton flash even when data is prefetched\n\n### Mutations\n\nComponents import service functions for mutations. Use query key factories for invalidation:\n\n```tsx\nimport { createEntity } from '../api/service';\nimport { entityKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createEntity(data),\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: entityKeys.all });\n    toast.success('Created');\n  }\n});\n```\n\n### URL State Management\n\nUse `nuqs` for search params state:\n\n- `searchParamsCache` (server) — reads params in server components\n- `useQueryState` (client) — reads/writes params in client components with `shallow: true`\n\n### Data Tables\n\nTables use TanStack Table v9 (DiceUI data table) with React Query:\n\n- Query options in `features/*/api/queries.ts`\n- Column definitions in `features/*/components/*-tables/columns.tsx`, typed `ColumnDef<DataTableFeatures, T>[]`\n- Table component in `src/components/data-table/data-table.tsx`\n- Feature set + `DataTableFeatures` type in `src/lib/data-table-features.ts`; URL parsers in `src/lib/data-table-parsers.ts`\n- Column pinning via `initialState.columnPinning` (`{ start: [], end: ['actions'] }`) in `useDataTable`\n\n---\n\n## Error Handling & Monitoring\n\n### Sentry Integration\n\nSentry is configured for both client and server:\n\n- Client config: `src/instrumentation-client.ts`\n- Server config: `src/instrumentation.ts`\n- Global error: `src/app/global-error.tsx`\n\nTo disable Sentry in development:\n\n```env\nNEXT_PUBLIC_SENTRY_DISABLED=\"true\"\n```\n\n### Error Boundaries\n\n- `global-error.tsx` - Catches all errors, reports to Sentry\n- Parallel route `error.tsx` files for specific sections\n\n---\n\n## Testing Strategy\n\n**Note**: This project does not include a test suite by default. Consider adding:\n\n- **Unit tests**: Vitest or Jest for utilities and hooks\n- **Component tests**: React Testing Library for UI components\n- **E2E tests**: Playwright for critical user flows\n\nRecommended test locations:\n\n```\n/src\n  /__tests__           # Unit tests\n  /features/*/tests    # Feature tests\n/e2e                   # Playwright tests\n```\n\n---\n\n## Deployment\n\nCanonical guide: [docs/deployment.md](./docs/deployment.md) (Vercel, production environment variables, Docker).\n\n### Vercel (Recommended)\n\n1. Connect repository to Vercel\n2. Add environment variables in dashboard\n3. Deploy\n\n### Environment Variables for Production\n\nEnsure these are set in your deployment platform:\n\n- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`\n- `CLERK_SECRET_KEY`\n- All `NEXT_PUBLIC_*` variables for client-side access\n- `SENTRY_*` variables if using error tracking\n\n### Docker\n\nProduction-ready Dockerfiles are included:\n\n- `Dockerfile` — Node.js-based\n- `Dockerfile.bun` — Bun-based\n\nBoth use `output: 'standalone'` in `next.config.ts`. Pass `NEXT_PUBLIC_*` vars as `--build-arg` at build time, and runtime secrets via `-e` at run time.\n\n### Build Considerations\n\n- Output: `standalone` (optimized for Docker/self-hosting)\n- Images: Configured for `api.slingacademy.com`, `img.clerk.com`, `clerk.com`\n- Sentry source maps uploaded automatically in CI\n\n---\n\n## Feature Cleanup System\n\nA single `scripts/cleanup.js` file handles removal of optional features:\n\n```bash\n# Interactive mode — prompts for each feature\nnode scripts/cleanup.js --interactive\n\n# Remove specific features\nnode scripts/cleanup.js clerk           # Remove auth/org/billing\nnode scripts/cleanup.js kanban          # Remove kanban board\nnode scripts/cleanup.js chat            # Remove messaging UI\nnode scripts/cleanup.js ai-chat         # Remove AI chat demo\nnode scripts/cleanup.js notifications   # Remove notification center\nnode scripts/cleanup.js themes          # Keep one theme, remove rest\nnode scripts/cleanup.js sentry          # Remove error tracking\n\n# Remove multiple at once\nnode scripts/cleanup.js kanban chat notifications\n\n# Preview without changing files\nnode scripts/cleanup.js --dry-run kanban\n\n# List all features\nnode scripts/cleanup.js --list\n```\n\n**Safety**: Script requires git repository with at least one commit. Use `--force` to skip.\n\nReplacement files live in `scripts/cleanup-templates/` as real `.ts`/`.tsx` files typechecked by `tsc` and `next build`, so template rot fails loudly instead of shipping broken code.\n\nAfter cleanup, delete `scripts/cleanup.js` and `scripts/cleanup-templates/` — the dev server message auto-cleans on next start.\n\n---\n\n## Icon System\n\n**All icons come from a single source: `src/components/icons.tsx`.**\n\nThe project uses `@tabler/icons-react` as the sole icon package. Every icon is re-exported through a centralized `Icons` object — **never import directly from `@tabler/icons-react` or any other icon package**.\n\n### Usage\n\n```tsx\nimport { Icons } from '@/components/icons';\n\n// In JSX\n<Icons.search className='h-4 w-4' />\n<Icons.chevronRight className='h-4 w-4' />\n\n// Passing as a prop\nicon={Icons.check}\n```\n\n### Adding a New Icon\n\n1. Import the tabler icon in `src/components/icons.tsx`\n2. Add a semantic key to the `Icons` object\n3. Use `Icons.yourKey` everywhere — never the raw import\n\n```tsx\n// In src/components/icons.tsx\nimport { IconNewIcon } from '@tabler/icons-react';\n\nexport const Icons = {\n  // ...existing icons\n  newIcon: IconNewIcon\n};\n```\n\n### Available Icon Categories\n\n| Category        | Example Keys                                                                  |\n| --------------- | ----------------------------------------------------------------------------- |\n| General         | `check`, `close`, `search`, `settings`, `trash`, `spinner`, `info`, `warning` |\n| Navigation      | `chevronDown`, `chevronLeft`, `chevronRight`, `chevronUp`, `chevronsUpDown`   |\n| Layout          | `dashboard`, `kanban`, `panelLeft`                                            |\n| User            | `user`, `account`, `profile`, `teams`                                         |\n| Communication   | `chat`, `notification`, `phone`, `video`, `send`                              |\n| Files           | `page`, `post`, `media`, `fileTypePdf`, `fileTypeDoc`                         |\n| Actions         | `add`, `edit`, `upload`, `share`, `login`, `logout`                           |\n| Theme           | `sun`, `moon`, `brightness`, `laptop`, `palette`                              |\n| Text formatting | `bold`, `italic`, `underline`, `text`                                         |\n| Data / Charts   | `trendingUp`, `trendingDown`, `eyeOff`, `adjustments`                         |\n\n### Icon Showcase Page\n\nBrowse all available icons at `/dashboard/elements/icons` — a searchable grid of every icon in the registry.\n\n### Why This Pattern?\n\n- **Single source of truth** — swap icon packages by editing one file\n- **Semantic naming** — `Icons.trash` is clearer than `IconTrash` scattered across files\n- **Discoverability** — autocomplete on `Icons.` shows every available icon\n- **No direct dependencies** — components never couple to a specific icon package\n\n---\n\n## Common Development Tasks\n\n### Adding a New Feature (End-to-End)\n\n1. Create `src/features/<name>/api/types.ts` — response types, filter types, mutation payloads\n2. Create `src/features/<name>/api/service.ts` — data access functions (mock by default)\n3. Create `src/features/<name>/api/queries.ts` — query key factory + `queryOptions`\n4. Create page route: `src/app/dashboard/<name>/page.tsx`\n5. Create feature components in `src/features/<name>/components/`\n6. Add navigation item in `src/config/nav-config.ts`\n7. (Optional) Add route handlers in `src/app/api/<name>/` for REST API patterns\n8. (Optional) Register new icon in `src/components/icons.tsx`\n\n### Adding a New API Route\n\n1. Create: `src/app/api/my-route/route.ts`\n2. Export HTTP method handlers: `GET`, `POST`, etc.\n3. For BFF pattern: proxy requests to your external backend\n\n### Adding a shadcn Component\n\n```bash\nnpx shadcn add component-name\n```\n\n### Adding a New Theme\n\nSee \"Theming System\" section above or `docs/themes.md`.\n\n---\n\n## Troubleshooting\n\n### Common Issues\n\n**Build fails with Tailwind errors**\n\n- Ensure using Tailwind CSS v4 syntax (`@import 'tailwindcss'`)\n- Check `postcss.config.js` uses `@tailwindcss/postcss`\n\n**Clerk keyless mode popup**\n\n- Run `npx clerk@latest init` to provision a dev instance in seconds (no account needed)\n- It writes keys to `.env.local`; later you can claim application or set env variables\n\n**Theme not applying**\n\n- Check theme name matches in CSS `[data-theme]` and `theme.config.ts`\n- Verify theme CSS is imported in `theme.css`\n\n**Navigation items not showing**\n\n- Check `access` property in nav config\n- Verify user has required org/permission/role\n\n---\n\n## External Documentation\n\n- [Next.js App Router](https://nextjs.org/docs/app)\n- [Clerk Next.js SDK](https://clerk.com/docs/references/nextjs)\n- [shadcn/ui](https://ui.shadcn.com/docs)\n- [Tailwind CSS v4](https://tailwindcss.com/docs)\n- [TanStack Table](https://tanstack.com/table/latest)\n- [Sentry Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/)\n\n---\n\n## Notes for AI Agents\n\n1. **Always use `cn()` for className merging** - never concatenate strings manually\n2. **Respect the feature-based structure** - put new feature code in `src/features/`\n3. **Server components by default** - only add `'use client'` when using browser APIs or React hooks\n4. **Type safety first** - avoid `any`, prefer explicit types\n5. **Follow existing patterns** - look at similar components before creating new ones\n6. **Environment variables** - prefix with `NEXT_PUBLIC_` for client-side access\n7. **shadcn components** - don't modify files in `src/components/ui/` directly; extend them instead\n8. **Icons** - NEVER import icons directly from `@tabler/icons-react` or any other icon package. All icons must be registered in `src/components/icons.tsx` and imported as `import { Icons } from '@/components/icons'`. To add a new icon: add the tabler import to `icons.tsx`, add a semantic key to the `Icons` object, then use `Icons.keyName` in your component.\n9. **Page headers** - Always use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`) for page headers. Never import `<Heading>` manually in pages — `PageContainer` handles that internally.\n10. **Forms** - Use `useAppForm` from `@/lib/form` with `form.AppField` rendering the shared field components (`field.TextField`, `field.SelectField`, …) from `@/components/forms/fields`. Each component follows the official shadcn TanStack Form anatomy; drop down to raw `form.Field` render props for one-off custom fields. Never use `useState` inside a render prop — extract stateful controls into components.\n11. **Button loading** - Use `<Button isLoading={isPending}>` for loading states. Uses CSS Grid overlap trick for zero layout shift. When `isLoading` is not passed, button behaves as default shadcn. `SubmitButton` in forms handles this automatically via form `isSubmitting` state.\n12. **Data layer** - Always go through the service layer: `types.ts` → `service.ts` → `queries.ts`. Components import types from `types.ts`, functions from `service.ts`, query options from `queries.ts`. Never import from `@/constants/mock-api*` directly in components.\n","CLAUDE.md":"# CLAUDE.md\n\nThis is a Next.js 16 + shadcn/ui admin dashboard starter kit.\n\n## Key References\n\n- **[AGENTS.md](./AGENTS.md)** — Full project overview, tech stack, structure, conventions, data fetching patterns, deployment\n- **[docs/forms.md](./docs/forms.md)** — Form system: TanStack Form + Zod, composable fields, validation, multi-step, sheet/dialog forms\n- **[docs/themes.md](./docs/themes.md)** — Theme system: OKLCH colors, adding themes, font config\n- **[docs/nav-rbac.md](./docs/nav-rbac.md)** — Navigation RBAC: access control, Clerk integration\n- **[docs/clerk_setup.md](./docs/clerk_setup.md)** — Clerk auth setup: organizations, billing, environment variables\n- **[docs/deployment.md](./docs/deployment.md)** — Deployment: Vercel, production environment variables, Docker\n\n## Critical Conventions\n\n- **React Query** for all data fetching — `void prefetchQuery()` on server + `useSuspenseQuery` on client (standard TanStack pattern), `useMutation` for forms, `HydrationBoundary` + `dehydrate` for hydration, `<Suspense fallback>` for streaming\n- **API layer** per feature — `api/types.ts` → `api/service.ts` → `api/queries.ts`; queries use key factories (`entityKeys.all/list/detail`); components import from service and queries, never from mock APIs directly\n- **nuqs** for URL search params — `searchParamsCache` on server, `useQueryStates` on client, use `getSortingStateParser` for sort (same parser as `useDataTable`)\n- **Icons** — only import from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` from `@/lib/form` (TanStack `createFormHook`) + `form.AppField` rendering the field components in `@/components/forms/fields` (`field.TextField`, `field.SelectField`, …); each component is the shadcn TanStack Form doc anatomy; raw `form.Field` for one-off custom fields; form-level Zod `onSubmit` validators\n- **Page headers** — use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`), never import `<Heading>` manually\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/AGENTS.md","title":"AI Agent Protocol & Instructions","category":"root-instruction","format":"markdown","content":"# AGENTS.md - AI Coding Agent Reference\n\nThis file provides essential information for AI coding agents working on this project. It contains project-specific details, conventions, and guidelines that complement the README.\n\n---\n\n## Project Overview\n\n**Next.js Admin Dashboard Starter** is a production-ready admin dashboard template built with:\n\n- **Framework**: Next.js 16 (App Router)\n- **Language**: TypeScript 7\n- **Styling**: Tailwind CSS v4\n- **UI Components**: shadcn/ui (base-nova style, Base UI primitives)\n- **Authentication**: Clerk (with Organizations/Billing support)\n- **Error Tracking**: Sentry\n- **Charts**: Recharts\n- **Containerization**: Docker (Node.js & Bun Dockerfiles)\n- **Package Manager**: Bun (preferred) or npm\n\nThe project follows a feature-based folder structure designed for scalability in SaaS applications, internal tools, and admin panels.\n\n---\n\n## Technology Stack Details\n\n### Core Framework & Runtime\n\n- Next.js 16.4 with App Router\n- React 19.3\n- TypeScript 7 (native compiler) with strict mode enabled\n\n### Styling & UI\n\n- Tailwind CSS v4 (using `@import 'tailwindcss'` syntax)\n- PostCSS with `@tailwindcss/postcss` plugin\n- shadcn/ui component library (Base UI primitives)\n- CSS custom properties for theming (OKLCH color format)\n\n### State Management\n\n- Zustand 5.x for local UI state in the stateful demo features\n- Nuqs for URL search params state management\n- TanStack Form + Zod for form handling (`createFormHook` + shadcn `Field`-anatomy components)\n\n### Data Fetching & Caching\n\n- TanStack React Query for data fetching, caching, and mutations\n- Server-side prefetching with `HydrationBoundary` + `dehydrate`\n- Client-side `useQuery` + nuqs `shallow: true` for tables (no RSC round-trips on pagination/filter)\n- `useMutation` + `invalidateQueries` for form submissions\n- Query client singleton in `src/lib/query-client.ts`\n\n### Authentication & Authorization\n\n- Clerk for authentication and user management\n- Clerk Organizations for multi-tenant workspaces\n- Clerk Billing for subscription management (B2B)\n- Client-side RBAC for navigation visibility\n\n### Data & APIs\n\n- TanStack Table for data tables\n- TanStack React Query for data fetching and mutations\n- Recharts for analytics/charts\n- Service layer per feature (`api/types.ts` → `api/service.ts` → `api/queries.ts`)\n- Route handlers at `src/app/api/` (for Route Handler or BFF patterns)\n- Mock data in `src/constants/mock-api*.ts` (default, swap via service layer)\n- API client utility in `src/lib/api-client.ts` (for fetch-based patterns)\n\n### Development Tools\n\n- ESLint 8.x with Next.js core-web-vitals config\n- Prettier 3.x with prettier-plugin-tailwindcss\n- Husky for git hooks\n- lint-staged for pre-commit formatting\n\n---\n\n## Project Structure\n\n```\n/src\n├── app/                    # Next.js App Router\n│   ├── auth/              # Authentication routes (sign-in, sign-up)\n│   ├── dashboard/         # Dashboard routes\n│   │   ├── overview/      # Parallel routes (@area_stats, @bar_stats, etc.)\n│   │   ├── product/       # Product management pages\n│   │   ├── kanban/        # Kanban board page\n│   │   ├── chat/          # Messaging page\n│   │   ├── ai-chat/       # AI chat streaming demo\n│   │   ├── notifications/ # Notifications page\n│   │   ├── workspaces/    # Organization management\n│   │   ├── billing/       # Subscription billing\n│   │   ├── exclusive/     # Pro plan feature example\n│   │   └── profile/       # User profile\n│   ├── api/               # API routes (if any)\n│   ├── layout.tsx         # Root layout with providers\n│   ├── page.tsx           # Landing page\n│   ├── global-error.tsx   # Global error boundary\n│   └── not-found.tsx      # 404 page\n│\n├── components/\n│   ├── ui/                # shadcn/ui components (50+ components)\n│   ├── layout/            # Layout components (sidebar, header, etc.)\n│   ├── forms/             # Field components (shadcn TanStack Form anatomy) + demos\n│   ├── themes/            # Theme system components\n│   ├── kbar/              # Command+K search bar\n│   ├── icons.tsx          # Icon registry\n│   └── ...\n│\n├── features/              # Feature-based modules\n│   ├── auth/              # Authentication components\n│   ├── overview/          # Dashboard analytics\n│   ├── products/          # Product management (React Query + nuqs)\n│   │   ├── api/\n│   │   │   ├── types.ts   # Type contract (response shapes, filters, payloads)\n│   │   │   ├── service.ts # Data access layer (swap for your backend)\n│   │   │   └── queries.ts # React Query options + key factories\n│   │   ├── components/    # Listing, form, table components\n│   │   ├── schemas/       # Zod schemas\n│   │   └── constants/     # Filter options\n│   ├── users/             # User management (React Query + nuqs)\n│   │   ├── api/           # Same pattern: types.ts → service.ts → queries.ts\n│   │   └── components/    # Listing, table components\n│   ├── react-query-demo/  # React Query showcase (Pokemon API)\n│   ├── kanban/            # Kanban board with dnd-kit\n│   ├── chat/              # Messaging UI (conversations, bubbles, composer)\n│   ├── ai-chat/           # Scripted useChat streaming demo\n│   ├── notifications/     # Notification center & store\n│   └── profile/           # Profile management\n│\n├── config/                # Configuration files\n│   ├── nav-config.ts      # Navigation with RBAC\n│   └── ...\n│\n├── hooks/                 # Custom React hooks\n│   ├── use-nav.ts         # RBAC navigation filtering\n│   ├── use-data-table.ts  # Data table state\n│   └── ...\n│\n├── lib/                   # Utility functions\n│   ├── utils.ts           # cn() and formatters\n│   ├── searchparams.ts    # Search param utilities\n│   └── ...\n│\n├── types/                 # TypeScript type definitions\n│   └── index.ts           # Core types (NavItem, etc.)\n│\n└── styles/                # Global styles\n    ├── globals.css        # Tailwind imports + view transitions\n    ├── theme.css          # Theme imports\n    └── themes/            # Individual theme files\n\n/docs                      # Documentation\n│   ├── clerk_setup.md     # Clerk configuration guide\n│   ├── nav-rbac.md        # Navigation RBAC documentation\n│   └── themes.md          # Theme customization guide\n\n/scripts                   # Dev tooling\n    ├── cleanup.js         # Feature removal, run via `bun run cleanup` (templates in cleanup-templates/, typechecked)\n    └── cleanup-templates/ # Replacement files cleanup.js copies into the repo\n\nDockerfile                 # Node.js production Dockerfile\nDockerfile.bun             # Bun production Dockerfile\n.dockerignore              # Docker build exclusions\n```\n\n---\n\n## Build & Development Commands\n\n```bash\n# Install dependencies\nbun install\n\n# Development server\nbun run dev          # Starts at http://localhost:3000\n\n# Build for production\nbun run build\n\n# Start production server\nbun run start\n\n# Linting\nbun run lint         # Run ESLint\nbun run lint:fix     # Fix ESLint issues and format\nbun run lint:strict  # Zero warnings tolerance\n\nbun run typecheck    # tsc --noEmit\n\n# Formatting\nbun run format       # Format with Prettier\nbun run format:check # Check formatting\n\n# Git hooks\nbun run prepare      # Install Husky hooks\n```\n\n---\n\n## Environment Configuration\n\nCopy `env.example.txt` to `.env.local` and configure:\n\n### Required for Authentication (Clerk)\n\n```env\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...\nCLERK_SECRET_KEY=sk_...\n\n# Redirect URLs\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=\"/auth/sign-in\"\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=\"/auth/sign-up\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=\"/dashboard/overview\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=\"/dashboard/overview\"\n```\n\n### Optional for Error Tracking (Sentry)\n\n```env\nNEXT_PUBLIC_SENTRY_DSN=https://...@....ingest.sentry.io/...\nNEXT_PUBLIC_SENTRY_ORG=your-org\nNEXT_PUBLIC_SENTRY_PROJECT=your-project\nSENTRY_AUTH_TOKEN=sntrys_...\nNEXT_PUBLIC_SENTRY_DISABLED=\"false\"  # Set to \"true\" to disable in dev\n```\n\n**Note**: Clerk supports \"keyless mode\" — run `npx clerk@latest init` to provision a development instance in seconds (no account needed; keys are written to `.env.local`).\n\n---\n\n## Code Style Guidelines\n\n### TypeScript\n\n- Strict mode enabled\n- Use explicit return types for public functions\n- Prefer interface over type for object definitions\n- Use `@/*` alias for imports from src\n\n### Formatting (Prettier)\n\n```json\n{\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"semi\": true,\n  \"trailingComma\": \"none\",\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\"\n}\n```\n\n### ESLint Rules\n\n- `@typescript-eslint/no-unused-vars`: warn\n- `no-console`: warn\n- `react-hooks/exhaustive-deps`: warn\n- `import/no-unresolved`: off (handled by TypeScript)\n\n### Component Conventions\n\n- Use function declarations for components: `function ComponentName() {}`\n- Props interface named `{ComponentName}Props`\n- shadcn/ui components use `cn()` utility for class merging\n- Server components by default, `'use client'` only when needed\n\n---\n\n## Theming System\n\nThe project uses a sophisticated multi-theme system with 10 built-in themes:\n\n- `vercel` (default)\n- `claude`\n- `discord`\n- `supabase`\n- `mono`\n- `notebook`\n- `light-green`\n- `zen`\n- `astro-vista`\n- `whatsapp`\n\n### Theme Files\n\n- CSS files: `src/styles/themes/{theme-name}.css`\n- Theme registry: `src/components/themes/theme.config.ts`\n- Font config: `src/components/themes/font.config.ts`\n- Active theme provider: `src/components/themes/active-theme.tsx`\n\n### Adding a New Theme\n\n1. Create `src/styles/themes/your-theme.css` with `[data-theme='your-theme']` selector\n2. Import in `src/styles/theme.css`\n3. Add to `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add fonts in `font.config.ts`\n5. (Optional) Set as default in `theme.config.ts`\n\nSee `docs/themes.md` for detailed theming guide.\n\n---\n\n## Navigation & RBAC System\n\n### Navigation Configuration\n\nNavigation is organized into groups in `src/config/nav-config.ts`:\n\n```typescript\nimport { NavGroup } from '@/types';\n\nexport const navGroups: NavGroup[] = [\n  {\n    label: 'Overview',\n    items: [\n      {\n        title: 'Dashboard',\n        url: '/dashboard/overview',\n        icon: 'dashboard',\n        shortcut: ['d', 'd'],\n        items: [],\n        access: { requireOrg: true } // RBAC check\n      }\n    ]\n  }\n];\n```\n\n### Access Control Properties\n\n- `requireOrg: boolean` - Requires active organization\n- `permission: string` - Requires specific permission\n- `role: string` - Requires specific role\n- `plan: string` - Requires specific subscription plan\n- `feature: string` - Requires specific feature\n\n### Client-Side Filtering\n\nThe `useFilteredNavItems()` hook in `src/hooks/use-nav.ts` filters navigation client-side using Clerk's `useOrganization()` and `useUser()` hooks. This is for UX only - actual security checks must happen server-side.\n\n---\n\n## Authentication Patterns\n\n### Protected Routes\n\nRoute protection is resource-based (Clerk's recommended pattern): `src/proxy.ts` runs a bare `clerkMiddleware()` to attach the auth context, and every dashboard `layout`/`page` and API route handler performs its own check.\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nexport default async function Page() {\n  await auth.protect(); // redirects signed-out users to sign-in\n  // ...\n}\n```\n\nAPI route handlers return 401 instead of redirecting:\n\n```tsx\nconst { userId } = await auth();\nif (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n```\n\nPages that require an organization:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\nimport { redirect } from 'next/navigation';\n\nexport default async function Page() {\n  await auth.protect();\n  const { orgId } = await auth();\n  if (!orgId) redirect('/dashboard/workspaces');\n  // ...\n}\n```\n\n### Plan/Feature Protection\n\nUse Clerk's `<Show>` component for client-side:\n\n```tsx\nimport { Show } from '@clerk/nextjs';\n\n<Show when={{ plan: 'pro' }} fallback={<UpgradePrompt />}>\n  <PremiumContent />\n</Show>;\n```\n\nUse `has()` function for server-side checks:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nconst { has } = await auth();\nconst hasFeature = has({ feature: 'premium_access' });\n```\n\n---\n\n## Data Fetching Patterns\n\n### Service Layer Architecture\n\nEach feature has a three-file API layer:\n\n```\nsrc/features/<name>/api/\n  types.ts      ← Type contract (response shapes, filters, payloads)\n  service.ts    ← Data access functions (the ONE file to swap for your backend)\n  queries.ts    ← React Query options + query key factories (stable, never changes)\n```\n\n**`service.ts` is the only file you modify when connecting to a real backend.** Queries and components import from it — they never change.\n\n#### Backend Patterns\n\n| Pattern                                            | How to implement                                                                            |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |\n| **Server Actions + ORM** (Prisma/Drizzle/Supabase) | Add `'use server'` at top of `service.ts`, call ORM directly                                |\n| **Route Handlers + ORM**                           | `service.ts` calls `/api/` routes via `apiClient`, route handlers call ORM                  |\n| **BFF** (Next.js proxies to Laravel/Go/etc.)       | `service.ts` calls `/api/` routes via `apiClient`, route handlers proxy to external backend |\n| **Direct external API** (frontend-only)            | `service.ts` calls external URL via `fetch()`                                               |\n| **Mock** (default)                                 | `service.ts` calls in-memory fake data stores                                               |\n\nRoute handlers at `src/app/api/` are ready for patterns 2 and 3. `src/lib/api-client.ts` provides a typed `fetch` wrapper.\n\n### Query Key Factories\n\nEach feature defines a key factory in `queries.ts` for type-safe, hierarchical cache invalidation:\n\n```tsx\nexport const entityKeys = {\n  all: ['entities'] as const,\n  list: (filters: EntityFilters) => [...entityKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...entityKeys.all, 'detail', id] as const\n};\n\n// Usage in queryOptions\nqueryKey: entityKeys.list(filters);\n\n// Usage in mutations — invalidate all entity queries\nqueryClient.invalidateQueries({ queryKey: entityKeys.all });\n```\n\n### React Query (Default for all new pages)\n\nThe project uses TanStack React Query with server-side prefetching and client-side cache management:\n\n1. **Query options** defined in `queries.ts` — shared between server prefetch and client hooks\n2. **Server prefetch** using `void queryClient.prefetchQuery()` + `HydrationBoundary` + `dehydrate` — `void` (fire-and-forget) is the standard TanStack pattern for Next.js App Router\n3. **Client fetch** using `useSuspenseQuery()` — integrates with React Suspense so prefetched data streams in without showing a loading skeleton on first load\n4. **Suspense boundary** wraps the client component — shows a fallback skeleton only on subsequent client-side navigations when cache is empty\n\n```tsx\n// Server component: prefetch + dehydrate\nconst queryClient = getQueryClient();\nvoid queryClient.prefetchQuery(entitiesQueryOptions(filters)); // void, not await\n\nreturn (\n  <HydrationBoundary state={dehydrate(queryClient)}>\n    <Suspense fallback={<Skeleton />}>\n      <EntityTable />\n    </Suspense>\n  </HydrationBoundary>\n);\n\n// Client component: useSuspenseQuery (not useQuery)\nconst { data } = useSuspenseQuery(entitiesQueryOptions(filters));\n```\n\n**Why `void` + `useSuspenseQuery`:**\n\n- `void` fires the prefetch without blocking the server component\n- `useSuspenseQuery` integrates with React Suspense — the pending query streams in via Next.js streaming SSR\n- With `<Suspense fallback={<Skeleton />}>`: skeleton shows immediately while data streams in — this is expected behavior, the skeleton IS the Suspense fallback during streaming\n- Without `<Suspense>` wrapper: no skeleton, but the previous page stays visible until data fully resolves (feels like a slow navigation)\n- Once data is cached (within `staleTime`), subsequent visits are instant — no skeleton\n\n**Why NOT `useQuery`:**\n\n- `useQuery` doesn't integrate with Suspense — returns `isLoading: true` and you must handle loading state manually\n- Hydrated pending queries from `void` prefetch won't prevent the loading state\n- Results in skeleton flash even when data is prefetched\n\n### Mutations\n\nComponents import service functions for mutations. Use query key factories for invalidation:\n\n```tsx\nimport { createEntity } from '../api/service';\nimport { entityKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createEntity(data),\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: entityKeys.all });\n    toast.success('Created');\n  }\n});\n```\n\n### URL State Management\n\nUse `nuqs` for search params state:\n\n- `searchParamsCache` (server) — reads params in server components\n- `useQueryState` (client) — reads/writes params in client components with `shallow: true`\n\n### Data Tables\n\nTables use TanStack Table v9 (DiceUI data table) with React Query:\n\n- Query options in `features/*/api/queries.ts`\n- Column definitions in `features/*/components/*-tables/columns.tsx`, typed `ColumnDef<DataTableFeatures, T>[]`\n- Table component in `src/components/data-table/data-table.tsx`\n- Feature set + `DataTableFeatures` type in `src/lib/data-table-features.ts`; URL parsers in `src/lib/data-table-parsers.ts`\n- Column pinning via `initialState.columnPinning` (`{ start: [], end: ['actions'] }`) in `useDataTable`\n\n---\n\n## Error Handling & Monitoring\n\n### Sentry Integration\n\nSentry is configured for both client and server:\n\n- Client config: `src/instrumentation-client.ts`\n- Server config: `src/instrumentation.ts`\n- Global error: `src/app/global-error.tsx`\n\nTo disable Sentry in development:\n\n```env\nNEXT_PUBLIC_SENTRY_DISABLED=\"true\"\n```\n\n### Error Boundaries\n\n- `global-error.tsx` - Catches all errors, reports to Sentry\n- Parallel route `error.tsx` files for specific sections\n\n---\n\n## Testing Strategy\n\n**Note**: This project does not include a test suite by default. Consider adding:\n\n- **Unit tests**: Vitest or Jest for utilities and hooks\n- **Component tests**: React Testing Library for UI components\n- **E2E tests**: Playwright for critical user flows\n\nRecommended test locations:\n\n```\n/src\n  /__tests__           # Unit tests\n  /features/*/tests    # Feature tests\n/e2e                   # Playwright tests\n```\n\n---\n\n## Deployment\n\nCanonical guide: [docs/deployment.md](./docs/deployment.md) (Vercel, production environment variables, Docker).\n\n### Vercel (Recommended)\n\n1. Connect repository to Vercel\n2. Add environment variables in dashboard\n3. Deploy\n\n### Environment Variables for Production\n\nEnsure these are set in your deployment platform:\n\n- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`\n- `CLERK_SECRET_KEY`\n- All `NEXT_PUBLIC_*` variables for client-side access\n- `SENTRY_*` variables if using error tracking\n\n### Docker\n\nProduction-ready Dockerfiles are included:\n\n- `Dockerfile` — Node.js-based\n- `Dockerfile.bun` — Bun-based\n\nBoth use `output: 'standalone'` in `next.config.ts`. Pass `NEXT_PUBLIC_*` vars as `--build-arg` at build time, and runtime secrets via `-e` at run time.\n\n### Build Considerations\n\n- Output: `standalone` (optimized for Docker/self-hosting)\n- Images: Configured for `api.slingacademy.com`, `img.clerk.com`, `clerk.com`\n- Sentry source maps uploaded automatically in CI\n\n---\n\n## Feature Cleanup System\n\nA single `scripts/cleanup.js` file handles removal of optional features:\n\n```bash\n# Interactive mode — prompts for each feature\nnode scripts/cleanup.js --interactive\n\n# Remove specific features\nnode scripts/cleanup.js clerk           # Remove auth/org/billing\nnode scripts/cleanup.js kanban          # Remove kanban board\nnode scripts/cleanup.js chat            # Remove messaging UI\nnode scripts/cleanup.js ai-chat         # Remove AI chat demo\nnode scripts/cleanup.js notifications   # Remove notification center\nnode scripts/cleanup.js themes          # Keep one theme, remove rest\nnode scripts/cleanup.js sentry          # Remove error tracking\n\n# Remove multiple at once\nnode scripts/cleanup.js kanban chat notifications\n\n# Preview without changing files\nnode scripts/cleanup.js --dry-run kanban\n\n# List all features\nnode scripts/cleanup.js --list\n```\n\n**Safety**: Script requires git repository with at least one commit. Use `--force` to skip.\n\nReplacement files live in `scripts/cleanup-templates/` as real `.ts`/`.tsx` files typechecked by `tsc` and `next build`, so template rot fails loudly instead of shipping broken code.\n\nAfter cleanup, delete `scripts/cleanup.js` and `scripts/cleanup-templates/` — the dev server message auto-cleans on next start.\n\n---\n\n## Icon System\n\n**All icons come from a single source: `src/components/icons.tsx`.**\n\nThe project uses `@tabler/icons-react` as the sole icon package. Every icon is re-exported through a centralized `Icons` object — **never import directly from `@tabler/icons-react` or any other icon package**.\n\n### Usage\n\n```tsx\nimport { Icons } from '@/components/icons';\n\n// In JSX\n<Icons.search className='h-4 w-4' />\n<Icons.chevronRight className='h-4 w-4' />\n\n// Passing as a prop\nicon={Icons.check}\n```\n\n### Adding a New Icon\n\n1. Import the tabler icon in `src/components/icons.tsx`\n2. Add a semantic key to the `Icons` object\n3. Use `Icons.yourKey` everywhere — never the raw import\n\n```tsx\n// In src/components/icons.tsx\nimport { IconNewIcon } from '@tabler/icons-react';\n\nexport const Icons = {\n  // ...existing icons\n  newIcon: IconNewIcon\n};\n```\n\n### Available Icon Categories\n\n| Category        | Example Keys                                                                  |\n| --------------- | ----------------------------------------------------------------------------- |\n| General         | `check`, `close`, `search`, `settings`, `trash`, `spinner`, `info`, `warning` |\n| Navigation      | `chevronDown`, `chevronLeft`, `chevronRight`, `chevronUp`, `chevronsUpDown`   |\n| Layout          | `dashboard`, `kanban`, `panelLeft`                                            |\n| User            | `user`, `account`, `profile`, `teams`                                         |\n| Communication   | `chat`, `notification`, `phone`, `video`, `send`                              |\n| Files           | `page`, `post`, `media`, `fileTypePdf`, `fileTypeDoc`                         |\n| Actions         | `add`, `edit`, `upload`, `share`, `login`, `logout`                           |\n| Theme           | `sun`, `moon`, `brightness`, `laptop`, `palette`                              |\n| Text formatting | `bold`, `italic`, `underline`, `text`                                         |\n| Data / Charts   | `trendingUp`, `trendingDown`, `eyeOff`, `adjustments`                         |\n\n### Icon Showcase Page\n\nBrowse all available icons at `/dashboard/elements/icons` — a searchable grid of every icon in the registry.\n\n### Why This Pattern?\n\n- **Single source of truth** — swap icon packages by editing one file\n- **Semantic naming** — `Icons.trash` is clearer than `IconTrash` scattered across files\n- **Discoverability** — autocomplete on `Icons.` shows every available icon\n- **No direct dependencies** — components never couple to a specific icon package\n\n---\n\n## Common Development Tasks\n\n### Adding a New Feature (End-to-End)\n\n1. Create `src/features/<name>/api/types.ts` — response types, filter types, mutation payloads\n2. Create `src/features/<name>/api/service.ts` — data access functions (mock by default)\n3. Create `src/features/<name>/api/queries.ts` — query key factory + `queryOptions`\n4. Create page route: `src/app/dashboard/<name>/page.tsx`\n5. Create feature components in `src/features/<name>/components/`\n6. Add navigation item in `src/config/nav-config.ts`\n7. (Optional) Add route handlers in `src/app/api/<name>/` for REST API patterns\n8. (Optional) Register new icon in `src/components/icons.tsx`\n\n### Adding a New API Route\n\n1. Create: `src/app/api/my-route/route.ts`\n2. Export HTTP method handlers: `GET`, `POST`, etc.\n3. For BFF pattern: proxy requests to your external backend\n\n### Adding a shadcn Component\n\n```bash\nnpx shadcn add component-name\n```\n\n### Adding a New Theme\n\nSee \"Theming System\" section above or `docs/themes.md`.\n\n---\n\n## Troubleshooting\n\n### Common Issues\n\n**Build fails with Tailwind errors**\n\n- Ensure using Tailwind CSS v4 syntax (`@import 'tailwindcss'`)\n- Check `postcss.config.js` uses `@tailwindcss/postcss`\n\n**Clerk keyless mode popup**\n\n- Run `npx clerk@latest init` to provision a dev instance in seconds (no account needed)\n- It writes keys to `.env.local`; later you can claim application or set env variables\n\n**Theme not applying**\n\n- Check theme name matches in CSS `[data-theme]` and `theme.config.ts`\n- Verify theme CSS is imported in `theme.css`\n\n**Navigation items not showing**\n\n- Check `access` property in nav config\n- Verify user has required org/permission/role\n\n---\n\n## External Documentation\n\n- [Next.js App Router](https://nextjs.org/docs/app)\n- [Clerk Next.js SDK](https://clerk.com/docs/references/nextjs)\n- [shadcn/ui](https://ui.shadcn.com/docs)\n- [Tailwind CSS v4](https://tailwindcss.com/docs)\n- [TanStack Table](https://tanstack.com/table/latest)\n- [Sentry Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/)\n\n---\n\n## Notes for AI Agents\n\n1. **Always use `cn()` for className merging** - never concatenate strings manually\n2. **Respect the feature-based structure** - put new feature code in `src/features/`\n3. **Server components by default** - only add `'use client'` when using browser APIs or React hooks\n4. **Type safety first** - avoid `any`, prefer explicit types\n5. **Follow existing patterns** - look at similar components before creating new ones\n6. **Environment variables** - prefix with `NEXT_PUBLIC_` for client-side access\n7. **shadcn components** - don't modify files in `src/components/ui/` directly; extend them instead\n8. **Icons** - NEVER import icons directly from `@tabler/icons-react` or any other icon package. All icons must be registered in `src/components/icons.tsx` and imported as `import { Icons } from '@/components/icons'`. To add a new icon: add the tabler import to `icons.tsx`, add a semantic key to the `Icons` object, then use `Icons.keyName` in your component.\n9. **Page headers** - Always use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`) for page headers. Never import `<Heading>` manually in pages — `PageContainer` handles that internally.\n10. **Forms** - Use `useAppForm` from `@/lib/form` with `form.AppField` rendering the shared field components (`field.TextField`, `field.SelectField`, …) from `@/components/forms/fields`. Each component follows the official shadcn TanStack Form anatomy; drop down to raw `form.Field` render props for one-off custom fields. Never use `useState` inside a render prop — extract stateful controls into components.\n11. **Button loading** - Use `<Button isLoading={isPending}>` for loading states. Uses CSS Grid overlap trick for zero layout shift. When `isLoading` is not passed, button behaves as default shadcn. `SubmitButton` in forms handles this automatically via form `isSubmitting` state.\n12. **Data layer** - Always go through the service layer: `types.ts` → `service.ts` → `queries.ts`. Components import types from `types.ts`, functions from `service.ts`, query options from `queries.ts`. Never import from `@/constants/mock-api*` directly in components.\n","isInternal":false,"tokens":6586,"sizeBytes":28241},{"name":"CLAUDE.md","path":"CLAUDE.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/CLAUDE.md","title":"Claude Agent Guidelines & System Prompt","category":"claude-rule","format":"markdown","content":"# CLAUDE.md\n\nThis is a Next.js 16 + shadcn/ui admin dashboard starter kit.\n\n## Key References\n\n- **[AGENTS.md](./AGENTS.md)** — Full project overview, tech stack, structure, conventions, data fetching patterns, deployment\n- **[docs/forms.md](./docs/forms.md)** — Form system: TanStack Form + Zod, composable fields, validation, multi-step, sheet/dialog forms\n- **[docs/themes.md](./docs/themes.md)** — Theme system: OKLCH colors, adding themes, font config\n- **[docs/nav-rbac.md](./docs/nav-rbac.md)** — Navigation RBAC: access control, Clerk integration\n- **[docs/clerk_setup.md](./docs/clerk_setup.md)** — Clerk auth setup: organizations, billing, environment variables\n- **[docs/deployment.md](./docs/deployment.md)** — Deployment: Vercel, production environment variables, Docker\n\n## Critical Conventions\n\n- **React Query** for all data fetching — `void prefetchQuery()` on server + `useSuspenseQuery` on client (standard TanStack pattern), `useMutation` for forms, `HydrationBoundary` + `dehydrate` for hydration, `<Suspense fallback>` for streaming\n- **API layer** per feature — `api/types.ts` → `api/service.ts` → `api/queries.ts`; queries use key factories (`entityKeys.all/list/detail`); components import from service and queries, never from mock APIs directly\n- **nuqs** for URL search params — `searchParamsCache` on server, `useQueryStates` on client, use `getSortingStateParser` for sort (same parser as `useDataTable`)\n- **Icons** — only import from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` from `@/lib/form` (TanStack `createFormHook`) + `form.AppField` rendering the field components in `@/components/forms/fields` (`field.TextField`, `field.SelectField`, …); each component is the shadcn TanStack Form doc anatomy; raw `form.Field` for one-off custom fields; form-level Zod `onSubmit` validators\n- **Page headers** — use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`), never import `<Heading>` manually\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n","isInternal":false,"tokens":527,"sizeBytes":2118},{"name":"charts-guide.md","path":".agents/skills/kiranism-shadcn-dashboard/references/charts-guide.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/kiranism-shadcn-dashboard/references/charts-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Charts & Analytics Guide\n\n## Table of Contents\n\n1. [Overview Architecture](#overview-architecture)\n2. [Parallel Routes Pattern](#parallel-routes-pattern)\n3. [Chart Components](#chart-components)\n4. [Stats Cards](#stats-cards)\n5. [Skeleton Loading](#skeleton-loading)\n6. [Adding a New Chart Section](#adding-a-new-chart-section)\n\n---\n\n## Overview Architecture\n\nThe analytics dashboard at `/dashboard/overview` uses **Next.js parallel routes** to load multiple chart sections independently. Each chart slot streams in as its data becomes ready — no waterfall, no blocking.\n\n**File structure:**\n\n```\nsrc/app/dashboard/overview/\n├── layout.tsx              # Composes all slots into a grid\n├── @area_stats/\n│   ├── page.tsx            # Async server component (fetches data)\n│   ├── loading.tsx         # Skeleton shown while streaming\n│   └── error.tsx           # Error boundary if fetch fails\n├── @bar_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n├── @pie_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n└── @sales/\n    ├── page.tsx\n    ├── loading.tsx\n    └── error.tsx\n\nsrc/features/overview/components/\n├── area-graph.tsx          # Client chart component\n├── area-graph-skeleton.tsx # Matching skeleton\n├── bar-graph.tsx\n├── bar-graph-skeleton.tsx\n├── pie-graph.tsx\n├── pie-graph-skeleton.tsx\n├── recent-sales.tsx\n└── recent-sales-skeleton.tsx\n```\n\n---\n\n## Parallel Routes Pattern\n\n### Layout (`layout.tsx`)\n\nThe layout receives each parallel route as a prop and arranges them in a grid:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats\n}: {\n  sales: React.ReactNode;\n  pie_stats: React.ReactNode;\n  bar_stats: React.ReactNode;\n  area_stats: React.ReactNode;\n}) {\n  return (\n    <PageContainer pageTitle='Dashboard' pageDescription='Overview analytics.'>\n      {/* Stats cards row */}\n      <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>\n        <Card>\n          <CardHeader className='flex flex-row items-center justify-between pb-2'>\n            <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n            <Icons.billing className='h-4 w-4 text-muted-foreground' />\n          </CardHeader>\n          <CardContent>\n            <div className='text-2xl font-bold'>$45,231.89</div>\n            <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n          </CardContent>\n        </Card>\n        {/* ...more stat cards */}\n      </div>\n\n      {/* Charts grid — each slot loads independently */}\n      <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n        <div className='col-span-4'>{area_stats}</div>\n        <div className='col-span-3'>{sales}</div>\n        <div className='col-span-4'>{bar_stats}</div>\n        <div className='col-span-3'>{pie_stats}</div>\n      </div>\n    </PageContainer>\n  );\n}\n```\n\n### Slot Page (`@area_stats/page.tsx`)\n\nEach slot is an async server component that fetches data then renders the chart:\n\n```tsx\nimport { delay } from '@/constants/mock-api';\nimport { AreaGraph } from '@/features/overview/components/area-graph';\n\nexport default async function AreaStatsPage() {\n  await delay(2000); // Simulates API fetch\n  return <AreaGraph />;\n}\n```\n\n### Slot Loading (`@area_stats/loading.tsx`)\n\n```tsx\nimport { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';\n\nexport default function Loading() {\n  return <AreaGraphSkeleton />;\n}\n```\n\n### Slot Error (`@area_stats/error.tsx`)\n\n```tsx\n'use client';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Icons } from '@/components/icons';\n\nexport default function AreaStatsError({ error }: { error: Error }) {\n  return (\n    <Alert variant='destructive'>\n      <Icons.alertCircle className='h-4 w-4' />\n      <AlertTitle>Error</AlertTitle>\n      <AlertDescription>Failed to load area stats: {error.message}</AlertDescription>\n    </Alert>\n  );\n}\n```\n\nEach slot can fail independently without affecting others.\n\n---\n\n## Chart Components\n\nAll chart components are `'use client'` and use **Recharts** wrapped in shadcn's `ChartContainer`.\n\n### Chart Config\n\nEvery chart defines a config object mapping data keys to labels and theme colors:\n\n```tsx\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n```\n\nTheme colors `--chart-1` through `--chart-5` are defined in each theme's CSS file and automatically adapt to light/dark mode.\n\n### Area Chart Example\n\n```tsx\n'use client';\nimport { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Icons } from '@/components/icons';\n\nconst chartData = [\n  { month: 'January', desktop: 186, mobile: 80 },\n  { month: 'February', desktop: 305, mobile: 200 }\n  // ...more months\n];\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n\nexport function AreaGraph() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <CardTitle>Area Chart - Stacked</CardTitle>\n        <Badge variant='outline'>\n          <Icons.trendingUp className='mr-1 h-3 w-3' /> +12.5%\n        </Badge>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <AreaChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey='month'\n              tickLine={false}\n              axisLine={false}\n              tickFormatter={(value) => value.slice(0, 3)}\n            />\n            <ChartTooltip content={<ChartTooltipContent indicator='dot' />} />\n            <Area\n              dataKey='mobile'\n              type='natural'\n              fill='var(--color-mobile)'\n              stroke='var(--color-mobile)'\n              stackId='a'\n            />\n            <Area\n              dataKey='desktop'\n              type='natural'\n              fill='var(--color-desktop)'\n              stroke='var(--color-desktop)'\n              stackId='a'\n            />\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### Bar Chart Pattern\n\nSame structure, using `BarChart` + `Bar`:\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <BarChart data={chartData}>\n    <CartesianGrid vertical={false} />\n    <XAxis dataKey='month' tickLine={false} axisLine={false} />\n    <ChartTooltip content={<ChartTooltipContent />} />\n    <Bar dataKey='desktop' fill='var(--color-desktop)' radius={4} />\n    <Bar dataKey='mobile' fill='var(--color-mobile)' radius={4} />\n  </BarChart>\n</ChartContainer>\n```\n\n### Pie/Donut Chart Pattern\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <PieChart>\n    <ChartTooltip content={<ChartTooltipContent hideLabel />} />\n    <Pie data={chartData} dataKey='visitors' nameKey='browser' innerRadius={30}>\n      <LabelList dataKey='visitors' className='fill-background' />\n    </Pie>\n  </PieChart>\n</ChartContainer>\n```\n\n---\n\n## Stats Cards\n\nStats cards are simple server-rendered `Card` components at the top of the layout — no parallel routes needed since they render instantly:\n\n```tsx\n<Card>\n  <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>\n    <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n    <Icons.billing className='h-4 w-4 text-muted-foreground' />\n  </CardHeader>\n  <CardContent>\n    <div className='text-2xl font-bold'>$45,231.89</div>\n    <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n  </CardContent>\n</Card>\n```\n\nFor dynamic stats that need data fetching, wrap in their own Suspense boundary or parallel route slot.\n\n---\n\n## Skeleton Loading\n\nEach chart has a matching skeleton component. Pattern:\n\n```tsx\nimport { Card, CardContent, CardHeader } from '@/components/ui/card';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport function AreaGraphSkeleton() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <Skeleton className='h-5 w-[140px]' />\n        <Skeleton className='h-4 w-[80px]' />\n      </CardHeader>\n      <CardContent>\n        <Skeleton className='h-[250px] w-full rounded-md' />\n      </CardContent>\n    </Card>\n  );\n}\n```\n\nMatch the skeleton dimensions to the actual chart for smooth visual transitions.\n\n---\n\n## Adding a New Chart Section\n\nTo add a new chart (e.g., line chart for user growth):\n\n### 1. Create the chart component\n\n`src/features/overview/components/line-graph.tsx`:\n\n```tsx\n'use client';\nimport { Line, LineChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  users: { label: 'Users', color: 'var(--chart-3)' }\n} satisfies ChartConfig;\n\nconst chartData = [\n  /* monthly user data */\n];\n\nexport function LineGraph() {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>User Growth</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <LineChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis dataKey='month' tickLine={false} axisLine={false} />\n            <ChartTooltip content={<ChartTooltipContent />} />\n            <Line dataKey='users' type='monotone' stroke='var(--color-users)' strokeWidth={2} />\n          </LineChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### 2. Create matching skeleton\n\n`src/features/overview/components/line-graph-skeleton.tsx`\n\n### 3. Create parallel route slot\n\n```\nsrc/app/dashboard/overview/@line_stats/\n├── page.tsx     → async, fetches data, returns <LineGraph />\n├── loading.tsx  → returns <LineGraphSkeleton />\n├── error.tsx    → error alert\n└── default.tsx  → return null (fallback when route doesn't match)\n```\n\n`default.tsx` is required for parallel routes — return `null` or a fallback:\n\n```tsx\nexport default function Default() {\n  return null;\n}\n```\n\n### 4. Add slot to layout\n\nUpdate `src/app/dashboard/overview/layout.tsx`:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats,\n  line_stats // ← add new slot\n}: {\n  /* ...types */\n}) {\n  return (\n    <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n      {/* existing charts */}\n      <div className='col-span-4'>{line_stats}</div>\n    </div>\n  );\n}\n```\n\n### Available Recharts Components\n\nCommon chart types to use with `ChartContainer`:\n\n- `AreaChart` + `Area` — filled area charts (stacked or standalone)\n- `BarChart` + `Bar` — vertical/horizontal bars\n- `LineChart` + `Line` — line/trend charts\n- `PieChart` + `Pie` — pie/donut charts\n- `RadarChart` + `Radar` — radar/spider charts\n- `RadialBarChart` + `RadialBar` — radial progress bars\n\nAll support `ChartTooltip`, `ChartLegend`, and theme-aware colors via `var(--chart-N)`.\n","isInternal":false,"tokens":2884,"sizeBytes":11600},{"name":"forms-guide.md","path":".agents/skills/kiranism-shadcn-dashboard/references/forms-guide.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/kiranism-shadcn-dashboard/references/forms-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Forms Guide\n\n## Table of Contents\n\n1. [Architecture](#architecture)\n2. [Field Types](#field-types)\n3. [Usage Patterns](#usage-patterns)\n4. [Validation Strategies](#validation-strategies)\n5. [Sheet/Dialog Forms](#sheetdialog-forms)\n6. [Multi-Step Forms](#multi-step-forms)\n7. [Advanced Patterns](#advanced-patterns)\n\n---\n\n## Architecture\n\nThe form system is built on **TanStack Form + Zod** with a composable field layer.\n\n**Key files:**\n\n- `src/components/ui/tanstack-form.tsx` — exports `useAppForm`, `useFormFields<T>()`, composed fields\n- `src/components/ui/form-context.tsx` — contexts, `createFormField`, structural components\n- `src/components/forms/fields/*.tsx` — 8 field type implementations\n\n**Key exports:**\n\n```tsx\nimport { useAppForm, useFormFields } from '@/components/ui/tanstack-form';\n```\n\n- `useAppForm(config)` — creates a form instance with `defaultValues`, `validators`, `onSubmit`\n- `useFormFields<T>()` — returns all 8 typed field components with name autocomplete from `T`\n- `form.AppForm` — context provider wrapper\n- `form.Form` — `<form>` element that handles submit\n- `form.SubmitButton` — auto-disabled when form is invalid or submitting\n- `form.AppField` — low-level render prop for custom fields\n\n---\n\n## Field Types\n\nAll fields accept: `name`, `label`, `description`, `required`, `disabled`, `validators`, `listeners`, `className`.\n\n| Component             | Props                                                                                         | Notes                                   |\n| --------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |\n| `FormTextField`       | `type` (text/email/number/password/tel/url), `placeholder`, `min`, `max`, `step`, `maxLength` | For numbers use `type='number'`         |\n| `FormTextareaField`   | `placeholder`, `rows`, `maxLength`                                                            | Multiline text                          |\n| `FormSelectField`     | `options: {value, label}[]`, `placeholder`                                                    | Single select dropdown                  |\n| `FormCheckboxField`   | `options?: {value, label}[]`                                                                  | Single checkbox or multi-checkbox group |\n| `FormSwitchField`     | —                                                                                             | Toggle switch                           |\n| `FormRadioGroupField` | `options: {value, label}[]`, `orientation`                                                    | Radio button group                      |\n| `FormSliderField`     | `min`, `max`, `step`                                                                          | Range slider                            |\n| `FormFileUploadField` | `maxSize`, `maxFiles`, `accept`                                                               | Drag-and-drop with preview              |\n\n---\n\n## Usage Patterns\n\n### Pattern 1: `useFormFields<T>()` (Recommended)\n\nType-safe field components with name autocomplete:\n\n```tsx\nconst { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n<FormTextField name='customer' label='Customer' required placeholder='Name'\n  validators={{ onBlur: z.string().min(2) }} />\n\n<FormSelectField name='status' label='Status' required options={STATUS_OPTIONS}\n  validators={{ onBlur: z.string().min(1) }} />\n```\n\n### Pattern 2: `form.AppField` render prop\n\nFull control for custom field rendering:\n\n```tsx\n<form.AppField name='framework'>\n  {(field) => (\n    <field.FieldSet>\n      <field.Field>\n        <field.TextField label='Framework' />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Pattern 3: Direct import (no type safety)\n\nFor quick prototyping:\n\n```tsx\nimport { FormTextField } from '@/components/ui/tanstack-form';\n<FormTextField name='name' label='Name' />;\n```\n\n---\n\n## Validation Strategies\n\n### Field-level (recommended for UX)\n\n```tsx\n<FormTextField\n  name='email'\n  label='Email'\n  validators={{\n    onBlur: z.string().email('Invalid email') // Validates when field loses focus\n  }}\n/>\n```\n\n### Form-level (catch-all on submit)\n\n```tsx\nconst form = useAppForm({\n  validators: { onSubmit: orderSchema }, // Validates entire form on submit\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n### Async validation (server-side checks)\n\n```tsx\n<FormTextField\n  name='username'\n  label='Username'\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const exists = await checkUsername(value);\n      return exists ? 'Username taken' : undefined;\n    }\n  }}\n  asyncDebounceMs={500}\n/>\n```\n\n### Linked field validation\n\nFor dependent fields (e.g., confirm password):\n\n```tsx\n<FormTextField\n  name='confirmPassword'\n  label='Confirm Password'\n  validators={{\n    onChangeListenTo: ['password'],\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      return value !== password ? 'Passwords must match' : undefined;\n    }\n  }}\n/>\n```\n\n---\n\n## Sheet/Dialog Forms\n\nThe key pattern for forms inside sheets or dialogs: give the `<form.Form>` an `id`, and use that `id` on the submit button's `form` attribute. This allows the submit button to live outside the form element (e.g., in `SheetFooter`).\n\n```tsx\n<form.AppForm>\n  <form.Form id='my-sheet-form' className='space-y-4'>\n    {/* fields */}\n  </form.Form>\n</form.AppForm>;\n\n{\n  /* In SheetFooter — button is outside the <form> but still submits it */\n}\n<SheetFooter>\n  <Button type='submit' form='my-sheet-form'>\n    Save\n  </Button>\n</SheetFooter>;\n```\n\nOn success, call `onOpenChange(false)` to close the sheet and `form.reset()` for create forms.\n\n---\n\n## Multi-Step Forms\n\nUse `withFieldGroup` + `useAppForm` with `StepButton`:\n\n```tsx\n// Define field groups for each step\nconst Step1 = withFieldGroup({\n  fields: ['name', 'email'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='name' label='Name' />\n        <FormTextField name='email' label='Email' />\n        <form.StepButton direction='next' label='Next' />\n      </>\n    );\n  }\n});\n\nconst Step2 = withFieldGroup({\n  fields: ['address', 'city'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='address' label='Address' />\n        <FormTextField name='city' label='City' />\n        <form.StepButton direction='prev' label='Back' />\n        <form.SubmitButton label='Submit' />\n      </>\n    );\n  }\n});\n```\n\nUse the `useStepper` hook from `src/hooks/use-stepper.tsx` to manage step state.\n\n---\n\n## Advanced Patterns\n\n### Nested objects (dot notation)\n\n```tsx\n<FormTextField name='address.street' label='Street' />\n<FormTextField name='address.city' label='City' />\n```\n\n### Dynamic array rows\n\n```tsx\n<form.AppField name='items' mode='array'>\n  {(field) => (\n    <>\n      {field.state.value.map((_, i) => (\n        <form.AppField key={i} name={`items[${i}].name`}>\n          {(subField) => <subField.TextField label={`Item ${i + 1}`} />}\n        </form.AppField>\n      ))}\n      <Button onClick={() => field.pushValue({ name: '' })}>Add Row</Button>\n    </>\n  )}\n</form.AppField>\n```\n\n### Side effects with listeners\n\n```tsx\n<FormSelectField\n  name='country'\n  label='Country'\n  options={countryOptions}\n  listeners={{\n    onChange: ({ value }) => {\n      // Reset city when country changes\n      form.setFieldValue('city', '');\n    }\n  }}\n/>\n```\n\n### Custom field with `form.AppField`\n\nFor fields not covered by the built-in 8 types:\n\n```tsx\n<form.AppField name='color'>\n  {(field) => (\n    <field.FieldSet>\n      <Label>Pick a color</Label>\n      <field.Field>\n        <input\n          type='color'\n          value={field.state.value}\n          onChange={(e) => field.handleChange(e.target.value)}\n        />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Form-level errors\n\nDisplay errors that apply to the whole form (e.g., server errors):\n\n```tsx\nimport { FormErrors } from '@/components/ui/form-context';\n\n<form.AppForm>\n  <form.Form>\n    <FormErrors /> {/* Renders form-level validation errors */}\n    {/* fields... */}\n  </form.Form>\n</form.AppForm>;\n```\n","isInternal":false,"tokens":1928,"sizeBytes":8426},{"name":"mock-api-guide.md","path":".agents/skills/kiranism-shadcn-dashboard/references/mock-api-guide.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/kiranism-shadcn-dashboard/references/mock-api-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Mock API Guide\n\n## Table of Contents\n\n1. [Structure](#structure)\n2. [Full Template](#full-template)\n3. [Key Patterns](#key-patterns)\n4. [Integrating with React Query](#integrating-with-react-query)\n\n---\n\n## Structure\n\nEach mock API file lives in `src/constants/mock-api-<name>.ts` and is a self-contained in-memory database. It uses:\n\n- **faker** for generating sample data\n- **match-sorter** for fuzzy search across fields\n- **delay** (from `./mock-api`) to simulate network latency\n\nThe `delay` function is exported from `src/constants/mock-api.ts`:\n\n```tsx\nexport async function delay(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n```\n\n---\n\n## Full Template\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\n// 1. Define the entity type\nexport type Order = {\n  id: number;\n  customer: string;\n  email: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\n// 2. Create the fake database object\nexport const fakeOrders = {\n  records: [] as Order[],\n\n  // 3. Initialize with faker data\n  initialize() {\n    const statuses = ['pending', 'processing', 'completed', 'cancelled'];\n    for (let i = 1; i <= 20; i++) {\n      this.records.push({\n        id: i,\n        customer: faker.person.fullName(),\n        email: faker.internet.email(),\n        status: faker.helpers.arrayElement(statuses),\n        total: parseFloat(faker.commerce.price({ min: 10, max: 500 })),\n        created_at: faker.date.between({ from: '2023-01-01', to: Date.now() }).toISOString(),\n        updated_at: faker.date.recent().toISOString()\n      });\n    }\n  },\n\n  // 4. Get all with optional search (used internally)\n  async getAll({ search }: { search?: string } = {}) {\n    let items = [...this.records];\n    if (search) {\n      items = matchSorter(items, search, {\n        keys: ['customer', 'email']\n      });\n    }\n    return items;\n  },\n\n  // 5. Paginated list with filtering and sorting\n  async getOrders(params: {\n    page?: number;\n    limit?: number;\n    search?: string;\n    statuses?: string;\n    sort?: string;\n  }) {\n    await delay(800);\n    const { page = 1, limit = 10, search, statuses, sort } = params;\n\n    let items = await this.getAll({ search });\n\n    // Filter by comma-separated values\n    if (statuses) {\n      const statusList = statuses.split('.');\n      items = items.filter((item) => statusList.includes(item.status));\n    }\n\n    // Sort by column\n    if (sort) {\n      const parsedSort = JSON.parse(sort) as { id: string; desc: boolean }[];\n      if (parsedSort.length > 0) {\n        const { id, desc } = parsedSort[0];\n        items.sort((a, b) => {\n          const aVal = a[id as keyof Order];\n          const bVal = b[id as keyof Order];\n          if (aVal < bVal) return desc ? 1 : -1;\n          if (aVal > bVal) return desc ? -1 : 1;\n          return 0;\n        });\n      }\n    }\n\n    // Paginate\n    const total_items = items.length;\n    items = items.slice((page - 1) * limit, page * limit);\n\n    return { items, total_items };\n  },\n\n  // 6. Get single record by ID\n  async getOrderById(id: number) {\n    await delay(800);\n    return this.records.find((r) => r.id === id) || null;\n  },\n\n  // 7. Create\n  async createOrder(data: Omit<Order, 'id' | 'created_at' | 'updated_at'>) {\n    await delay(800);\n    const newRecord: Order = {\n      ...data,\n      id: this.records.length + 1,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString()\n    };\n    this.records.push(newRecord);\n    return newRecord;\n  },\n\n  // 8. Update\n  async updateOrder(id: number, data: Partial<Order>) {\n    await delay(800);\n    const idx = this.records.findIndex((r) => r.id === id);\n    if (idx === -1) return null;\n    this.records[idx] = {\n      ...this.records[idx],\n      ...data,\n      updated_at: new Date().toISOString()\n    };\n    return this.records[idx];\n  },\n\n  // 9. Delete\n  async deleteOrder(id: number) {\n    await delay(800);\n    this.records = this.records.filter((r) => r.id !== id);\n    return true;\n  }\n};\n\n// 10. Auto-initialize on import\nfakeOrders.initialize();\n```\n\n---\n\n## Key Patterns\n\n### Search with match-sorter\n\nAlways specify which fields to search across:\n\n```tsx\nmatchSorter(items, search, { keys: ['customer', 'email', 'status'] });\n```\n\n### Comma-separated filter values\n\nFor multi-select filters (roles, statuses), the URL param uses `.` as delimiter:\n\n```tsx\nif (statuses) {\n  const list = statuses.split('.');\n  items = items.filter((item) => list.includes(item.status));\n}\n```\n\n### Computed column sorting\n\nWhen a table has a computed column (e.g., combining first_name + last_name into \"name\"), handle it in the sort logic:\n\n```tsx\nif (id === 'name') {\n  const aName = `${a.first_name} ${a.last_name}`;\n  const bName = `${b.first_name} ${b.last_name}`;\n  return desc ? bName.localeCompare(aName) : aName.localeCompare(bName);\n}\n```\n\n### Return shape\n\nList methods must return `{ items, total_items }` (or `{ products, total }` etc. — match the query option expectations). The total is the count **before** pagination, used for `pageCount` calculation.\n\n---\n\n## Integrating with the API Layer\n\nThe mock API is only imported in `service.ts`. Queries and components import from the service and types files:\n\n```\nmock-api-orders.ts  →  api/service.ts  →  api/queries.ts  →  components\n(data source)          (data access)       (key factory +     (useSuspenseQuery\n                                            queryOptions)       + useMutation)\n```\n\n**service.ts** imports from the mock API:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\n```\n\n**queries.ts** imports from service, uses key factories:\n\n```tsx\nimport { getOrders } from './service';\nimport type { OrderFilters } from './types';\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({ queryKey: orderKeys.list(filters), queryFn: () => getOrders(filters) });\n```\n\n**Mutations** in components use service functions + key factories:\n\n```tsx\nimport { createOrder } from '../api/service';\nimport { orderKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })\n});\n```\n","isInternal":false,"tokens":1721,"sizeBytes":6803},{"name":"query-abstractions.md","path":".agents/skills/kiranism-shadcn-dashboard/references/query-abstractions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/kiranism-shadcn-dashboard/references/query-abstractions.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# TanStack Query Abstractions (v5)\n\nThe core insight: **`queryOptions` and `mutationOptions` are the right abstraction — not custom hooks.**\n\n---\n\n## Query Abstraction\n\n### The Pattern\n\n```ts\n// queries/invoice.ts\nimport { queryOptions } from '@tanstack/react-query';\n\nexport function invoiceOptions(id: number) {\n  return queryOptions({\n    queryKey: ['invoice', id],\n    queryFn: () => fetchInvoice(id)\n  });\n}\n\nexport function invoiceListOptions(filters: InvoiceFilters) {\n  return queryOptions({\n    queryKey: ['invoices', filters],\n    queryFn: () => fetchInvoices(filters),\n    staleTime: 30_000\n  });\n}\n```\n\n### Usage — always compose at the call site\n\n```ts\n// basic\nconst { data } = useQuery(invoiceOptions(id));\n\n// with suspense — same options, different hook\nconst { data } = useSuspenseQuery(invoiceOptions(id));\n\n// with extra options spread on top — full type inference, no TS pain\nconst { data } = useQuery({\n  ...invoiceOptions(id),\n  select: (invoice) => invoice.createdAt, // data infers as string | undefined\n  enabled: !!id\n});\n\n// prefetch in a route loader (works outside React — this is why hooks are wrong)\nawait queryClient.prefetchQuery(invoiceOptions(id));\n\n// read from cache imperatively — queryKey is typed via DataTag symbol\nconst invoice = queryClient.getQueryData(invoiceOptions(id).queryKey);\n\n// invalidate\nqueryClient.invalidateQueries({ queryKey: invoiceOptions(id).queryKey });\n```\n\n### Why NOT a custom hook\n\nCustom hooks like `useInvoice(id)` have three critical problems:\n\n1. **Hooks only work in components/hooks** — but queries are now used in route loaders, server prefetching, event handlers, and server components. `queryOptions` is just a plain function — works anywhere.\n2. **They share logic, not configuration** — what you actually want to share is the `queryKey` + `queryFn` config. Hooks are the wrong primitive for that.\n3. **They lock you to one hook** — you can't use `useInvoice()` with `useSuspenseQuery`, `useQueries`, or imperative `queryClient` methods.\n\n### Why NOT `UseQueryOptions` type directly\n\n```ts\n// BAD — data becomes unknown\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions>) { ... }\n\n// STILL BAD — select breaks with TS error\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions<Invoice>>) { ... }\n// select: (invoice) => invoice.createdAt\n// Error: Type 'string' is not assignable to type 'Invoice'\n```\n\n`queryOptions` solves this via a `DataTag` symbol on the queryKey — full inference, zero manual generics.\n\n### Custom hooks are still fine on top\n\nIf a component always uses the same composition, a hook is fine — but build it _on top of_ `queryOptions`:\n\n```ts\n// OK — hook built on queryOptions\nfunction useInvoice(id: number) {\n  return useQuery(invoiceOptions(id));\n}\n\n// OK — hook that adds per-feature defaults\nfunction useInvoiceWithSuspense(id: number) {\n  return useSuspenseQuery(invoiceOptions(id));\n}\n```\n\n---\n\n## Mutation Abstraction\n\n### The Pattern\n\n```ts\n// mutations/invoice.ts\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\n\nexport const createInvoiceMutation = mutationOptions({\n  mutationFn: (data: CreateInvoiceInput) => createInvoice(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n\nexport const updateInvoiceMutation = mutationOptions({\n  mutationFn: ({ id, ...data }: UpdateInvoiceInput) => updateInvoice(id, data),\n  onSuccess: (updated) => {\n    const qc = getQueryClient();\n    qc.setQueryData(invoiceOptions(updated.id).queryKey, updated);\n    qc.invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n```\n\n> **Note on queryClient**: Import `getQueryClient()` directly — do NOT pass `queryClient` as a function argument. The `getQueryClient()` pattern handles both SSR (fresh per request) and client (singleton) correctly.\n\n### Usage\n\n```ts\n// basic\nconst { mutate } = useMutation(createInvoiceMutation);\n\n// composed — add per-usage callbacks on top\nconst { mutate } = useMutation({\n  ...createInvoiceMutation,\n  onError: (err) => toast.error(err.message),\n  onSuccess: (data) => {\n    // this runs AFTER the shared onSuccess above\n    router.push(`/invoices/${data.id}`);\n  }\n});\n```\n\n---\n\n## Rules Summary\n\n| Rule                                                          | Reason                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| Use `queryOptions()` not custom hooks as the base abstraction | Works everywhere — loaders, server, imperative calls                   |\n| Keep options factories lean — no extra config params          | Best abstractions are not configurable                                 |\n| Compose extra options at the call site via spread             | Full TS inference without manual generics                              |\n| Import `getQueryClient()` in mutation files                   | Handles SSR/client correctly without prop drilling                     |\n| Co-locate `queryKey` inside `queryOptions`                    | Typed key reuse in `invalidateQueries`, `setQueryData`, `getQueryData` |\n| Custom hooks are fine — but built ON TOP of `queryOptions`    | Hooks for component convenience, `queryOptions` for sharing config     |\n","isInternal":false,"tokens":1177,"sizeBytes":5418},{"name":"theming-guide.md","path":".agents/skills/kiranism-shadcn-dashboard/references/theming-guide.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/kiranism-shadcn-dashboard/references/theming-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"# Theme Creation Guide\n\n## Table of Contents\n\n1. [Create Theme CSS](#1-create-theme-css)\n2. [Import Theme](#2-import-theme)\n3. [Register Theme](#3-register-theme)\n4. [Add Custom Fonts](#4-add-custom-fonts-optional)\n5. [Set as Default](#5-set-as-default-optional)\n6. [Required Tokens](#required-tokens)\n7. [Color Format Reference](#color-format-reference)\n\n---\n\n## 1. Create Theme CSS\n\nCreate `src/styles/themes/<name>.css`:\n\n```css\n/* Light mode */\n[data-theme='your-theme'] {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(...);\n  --card-foreground: oklch(...);\n  --popover: oklch(...);\n  --popover-foreground: oklch(...);\n  --primary: oklch(...);\n  --primary-foreground: oklch(...);\n  --secondary: oklch(...);\n  --secondary-foreground: oklch(...);\n  --muted: oklch(...);\n  --muted-foreground: oklch(...);\n  --accent: oklch(...);\n  --accent-foreground: oklch(...);\n  --destructive: oklch(...);\n  --destructive-foreground: oklch(...);\n  --border: oklch(...);\n  --input: oklch(...);\n  --ring: oklch(...);\n  --chart-1: oklch(...);\n  --chart-2: oklch(...);\n  --chart-3: oklch(...);\n  --chart-4: oklch(...);\n  --chart-5: oklch(...);\n  --sidebar: oklch(...);\n  --sidebar-foreground: oklch(...);\n  --sidebar-primary: oklch(...);\n  --sidebar-primary-foreground: oklch(...);\n  --sidebar-accent: oklch(...);\n  --sidebar-accent-foreground: oklch(...);\n  --sidebar-border: oklch(...);\n  --sidebar-ring: oklch(...);\n  --font-sans: 'Font Name', sans-serif;\n  --font-mono: 'Mono Font', monospace;\n  --radius: 0.5rem;\n  --spacing: 0.25rem;\n}\n\n/* Dark mode */\n[data-theme='your-theme'].dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  /* ... all tokens with dark values */\n}\n\n/* Tailwind integration (required) */\n[data-theme='your-theme'] {\n  @theme inline {\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-destructive-foreground: var(--destructive-foreground);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n    --font-sans: var(--font-sans);\n    --font-mono: var(--font-mono);\n    --font-serif: var(--font-serif);\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n  }\n}\n```\n\n## 2. Import Theme\n\nAdd to `src/styles/theme.css`:\n\n```css\n@import './themes/your-theme.css';\n```\n\n## 3. Register Theme\n\nAdd to `THEMES` array in `src/components/themes/theme.config.ts`:\n\n```typescript\n{ name: 'Your Theme', value: 'your-theme' }\n```\n\nThe `value` must exactly match the `data-theme` attribute in your CSS.\n\n## 4. Add Custom Fonts (Optional)\n\nOnly if using a Google Font not already loaded.\n\nIn `src/components/themes/font.config.ts`:\n\n```typescript\nimport { Your_Font } from 'next/font/google';\n\nconst fontYourName = Your_Font({\n  subsets: ['latin'],\n  weight: ['400', '500', '700'],\n  variable: '--font-your-name'\n});\n\nexport const fontVariables = cn(\n  // ... existing fonts\n  fontYourName.variable\n);\n```\n\nIn your theme CSS, use the font's **display name** (not the CSS variable):\n\n```css\n--font-sans: 'Your Font', sans-serif;\n```\n\n## 5. Set as Default (Optional)\n\nIn `src/components/themes/theme.config.ts`:\n\n```typescript\nexport const DEFAULT_THEME = 'your-theme';\n```\n\n## Required Tokens\n\nMinimum required: `--background`, `--foreground`, `--card` & `--card-foreground`, `--popover` & `--popover-foreground`, `--primary` & `--primary-foreground`, `--secondary` & `--secondary-foreground`, `--muted` & `--muted-foreground`, `--accent` & `--accent-foreground`, `--destructive` & `--destructive-foreground`, `--border`, `--input`, `--ring`, `--radius`.\n\nOptional: `--chart-*`, `--sidebar-*`, `--font-*`, `--shadow-*`, `--tracking-normal`, `--spacing`.\n\n## Color Format Reference\n\nOKLCH: `oklch(lightness chroma hue)`\n\n- Lightness: 0-1 (0=black, 1=white)\n- Chroma: 0+ (0=gray, higher=saturated)\n- Hue: 0-360 (0=red, 120=green, 240=blue)\n\nSee `src/styles/themes/claude.css` for a complete example.\n","isInternal":false,"tokens":1559,"sizeBytes":5290},{"name":"SKILL.md","path":".agents/skills/kiranism-shadcn-dashboard/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/kiranism-shadcn-dashboard/SKILL.md","title":"kiranism-shadcn-dashboard","category":"anthropic-skill","format":"markdown","content":"---\nname: kiranism-shadcn-dashboard\ndescription: |\n  Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention \"dashboard\" explicitly — if they're adding UI, pages, or features to this project, use this skill.\n---\n\n# Dashboard Development Guide\n\nThis skill encodes the exact patterns and conventions used in this Next.js 16 + shadcn/ui admin dashboard template.\n\n## Quick Reference: What Goes Where\n\n| Task                  | Location                                |\n| --------------------- | --------------------------------------- |\n| New page              | `src/app/dashboard/<name>/page.tsx`     |\n| New feature module    | `src/features/<name>/`                  |\n| Feature components    | `src/features/<name>/components/`       |\n| API types             | `src/features/<name>/api/types.ts`      |\n| Service layer         | `src/features/<name>/api/service.ts`    |\n| Query options         | `src/features/<name>/api/queries.ts`    |\n| Mutation options      | `src/features/<name>/api/mutations.ts`  |\n| Zod schemas           | `src/features/<name>/schemas/<name>.ts` |\n| Filter/select options | `src/features/<name>/constants/`        |\n| Nav config            | `src/config/nav-config.ts`              |\n| Types                 | `src/types/index.ts`                    |\n| Mock data             | `src/constants/mock-api-<name>.ts`      |\n| Search params         | `src/lib/searchparams.ts`               |\n| Query client          | `src/lib/query-client.ts`               |\n| Theme CSS             | `src/styles/themes/<name>.css`          |\n| Theme registry        | `src/components/themes/theme.config.ts` |\n| Custom hook           | `src/hooks/`                            |\n| Icons registry        | `src/components/icons.tsx`              |\n\n---\n\n## Adding a New Feature (End-to-End)\n\nWhen a user asks to add a feature (e.g., \"add an orders page\"), follow these steps in order. Each step below shows the minimal pattern — see reference files for full templates.\n\n### Step 1: Mock API (`src/constants/mock-api-<name>.ts`)\n\nSee [references/mock-api-guide.md](references/mock-api-guide.md) for the complete template. Key structure:\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\nexport type Order = {\n  id: number;\n  customer: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\nexport const fakeOrders = {\n  records: [] as Order[],\n  initialize() {\n    /* generate with faker */\n  },\n  async getOrders({ page, limit, search, sort }) {\n    /* filter, sort, paginate, return { items, total_items } */\n  },\n  async getOrderById(id: number) {\n    /* find by id */\n  },\n  async createOrder(data) {\n    /* push to records */\n  },\n  async updateOrder(id, data) {\n    /* merge into record */\n  },\n  async deleteOrder(id) {\n    /* filter out */\n  }\n};\nfakeOrders.initialize();\n```\n\nEvery method should call `await delay(800)` to simulate network latency. Use `matchSorter` for search. Return `{ items, total_items }` from list methods.\n\n### Step 2: API Layer (`src/features/<name>/api/`)\n\nEach feature has 4 API files: **types** → **service** → **queries** → **mutations**.\n\n**Types** (`api/types.ts`) — re-export the entity type from mock API, plus filter/response/payload types:\n\n```tsx\nexport type { Order } from '@/constants/mock-api-orders';\nexport type OrderFilters = { page?: number; limit?: number; search?: string; sort?: string };\nexport type OrdersResponse = { items: Order[]; total_items: number };\nexport type OrderMutationPayload = { customer: string; status: string; total: number };\n```\n\n**Service** (`api/service.ts`) — data access layer. One exported function per operation:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse, OrderMutationPayload } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function getOrderById(id: number) {\n  return fakeOrders.getOrderById(id);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\nexport async function updateOrder(id: number, data: OrderMutationPayload) {\n  return fakeOrders.updateOrder(id, data);\n}\nexport async function deleteOrder(id: number) {\n  return fakeOrders.deleteOrder(id);\n}\n```\n\n**Queries** (`api/queries.ts`) — query key factory + query options:\n\n```tsx\nimport { queryOptions } from '@tanstack/react-query';\nimport { getOrders, getOrderById } from './service';\nimport type { Order, OrderFilters } from './types';\n\nexport type { Order };\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({\n    queryKey: orderKeys.list(filters),\n    queryFn: () => getOrders(filters)\n  });\n\nexport const orderByIdOptions = (id: number) =>\n  queryOptions({\n    queryKey: orderKeys.detail(id),\n    queryFn: () => getOrderById(id)\n  });\n```\n\n**Mutations** (`api/mutations.ts`) — use `mutationOptions` + `getQueryClient()` (not custom hooks with `useQueryClient()`):\n\n```tsx\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { createOrder, updateOrder, deleteOrder } from './service';\nimport { orderKeys } from './queries';\nimport type { OrderMutationPayload } from './types';\n\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data: OrderMutationPayload) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const updateOrderMutation = mutationOptions({\n  mutationFn: ({ id, values }: { id: number; values: OrderMutationPayload }) =>\n    updateOrder(id, values),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const deleteOrderMutation = mutationOptions({\n  mutationFn: (id: number) => deleteOrder(id),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n```\n\n`mutationOptions` is the right abstraction because it works outside React (event handlers, tests, utilities), composes via spread at the call site, and uses `getQueryClient()` which handles both SSR (fresh per request) and client (singleton) correctly. See [references/query-abstractions.md](references/query-abstractions.md) for the full rationale.\n\n### Step 3: Zod Schema (`src/features/<name>/schemas/<name>.ts`)\n\n```tsx\nimport { z } from 'zod';\n\nexport const orderSchema = z.object({\n  customer: z.string().min(2, 'Customer name must be at least 2 characters'),\n  status: z.string().min(1, 'Please select a status'),\n  total: z.number({ message: 'Total is required' })\n});\n\nexport type OrderFormValues = z.infer<typeof orderSchema>;\n```\n\n### Step 4: Feature Components\n\nCreate `src/features/<name>/components/` with:\n\n**Listing page** (server component — `<name>-listing.tsx`):\n\n```tsx\nimport { HydrationBoundary, dehydrate } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport { ordersQueryOptions } from '../api/queries';\nimport { OrderTable, OrderTableSkeleton } from './orders-table';\nimport { Suspense } from 'react';\n\nexport default function OrderListingPage() {\n  const page = searchParamsCache.get('page');\n  const search = searchParamsCache.get('name');\n  const pageLimit = searchParamsCache.get('perPage');\n  const sort = searchParamsCache.get('sort');\n\n  const filters = {\n    page,\n    limit: pageLimit,\n    ...(search && { search }),\n    ...(sort.length > 0 && { sort: JSON.stringify(sort) })\n  };\n\n  const queryClient = getQueryClient();\n  void queryClient.prefetchQuery(ordersQueryOptions(filters));\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      <Suspense fallback={<OrderTableSkeleton />}>\n        <OrderTable />\n      </Suspense>\n    </HydrationBoundary>\n  );\n}\n```\n\n**Table + skeleton** (client component — `orders-table/index.tsx`):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';\nimport { getSortingStateParser } from '@/lib/data-table-parsers';\nimport { useDataTable } from '@/hooks/use-data-table';\nimport { DataTable } from '@/components/data-table/data-table';\nimport { DataTableToolbar } from '@/components/data-table/data-table-toolbar';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { ordersQueryOptions } from '../../api/queries';\nimport { columns } from './columns';\n\nexport function OrderTable() {\n  const [params] = useQueryStates({\n    page: parseAsInteger.withDefault(1),\n    perPage: parseAsInteger.withDefault(10),\n    name: parseAsString,\n    sort: getSortingStateParser().withDefault([]) // same parser as searchParamsCache\n  });\n\n  const filters = {\n    page: params.page,\n    limit: params.perPage,\n    ...(params.name && { search: params.name }),\n    ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })\n  };\n\n  const { data } = useSuspenseQuery(ordersQueryOptions(filters));\n\n  const { table } = useDataTable({\n    data: data.items,\n    columns,\n    pageCount: Math.ceil(data.total_items / params.perPage),\n    shallow: true,\n    debounceMs: 500,\n    initialState: { columnPinning: { start: [], end: ['actions'] } }\n  });\n\n  return (\n    <DataTable table={table}>\n      <DataTableToolbar table={table} />\n    </DataTable>\n  );\n}\n\nexport function OrderTableSkeleton() {\n  return (\n    <div className='space-y-4 p-4'>\n      <Skeleton className='h-10 w-full' />\n      <Skeleton className='h-96 w-full' />\n    </div>\n  );\n}\n```\n\n**Column definitions** (`orders-table/columns.tsx`):\n\nEach column needs `id`, `accessorKey` (or `accessorFn`), `header` with `DataTableColumnHeader`, and optionally `meta` for filtering + `enableColumnFilter: true`. Type columns as `ColumnDef<DataTableFeatures, Order>[]` (TanStack Table v9; `DataTableFeatures` from `@/lib/data-table-features` types `meta`).\n\n```tsx\nexport const columns: ColumnDef<DataTableFeatures, Order>[] = [\n  {\n    id: 'customer',\n    accessorKey: 'customer',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Customer' />,\n    meta: { label: 'Customer', placeholder: 'Search...', variant: 'text', icon: Icons.text },\n    enableColumnFilter: true\n  },\n  {\n    id: 'status',\n    accessorKey: 'status',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Status' />,\n    cell: ({ cell }) => (\n      <Badge variant='outline' className='capitalize'>\n        {cell.getValue<string>()}\n      </Badge>\n    ),\n    enableColumnFilter: true,\n    meta: { label: 'Status', variant: 'multiSelect', options: STATUS_OPTIONS }\n  },\n  { id: 'actions', cell: ({ row }) => <CellAction data={row.original} /> }\n];\n```\n\nFilter `meta.variant` options: `text`, `number`, `range`, `date`, `dateRange`, `select`, `multiSelect`, `boolean`. For multiSelect, provide `options: { value, label, icon? }[]`.\n\n**Cell actions** (`orders-table/cell-action.tsx`):\n\nPattern: `DropdownMenu` with edit/delete items + `AlertModal` for delete confirmation + `useMutation` for the delete API call.\n\n```tsx\nimport { deleteOrderMutation } from '../../api/mutations';\n\nexport const CellAction: React.FC<{ data: Order }> = ({ data }) => {\n  const [deleteOpen, setDeleteOpen] = useState(false);\n  const deleteMutation = useMutation({\n    ...deleteOrderMutation,\n    onSuccess: () => {\n      toast.success('Deleted');\n      setDeleteOpen(false);\n    }\n  });\n  return (\n    <>\n      <AlertModal\n        isOpen={deleteOpen}\n        onClose={() => setDeleteOpen(false)}\n        onConfirm={() => deleteMutation.mutate(data.id)}\n        loading={deleteMutation.isPending}\n      />\n      <DropdownMenu modal={false}>\n        <DropdownMenuTrigger asChild>\n          <Button variant='ghost' className='h-8 w-8 p-0'>\n            <Icons.ellipsis className='h-4 w-4' />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align='end'>\n          <DropdownMenuLabel>Actions</DropdownMenuLabel>\n          <DropdownMenuItem onClick={() => router.push(`/dashboard/orders/${data.id}`)}>\n            <Icons.edit className='mr-2 h-4 w-4' /> Edit\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setDeleteOpen(true)}>\n            <Icons.trash className='mr-2 h-4 w-4' /> Delete\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </>\n  );\n};\n```\n\nFor **sheet-based editing** (like Users), replace `router.push` with opening a `<FormSheet>` — see the Forms section below.\n\n### Step 5: Page Route (`src/app/dashboard/<name>/page.tsx`)\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderListingPage from '@/features/orders/components/order-listing';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport type { SearchParams } from 'nuqs/server';\n\nexport const metadata = { title: 'Dashboard: Orders' };\ntype PageProps = { searchParams: Promise<SearchParams> };\n\nexport default async function Page(props: PageProps) {\n  const searchParams = await props.searchParams;\n  searchParamsCache.parse(searchParams);\n\n  return (\n    <PageContainer\n      scrollable={false}\n      pageTitle='Orders'\n      pageDescription='Manage your orders.'\n      pageHeaderAction={/* Add button — Link or SheetTrigger */}\n    >\n      <OrderListingPage />\n    </PageContainer>\n  );\n}\n```\n\n**PageContainer props**: `scrollable`, `pageTitle`, `pageDescription`, `pageHeaderAction` (React node for the top-right button), `infoContent` (help sidebar), `access` + `accessFallback` (RBAC gating).\n\n**Detail/Edit page** (`src/app/dashboard/<name>/[id]/page.tsx`):\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderViewPage from '@/features/orders/components/order-view-page';\n\nexport const metadata = { title: 'Dashboard: Order Details' };\ntype PageProps = { params: Promise<{ id: string }> };\n\nexport default async function Page(props: PageProps) {\n  const { id } = await props.params;\n  return (\n    <PageContainer scrollable pageTitle='Order Details'>\n      <OrderViewPage orderId={id} />\n    </PageContainer>\n  );\n}\n```\n\n**View page component** (client — handles new vs edit):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { notFound } from 'next/navigation';\nimport { orderByIdOptions } from '../api/queries';\nimport OrderForm from './order-form';\n\nexport default function OrderViewPage({ orderId }: { orderId: string }) {\n  if (orderId === 'new') return <OrderForm initialData={null} pageTitle='Create Order' />;\n  const { data } = useSuspenseQuery(orderByIdOptions(Number(orderId)));\n  if (!data) notFound();\n  return <OrderForm initialData={data} pageTitle='Edit Order' />;\n}\n```\n\n### Step 6: Search Params (`src/lib/searchparams.ts`)\n\nAdd any new filter keys. Existing params: `page`, `perPage`, `name`, `gender`, `category`, `role`, `sort`.\n\n### Step 7: Navigation (`src/config/nav-config.ts`)\n\n```tsx\n{ title: 'Orders', url: '/dashboard/orders', icon: 'product', items: [] }\n```\n\n### Step 8: Icons (`src/components/icons.tsx`)\n\nTo register a new icon, import from `@tabler/icons-react` and add to the `Icons` object:\n\n```tsx\nimport { IconShoppingCart } from '@tabler/icons-react';\nexport const Icons = { /* ...existing */ cart: IconShoppingCart };\n```\n\nNever import `@tabler/icons-react` anywhere else. Always use `Icons.keyName`.\n\n**Existing icon keys** (partial): `dashboard`, `product`, `kanban`, `chat`, `forms`, `user`, `teams`, `billing`, `settings`, `add`, `edit`, `trash`, `search`, `check`, `close`, `clock`, `ellipsis`, `text`, `calendar`, `upload`, `spinner`, `chevronDown/Left/Right/Up`, `sun`, `moon`, `palette`, `pro`, `workspace`, `notification`.\n\n---\n\n## Forms\n\nForms use **TanStack Form + Zod** with `useAppForm` + `useFormFields<T>()` and `useMutation` for submission. See [references/forms-guide.md](references/forms-guide.md) for all field types, validation strategies, multi-step forms, and advanced patterns.\n\n### Page Form (Create/Edit on a dedicated route)\n\nThe full pattern is shown in Steps 1-4 above. The key structure:\n\n1. **Schema** — Zod schema + inferred type in `schemas/<name>.ts`\n2. **Form component** — `useAppForm({ defaultValues, validators: { onSubmit: schema }, onSubmit })` + `useFormFields<T>()` for typed fields\n3. **Mutations** — `useMutation({ ...createOrderMutation, onSuccess: () => { toast(); router.push() } })`, spread shared mutation options from `api/mutations.ts` and layer on UI callbacks\n4. **View page** — client component that checks `id === 'new'` for create vs `useSuspenseQuery(byIdOptions)` for edit\n\n### Sheet Form (Inline create/edit in a side panel)\n\nFor features where a separate page is overkill (like Users). The sheet manages open state; the form uses a `form` attribute to connect to the sheet footer's submit button.\n\n```tsx\n'use client';\nimport { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';\n\nexport function OrderFormSheet({\n  order,\n  open,\n  onOpenChange\n}: {\n  order?: Order;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}) {\n  const isEdit = !!order;\n  const mutation = useMutation({\n    ...(isEdit ? updateOrderMutation : createOrderMutation),\n    onSuccess: () => {\n      onOpenChange(false);\n    }\n  });\n  const form = useAppForm({\n    defaultValues: { customer: order?.customer ?? '' /* ... */ } as OrderFormValues,\n    validators: { onSubmit: orderSchema },\n    onSubmit: async ({ value }) => {\n      await mutation.mutateAsync(value);\n    }\n  });\n  const { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n  return (\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className='flex flex-col'>\n        <SheetHeader>\n          <SheetTitle>{isEdit ? 'Edit' : 'New'} Order</SheetTitle>\n        </SheetHeader>\n        <div className='flex-1 overflow-auto'>\n          <form.AppForm>\n            <form.Form id='order-sheet-form' className='space-y-4'>\n              <FormTextField name='customer' label='Customer' required />\n              <FormSelectField name='status' label='Status' required options={STATUS_OPTIONS} />\n            </form.Form>\n          </form.AppForm>\n        </div>\n        <SheetFooter>\n          <Button variant='outline' onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button type='submit' form='order-sheet-form' disabled={mutation.isPending}>\n            {mutation.isPending ? 'Saving...' : isEdit ? 'Update' : 'Create'}\n          </Button>\n        </SheetFooter>\n      </SheetContent>\n    </Sheet>\n  );\n}\n```\n\nFor cell actions, add `const [editOpen, setEditOpen] = useState(false)` and render `<OrderFormSheet order={data} open={editOpen} onOpenChange={setEditOpen} />` with a `<DropdownMenuItem onClick={() => setEditOpen(true)}>`. For the page header \"Add\" button, create a trigger component that manages `open` state and renders the sheet.\n\n**Available field components** from `useFormFields<T>()`: `FormTextField`, `FormTextareaField`, `FormSelectField`, `FormCheckboxField`, `FormSwitchField`, `FormRadioGroupField`, `FormSliderField`, `FormFileUploadField`.\n\n---\n\n## Data Fetching with React Query\n\nThe pattern is: server prefetch → HydrationBoundary → client useSuspenseQuery.\n\n1. **Server**: `void queryClient.prefetchQuery(options)` — fire-and-forget during SSR streaming\n2. **Client**: `useSuspenseQuery(options)` — picks up dehydrated data, suspends until resolved\n3. **HydrationBoundary + dehydrate**: bridges server cache → client cache\n4. **Suspense fallback**: skeleton shown while data streams\n\n**Why `useSuspenseQuery` not `useQuery`:** `useQuery` doesn't integrate with Suspense — it shows loading even when data is prefetched. `useSuspenseQuery` picks up the dehydrated pending query. Once cached (within `staleTime: 60s`), subsequent visits are instant.\n\n**Mutations** use `mutationOptions` + `getQueryClient()` in `mutations.ts`, composed via spread at the call site:\n\n```tsx\n// In mutations.ts — shared config\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\n// In component — spread + layer UI callbacks\nconst mutation = useMutation({\n  ...createOrderMutation,\n  onSuccess: () => toast.success('Created')\n});\n```\n\nSee [references/query-abstractions.md](references/query-abstractions.md) for why `mutationOptions`/`queryOptions` are the right abstraction over custom hooks.\n\n---\n\n## Navigation & RBAC\n\nConfigure in `src/config/nav-config.ts`. Items are filtered client-side in `src/hooks/use-nav.ts` using Clerk.\n\n**Access control properties** on nav items:\n\n- `requireOrg: boolean` — requires active Clerk organization\n- `permission: string` — requires specific Clerk permission\n- `role: string` — requires specific Clerk role\n- `plan: string` — requires subscription plan (server-side)\n- `feature: string` — requires feature flag (server-side)\n\nItems without `access` are visible to everyone. All client-side checks are synchronous — no loading states.\n\n---\n\n## Themes\n\nSee [references/theming-guide.md](references/theming-guide.md) for the complete guide. Quick steps:\n\n1. Create `src/styles/themes/<name>.css` with OKLCH color tokens + `@theme inline` block\n2. Import in `src/styles/theme.css`\n3. Register in `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add Google Fonts in `src/components/themes/font.config.ts`\n\n---\n\n## Code Conventions\n\n- **`cn()`** for class merging — never concatenate className strings\n- **Server components by default** — only add `'use client'` when needed\n- **React Query** — `void prefetchQuery()` on server + `useSuspenseQuery` on client\n- **API layer** — `types.ts` → `service.ts` → `queries.ts` → `mutations.ts` per feature; `queryOptions`/`mutationOptions` as base abstractions (not custom hooks); `getQueryClient()` in mutations (not `useQueryClient()`); key factories (`entityKeys.all/list/detail`); components never import mock APIs directly\n- **nuqs** — `searchParamsCache` on server, `useQueryStates` on client with `shallow: true`\n- **Icons** — only from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` + `useFormFields<T>()` from `@/components/ui/tanstack-form`\n- **Page headers** — `PageContainer` props, never import `<Heading>` manually\n- **Sort parser** — use `getSortingStateParser` from `@/lib/data-table-parsers` (same parser as `useDataTable`)\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n","frontmatter":{"name":"kiranism-shadcn-dashboard","description":"Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention \"dashboard\" explicitly — if they're adding UI, pages, or features to this project, use this skill.\n"},"isInternal":false,"tokens":5634,"sizeBytes":23493},{"name":"async-patterns.md","path":".agents/skills/next-best-practices/async-patterns.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/async-patterns.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Async Patterns\n\nIn Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous.\n\n## Async Params and SearchParams\n\nAlways type them as `Promise<...>` and await them.\n\n### Pages and Layouts\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Page({ params }: Props) {\n  const { slug } = await params;\n}\n```\n\n### Route Handlers\n\n```tsx\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n### SearchParams\n\n```tsx\ntype Props = {\n  params: Promise<{ slug: string }>;\n  searchParams: Promise<{ query?: string }>;\n};\n\nexport default async function Page({ params, searchParams }: Props) {\n  const { slug } = await params;\n  const { query } = await searchParams;\n}\n```\n\n### Synchronous Components\n\nUse `React.use()` for non-async components:\n\n```tsx\nimport { use } from 'react';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default function Page({ params }: Props) {\n  const { slug } = use(params);\n}\n```\n\n### generateMetadata\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  return { title: slug };\n}\n```\n\n## Async Cookies and Headers\n\n```tsx\nimport { cookies, headers } from 'next/headers';\n\nexport default async function Page() {\n  const cookieStore = await cookies();\n  const headersList = await headers();\n\n  const theme = cookieStore.get('theme');\n  const userAgent = headersList.get('user-agent');\n}\n```\n\n## Migration Codemod\n\n```bash\nnpx @next/codemod@latest next-async-request-api .\n```\n","isInternal":false,"tokens":413,"sizeBytes":1677},{"name":"bundling.md","path":".agents/skills/next-best-practices/bundling.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/bundling.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Bundling\n\nFix common bundling issues with third-party packages.\n\n## Server-Incompatible Packages\n\nSome packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components.\n\n### Error Signs\n\n```\nReferenceError: window is not defined\nReferenceError: document is not defined\nReferenceError: localStorage is not defined\nModule not found: Can't resolve 'fs'\n```\n\n### Solution 1: Mark as Client-Only\n\nIf the package is only needed on client:\n\n```tsx\n// Bad: Fails - package uses window\nimport SomeChart from 'some-chart-library';\n\nexport default function Page() {\n  return <SomeChart />;\n}\n\n// Good: Use dynamic import with ssr: false\nimport dynamic from 'next/dynamic';\n\nconst SomeChart = dynamic(() => import('some-chart-library'), {\n  ssr: false\n});\n\nexport default function Page() {\n  return <SomeChart />;\n}\n```\n\n### Solution 2: Externalize from Server Bundle\n\nFor packages that should run on server but have bundling issues:\n\n```js\n// next.config.js\nmodule.exports = {\n  serverExternalPackages: ['problematic-package']\n};\n```\n\nUse this for:\n\n- Packages with native bindings (sharp, bcrypt)\n- Packages that don't bundle well (some ORMs)\n- Packages with circular dependencies\n\n### Solution 3: Client Component Wrapper\n\nWrap the entire usage in a client component:\n\n```tsx\n// components/ChartWrapper.tsx\n'use client';\n\nimport { Chart } from 'chart-library';\n\nexport function ChartWrapper(props) {\n  return <Chart {...props} />;\n}\n\n// app/page.tsx (server component)\nimport { ChartWrapper } from '@/components/ChartWrapper';\n\nexport default function Page() {\n  return <ChartWrapper data={data} />;\n}\n```\n\n## CSS Imports\n\nImport CSS files instead of using `<link>` tags. Next.js handles bundling and optimization.\n\n```tsx\n// Bad: Manual link tag\n<link rel='stylesheet' href='/styles.css' />;\n\n// Good: Import CSS\nimport './styles.css';\n\n// Good: CSS Modules\nimport styles from './Button.module.css';\n```\n\n## Polyfills\n\nNext.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs.\n\nAlready included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others.\n\n```tsx\n// Bad: Redundant polyfills\n<script src='https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,Array.from' />\n\n// Good: Next.js includes these automatically\n```\n\n## ESM/CommonJS Issues\n\n### Error Signs\n\n```\nSyntaxError: Cannot use import statement outside a module\nError: require() of ES Module\nModule not found: ESM packages need to be imported\n```\n\n### Solution: Transpile Package\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['some-esm-package', 'another-package']\n};\n```\n\n## Common Problematic Packages\n\n| Package         | Issue           | Solution                                                        |\n| --------------- | --------------- | --------------------------------------------------------------- |\n| `sharp`         | Native bindings | `serverExternalPackages: ['sharp']`                             |\n| `bcrypt`        | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs`          |\n| `canvas`        | Native bindings | `serverExternalPackages: ['canvas']`                            |\n| `recharts`      | Uses window     | `dynamic(() => import('recharts'), { ssr: false })`             |\n| `react-quill`   | Uses document   | `dynamic(() => import('react-quill'), { ssr: false })`          |\n| `mapbox-gl`     | Uses window     | `dynamic(() => import('mapbox-gl'), { ssr: false })`            |\n| `monaco-editor` | Uses window     | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |\n| `lottie-web`    | Uses document   | `dynamic(() => import('lottie-react'), { ssr: false })`         |\n\n## Bundle Analysis\n\nAnalyze bundle size with the built-in analyzer (Next.js 16.1+):\n\n```bash\nnext experimental-analyze\n```\n\nThis opens an interactive UI to:\n\n- Filter by route, environment (client/server), and type\n- Inspect module sizes and import chains\n- View treemap visualization\n\nSave output for comparison:\n\n```bash\nnext experimental-analyze --output\n# Output saved to .next/diagnostics/analyze\n```\n\nReference: https://nextjs.org/docs/app/guides/package-bundling\n\n## Migrating from Webpack to Turbopack\n\nTurbopack is the default bundler in Next.js 15+. If you have custom webpack config, migrate to Turbopack-compatible alternatives:\n\n```js\n// next.config.js\nmodule.exports = {\n  // Good: Works with Turbopack\n  serverExternalPackages: ['package'],\n  transpilePackages: ['package'],\n\n  // Bad: Webpack-only - migrate away from this\n  webpack: (config) => {\n    // custom webpack config\n  }\n};\n```\n\nReference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack\n","isInternal":false,"tokens":1131,"sizeBytes":4772},{"name":"data-patterns.md","path":".agents/skills/next-best-practices/data-patterns.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/data-patterns.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Data Patterns\n\nChoose the right data fetching pattern for each use case.\n\n## Decision Tree\n\n```\nNeed to fetch data?\n├── From a Server Component?\n│   └── Use: Fetch directly (no API needed)\n│\n├── From a Client Component?\n│   ├── Is it a mutation (POST/PUT/DELETE)?\n│   │   └── Use: Server Action\n│   └── Is it a read (GET)?\n│       └── Use: Route Handler OR pass from Server Component\n│\n├── Need external API access (webhooks, third parties)?\n│   └── Use: Route Handler\n│\n└── Need REST API for mobile app / external clients?\n    └── Use: Route Handler\n```\n\n## Pattern 1: Server Components (Preferred for Reads)\n\nFetch data directly in Server Components - no API layer needed.\n\n```tsx\n// app/users/page.tsx\nasync function UsersPage() {\n  // Direct database access - no API round-trip\n  const users = await db.user.findMany();\n\n  // Or fetch from external API\n  const posts = await fetch('https://api.example.com/posts').then((r) => r.json());\n\n  return (\n    <ul>\n      {users.map((user) => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\n**Benefits**:\n\n- No API to maintain\n- No client-server waterfall\n- Secrets stay on server\n- Direct database access\n\n## Pattern 2: Server Actions (Preferred for Mutations)\n\nServer Actions are the recommended way to handle mutations.\n\n```tsx\n// app/actions.ts\n'use server';\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function createPost(formData: FormData) {\n  const title = formData.get('title') as string;\n\n  await db.post.create({ data: { title } });\n\n  revalidatePath('/posts');\n}\n\nexport async function deletePost(id: string) {\n  await db.post.delete({ where: { id } });\n\n  revalidateTag('posts');\n}\n```\n\n```tsx\n// app/posts/new/page.tsx\nimport { createPost } from '@/app/actions';\n\nexport default function NewPost() {\n  return (\n    <form action={createPost}>\n      <input name='title' required />\n      <button type='submit'>Create</button>\n    </form>\n  );\n}\n```\n\n**Benefits**:\n\n- End-to-end type safety\n- Progressive enhancement (works without JS)\n- Automatic request handling\n- Integrated with React transitions\n\n**Constraints**:\n\n- POST only (no GET caching semantics)\n- Internal use only (no external access)\n- Cannot return non-serializable data\n\n## Pattern 3: Route Handlers (APIs)\n\nUse Route Handlers when you need a REST API.\n\n```tsx\n// app/api/posts/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\n// GET is cacheable\nexport async function GET(request: NextRequest) {\n  const posts = await db.post.findMany();\n  return NextResponse.json(posts);\n}\n\n// POST for mutations\nexport async function POST(request: NextRequest) {\n  const body = await request.json();\n  const post = await db.post.create({ data: body });\n  return NextResponse.json(post, { status: 201 });\n}\n```\n\n**When to use**:\n\n- External API access (mobile apps, third parties)\n- Webhooks from external services\n- GET endpoints that need HTTP caching\n- OpenAPI/Swagger documentation needed\n\n**When NOT to use**:\n\n- Internal data fetching (use Server Components)\n- Mutations from your UI (use Server Actions)\n\n## Avoiding Data Waterfalls\n\n### Problem: Sequential Fetches\n\n```tsx\n// Bad: Sequential waterfalls\nasync function Dashboard() {\n  const user = await getUser(); // Wait...\n  const posts = await getPosts(); // Then wait...\n  const comments = await getComments(); // Then wait...\n\n  return <div>...</div>;\n}\n```\n\n### Solution 1: Parallel Fetching with Promise.all\n\n```tsx\n// Good: Parallel fetching\nasync function Dashboard() {\n  const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);\n\n  return <div>...</div>;\n}\n```\n\n### Solution 2: Streaming with Suspense\n\n```tsx\n// Good: Show content progressively\nimport { Suspense } from 'react';\n\nasync function Dashboard() {\n  return (\n    <div>\n      <Suspense fallback={<UserSkeleton />}>\n        <UserSection />\n      </Suspense>\n      <Suspense fallback={<PostsSkeleton />}>\n        <PostsSection />\n      </Suspense>\n    </div>\n  );\n}\n\nasync function UserSection() {\n  const user = await getUser(); // Fetches independently\n  return <div>{user.name}</div>;\n}\n\nasync function PostsSection() {\n  const posts = await getPosts(); // Fetches independently\n  return <PostList posts={posts} />;\n}\n```\n\n### Solution 3: Preload Pattern\n\n```tsx\n// lib/data.ts\nimport { cache } from 'react';\n\nexport const getUser = cache(async (id: string) => {\n  return db.user.findUnique({ where: { id } });\n});\n\nexport const preloadUser = (id: string) => {\n  void getUser(id); // Fire and forget\n};\n```\n\n```tsx\n// app/user/[id]/page.tsx\nimport { getUser, preloadUser } from '@/lib/data';\n\nexport default async function UserPage({ params }) {\n  const { id } = await params;\n\n  // Start fetching early\n  preloadUser(id);\n\n  // Do other work...\n\n  // Data likely ready by now\n  const user = await getUser(id);\n  return <div>{user.name}</div>;\n}\n```\n\n## Client Component Data Fetching\n\nWhen Client Components need data:\n\n### Option 1: Pass from Server Component (Preferred)\n\n```tsx\n// Server Component\nasync function Page() {\n  const data = await fetchData();\n  return <ClientComponent initialData={data} />;\n}\n\n// Client Component\n('use client');\nfunction ClientComponent({ initialData }) {\n  const [data, setData] = useState(initialData);\n  // ...\n}\n```\n\n### Option 2: Fetch on Mount (When Necessary)\n\n```tsx\n'use client';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    fetch('/api/data')\n      .then((r) => r.json())\n      .then(setData);\n  }, []);\n\n  if (!data) return <Loading />;\n  return <div>{data.value}</div>;\n}\n```\n\n### Option 3: Server Action for Reads (Works But Not Ideal)\n\nServer Actions can be called from Client Components for reads, but this is not their intended purpose:\n\n```tsx\n'use client';\nimport { getData } from './actions';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    getData().then(setData);\n  }, []);\n\n  return <div>{data?.value}</div>;\n}\n```\n\n**Note**: Server Actions always use POST, so no HTTP caching. Prefer Route Handlers for cacheable reads.\n\n## Quick Reference\n\n| Pattern                | Use Case                    | HTTP Method | Caching              |\n| ---------------------- | --------------------------- | ----------- | -------------------- |\n| Server Component fetch | Internal reads              | Any         | Full Next.js caching |\n| Server Action          | Mutations, form submissions | POST only   | No                   |\n| Route Handler          | External APIs, webhooks     | Any         | GET can be cached    |\n| Client fetch to API    | Client-side reads           | Any         | HTTP cache headers   |\n","isInternal":false,"tokens":1636,"sizeBytes":6880},{"name":"debug-tricks.md","path":".agents/skills/next-best-practices/debug-tricks.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/debug-tricks.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Debug Tricks\n\nTricks to speed up debugging Next.js applications.\n\n## MCP Endpoint (Dev Server)\n\nNext.js exposes a `/_next/mcp` endpoint in development for AI-assisted debugging via MCP (Model Context Protocol).\n\n- **Next.js 16+**: Enabled by default, use `next-devtools-mcp`\n- **Next.js < 16**: Requires `experimental.mcpServer: true` in next.config.js\n\nReference: https://nextjs.org/docs/app/guides/mcp\n\n**Important**: Find the actual port of the running Next.js dev server (check terminal output or `package.json` scripts). Don't assume port 3000.\n\n### Request Format\n\nThe endpoint uses JSON-RPC 2.0 over HTTP POST:\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\",\n    \"id\": \"1\",\n    \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"<tool-name>\",\n      \"arguments\": {}\n    }\n  }'\n```\n\n### Available Tools\n\n#### `get_errors`\n\nGet current errors from dev server (build errors, runtime errors with source-mapped stacks):\n\n```json\n{ \"name\": \"get_errors\", \"arguments\": {} }\n```\n\n#### `get_routes`\n\nDiscover all routes by scanning filesystem:\n\n```json\n{ \"name\": \"get_routes\", \"arguments\": {} }\n// Optional: { \"name\": \"get_routes\", \"arguments\": { \"routerType\": \"app\" } }\n```\n\nReturns: `{ \"appRouter\": [\"/\", \"/api/users/[id]\", ...], \"pagesRouter\": [...] }`\n\n#### `get_project_metadata`\n\nGet project path and dev server URL:\n\n```json\n{ \"name\": \"get_project_metadata\", \"arguments\": {} }\n```\n\nReturns: `{ \"projectPath\": \"/path/to/project\", \"devServerUrl\": \"http://localhost:3000\" }`\n\n#### `get_page_metadata`\n\nGet runtime metadata about current page render (requires active browser session):\n\n```json\n{ \"name\": \"get_page_metadata\", \"arguments\": {} }\n```\n\nReturns segment trie data showing layouts, boundaries, and page components.\n\n#### `get_logs`\n\nGet path to Next.js development log file:\n\n```json\n{ \"name\": \"get_logs\", \"arguments\": {} }\n```\n\nReturns path to `<distDir>/logs/next-development.log`\n\n#### `get_server_action_by_id`\n\nLocate a Server Action by ID:\n\n```json\n{ \"name\": \"get_server_action_by_id\", \"arguments\": { \"actionId\": \"<action-id>\" } }\n```\n\n### Example: Get Errors\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"tools/call\",\"params\":{\"name\":\"get_errors\",\"arguments\":{}}}'\n```\n\n## Rebuild Specific Routes (Next.js 16+)\n\nUse `--debug-build-paths` to rebuild only specific routes instead of the entire app:\n\n```bash\n# Rebuild a specific route\nnext build --debug-build-paths \"/dashboard\"\n\n# Rebuild routes matching a glob\nnext build --debug-build-paths \"/api/*\"\n\n# Dynamic routes\nnext build --debug-build-paths \"/blog/[slug]\"\n```\n\nUse this to:\n\n- Quickly verify a build fix without full rebuild\n- Debug static generation issues for specific pages\n- Iterate faster on build errors\n","isInternal":false,"tokens":781,"sizeBytes":2970},{"name":"directives.md","path":".agents/skills/next-best-practices/directives.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/directives.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Directives\n\n## React Directives\n\nThese are React directives, not Next.js specific.\n\n### `'use client'`\n\nMarks a component as a Client Component. Required for:\n\n- React hooks (`useState`, `useEffect`, etc.)\n- Event handlers (`onClick`, `onChange`)\n- Browser APIs (`window`, `localStorage`)\n\n```tsx\n'use client';\n\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-client\n\n### `'use server'`\n\nMarks a function as a Server Action. Can be passed to Client Components.\n\n```tsx\n'use server';\n\nexport async function submitForm(formData: FormData) {\n  // Runs on server\n}\n```\n\nOr inline within a Server Component:\n\n```tsx\nexport default function Page() {\n  async function submit() {\n    'use server';\n    // Runs on server\n  }\n  return <form action={submit}>...</form>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-server\n\n---\n\n## Next.js Directive\n\n### `'use cache'`\n\nMarks a function or component for caching. Part of Next.js Cache Components.\n\n```tsx\n'use cache';\n\nexport async function getCachedData() {\n  return await fetchData();\n}\n```\n\nRequires `cacheComponents: true` in `next.config.ts`.\n\nFor detailed usage including cache profiles, `cacheLife()`, `cacheTag()`, and `updateTag()`, see the `next-cache-components` skill.\n\nReference: https://nextjs.org/docs/app/api-reference/directives/use-cache\n","isInternal":false,"tokens":351,"sizeBytes":1486},{"name":"error-handling.md","path":".agents/skills/next-best-practices/error-handling.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/error-handling.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Error Handling\n\nHandle errors gracefully in Next.js applications.\n\nReference: https://nextjs.org/docs/app/getting-started/error-handling\n\n## Error Boundaries\n\n### `error.tsx`\n\nCatches errors in a route segment and its children:\n\n```tsx\n'use client';\n\nexport default function Error({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <div>\n      <h2>Something went wrong!</h2>\n      <button onClick={() => reset()}>Try again</button>\n    </div>\n  );\n}\n```\n\n**Important:** `error.tsx` must be a Client Component.\n\n### `global-error.tsx`\n\nCatches errors in root layout:\n\n```tsx\n'use client';\n\nexport default function GlobalError({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <html>\n      <body>\n        <h2>Something went wrong!</h2>\n        <button onClick={() => reset()}>Try again</button>\n      </body>\n    </html>\n  );\n}\n```\n\n**Important:** Must include `<html>` and `<body>` tags.\n\n## Server Actions: Navigation API Gotcha\n\n**Do NOT wrap navigation APIs in try-catch.** They throw special errors that Next.js handles internally.\n\nReference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior\n\n```tsx\n'use server'\n\nimport { redirect } from 'next/navigation'\nimport { notFound } from 'next/navigation'\n\n// Bad: try-catch catches the navigation \"error\"\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)  // This throws!\n  } catch (error) {\n    // redirect() throw is caught here - navigation fails!\n    return { error: 'Failed to create post' }\n  }\n}\n\n// Good: Call navigation APIs outside try-catch\nasync function createPost(formData: FormData) {\n  let post\n  try {\n    post = await db.post.create({ ... })\n  } catch (error) {\n    return { error: 'Failed to create post' }\n  }\n  redirect(`/posts/${post.id}`)  // Outside try-catch\n}\n\n// Good: Re-throw navigation errors\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)\n  } catch (error) {\n    if (error instanceof Error && error.message === 'NEXT_REDIRECT') {\n      throw error  // Re-throw navigation errors\n    }\n    return { error: 'Failed to create post' }\n  }\n}\n```\n\nSame applies to:\n\n- `redirect()` - 307 temporary redirect\n- `permanentRedirect()` - 308 permanent redirect\n- `notFound()` - 404 not found\n- `forbidden()` - 403 forbidden\n- `unauthorized()` - 401 unauthorized\n\nUse `unstable_rethrow()` to re-throw these errors in catch blocks:\n\n```tsx\nimport { unstable_rethrow } from 'next/navigation';\n\nasync function action() {\n  try {\n    // ...\n    redirect('/success');\n  } catch (error) {\n    unstable_rethrow(error); // Re-throws Next.js internal errors\n    return { error: 'Something went wrong' };\n  }\n}\n```\n\n## Redirects\n\n```tsx\nimport { redirect, permanentRedirect } from 'next/navigation';\n\n// 307 Temporary - use for most cases\nredirect('/new-path');\n\n// 308 Permanent - use for URL migrations (cached by browsers)\npermanentRedirect('/new-url');\n```\n\n## Auth Errors\n\nTrigger auth-related error pages:\n\n```tsx\nimport { forbidden, unauthorized } from 'next/navigation';\n\nasync function Page() {\n  const session = await getSession();\n\n  if (!session) {\n    unauthorized(); // Renders unauthorized.tsx (401)\n  }\n\n  if (!session.hasAccess) {\n    forbidden(); // Renders forbidden.tsx (403)\n  }\n\n  return <Dashboard />;\n}\n```\n\nCreate corresponding error pages:\n\n```tsx\n// app/forbidden.tsx\nexport default function Forbidden() {\n  return <div>You don't have access to this resource</div>;\n}\n\n// app/unauthorized.tsx\nexport default function Unauthorized() {\n  return <div>Please log in to continue</div>;\n}\n```\n\n## Not Found\n\n### `not-found.tsx`\n\nCustom 404 page for a route segment:\n\n```tsx\nexport default function NotFound() {\n  return (\n    <div>\n      <h2>Not Found</h2>\n      <p>Could not find the requested resource</p>\n    </div>\n  );\n}\n```\n\n### Triggering Not Found\n\n```tsx\nimport { notFound } from 'next/navigation';\n\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const post = await getPost(id);\n\n  if (!post) {\n    notFound(); // Renders closest not-found.tsx\n  }\n\n  return <div>{post.title}</div>;\n}\n```\n\n## Error Hierarchy\n\nErrors bubble up to the nearest error boundary:\n\n```\napp/\n├── error.tsx           # Catches errors from all children\n├── blog/\n│   ├── error.tsx       # Catches errors in /blog/*\n│   └── [slug]/\n│       ├── error.tsx   # Catches errors in /blog/[slug]\n│       └── page.tsx\n└── layout.tsx          # Errors here go to global-error.tsx\n```\n","isInternal":false,"tokens":1197,"sizeBytes":4762},{"name":"file-conventions.md","path":".agents/skills/next-best-practices/file-conventions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/file-conventions.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# File Conventions\n\nNext.js App Router uses file-based routing with special file conventions.\n\n## Project Structure\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure\n\n```\napp/\n├── layout.tsx          # Root layout (required)\n├── page.tsx            # Home page (/)\n├── loading.tsx         # Loading UI\n├── error.tsx           # Error UI\n├── not-found.tsx       # 404 UI\n├── global-error.tsx    # Global error UI\n├── route.ts            # API endpoint\n├── template.tsx        # Re-rendered layout\n├── default.tsx         # Parallel route fallback\n├── blog/\n│   ├── page.tsx        # /blog\n│   └── [slug]/\n│       └── page.tsx    # /blog/:slug\n└── (group)/            # Route group (no URL impact)\n    └── page.tsx\n```\n\n## Special Files\n\n| File            | Purpose                                  |\n| --------------- | ---------------------------------------- |\n| `page.tsx`      | UI for a route segment                   |\n| `layout.tsx`    | Shared UI for segment and children       |\n| `loading.tsx`   | Loading UI (Suspense boundary)           |\n| `error.tsx`     | Error UI (Error boundary)                |\n| `not-found.tsx` | 404 UI                                   |\n| `route.ts`      | API endpoint                             |\n| `template.tsx`  | Like layout but re-renders on navigation |\n| `default.tsx`   | Fallback for parallel routes             |\n\n## Route Segments\n\n```\napp/\n├── blog/               # Static segment: /blog\n├── [slug]/             # Dynamic segment: /:slug\n├── [...slug]/          # Catch-all: /a/b/c\n├── [[...slug]]/        # Optional catch-all: / or /a/b/c\n└── (marketing)/        # Route group (ignored in URL)\n```\n\n## Parallel Routes\n\n```\napp/\n├── @analytics/\n│   └── page.tsx\n├── @sidebar/\n│   └── page.tsx\n└── layout.tsx          # Receives { analytics, sidebar } as props\n```\n\n## Intercepting Routes\n\n```\napp/\n├── feed/\n│   └── page.tsx\n├── @modal/\n│   └── (.)photo/[id]/  # Intercepts /photo/[id] from /feed\n│       └── page.tsx\n└── photo/[id]/\n    └── page.tsx\n```\n\nConventions:\n\n- `(.)` - same level\n- `(..)` - one level up\n- `(..)(..)` - two levels up\n- `(...)` - from root\n\n## Private Folders\n\n```\napp/\n├── _components/        # Private folder (not a route)\n│   └── Button.tsx\n└── page.tsx\n```\n\nPrefix with `_` to exclude from routing.\n\n## Middleware / Proxy\n\n### Next.js 14-15: `middleware.ts`\n\n```ts\n// middleware.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  // Auth, redirects, rewrites, etc.\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n### Next.js 16+: `proxy.ts`\n\nRenamed for clarity - same capabilities, different names:\n\n```ts\n// proxy.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function proxy(request: NextRequest) {\n  // Same logic as middleware\n  return NextResponse.next();\n}\n\nexport const proxyConfig = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n| Version | File            | Export         | Config        |\n| ------- | --------------- | -------------- | ------------- |\n| v14-15  | `middleware.ts` | `middleware()` | `config`      |\n| v16+    | `proxy.ts`      | `proxy()`      | `proxyConfig` |\n\n**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.\n\n## File Conventions Reference\n\nReference: https://nextjs.org/docs/app/api-reference/file-conventions\n","isInternal":false,"tokens":979,"sizeBytes":3767},{"name":"font.md","path":".agents/skills/next-best-practices/font.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/font.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Font Optimization\n\nUse `next/font` for automatic font optimization with zero layout shift.\n\n## Google Fonts\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={inter.className}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Multiple Fonts\n\n```tsx\nimport { Inter, Roboto_Mono } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nconst robotoMono = Roboto_Mono({\n  subsets: ['latin'],\n  variable: '--font-roboto-mono'\n});\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={`${inter.variable} ${robotoMono.variable}`}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\nUse in CSS:\n\n```css\nbody {\n  font-family: var(--font-inter);\n}\n\ncode {\n  font-family: var(--font-roboto-mono);\n}\n```\n\n## Font Weights and Styles\n\n```tsx\n// Single weight\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: '400'\n});\n\n// Multiple weights\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: ['400', '500', '700']\n});\n\n// Variable font (recommended) - includes all weights\nconst inter = Inter({\n  subsets: ['latin']\n  // No weight needed - variable fonts support all weights\n});\n\n// With italic\nconst inter = Inter({\n  subsets: ['latin'],\n  style: ['normal', 'italic']\n});\n```\n\n## Local Fonts\n\n```tsx\nimport localFont from 'next/font/local';\n\nconst myFont = localFont({\n  src: './fonts/MyFont.woff2'\n});\n\n// Multiple files for different weights\nconst myFont = localFont({\n  src: [\n    {\n      path: './fonts/MyFont-Regular.woff2',\n      weight: '400',\n      style: 'normal'\n    },\n    {\n      path: './fonts/MyFont-Bold.woff2',\n      weight: '700',\n      style: 'normal'\n    }\n  ]\n});\n\n// Variable font\nconst myFont = localFont({\n  src: './fonts/MyFont-Variable.woff2',\n  variable: '--font-my-font'\n});\n```\n\n## Tailwind CSS Integration\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nexport default function RootLayout({ children }) {\n  return (\n    <html lang='en' className={inter.variable}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n```js\n// tailwind.config.js\nmodule.exports = {\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['var(--font-inter)']\n      }\n    }\n  }\n};\n```\n\n## Preloading Subsets\n\nOnly load needed character subsets:\n\n```tsx\n// Latin only (most common)\nconst inter = Inter({ subsets: ['latin'] });\n\n// Multiple subsets\nconst inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });\n```\n\n## Display Strategy\n\nControl font loading behavior:\n\n```tsx\nconst inter = Inter({\n  subsets: ['latin'],\n  display: 'swap' // Default - shows fallback, swaps when loaded\n});\n\n// Options:\n// 'auto' - browser decides\n// 'block' - short block period, then swap\n// 'swap' - immediate fallback, swap when ready (recommended)\n// 'fallback' - short block, short swap, then fallback\n// 'optional' - short block, no swap (use if font is optional)\n```\n\n## Don't Use Manual Font Links\n\nAlways use `next/font` instead of `<link>` tags for Google Fonts.\n\n```tsx\n// Bad: Manual link tag (blocks rendering, no optimization)\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Bad: Missing display and preconnect\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Good: Use next/font (self-hosted, zero layout shift)\nimport { Inter } from 'next/font/google'\n\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Importing font in every component\n// components/Button.tsx\nimport { Inter } from 'next/font/google'\nconst inter = Inter({ subsets: ['latin'] }) // Creates new instance each time!\n\n// Good: Import once in layout, use CSS variable\n// app/layout.tsx\nconst inter = Inter({ subsets: ['latin'], variable: '--font-inter' })\n\n// Bad: Using @import in CSS (blocks rendering)\n/* globals.css */\n@import url('https://fonts.googleapis.com/css2?family=Inter');\n\n// Good: Use next/font (self-hosted, no network request)\nimport { Inter } from 'next/font/google'\n\n// Bad: Loading all weights when only using a few\nconst inter = Inter({ subsets: ['latin'] }) // Loads all weights\n\n// Good: Specify only needed weights (for non-variable fonts)\nconst inter = Inter({ subsets: ['latin'], weight: ['400', '700'] })\n\n// Bad: Missing subset - loads all characters\nconst inter = Inter({})\n\n// Good: Always specify subset\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Font in Specific Components\n\n```tsx\n// For component-specific fonts, export from a shared file\n// lib/fonts.ts\nimport { Inter, Playfair_Display } from 'next/font/google';\n\nexport const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });\nexport const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });\n\n// components/Heading.tsx\nimport { playfair } from '@/lib/fonts';\n\nexport function Heading({ children }) {\n  return <h1 className={playfair.className}>{children}</h1>;\n}\n```\n","isInternal":false,"tokens":1317,"sizeBytes":5206},{"name":"functions.md","path":".agents/skills/next-best-practices/functions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/functions.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Functions\n\nNext.js function APIs.\n\nReference: https://nextjs.org/docs/app/api-reference/functions\n\n## Navigation Hooks (Client)\n\n| Hook                        | Purpose                                                        | Reference                                                                                |\n| --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `useRouter`                 | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router)                   |\n| `usePathname`               | Get current pathname                                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname)                 |\n| `useSearchParams`           | Read URL search parameters                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params)            |\n| `useParams`                 | Access dynamic route parameters                                | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params)                   |\n| `useSelectedLayoutSegment`  | Active child segment (one level)                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment)  |\n| `useSelectedLayoutSegments` | All active segments below layout                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |\n| `useLinkStatus`             | Check link prefetch status                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status)              |\n| `useReportWebVitals`        | Report Core Web Vitals metrics                                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals)        |\n\n## Server Functions\n\n| Function     | Purpose                                      | Reference                                                              |\n| ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |\n| `cookies`    | Read/write cookies                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies)    |\n| `headers`    | Read request headers                         | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers)    |\n| `draftMode`  | Enable preview of unpublished CMS content    | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |\n| `after`      | Run code after response finishes streaming   | [Docs](https://nextjs.org/docs/app/api-reference/functions/after)      |\n| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |\n| `userAgent`  | Parse User-Agent header                      | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent)  |\n\n## Generate Functions\n\n| Function                | Purpose                                 | Reference                                                                           |\n| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |\n| `generateStaticParams`  | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params)  |\n| `generateMetadata`      | Dynamic metadata                        | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata)       |\n| `generateViewport`      | Dynamic viewport config                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport)       |\n| `generateSitemaps`      | Multiple sitemaps for large sites       | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps)       |\n| `generateImageMetadata` | Multiple OG images per route            | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |\n\n## Request/Response\n\n| Function        | Purpose                        | Reference                                                                  |\n| --------------- | ------------------------------ | -------------------------------------------------------------------------- |\n| `NextRequest`   | Extended Request with helpers  | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request)   |\n| `NextResponse`  | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response)  |\n| `ImageResponse` | Generate OG images             | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |\n\n## Common Examples\n\n### Navigation\n\nUse `next/link` for internal navigation instead of `<a>` tags.\n\n```tsx\n// Bad: Plain anchor tag\n<a href='/about'>About</a>;\n\n// Good: Next.js Link\nimport Link from 'next/link';\n\n<Link href='/about'>About</Link>;\n```\n\nActive link styling:\n\n```tsx\n'use client';\n\nimport Link from 'next/link';\nimport { usePathname } from 'next/navigation';\n\nexport function NavLink({ href, children }) {\n  const pathname = usePathname();\n\n  return (\n    <Link href={href} className={pathname === href ? 'active' : ''}>\n      {children}\n    </Link>\n  );\n}\n```\n\n### Static Generation\n\n```tsx\n// app/blog/[slug]/page.tsx\nexport async function generateStaticParams() {\n  const posts = await getPosts();\n  return posts.map((post) => ({ slug: post.slug }));\n}\n```\n\n### After Response\n\n```tsx\nimport { after } from 'next/server';\n\nexport async function POST(request: Request) {\n  const data = await processRequest(request);\n\n  after(async () => {\n    await logAnalytics(data);\n  });\n\n  return Response.json({ success: true });\n}\n```\n","isInternal":false,"tokens":1103,"sizeBytes":5961},{"name":"hydration-error.md","path":".agents/skills/next-best-practices/hydration-error.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/hydration-error.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Hydration Errors\n\nDiagnose and fix React hydration mismatch errors.\n\n## Error Signs\n\n- \"Hydration failed because the initial UI does not match\"\n- \"Text content does not match server-rendered HTML\"\n\n## Debugging\n\nIn development, click the hydration error to see the server/client diff.\n\n## Common Causes and Fixes\n\n### Browser-only APIs\n\n```tsx\n// Bad: Causes mismatch - window doesn't exist on server\n<div>{window.innerWidth}</div>;\n\n// Good: Use client component with mounted check\n('use client');\nimport { useState, useEffect } from 'react';\n\nexport function ClientOnly({ children }: { children: React.ReactNode }) {\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted ? children : null;\n}\n```\n\n### Date/Time Rendering\n\nServer and client may be in different timezones:\n\n```tsx\n// Bad: Causes mismatch\n<span>{new Date().toLocaleString()}</span>;\n\n// Good: Render on client only\n('use client');\nconst [time, setTime] = useState<string>();\nuseEffect(() => setTime(new Date().toLocaleString()), []);\n```\n\n### Random Values or IDs\n\n```tsx\n// Bad: Random values differ between server and client\n<div id={Math.random().toString()}>\n\n// Good: Use useId hook\nimport { useId } from 'react'\n\nfunction Input() {\n  const id = useId()\n  return <input id={id} />\n}\n```\n\n### Invalid HTML Nesting\n\n```tsx\n// Bad: Invalid - div inside p\n<p><div>Content</div></p>\n\n// Bad: Invalid - p inside p\n<p><p>Nested</p></p>\n\n// Good: Valid nesting\n<div><p>Content</p></div>\n```\n\n### Third-party Scripts\n\nScripts that modify DOM during hydration.\n\n```tsx\n// Good: Use next/script with afterInteractive\nimport Script from 'next/script';\n\nexport default function Page() {\n  return <Script src='https://example.com/script.js' strategy='afterInteractive' />;\n}\n```\n","isInternal":false,"tokens":431,"sizeBytes":1791},{"name":"image.md","path":".agents/skills/next-best-practices/image.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/image.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Image Optimization\n\nUse `next/image` for automatic image optimization.\n\n## Always Use next/image\n\n```tsx\n// Bad: Avoid native img\n<img src='/hero.png' alt='Hero' />;\n\n// Good: Use next/image\nimport Image from 'next/image';\n<Image src='/hero.png' alt='Hero' width={800} height={400} />;\n```\n\n## Required Props\n\nImages need explicit dimensions to prevent layout shift:\n\n```tsx\n// Local images - dimensions inferred automatically\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" />\n\n// Remote images - must specify width/height\n<Image src=\"https://example.com/image.jpg\" alt=\"Hero\" width={800} height={400} />\n\n// Or use fill for parent-relative sizing\n<div style={{ position: 'relative', width: '100%', height: 400 }}>\n  <Image src=\"/hero.png\" alt=\"Hero\" fill style={{ objectFit: 'cover' }} />\n</div>\n```\n\n## Remote Images Configuration\n\nRemote domains must be configured in `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'example.com',\n        pathname: '/images/**'\n      },\n      {\n        protocol: 'https',\n        hostname: '*.cdn.com' // Wildcard subdomain\n      }\n    ]\n  }\n};\n```\n\n## Responsive Images\n\nUse `sizes` to tell the browser which size to download:\n\n```tsx\n// Full-width hero\n<Image\n  src=\"/hero.png\"\n  alt=\"Hero\"\n  fill\n  sizes=\"100vw\"\n/>\n\n// Responsive grid (3 columns on desktop, 1 on mobile)\n<Image\n  src=\"/card.png\"\n  alt=\"Card\"\n  fill\n  sizes=\"(max-width: 768px) 100vw, 33vw\"\n/>\n\n// Fixed sidebar image\n<Image\n  src=\"/avatar.png\"\n  alt=\"Avatar\"\n  width={200}\n  height={200}\n  sizes=\"200px\"\n/>\n```\n\n## Blur Placeholder\n\nPrevent layout shift with placeholders:\n\n```tsx\n// Local images - automatic blur hash\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" placeholder=\"blur\" />\n\n// Remote images - provide blurDataURL\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"blur\"\n  blurDataURL=\"data:image/jpeg;base64,/9j/4AAQSkZJRg...\"\n/>\n\n// Or use color placeholder\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"empty\"\n  style={{ backgroundColor: '#e0e0e0' }}\n/>\n```\n\n## Priority Loading\n\nUse `priority` for above-the-fold images (LCP):\n\n```tsx\n// Hero image - loads immediately\n<Image src=\"/hero.png\" alt=\"Hero\" fill priority />\n\n// Below-fold images - lazy loaded by default (no priority needed)\n<Image src=\"/card.png\" alt=\"Card\" width={400} height={300} />\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Missing sizes with fill - downloads largest image\n<Image src=\"/hero.png\" alt=\"Hero\" fill />\n\n// Good: Add sizes for proper responsive behavior\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" />\n\n// Bad: Using width/height for aspect ratio only\n<Image src=\"/hero.png\" alt=\"Hero\" width={16} height={9} />\n\n// Good: Use actual display dimensions or fill with sizes\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" style={{ objectFit: 'cover' }} />\n\n// Bad: Remote image without config\n<Image src=\"https://untrusted.com/image.jpg\" alt=\"Image\" width={400} height={300} />\n// Error: Invalid src prop, hostname not configured\n\n// Good: Add hostname to next.config.js remotePatterns\n```\n\n## Static Export\n\nWhen using `output: 'export'`, use `unoptimized` or custom loader:\n\n```tsx\n// Option 1: Disable optimization\n<Image src='/hero.png' alt='Hero' width={800} height={400} unoptimized />;\n\n// Option 2: Global config\n// next.config.js\nmodule.exports = {\n  output: 'export',\n  images: { unoptimized: true }\n};\n\n// Option 3: Custom loader (Cloudinary, Imgix, etc.)\nconst cloudinaryLoader = ({ src, width, quality }) => {\n  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;\n};\n\n<Image loader={cloudinaryLoader} src='sample.jpg' alt='Sample' width={800} height={400} />;\n```\n","isInternal":false,"tokens":1027,"sizeBytes":3885},{"name":"metadata.md","path":".agents/skills/next-best-practices/metadata.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/metadata.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Metadata\n\nAdd SEO metadata to Next.js pages using the Metadata API.\n\n## Important: Server Components Only\n\nThe `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.\n\nIf the target page has `'use client'`:\n\n1. Remove `'use client'` if possible, move client logic to child components\n2. Or extract metadata to a parent Server Component layout\n3. Or split the file: Server Component with metadata imports Client Components\n\n## Static Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n  title: 'Page Title',\n  description: 'Page description for search engines'\n};\n```\n\n## Dynamic Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  const post = await getPost(slug);\n  return { title: post.title, description: post.description };\n}\n```\n\n## Avoid Duplicate Fetches\n\nUse React `cache()` when the same data is needed for both metadata and page:\n\n```tsx\nimport { cache } from 'react';\n\nexport const getPost = cache(async (slug: string) => {\n  return await db.posts.findFirst({ where: { slug } });\n});\n```\n\n## Viewport\n\nSeparate from metadata for streaming support:\n\n```tsx\nimport type { Viewport } from 'next';\n\nexport const viewport: Viewport = {\n  width: 'device-width',\n  initialScale: 1,\n  themeColor: '#000000'\n};\n\n// Or dynamic\nexport function generateViewport({ params }): Viewport {\n  return { themeColor: getThemeColor(params) };\n}\n```\n\n## Title Templates\n\nIn root layout for consistent naming:\n\n```tsx\nexport const metadata: Metadata = {\n  title: { default: 'Site Name', template: '%s | Site Name' }\n};\n```\n\n## Metadata File Conventions\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions\n\nPlace these files in `app/` directory (or route segments):\n\n| File                            | Purpose                                       |\n| ------------------------------- | --------------------------------------------- |\n| `favicon.ico`                   | Favicon                                       |\n| `icon.png` / `icon.svg`         | App icon                                      |\n| `apple-icon.png`                | Apple app icon                                |\n| `opengraph-image.png`           | OG image                                      |\n| `twitter-image.png`             | Twitter card image                            |\n| `sitemap.ts` / `sitemap.xml`    | Sitemap (use `generateSitemaps` for multiple) |\n| `robots.ts` / `robots.txt`      | Robots directives                             |\n| `manifest.ts` / `manifest.json` | Web app manifest                              |\n\n## SEO Best Practice: Static Files Are Often Enough\n\nFor most sites, **static metadata files provide excellent SEO coverage**:\n\n```\napp/\n├── favicon.ico\n├── opengraph-image.png     # Works for both OG and Twitter\n├── sitemap.ts\n├── robots.ts\n└── layout.tsx              # With title/description metadata\n```\n\n**Tips:**\n\n- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)\n- Static `title` and `description` in layout metadata is sufficient for most pages\n- Only use dynamic `generateMetadata` when content varies per page\n\n---\n\n# OG Image Generation\n\nGenerate dynamic Open Graph images using `next/og`.\n\n## Important Rules\n\n1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)\n2. **No searchParams** - OG images can't access search params, use route params instead\n3. **Avoid Edge runtime** - Use default Node.js runtime\n\n```tsx\n// Good\nimport { ImageResponse } from 'next/og';\n\n// Bad\n// import { ImageResponse } from '@vercel/og'\n// export const runtime = 'edge'\n```\n\n## Basic OG Image\n\n```tsx\n// app/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Site Name';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\nexport default function Image() {\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 128,\n        background: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center'\n      }}\n    >\n      Hello World\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Dynamic OG Image\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Blog Post';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Image({ params }: Props) {\n  const { slug } = await params;\n  const post = await getPost(slug);\n\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 48,\n        background: 'linear-gradient(to bottom, #1a1a1a, #333)',\n        color: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: 48\n      }}\n    >\n      <div style={{ fontSize: 64, fontWeight: 'bold' }}>{post.title}</div>\n      <div style={{ marginTop: 24, opacity: 0.8 }}>{post.description}</div>\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Custom Fonts\n\n```tsx\nimport { ImageResponse } from 'next/og';\nimport { join } from 'path';\nimport { readFile } from 'fs/promises';\n\nexport default async function Image() {\n  const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');\n  const fontData = await readFile(fontPath);\n\n  return new ImageResponse(\n    <div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [{ name: 'Inter', data: fontData, style: 'normal' }]\n    }\n  );\n}\n```\n\n## File Naming\n\n- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)\n- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)\n\n## Styling Notes\n\nImageResponse uses Flexbox layout:\n\n- Use `display: 'flex'`\n- No CSS Grid support\n- Styles must be inline objects\n\n## Multiple OG Images\n\nUse `generateImageMetadata` for multiple images per route:\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport async function generateImageMetadata({ params }) {\n  const images = await getPostImages(params.slug);\n  return images.map((img, idx) => ({\n    id: idx,\n    alt: img.alt,\n    size: { width: 1200, height: 630 },\n    contentType: 'image/png'\n  }));\n}\n\nexport default async function Image({ params, id }) {\n  const images = await getPostImages(params.slug);\n  const image = images[id];\n  return new ImageResponse(/* ... */);\n}\n```\n\n## Multiple Sitemaps\n\nUse `generateSitemaps` for large sites:\n\n```tsx\n// app/sitemap.ts\nimport type { MetadataRoute } from 'next';\n\nexport async function generateSitemaps() {\n  // Return array of sitemap IDs\n  return [{ id: 0 }, { id: 1 }, { id: 2 }];\n}\n\nexport default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {\n  const start = id * 50000;\n  const end = start + 50000;\n  const products = await getProducts(start, end);\n\n  return products.map((product) => ({\n    url: `https://example.com/product/${product.id}`,\n    lastModified: product.updatedAt\n  }));\n}\n```\n\nGenerates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.\n","isInternal":false,"tokens":1809,"sizeBytes":7490},{"name":"parallel-routes.md","path":".agents/skills/next-best-practices/parallel-routes.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/parallel-routes.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Parallel & Intercepting Routes\n\nParallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.\n\n## File Structure\n\n```\napp/\n├── @modal/                    # Parallel route slot\n│   ├── default.tsx            # Required! Returns null\n│   ├── (.)photos/             # Intercepts /photos/*\n│   │   └── [id]/\n│   │       └── page.tsx       # Modal content\n│   └── [...]catchall/         # Optional: catch unmatched\n│       └── page.tsx\n├── photos/\n│   └── [id]/\n│       └── page.tsx           # Full page (direct access)\n├── layout.tsx                 # Renders both children and @modal\n└── page.tsx\n```\n\n## Step 1: Root Layout with Slot\n\n```tsx\n// app/layout.tsx\nexport default function RootLayout({\n  children,\n  modal\n}: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\n    <html>\n      <body>\n        {children}\n        {modal}\n      </body>\n    </html>\n  );\n}\n```\n\n## Step 2: Default File (Critical!)\n\n**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.\n\n```tsx\n// app/@modal/default.tsx\nexport default function Default() {\n  return null;\n}\n```\n\nWithout this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.\n\n## Step 3: Intercepting Route (Modal)\n\nThe `(.)` prefix intercepts routes at the same level.\n\n```tsx\n// app/@modal/(.)photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Step 4: Full Page (Direct Access)\n\n```tsx\n// app/photos/[id]/page.tsx\nexport default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <div className='full-page'>\n      <img src={photo.url} alt={photo.title} />\n      <h1>{photo.title}</h1>\n    </div>\n  );\n}\n```\n\n## Step 5: Modal Component with Correct Closing\n\n**Critical: Use `router.back()` to close modals, NOT `router.push()` or `<Link>`.**\n\n```tsx\n// components/modal.tsx\n'use client';\n\nimport { useRouter } from 'next/navigation';\nimport { useCallback, useEffect, useRef } from 'react';\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter();\n  const overlayRef = useRef<HTMLDivElement>(null);\n\n  // Close on escape key\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') {\n        router.back(); // Correct\n      }\n    }\n    document.addEventListener('keydown', onKeyDown);\n    return () => document.removeEventListener('keydown', onKeyDown);\n  }, [router]);\n\n  // Close on overlay click\n  const handleOverlayClick = useCallback(\n    (e: React.MouseEvent) => {\n      if (e.target === overlayRef.current) {\n        router.back(); // Correct\n      }\n    },\n    [router]\n  );\n\n  return (\n    <div\n      ref={overlayRef}\n      onClick={handleOverlayClick}\n      className='fixed inset-0 bg-black/50 flex items-center justify-center z-50'\n    >\n      <div className='bg-white rounded-lg p-6 max-w-2xl w-full mx-4'>\n        <button\n          onClick={() => router.back()} // Correct!\n          className='absolute top-4 right-4'\n        >\n          Close\n        </button>\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n### Why NOT `router.push('/')` or `<Link href=\"/\">`?\n\nUsing `push` or `Link` to \"close\" a modal:\n\n1. Adds a new history entry (back button shows modal again)\n2. Doesn't properly clear the intercepted route\n3. Can cause the modal to flash or persist unexpectedly\n\n`router.back()` correctly:\n\n1. Removes the intercepted route from history\n2. Returns to the previous page\n3. Properly unmounts the modal\n\n## Route Matcher Reference\n\nMatchers match **route segments**, not filesystem paths:\n\n| Matcher    | Matches       | Example                                                               |\n| ---------- | ------------- | --------------------------------------------------------------------- |\n| `(.)`      | Same level    | `@modal/(.)photos` intercepts `/photos`                               |\n| `(..)`     | One level up  | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |\n| `(..)(..)` | Two levels up | Rarely used                                                           |\n| `(...)`    | From root     | `@modal/(...)photos` intercepts `/photos` from anywhere               |\n\n**Common mistake**: Thinking `(..)` means \"parent folder\" - it means \"parent route segment\".\n\n## Handling Hard Navigation\n\nWhen users directly visit `/photos/123` (bookmark, refresh, shared link):\n\n- The intercepting route is bypassed\n- The full `photos/[id]/page.tsx` renders\n- Modal doesn't appear (expected behavior)\n\nIf you want the modal to appear on direct access too, you need additional logic:\n\n```tsx\n// app/photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoPage({ params }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  // Option: Render as modal on direct access too\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Common Gotchas\n\n### 1. Missing `default.tsx` → 404 on Refresh\n\nEvery `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).\n\n### 2. Modal Persists After Navigation\n\nYou're using `router.push()` instead of `router.back()`.\n\n### 3. Nested Parallel Routes Need Defaults Too\n\nIf you have `@modal` inside a route group, each level needs its own `default.tsx`:\n\n```\napp/\n├── (marketing)/\n│   ├── @modal/\n│   │   └── default.tsx     # Needed!\n│   └── layout.tsx\n└── layout.tsx\n```\n\n### 4. Intercepted Route Shows Wrong Content\n\nCheck your matcher:\n\n- `(.)photos` intercepts `/photos` from the same route level\n- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`\n\n### 5. TypeScript Errors with `params`\n\nIn Next.js 15+, `params` is a Promise:\n\n```tsx\n// Correct\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n## Complete Example: Photo Gallery Modal\n\n```\napp/\n├── @modal/\n│   ├── default.tsx\n│   └── (.)photos/\n│       └── [id]/\n│           └── page.tsx\n├── photos/\n│   ├── page.tsx           # Gallery grid\n│   └── [id]/\n│       └── page.tsx       # Full photo page\n├── layout.tsx\n└── page.tsx\n```\n\nLinks in the gallery:\n\n```tsx\n// app/photos/page.tsx\nimport Link from 'next/link';\n\nexport default async function Gallery() {\n  const photos = await getPhotos();\n\n  return (\n    <div className='grid grid-cols-3 gap-4'>\n      {photos.map((photo) => (\n        <Link key={photo.id} href={`/photos/${photo.id}`}>\n          <img src={photo.thumbnail} alt={photo.title} />\n        </Link>\n      ))}\n    </div>\n  );\n}\n```\n\nClicking a photo → Modal opens (intercepted)\nDirect URL → Full page renders\nRefresh while modal open → Full page renders\n","isInternal":false,"tokens":1840,"sizeBytes":7452},{"name":"route-handlers.md","path":".agents/skills/next-best-practices/route-handlers.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/route-handlers.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Route Handlers\n\nCreate API endpoints with `route.ts` files.\n\n## Basic Usage\n\n```tsx\n// app/api/users/route.ts\nexport async function GET() {\n  const users = await getUsers();\n  return Response.json(users);\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  const user = await createUser(body);\n  return Response.json(user, { status: 201 });\n}\n```\n\n## Supported Methods\n\n`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`\n\n## GET Handler Conflicts with page.tsx\n\n**A `route.ts` and `page.tsx` cannot coexist in the same folder.**\n\n```\napp/\n├── api/\n│   └── users/\n│       └── route.ts    # /api/users\n└── users/\n    ├── page.tsx        # /users (page)\n    └── route.ts        # Warning: Conflicts with page.tsx!\n```\n\nIf you need both a page and an API at the same path, use different paths:\n\n```\napp/\n├── users/\n│   └── page.tsx        # /users (page)\n└── api/\n    └── users/\n        └── route.ts    # /api/users (API)\n```\n\n## Environment Behavior\n\nRoute handlers run in a **Server Component-like environment**:\n\n- Yes: Can use `async/await`\n- Yes: Can access `cookies()`, `headers()`\n- Yes: Can use Node.js APIs\n- No: Cannot use React hooks\n- No: Cannot use React DOM APIs\n- No: Cannot use browser APIs\n\n```tsx\n// Bad: This won't work - no React DOM in route handlers\nimport { renderToString } from 'react-dom/server';\n\nexport async function GET() {\n  const html = renderToString(<Component />); // Error!\n  return new Response(html);\n}\n```\n\n## Dynamic Route Handlers\n\n```tsx\n// app/api/users/[id]/route.ts\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const user = await getUser(id);\n\n  if (!user) {\n    return Response.json({ error: 'Not found' }, { status: 404 });\n  }\n\n  return Response.json(user);\n}\n```\n\n## Request Helpers\n\n```tsx\nexport async function GET(request: Request) {\n  // URL and search params\n  const { searchParams } = new URL(request.url);\n  const query = searchParams.get('q');\n\n  // Headers\n  const authHeader = request.headers.get('authorization');\n\n  // Cookies (Next.js helper)\n  const cookieStore = await cookies();\n  const token = cookieStore.get('token');\n\n  return Response.json({ query, token });\n}\n```\n\n## Response Helpers\n\n```tsx\n// JSON response\nreturn Response.json({ data });\n\n// With status\nreturn Response.json({ error: 'Not found' }, { status: 404 });\n\n// With headers\nreturn Response.json(data, {\n  headers: {\n    'Cache-Control': 'max-age=3600'\n  }\n});\n\n// Redirect\nreturn Response.redirect(new URL('/login', request.url));\n\n// Stream\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/event-stream' }\n});\n```\n\n## When to Use Route Handlers vs Server Actions\n\n| Use Case                 | Route Handlers | Server Actions |\n| ------------------------ | -------------- | -------------- |\n| Form submissions         | No             | Yes            |\n| Data mutations from UI   | No             | Yes            |\n| Third-party webhooks     | Yes            | No             |\n| External API consumption | Yes            | No             |\n| Public REST API          | Yes            | No             |\n| File uploads             | Both work      | Both work      |\n\n**Prefer Server Actions** for mutations triggered from your UI.\n**Use Route Handlers** for external integrations and public APIs.\n","isInternal":false,"tokens":836,"sizeBytes":3449},{"name":"rsc-boundaries.md","path":".agents/skills/next-best-practices/rsc-boundaries.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/rsc-boundaries.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# RSC Boundaries\n\nDetect and prevent invalid patterns when crossing Server/Client component boundaries.\n\n## Detection Rules\n\n### 1. Async Client Components Are Invalid\n\nClient components **cannot** be async functions. Only Server Components can be async.\n\n**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`\n\n```tsx\n// Bad: async client component\n'use client';\nexport default async function UserProfile() {\n  const user = await getUser(); // Cannot await in client component\n  return <div>{user.name}</div>;\n}\n\n// Good: Remove async, fetch data in parent server component\n// page.tsx (server component - no 'use client')\nexport default async function Page() {\n  const user = await getUser();\n  return <UserProfile user={user} />;\n}\n\n// UserProfile.tsx (client component)\n('use client');\nexport function UserProfile({ user }: { user: User }) {\n  return <div>{user.name}</div>;\n}\n```\n\n```tsx\n// Bad: async arrow function client component\n'use client';\nconst Dashboard = async () => {\n  const data = await fetchDashboard();\n  return <div>{data}</div>;\n};\n\n// Good: Fetch in server component, pass data down\n```\n\n### 2. Non-Serializable Props to Client Components\n\nProps passed from Server → Client must be JSON-serializable.\n\n**Detect:** Server component passes these to a client component:\n\n- Functions (except Server Actions with `'use server'`)\n- `Date` objects\n- `Map`, `Set`, `WeakMap`, `WeakSet`\n- Class instances\n- `Symbol` (unless globally registered)\n- Circular references\n\n```tsx\n// Bad: Function prop\n// page.tsx (server)\nexport default function Page() {\n  const handleClick = () => console.log('clicked');\n  return <ClientButton onClick={handleClick} />;\n}\n\n// Good: Define function inside client component\n// ClientButton.tsx\n('use client');\nexport function ClientButton() {\n  const handleClick = () => console.log('clicked');\n  return <button onClick={handleClick}>Click</button>;\n}\n```\n\n```tsx\n// Bad: Date object (silently becomes string, then crashes)\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt} />; // Date object\n}\n\n// PostCard.tsx (client) - will crash on .getFullYear()\n('use client');\nexport function PostCard({ createdAt }: { createdAt: Date }) {\n  return <span>{createdAt.getFullYear()}</span>; // Runtime error!\n}\n\n// Good: Serialize to string on server\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt.toISOString()} />;\n}\n\n// PostCard.tsx (client)\n('use client');\nexport function PostCard({ createdAt }: { createdAt: string }) {\n  const date = new Date(createdAt);\n  return <span>{date.getFullYear()}</span>;\n}\n```\n\n```tsx\n// Bad: Class instance\nconst user = new UserModel(data)\n<ClientProfile user={user} /> // Methods will be stripped\n\n// Good: Pass plain object\nconst user = await getUser()\n<ClientProfile user={{ id: user.id, name: user.name }} />\n```\n\n```tsx\n// Bad: Map/Set\n<ClientComponent items={new Map([['a', 1]])} />\n\n// Good: Convert to array/object\n<ClientComponent items={Object.fromEntries(map)} />\n<ClientComponent items={Array.from(set)} />\n```\n\n### 3. Server Actions Are the Exception\n\nFunctions marked with `'use server'` CAN be passed to client components.\n\n```tsx\n// Valid: Server Action can be passed\n// actions.ts\n'use server';\nexport async function submitForm(formData: FormData) {\n  // server-side logic\n}\n\n// page.tsx (server)\nimport { submitForm } from './actions';\nexport default function Page() {\n  return <ClientForm onSubmit={submitForm} />; // OK!\n}\n\n// ClientForm.tsx (client)\n('use client');\nexport function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {\n  return <form action={onSubmit}>...</form>;\n}\n```\n\n## Quick Reference\n\n| Pattern                           | Valid? | Fix                                   |\n| --------------------------------- | ------ | ------------------------------------- |\n| `'use client'` + `async function` | No     | Fetch in server parent, pass data     |\n| Pass `() => {}` to client         | No     | Define in client or use server action |\n| Pass `new Date()` to client       | No     | Use `.toISOString()`                  |\n| Pass `new Map()` to client        | No     | Convert to object/array               |\n| Pass class instance to client     | No     | Pass plain object                     |\n| Pass server action to client      | Yes    | -                                     |\n| Pass `string/number/boolean`      | Yes    | -                                     |\n| Pass plain object/array           | Yes    | -                                     |\n","isInternal":false,"tokens":1062,"sizeBytes":4707},{"name":"runtime-selection.md","path":".agents/skills/next-best-practices/runtime-selection.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/runtime-selection.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Runtime Selection\n\n## Use Node.js Runtime by Default\n\nUse the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.\n\n```tsx\n// Good: Default - no runtime config needed (uses Node.js)\nexport default function Page() { ... }\n\n// Caution: Only if already used in project or specifically required\nexport const runtime = 'edge'\n```\n\n## When to Use Each\n\n### Node.js Runtime (Default)\n\n- Full Node.js API support\n- File system access (`fs`)\n- Full `crypto` support\n- Database connections\n- Most npm packages work\n\n### Edge Runtime\n\n- Only for specific edge-location latency requirements\n- Limited API (no `fs`, limited `crypto`)\n- Smaller cold start\n- Geographic distribution needs\n\n## Detection\n\n**Before adding `runtime = 'edge'`**, check:\n\n1. Does the project already use Edge runtime?\n2. Is there a specific latency requirement?\n3. Are all dependencies Edge-compatible?\n\nIf unsure, use Node.js runtime.\n","isInternal":false,"tokens":220,"sizeBytes":986},{"name":"scripts.md","path":".agents/skills/next-best-practices/scripts.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/scripts.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Scripts\n\nLoading third-party scripts in Next.js.\n\n## Use next/script\n\nAlways use `next/script` instead of native `<script>` tags for better performance.\n\n```tsx\n// Bad: Native script tag\n<script src='https://example.com/script.js'></script>;\n\n// Good: Next.js Script component\nimport Script from 'next/script';\n\n<Script src='https://example.com/script.js' />;\n```\n\n## Inline Scripts Need ID\n\nInline scripts require an `id` attribute for Next.js to track them.\n\n```tsx\n// Bad: Missing id\n<Script dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Has id\n<Script id=\"my-script\" dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Inline with id\n<Script id=\"show-banner\">\n  {`document.getElementById('banner').classList.remove('hidden')`}\n</Script>\n```\n\n## Don't Put Script in Head\n\n`next/script` should not be placed inside `next/head`. It handles its own positioning.\n\n```tsx\n// Bad: Script inside Head\nimport Head from 'next/head'\nimport Script from 'next/script'\n\n<Head>\n  <Script src=\"/analytics.js\" />\n</Head>\n\n// Good: Script outside Head\n<Head>\n  <title>Page</title>\n</Head>\n<Script src=\"/analytics.js\" />\n```\n\n## Loading Strategies\n\n```tsx\n// afterInteractive (default) - Load after page is interactive\n<Script src=\"/analytics.js\" strategy=\"afterInteractive\" />\n\n// lazyOnload - Load during idle time\n<Script src=\"/widget.js\" strategy=\"lazyOnload\" />\n\n// beforeInteractive - Load before page is interactive (use sparingly)\n// Only works in app/layout.tsx or pages/_document.js\n<Script src=\"/critical.js\" strategy=\"beforeInteractive\" />\n\n// worker - Load in web worker (experimental)\n<Script src=\"/heavy.js\" strategy=\"worker\" />\n```\n\n## Google Analytics\n\nUse `@next/third-parties` instead of inline GA scripts.\n\n```tsx\n// Bad: Inline GA script\n<Script src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\" />\n<Script id=\"ga-init\">\n  {`window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n    gtag('config', 'G-XXXXX');`}\n</Script>\n\n// Good: Next.js component\nimport { GoogleAnalytics } from '@next/third-parties/google'\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <body>{children}</body>\n      <GoogleAnalytics gaId=\"G-XXXXX\" />\n    </html>\n  )\n}\n```\n\n## Google Tag Manager\n\n```tsx\nimport { GoogleTagManager } from '@next/third-parties/google';\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <GoogleTagManager gtmId='GTM-XXXXX' />\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Other Third-Party Scripts\n\n```tsx\n// YouTube embed\nimport { YouTubeEmbed } from '@next/third-parties/google';\n\n<YouTubeEmbed videoid='dQw4w9WgXcQ' />;\n\n// Google Maps\nimport { GoogleMapsEmbed } from '@next/third-parties/google';\n\n<GoogleMapsEmbed apiKey='YOUR_API_KEY' mode='place' q='Brooklyn+Bridge,New+York,NY' />;\n```\n\n## Quick Reference\n\n| Pattern                                       | Issue                      | Fix                       |\n| --------------------------------------------- | -------------------------- | ------------------------- |\n| `<script src=\"...\">`                          | No optimization            | Use `next/script`         |\n| `<Script>` without id                         | Can't track inline scripts | Add `id` attribute        |\n| `<Script>` inside `<Head>`                    | Wrong placement            | Move outside Head         |\n| Inline GA/GTM scripts                         | No optimization            | Use `@next/third-parties` |\n| `strategy=\"beforeInteractive\"` outside layout | Won't work                 | Only use in root layout   |\n","isInternal":false,"tokens":882,"sizeBytes":3657},{"name":"self-hosting.md","path":".agents/skills/next-best-practices/self-hosting.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/self-hosting.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Self-Hosting Next.js\n\nDeploy Next.js outside of Vercel with confidence.\n\n## Quick Start: Standalone Output\n\nFor Docker or any containerized deployment, use standalone output:\n\n```js\n// next.config.js\nmodule.exports = {\n  output: 'standalone'\n};\n```\n\nThis creates a minimal `standalone` folder with only production dependencies:\n\n```\n.next/\n├── standalone/\n│   ├── server.js          # Entry point\n│   ├── node_modules/      # Only production deps\n│   └── .next/             # Build output\n└── static/                # Must be copied separately\n```\n\n## Docker Deployment\n\n### Dockerfile\n\n```dockerfile\nFROM node:20-alpine AS base\n\n# Install dependencies\nFROM base AS deps\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci\n\n# Build\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nRUN npm run build\n\n# Production\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\n\n# Create non-root user\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\n# Copy standalone output\nCOPY --from=builder /app/.next/standalone ./\nCOPY --from=builder /app/.next/static ./.next/static\nCOPY --from=builder /app/public ./public\n\nUSER nextjs\n\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n```\n\n### Docker Compose\n\n```yaml\nversion: '3.8'\n\nservices:\n  web:\n    build: .\n    ports:\n      - '3000:3000'\n    environment:\n      - NODE_ENV=production\n    restart: unless-stopped\n    healthcheck:\n      test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']\n      interval: 30s\n      timeout: 10s\n      retries: 3\n```\n\n## PM2 Deployment\n\nFor traditional server deployments:\n\n```js\n// ecosystem.config.js\nmodule.exports = {\n  apps: [\n    {\n      name: 'nextjs',\n      script: '.next/standalone/server.js',\n      instances: 'max',\n      exec_mode: 'cluster',\n      env: {\n        NODE_ENV: 'production',\n        PORT: 3000\n      }\n    }\n  ]\n};\n```\n\n```bash\nnpm run build\npm2 start ecosystem.config.js\n```\n\n## ISR and Cache Handlers\n\n### The Problem\n\nISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:\n\n- Instance A regenerates page → saves to its local disk\n- Instance B serves stale page → doesn't see Instance A's cache\n- Load balancer sends users to random instances → inconsistent content\n\n### Solution: Custom Cache Handler\n\nNext.js 14+ supports custom cache handlers for shared storage:\n\n```js\n// next.config.js\nmodule.exports = {\n  cacheHandler: require.resolve('./cache-handler.js'),\n  cacheMaxMemorySize: 0 // Disable in-memory cache\n};\n```\n\n#### Redis Cache Handler Example\n\n```js\n// cache-handler.js\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL);\nconst CACHE_PREFIX = 'nextjs:';\n\nmodule.exports = class CacheHandler {\n  constructor(options) {\n    this.options = options;\n  }\n\n  async get(key) {\n    const data = await redis.get(CACHE_PREFIX + key);\n    if (!data) return null;\n\n    const parsed = JSON.parse(data);\n    return {\n      value: parsed.value,\n      lastModified: parsed.lastModified\n    };\n  }\n\n  async set(key, data, ctx) {\n    const cacheData = {\n      value: data,\n      lastModified: Date.now()\n    };\n\n    // Set TTL based on revalidate option\n    if (ctx?.revalidate) {\n      await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));\n    } else {\n      await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));\n    }\n  }\n\n  async revalidateTag(tags) {\n    // Implement tag-based invalidation\n    // This requires tracking which keys have which tags\n  }\n};\n```\n\n#### S3 Cache Handler Example\n\n```js\n// cache-handler.js\nconst { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');\n\nconst s3 = new S3Client({ region: process.env.AWS_REGION });\nconst BUCKET = process.env.CACHE_BUCKET;\n\nmodule.exports = class CacheHandler {\n  async get(key) {\n    try {\n      const response = await s3.send(\n        new GetObjectCommand({\n          Bucket: BUCKET,\n          Key: `cache/${key}`\n        })\n      );\n      const body = await response.Body.transformToString();\n      return JSON.parse(body);\n    } catch (err) {\n      if (err.name === 'NoSuchKey') return null;\n      throw err;\n    }\n  }\n\n  async set(key, data, ctx) {\n    await s3.send(\n      new PutObjectCommand({\n        Bucket: BUCKET,\n        Key: `cache/${key}`,\n        Body: JSON.stringify({\n          value: data,\n          lastModified: Date.now()\n        }),\n        ContentType: 'application/json'\n      })\n    );\n  }\n};\n```\n\n## What Works vs What Needs Setup\n\n| Feature              | Single Instance | Multi-Instance      | Notes                       |\n| -------------------- | --------------- | ------------------- | --------------------------- |\n| SSR                  | Yes             | Yes                 | No special setup            |\n| SSG                  | Yes             | Yes                 | Built at deploy time        |\n| ISR                  | Yes             | Needs cache handler | Filesystem cache breaks     |\n| Image Optimization   | Yes             | Yes                 | CPU-intensive, consider CDN |\n| Middleware           | Yes             | Yes                 | Runs on Node.js             |\n| Edge Runtime         | Limited         | Limited             | Some features Node-only     |\n| `revalidatePath/Tag` | Yes             | Needs cache handler | Must share cache            |\n| `next/font`          | Yes             | Yes                 | Fonts bundled at build      |\n| Draft Mode           | Yes             | Yes                 | Cookie-based                |\n\n## Image Optimization\n\nNext.js Image Optimization works out of the box but is CPU-intensive.\n\n### Option 1: Built-in (Simple)\n\nWorks automatically, but consider:\n\n- Set `deviceSizes` and `imageSizes` in config to limit variants\n- Use `minimumCacheTTL` to reduce regeneration\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    minimumCacheTTL: 60 * 60 * 24, // 24 hours\n    deviceSizes: [640, 750, 1080, 1920] // Limit sizes\n  }\n};\n```\n\n### Option 2: External Loader (Recommended for Scale)\n\nOffload to Cloudinary, Imgix, or similar:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    loader: 'custom',\n    loaderFile: './lib/image-loader.js'\n  }\n};\n```\n\n```js\n// lib/image-loader.js\nexport default function cloudinaryLoader({ src, width, quality }) {\n  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];\n  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;\n}\n```\n\n## Environment Variables\n\n### Build-time vs Runtime\n\n```js\n// Available at build time only (baked into bundle)\nNEXT_PUBLIC_API_URL=https://api.example.com\n\n// Available at runtime (server-side only)\nDATABASE_URL=postgresql://...\nAPI_SECRET=...\n```\n\n### Runtime Configuration\n\nFor truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:\n\n```tsx\n// app/api/config/route.ts\nexport async function GET() {\n  return Response.json({\n    apiUrl: process.env.API_URL,\n    features: process.env.FEATURES?.split(',')\n  });\n}\n```\n\n## OpenNext: Serverless Without Vercel\n\n[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.\n\n```bash\nnpx create-sst@latest\n# or\nnpx @opennextjs/aws build\n```\n\nSupports:\n\n- AWS Lambda + CloudFront\n- Cloudflare Workers\n- Netlify Functions\n- Deno Deploy\n\n## Health Check Endpoint\n\nAlways include a health check for load balancers:\n\n```tsx\n// app/api/health/route.ts\nexport async function GET() {\n  try {\n    // Optional: check database connection\n    // await db.$queryRaw`SELECT 1`;\n\n    return Response.json({ status: 'healthy' }, { status: 200 });\n  } catch (error) {\n    return Response.json({ status: 'unhealthy' }, { status: 503 });\n  }\n}\n```\n\n## Pre-Deployment Checklist\n\n1. **Build locally first**: `npm run build` - catch errors before deploy\n2. **Test standalone output**: `node .next/standalone/server.js`\n3. **Set `output: 'standalone'`** for Docker\n4. **Configure cache handler** for multi-instance ISR\n5. **Set `HOSTNAME=\"0.0.0.0\"`** for containers\n6. **Copy `public/` and `.next/static/`** - not included in standalone\n7. **Add health check endpoint**\n8. **Test ISR revalidation** after deployment\n9. **Monitor memory usage** - Node.js defaults may need tuning\n\n## Testing Cache Handler\n\n**Critical**: Test your cache handler on every Next.js upgrade:\n\n```bash\n# Start multiple instances\nPORT=3001 node .next/standalone/server.js &\nPORT=3002 node .next/standalone/server.js &\n\n# Trigger ISR revalidation\ncurl http://localhost:3001/api/revalidate?path=/posts\n\n# Verify both instances see the update\ncurl http://localhost:3001/posts\ncurl http://localhost:3002/posts\n# Should return identical content\n```\n","isInternal":false,"tokens":2173,"sizeBytes":8817},{"name":"SKILL.md","path":".agents/skills/next-best-practices/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/SKILL.md","title":"next-best-practices","category":"anthropic-skill","format":"markdown","content":"---\nname: next-best-practices\ndescription: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling\nuser-invocable: false\n---\n\n# Next.js Best Practices\n\nApply these rules when writing or reviewing Next.js code.\n\n## File Conventions\n\nSee [file-conventions.md](./file-conventions.md) for:\n\n- Project structure and special files\n- Route segments (dynamic, catch-all, groups)\n- Parallel and intercepting routes\n- Middleware rename in v16 (middleware → proxy)\n\n## RSC Boundaries\n\nDetect invalid React Server Component patterns.\n\nSee [rsc-boundaries.md](./rsc-boundaries.md) for:\n\n- Async client component detection (invalid)\n- Non-serializable props detection\n- Server Action exceptions\n\n## Async Patterns\n\nNext.js 15+ async API changes.\n\nSee [async-patterns.md](./async-patterns.md) for:\n\n- Async `params` and `searchParams`\n- Async `cookies()` and `headers()`\n- Migration codemod\n\n## Runtime Selection\n\nSee [runtime-selection.md](./runtime-selection.md) for:\n\n- Default to Node.js runtime\n- When Edge runtime is appropriate\n\n## Directives\n\nSee [directives.md](./directives.md) for:\n\n- `'use client'`, `'use server'` (React)\n- `'use cache'` (Next.js)\n\n## Functions\n\nSee [functions.md](./functions.md) for:\n\n- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`\n- Server functions: `cookies`, `headers`, `draftMode`, `after`\n- Generate functions: `generateStaticParams`, `generateMetadata`\n\n## Error Handling\n\nSee [error-handling.md](./error-handling.md) for:\n\n- `error.tsx`, `global-error.tsx`, `not-found.tsx`\n- `redirect`, `permanentRedirect`, `notFound`\n- `forbidden`, `unauthorized` (auth errors)\n- `unstable_rethrow` for catch blocks\n\n## Data Patterns\n\nSee [data-patterns.md](./data-patterns.md) for:\n\n- Server Components vs Server Actions vs Route Handlers\n- Avoiding data waterfalls (`Promise.all`, Suspense, preload)\n- Client component data fetching\n\n## Route Handlers\n\nSee [route-handlers.md](./route-handlers.md) for:\n\n- `route.ts` basics\n- GET handler conflicts with `page.tsx`\n- Environment behavior (no React DOM)\n- When to use vs Server Actions\n\n## Metadata & OG Images\n\nSee [metadata.md](./metadata.md) for:\n\n- Static and dynamic metadata\n- `generateMetadata` function\n- OG image generation with `next/og`\n- File-based metadata conventions\n\n## Image Optimization\n\nSee [image.md](./image.md) for:\n\n- Always use `next/image` over `<img>`\n- Remote images configuration\n- Responsive `sizes` attribute\n- Blur placeholders\n- Priority loading for LCP\n\n## Font Optimization\n\nSee [font.md](./font.md) for:\n\n- `next/font` setup\n- Google Fonts, local fonts\n- Tailwind CSS integration\n- Preloading subsets\n\n## Bundling\n\nSee [bundling.md](./bundling.md) for:\n\n- Server-incompatible packages\n- CSS imports (not link tags)\n- Polyfills (already included)\n- ESM/CommonJS issues\n- Bundle analysis\n\n## Scripts\n\nSee [scripts.md](./scripts.md) for:\n\n- `next/script` vs native script tags\n- Inline scripts need `id`\n- Loading strategies\n- Google Analytics with `@next/third-parties`\n\n## Hydration Errors\n\nSee [hydration-error.md](./hydration-error.md) for:\n\n- Common causes (browser APIs, dates, invalid HTML)\n- Debugging with error overlay\n- Fixes for each cause\n\n## Suspense Boundaries\n\nSee [suspense-boundaries.md](./suspense-boundaries.md) for:\n\n- CSR bailout with `useSearchParams` and `usePathname`\n- Which hooks require Suspense boundaries\n\n## Parallel & Intercepting Routes\n\nSee [parallel-routes.md](./parallel-routes.md) for:\n\n- Modal patterns with `@slot` and `(.)` interceptors\n- `default.tsx` for fallbacks\n- Closing modals correctly with `router.back()`\n\n## Self-Hosting\n\nSee [self-hosting.md](./self-hosting.md) for:\n\n- `output: 'standalone'` for Docker\n- Cache handlers for multi-instance ISR\n- What works vs needs extra setup\n\n## Debug Tricks\n\nSee [debug-tricks.md](./debug-tricks.md) for:\n\n- MCP endpoint for AI-assisted debugging\n- Rebuild specific routes with `--debug-build-paths`\n","frontmatter":{"name":"next-best-practices","description":"Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling","user-invocable":false},"isInternal":false,"tokens":1011,"sizeBytes":4022},{"name":"suspense-boundaries.md","path":".agents/skills/next-best-practices/suspense-boundaries.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/next-best-practices/suspense-boundaries.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"# Suspense Boundaries\n\nClient hooks that cause CSR bailout without Suspense boundaries.\n\n## useSearchParams\n\nAlways requires Suspense boundary in static routes. Without it, the entire page becomes client-side rendered.\n\n```tsx\n// Bad: Entire page becomes CSR\n'use client';\n\nimport { useSearchParams } from 'next/navigation';\n\nexport default function SearchBar() {\n  const searchParams = useSearchParams();\n  return <div>Query: {searchParams.get('q')}</div>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\nimport { Suspense } from 'react';\nimport SearchBar from './search-bar';\n\nexport default function Page() {\n  return (\n    <Suspense fallback={<div>Loading...</div>}>\n      <SearchBar />\n    </Suspense>\n  );\n}\n```\n\n## usePathname\n\nRequires Suspense boundary when route has dynamic parameters.\n\n```tsx\n// In dynamic route [slug]\n// Bad: No Suspense\n'use client';\nimport { usePathname } from 'next/navigation';\n\nexport function Breadcrumb() {\n  const pathname = usePathname();\n  return <nav>{pathname}</nav>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\n<Suspense fallback={<BreadcrumbSkeleton />}>\n  <Breadcrumb />\n</Suspense>\n```\n\nIf you use `generateStaticParams`, Suspense is optional.\n\n## Quick Reference\n\n| Hook                | Suspense Required    |\n| ------------------- | -------------------- |\n| `useSearchParams()` | Yes                  |\n| `usePathname()`     | Yes (dynamic routes) |\n| `useParams()`       | No                   |\n| `useRouter()`       | No                   |\n","isInternal":false,"tokens":340,"sizeBytes":1482},{"name":"cli.md","path":".agents/skills/shadcn/cli.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/cli.md","title":"Shadcn Skill","category":"anthropic-skill","format":"markdown","content":"# shadcn CLI Reference\n\nConfiguration is read from `components.json`.\n\n> **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.\n\n> **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.\n\n## Contents\n\n- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build\n- Templates: next, vite, start, react-router, astro\n- Presets: named, code, URL formats and fields\n- Switching presets\n\n---\n\n## Commands\n\n### `init` — Initialize or create a project\n\n```bash\nnpx shadcn@latest init [components...] [options]\n```\n\nInitializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.\n\n| Flag                    | Short | Description                                               | Default |\n| ----------------------- | ----- | --------------------------------------------------------- | ------- |\n| `--template <template>` | `-t`  | Template (next, start, vite, next-monorepo, react-router) | —       |\n| `--preset [name]`       | `-p`  | Preset configuration (named, code, or URL)                | —       |\n| `--yes`                 | `-y`  | Skip confirmation prompt                                  | `true`  |\n| `--defaults`            | `-d`  | Use defaults (`--template=next --preset=base-nova`)       | `false` |\n| `--force`               | `-f`  | Force overwrite existing configuration                    | `false` |\n| `--cwd <cwd>`           | `-c`  | Working directory                                         | current |\n| `--name <name>`         | `-n`  | Name for new project                                      | —       |\n| `--silent`              | `-s`  | Mute output                                               | `false` |\n| `--rtl`                 |       | Enable RTL support                                        | —       |\n| `--reinstall`           |       | Re-install existing UI components                         | `false` |\n| `--monorepo`            |       | Scaffold a monorepo project                               | —       |\n| `--no-monorepo`         |       | Skip the monorepo prompt                                  | —       |\n\n`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.\n\n### `apply` — Apply a preset to an existing project\n\n```bash\nnpx shadcn@latest apply [preset] [options]\n```\n\nApplies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components.\n\n| Flag                | Short | Description                                | Default |\n| ------------------- | ----- | ------------------------------------------ | ------- |\n| `--preset <preset>` | —     | Preset configuration (named, code, or URL) | —       |\n| `--yes`             | `-y`  | Skip confirmation prompt                   | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                          | current |\n| `--silent`          | `-s`  | Mute output                                | `false` |\n\n`[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match.\nIf no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`.\n\n### `add` — Add components\n\n> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.\n\n```bash\nnpx shadcn@latest add [components...] [options]\n```\n\nAccepts component names, registry-prefixed names (`@magicui/shimmer-button`),\nGitHub item addresses (`owner/repo/item`), URLs, or local paths.\n\n| Flag            | Short | Description                                                                                                          | Default |\n| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |\n| `--yes`         | `-y`  | Skip confirmation prompt                                                                                             | `false` |\n| `--overwrite`   | `-o`  | Overwrite existing files                                                                                             | `false` |\n| `--cwd <cwd>`   | `-c`  | Working directory                                                                                                    | current |\n| `--all`         | `-a`  | Add all available components                                                                                         | `false` |\n| `--path <path>` | `-p`  | Target path for the component                                                                                        | —       |\n| `--silent`      | `-s`  | Mute output                                                                                                          | `false` |\n| `--dry-run`     |       | Preview all changes without writing files                                                                            | `false` |\n| `--diff [path]` |       | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`)         | —       |\n| `--view [path]` |       | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | —       |\n\n#### Dry-Run Mode\n\nUse `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.\n\n```bash\n# Preview all changes.\nnpx shadcn@latest add button --dry-run\n\n# Show diffs for all files (top 5).\nnpx shadcn@latest add button --diff\n\n# Show the diff for a specific file.\nnpx shadcn@latest add button --diff button.tsx\n\n# Show contents for all files (top 5).\nnpx shadcn@latest add button --view\n\n# Show the full content of a specific file.\nnpx shadcn@latest add button --view button.tsx\n\n# Works with URLs too.\nnpx shadcn@latest add https://api.npoint.io/abc123 --dry-run\n\n# Works with public GitHub registries too.\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# CSS diffs.\nnpx shadcn@latest add button --diff globals.css\n```\n\n**When to use dry-run:**\n\n- When the user asks \"what files will this add?\" or \"what will this change?\" — use `--dry-run`.\n- Before overwriting existing components — use `--diff` to preview the changes first.\n- When the user wants to inspect component source code without installing — use `--view`.\n- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.\n- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.\n\n> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.\n\n#### Smart Merge from Upstream\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow.\n\n### `search` — Search registries\n\n```bash\nnpx shadcn@latest search [registries...] [options]\n```\n\nFuzzy search across registries. Also aliased as `npx shadcn@latest list`.\nSupports namespaces (`@acme`), public GitHub registry sources (`owner/repo`),\nand registry catalog URLs. Without `-q`, lists all items. When no registries are\npassed, searches every registry configured in `components.json`.\n\n| Flag                | Short | Description                                       | Default |\n| ------------------- | ----- | ------------------------------------------------- | ------- |\n| `--query <query>`   | `-q`  | Search query                                      | —       |\n| `--type <type>`     | `-t`  | Filter by item type (e.g. `ui`, `block`, `hook`); comma-separated | —       |\n| `--limit <number>`  | `-l`  | Max items to display                              | `100`   |\n| `--offset <number>` | `-o`  | Items to skip                                     | `0`     |\n| `--json`            |       | Output as JSON                                    | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                                 | current |\n\n### `view` — View item details\n\n```bash\nnpx shadcn@latest view <items...> [options]\n```\n\nDisplays item info including file contents. Examples:\n`npx shadcn@latest view @shadcn/button`,\n`npx shadcn@latest view owner/repo/item`.\n\n### `docs` — Get component documentation URLs\n\n```bash\nnpx shadcn@latest docs <components...> [options]\n```\n\nOutputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.\n\nExample output for `npx shadcn@latest docs input button`:\n\n```\nbase  radix\n\ninput\n  docs      https://ui.shadcn.com/docs/components/radix/input\n  examples  https://raw.githubusercontent.com/.../examples/input-example.tsx\n\nbutton\n  docs      https://ui.shadcn.com/docs/components/radix/button\n  examples  https://raw.githubusercontent.com/.../examples/button-example.tsx\n```\n\nSome components include an `api` link to the underlying library (e.g. `cmdk` for the command component).\n\n### `diff` — Check for updates\n\nDo not use this command. Use `npx shadcn@latest add --diff` instead.\n\n### `info` — Project information\n\n```bash\nnpx shadcn@latest info [options]\n```\n\nDisplays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.\n\n| Flag          | Short | Description       | Default |\n| ------------- | ----- | ----------------- | ------- |\n| `--cwd <cwd>` | `-c`  | Working directory | current |\n\n**Project Info fields:**\n\n| Field                | Type      | Meaning                                                            |\n| -------------------- | --------- | ------------------------------------------------------------------ |\n| `framework`          | `string`  | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |\n| `frameworkVersion`   | `string`  | Framework version (e.g. `15.2.4`)                                  |\n| `isSrcDir`           | `boolean` | Whether the project uses a `src/` directory                        |\n| `isRSC`              | `boolean` | Whether React Server Components are enabled                        |\n| `isTsx`              | `boolean` | Whether the project uses TypeScript                                |\n| `tailwindVersion`    | `string`  | `\"v3\"` or `\"v4\"`                                                   |\n| `tailwindConfigFile` | `string`  | Path to the Tailwind config file                                   |\n| `tailwindCssFile`    | `string`  | Path to the global CSS file                                        |\n| `aliasPrefix`        | `string`  | Import alias prefix (e.g. `@`, `~`, `@/`)                          |\n| `packageManager`     | `string`  | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`)            |\n\n**Components.json fields:**\n\n| Field                | Type      | Meaning                                                                                    |\n| -------------------- | --------- | ------------------------------------------------------------------------------------------ |\n| `base`               | `string`  | Primitive library (`radix` or `base`) — determines component APIs and available props      |\n| `style`              | `string`  | Visual style (e.g. `nova`, `vega`)                                                         |\n| `rsc`                | `boolean` | RSC flag from config                                                                       |\n| `tsx`                | `boolean` | TypeScript flag                                                                            |\n| `tailwind.config`    | `string`  | Tailwind config path                                                                       |\n| `tailwind.css`       | `string`  | Global CSS path — this is where custom CSS variables go                                    |\n| `iconLibrary`        | `string`  | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |\n| `aliases.components` | `string`  | Component import alias (e.g. `@/components`)                                               |\n| `aliases.utils`      | `string`  | Utils import alias (e.g. `@/lib/utils`)                                                    |\n| `aliases.ui`         | `string`  | UI component alias (e.g. `@/components/ui`)                                                |\n| `aliases.lib`        | `string`  | Lib alias (e.g. `@/lib`)                                                                   |\n| `aliases.hooks`      | `string`  | Hooks alias (e.g. `@/hooks`)                                                               |\n| `resolvedPaths`      | `object`  | Absolute file-system paths for each alias                                                  |\n| `registries`         | `object`  | Configured custom registries                                                               |\n\n**Links fields:**\n\nThe `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.\n\n### `build` — Build a custom registry\n\n```bash\nnpx shadcn@latest build [registry] [options]\n```\n\nBuilds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.\n\nFor authoring rules, `include`, item definitions, `registryDependencies`, and\nGitHub registry behavior, see [registry.md](./registry.md).\n\n| Flag              | Short | Description       | Default      |\n| ----------------- | ----- | ----------------- | ------------ |\n| `--output <path>` | `-o`  | Output directory  | `./public/r` |\n| `--cwd <cwd>`     | `-c`  | Working directory | current      |\n\n---\n\n## Templates\n\n| Value          | Framework      | Monorepo support |\n| -------------- | -------------- | ---------------- |\n| `next`         | Next.js        | Yes              |\n| `vite`         | Vite           | Yes              |\n| `start`        | TanStack Start | Yes              |\n| `react-router` | React Router   | Yes              |\n| `astro`        | Astro          | Yes              |\n| `laravel`      | Laravel        | No               |\n\nAll templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.\n\n---\n\n## Presets\n\nThree ways to specify a preset via `--preset`:\n\n1. **Named:** `--preset nova` or `--preset lyra`\n2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`)\n3. **URL:** `--preset \"https://ui.shadcn.com/init?base=radix&style=nova&...\"`\n\n> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.\n> Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset.\n\n## Switching Presets\n\nAsk the user first: **overwrite**, **merge**, or **skip** existing components?\n\n- **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components.\n- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.\n- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.\n\nAlways 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 <current-base>` explicitly — preset codes do not encode the base.\n","isInternal":false,"tokens":3862,"sizeBytes":17304},{"name":"customization.md","path":".agents/skills/shadcn/customization.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/customization.md","title":"Shadcn Skill","category":"anthropic-skill","format":"markdown","content":"# Customization & Theming\n\nComponents reference semantic CSS variable tokens. Change the variables to change every component.\n\n## Contents\n\n- How it works (CSS variables → Tailwind utilities → components)\n- Color variables and OKLCH format\n- Dark mode setup\n- Changing the theme (presets, CSS variables)\n- Adding custom colors (Tailwind v3 and v4)\n- Border radius\n- Customizing components (variants, className, wrappers)\n- Checking for updates\n\n---\n\n## How It Works\n\n1. CSS variables defined in `:root` (light) and `.dark` (dark mode).\n2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.\n3. Components use these utilities — changing a variable changes all components that reference it.\n\n---\n\n## Color Variables\n\nEvery color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.\n\n| Variable                                     | Purpose                          |\n| -------------------------------------------- | -------------------------------- |\n| `--background` / `--foreground`              | Page background and default text |\n| `--card` / `--card-foreground`               | Card surfaces                    |\n| `--primary` / `--primary-foreground`         | Primary buttons and actions      |\n| `--secondary` / `--secondary-foreground`     | Secondary actions                |\n| `--muted` / `--muted-foreground`             | Muted/disabled states            |\n| `--accent` / `--accent-foreground`           | Hover and accent states          |\n| `--destructive` / `--destructive-foreground` | Error and destructive actions    |\n| `--border`                                   | Default border color             |\n| `--input`                                    | Form input borders               |\n| `--ring`                                     | Focus ring color                 |\n| `--chart-1` through `--chart-5`              | Chart/data visualization         |\n| `--sidebar-*`                                | Sidebar-specific colors          |\n| `--surface` / `--surface-foreground`         | Secondary surface                |\n\nColors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).\n\n---\n\n## Dark Mode\n\nClass-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:\n\n```tsx\nimport { ThemeProvider } from \"next-themes\"\n\n<ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n  {children}\n</ThemeProvider>\n```\n\n---\n\n## Changing the Theme\n\n```bash\n# Apply a preset code from ui.shadcn.com.\nnpx shadcn@latest apply --preset a2r6bw\n\n# Positional shorthand also works.\nnpx shadcn@latest apply a2r6bw\n\n# Switch to a named preset and overwrite existing components.\nnpx shadcn@latest apply --preset nova\n\n# Preserve existing components instead.\nnpx shadcn@latest init --preset nova --force --no-reinstall\n\n# Use a custom theme URL.\nnpx shadcn@latest apply --preset \"https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&...\"\n```\n\nOr edit CSS variables directly in `globals.css`.\n\n---\n\n## Adding Custom Colors\n\nAdd variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.\n\n```css\n/* 1. Define in the global CSS file. */\n:root {\n  --warning: oklch(0.84 0.16 84);\n  --warning-foreground: oklch(0.28 0.07 46);\n}\n.dark {\n  --warning: oklch(0.41 0.11 46);\n  --warning-foreground: oklch(0.99 0.02 95);\n}\n```\n\n```css\n/* 2a. Register with Tailwind v4 (@theme inline). */\n@theme inline {\n  --color-warning: var(--warning);\n  --color-warning-foreground: var(--warning-foreground);\n}\n```\n\nWhen `tailwindVersion` is `\"v3\"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:\n\n```js\n// 2b. Register with Tailwind v3 (tailwind.config.js).\nmodule.exports = {\n  theme: {\n    extend: {\n      colors: {\n        warning: \"oklch(var(--warning) / <alpha-value>)\",\n        \"warning-foreground\":\n          \"oklch(var(--warning-foreground) / <alpha-value>)\",\n      },\n    },\n  },\n}\n```\n\n```tsx\n// 3. Use in components.\n<div className=\"bg-warning text-warning-foreground\">Warning</div>\n```\n\n---\n\n## Border Radius\n\n`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).\n\n---\n\n## Customizing Components\n\nSee also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.\n\nPrefer these approaches in order:\n\n### 1. Built-in variants\n\n```tsx\n<Button variant=\"outline\" size=\"sm\">\n  Click\n</Button>\n```\n\n### 2. Tailwind classes via `className`\n\n```tsx\n<Card className=\"mx-auto max-w-md\">...</Card>\n```\n\n### 3. Add a new variant\n\nEdit the component source to add a variant via `cva`:\n\n```tsx\n// components/ui/button.tsx\nwarning: \"bg-warning text-warning-foreground hover:bg-warning/90\",\n```\n\n### 4. Wrapper components\n\nCompose shadcn/ui primitives into higher-level components:\n\n```tsx\nexport function ConfirmDialog({ title, description, onConfirm, children }) {\n  return (\n    <AlertDialog>\n      <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{description}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n```\n\n---\n\n## Checking for Updates\n\n```bash\nnpx shadcn@latest add button --diff\n```\n\nTo preview exactly what would change before updating, use `--dry-run` and `--diff`:\n\n```bash\nnpx shadcn@latest add button --dry-run        # see all affected files\nnpx shadcn@latest add button --diff button.tsx # see the diff for a specific file\n```\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.\n","isInternal":false,"tokens":1487,"sizeBytes":6074},{"name":"evals.json","path":".agents/skills/shadcn/evals/evals.json","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/evals/evals.json","title":"Evals Skill","category":"anthropic-skill","format":"json","content":"{\n  \"skill_name\": \"shadcn\",\n  \"evals\": [\n    {\n      \"id\": 1,\n      \"prompt\": \"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.\",\n      \"expected_output\": \"A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses FieldGroup and Field components for form layout instead of raw div with space-y\",\n        \"Uses Switch for independent on/off notification toggles (not looping Button with manual active state)\",\n        \"Uses data-invalid on Field and aria-invalid on the input control for validation states\",\n        \"Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing\",\n        \"Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500\",\n        \"No manual dark: color overrides\"\n      ]\n    },\n    {\n      \"id\": 2,\n      \"prompt\": \"Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.\",\n      \"expected_output\": \"A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Includes DialogTitle for accessibility (visible or with sr-only class)\",\n        \"Avatar component includes AvatarFallback\",\n        \"Icons on buttons use the data-icon attribute (data-icon=\\\"inline-start\\\" or data-icon=\\\"inline-end\\\")\",\n        \"No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)\",\n        \"Uses tabler icons (@tabler/icons-react) instead of lucide-react\",\n        \"Uses asChild for custom triggers (radix preset)\"\n      ]\n    },\n    {\n      \"id\": 3,\n      \"prompt\": \"Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.\",\n      \"expected_output\": \"A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)\",\n        \"Uses Skeleton component for loading placeholders instead of custom animate-pulse divs\",\n        \"Uses Badge component for percentage change instead of custom styled spans\",\n        \"Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600\",\n        \"Uses gap-* instead of space-y-* or space-x-* for spacing\",\n        \"Uses size-* when width and height are equal instead of separate w-* h-*\"\n      ]\n    },\n    {\n      \"id\": 4,\n      \"prompt\": \"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Build a chat conversation view: a scrollable thread of messages from two different people, each with an avatar, sender name, timestamp, and message bubble. A couple of messages include an image attachment and a PDF file attachment, and there's a 'Today' divider separating the days.\",\n      \"expected_output\": \"A React component composing MessageScroller, Message, Bubble, Attachment, and Marker from the registry instead of hand-rolled bubble/divider/attachment markup.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses MessageScroller (MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem) for the scrollable thread instead of a raw overflow-y-auto div or ScrollArea\",\n        \"Wraps each row in MessageScrollerItem inside MessageScrollerContent\",\n        \"Uses Message with MessageAvatar/MessageContent/MessageHeader for row layout instead of custom flex divs\",\n        \"Uses Bubble + BubbleContent for the message surface instead of a styled div with bg-muted/bg-primary\",\n        \"Uses Attachment (AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription) for the file and image attachments instead of Item or a custom card\",\n        \"Uses Marker (variant=\\\"separator\\\") for the 'Today' divider instead of Separator plus a centered label\",\n        \"Uses semantic color tokens and gap-* spacing; no raw colors like bg-emerald-500 and no space-y-*\",\n        \"Includes \\\"use client\\\" when the component uses state or event handlers (isRSC)\"\n      ]\n    },\n    {\n      \"id\": 5,\n      \"prompt\": \"Using shadcn/ui (base-nova preset, lucide icons), build a streaming AI chat UI. The assistant's reply streams in while it generates, the view auto-scrolls to follow the latest content but stops following if the user scrolls up to read earlier messages, a 'jump to latest' button appears when the user has scrolled away from the bottom, and a subtle 'thinking…' shimmer shows while the model is generating.\",\n      \"expected_output\": \"A React component that delegates scroll/anchor behavior to MessageScroller and uses MessageScrollerButton for jump-to-latest and the shimmer utility for the thinking indicator — no hand-rolled scroll logic or custom shimmer keyframes.\",\n      \"files\": [],\n      \"expectations\": [\n        \"Uses MessageScroller with MessageScrollerProvider (autoScroll) and scrollAnchor on message items for the stick-to-bottom/follow behavior instead of a custom useStickToBottom hook or ResizeObserver/scrollTop wiring\",\n        \"Uses MessageScrollerButton for the jump-to-latest control instead of a hand-built conditional button driven by manual scroll-position state\",\n        \"Uses the shimmer utility class for the 'thinking…' indicator instead of a custom @keyframes or bg-clip-text gradient animation\",\n        \"Wraps each message row in MessageScrollerItem inside MessageScrollerContent\",\n        \"Uses Message + Bubble + BubbleContent for the conversation rows instead of hand-rolled bubble divs\",\n        \"Uses semantic color tokens and gap-* spacing; includes \\\"use client\\\" (isRSC)\"\n      ]\n    }\n  ]\n}\n","frontmatter":{"skill_name":"shadcn","evals":[{"id":1,"prompt":"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Create a settings form component with fields for: full name, email address, and notification preferences (email, SMS, push notifications as toggle options). Add validation states for required fields.","expected_output":"A React component using FieldGroup, Field, ToggleGroup, data-invalid/aria-invalid validation, gap-* spacing, and semantic colors.","files":[],"expectations":["Uses FieldGroup and Field components for form layout instead of raw div with space-y","Uses Switch for independent on/off notification toggles (not looping Button with manual active state)","Uses data-invalid on Field and aria-invalid on the input control for validation states","Uses gap-* (e.g. gap-4, gap-6) instead of space-y-* or space-x-* for spacing","Uses semantic color tokens (e.g. bg-background, text-muted-foreground, text-destructive) instead of raw colors like bg-red-500","No manual dark: color overrides"]},{"id":2,"prompt":"Create a dialog component for editing a user profile. It should have the user's avatar at the top, input fields for name and bio, and Save/Cancel buttons with appropriate icons. Using shadcn/ui with radix-nova preset and tabler icons.","expected_output":"A React component with DialogTitle, Avatar+AvatarFallback, data-icon on icon buttons, no icon sizing classes, tabler icon imports.","files":[],"expectations":["Includes DialogTitle for accessibility (visible or with sr-only class)","Avatar component includes AvatarFallback","Icons on buttons use the data-icon attribute (data-icon=\"inline-start\" or data-icon=\"inline-end\")","No sizing classes on icons inside components (no size-4, w-4, h-4, etc.)","Uses tabler icons (@tabler/icons-react) instead of lucide-react","Uses asChild for custom triggers (radix preset)"]},{"id":3,"prompt":"Create a dashboard component that shows 4 stat cards in a grid. Each card has a title, large number, percentage change badge, and a loading skeleton state. Using shadcn/ui with base-nova preset and lucide icons.","expected_output":"A React component with full Card composition, Skeleton for loading, Badge for changes, semantic colors, gap-* spacing.","files":[],"expectations":["Uses full Card composition with CardHeader, CardTitle, CardContent (not dumping everything into CardContent)","Uses Skeleton component for loading placeholders instead of custom animate-pulse divs","Uses Badge component for percentage change instead of custom styled spans","Uses semantic color tokens instead of raw color values like bg-green-500 or text-red-600","Uses gap-* instead of space-y-* or space-x-* for spacing","Uses size-* when width and height are equal instead of separate w-* h-*"]},{"id":4,"prompt":"I'm building a Next.js app with shadcn/ui (base-nova preset, lucide icons). Build a chat conversation view: a scrollable thread of messages from two different people, each with an avatar, sender name, timestamp, and message bubble. A couple of messages include an image attachment and a PDF file attachment, and there's a 'Today' divider separating the days.","expected_output":"A React component composing MessageScroller, Message, Bubble, Attachment, and Marker from the registry instead of hand-rolled bubble/divider/attachment markup.","files":[],"expectations":["Uses MessageScroller (MessageScrollerProvider, MessageScrollerViewport, MessageScrollerContent, MessageScrollerItem) for the scrollable thread instead of a raw overflow-y-auto div or ScrollArea","Wraps each row in MessageScrollerItem inside MessageScrollerContent","Uses Message with MessageAvatar/MessageContent/MessageHeader for row layout instead of custom flex divs","Uses Bubble + BubbleContent for the message surface instead of a styled div with bg-muted/bg-primary","Uses Attachment (AttachmentMedia, AttachmentContent, AttachmentTitle, AttachmentDescription) for the file and image attachments instead of Item or a custom card","Uses Marker (variant=\"separator\") for the 'Today' divider instead of Separator plus a centered label","Uses semantic color tokens and gap-* spacing; no raw colors like bg-emerald-500 and no space-y-*","Includes \"use client\" when the component uses state or event handlers (isRSC)"]},{"id":5,"prompt":"Using shadcn/ui (base-nova preset, lucide icons), build a streaming AI chat UI. The assistant's reply streams in while it generates, the view auto-scrolls to follow the latest content but stops following if the user scrolls up to read earlier messages, a 'jump to latest' button appears when the user has scrolled away from the bottom, and a subtle 'thinking…' shimmer shows while the model is generating.","expected_output":"A React component that delegates scroll/anchor behavior to MessageScroller and uses MessageScrollerButton for jump-to-latest and the shimmer utility for the thinking indicator — no hand-rolled scroll logic or custom shimmer keyframes.","files":[],"expectations":["Uses MessageScroller with MessageScrollerProvider (autoScroll) and scrollAnchor on message items for the stick-to-bottom/follow behavior instead of a custom useStickToBottom hook or ResizeObserver/scrollTop wiring","Uses MessageScrollerButton for the jump-to-latest control instead of a hand-built conditional button driven by manual scroll-position state","Uses the shimmer utility class for the 'thinking…' indicator instead of a custom @keyframes or bg-clip-text gradient animation","Wraps each message row in MessageScrollerItem inside MessageScrollerContent","Uses Message + Bubble + BubbleContent for the conversation rows instead of hand-rolled bubble divs","Uses semantic color tokens and gap-* spacing; includes \"use client\" (isRSC)"]}]},"isInternal":false,"tokens":1387,"sizeBytes":6330},{"name":"mcp.md","path":".agents/skills/shadcn/mcp.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/mcp.md","title":"Shadcn Skill","category":"anthropic-skill","format":"markdown","content":"# shadcn MCP Server\n\nThe CLI includes an MCP server that lets AI assistants search, browse, view, and install items from registries.\n\n---\n\n## Setup\n\n```bash\nshadcn mcp        # start the MCP server (stdio)\nshadcn mcp init   # write config for your editor\n```\n\nEditor config files:\n\n| Editor      | Config file                     |\n| ----------- | ------------------------------- |\n| Claude Code | `.mcp.json`                     |\n| Cursor      | `.cursor/mcp.json`              |\n| VS Code     | `.vscode/mcp.json`              |\n| OpenCode    | `opencode.json`                 |\n| Codex       | `~/.codex/config.toml` (manual) |\n\n---\n\n## Tools\n\n> **Tip:** MCP tools handle registry operations (search, view, install). For project configuration (aliases, framework, Tailwind version), use `npx shadcn@latest info` — there is no MCP equivalent.\n\n### `shadcn:get_project_registries`\n\nReturns registry names from `components.json`. Errors if no `components.json` exists.\n\n**Input:** none\n\n### `shadcn:list_items_in_registries`\n\nLists all items from one or more registries. Registries can be configured\nnamespaces such as `@acme`, public GitHub sources such as `owner/repo`, or\nregistry catalog URLs. Omit `registries` to list from every registry configured\nin `components.json`.\n\n**Input:** `registries` (string[], optional — omit for all configured), `types` (string[], optional — e.g. `[\"ui\", \"block\"]`), `limit` (number, optional, defaults to 100), `offset` (number, optional)\n\n### `shadcn:search_items_in_registries`\n\nFuzzy search across registries. Registries can be configured namespaces, public\nGitHub sources, or registry catalog URLs. Omit `registries` to search every\nregistry configured in `components.json` — e.g. \"find me a hero\" across all\nconfigured registries.\n\n**Input:** `registries` (string[], optional — omit for all configured), `query` (string), `types` (string[], optional — e.g. `[\"ui\", \"block\"]`), `limit` (number, optional, defaults to 100), `offset` (number, optional)\n\n### `shadcn:view_items_in_registries`\n\nView item details including full file contents.\n\n**Input:** `items` (string[]) — e.g.\n`[\"@shadcn/button\", \"@shadcn/card\", \"owner/repo/item\"]`\n\n### `shadcn:get_item_examples_from_registries`\n\nFind usage examples and demos with source code. Omit `registries` to search\nevery registry configured in `components.json`.\n\n**Input:** `registries` (string[], optional — omit for all configured), `query` (string) — e.g. `\"accordion-demo\"`, `\"button example\"`\n\n### `shadcn:get_add_command_for_items`\n\nReturns the CLI install command.\n\n**Input:** `items` (string[]) — e.g. `[\"@shadcn/button\"]`\n\n### `shadcn:get_audit_checklist`\n\nReturns a checklist for verifying components (imports, deps, lint, TypeScript).\n\n**Input:** none\n\n---\n\n## Configuring Registries\n\nNamespaced and authenticated registries are set in `components.json`. The\n`@shadcn` registry is always built-in. Public GitHub registries can also be used\ndirectly as `owner/repo` registry sources when the repository has a root\n`registry.json`; they do not need `components.json` configuration.\n\n```json\n{\n  \"registries\": {\n    \"@acme\": \"https://acme.com/r/{name}.json\",\n    \"@private\": {\n      \"url\": \"https://private.com/r/{name}.json\",\n      \"headers\": { \"Authorization\": \"Bearer ${MY_TOKEN}\" }\n    }\n  }\n}\n```\n\n- Names must start with `@`.\n- URLs must contain `{name}`.\n- `${VAR}` references are resolved from environment variables.\n\nCommunity registry index: `https://ui.shadcn.com/r/registries.json`\n","isInternal":false,"tokens":886,"sizeBytes":3510},{"name":"registry.md","path":".agents/skills/shadcn/registry.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/registry.md","title":"Shadcn Skill","category":"anthropic-skill","format":"markdown","content":"# Registry Authoring and Addresses\n\nUse this reference when the user wants to create, fix, publish, or reason about\na shadcn registry.\n\n## Mental Model\n\nA registry has two forms:\n\n- **Source registry**: an authored `registry.json` in a project or repository.\n  It may use `include` and file paths that point at source files.\n- **Built registry**: generated JSON files served to CLI consumers, usually\n  from `public/r`. Use `npx shadcn@latest build` to create this form.\n\nThe CLI installer consumes registry item payloads. A source registry is a way to\nauthor those payloads from real files.\n\nRegistry items are not limited to React components. They can distribute\ncomponents, hooks, utilities, design tokens, pages, config files, docs, rules,\nworkflows, templates, MCP files, and other project files.\n\n## Root `registry.json`\n\nThe root registry file should define registry metadata and either `items` or\n`include`.\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"acme\",\n  \"homepage\": \"https://acme.com\",\n  \"items\": [\n    {\n      \"name\": \"absolute-url\",\n      \"type\": \"registry:lib\",\n      \"title\": \"Absolute URL\",\n      \"description\": \"A utility to turn any path into an absolute URL.\",\n      \"files\": [\n        {\n          \"path\": \"lib/absolute-url.ts\",\n          \"type\": \"registry:lib\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nRoot registry rules:\n\n- Root `registry.json` must include `name` and `homepage`.\n- `items` is an array of registry item definitions.\n- `include` may be used to split the source registry into multiple files.\n- Included registry files may omit `name` and `homepage`.\n\n## Include\n\nUse `include` to keep large registries modular.\n\n```json\n{\n  \"$schema\": \"https://ui.shadcn.com/schema/registry.json\",\n  \"name\": \"acme\",\n  \"homepage\": \"https://acme.com\",\n  \"include\": [\"registry/ui/registry.json\", \"registry/blocks/registry.json\"]\n}\n```\n\nInclude rules:\n\n- Include paths are relative to the `registry.json` that declares them.\n- Include paths must explicitly point to a `registry.json` file.\n- Do not use remote URLs, absolute paths, or parent traversal (`..`).\n- Item file paths are relative to the registry file that declares the item.\n- Duplicate item names fail across the resolved registry.\n\nExample included file:\n\n```json\n{\n  \"items\": [\n    {\n      \"name\": \"button\",\n      \"type\": \"registry:ui\",\n      \"files\": [\n        {\n          \"path\": \"button.tsx\",\n          \"type\": \"registry:ui\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nIf this file is at `registry/ui/registry.json`, then `button.tsx` is read from\n`registry/ui/button.tsx`, and the built item path is emitted relative to the\nroot registry.\n\n## Item Definitions\n\nCommon item fields:\n\n```json\n{\n  \"name\": \"login-form\",\n  \"type\": \"registry:block\",\n  \"title\": \"Login Form\",\n  \"description\": \"A login form with email and password fields.\",\n  \"dependencies\": [\"zod\"],\n  \"registryDependencies\": [\"button\", \"input\", \"label\"],\n  \"files\": [\n    {\n      \"path\": \"blocks/login-form.tsx\",\n      \"type\": \"registry:block\"\n    }\n  ],\n  \"cssVars\": {\n    \"light\": {\n      \"brand\": \"oklch(0.62 0.18 250)\"\n    },\n    \"dark\": {\n      \"brand\": \"oklch(0.72 0.16 250)\"\n    }\n  }\n}\n```\n\nImportant fields:\n\n- `name`: the installable item name. It is not necessarily a file path.\n- `type`: one of the registry item types, such as `registry:ui`,\n  `registry:block`, `registry:lib`, `registry:hook`, `registry:file`,\n  `registry:page`, `registry:theme`, `registry:style`, `registry:font`, or\n  `registry:item`.\n- `files`: source files copied or generated by the item.\n- `dependencies`: npm runtime dependencies.\n- `devDependencies`: npm development dependencies.\n- `registryDependencies`: other registry items required by this item.\n- `cssVars`, `css`, `tailwind`, `envVars`, and `docs`: optional install-time\n  additions.\n\nFile rules:\n\n- File paths are relative to the declaring `registry.json`.\n- `registry:file` and `registry:page` files require a `target`.\n- Do not use remote file URLs in source registry file paths.\n- Keep source files copy-pasteable: no hidden app-only imports.\n\n## Registry Dependencies\n\n`registryDependencies` entries are item addresses, not file paths.\n\n```json\n{\n  \"name\": \"login-form\",\n  \"type\": \"registry:block\",\n  \"registryDependencies\": [\"button\", \"@acme/input\", \"acme/ui/card#v1.2.0\"],\n  \"files\": [\n    {\n      \"path\": \"blocks/login-form.tsx\",\n      \"type\": \"registry:block\"\n    }\n  ]\n}\n```\n\nDependency rules:\n\n- Bare names such as `\"button\"` mean official shadcn items.\n- Bare names never mean same-registry or same-repository items.\n- Namespaced dependencies use `@namespace/item-name`.\n- GitHub dependencies use `owner/repo/item-name`.\n- Pin GitHub dependencies with `owner/repo/item-name#ref` when needed.\n- Refs are not inherited. If `owner/repo/foo#v2` depends on `bar` from the same\n  repo at `v2`, write `owner/repo/bar#v2`.\n- Do not use relative dependencies such as `\"./bar\"`.\n\n## Address Schemes\n\nWhen reasoning about a registry item string, classify it first.\n\n| Address                             | Scheme    | Meaning                                                      |\n| ----------------------------------- | --------- | ------------------------------------------------------------ |\n| `button`                            | shadcn    | Official shadcn item named `button`.                         |\n| `@acme/button`                      | namespace | Item `button` from configured registry `@acme`.              |\n| `@acme/ui/button`                   | namespace | Item `ui/button` from configured registry `@acme`.           |\n| `https://example.com/r/button.json` | url       | Built registry item JSON at that URL.                        |\n| `./button.json`                     | file      | Built registry item JSON on disk.                            |\n| `acme/ui/button`                    | github    | Item `button` from GitHub repo `acme/ui`.                    |\n| `acme/ui/forms/login#main`          | github    | Item `forms/login` from GitHub repo `acme/ui` at ref `main`. |\n\nFor namespace and GitHub addresses, slashful item names are allowed and are item\nnames, not file paths. Addresses ending in `.json` keep file-address\nprecedence, so `acme/ui/data/schema.json` is treated as a file path, not a\nGitHub item address.\n\n## GitHub Registries\n\nA public GitHub repository can act as a source registry when it has a root\n`registry.json`.\n\n```txt\nowner/repo/item-name[#ref]\n```\n\nRules:\n\n- The first two path segments are GitHub owner and repo.\n- All remaining path segments are the registry item name.\n- The source entrypoint is always root `registry.json`.\n- GitHub registries are source registries consumed directly by the CLI. They do\n  not require `shadcn build` or generated item JSON files.\n- `include` follows the same source-registry rules as local registries.\n- Currently, GitHub addresses support public `github.com` repositories only.\n- Private repos and GitHub Enterprise require explicit product decisions.\n\nWhen implementing GitHub registry fetching, resolve refs to a commit SHA before\nreading source files. Do not read moving refs directly from\n`raw.githubusercontent.com`, because branch-like refs can be cached for several\nminutes.\n\nPreferred flow:\n\n```txt\nowner/repo[#ref]\n  -> resolve ref with git ls-remote\n  -> commit SHA\n  -> read https://raw.githubusercontent.com/{owner}/{repo}/{sha}/registry.json\n  -> read includes and item files from the same SHA\n```\n\nThis keeps a command on one consistent repository snapshot.\n\nFull 40-character commit SHAs are already stable and can be used directly.\nBranches, tags, and short refs require Git so the CLI can resolve them to a\ncommit SHA first.\n\n## Build and Verify\n\nUse the CLI to build source registries:\n\n```bash\nnpx shadcn@latest build\nnpx shadcn@latest build registry.json --output public/r\n```\n\nUse CLI commands to inspect the result:\n\n```bash\nnpx shadcn@latest list @acme\nnpx shadcn@latest search @acme -q \"login\"\nnpx shadcn@latest view @acme/login-form\nnpx shadcn@latest add @acme/login-form --dry-run\nnpx shadcn@latest registry validate ./registry.json\n```\n\nUse GitHub addresses directly for public GitHub registries:\n\n```bash\nnpx shadcn@latest list owner/repo\nnpx shadcn@latest search owner/repo -q \"login\"\nnpx shadcn@latest view owner/repo/item\nnpx shadcn@latest add owner/repo/item --dry-run\nnpx shadcn@latest registry validate owner/repo\n```\n\nWhen working on registry implementation in the shadcn/ui codebase:\n\n- Keep address parsing pure and testable.\n- Do not add side effects to validators.\n- Preserve existing behavior for official shadcn, namespace, URL, and file\n  schemes.\n- Add tests for address parsing, source loading, dependency resolution, list,\n  search, view, and add paths.\n- Prefer small source-reader abstractions over a plugin system until there are\n  multiple real providers.\n","isInternal":false,"tokens":2134,"sizeBytes":8817},{"name":"base-vs-radix.md","path":".agents/skills/shadcn/rules/base-vs-radix.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/rules/base-vs-radix.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Base vs Radix\n\nAPI differences between `base` and `radix`. Check the `base` field from `npx shadcn@latest info`.\n\n## Contents\n\n- Composition: asChild vs render\n- Button / trigger as non-button element\n- Select (items prop, placeholder, positioning, multiple, object values)\n- ToggleGroup (type vs multiple)\n- Slider (scalar vs array)\n- Accordion (type and defaultValue)\n\n---\n\n## Composition: asChild (radix) vs render (base)\n\nRadix uses `asChild` to replace the default element. Base uses `render`. Don't wrap triggers in extra elements.\n\n**Incorrect:**\n\n```tsx\n<DialogTrigger>\n  <div>\n    <Button>Open</Button>\n  </div>\n</DialogTrigger>\n```\n\n**Correct (radix):**\n\n```tsx\n<DialogTrigger asChild>\n  <Button>Open</Button>\n</DialogTrigger>\n```\n\n**Correct (base):**\n\n```tsx\n<DialogTrigger render={<Button />}>Open</DialogTrigger>\n```\n\nThis applies to all trigger and close components: `DialogTrigger`, `SheetTrigger`, `AlertDialogTrigger`, `DropdownMenuTrigger`, `PopoverTrigger`, `TooltipTrigger`, `CollapsibleTrigger`, `DialogClose`, `SheetClose`, `NavigationMenuLink`, `BreadcrumbLink`, `SidebarMenuButton`, `Badge`, `Item`.\n\n---\n\n## Button / trigger as non-button element (base only)\n\nWhen `render` changes an element to a non-button (`<a>`, `<span>`), add `nativeButton={false}`.\n\n**Incorrect (base):** missing `nativeButton={false}`.\n\n```tsx\n<Button render={<a href=\"/docs\" />}>Read the docs</Button>\n```\n\n**Correct (base):**\n\n```tsx\n<Button render={<a href=\"/docs\" />} nativeButton={false}>\n  Read the docs\n</Button>\n```\n\n**Correct (radix):**\n\n```tsx\n<Button asChild>\n  <a href=\"/docs\">Read the docs</a>\n</Button>\n```\n\nSame for triggers whose `render` is not a `Button`:\n\n```tsx\n// base.\n<PopoverTrigger render={<InputGroupAddon />} nativeButton={false}>\n  Pick date\n</PopoverTrigger>\n```\n\n---\n\n## Select\n\n**items prop (base only).** Base requires an `items` prop on the root. Radix uses inline JSX only.\n\n**Incorrect (base):**\n\n```tsx\n<Select>\n  <SelectTrigger><SelectValue placeholder=\"Select a fruit\" /></SelectTrigger>\n</Select>\n```\n\n**Correct (base):**\n\n```tsx\nconst items = [\n  { label: \"Select a fruit\", value: null },\n  { label: \"Apple\", value: \"apple\" },\n  { label: \"Banana\", value: \"banana\" },\n]\n\n<Select items={items}>\n  <SelectTrigger>\n    <SelectValue />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      {items.map((item) => (\n        <SelectItem key={item.value} value={item.value}>{item.label}</SelectItem>\n      ))}\n    </SelectGroup>\n  </SelectContent>\n</Select>\n```\n\n**Correct (radix):**\n\n```tsx\n<Select>\n  <SelectTrigger>\n    <SelectValue placeholder=\"Select a fruit\" />\n  </SelectTrigger>\n  <SelectContent>\n    <SelectGroup>\n      <SelectItem value=\"apple\">Apple</SelectItem>\n      <SelectItem value=\"banana\">Banana</SelectItem>\n    </SelectGroup>\n  </SelectContent>\n</Select>\n```\n\n**Placeholder.** Base uses a `{ value: null }` item in the items array. Radix uses `<SelectValue placeholder=\"...\">`.\n\n**Content positioning.** Base uses `alignItemWithTrigger`. Radix uses `position`.\n\n```tsx\n// base.\n<SelectContent alignItemWithTrigger={false} side=\"bottom\">\n\n// radix.\n<SelectContent position=\"popper\">\n```\n\n---\n\n## Select — multiple selection and object values (base only)\n\nBase supports `multiple`, render-function children on `SelectValue`, and object values with `itemToStringValue`. Radix is single-select with string values only.\n\n**Correct (base — multiple selection):**\n\n```tsx\n<Select items={items} multiple defaultValue={[]}>\n  <SelectTrigger>\n    <SelectValue>\n      {(value: string[]) => value.length === 0 ? \"Select fruits\" : `${value.length} selected`}\n    </SelectValue>\n  </SelectTrigger>\n  ...\n</Select>\n```\n\n**Correct (base — object values):**\n\n```tsx\n<Select defaultValue={plans[0]} itemToStringValue={(plan) => plan.name}>\n  <SelectTrigger>\n    <SelectValue>{(value) => value.name}</SelectValue>\n  </SelectTrigger>\n  ...\n</Select>\n```\n\n---\n\n## ToggleGroup\n\nBase uses a `multiple` boolean prop. Radix uses `type=\"single\"` or `type=\"multiple\"`.\n\n**Incorrect (base):**\n\n```tsx\n<ToggleGroup type=\"single\" defaultValue=\"daily\">\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Correct (base):**\n\n```tsx\n// Single (no prop needed), defaultValue is always an array.\n<ToggleGroup defaultValue={[\"daily\"]} spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n</ToggleGroup>\n\n// Multi-selection.\n<ToggleGroup multiple>\n  <ToggleGroupItem value=\"bold\">Bold</ToggleGroupItem>\n  <ToggleGroupItem value=\"italic\">Italic</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Correct (radix):**\n\n```tsx\n// Single, defaultValue is a string.\n<ToggleGroup type=\"single\" defaultValue=\"daily\" spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n</ToggleGroup>\n\n// Multi-selection.\n<ToggleGroup type=\"multiple\">\n  <ToggleGroupItem value=\"bold\">Bold</ToggleGroupItem>\n  <ToggleGroupItem value=\"italic\">Italic</ToggleGroupItem>\n</ToggleGroup>\n```\n\n**Controlled single value:**\n\n```tsx\n// base — wrap/unwrap arrays.\nconst [value, setValue] = React.useState(\"normal\")\n<ToggleGroup value={[value]} onValueChange={(v) => setValue(v[0])}>\n\n// radix — plain string.\nconst [value, setValue] = React.useState(\"normal\")\n<ToggleGroup type=\"single\" value={value} onValueChange={setValue}>\n```\n\n---\n\n## Slider\n\nBase accepts a plain number for a single thumb. Radix always requires an array.\n\n**Incorrect (base):**\n\n```tsx\n<Slider defaultValue={[50]} max={100} step={1} />\n```\n\n**Correct (base):**\n\n```tsx\n<Slider defaultValue={50} max={100} step={1} />\n```\n\n**Correct (radix):**\n\n```tsx\n<Slider defaultValue={[50]} max={100} step={1} />\n```\n\nBoth use arrays for range sliders. Controlled `onValueChange` in base may need a cast:\n\n```tsx\n// base.\nconst [value, setValue] = React.useState([0.3, 0.7])\n<Slider value={value} onValueChange={(v) => setValue(v as number[])} />\n\n// radix.\nconst [value, setValue] = React.useState([0.3, 0.7])\n<Slider value={value} onValueChange={setValue} />\n```\n\n---\n\n## Accordion\n\nRadix requires `type=\"single\"` or `type=\"multiple\"` and supports `collapsible`. `defaultValue` is a string. Base uses no `type` prop, uses `multiple` boolean, and `defaultValue` is always an array.\n\n**Incorrect (base):**\n\n```tsx\n<Accordion type=\"single\" collapsible defaultValue=\"item-1\">\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n```\n\n**Correct (base):**\n\n```tsx\n<Accordion defaultValue={[\"item-1\"]}>\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n\n// Multi-select.\n<Accordion multiple defaultValue={[\"item-1\", \"item-2\"]}>\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n  <AccordionItem value=\"item-2\">...</AccordionItem>\n</Accordion>\n```\n\n**Correct (radix):**\n\n```tsx\n<Accordion type=\"single\" collapsible defaultValue=\"item-1\">\n  <AccordionItem value=\"item-1\">...</AccordionItem>\n</Accordion>\n```\n","isInternal":false,"tokens":1824,"sizeBytes":6954},{"name":"chat.md","path":".agents/skills/shadcn/rules/chat.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/rules/chat.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Chat & Messaging\n\nComponents for conversation and chat UI. Compose these instead of hand-rolling\nbubbles, scroll containers, dividers, or attachment cards.\n\nInstall: `npx shadcn@latest add message-scroller message bubble attachment marker`\n\nThe same component names and props ship for both `base` and `radix`; only\ncomposition differs (`render` vs `asChild`). See [base-vs-radix.md](./base-vs-radix.md).\n\n## Contents\n\n- Scrollable threads use MessageScroller\n- Message rows use Message\n- Message surfaces use Bubble\n- Attachments use Attachment\n- System notes and dividers use Marker\n- Streaming, anchoring, and jump-to-latest are built in\n- Escape hatch: the scroller hooks\n\n---\n\n## Scrollable threads use MessageScroller\n\nA conversation that scrolls, follows new messages, restores position, or jumps\nto a message uses `MessageScroller`. Don't build a raw overflow container with\nmanual scroll wiring, and don't reach for `ScrollArea`.\n\nThe parts nest in a fixed order. Every direct child of the content is wrapped in\na `MessageScrollerItem` so the scroller can measure, anchor, preserve position,\ntrack visibility, and jump to it. `MessageScrollerButton` sits inside\n`MessageScroller`, after the viewport.\n\n**Incorrect:**\n\n```tsx\n// Hand-rolled scroll container with manual stick-to-bottom logic.\n<div ref={scrollRef} onScroll={handleScroll} className=\"flex-1 overflow-y-auto\">\n  <div className=\"flex flex-col gap-6 p-4\">\n    {messages.map((m) => (\n      <ChatMessage key={m.id} message={m} />\n    ))}\n  </div>\n</div>\n```\n\n**Correct:**\n\n```tsx\n<MessageScrollerProvider autoScroll>\n  <MessageScroller>\n    <MessageScrollerViewport>\n      <MessageScrollerContent>\n        {messages.map((message) => (\n          <MessageScrollerItem\n            key={message.id}\n            messageId={message.id}\n            scrollAnchor={message.role === \"user\"}\n          >\n            <Message align={message.role === \"user\" ? \"end\" : \"start\"}>\n              {/* ...message content... */}\n            </Message>\n          </MessageScrollerItem>\n        ))}\n      </MessageScrollerContent>\n    </MessageScrollerViewport>\n    <MessageScrollerButton />\n  </MessageScroller>\n</MessageScrollerProvider>\n```\n\n---\n\n## Message rows use Message\n\n`Message` lays out a single row: avatar, header, content, footer, with\nalignment. Group consecutive rows from one sender with `MessageGroup`. Don't\nrebuild the row from flex divs.\n\n`align=\"end\"` is the current user's side; `align=\"start\"` is everyone else.\n\n```tsx\n<Message align=\"start\">\n  <MessageAvatar>\n    <Avatar>\n      <AvatarImage src={sender.avatar} alt={sender.name} />\n      <AvatarFallback>{initials}</AvatarFallback>\n    </Avatar>\n  </MessageAvatar>\n  <MessageContent>\n    <MessageHeader>{sender.name}</MessageHeader>\n    <Bubble>\n      <BubbleContent>{text}</BubbleContent>\n    </Bubble>\n    <MessageFooter>{time}</MessageFooter>\n  </MessageContent>\n</Message>\n```\n\n---\n\n## Message surfaces use Bubble\n\nThe colored message surface is `Bubble` + `BubbleContent`, never a styled `div`\nwith `bg-muted` / `bg-primary` and hand-managed corners.\n\n- `variant`: `default`, `secondary`, `muted`, `tinted`, `outline`, `ghost`, `destructive`.\n- `align`: `start` or `end` (matches the `Message` side).\n\n`BubbleReactions` renders the reaction cluster. `side` (`top` | `bottom`) and\n`align` (`start` | `end`) position it against the bubble. Don't lay reactions out\nwith absolutely-positioned `Badge`s.\n\n**Incorrect:**\n\n```tsx\n<div className=\"w-fit rounded-2xl bg-primary px-3 py-2 text-primary-foreground\">\n  {text}\n</div>\n```\n\n**Correct:**\n\n```tsx\n<Bubble variant=\"default\" align=\"end\">\n  <BubbleContent>{text}</BubbleContent>\n  <BubbleReactions side=\"bottom\" align=\"end\">\n    <Badge variant=\"secondary\">👍 2</Badge>\n  </BubbleReactions>\n</Bubble>\n```\n\n---\n\n## Attachments use Attachment\n\nFile and image attachments use `Attachment`, not `Item` or a custom card. It\ncarries upload state, so wire `state` to the real status rather than rendering a\nseparate spinner.\n\n- `state`: `idle`, `uploading`, `processing`, `error`, `done`. `uploading` and\n  `processing` apply the `shimmer` animation to the title automatically.\n- `size`: `default`, `sm`, `xs`. `orientation`: `horizontal`, `vertical`.\n- Use `AttachmentGroup` to lay out several attachments in a scrolling row.\n\n```tsx\n<Attachment state=\"done\">\n  <AttachmentMedia variant=\"icon\">\n    <FileTextIcon />\n  </AttachmentMedia>\n  <AttachmentContent>\n    <AttachmentTitle>homepage-feedback.pdf</AttachmentTitle>\n    <AttachmentDescription>PDF · 2.4 MB</AttachmentDescription>\n  </AttachmentContent>\n  <AttachmentActions>\n    <AttachmentAction>\n      <DownloadIcon />\n    </AttachmentAction>\n  </AttachmentActions>\n</Attachment>\n```\n\nFor an image, use `<AttachmentMedia variant=\"image\">` with an `img` child.\n\n---\n\n## System notes and dividers use Marker\n\nStatus lines (\"Sarah joined the conversation\"), date dividers (\"Today\"), and\nlabeled separators are `Marker`, not a `Separator` plus a centered span.\n\n- `variant`: `default` (plain row), `separator` (centered label with rules on\n  each side), `border` (bottom-bordered row).\n- `MarkerIcon` holds a leading icon; `MarkerContent` holds the label.\n\n**Incorrect:**\n\n```tsx\n<div className=\"flex items-center gap-3 py-2\">\n  <Separator className=\"flex-1\" />\n  <span className=\"text-xs text-muted-foreground\">Today</span>\n  <Separator className=\"flex-1\" />\n</div>\n```\n\n**Correct:**\n\n```tsx\n<Marker variant=\"separator\">\n  <MarkerContent>Today</MarkerContent>\n</Marker>\n```\n\n---\n\n## Streaming, anchoring, and jump-to-latest are built in\n\n`MessageScroller` handles the behavior that chat UIs usually reinvent. Don't\nwrite a `useStickToBottom` hook, a `ResizeObserver`, or manual `scrollTop` math.\n\n- **Follow the live edge while streaming.** `MessageScrollerProvider` with\n  `autoScroll` keeps the view pinned to new content and yields the moment the\n  user scrolls up. Streaming token updates that grow the last message are\n  followed automatically.\n- **Anchor a turn.** `scrollAnchor` on a `MessageScrollerItem` marks the row to\n  hold in view (typically the user's message that started the turn).\n- **Jump to latest.** `MessageScrollerButton` appears when the user scrolls away\n  and scrolls back on click. `direction=\"end\"` (default) or `direction=\"start\"`.\n  It is a self-managing control, so don't gate it behind your own scroll-position\n  state.\n\nFor a \"thinking…\" indicator while the model generates, apply the `shimmer`\nutility to text. Don't author a custom keyframe animation. See\n[styling.md](./styling.md).\n\n---\n\n## Escape hatch: the scroller hooks\n\nFor behavior the parts don't expose, read state from the hooks rather than\nre-implementing the scroller: `useMessageScroller`,\n`useMessageScrollerVisibility`, and `useMessageScrollerScrollable`. They come\nfrom the auto-installed `@shadcn/react` dependency, so there's nothing extra to\ninstall. Reach for them only when composition can't express what you need.\n","isInternal":false,"tokens":1723,"sizeBytes":6948},{"name":"composition.md","path":".agents/skills/shadcn/rules/composition.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/rules/composition.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Component Composition\n\n## Contents\n\n- Items always inside their Group component\n- Callouts use Alert\n- Empty states use Empty component\n- Toast notifications use sonner\n- Choosing between overlay components\n- Dialog, Sheet, and Drawer always need a Title\n- Card structure\n- Button has no isPending or isLoading prop\n- TabsTrigger must be inside TabsList\n- Avatar always needs AvatarFallback\n- Use Separator instead of raw hr or border divs\n- Use Skeleton for loading placeholders\n- Use Badge instead of custom styled spans\n\n---\n\n## Items always inside their Group component\n\nNever render items directly inside the content container.\n\n**Incorrect:**\n\n```tsx\n<SelectContent>\n  <SelectItem value=\"apple\">Apple</SelectItem>\n  <SelectItem value=\"banana\">Banana</SelectItem>\n</SelectContent>\n```\n\n**Correct:**\n\n```tsx\n<SelectContent>\n  <SelectGroup>\n    <SelectItem value=\"apple\">Apple</SelectItem>\n    <SelectItem value=\"banana\">Banana</SelectItem>\n  </SelectGroup>\n</SelectContent>\n```\n\nThis applies to all group-based components:\n\n| Item | Group |\n|------|-------|\n| `SelectItem`, `SelectLabel` | `SelectGroup` |\n| `DropdownMenuItem`, `DropdownMenuLabel`, `DropdownMenuSub` | `DropdownMenuGroup` |\n| `MenubarItem` | `MenubarGroup` |\n| `ContextMenuItem` | `ContextMenuGroup` |\n| `CommandItem` | `CommandGroup` |\n| `MessageScrollerItem` | `MessageScrollerContent` |\n| `Message` (consecutive, same sender) | `MessageGroup` |\n| `Bubble` (stacked) | `BubbleGroup` |\n| `Attachment` (in a row) | `AttachmentGroup` |\n\nChat components nest in a fixed order (`MessageScrollerProvider` → `MessageScroller` → `MessageScrollerViewport` → `MessageScrollerContent` → `MessageScrollerItem`). See [chat.md](./chat.md).\n\n---\n\n## Callouts use Alert\n\n```tsx\n<Alert>\n  <AlertTitle>Warning</AlertTitle>\n  <AlertDescription>Something needs attention.</AlertDescription>\n</Alert>\n```\n\n---\n\n## Empty states use Empty component\n\n```tsx\n<Empty>\n  <EmptyHeader>\n    <EmptyMedia variant=\"icon\"><FolderIcon /></EmptyMedia>\n    <EmptyTitle>No projects yet</EmptyTitle>\n    <EmptyDescription>Get started by creating a new project.</EmptyDescription>\n  </EmptyHeader>\n  <EmptyContent>\n    <Button>Create Project</Button>\n  </EmptyContent>\n</Empty>\n```\n\n---\n\n## Toast notifications use sonner\n\n```tsx\nimport { toast } from \"sonner\"\n\ntoast.success(\"Changes saved.\")\ntoast.error(\"Something went wrong.\")\ntoast(\"File deleted.\", {\n  action: { label: \"Undo\", onClick: () => undoDelete() },\n})\n```\n\n---\n\n## Choosing between overlay components\n\n| Use case | Component |\n|----------|-----------|\n| Focused task that requires input | `Dialog` |\n| Destructive action confirmation | `AlertDialog` |\n| Side panel with details or filters | `Sheet` |\n| Mobile-first bottom panel | `Drawer` |\n| Quick info on hover | `HoverCard` |\n| Small contextual content on click | `Popover` |\n\n---\n\n## Dialog, Sheet, and Drawer always need a Title\n\n`DialogTitle`, `SheetTitle`, `DrawerTitle` are required for accessibility. Use `className=\"sr-only\"` if visually hidden.\n\n```tsx\n<DialogContent>\n  <DialogHeader>\n    <DialogTitle>Edit Profile</DialogTitle>\n    <DialogDescription>Update your profile.</DialogDescription>\n  </DialogHeader>\n  ...\n</DialogContent>\n```\n\n---\n\n## Card structure\n\nUse full composition — don't dump everything into `CardContent`:\n\n```tsx\n<Card>\n  <CardHeader>\n    <CardTitle>Team Members</CardTitle>\n    <CardDescription>Manage your team.</CardDescription>\n  </CardHeader>\n  <CardContent>...</CardContent>\n  <CardFooter>\n    <Button>Invite</Button>\n  </CardFooter>\n</Card>\n```\n\n---\n\n## Button has no isPending or isLoading prop\n\nCompose with `Spinner` + `data-icon` + `disabled`:\n\n```tsx\n<Button disabled>\n  <Spinner data-icon=\"inline-start\" />\n  Saving...\n</Button>\n```\n\n---\n\n## TabsTrigger must be inside TabsList\n\nNever render `TabsTrigger` directly inside `Tabs` — always wrap in `TabsList`:\n\n```tsx\n<Tabs defaultValue=\"account\">\n  <TabsList>\n    <TabsTrigger value=\"account\">Account</TabsTrigger>\n    <TabsTrigger value=\"password\">Password</TabsTrigger>\n  </TabsList>\n  <TabsContent value=\"account\">...</TabsContent>\n</Tabs>\n```\n\n---\n\n## Avatar always needs AvatarFallback\n\nAlways include `AvatarFallback` for when the image fails to load:\n\n```tsx\n<Avatar>\n  <AvatarImage src=\"/avatar.png\" alt=\"User\" />\n  <AvatarFallback>JD</AvatarFallback>\n</Avatar>\n```\n\n---\n\n## Use existing components instead of custom markup\n\n| Instead of | Use |\n|---|---|\n| `<hr>` or `<div className=\"border-t\">` | `<Separator />` |\n| `<div className=\"animate-pulse\">` with styled divs | `<Skeleton className=\"h-4 w-3/4\" />` |\n| `<span className=\"rounded-full bg-green-100 ...\">` | `<Badge variant=\"secondary\">` |\n","isInternal":false,"tokens":1159,"sizeBytes":4667},{"name":"forms.md","path":".agents/skills/shadcn/rules/forms.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/rules/forms.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Forms & Inputs\n\n## Contents\n\n- Forms use FieldGroup + Field\n- InputGroup requires InputGroupInput/InputGroupTextarea\n- Buttons inside inputs use InputGroup + InputGroupAddon\n- Option sets (2–7 choices) use ToggleGroup\n- FieldSet + FieldLegend for grouping related fields\n- Field validation and disabled states\n\n---\n\n## Forms use FieldGroup + Field\n\nAlways use `FieldGroup` + `Field` — never raw `div` with `space-y-*`:\n\n```tsx\n<FieldGroup>\n  <Field>\n    <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n    <Input id=\"email\" type=\"email\" />\n  </Field>\n  <Field>\n    <FieldLabel htmlFor=\"password\">Password</FieldLabel>\n    <Input id=\"password\" type=\"password\" />\n  </Field>\n</FieldGroup>\n```\n\nUse `Field orientation=\"horizontal\"` for settings pages. Use `FieldLabel className=\"sr-only\"` for visually hidden labels.\n\n**Choosing form controls:**\n\n- Simple text input → `Input`\n- Dropdown with predefined options → `Select`\n- Searchable dropdown → `Combobox`\n- Native HTML select (no JS) → `native-select`\n- Boolean toggle → `Switch` (for settings) or `Checkbox` (for forms)\n- Single choice from few options → `RadioGroup`\n- Toggle between 2–5 options → `ToggleGroup` + `ToggleGroupItem`\n- OTP/verification code → `InputOTP`\n- Multi-line text → `Textarea`\n\n---\n\n## InputGroup requires InputGroupInput/InputGroupTextarea\n\nNever use raw `Input` or `Textarea` inside an `InputGroup`.\n\n**Incorrect:**\n\n```tsx\n<InputGroup>\n  <Input placeholder=\"Search...\" />\n</InputGroup>\n```\n\n**Correct:**\n\n```tsx\nimport { InputGroup, InputGroupInput } from \"@/components/ui/input-group\"\n\n<InputGroup>\n  <InputGroupInput placeholder=\"Search...\" />\n</InputGroup>\n```\n\n---\n\n## Buttons inside inputs use InputGroup + InputGroupAddon\n\nNever place a `Button` directly inside or adjacent to an `Input` with custom positioning.\n\n**Incorrect:**\n\n```tsx\n<div className=\"relative\">\n  <Input placeholder=\"Search...\" className=\"pr-10\" />\n  <Button className=\"absolute right-0 top-0\" size=\"icon\">\n    <SearchIcon />\n  </Button>\n</div>\n```\n\n**Correct:**\n\n```tsx\nimport { InputGroup, InputGroupInput, InputGroupAddon } from \"@/components/ui/input-group\"\n\n<InputGroup>\n  <InputGroupInput placeholder=\"Search...\" />\n  <InputGroupAddon>\n    <Button size=\"icon\">\n      <SearchIcon data-icon=\"inline-start\" />\n    </Button>\n  </InputGroupAddon>\n</InputGroup>\n```\n\n---\n\n## Option sets (2–7 choices) use ToggleGroup\n\nDon't manually loop `Button` components with active state.\n\n**Incorrect:**\n\n```tsx\nconst [selected, setSelected] = useState(\"daily\")\n\n<div className=\"flex gap-2\">\n  {[\"daily\", \"weekly\", \"monthly\"].map((option) => (\n    <Button\n      key={option}\n      variant={selected === option ? \"default\" : \"outline\"}\n      onClick={() => setSelected(option)}\n    >\n      {option}\n    </Button>\n  ))}\n</div>\n```\n\n**Correct:**\n\n```tsx\nimport { ToggleGroup, ToggleGroupItem } from \"@/components/ui/toggle-group\"\n\n<ToggleGroup spacing={2}>\n  <ToggleGroupItem value=\"daily\">Daily</ToggleGroupItem>\n  <ToggleGroupItem value=\"weekly\">Weekly</ToggleGroupItem>\n  <ToggleGroupItem value=\"monthly\">Monthly</ToggleGroupItem>\n</ToggleGroup>\n```\n\nCombine with `Field` for labelled toggle groups:\n\n```tsx\n<Field orientation=\"horizontal\">\n  <FieldTitle id=\"theme-label\">Theme</FieldTitle>\n  <ToggleGroup aria-labelledby=\"theme-label\" spacing={2}>\n    <ToggleGroupItem value=\"light\">Light</ToggleGroupItem>\n    <ToggleGroupItem value=\"dark\">Dark</ToggleGroupItem>\n    <ToggleGroupItem value=\"system\">System</ToggleGroupItem>\n  </ToggleGroup>\n</Field>\n```\n\n> **Note:** `defaultValue` and `type`/`multiple` props differ between base and radix. See [base-vs-radix.md](./base-vs-radix.md#togglegroup).\n\n---\n\n## FieldSet + FieldLegend for grouping related fields\n\nUse `FieldSet` + `FieldLegend` for related checkboxes, radios, or switches — not `div` with a heading:\n\n```tsx\n<FieldSet>\n  <FieldLegend variant=\"label\">Preferences</FieldLegend>\n  <FieldDescription>Select all that apply.</FieldDescription>\n  <FieldGroup className=\"gap-3\">\n    <Field orientation=\"horizontal\">\n      <Checkbox id=\"dark\" />\n      <FieldLabel htmlFor=\"dark\" className=\"font-normal\">Dark mode</FieldLabel>\n    </Field>\n  </FieldGroup>\n</FieldSet>\n```\n\n---\n\n## Field validation and disabled states\n\nBoth attributes are needed — `data-invalid`/`data-disabled` styles the field (label, description), while `aria-invalid`/`disabled` styles the control.\n\n```tsx\n// Invalid.\n<Field data-invalid>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" aria-invalid />\n  <FieldDescription>Invalid email address.</FieldDescription>\n</Field>\n\n// Disabled.\n<Field data-disabled>\n  <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n  <Input id=\"email\" disabled />\n</Field>\n```\n\nWorks for all controls: `Input`, `Textarea`, `Select`, `Checkbox`, `RadioGroupItem`, `Switch`, `Slider`, `NativeSelect`, `InputOTP`.\n","isInternal":false,"tokens":1203,"sizeBytes":4857},{"name":"icons.md","path":".agents/skills/shadcn/rules/icons.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/rules/icons.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Icons\n\n**Always use the project's configured `iconLibrary` for imports.** Check the `iconLibrary` field from project context: `lucide` → `lucide-react`, `tabler` → `@tabler/icons-react`, etc. Never assume `lucide-react`.\n\n---\n\n## Icons in Button use data-icon attribute\n\nAdd `data-icon=\"inline-start\"` (prefix) or `data-icon=\"inline-end\"` (suffix) to the icon. No sizing classes on the icon.\n\n**Incorrect:**\n\n```tsx\n<Button>\n  <SearchIcon className=\"mr-2 size-4\" />\n  Search\n</Button>\n```\n\n**Correct:**\n\n```tsx\n<Button>\n  <SearchIcon data-icon=\"inline-start\"/>\n  Search\n</Button>\n\n<Button>\n  Next\n  <ArrowRightIcon data-icon=\"inline-end\"/>\n</Button>\n```\n\n---\n\n## No sizing classes on icons inside components\n\nComponents handle icon sizing via CSS. Don't add `size-4`, `w-4 h-4`, or other sizing classes to icons inside `Button`, `DropdownMenuItem`, `Alert`, `Sidebar*`, or other shadcn components. Unless the user explicitly asks for custom icon sizes.\n\n**Incorrect:**\n\n```tsx\n<Button>\n  <SearchIcon className=\"size-4\" data-icon=\"inline-start\" />\n  Search\n</Button>\n\n<DropdownMenuItem>\n  <SettingsIcon className=\"mr-2 size-4\" />\n  Settings\n</DropdownMenuItem>\n```\n\n**Correct:**\n\n```tsx\n<Button>\n  <SearchIcon data-icon=\"inline-start\" />\n  Search\n</Button>\n\n<DropdownMenuItem>\n  <SettingsIcon />\n  Settings\n</DropdownMenuItem>\n```\n\n---\n\n## Pass icons as component objects, not string keys\n\nUse `icon={CheckIcon}`, not a string key to a lookup map.\n\n**Incorrect:**\n\n```tsx\nconst iconMap = {\n  check: CheckIcon,\n  alert: AlertIcon,\n}\n\nfunction StatusBadge({ icon }: { icon: string }) {\n  const Icon = iconMap[icon]\n  return <Icon />\n}\n\n<StatusBadge icon=\"check\" />\n```\n\n**Correct:**\n\n```tsx\n// Import from the project's configured iconLibrary (e.g. lucide-react, @tabler/icons-react).\nimport { CheckIcon } from \"lucide-react\"\n\nfunction StatusBadge({ icon: Icon }: { icon: React.ComponentType }) {\n  return <Icon />\n}\n\n<StatusBadge icon={CheckIcon} />\n```\n","isInternal":false,"tokens":522,"sizeBytes":1958},{"name":"styling.md","path":".agents/skills/shadcn/rules/styling.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/rules/styling.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Styling & Customization\n\nSee [customization.md](../customization.md) for theming, CSS variables, and adding custom colors.\n\n## Contents\n\n- Semantic colors\n- Built-in variants first\n- className for layout only\n- No space-x-* / space-y-*\n- Prefer size-* over w-* h-* when equal\n- Prefer truncate shorthand\n- No manual dark: color overrides\n- Use cn() for conditional classes\n- No manual z-index on overlay components\n- Use shimmer / scroll-fade utilities, not custom animations\n\n---\n\n## Semantic colors\n\n**Incorrect:**\n\n```tsx\n<div className=\"bg-blue-500 text-white\">\n  <p className=\"text-gray-600\">Secondary text</p>\n</div>\n```\n\n**Correct:**\n\n```tsx\n<div className=\"bg-primary text-primary-foreground\">\n  <p className=\"text-muted-foreground\">Secondary text</p>\n</div>\n```\n\n---\n\n## No raw color values for status/state indicators\n\nFor positive, negative, or status indicators, use Badge variants, semantic tokens like `text-destructive`, or define custom CSS variables — don't reach for raw Tailwind colors.\n\n**Incorrect:**\n\n```tsx\n<span className=\"text-emerald-600\">+20.1%</span>\n<span className=\"text-green-500\">Active</span>\n<span className=\"text-red-600\">-3.2%</span>\n```\n\n**Correct:**\n\n```tsx\n<Badge variant=\"secondary\">+20.1%</Badge>\n<Badge>Active</Badge>\n<span className=\"text-destructive\">-3.2%</span>\n```\n\nIf you need a success/positive color that doesn't exist as a semantic token, use a Badge variant or ask the user about adding a custom CSS variable to the theme (see [customization.md](../customization.md)).\n\n---\n\n## Built-in variants first\n\n**Incorrect:**\n\n```tsx\n<Button className=\"border border-input bg-transparent hover:bg-accent\">\n  Click me\n</Button>\n```\n\n**Correct:**\n\n```tsx\n<Button variant=\"outline\">Click me</Button>\n```\n\n---\n\n## className for layout only\n\nUse `className` for layout (e.g. `max-w-md`, `mx-auto`, `mt-4`), **not** for overriding component colors or typography. To change colors, use semantic tokens, built-in variants, or CSS variables.\n\n**Incorrect:**\n\n```tsx\n<Card className=\"bg-blue-100 text-blue-900 font-bold\">\n  <CardContent>Dashboard</CardContent>\n</Card>\n```\n\n**Correct:**\n\n```tsx\n<Card className=\"max-w-md mx-auto\">\n  <CardContent>Dashboard</CardContent>\n</Card>\n```\n\nTo customize a component's appearance, prefer these approaches in order:\n1. **Built-in variants** — `variant=\"outline\"`, `variant=\"destructive\"`, etc.\n2. **Semantic color tokens** — `bg-primary`, `text-muted-foreground`.\n3. **CSS variables** — define custom colors in the global CSS file (see [customization.md](../customization.md)).\n\n---\n\n## No space-x-* / space-y-*\n\nUse `gap-*` instead. `space-y-4` → `flex flex-col gap-4`. `space-x-2` → `flex gap-2`.\n\n```tsx\n<div className=\"flex flex-col gap-4\">\n  <Input />\n  <Input />\n  <Button>Submit</Button>\n</div>\n```\n\n---\n\n## Prefer size-* over w-* h-* when equal\n\n`size-10` not `w-10 h-10`. Applies to icons, avatars, skeletons, etc.\n\n---\n\n## Prefer truncate shorthand\n\n`truncate` not `overflow-hidden text-ellipsis whitespace-nowrap`.\n\n---\n\n## No manual dark: color overrides\n\nUse semantic tokens — they handle light/dark via CSS variables. `bg-background text-foreground` not `bg-white dark:bg-gray-950`.\n\n---\n\n## Use cn() for conditional classes\n\nUse the `cn()` utility from the project for conditional or merged class names. Don't write manual ternaries in className strings.\n\n**Incorrect:**\n\n```tsx\n<div className={`flex items-center ${isActive ? \"bg-primary text-primary-foreground\" : \"bg-muted\"}`}>\n```\n\n**Correct:**\n\n```tsx\nimport { cn } from \"@/lib/utils\"\n\n<div className={cn(\"flex items-center\", isActive ? \"bg-primary text-primary-foreground\" : \"bg-muted\")}>\n```\n\n---\n\n## No manual z-index on overlay components\n\n`Dialog`, `Sheet`, `Drawer`, `AlertDialog`, `DropdownMenu`, `Popover`, `Tooltip`, `HoverCard` handle their own stacking. Never add `z-50` or `z-[999]`.\n\n---\n\n## Use shimmer / scroll-fade utilities, not custom animations\n\nFor a live \"thinking…\" or loading-text shimmer, apply the `shimmer` utility. Don't author a custom `@keyframes` or a `bg-clip-text` gradient sweep.\n\nFor scroll-aware edge fading on a scroll container, use `scroll-fade` (and the axis variants `scroll-fade-x` / `scroll-fade-b`). Don't hand-roll mask gradients. The chat components already apply these internally: `Attachment` shimmers its title during upload, and `MessageScrollerViewport` fades its edges.\n\n**Incorrect:**\n\n```tsx\n<span className=\"animate-pulse bg-gradient-to-r from-muted-foreground/40 via-foreground/70 to-muted-foreground/40 bg-clip-text text-transparent [animation:shimmer_1.6s_infinite]\">\n  Thinking…\n</span>\n```\n\n**Correct:**\n\n```tsx\n<span className=\"shimmer\">Thinking…</span>\n```\n","isInternal":false,"tokens":1188,"sizeBytes":4684},{"name":"SKILL.md","path":".agents/skills/shadcn/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/shadcn/SKILL.md","title":"shadcn","category":"anthropic-skill","format":"markdown","content":"---\nname: shadcn\ndescription: Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. 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\".\nuser-invocable: false\nallowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)\n---\n\n# shadcn/ui\n\nA framework for building ui, components and design systems. Components are added as source code to the user's project via the CLI.\n\n> **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.\n\n## Current Project Context\n\n```json\n!`npx shadcn@latest info --json`\n```\n\nThe JSON above contains the project config and installed components. Use `npx shadcn@latest docs <component>` to get documentation and example URLs for any component.\n\n## Principles\n\n1. **Use existing components first.** Use `npx shadcn@latest search` to check registries before writing custom UI. Check community registries too.\n2. **Compose, don't reinvent.** Settings page = Tabs + Card + form controls. Dashboard = Sidebar + Card + Chart + Table.\n3. **Use built-in variants before custom styles.** `variant=\"outline\"`, `size=\"sm\"`, etc.\n4. **Use semantic colors.** `bg-primary`, `text-muted-foreground` — never raw values like `bg-blue-500`.\n\n## Critical Rules\n\nThese rules are **always enforced**. Each links to a file with Incorrect/Correct code pairs.\n\n### Styling & Tailwind → [styling.md](./rules/styling.md)\n\n- **`className` for layout, not styling.** Never override component colors or typography.\n- **No `space-x-*` or `space-y-*`.** Use `flex` with `gap-*`. For vertical stacks, `flex flex-col gap-*`.\n- **Use `size-*` when width and height are equal.** `size-10` not `w-10 h-10`.\n- **Use `truncate` shorthand.** Not `overflow-hidden text-ellipsis whitespace-nowrap`.\n- **No manual `dark:` color overrides.** Use semantic tokens (`bg-background`, `text-muted-foreground`).\n- **Use `cn()` for conditional classes.** Don't write manual template literal ternaries.\n- **No manual `z-index` on overlay components.** Dialog, Sheet, Popover, etc. handle their own stacking.\n\n### Forms & Inputs → [forms.md](./rules/forms.md)\n\n- **Forms use `FieldGroup` + `Field`.** Never use raw `div` with `space-y-*` or `grid gap-*` for form layout.\n- **`InputGroup` uses `InputGroupInput`/`InputGroupTextarea`.** Never raw `Input`/`Textarea` inside `InputGroup`.\n- **Buttons inside inputs use `InputGroup` + `InputGroupAddon`.**\n- **Option sets (2–7 choices) use `ToggleGroup`.** Don't loop `Button` with manual active state.\n- **`FieldSet` + `FieldLegend` for grouping related checkboxes/radios.** Don't use a `div` with a heading.\n- **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.\n\n### Component Structure → [composition.md](./rules/composition.md)\n\n- **Items always inside their Group.** `SelectItem` → `SelectGroup`. `DropdownMenuItem` → `DropdownMenuGroup`. `CommandItem` → `CommandGroup`.\n- **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)\n- **Dialog, Sheet, and Drawer always need a Title.** `DialogTitle`, `SheetTitle`, `DrawerTitle` required for accessibility. Use `className=\"sr-only\"` if visually hidden.\n- **Use full Card composition.** `CardHeader`/`CardTitle`/`CardDescription`/`CardContent`/`CardFooter`. Don't dump everything in `CardContent`.\n- **Button has no `isPending`/`isLoading`.** Compose with `Spinner` + `data-icon` + `disabled`.\n- **`TabsTrigger` must be inside `TabsList`.** Never render triggers directly in `Tabs`.\n- **`Avatar` always needs `AvatarFallback`.** For when the image fails to load.\n\n### Use Components, Not Custom Markup → [composition.md](./rules/composition.md)\n\n- **Use existing components before custom markup.** Check if a component exists before writing a styled `div`.\n- **Callouts use `Alert`.** Don't build custom styled divs.\n- **Empty states use `Empty`.** Don't build custom empty state markup.\n- **Toast via `sonner`.** Use `toast()` from `sonner`.\n- **Use `Separator`** instead of `<hr>` or `<div className=\"border-t\">`.\n- **Use `Skeleton`** for loading placeholders. No custom `animate-pulse` divs.\n- **Use `Badge`** instead of custom styled spans.\n\n### Icons → [icons.md](./rules/icons.md)\n\n- **Icons in `Button` use `data-icon`.** `data-icon=\"inline-start\"` or `data-icon=\"inline-end\"` on the icon.\n- **No sizing classes on icons inside components.** Components handle icon sizing via CSS. No `size-4` or `w-4 h-4`.\n- **Pass icons as objects, not string keys.** `icon={CheckIcon}`, not a string lookup.\n\n### Chat & Messaging → [chat.md](./rules/chat.md)\n\n- **Chat UI composes the chat primitives.** Conversations use `MessageScroller`, rows use `Message`, surfaces use `Bubble`. Never hand-rolled bubble `div`s or a raw scroll container.\n- **`MessageScroller` owns scroll behavior.** Streaming follow, anchoring, and jump-to-latest (`MessageScrollerButton`) are built in. Don't write a `useStickToBottom`/`ResizeObserver` hook.\n- **Attachments use `Attachment`; system notes and dividers use `Marker`.** Not `Item` cards or `Separator` + a label.\n\n### CLI\n\n- **Never decode preset codes or build preset URLs manually.** Use `npx shadcn@latest preset decode <code>`, `preset url <code>`, or `preset open <code>`. For project-aware preset detection, use `npx shadcn@latest preset resolve`.\n- **Apply preset codes directly with the CLI.** Use `npx shadcn@latest apply <code>` for existing projects, or `npx shadcn@latest init --preset <code>` when initializing.\n\n## Key Patterns\n\nThese are the most common patterns that differentiate correct shadcn/ui code. For edge cases, see the linked rule files above.\n\n```tsx\n// Form layout: FieldGroup + Field, not div + Label.\n<FieldGroup>\n  <Field>\n    <FieldLabel htmlFor=\"email\">Email</FieldLabel>\n    <Input id=\"email\" />\n  </Field>\n</FieldGroup>\n\n// Validation: data-invalid on Field, aria-invalid on the control.\n<Field data-invalid>\n  <FieldLabel>Email</FieldLabel>\n  <Input aria-invalid />\n  <FieldDescription>Invalid email.</FieldDescription>\n</Field>\n\n// Icons in buttons: data-icon, no sizing classes.\n<Button>\n  <SearchIcon data-icon=\"inline-start\" />\n  Search\n</Button>\n\n// Spacing: gap-*, not space-y-*.\n<div className=\"flex flex-col gap-4\">  // correct\n<div className=\"space-y-4\">           // wrong\n\n// Equal dimensions: size-*, not w-* h-*.\n<Avatar className=\"size-10\">   // correct\n<Avatar className=\"w-10 h-10\"> // wrong\n\n// Status colors: Badge variants or semantic tokens, not raw colors.\n<Badge variant=\"secondary\">+20.1%</Badge>    // correct\n<span className=\"text-emerald-600\">+20.1%</span> // wrong\n```\n\n## Component Selection\n\n| Need                       | Use                                                                                                 |\n| -------------------------- | --------------------------------------------------------------------------------------------------- |\n| Button/action              | `Button` with appropriate variant                                                                   |\n| Form inputs                | `Input`, `Select`, `Combobox`, `Switch`, `Checkbox`, `RadioGroup`, `Textarea`, `InputOTP`, `Slider` |\n| Toggle between 2–5 options | `ToggleGroup` + `ToggleGroupItem`                                                                   |\n| Data display               | `Table`, `Card`, `Badge`, `Avatar`                                                                  |\n| Navigation                 | `Sidebar`, `NavigationMenu`, `Breadcrumb`, `Tabs`, `Pagination`                                     |\n| Overlays                   | `Dialog` (modal), `Sheet` (side panel), `Drawer` (bottom sheet), `AlertDialog` (confirmation)       |\n| Feedback                   | `sonner` (toast), `Alert`, `Progress`, `Skeleton`, `Spinner`                                        |\n| Command palette            | `Command` inside `Dialog`                                                                           |\n| Charts                     | `Chart` (wraps Recharts)                                                                            |\n| Layout                     | `Card`, `Separator`, `Resizable`, `ScrollArea`, `Accordion`, `Collapsible`                          |\n| Empty states               | `Empty`                                                                                             |\n| Menus                      | `DropdownMenu`, `ContextMenu`, `Menubar`                                                            |\n| Tooltips/info              | `Tooltip`, `HoverCard`, `Popover`                                                                   |\n| Chat / conversation UI     | `MessageScroller`, `Message`, `Bubble`, `Attachment`, `Marker`                                      |\n\n## Key Fields\n\nThe injected project context contains these key fields:\n\n- **`aliases`** → use the actual alias prefix for imports (e.g. `@/`, `~/`), never hardcode.\n- **`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.\n- **`tailwindVersion`** → `\"v4\"` uses `@theme inline` blocks; `\"v3\"` uses `tailwind.config.js`.\n- **`tailwindCssFile`** → the global CSS file where custom CSS variables are defined. Always edit this file, never create a new one.\n- **`style`** → component visual treatment (e.g. `nova`, `vega`).\n- **`base`** → primitive library (`radix` or `base`). Affects component APIs and available props.\n- **`iconLibrary`** → determines icon imports. Use `lucide-react` for `lucide`, `@tabler/icons-react` for `tabler`, etc. Never assume `lucide-react`.\n- **`resolvedPaths`** → exact file-system destinations for components, utils, hooks, etc.\n- **`framework`** → routing and file conventions (e.g. Next.js App Router vs Vite SPA).\n- **`packageManager`** → use this for any non-shadcn dependency installs (e.g. `pnpm add date-fns` vs `npm install date-fns`).\n- **`preset`** → resolved preset code and values for the current project. Use `npx shadcn@latest preset resolve --json` when you only need preset information.\n\nSee [cli.md — `info` command](./cli.md) for the full field reference.\n\n## Component Docs, Examples, and Usage\n\nRun `npx shadcn@latest docs <component>` to get the URLs for a component's documentation, examples, and API reference. Fetch these URLs to get the actual content.\n\n```bash\nnpx shadcn@latest docs button dialog select\n```\n\n**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.\n\n## Workflow\n\n1. **Get project context** — already injected above. Run `npx shadcn@latest info` again if you need to refresh.\n2. **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.\n3. **Find components** — `npx shadcn@latest search`.\n4. **Get docs and examples** — run `npx shadcn@latest docs <component>` 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`.\n5. **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).\n6. **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.\n7. **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.\n8. **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`, `owner/repo`, etc.), ask which registry to use. Never default to a registry on behalf of the user.\n9. **Switching presets** — Ask the user first: **overwrite**, **partial**, **merge**, or **skip**?\n   - **Inspect current preset**: `npx shadcn@latest preset resolve`. Use `--json` when you need structured values.\n   - **Inspect incoming preset**: `npx shadcn@latest preset decode <code>`. Use `preset url <code>` or `preset open <code>` to share or open the preset builder.\n   - **Overwrite**: `npx shadcn@latest apply <code>`. Overwrites detected components, fonts, and CSS variables.\n   - **Partial**: `npx shadcn@latest apply <code> --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.\n   - **Merge**: `npx shadcn@latest init --preset <code> --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.\n   - **Skip**: `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS, leaves components as-is.\n   - **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 <current-base>` explicitly — preset codes do not encode the base.\n\n## Updating Components\n\nWhen 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.**\n\n1. Run `npx shadcn@latest add <component> --dry-run` to see all files that would be affected.\n2. For each file, run `npx shadcn@latest add <component> --diff <file>` to see what changed upstream vs local.\n3. Decide per file based on the diff:\n   - No local changes → safe to overwrite.\n   - Has local changes → read the local file, analyze the diff, and apply upstream updates while preserving local modifications.\n   - User says \"just update everything\" → use `--overwrite`, but confirm first.\n4. **Never use `--overwrite` without the user's explicit approval.**\n\n## Quick Reference\n\n```bash\n# Create a new project.\nnpx shadcn@latest init --name my-app --preset base-nova\nnpx shadcn@latest init --name my-app --preset a2r6bw --template vite\n\n# Create a monorepo project.\nnpx shadcn@latest init --name my-app --preset base-nova --monorepo\nnpx shadcn@latest init --name my-app --preset base-nova --template next --monorepo\n\n# Initialize existing project.\nnpx shadcn@latest init --preset base-nova\nnpx shadcn@latest init --defaults  # shortcut: --template=next --preset=nova (base style implied)\n\n# Apply a preset to an existing project.\nnpx shadcn@latest apply a2r6bw\nnpx shadcn@latest apply a2r6bw --only theme\nnpx shadcn@latest apply a2r6bw --only font\nnpx shadcn@latest apply a2r6bw --only theme,font\n\n# Inspect preset codes and project preset state.\nnpx shadcn@latest preset decode a2r6bw\nnpx shadcn@latest preset url a2r6bw\nnpx shadcn@latest preset open a2r6bw\nnpx shadcn@latest preset resolve\nnpx shadcn@latest preset resolve --json\n\n# Add components.\nnpx shadcn@latest add button card dialog\nnpx shadcn@latest add @magicui/shimmer-button\nnpx shadcn@latest add owner/repo/item\nnpx shadcn@latest add --all\n\n# Preview changes before adding/updating.\nnpx shadcn@latest add button --dry-run\nnpx shadcn@latest add button --diff button.tsx\nnpx shadcn@latest add @acme/form --view button.tsx\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# Search registries.\nnpx shadcn@latest search @shadcn -q \"sidebar\"\nnpx shadcn@latest search @tailark -q \"stats\"\nnpx shadcn@latest search owner/repo -q \"login\"\nnpx shadcn@latest search                          # all configured registries\nnpx shadcn@latest search @shadcn -q \"menu\" -t ui  # filter by item type\n\n# Get component docs and example URLs.\nnpx shadcn@latest docs button dialog select\n\n# View registry item details (for items not yet installed).\nnpx shadcn@latest view @shadcn/button\nnpx shadcn@latest view owner/repo/item\n```\n\n**Named presets:** `nova`, `vega`, `maia`, `lyra`, `mira`, `luma`\n**Templates:** `next`, `vite`, `start`, `react-router`, `astro` (all support `--monorepo`) and `laravel` (not supported for monorepo)\n**Preset codes:** Version-prefixed base62 strings (e.g. `a2r6bw` or `b0`), from [ui.shadcn.com](https://ui.shadcn.com).\n\n## Detailed References\n\n- [rules/forms.md](./rules/forms.md) — FieldGroup, Field, InputGroup, ToggleGroup, FieldSet, validation states\n- [rules/composition.md](./rules/composition.md) — Groups, overlays, Card, Tabs, Avatar, Alert, Empty, Toast, Separator, Skeleton, Badge, Button loading\n- [rules/chat.md](./rules/chat.md) — MessageScroller, Message, Bubble, Attachment, Marker; streaming, anchoring, jump-to-latest\n- [rules/icons.md](./rules/icons.md) — data-icon, icon sizing, passing icons as objects\n- [rules/styling.md](./rules/styling.md) — Semantic colors, variants, className, spacing, size, truncate, dark mode, cn(), z-index\n- [rules/base-vs-radix.md](./rules/base-vs-radix.md) — asChild vs render, Select, ToggleGroup, Slider, Accordion\n- [cli.md](./cli.md) — Commands, flags, presets, templates\n- [registry.md](./registry.md) — Authoring source registries, `include`, item definitions, dependencies, GitHub registry rules\n- [customization.md](./customization.md) — Theming, CSS variables, extending components\n","frontmatter":{"name":"shadcn","description":"Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. 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 *)","allowedTools":["Bash(npx","shadcn@latest","*),","Bash(pnpm","dlx","shadcn@latest","*),","Bash(bunx","--bun","shadcn@latest","*)"]},"allowedTools":["Bash(npx","shadcn@latest","*),","Bash(pnpm","dlx","shadcn@latest","*),","Bash(bunx","--bun","shadcn@latest","*)"],"isInternal":false,"tokens":4829,"sizeBytes":19415},{"name":"SKILL.md","path":".agents/skills/tanstack-form/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/tanstack-form/SKILL.md","title":"tanstack-form","category":"anthropic-skill","format":"markdown","content":"---\nname: tanstack-form\ndescription: Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte.\n---\n\n## Overview\n\nTanStack Form is a headless form library with deep TypeScript integration. It provides field-level and form-level validation (sync/async), array fields, linked/dependent fields, fine-grained reactivity, and schema validation adapter support (Zod, Valibot, Yup).\n\n**Package:** `@tanstack/react-form`\n**Adapters:** `@tanstack/zod-form-adapter`, `@tanstack/valibot-form-adapter`\n**Status:** Stable (v1)\n\n## Installation\n\n```bash\nnpm install @tanstack/react-form\n# Optional schema adapters:\nnpm install @tanstack/zod-form-adapter zod\nnpm install @tanstack/valibot-form-adapter valibot\n```\n\n## Core: useForm\n\n```tsx\nimport { useForm } from '@tanstack/react-form';\n\nfunction MyForm() {\n  const form = useForm({\n    defaultValues: {\n      firstName: '',\n      lastName: '',\n      email: '',\n      age: 0\n    },\n    onSubmit: async ({ value }) => {\n      // value is fully typed\n      await submitToServer(value);\n    },\n    onSubmitInvalid: ({ value, formApi }) => {\n      console.log('Validation failed:', formApi.state.errors);\n    }\n  });\n\n  return (\n    <form\n      onSubmit={(e) => {\n        e.preventDefault();\n        e.stopPropagation();\n        form.handleSubmit();\n      }}\n    >\n      {/* Fields */}\n      <form.Subscribe\n        selector={(state) => ({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting })}\n        children={({ canSubmit, isSubmitting }) => (\n          <button type='submit' disabled={!canSubmit}>\n            {isSubmitting ? 'Submitting...' : 'Submit'}\n          </button>\n        )}\n      />\n    </form>\n  );\n}\n```\n\n## Fields (form.Field)\n\n```tsx\n<form.Field\n  name=\"firstName\"\n  validators={{\n    onChange: ({ value }) =>\n      value.length < 3 ? 'Must be at least 3 characters' : undefined,\n  }}\n  children={(field) => (\n    <div>\n      <label htmlFor={field.name}>First Name</label>\n      <input\n        id={field.name}\n        name={field.name}\n        value={field.state.value}\n        onBlur={field.handleBlur}\n        onChange={(e) => field.handleChange(e.target.value)}\n      />\n      {field.state.meta.isTouched && field.state.meta.errors.length > 0 && (\n        <em>{field.state.meta.errors.join(', ')}</em>\n      )}\n    </div>\n  )}\n/>\n\n<!-- Nested fields use dot notation -->\n<form.Field name=\"address.city\">\n  {(field) => (\n    <input\n      value={field.state.value}\n      onChange={(e) => field.handleChange(e.target.value)}\n      onBlur={field.handleBlur}\n    />\n  )}\n</form.Field>\n```\n\n## Validation\n\n### Validation Timing\n\n| Cause      | When                     |\n| ---------- | ------------------------ |\n| `onChange` | After every value change |\n| `onBlur`   | When field loses focus   |\n| `onSubmit` | During submission        |\n| `onMount`  | When field mounts        |\n\n### Synchronous Validation\n\n```tsx\n<form.Field\n  name='age'\n  validators={{\n    onChange: ({ value }) => {\n      if (value < 18) return 'Must be 18 or older';\n      return undefined; // undefined = valid\n    },\n    onBlur: ({ value }) => {\n      if (!value) return 'Required';\n      return undefined;\n    }\n  }}\n/>\n```\n\n### Asynchronous Validation\n\n```tsx\n<form.Field\n  name='username'\n  asyncDebounceMs={500}\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const res = await fetch(`/api/check-username?q=${value}`);\n      const { available } = await res.json();\n      if (!available) return 'Username taken';\n      return undefined;\n    }\n  }}\n>\n  {(field) => (\n    <>\n      <input value={field.state.value} onChange={(e) => field.handleChange(e.target.value)} />\n      {field.state.meta.isValidating && <span>Checking...</span>}\n    </>\n  )}\n</form.Field>\n```\n\n### Schema Validation (Zod)\n\n```tsx\nimport { zodValidator } from '@tanstack/zod-form-adapter'\nimport { z } from 'zod'\n\nconst form = useForm({\n  defaultValues: { email: '', age: 0 },\n  validatorAdapter: zodValidator(),\n  onSubmit: async ({ value }) => { /* ... */ },\n})\n\n<form.Field\n  name=\"email\"\n  validators={{\n    onChange: z.string().email('Invalid email'),\n    onBlur: z.string().min(1, 'Required'),\n  }}\n/>\n\n<form.Field\n  name=\"age\"\n  validators={{\n    onChange: z.number().min(18, 'Must be 18+'),\n  }}\n/>\n```\n\n### Form-Level Validation\n\n```tsx\nconst form = useForm({\n  defaultValues: { password: '', confirmPassword: '' },\n  validators: {\n    onChange: ({ value }) => {\n      if (value.password !== value.confirmPassword) {\n        return 'Passwords do not match';\n      }\n      return undefined;\n    }\n  }\n});\n```\n\n### Linked/Dependent Fields\n\n```tsx\n<form.Field\n  name='confirmPassword'\n  validators={{\n    onChangeListenTo: ['password'], // Re-validate when password changes\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      if (value !== password) return 'Passwords do not match';\n      return undefined;\n    }\n  }}\n/>\n```\n\n## Array Fields\n\n```tsx\n<form.Field name='people' mode='array'>\n  {(field) => (\n    <div>\n      {field.state.value.map((_, index) => (\n        <div key={index}>\n          <form.Field name={`people[${index}].name`}>\n            {(subField) => (\n              <input\n                value={subField.state.value}\n                onChange={(e) => subField.handleChange(e.target.value)}\n              />\n            )}\n          </form.Field>\n          <button type='button' onClick={() => field.removeValue(index)}>\n            Remove\n          </button>\n        </div>\n      ))}\n      <button type='button' onClick={() => field.pushValue({ name: '', age: 0 })}>\n        Add Person\n      </button>\n    </div>\n  )}\n</form.Field>\n```\n\n### Array Methods\n\n```typescript\nfield.pushValue(item); // Add to end\nfield.insertValue(index, item); // Insert at index\nfield.replaceValue(index, item); // Replace at index\nfield.removeValue(index); // Remove at index\nfield.swapValues(indexA, indexB); // Swap positions\nfield.moveValue(from, to); // Move position\n```\n\n## Listeners (Side Effects)\n\n```tsx\n<form.Field\n  name='country'\n  listeners={{\n    onChange: ({ value }) => {\n      // Side effect: reset dependent fields\n      form.setFieldValue('state', '');\n      form.setFieldValue('postalCode', '');\n    }\n  }}\n/>\n```\n\n## Reactivity (form.Subscribe & useStore)\n\n```tsx\n// Render-prop subscription (fine-grained)\n<form.Subscribe\n  selector={(state) => ({ canSubmit: state.canSubmit, isDirty: state.isDirty })}\n  children={({ canSubmit, isDirty }) => (\n    <div>\n      {isDirty && <span>Unsaved changes</span>}\n      <button disabled={!canSubmit}>Save</button>\n    </div>\n  )}\n/>;\n\n// Hook-based subscription\nfunction FormStatus() {\n  const isValid = form.useStore((s) => s.isValid);\n  return isValid ? null : <p>Fix errors</p>;\n}\n```\n\n## Form State\n\n```typescript\ninterface FormState {\n  values: TFormData;\n  errors: ValidationError[];\n  errorMap: Record<string, ValidationError>;\n  isFormValid: boolean;\n  isFieldsValid: boolean;\n  isValid: boolean; // isFormValid && isFieldsValid\n  isTouched: boolean;\n  isPristine: boolean;\n  isDirty: boolean;\n  isSubmitting: boolean;\n  isSubmitted: boolean;\n  isSubmitSuccessful: boolean;\n  submissionAttempts: number;\n  canSubmit: boolean; // isValid && !isSubmitting\n}\n```\n\n## Field State\n\n```typescript\ninterface FieldState<TData> {\n  value: TData;\n  meta: {\n    isTouched: boolean;\n    isDirty: boolean;\n    isPristine: boolean;\n    isValidating: boolean;\n    errors: ValidationError[];\n    errorMap: Record<ValidationCause, ValidationError>;\n  };\n}\n```\n\n## FormApi Methods\n\n```typescript\nform.handleSubmit();\nform.reset();\nform.getFieldValue(field);\nform.setFieldValue(field, value);\nform.getFieldMeta(field);\nform.setFieldMeta(field, updater);\nform.validateAllFields(cause);\nform.validateField(field, cause);\nform.deleteField(field);\n```\n\n## Shared Form Options (formOptions)\n\n```tsx\nimport { formOptions } from '@tanstack/react-form';\n\nconst sharedOpts = formOptions({\n  defaultValues: { firstName: '', lastName: '' }\n});\n\n// Reuse across components\nconst form = useForm({\n  ...sharedOpts,\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n## Server-Side Validation\n\n```tsx\n// TanStack Start / Next.js server action\nimport { ServerValidateError } from '@tanstack/react-form/nextjs';\n\nexport async function validateForm(data: FormData) {\n  const email = data.get('email') as string;\n  if (await checkEmailExists(email)) {\n    throw new ServerValidateError({\n      form: 'Submission failed',\n      fields: { email: 'Email already registered' }\n    });\n  }\n}\n```\n\n## TypeScript Integration\n\n```tsx\n// Type-safe field paths with DeepKeys\ninterface UserForm {\n  name: string\n  address: { street: string; city: string }\n  tags: string[]\n  contacts: Array<{ name: string; phone: string }>\n}\n\n// TypeScript auto-completes all valid paths:\n// 'name', 'address', 'address.street', 'address.city', 'tags', 'contacts'\n<form.Field name=\"address.city\" />     // OK\n<form.Field name=\"nonexistent\" />       // Type Error!\n```\n\n## Best Practices\n\n1. **Always call `e.preventDefault()` and `e.stopPropagation()`** on form submit\n2. **Always attach `onBlur={field.handleBlur}`** for blur validation and isTouched tracking\n3. **Use `mode=\"array\"`** for array fields to get array methods\n4. **Return `undefined`** (not null/false) for valid validators\n5. **Use `asyncDebounceMs`** for async validators to prevent API spam\n6. **Check `isTouched` before showing errors** for better UX\n7. **Use `form.Subscribe` with selectors** to minimize re-renders\n8. **Use `formOptions`** for shared configuration across components\n9. **Use schema validators** (Zod/Valibot) for complex validation rules\n10. **Use `onChangeListenTo`** for cross-field validation dependencies\n\n## Common Pitfalls\n\n- Forgetting `e.preventDefault()` on form submit (causes page reload)\n- Not attaching `onBlur` to inputs (breaks blur validation and isTouched)\n- Returning `null` or `false` instead of `undefined` for valid fields\n- Using `mode=\"array\"` incorrectly (only needed on the array field itself, not sub-fields)\n- Subscribing to entire form state instead of using selectors (unnecessary re-renders)\n- Not using `asyncDebounceMs` with async validators (fires on every keystroke)\n","frontmatter":{"name":"tanstack-form","description":"Headless, performant, and type-safe form state management for TS/JS, React, Vue, Angular, Solid, Lit, and Svelte."},"isInternal":false,"tokens":2510,"sizeBytes":10247},{"name":"cache-strategies.md","path":".agents/skills/tanstack-query/resources/cache-strategies.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/tanstack-query/resources/cache-strategies.md","title":"Resources Skill","category":"anthropic-skill","format":"markdown","content":"# Cache Management Strategies\n\n## Cache Time Configuration\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  staleTime: 5 * 60 * 1000, // Consider fresh for 5 minutes\n  gcTime: 10 * 60 * 1000 // Keep in cache for 10 minutes (formerly cacheTime)\n});\n```\n\n## Cache Invalidation\n\n### Invalidate Specific Queries\n\n```typescript\nconst queryClient = useQueryClient();\n\n// Invalidate all post queries\nqueryClient.invalidateQueries({ queryKey: ['posts'] });\n\n// Invalidate specific post\nqueryClient.invalidateQueries({ queryKey: ['post', postId] });\n\n// Invalidate with exact match\nqueryClient.invalidateQueries({\n  queryKey: ['posts'],\n  exact: true // Only ['posts'], not ['posts', 'list']\n});\n```\n\n### Invalidate on Mutation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n  }\n});\n```\n\n## Manual Cache Updates\n\n### Set Query Data\n\n```typescript\n// Update cache directly\nqueryClient.setQueryData(['post', postId], (oldData) => ({\n  ...oldData,\n  title: 'New Title'\n}));\n\n// Set new data\nqueryClient.setQueryData(['post', postId], newPost);\n```\n\n### Get Query Data\n\n```typescript\n// Read from cache\nconst cachedPost = queryClient.getQueryData(['post', postId]);\n\n// Use in initialData\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  initialData: () => queryClient.getQueryData(['posts'])?.find((p) => p.id === postId)\n});\n```\n\n## Refetch Strategies\n\n### Refetch on Window Focus\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  refetchOnWindowFocus: true // Refetch when tab regains focus\n});\n```\n\n### Refetch on Reconnect\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: fetchPosts,\n  refetchOnReconnect: true // Refetch when internet reconnects\n});\n```\n\n### Refetch Intervals\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['live-data'],\n  queryFn: fetchLiveData,\n  refetchInterval: 5000, // Refetch every 5 seconds\n  refetchIntervalInBackground: false // Pause when tab not active\n});\n```\n\n## Cache Persistence\n\n### Persist to localStorage\n\n```typescript\nimport { QueryClient } from '@tanstack/react-query';\nimport { PersistQueryClientProvider } from '@tanstack/react-query-persist-client';\nimport { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister';\n\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    queries: {\n      gcTime: 1000 * 60 * 60 * 24  // 24 hours\n    }\n  }\n});\n\nconst persister = createSyncStoragePersister({\n  storage: window.localStorage\n});\n\n<PersistQueryClientProvider\n  client={queryClient}\n  persister={persister}\n>\n  <App />\n</PersistQueryClientProvider>\n```\n\n## Cache Deduplication\n\n### Automatic Request Deduplication\n\n```typescript\n// Both components will share the same request\nfunction Component1() {\n  const { data } = useQuery({\n    queryKey: ['posts'],\n    queryFn: fetchPosts\n  });\n}\n\nfunction Component2() {\n  const { data } = useQuery({\n    queryKey: ['posts'], // Same key = same request\n    queryFn: fetchPosts\n  });\n}\n```\n\n## Cache Preloading\n\n### Prefetch Queries\n\n```typescript\nconst queryClient = useQueryClient();\n\n// Prefetch before navigation\nconst handleMouseEnter = () => {\n  queryClient.prefetchQuery({\n    queryKey: ['post', postId],\n    queryFn: () => fetchPost(postId)\n  });\n};\n\n// Prefetch in loader\nrouter.beforeEach(async (to, from, next) => {\n  await queryClient.prefetchQuery({\n    queryKey: ['user', to.params.userId],\n    queryFn: () => fetchUser(to.params.userId)\n  });\n  next();\n});\n```\n\n### Ensure Query Data\n\n```typescript\n// Fetch if not in cache, otherwise use cached\nawait queryClient.ensureQueryData({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId)\n});\n```\n\n## Selective Cache Updates\n\n### Update Nested Data\n\n```typescript\nqueryClient.setQueryData(['posts'], (oldPosts) => {\n  return oldPosts.map((post) => (post.id === updatedPost.id ? updatedPost : post));\n});\n```\n\n### Add to List Cache\n\n```typescript\n// After creating a post\nqueryClient.setQueryData(['posts'], (oldPosts = []) => {\n  return [newPost, ...oldPosts];\n});\n```\n\n### Remove from List Cache\n\n```typescript\n// After deleting a post\nqueryClient.setQueryData(['posts'], (oldPosts) => {\n  return oldPosts.filter((post) => post.id !== deletedPostId);\n});\n```\n\n## Cache Debugging\n\n### React Query Devtools\n\n```typescript\nimport { ReactQueryDevtools } from '@tanstack/react-query-devtools';\n\n<QueryClientProvider client={queryClient}>\n  <App />\n  <ReactQueryDevtools initialIsOpen={false} />\n</QueryClientProvider>\n```\n\n### Query Cache Events\n\n```typescript\nconst queryCache = queryClient.getQueryCache();\n\nqueryCache.subscribe((event) => {\n  console.log('Query cache event:', event.type, event.query.queryKey);\n});\n```\n\n## Best Practices\n\n1. **Set Appropriate staleTime** - Balance freshness vs performance\n2. **Use Invalidation Over Refetch** - Let queries refetch when needed\n3. **Prefetch Predictably** - Preload data on hover/intent\n4. **Update Cache on Mutations** - Keep UI in sync\n5. **Use Devtools** - Debug cache issues visually\n6. **Persist Important Data** - Save to localStorage for offline support\n7. **Deduplicate Requests** - Rely on automatic deduplication\n","isInternal":false,"tokens":1352,"sizeBytes":5348},{"name":"data-fetching.md","path":".agents/skills/tanstack-query/resources/data-fetching.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/tanstack-query/resources/data-fetching.md","title":"Resources Skill","category":"anthropic-skill","format":"markdown","content":"# Advanced Data Fetching Patterns with TanStack Query\n\n## Dependent Queries\n\nQueries that depend on data from other queries:\n\n```typescript\n// First query - Get user ID\nconst { data: user } = useQuery({\n  queryKey: ['user'],\n  queryFn: fetchCurrentUser\n});\n\n// Second query - Depends on user ID\nconst { data: posts } = useQuery({\n  queryKey: ['posts', user?.id],\n  queryFn: () => fetchUserPosts(user!.id),\n  enabled: !!user // Only run when user is available\n});\n```\n\n## Parallel Queries\n\nFetch multiple independent queries simultaneously:\n\n```typescript\nfunction Dashboard() {\n  const queries = useQueries({\n    queries: [\n      { queryKey: ['stats'], queryFn: fetchStats },\n      { queryKey: ['recentPosts'], queryFn: fetchRecentPosts },\n      { queryKey: ['notifications'], queryFn: fetchNotifications }\n    ]\n  });\n\n  const [statsQuery, postsQuery, notificationsQuery] = queries;\n\n  if (queries.some(q => q.isLoading)) return <Loading />;\n\n  return <Dashboard\n    stats={statsQuery.data}\n    posts={postsQuery.data}\n    notifications={notificationsQuery.data}\n  />;\n}\n```\n\n## Infinite Queries\n\nFor pagination and infinite scroll:\n\n```typescript\nconst {\n  data,\n  fetchNextPage,\n  hasNextPage,\n  isFetchingNextPage\n} = useInfiniteQuery({\n  queryKey: ['posts'],\n  queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),\n  getNextPageParam: (lastPage, pages) => lastPage.nextCursor,\n  initialPageParam: 1\n});\n\n// Flatten pages\nconst allPosts = data?.pages.flatMap(page => page.posts) ?? [];\n\nreturn (\n  <div>\n    {allPosts.map(post => <PostCard key={post.id} post={post} />)}\n    {hasNextPage && (\n      <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>\n        {isFetchingNextPage ? 'Loading...' : 'Load More'}\n      </button>\n    )}\n  </div>\n);\n```\n\n## Prefetching\n\nPreload data before it's needed:\n\n```typescript\nimport { useQueryClient } from '@tanstack/react-query';\n\nfunction PostLink({ postId }: { postId: string }) {\n  const queryClient = useQueryClient();\n\n  const handleMouseEnter = () => {\n    queryClient.prefetchQuery({\n      queryKey: ['post', postId],\n      queryFn: () => fetchPost(postId)\n    });\n  };\n\n  return (\n    <Link to={`/posts/${postId}`} onMouseEnter={handleMouseEnter}>\n      View Post\n    </Link>\n  );\n}\n```\n\n## Suspense Mode\n\nUse with React Suspense:\n\n```typescript\nimport { useSuspenseQuery } from '@tanstack/react-query';\n\nfunction PostDetails({ postId }: { postId: string }) {\n  // Throws promise on loading, error on error\n  const { data: post } = useSuspenseQuery({\n    queryKey: ['post', postId],\n    queryFn: () => fetchPost(postId)\n  });\n\n  return <div>{post.title}</div>;\n}\n\n// Wrap with Suspense\n<Suspense fallback={<Loading />}>\n  <PostDetails postId=\"123\" />\n</Suspense>\n```\n\n## Query Cancellation\n\nCancel queries when component unmounts:\n\n```typescript\nconst { data, isLoading } = useQuery({\n  queryKey: ['search', searchTerm],\n  queryFn: async ({ signal }) => {\n    const response = await fetch(`/api/search?q=${searchTerm}`, { signal });\n    return response.json();\n  }\n});\n```\n\n## Initial Data\n\nProvide initial data to avoid loading state:\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  initialData: () => {\n    // Get from cache or other source\n    return queryClient.getQueryData(['posts'])?.find((post) => post.id === postId);\n  }\n});\n```\n\n## Placeholder Data\n\nShow placeholder while loading:\n\n```typescript\nconst { data, isPlaceholderData } = useQuery({\n  queryKey: ['posts', page],\n  queryFn: () => fetchPosts(page),\n  placeholderData: (previousData) => previousData  // Keep previous page while loading\n});\n\n// Or provide static placeholder\nplaceholderData: { posts: [], total: 0 }\n```\n\n## Optimistic Updates with Queries\n\nUpdate UI immediately, rollback on error:\n\n```typescript\nconst queryClient = useQueryClient();\n\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (newPost) => {\n    // Cancel outgoing queries\n    await queryClient.cancelQueries({ queryKey: ['post', newPost.id] });\n\n    // Snapshot current value\n    const previousPost = queryClient.getQueryData(['post', newPost.id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['post', newPost.id], newPost);\n\n    return { previousPost };\n  },\n  onError: (err, newPost, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['post', newPost.id], context?.previousPost);\n  },\n  onSettled: (newPost) => {\n    // Refetch after success or error\n    queryClient.invalidateQueries({ queryKey: ['post', newPost.id] });\n  }\n});\n```\n\n## Query Retries\n\nConfigure retry behavior:\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  retry: 3, // Retry 3 times\n  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff\n});\n```\n\n## Error Handling\n\nHandle query errors:\n\n```typescript\nconst { data, error, isError } = useQuery({\n  queryKey: ['post', postId],\n  queryFn: () => fetchPost(postId),\n  throwOnError: false  // Don't throw, just set error\n});\n\nif (isError) {\n  return <ErrorMessage error={error} />;\n}\n```\n\n## Best Practices\n\n1. **Use Suspense** - Better loading UX with React Suspense\n2. **Prefetch on Intent** - Preload data on hover/focus\n3. **Enable Queries Conditionally** - Use `enabled` option\n4. **Cancel on Unmount** - Use abort signals\n5. **Handle Errors Gracefully** - Show error states\n6. **Optimize with Placeholders** - Show previous data while loading\n","isInternal":false,"tokens":1403,"sizeBytes":5512},{"name":"mutation-patterns.md","path":".agents/skills/tanstack-query/resources/mutation-patterns.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/tanstack-query/resources/mutation-patterns.md","title":"Resources Skill","category":"anthropic-skill","format":"markdown","content":"# Complex Mutation Patterns\n\n## Basic Mutations\n\n```typescript\nconst { mutate, isPending, isError, error } = useMutation({\n  mutationFn: (newPost: CreatePostDto) => createPost(newPost),\n  onSuccess: (data) => {\n    console.log('Post created:', data);\n  },\n  onError: (error) => {\n    console.error('Failed to create post:', error);\n  }\n});\n\n// Trigger mutation\nmutate({ title: 'New Post', content: '...' });\n```\n\n## Optimistic Updates\n\nUpdate UI immediately, rollback on error:\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onMutate: async (newPost) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['posts'] });\n\n    // Snapshot previous value\n    const previousPosts = queryClient.getQueryData(['posts']);\n\n    // Optimistically update to the new value\n    queryClient.setQueryData(['posts'], (old) =>\n      old.map((post) => (post.id === newPost.id ? newPost : post))\n    );\n\n    // Return context with snapshot\n    return { previousPosts };\n  },\n  onError: (err, newPost, context) => {\n    // Rollback to previous value\n    queryClient.setQueryData(['posts'], context.previousPosts);\n  },\n  onSettled: () => {\n    // Always refetch after error or success\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n  }\n});\n```\n\n## Sequential Mutations\n\nRun mutations in sequence:\n\n```typescript\nconst createAndPublish = async (postData) => {\n  // Create post\n  const post = await createPostMutation.mutateAsync(postData);\n\n  // Publish post\n  const published = await publishPostMutation.mutateAsync(post.id);\n\n  return published;\n};\n```\n\n## Parallel Mutations\n\nRun multiple mutations simultaneously:\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: async (updates) => {\n    const results = await Promise.all([\n      updateProfile(updates.profile),\n      updateSettings(updates.settings),\n      updatePreferences(updates.preferences)\n    ]);\n    return results;\n  }\n});\n```\n\n## Mutation with Invalidation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: () => {\n    // Invalidate and refetch\n    queryClient.invalidateQueries({ queryKey: ['posts'] });\n\n    // Or update cache directly\n    queryClient.setQueryData(['posts'], (old) => [newPost, ...old]);\n  }\n});\n```\n\n## Mutation with Multiple Cache Updates\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: deletePost,\n  onSuccess: (_, deletedPostId) => {\n    // Update posts list\n    queryClient.setQueryData(['posts'], (old) => old.filter((post) => post.id !== deletedPostId));\n\n    // Update post count\n    queryClient.setQueryData(['postsCount'], (old) => old - 1);\n\n    // Invalidate related queries\n    queryClient.invalidateQueries({ queryKey: ['user', 'stats'] });\n  }\n});\n```\n\n## Error Handling\n\n```typescript\nconst { mutate, isError, error, reset } = useMutation({\n  mutationFn: createPost,\n  onError: (error) => {\n    if (error.code === 'VALIDATION_ERROR') {\n      setFormErrors(error.fields);\n    } else if (error.code === 'NETWORK_ERROR') {\n      showRetryDialog();\n    } else {\n      showGenericError();\n    }\n  }\n});\n\n// Clear error state\nreset();\n```\n\n## Retry Failed Mutations\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  retry: 3, // Retry 3 times on failure\n  retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000) // Exponential backoff\n});\n```\n\n## Mutation with Loading State\n\n```typescript\nfunction CreatePostForm() {\n  const { mutate, isPending } = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      navigate('/posts');\n    }\n  });\n\n  const handleSubmit = (data) => {\n    mutate(data);\n  };\n\n  return (\n    <form onSubmit={handleSubmit}>\n      {/* form fields */}\n      <button type=\"submit\" disabled={isPending}>\n        {isPending ? 'Creating...' : 'Create Post'}\n      </button>\n    </form>\n  );\n}\n```\n\n## Mutation with Variables\n\n```typescript\nconst { mutate, variables } = useMutation({\n  mutationFn: updatePost\n});\n\n// Access last mutation variables\nconsole.log('Last updated post:', variables);\n```\n\n## Mutation Callbacks\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onMutate: (variables) => {\n    console.log('Starting mutation with:', variables);\n  },\n  onSuccess: (data, variables, context) => {\n    console.log('Success!', data);\n  },\n  onError: (error, variables, context) => {\n    console.error('Error!', error);\n  },\n  onSettled: (data, error, variables, context) => {\n    console.log('Mutation finished (success or error)');\n  }\n});\n```\n\n## Mutation with Form Integration\n\n```typescript\nimport { useForm } from 'react-hook-form';\n\nfunction CreatePostForm() {\n  const { register, handleSubmit, reset } = useForm();\n\n  const { mutate, isPending, isError, error } = useMutation({\n    mutationFn: createPost,\n    onSuccess: () => {\n      reset();  // Clear form\n      toast.success('Post created!');\n    },\n    onError: (error) => {\n      toast.error(error.message);\n    }\n  });\n\n  const onSubmit = (data) => {\n    mutate(data);\n  };\n\n  return (\n    <form onSubmit={handleSubmit(onSubmit)}>\n      <input {...register('title')} />\n      <textarea {...register('content')} />\n      <button type=\"submit\" disabled={isPending}>\n        Submit\n      </button>\n      {isError && <ErrorMessage error={error} />}\n    </form>\n  );\n}\n```\n\n## Mutation State Reset\n\n```typescript\nconst { mutate, data, error, reset } = useMutation({\n  mutationFn: createPost\n});\n\n// Clear mutation state\nconst handleReset = () => {\n  reset(); // Clears data, error, status, etc.\n};\n```\n\n## Global Mutation Configuration\n\n```typescript\nconst queryClient = new QueryClient({\n  defaultOptions: {\n    mutations: {\n      retry: 3,\n      retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000),\n      onError: (error) => {\n        // Global error handler\n        console.error('Mutation error:', error);\n      }\n    }\n  }\n});\n```\n\n## Mutation Lifecycle\n\n```\n┌─────────────┐\n│ idle        │\n└──────┬──────┘\n       │ mutate()\n       ▼\n┌─────────────┐\n│ pending     │ ─── onMutate()\n└──────┬──────┘\n       │\n       ├─ success ──► onSuccess() ──┐\n       │                            │\n       └─ error ────► onError() ────┤\n                                    │\n                                    ▼\n                              onSettled()\n```\n\n## Best Practices\n\n1. **Use Optimistic Updates** - Better UX for fast operations\n2. **Always Handle Errors** - Show clear error messages\n3. **Invalidate Related Queries** - Keep cache in sync\n4. **Use onSettled** - For cleanup that runs regardless of success/error\n5. **Reset on Unmount** - Clear mutation state when component unmounts\n6. **Retry Network Errors** - Configure retry for transient failures\n7. **Show Loading States** - Disable buttons during mutation\n8. **Rollback on Error** - Revert optimistic updates if mutation fails\n\n## Common Patterns\n\n### Create with Redirect\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: createPost,\n  onSuccess: (newPost) => {\n    navigate(`/posts/${newPost.id}`);\n  }\n});\n```\n\n### Update with Toast\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: updatePost,\n  onSuccess: () => {\n    toast.success('Post updated!');\n  },\n  onError: () => {\n    toast.error('Failed to update post');\n  }\n});\n```\n\n### Delete with Confirmation\n\n```typescript\nconst { mutate } = useMutation({\n  mutationFn: deletePost,\n  onMutate: async () => {\n    const confirmed = await confirm('Are you sure?');\n    if (!confirmed) throw new Error('Cancelled');\n  },\n  onSuccess: () => {\n    toast.success('Post deleted');\n    navigate('/posts');\n  }\n});\n```\n","isInternal":false,"tokens":1890,"sizeBytes":7825},{"name":"skill-rules-fragment.json","path":".agents/skills/tanstack-query/skill-rules-fragment.json","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/tanstack-query/skill-rules-fragment.json","title":"Tanstack-query Skill","category":"anthropic-skill","format":"json","content":"{\n  \"tanstack-query\": {\n    \"type\": \"domain\",\n    \"enforcement\": \"suggest\",\n    \"priority\": \"high\",\n    \"promptTriggers\": {\n      \"keywords\": [\n        \"tanstack query\",\n        \"react query\",\n        \"@tanstack/react-query\",\n        \"useQuery\",\n        \"useMutation\",\n        \"useSuspenseQuery\",\n        \"useInfiniteQuery\",\n        \"useQueries\",\n        \"useQueryClient\",\n        \"useIsFetching\",\n        \"useIsMutating\",\n        \"useMutationState\",\n        \"QueryClient\",\n        \"QueryClientProvider\",\n        \"queryKey\",\n        \"queryFn\",\n        \"mutationFn\",\n        \"invalidateQueries\",\n        \"setQueryData\",\n        \"getQueryData\",\n        \"removeQueries\",\n        \"resetQueries\",\n        \"staleTime\",\n        \"cacheTime\",\n        \"refetchInterval\",\n        \"refetchOnWindowFocus\",\n        \"refetchOnReconnect\",\n        \"isLoading\",\n        \"isError\",\n        \"isFetching\",\n        \"isSuccess\",\n        \"queryCache\",\n        \"mutationCache\"\n      ],\n      \"intentPatterns\": [\n        \"use.*tanstack.*query\",\n        \"use.*react.*query\",\n        \"create.*tanstack.*(query|mutation)\",\n        \"invalidate.*query.*cache\",\n        \"invalidate.*tanstack.*queries\",\n        \"prefetch.*query\",\n        \"setup.*query.*client\",\n        \"configure.*tanstack.*query\",\n        \"use.*(useQuery|useMutation|useInfiniteQuery)\",\n        \"implement.*query.*invalidation\",\n        \"add.*query.*key\",\n        \"set.*query.*data\"\n      ]\n    },\n    \"fileTriggers\": {\n      \"pathPatterns\": [\n        \"**/api/**/*.ts\",\n        \"**/api/**/*.tsx\",\n        \"**/queries/**/*.ts\",\n        \"**/queries/**/*.tsx\",\n        \"**/hooks/**/*.ts\",\n        \"**/hooks/**/*.tsx\",\n        \"**/mutations/**/*.ts\",\n        \"**/mutations/**/*.tsx\"\n      ],\n      \"contentPatterns\": [\n        \"useQuery\\\\(\",\n        \"useMutation\\\\(\",\n        \"useSuspenseQuery\\\\(\",\n        \"useInfiniteQuery\\\\(\",\n        \"useQueryClient\\\\(\",\n        \"queryClient\\\\.\",\n        \"import.*@tanstack/react-query\",\n        \"from '@tanstack/react-query'\",\n        \"QueryClientProvider\",\n        \"new QueryClient\\\\(\",\n        \"invalidateQueries\\\\(\",\n        \"setQueryData\\\\(\",\n        \"queryKey:\",\n        \"queryFn:\"\n      ]\n    }\n  }\n}\n","frontmatter":{"tanstack-query":{"type":"domain","enforcement":"suggest","priority":"high","promptTriggers":{"keywords":["tanstack query","react query","@tanstack/react-query","useQuery","useMutation","useSuspenseQuery","useInfiniteQuery","useQueries","useQueryClient","useIsFetching","useIsMutating","useMutationState","QueryClient","QueryClientProvider","queryKey","queryFn","mutationFn","invalidateQueries","setQueryData","getQueryData","removeQueries","resetQueries","staleTime","cacheTime","refetchInterval","refetchOnWindowFocus","refetchOnReconnect","isLoading","isError","isFetching","isSuccess","queryCache","mutationCache"],"intentPatterns":["use.*tanstack.*query","use.*react.*query","create.*tanstack.*(query|mutation)","invalidate.*query.*cache","invalidate.*tanstack.*queries","prefetch.*query","setup.*query.*client","configure.*tanstack.*query","use.*(useQuery|useMutation|useInfiniteQuery)","implement.*query.*invalidation","add.*query.*key","set.*query.*data"]},"fileTriggers":{"pathPatterns":["**/api/**/*.ts","**/api/**/*.tsx","**/queries/**/*.ts","**/queries/**/*.tsx","**/hooks/**/*.ts","**/hooks/**/*.tsx","**/mutations/**/*.ts","**/mutations/**/*.tsx"],"contentPatterns":["useQuery\\(","useMutation\\(","useSuspenseQuery\\(","useInfiniteQuery\\(","useQueryClient\\(","queryClient\\.","import.*@tanstack/react-query","from '@tanstack/react-query'","QueryClientProvider","new QueryClient\\(","invalidateQueries\\(","setQueryData\\(","queryKey:","queryFn:"]}}},"isInternal":false,"tokens":535,"sizeBytes":2179},{"name":"SKILL.md","path":".agents/skills/tanstack-query/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/tanstack-query/SKILL.md","title":"tanstack-query","category":"anthropic-skill","format":"markdown","content":"---\nname: tanstack-query\ndescription: TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration. Use when fetching data, managing server state, or working with TanStack Query hooks.\n---\n\n# TanStack Query Patterns\n\n## Purpose\n\nModern data fetching with TanStack Query v5 (latest: 5.90.5, November 2025), emphasizing Suspense-based queries, cache-first strategies, and centralized API services.\n\n**Note**: v5 (released October 2023) has breaking changes from v4:\n\n- `isLoading` → `isPending` for status\n- `cacheTime` → `gcTime` (garbage collection time)\n- React 18.0+ required\n- Callbacks removed from useQuery (onError, onSuccess, onSettled)\n- `keepPreviousData` replaced with `placeholderData` function\n\n## When to Use This Skill\n\n- Fetching data with TanStack Query\n- Using useSuspenseQuery or useQuery\n- Managing mutations\n- Cache invalidation and updates\n- API service patterns\n\n---\n\n## Quick Start\n\n### Primary Pattern: useSuspenseQuery\n\nFor **all new components**, use `useSuspenseQuery`:\n\n```typescript\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { postsApi } from '~/features/posts/api/postsApi';\n\nfunction PostList() {\n  const { data: posts } = useSuspenseQuery({\n    queryKey: ['posts'],\n    queryFn: postsApi.getAll,\n  });\n\n  return (\n    <div>\n      {posts.map(post => (\n        <PostCard key={post.id} post={post} />\n      ))}\n    </div>\n  );\n}\n\n// Wrap with Suspense\n<Suspense fallback={<PostsSkeleton />}>\n  <PostList />\n</Suspense>\n```\n\n**Benefits:**\n\n- No `isLoading` checks needed\n- Integrates with Suspense boundaries\n- Cleaner component code\n- Consistent loading UX\n\n---\n\n## useSuspenseQuery Patterns\n\n### Basic Usage\n\n```typescript\nconst { data } = useSuspenseQuery({\n  queryKey: ['user', userId],\n  queryFn: () => userApi.get(userId),\n});\n\n// data is never undefined - guaranteed by Suspense\nreturn <div>{data.name}</div>;\n```\n\n### With Parameters\n\n```typescript\nfunction UserPosts({ userId }: { userId: string }) {\n  const { data: posts } = useSuspenseQuery({\n    queryKey: ['users', userId, 'posts'],\n    queryFn: () => postsApi.getByUser(userId),\n  });\n\n  return <div>{posts.length} posts</div>;\n}\n```\n\n### Dependent Queries\n\n```typescript\nfunction PostDetails({ postId }: { postId: string }) {\n  // First query\n  const { data: post } = useSuspenseQuery({\n    queryKey: ['posts', postId],\n    queryFn: () => postsApi.get(postId),\n  });\n\n  // Second query depends on first\n  const { data: author } = useSuspenseQuery({\n    queryKey: ['users', post.authorId],\n    queryFn: () => userApi.get(post.authorId),\n  });\n\n  return <div>{author.name} wrote {post.title}</div>;\n}\n```\n\n---\n\n## useQuery (Legacy Pattern)\n\nUse `useQuery` only when you need loading/error states in the component:\n\n```typescript\nimport { useQuery } from '@tanstack/react-query';\n\nfunction Component() {\n  const { data, isPending, error } = useQuery({\n    queryKey: ['posts'],\n    queryFn: postsApi.getAll,\n  });\n\n  if (isPending) return <Spinner />;\n  if (error) return <Error error={error} />;\n\n  return <div>{data.map(...)}</div>;\n}\n```\n\n**When to use `useQuery` vs `useSuspenseQuery`:**\n\n- Use `useSuspenseQuery` by default (preferred)\n- Use `useQuery` only when you need component-level loading states\n- Most cases should use `useSuspenseQuery` + Suspense boundaries\n\n---\n\n## Mutations\n\n### Basic Mutation\n\n```typescript\nimport { useMutation, useQueryClient } from '@tanstack/react-query';\n\nfunction CreatePostButton() {\n  const queryClient = useQueryClient();\n\n  const mutation = useMutation({\n    mutationFn: postsApi.create,\n    onSuccess: () => {\n      // Invalidate and refetch\n      queryClient.invalidateQueries({ queryKey: ['posts'] });\n    },\n  });\n\n  const handleCreate = () => {\n    mutation.mutate({\n      title: 'New Post',\n      content: 'Content here',\n    });\n  };\n\n  return (\n    <button onClick={handleCreate} disabled={mutation.isPending}>\n      {mutation.isPending ? 'Creating...' : 'Create Post'}\n    </button>\n  );\n}\n```\n\n### Optimistic Updates\n\n```typescript\nconst mutation = useMutation({\n  mutationFn: postsApi.update,\n  onMutate: async (updatedPost) => {\n    // Cancel outgoing refetches\n    await queryClient.cancelQueries({ queryKey: ['posts', updatedPost.id] });\n\n    // Snapshot previous value\n    const previousPost = queryClient.getQueryData(['posts', updatedPost.id]);\n\n    // Optimistically update\n    queryClient.setQueryData(['posts', updatedPost.id], updatedPost);\n\n    // Return context with snapshot\n    return { previousPost };\n  },\n  onError: (err, updatedPost, context) => {\n    // Rollback on error\n    queryClient.setQueryData(['posts', updatedPost.id], context.previousPost);\n  },\n  onSettled: (data, error, variables) => {\n    // Refetch after mutation\n    queryClient.invalidateQueries({ queryKey: ['posts', variables.id] });\n  }\n});\n```\n\n---\n\n## Cache Management\n\n### Invalidation\n\n```typescript\nimport { useQueryClient } from '@tanstack/react-query';\n\nconst queryClient = useQueryClient();\n\n// Invalidate all posts queries\nqueryClient.invalidateQueries({ queryKey: ['posts'] });\n\n// Invalidate specific post\nqueryClient.invalidateQueries({ queryKey: ['posts', postId] });\n\n// Invalidate all queries\nqueryClient.invalidateQueries();\n```\n\n### Manual Updates\n\n```typescript\n// Update cache directly\nqueryClient.setQueryData(['posts', postId], newPost);\n\n// Update with function\nqueryClient.setQueryData(['posts'], (oldPosts) => [...oldPosts, newPost]);\n```\n\n### Prefetching\n\n```typescript\n// Prefetch data\nawait queryClient.prefetchQuery({\n  queryKey: ['posts', postId],\n  queryFn: () => postsApi.get(postId),\n});\n\n// In a component\nconst prefetchPost = (postId: string) => {\n  queryClient.prefetchQuery({\n    queryKey: ['posts', postId],\n    queryFn: () => postsApi.get(postId),\n  });\n};\n\n<Link\n  to={`/posts/${post.id}`}\n  onMouseEnter={() => prefetchPost(post.id)}\n>\n  {post.title}\n</Link>\n```\n\n---\n\n## API Service Pattern\n\n### Centralized API Service\n\n```typescript\n// features/posts/api/postsApi.ts\nimport { apiClient } from '@/lib/apiClient';\nimport type { Post, CreatePostDto, UpdatePostDto } from '~/types/post';\n\nexport const postsApi = {\n  getAll: async (): Promise<Post[]> => {\n    const response = await apiClient.get('/posts');\n    return response.data;\n  },\n\n  get: async (id: string): Promise<Post> => {\n    const response = await apiClient.get(`/posts/${id}`);\n    return response.data;\n  },\n\n  create: async (data: CreatePostDto): Promise<Post> => {\n    const response = await apiClient.post('/posts', data);\n    return response.data;\n  },\n\n  update: async (id: string, data: UpdatePostDto): Promise<Post> => {\n    const response = await apiClient.put(`/posts/${id}`, data);\n    return response.data;\n  },\n\n  delete: async (id: string): Promise<void> => {\n    await apiClient.delete(`/posts/${id}`);\n  },\n\n  getByUser: async (userId: string): Promise<Post[]> => {\n    const response = await apiClient.get(`/users/${userId}/posts`);\n    return response.data;\n  }\n};\n```\n\n### Usage in Components\n\n```typescript\nimport { postsApi } from '~/features/posts/api/postsApi';\n\n// In query\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll\n});\n\n// In mutation\nconst mutation = useMutation({\n  mutationFn: postsApi.create\n});\n```\n\n---\n\n## Query Keys\n\n### Key Structure\n\n```typescript\n// List queries\n['posts'][('posts', { status: 'published' })][ // All posts // Filtered posts\n  // Detail queries\n  ('posts', postId)\n][('posts', postId, 'comments')][ // Single post // Post comments\n  // Nested resources\n  ('users', userId, 'posts')\n][('users', userId, 'posts', postId)]; // User's posts // Specific user post\n```\n\n### Key Factories\n\n```typescript\n// features/posts/api/postKeys.ts\nexport const postKeys = {\n  all: ['posts'] as const,\n  lists: () => [...postKeys.all, 'list'] as const,\n  list: (filters: string) => [...postKeys.lists(), { filters }] as const,\n  details: () => [...postKeys.all, 'detail'] as const,\n  detail: (id: string) => [...postKeys.details(), id] as const,\n  comments: (id: string) => [...postKeys.detail(id), 'comments'] as const\n};\n\n// Usage\nconst { data } = useSuspenseQuery({\n  queryKey: postKeys.detail(postId),\n  queryFn: () => postsApi.get(postId)\n});\n\n// Invalidate all post lists\nqueryClient.invalidateQueries({ queryKey: postKeys.lists() });\n```\n\n---\n\n## Error Handling\n\n### With Error Boundaries\n\n```typescript\nimport { ErrorBoundary } from 'react-error-boundary';\n\n<ErrorBoundary fallback={<ErrorFallback />}>\n  <Suspense fallback={<Loading />}>\n    <DataComponent />\n  </Suspense>\n</ErrorBoundary>\n\n// In component\nfunction DataComponent() {\n  const { data } = useSuspenseQuery({\n    queryKey: ['data'],\n    queryFn: fetchData,\n    // Errors automatically caught by ErrorBoundary\n  });\n\n  return <div>{data}</div>;\n}\n```\n\n### Retry and Cache Configuration\n\n```typescript\nconst { data } = useQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll,\n  retry: 3, // Retry 3 times\n  retryDelay: 1000, // Wait 1s between retries\n  gcTime: 5 * 60 * 1000 // Garbage collection time: 5 minutes (v5: was 'cacheTime')\n});\n```\n\n---\n\n## Best Practices\n\n### 1. Use Suspense by Default\n\n```typescript\n// ✅ Good: useSuspenseQuery + Suspense\n<Suspense fallback={<Skeleton />}>\n  <DataComponent />\n</Suspense>\n\nfunction DataComponent() {\n  const { data } = useSuspenseQuery({...});\n  return <div>{data}</div>;\n}\n\n// ❌ Avoid: useQuery with manual loading\nfunction DataComponent() {\n  const { data, isPending } = useQuery({...});\n  if (isPending) return <Spinner />;\n  return <div>{data}</div>;\n}\n```\n\n### 2. Consistent Query Keys\n\n```typescript\n// ✅ Good: Use key factories\nconst { data } = useSuspenseQuery({\n  queryKey: postKeys.detail(id),\n  queryFn: () => postsApi.get(id)\n});\n\n// ❌ Avoid: Inconsistent keys\nconst { data } = useSuspenseQuery({\n  queryKey: ['post', id], // Different format\n  queryFn: () => postsApi.get(id)\n});\n```\n\n### 3. Centralized API Services\n\n```typescript\n// ✅ Good: API service\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: postsApi.getAll\n});\n\n// ❌ Avoid: Inline fetching\nconst { data } = useSuspenseQuery({\n  queryKey: ['posts'],\n  queryFn: async () => {\n    const res = await fetch('/api/posts');\n    return res.json();\n  }\n});\n```\n\n---\n\n## Additional Resources\n\nFor more patterns, see:\n\n- [data-fetching.md](resources/data-fetching.md) - Advanced patterns\n- [cache-strategies.md](resources/cache-strategies.md) - Cache management\n- [mutation-patterns.md](resources/mutation-patterns.md) - Complex mutations\n","frontmatter":{"name":"tanstack-query","description":"TanStack Query v5 data fetching patterns including useSuspenseQuery, useQuery, mutations, cache management, and API service integration. Use when fetching data, managing server state, or working with TanStack Query hooks."},"isInternal":false,"tokens":2687,"sizeBytes":10583},{"name":"AGENTS.md","path":".agents/skills/vercel-composition-patterns/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/AGENTS.md","title":"Vercel-composition-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# React Composition Patterns\n\n**Version 1.0.0**  \nEngineering  \nJanuary 2026\n\n> **Note:**  \n> This document is mainly for agents and LLMs to follow when maintaining,  \n> generating, or refactoring React codebases using composition. Humans  \n> may also find it useful, but guidance here is optimized for automation  \n> and consistency by AI-assisted workflows.\n\n---\n\n## Abstract\n\nComposition patterns for building flexible, maintainable React components. Avoid boolean prop proliferation by using compound components, lifting state, and composing internals. These patterns make codebases easier for both humans and AI agents to work with as they scale.\n\n---\n\n## Table of Contents\n\n1. [Component Architecture](#1-component-architecture) — **HIGH**\n   - 1.1 [Avoid Boolean Prop Proliferation](#11-avoid-boolean-prop-proliferation)\n   - 1.2 [Use Compound Components](#12-use-compound-components)\n2. [State Management](#2-state-management) — **MEDIUM**\n   - 2.1 [Decouple State Management from UI](#21-decouple-state-management-from-ui)\n   - 2.2 [Define Generic Context Interfaces for Dependency Injection](#22-define-generic-context-interfaces-for-dependency-injection)\n   - 2.3 [Lift State into Provider Components](#23-lift-state-into-provider-components)\n3. [Implementation Patterns](#3-implementation-patterns) — **MEDIUM**\n   - 3.1 [Create Explicit Component Variants](#31-create-explicit-component-variants)\n   - 3.2 [Prefer Composing Children Over Render Props](#32-prefer-composing-children-over-render-props)\n4. [React 19 APIs](#4-react-19-apis) — **MEDIUM**\n   - 4.1 [React 19 API Changes](#41-react-19-api-changes)\n\n---\n\n## 1. Component Architecture\n\n**Impact: HIGH**\n\nFundamental patterns for structuring components to avoid prop\nproliferation and enable flexible composition.\n\n### 1.1 Avoid Boolean Prop Proliferation\n\n**Impact: CRITICAL (prevents unmaintainable component variants)**\n\nDon't add boolean props like `isThread`, `isEditing`, `isDMThread` to customize\n\ncomponent behavior. Each boolean doubles possible states and creates\n\nunmaintainable conditional logic. Use composition instead.\n\n**Incorrect: boolean props create exponential complexity**\n\n```tsx\nfunction Composer({\n  onSubmit,\n  isThread,\n  channelId,\n  isDMThread,\n  dmId,\n  isEditing,\n  isForwarding\n}: Props) {\n  return (\n    <form>\n      <Header />\n      <Input />\n      {isDMThread ? (\n        <AlsoSendToDMField id={dmId} />\n      ) : isThread ? (\n        <AlsoSendToChannelField id={channelId} />\n      ) : null}\n      {isEditing ? <EditActions /> : isForwarding ? <ForwardActions /> : <DefaultActions />}\n      <Footer onSubmit={onSubmit} />\n    </form>\n  );\n}\n```\n\n**Correct: composition eliminates conditionals**\n\n```tsx\n// Channel composer\nfunction ChannelComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Attachments />\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Thread composer - adds \"also send to channel\" field\nfunction ThreadComposer({ channelId }: { channelId: string }) {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <AlsoSendToChannelField id={channelId} />\n      <Composer.Footer>\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Edit composer - different footer actions\nfunction EditComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Formatting />\n        <Composer.Emojis />\n        <Composer.CancelEdit />\n        <Composer.SaveEdit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n```\n\nEach variant is explicit about what it renders. We can share internals without\n\nsharing a single monolithic parent.\n\n### 1.2 Use Compound Components\n\n**Impact: HIGH (enables flexible composition without prop drilling)**\n\nStructure complex components as compound components with a shared context. Each\n\nsubcomponent accesses shared state via context, not props. Consumers compose the\n\npieces they need.\n\n**Incorrect: monolithic component with render props**\n\n```tsx\nfunction Composer({\n  renderHeader,\n  renderFooter,\n  renderActions,\n  showAttachments,\n  showFormatting,\n  showEmojis\n}: Props) {\n  return (\n    <form>\n      {renderHeader?.()}\n      <Input />\n      {showAttachments && <Attachments />}\n      {renderFooter ? (\n        renderFooter()\n      ) : (\n        <Footer>\n          {showFormatting && <Formatting />}\n          {showEmojis && <Emojis />}\n          {renderActions?.()}\n        </Footer>\n      )}\n    </form>\n  );\n}\n```\n\n**Correct: compound components with shared context**\n\n```tsx\nconst ComposerContext = createContext<ComposerContextValue | null>(null);\n\nfunction ComposerProvider({ children, state, actions, meta }: ProviderProps) {\n  return <ComposerContext value={{ state, actions, meta }}>{children}</ComposerContext>;\n}\n\nfunction ComposerFrame({ children }: { children: React.ReactNode }) {\n  return <form>{children}</form>;\n}\n\nfunction ComposerInput() {\n  const {\n    state,\n    actions: { update },\n    meta: { inputRef }\n  } = use(ComposerContext);\n  return (\n    <TextInput\n      ref={inputRef}\n      value={state.input}\n      onChangeText={(text) => update((s) => ({ ...s, input: text }))}\n    />\n  );\n}\n\nfunction ComposerSubmit() {\n  const {\n    actions: { submit }\n  } = use(ComposerContext);\n  return <Button onPress={submit}>Send</Button>;\n}\n\n// Export as compound component\nconst Composer = {\n  Provider: ComposerProvider,\n  Frame: ComposerFrame,\n  Input: ComposerInput,\n  Submit: ComposerSubmit,\n  Header: ComposerHeader,\n  Footer: ComposerFooter,\n  Attachments: ComposerAttachments,\n  Formatting: ComposerFormatting,\n  Emojis: ComposerEmojis\n};\n```\n\n**Usage:**\n\n```tsx\n<Composer.Provider state={state} actions={actions} meta={meta}>\n  <Composer.Frame>\n    <Composer.Header />\n    <Composer.Input />\n    <Composer.Footer>\n      <Composer.Formatting />\n      <Composer.Submit />\n    </Composer.Footer>\n  </Composer.Frame>\n</Composer.Provider>\n```\n\nConsumers explicitly compose exactly what they need. No hidden conditionals. And the state, actions and meta are dependency-injected by a parent provider, allowing multiple usages of the same component structure.\n\n---\n\n## 2. State Management\n\n**Impact: MEDIUM**\n\nPatterns for lifting state and managing shared context across\ncomposed components.\n\n### 2.1 Decouple State Management from UI\n\n**Impact: MEDIUM (enables swapping state implementations without changing UI)**\n\nThe provider component should be the only place that knows how state is managed.\n\nUI components consume the context interface—they don't know if state comes from\n\nuseState, Zustand, or a server sync.\n\n**Incorrect: UI coupled to state implementation**\n\n```tsx\nfunction ChannelComposer({ channelId }: { channelId: string }) {\n  // UI component knows about global state implementation\n  const state = useGlobalChannelState(channelId);\n  const { submit, updateInput } = useChannelSync(channelId);\n\n  return (\n    <Composer.Frame>\n      <Composer.Input value={state.input} onChange={(text) => sync.updateInput(text)} />\n      <Composer.Submit onPress={() => sync.submit()} />\n    </Composer.Frame>\n  );\n}\n```\n\n**Correct: state management isolated in provider**\n\n```tsx\n// Provider handles all state management details\nfunction ChannelProvider({\n  channelId,\n  children\n}: {\n  channelId: string;\n  children: React.ReactNode;\n}) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n  const inputRef = useRef(null);\n\n  return (\n    <Composer.Provider state={state} actions={{ update, submit }} meta={{ inputRef }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n\n// UI component only knows about the context interface\nfunction ChannelComposer() {\n  return (\n    <Composer.Frame>\n      <Composer.Header />\n      <Composer.Input />\n      <Composer.Footer>\n        <Composer.Submit />\n      </Composer.Footer>\n    </Composer.Frame>\n  );\n}\n\n// Usage\nfunction Channel({ channelId }: { channelId: string }) {\n  return (\n    <ChannelProvider channelId={channelId}>\n      <ChannelComposer />\n    </ChannelProvider>\n  );\n}\n```\n\n**Different providers, same UI:**\n\n```tsx\n// Local state for ephemeral forms\nfunction ForwardMessageProvider({ children }) {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n\n  return (\n    <Composer.Provider state={state} actions={{ update: setState, submit: forwardMessage }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n\n// Global synced state for channels\nfunction ChannelProvider({ channelId, children }) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n\n  return (\n    <Composer.Provider state={state} actions={{ update, submit }}>\n      {children}\n    </Composer.Provider>\n  );\n}\n```\n\nThe same `Composer.Input` component works with both providers because it only\n\ndepends on the context interface, not the implementation.\n\n### 2.2 Define Generic Context Interfaces for Dependency Injection\n\n**Impact: HIGH (enables dependency-injectable state across use-cases)**\n\nDefine a **generic interface** for your component context with three parts:\n\n`state`, `actions`, and `meta`. This interface is a contract that any provider\n\ncan implement—enabling the same UI components to work with completely different\n\nstate implementations.\n\n**Core principle:** Lift state, compose internals, make state\n\ndependency-injectable.\n\n**Incorrect: UI coupled to specific state implementation**\n\n```tsx\nfunction ComposerInput() {\n  // Tightly coupled to a specific hook\n  const { input, setInput } = useChannelComposerState();\n  return <TextInput value={input} onChangeText={setInput} />;\n}\n```\n\n**Correct: generic interface enables dependency injection**\n\n```tsx\n// Define a GENERIC interface that any provider can implement\ninterface ComposerState {\n  input: string;\n  attachments: Attachment[];\n  isSubmitting: boolean;\n}\n\ninterface ComposerActions {\n  update: (updater: (state: ComposerState) => ComposerState) => void;\n  submit: () => void;\n}\n\ninterface ComposerMeta {\n  inputRef: React.RefObject<TextInput>;\n}\n\ninterface ComposerContextValue {\n  state: ComposerState;\n  actions: ComposerActions;\n  meta: ComposerMeta;\n}\n\nconst ComposerContext = createContext<ComposerContextValue | null>(null);\n```\n\n**UI components consume the interface, not the implementation:**\n\n```tsx\nfunction ComposerInput() {\n  const {\n    state,\n    actions: { update },\n    meta\n  } = use(ComposerContext);\n\n  // This component works with ANY provider that implements the interface\n  return (\n    <TextInput\n      ref={meta.inputRef}\n      value={state.input}\n      onChangeText={(text) => update((s) => ({ ...s, input: text }))}\n    />\n  );\n}\n```\n\n**Different providers implement the same interface:**\n\n```tsx\n// Provider A: Local state for ephemeral forms\nfunction ForwardMessageProvider({ children }: { children: React.ReactNode }) {\n  const [state, setState] = useState(initialState);\n  const inputRef = useRef(null);\n  const submit = useForwardMessage();\n\n  return (\n    <ComposerContext\n      value={{\n        state,\n        actions: { update: setState, submit },\n        meta: { inputRef }\n      }}\n    >\n      {children}\n    </ComposerContext>\n  );\n}\n\n// Provider B: Global synced state for channels\nfunction ChannelProvider({ channelId, children }: Props) {\n  const { state, update, submit } = useGlobalChannel(channelId);\n  const inputRef = useRef(null);\n\n  return (\n    <ComposerContext\n      value={{\n        state,\n        actions: { update, submit },\n        meta: { inputRef }\n      }}\n    >\n      {children}\n    </ComposerContext>\n  );\n}\n```\n\n**The same composed UI works with both:**\n\n```tsx\n// Works with ForwardMessageProvider (local state)\n<ForwardMessageProvider>\n  <Composer.Frame>\n    <Composer.Input />\n    <Composer.Submit />\n  </Composer.Frame>\n</ForwardMessageProvider>\n\n// Works with ChannelProvider (global synced state)\n<ChannelProvider channelId=\"abc\">\n  <Composer.Frame>\n    <Composer.Input />\n    <Composer.Submit />\n  </Composer.Frame>\n</ChannelProvider>\n```\n\n**Custom UI outside the component can access state and actions:**\n\n```tsx\nfunction ForwardMessageDialog() {\n  return (\n    <ForwardMessageProvider>\n      <Dialog>\n        {/* The composer UI */}\n        <Composer.Frame>\n          <Composer.Input placeholder=\"Add a message, if you'd like.\" />\n          <Composer.Footer>\n            <Composer.Formatting />\n            <Composer.Emojis />\n          </Composer.Footer>\n        </Composer.Frame>\n\n        {/* Custom UI OUTSIDE the composer, but INSIDE the provider */}\n        <MessagePreview />\n\n        {/* Actions at the bottom of the dialog */}\n        <DialogActions>\n          <CancelButton />\n          <ForwardButton />\n        </DialogActions>\n      </Dialog>\n    </ForwardMessageProvider>\n  );\n}\n\n// This button lives OUTSIDE Composer.Frame but can still submit based on its context!\nfunction ForwardButton() {\n  const {\n    actions: { submit }\n  } = use(ComposerContext);\n  return <Button onPress={submit}>Forward</Button>;\n}\n\n// This preview lives OUTSIDE Composer.Frame but can read composer's state!\nfunction MessagePreview() {\n  const { state } = use(ComposerContext);\n  return <Preview message={state.input} attachments={state.attachments} />;\n}\n```\n\nThe provider boundary is what matters—not the visual nesting. Components that\n\nneed shared state don't have to be inside the `Composer.Frame`. They just need\n\nto be within the provider.\n\nThe `ForwardButton` and `MessagePreview` are not visually inside the composer\n\nbox, but they can still access its state and actions. This is the power of\n\nlifting state into providers.\n\nThe UI is reusable bits you compose together. The state is dependency-injected\n\nby the provider. Swap the provider, keep the UI.\n\n### 2.3 Lift State into Provider Components\n\n**Impact: HIGH (enables state sharing outside component boundaries)**\n\nMove state management into dedicated provider components. This allows sibling\n\ncomponents outside the main UI to access and modify state without prop drilling\n\nor awkward refs.\n\n**Incorrect: state trapped inside component**\n\n```tsx\nfunction ForwardMessageComposer() {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n\n  return (\n    <Composer.Frame>\n      <Composer.Input />\n      <Composer.Footer />\n    </Composer.Frame>\n  );\n}\n\n// Problem: How does this button access composer state?\nfunction ForwardMessageDialog() {\n  return (\n    <Dialog>\n      <ForwardMessageComposer />\n      <MessagePreview /> {/* Needs composer state */}\n      <DialogActions>\n        <CancelButton />\n        <ForwardButton /> {/* Needs to call submit */}\n      </DialogActions>\n    </Dialog>\n  );\n}\n```\n\n**Incorrect: useEffect to sync state up**\n\n```tsx\nfunction ForwardMessageDialog() {\n  const [input, setInput] = useState('');\n  return (\n    <Dialog>\n      <ForwardMessageComposer onInputChange={setInput} />\n      <MessagePreview input={input} />\n    </Dialog>\n  );\n}\n\nfunction ForwardMessageComposer({ onInputChange }) {\n  const [state, setState] = useState(initialState);\n  useEffect(() => {\n    onInputChange(state.input); // Sync on every change 😬\n  }, [state.input]);\n}\n```\n\n**Incorrect: reading state from ref on submit**\n\n```tsx\nfunction ForwardMessageDialog() {\n  const stateRef = useRef(null);\n  return (\n    <Dialog>\n      <ForwardMessageComposer stateRef={stateRef} />\n      <ForwardButton onPress={() => submit(stateRef.current)} />\n    </Dialog>\n  );\n}\n```\n\n**Correct: state lifted to provider**\n\n```tsx\nfunction ForwardMessageProvider({ children }: { children: React.ReactNode }) {\n  const [state, setState] = useState(initialState);\n  const forwardMessage = useForwardMessage();\n  const inputRef = useRef(null);\n\n  return (\n    <Composer.Provider\n      state={state}\n      actions={{ update: setState, submit: forwardMessage }}\n      meta={{ inputRef }}\n    >\n      {children}\n    </Composer.Provider>\n  );\n}\n\nfunction ForwardMessageDialog() {\n  return (\n    <ForwardMessageProvider>\n      <Dialog>\n        <ForwardMessageComposer />\n        <MessagePreview /> {/* Custom components can access state and actions */}\n        <DialogActions>\n          <CancelButton />\n          <ForwardButton /> {/* Custom components can access state and actions */}\n        </DialogActions>\n      </Dialog>\n    </ForwardMessageProvider>\n  );\n}\n\nfunction ForwardButton() {\n  const { actions } = use(Composer.Context);\n  return <Button onPress={actions.submit}>Forward</Button>;\n}\n```\n\nThe ForwardButton lives outside the Composer.Frame but still has access to the\n\nsubmit action because it's within the provider. Even though it's a one-off\n\ncomponent, it can still access the composer's state and actions from outside the\n\nUI itself.\n\n**Key insight:** Components that need shared state don't have to be visually\n\nnested inside each other—they just need to be within the same provider.\n\n---\n\n## 3. Implementation Patterns\n\n**Impact: MEDIUM**\n\nSpecific techniques for implementing compound components and\ncontext providers.\n\n### 3.1 Create Explicit Component Variants\n\n**Impact: MEDIUM (self-documenting code, no hidden conditionals)**\n\nInstead of one component with many boolean props, create explicit variant\n\ncomponents. Each variant composes the pieces it needs. The code documents\n\nitself.\n\n**Incorrect: one component, many modes**\n\n```tsx\n// What does this component actually render?\n<Composer isThread isEditing={false} channelId='abc' showAttachments showFormatting={false} />\n```\n\n**Correct: explicit variants**\n\n```tsx\n// Immediately clear what this renders\n<ThreadComposer channelId=\"abc\" />\n\n// Or\n<EditMessageComposer messageId=\"xyz\" />\n\n// Or\n<ForwardMessageComposer messageId=\"123\" />\n```\n\nEach implementation is unique, explicit and self-contained. Yet they can each\n\nuse shared parts.\n\n**Implementation:**\n\n```tsx\nfunction ThreadComposer({ channelId }: { channelId: string }) {\n  return (\n    <ThreadProvider channelId={channelId}>\n      <Composer.Frame>\n        <Composer.Input />\n        <AlsoSendToChannelField channelId={channelId} />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.Submit />\n        </Composer.Footer>\n      </Composer.Frame>\n    </ThreadProvider>\n  );\n}\n\nfunction EditMessageComposer({ messageId }: { messageId: string }) {\n  return (\n    <EditMessageProvider messageId={messageId}>\n      <Composer.Frame>\n        <Composer.Input />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.CancelEdit />\n          <Composer.SaveEdit />\n        </Composer.Footer>\n      </Composer.Frame>\n    </EditMessageProvider>\n  );\n}\n\nfunction ForwardMessageComposer({ messageId }: { messageId: string }) {\n  return (\n    <ForwardMessageProvider messageId={messageId}>\n      <Composer.Frame>\n        <Composer.Input placeholder=\"Add a message, if you'd like.\" />\n        <Composer.Footer>\n          <Composer.Formatting />\n          <Composer.Emojis />\n          <Composer.Mentions />\n        </Composer.Footer>\n      </Composer.Frame>\n    </ForwardMessageProvider>\n  );\n}\n```\n\nEach variant is explicit about:\n\n- What provider/state it uses\n\n- What UI elements it includes\n\n- What actions are available\n\nNo boolean prop combinations to reason about. No impossible states.\n\n### 3.2 Prefer Composing Children Over Render Props\n\n**Impact: MEDIUM (cleaner composition, better readability)**\n\nUse `children` for composition instead of `renderX` props. Children are more\n\nreadable, compose naturally, and don't require understanding callback\n\nsignatures.\n\n**Incorrect: render props**\n\n```tsx\nfunction Composer({\n  renderHeader,\n  renderFooter,\n  renderActions\n}: {\n  renderHeader?: () => React.ReactNode;\n  renderFooter?: () => React.ReactNode;\n  renderActions?: () => React.ReactNode;\n}) {\n  return (\n    <form>\n      {renderHeader?.()}\n      <Input />\n      {renderFooter ? renderFooter() : <DefaultFooter />}\n      {renderActions?.()}\n    </form>\n  );\n}\n\n// Usage is awkward and inflexible\nreturn (\n  <Composer\n    renderHeader={() => <CustomHeader />}\n    renderFooter={() => (\n      <>\n        <Formatting />\n        <Emojis />\n      </>\n    )}\n    renderActions={() => <SubmitButton />}\n  />\n);\n```\n\n**Correct: compound components with children**\n\n```tsx\nfunction ComposerFrame({ children }: { children: React.ReactNode }) {\n  return <form>{children}</form>;\n}\n\nfunction ComposerFooter({ children }: { children: React.ReactNode }) {\n  return <footer className='flex'>{children}</footer>;\n}\n\n// Usage is flexible\nreturn (\n  <Composer.Frame>\n    <CustomHeader />\n    <Composer.Input />\n    <Composer.Footer>\n      <Composer.Formatting />\n      <Composer.Emojis />\n      <SubmitButton />\n    </Composer.Footer>\n  </Composer.Frame>\n);\n```\n\n**When render props are appropriate:**\n\n```tsx\n// Render props work well when you need to pass data back\n<List data={items} renderItem={({ item, index }) => <Item item={item} index={index} />} />\n```\n\nUse render props when the parent needs to provide data or state to the child.\n\nUse children when composing static structure.\n\n---\n\n## 4. React 19 APIs\n\n**Impact: MEDIUM**\n\nReact 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.\n\n### 4.1 React 19 API Changes\n\n**Impact: MEDIUM (cleaner component definitions and context usage)**\n\n> **⚠️ React 19+ only.** Skip this if you're on React 18 or earlier.\n\nIn React 19, `ref` is now a regular prop (no `forwardRef` wrapper needed), and `use()` replaces `useContext()`.\n\n**Incorrect: forwardRef in React 19**\n\n```tsx\nconst ComposerInput = forwardRef<TextInput, Props>((props, ref) => {\n  return <TextInput ref={ref} {...props} />;\n});\n```\n\n**Correct: ref as a regular prop**\n\n```tsx\nfunction ComposerInput({ ref, ...props }: Props & { ref?: React.Ref<TextInput> }) {\n  return <TextInput ref={ref} {...props} />;\n}\n```\n\n**Incorrect: useContext in React 19**\n\n```tsx\nconst value = useContext(MyContext);\n```\n\n**Correct: use instead of useContext**\n\n```tsx\nconst value = use(MyContext);\n```\n\n`use()` can also be called conditionally, unlike `useContext()`.\n\n---\n\n## References\n\n1. [https://react.dev](https://react.dev)\n2. [https://react.dev/learn/passing-data-deeply-with-context](https://react.dev/learn/passing-data-deeply-with-context)\n3. [https://react.dev/reference/react/use](https://react.dev/reference/react/use)\n","isInternal":false,"tokens":5017,"sizeBytes":22552},{"name":"README.md","path":".agents/skills/vercel-composition-patterns/README.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/README.md","title":"Vercel-composition-patterns Skill","category":"anthropic-skill","format":"markdown","content":"# React Composition Patterns\n\nA structured repository for React composition patterns that scale. These\npatterns help avoid boolean prop proliferation by using compound components,\nlifting state, and composing internals.\n\n## Structure\n\n- `rules/` - Individual rule files (one per rule)\n  - `_sections.md` - Section metadata (titles, impacts, descriptions)\n  - `_template.md` - Template for creating new rules\n  - `area-description.md` - Individual rule files\n- `metadata.json` - Document metadata (version, organization, abstract)\n- **`AGENTS.md`** - Compiled output (generated)\n\n## Rules\n\n### Component Architecture (CRITICAL)\n\n- `architecture-avoid-boolean-props.md` - Don't add boolean props to customize\n  behavior\n- `architecture-compound-components.md` - Structure as compound components with\n  shared context\n\n### State Management (HIGH)\n\n- `state-lift-state.md` - Lift state into provider components\n- `state-context-interface.md` - Define clear context interfaces\n  (state/actions/meta)\n- `state-decouple-implementation.md` - Decouple state management from UI\n\n### Implementation Patterns (MEDIUM)\n\n- `patterns-children-over-render-props.md` - Prefer children over renderX props\n- `patterns-explicit-variants.md` - Create explicit component variants\n\n## Core Principles\n\n1. **Composition over configuration** — Instead of adding props, let consumers\n   compose\n2. **Lift your state** — State in providers, not trapped in components\n3. **Compose your internals** — Subcomponents access context, not props\n4. **Explicit variants** — Create ThreadComposer, EditComposer, not Composer\n   with isThread\n\n## Creating a New Rule\n\n1. Copy `rules/_template.md` to `rules/area-description.md`\n2. Choose the appropriate area prefix:\n   - `architecture-` for Component Architecture\n   - `state-` for State Management\n   - `patterns-` for Implementation Patterns\n3. Fill in the frontmatter and content\n4. Ensure you have clear examples with explanations\n\n## Impact Levels\n\n- `CRITICAL` - Foundational patterns, prevents unmaintainable code\n- `HIGH` - Significant maintainability improvements\n- `MEDIUM` - Good practices for cleaner code\n","isInternal":false,"tokens":476,"sizeBytes":2140},{"name":"_sections.md","path":".agents/skills/vercel-composition-patterns/rules/_sections.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/_sections.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"# Sections\n\nThis file defines all sections, their ordering, impact levels, and descriptions.\nThe section ID (in parentheses) is the filename prefix used to group rules.\n\n---\n\n## 1. Component Architecture (architecture)\n\n**Impact:** HIGH  \n**Description:** Fundamental patterns for structuring components to avoid prop\nproliferation and enable flexible composition.\n\n## 2. State Management (state)\n\n**Impact:** MEDIUM  \n**Description:** Patterns for lifting state and managing shared context across\ncomposed components.\n\n## 3. Implementation Patterns (patterns)\n\n**Impact:** MEDIUM  \n**Description:** Specific techniques for implementing compound components and\ncontext providers.\n\n## 4. React 19 APIs (react19)\n\n**Impact:** MEDIUM  \n**Description:** React 19+ only. Don't use `forwardRef`; use `use()` instead of `useContext()`.\n","isInternal":false,"tokens":176,"sizeBytes":829},{"name":"_template.md","path":".agents/skills/vercel-composition-patterns/rules/_template.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/_template.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"---\ntitle: Rule Title Here\nimpact: MEDIUM\nimpactDescription: brief description of impact\ntags: composition, components\n---\n\n## Rule Title Here\n\nBrief explanation of the rule and why it matters.\n\n**Incorrect:**\n\n```tsx\n// Bad code example\n```\n\n**Correct:**\n\n```tsx\n// Good code example\n```\n\nReference: [Link](https://example.com)\n","frontmatter":{"title":"Rule Title Here","impact":"MEDIUM","impactDescription":"brief description of impact","tags":"composition, components"},"isInternal":false,"tokens":80,"sizeBytes":329},{"name":"architecture-avoid-boolean-props.md","path":".agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-avoid-boolean-props.md -->","frontmatter":{"title":"Avoid Boolean Prop Proliferation","impact":"CRITICAL","impactDescription":"prevents unmaintainable component variants","tags":"composition, props, architecture"},"isInternal":false,"tokens":501,"sizeBytes":2216},{"name":"architecture-compound-components.md","path":".agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/architecture-compound-components.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-compound-components.md -->","frontmatter":{"title":"Use Compound Components","impact":"HIGH","impactDescription":"enables flexible composition without prop drilling","tags":"composition, compound-components, architecture"},"isInternal":false,"tokens":570,"sizeBytes":2583},{"name":"patterns-children-over-render-props.md","path":".agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-children-over-render-props.md -->","frontmatter":{"title":"Prefer Composing Children Over Render Props","impact":"MEDIUM","impactDescription":"cleaner composition, better readability","tags":"composition, children, render-props"},"isInternal":false,"tokens":425,"sizeBytes":1889},{"name":"patterns-explicit-variants.md","path":".agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-explicit-variants.md -->","frontmatter":{"title":"Create Explicit Component Variants","impact":"MEDIUM","impactDescription":"self-documenting code, no hidden conditionals","tags":"composition, variants, architecture"},"isInternal":false,"tokens":495,"sizeBytes":2388},{"name":"react19-no-forwardref.md","path":".agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/react19-no-forwardref.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Freact19-no-forwardref.md -->","frontmatter":{"title":"React 19 API Changes","impact":"MEDIUM","impactDescription":"cleaner component definitions and context usage","tags":"react19, refs, context, hooks"},"isInternal":false,"tokens":255,"sizeBytes":958},{"name":"state-context-interface.md","path":".agents/skills/vercel-composition-patterns/rules/state-context-interface.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/state-context-interface.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-context-interface.md -->","frontmatter":{"title":"Define Generic Context Interfaces for Dependency Injection","impact":"HIGH","impactDescription":"enables dependency-injectable state across use-cases","tags":"composition, context, state, typescript, dependency-injection"},"isInternal":false,"tokens":1076,"sizeBytes":4996},{"name":"state-decouple-implementation.md","path":".agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/state-decouple-implementation.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-decouple-implementation.md -->","frontmatter":{"title":"Decouple State Management from UI","impact":"MEDIUM","impactDescription":"enables swapping state implementations without changing UI","tags":"composition, state, architecture"},"isInternal":false,"tokens":564,"sizeBytes":2651},{"name":"state-lift-state.md","path":".agents/skills/vercel-composition-patterns/rules/state-lift-state.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/rules/state-lift-state.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-lift-state.md -->","frontmatter":{"title":"Lift State into Provider Components","impact":"HIGH","impactDescription":"enables state sharing outside component boundaries","tags":"composition, state, context, providers"},"isInternal":false,"tokens":686,"sizeBytes":3242},{"name":"SKILL.md","path":".agents/skills/vercel-composition-patterns/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-composition-patterns/SKILL.md","title":"vercel-composition-patterns","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-composition-patterns%2FSKILL.md -->","frontmatter":{"name":"vercel-composition-patterns","description":"React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.","license":"MIT","metadata":{"author":"vercel","version":"1.0.0"}},"isInternal":false,"tokens":629,"sizeBytes":2884},{"name":"AGENTS.md","path":".agents/skills/vercel-react-best-practices/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/AGENTS.md","title":"Vercel-react-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FAGENTS.md -->","isInternal":false,"tokens":11951,"sizeBytes":50060},{"name":"README.md","path":".agents/skills/vercel-react-best-practices/README.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/README.md","title":"Vercel-react-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FREADME.md -->","isInternal":false,"tokens":852,"sizeBytes":3369},{"name":"_sections.md","path":".agents/skills/vercel-react-best-practices/rules/_sections.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/_sections.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2F_sections.md -->","isInternal":false,"tokens":318,"sizeBytes":1554},{"name":"_template.md","path":".agents/skills/vercel-react-best-practices/rules/_template.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/_template.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2F_template.md -->","frontmatter":{"title":"Rule Title Here","impact":"MEDIUM","impactDescription":"Optional description of impact (e.g., \"20-50% improvement\")","tags":"tag1, tag2"},"isInternal":false,"tokens":143,"sizeBytes":633},{"name":"advanced-event-handler-refs.md","path":".agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-event-handler-refs.md -->","frontmatter":{"title":"Store Event Handlers in Refs","impact":"LOW","impactDescription":"stable subscriptions","tags":"advanced, hooks, refs, event-handlers, optimization"},"isInternal":false,"tokens":337,"sizeBytes":1498},{"name":"advanced-init-once.md","path":".agents/skills/vercel-react-best-practices/rules/advanced-init-once.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/advanced-init-once.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-init-once.md -->","frontmatter":{"title":"Initialize App Once, Not Per Mount","impact":"LOW-MEDIUM","impactDescription":"avoids duplicate init in development","tags":"initialization, useEffect, app-startup, side-effects"},"isInternal":false,"tokens":230,"sizeBytes":967},{"name":"advanced-use-latest.md","path":".agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/advanced-use-latest.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-use-latest.md -->","frontmatter":{"title":"useEffectEvent for Stable Callback Refs","impact":"LOW","impactDescription":"prevents effect re-runs","tags":"advanced, hooks, useEffectEvent, refs, optimization"},"isInternal":false,"tokens":253,"sizeBytes":1081},{"name":"async-api-routes.md","path":".agents/skills/vercel-react-best-practices/rules/async-api-routes.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/async-api-routes.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-api-routes.md -->","frontmatter":{"title":"Prevent Waterfall Chains in API Routes","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"api-routes, server-actions, waterfalls, parallelization"},"isInternal":false,"tokens":237,"sizeBytes":1122},{"name":"async-defer-await.md","path":".agents/skills/vercel-react-best-practices/rules/async-defer-await.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/async-defer-await.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-defer-await.md -->","frontmatter":{"title":"Defer Await Until Needed","impact":"HIGH","impactDescription":"avoids blocking unused code paths","tags":"async, await, conditional, optimization"},"isInternal":false,"tokens":427,"sizeBytes":2024},{"name":"async-dependencies.md","path":".agents/skills/vercel-react-best-practices/rules/async-dependencies.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/async-dependencies.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-dependencies.md -->","frontmatter":{"title":"Dependency-Based Parallelization","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"async, parallelization, dependencies, better-all"},"isInternal":false,"tokens":290,"sizeBytes":1303},{"name":"async-parallel.md","path":".agents/skills/vercel-react-best-practices/rules/async-parallel.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/async-parallel.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-parallel.md -->","frontmatter":{"title":"Promise.all() for Independent Operations","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"async, parallelization, promises, waterfalls"},"isInternal":false,"tokens":140,"sizeBytes":650},{"name":"async-suspense-boundaries.md","path":".agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-suspense-boundaries.md -->","frontmatter":{"title":"Strategic Suspense Boundaries","impact":"HIGH","impactDescription":"faster initial paint","tags":"async, suspense, streaming, layout-shift"},"isInternal":false,"tokens":590,"sizeBytes":2517},{"name":"bundle-barrel-imports.md","path":".agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-barrel-imports.md -->","frontmatter":{"title":"Avoid Barrel File Imports","impact":"CRITICAL","impactDescription":"200-800ms import cost, slow builds","tags":"bundle, imports, tree-shaking, barrel-files, performance"},"isInternal":false,"tokens":595,"sizeBytes":2379},{"name":"bundle-conditional.md","path":".agents/skills/vercel-react-best-practices/rules/bundle-conditional.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/bundle-conditional.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-conditional.md -->","frontmatter":{"title":"Conditional Module Loading","impact":"HIGH","impactDescription":"loads large data only when needed","tags":"bundle, conditional-loading, lazy-loading"},"isInternal":false,"tokens":220,"sizeBytes":965},{"name":"bundle-defer-third-party.md","path":".agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-defer-third-party.md -->","frontmatter":{"title":"Defer Non-Critical Third-Party Libraries","impact":"MEDIUM","impactDescription":"loads after hydration","tags":"bundle, third-party, analytics, defer"},"isInternal":false,"tokens":221,"sizeBytes":923},{"name":"bundle-dynamic-imports.md","path":".agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-dynamic-imports.md -->","frontmatter":{"title":"Dynamic Imports for Heavy Components","impact":"CRITICAL","impactDescription":"directly affects TTI and LCP","tags":"bundle, dynamic-import, code-splitting, next-dynamic"},"isInternal":false,"tokens":197,"sizeBytes":794},{"name":"bundle-preload.md","path":".agents/skills/vercel-react-best-practices/rules/bundle-preload.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/bundle-preload.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-preload.md -->","frontmatter":{"title":"Preload Based on User Intent","impact":"MEDIUM","impactDescription":"reduces perceived latency","tags":"bundle, preload, user-intent, hover"},"isInternal":false,"tokens":259,"sizeBytes":1126},{"name":"client-event-listeners.md","path":".agents/skills/vercel-react-best-practices/rules/client-event-listeners.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/client-event-listeners.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-event-listeners.md -->","frontmatter":{"title":"Deduplicate Global Event Listeners","impact":"LOW","impactDescription":"single listener for N components","tags":"client, swr, event-listeners, subscription"},"isInternal":false,"tokens":470,"sizeBytes":2003},{"name":"client-localstorage-schema.md","path":".agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/client-localstorage-schema.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-localstorage-schema.md -->","frontmatter":{"title":"Version and Minimize localStorage Data","impact":"MEDIUM","impactDescription":"prevents schema conflicts, reduces storage size","tags":"client, localStorage, storage, versioning, data-minimization"},"isInternal":false,"tokens":442,"sizeBytes":1986},{"name":"client-passive-event-listeners.md","path":".agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-passive-event-listeners.md -->","frontmatter":{"title":"Use Passive Event Listeners for Scrolling Performance","impact":"MEDIUM","impactDescription":"eliminates scroll delay caused by event listeners","tags":"client, event-listeners, scrolling, performance, touch, wheel"},"isInternal":false,"tokens":357,"sizeBytes":1652},{"name":"client-swr-dedup.md","path":".agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/client-swr-dedup.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-swr-dedup.md -->","frontmatter":{"title":"Use SWR for Automatic Deduplication","impact":"MEDIUM-HIGH","impactDescription":"automatic deduplication","tags":"client, swr, deduplication, data-fetching"},"isInternal":false,"tokens":310,"sizeBytes":1171},{"name":"js-batch-dom-css.md","path":".agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-batch-dom-css.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-batch-dom-css.md -->","frontmatter":{"title":"Avoid Layout Thrashing","impact":"MEDIUM","impactDescription":"prevents forced synchronous layouts and reduces performance bottlenecks","tags":"javascript, dom, css, performance, reflow, layout-thrashing"},"isInternal":false,"tokens":747,"sizeBytes":3266},{"name":"js-cache-function-results.md","path":".agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-cache-function-results.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-function-results.md -->","frontmatter":{"title":"Cache Repeated Function Calls","impact":"MEDIUM","impactDescription":"avoid redundant computation","tags":"javascript, cache, memoization, performance"},"isInternal":false,"tokens":445,"sizeBytes":1936},{"name":"js-cache-property-access.md","path":".agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-cache-property-access.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-property-access.md -->","frontmatter":{"title":"Cache Property Access in Loops","impact":"LOW-MEDIUM","impactDescription":"reduces lookups","tags":"javascript, loops, optimization, caching"},"isInternal":false,"tokens":138,"sizeBytes":536},{"name":"js-cache-storage.md","path":".agents/skills/vercel-react-best-practices/rules/js-cache-storage.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-cache-storage.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-storage.md -->","frontmatter":{"title":"Cache Storage API Calls","impact":"LOW-MEDIUM","impactDescription":"reduces expensive I/O","tags":"javascript, localStorage, storage, caching, performance"},"isInternal":false,"tokens":374,"sizeBytes":1653},{"name":"js-combine-iterations.md","path":".agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-combine-iterations.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-combine-iterations.md -->","frontmatter":{"title":"Combine Multiple Array Iterations","impact":"LOW-MEDIUM","impactDescription":"reduces iterations","tags":"javascript, arrays, loops, performance"},"isInternal":false,"tokens":182,"sizeBytes":768},{"name":"js-early-exit.md","path":".agents/skills/vercel-react-best-practices/rules/js-early-exit.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-early-exit.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-early-exit.md -->","frontmatter":{"title":"Early Return from Functions","impact":"LOW-MEDIUM","impactDescription":"avoids unnecessary computation","tags":"javascript, functions, optimization, early-return"},"isInternal":false,"tokens":264,"sizeBytes":1139},{"name":"js-flatmap-filter.md","path":".agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-flatmap-filter.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-flatmap-filter.md -->","frontmatter":{"title":"Use flatMap to Map and Filter in One Pass","impact":"LOW-MEDIUM","impactDescription":"eliminates intermediate array","tags":"javascript, arrays, flatMap, filter, performance"},"isInternal":false,"tokens":343,"sizeBytes":1418},{"name":"js-hoist-regexp.md","path":".agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-hoist-regexp.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-hoist-regexp.md -->","frontmatter":{"title":"Hoist RegExp Creation","impact":"LOW-MEDIUM","impactDescription":"avoids recreation","tags":"javascript, regexp, optimization, memoization"},"isInternal":false,"tokens":281,"sizeBytes":1029},{"name":"js-index-maps.md","path":".agents/skills/vercel-react-best-practices/rules/js-index-maps.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-index-maps.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-index-maps.md -->","frontmatter":{"title":"Build Index Maps for Repeated Lookups","impact":"LOW-MEDIUM","impactDescription":"1M ops to 2K ops","tags":"javascript, map, indexing, optimization, performance"},"isInternal":false,"tokens":237,"sizeBytes":848},{"name":"js-length-check-first.md","path":".agents/skills/vercel-react-best-practices/rules/js-length-check-first.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-length-check-first.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-length-check-first.md -->","frontmatter":{"title":"Early Length Check for Array Comparisons","impact":"MEDIUM-HIGH","impactDescription":"avoids expensive operations when lengths differ","tags":"javascript, arrays, performance, optimization, comparison"},"isInternal":false,"tokens":373,"sizeBytes":1754},{"name":"js-min-max-loop.md","path":".agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-min-max-loop.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-min-max-loop.md -->","frontmatter":{"title":"Use Loop for Min/Max Instead of Sort","impact":"LOW","impactDescription":"O(n) instead of O(n log n)","tags":"javascript, arrays, performance, sorting, algorithms"},"isInternal":false,"tokens":597,"sizeBytes":2298},{"name":"js-set-map-lookups.md","path":".agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-set-map-lookups.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-set-map-lookups.md -->","frontmatter":{"title":"Use Set/Map for O(1) Lookups","impact":"LOW-MEDIUM","impactDescription":"O(n) to O(1)","tags":"javascript, set, map, data-structures, performance"},"isInternal":false,"tokens":154,"sizeBytes":532},{"name":"js-tosorted-immutable.md","path":".agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-tosorted-immutable.md -->","frontmatter":{"title":"Use toSorted() Instead of sort() for Immutability","impact":"MEDIUM-HIGH","impactDescription":"prevents mutation bugs in React state","tags":"javascript, arrays, immutability, react, state, mutation"},"isInternal":false,"tokens":436,"sizeBytes":1783},{"name":"rendering-activity.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-activity.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-activity.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-activity.md -->","frontmatter":{"title":"Use Activity Component for Show/Hide","impact":"MEDIUM","impactDescription":"preserves state/DOM","tags":"rendering, activity, visibility, state-preservation"},"isInternal":false,"tokens":131,"sizeBytes":566},{"name":"rendering-animate-svg-wrapper.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-animate-svg-wrapper.md -->","frontmatter":{"title":"Animate SVG Wrapper Instead of SVG Element","impact":"LOW","impactDescription":"enables hardware acceleration","tags":"rendering, svg, css, animation, performance"},"isInternal":false,"tokens":274,"sizeBytes":1121},{"name":"rendering-conditional-render.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-conditional-render.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-conditional-render.md -->","frontmatter":{"title":"Use Explicit Conditional Rendering","impact":"LOW","impactDescription":"prevents rendering 0 or NaN","tags":"rendering, conditional, jsx, falsy-values"},"isInternal":false,"tokens":257,"sizeBytes":938},{"name":"rendering-content-visibility.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-content-visibility.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-content-visibility.md -->","frontmatter":{"title":"CSS content-visibility for Long Lists","impact":"HIGH","impactDescription":"faster initial render","tags":"rendering, css, content-visibility, long-lists"},"isInternal":false,"tokens":205,"sizeBytes":818},{"name":"rendering-hoist-jsx.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hoist-jsx.md -->","frontmatter":{"title":"Hoist Static JSX Elements","impact":"LOW","impactDescription":"avoids re-creation","tags":"rendering, jsx, static, optimization"},"isInternal":false,"tokens":227,"sizeBytes":993},{"name":"rendering-hydration-no-flicker.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-no-flicker.md -->","frontmatter":{"title":"Prevent Hydration Mismatch Without Flickering","impact":"MEDIUM","impactDescription":"avoids visual flicker and hydration errors","tags":"rendering, ssr, hydration, localStorage, flicker"},"isInternal":false,"tokens":467,"sizeBytes":2249},{"name":"rendering-hydration-suppress-warning.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-suppress-warning.md -->","frontmatter":{"title":"Suppress Expected Hydration Mismatches","impact":"LOW-MEDIUM","impactDescription":"avoids noisy hydration warnings for known differences","tags":"rendering, hydration, ssr, nextjs"},"isInternal":false,"tokens":189,"sizeBytes":852},{"name":"rendering-resource-hints.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-resource-hints.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-resource-hints.md -->","frontmatter":{"title":"Use React DOM Resource Hints","impact":"HIGH","impactDescription":"reduces load time for critical resources","tags":"rendering, preload, preconnect, prefetch, resource-hints"},"isInternal":false,"tokens":633,"sizeBytes":2689},{"name":"rendering-script-defer-async.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-script-defer-async.md -->","frontmatter":{"title":"Use defer or async on Script Tags","impact":"HIGH","impactDescription":"eliminates render-blocking","tags":"rendering, script, defer, async, performance"},"isInternal":false,"tokens":430,"sizeBytes":1872},{"name":"rendering-svg-precision.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-svg-precision.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-svg-precision.md -->","frontmatter":{"title":"Optimize SVG Precision","impact":"LOW","impactDescription":"reduces file size","tags":"rendering, svg, optimization, svgo"},"isInternal":false,"tokens":167,"sizeBytes":588},{"name":"rendering-usetransition-loading.md","path":".agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-usetransition-loading.md -->","frontmatter":{"title":"Use useTransition Over Manual Loading States","impact":"LOW","impactDescription":"reduces re-renders and improves code clarity","tags":"rendering, transitions, useTransition, loading, state"},"isInternal":false,"tokens":457,"sizeBytes":2090},{"name":"rerender-defer-reads.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-defer-reads.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-defer-reads.md -->","frontmatter":{"title":"Defer State Reads to Usage Point","impact":"MEDIUM","impactDescription":"avoids unnecessary subscriptions","tags":"rerender, searchParams, localStorage, optimization"},"isInternal":false,"tokens":231,"sizeBytes":983},{"name":"rerender-dependencies.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-dependencies.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-dependencies.md -->","frontmatter":{"title":"Narrow Effect Dependencies","impact":"LOW","impactDescription":"minimizes effect re-runs","tags":"rerender, useEffect, dependencies, optimization"},"isInternal":false,"tokens":214,"sizeBytes":833},{"name":"rerender-derived-state-no-effect.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state-no-effect.md -->","frontmatter":{"title":"Calculate Derived State During Rendering","impact":"MEDIUM","impactDescription":"avoids redundant renders and state drift","tags":"rerender, derived-state, useEffect, state"},"isInternal":false,"tokens":282,"sizeBytes":1211},{"name":"rerender-derived-state.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-derived-state.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state.md -->","frontmatter":{"title":"Subscribe to Derived State","impact":"MEDIUM","impactDescription":"reduces re-render frequency","tags":"rerender, derived-state, media-query, optimization"},"isInternal":false,"tokens":174,"sizeBytes":732},{"name":"rerender-functional-setstate.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-functional-setstate.md -->","frontmatter":{"title":"Use Functional setState Updates","impact":"MEDIUM","impactDescription":"prevents stale closures and unnecessary callback recreations","tags":"react, hooks, useState, useCallback, callbacks, closures"},"isInternal":false,"tokens":663,"sizeBytes":2988},{"name":"rerender-lazy-state-init.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-lazy-state-init.md -->","frontmatter":{"title":"Use Lazy State Initialization","impact":"MEDIUM","impactDescription":"wasted computation on every render","tags":"react, hooks, useState, performance, initialization"},"isInternal":false,"tokens":430,"sizeBytes":2012},{"name":"rerender-memo-with-default-value.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo-with-default-value.md -->","frontmatter":{"title":"Extract Default Non-primitive Parameter Value from Memoized Component to Constant","impact":"MEDIUM","impactDescription":"restores memoization by using a constant for default value","tags":"rerender, memo, optimization"},"isInternal":false,"tokens":248,"sizeBytes":1171},{"name":"rerender-memo.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-memo.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-memo.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo.md -->","frontmatter":{"title":"Extract to Memoized Components","impact":"MEDIUM","impactDescription":"enables early returns","tags":"rerender, memo, useMemo, optimization"},"isInternal":false,"tokens":278,"sizeBytes":1158},{"name":"rerender-move-effect-to-event.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-move-effect-to-event.md -->","frontmatter":{"title":"Put Interaction Logic in Event Handlers","impact":"MEDIUM","impactDescription":"avoids effect re-runs and duplicate side effects","tags":"rerender, useEffect, events, side-effects, dependencies"},"isInternal":false,"tokens":285,"sizeBytes":1278},{"name":"rerender-no-inline-components.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-no-inline-components.md -->","frontmatter":{"title":"Don't Define Components Inside Components","impact":"HIGH","impactDescription":"prevents remount on every render","tags":"rerender, components, remount, performance"},"isInternal":false,"tokens":511,"sizeBytes":2100},{"name":"rerender-simple-expression-in-memo.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->","frontmatter":{"title":"Do not wrap a simple expression with a primitive result type in useMemo","impact":"LOW-MEDIUM","impactDescription":"wasted computation on every render","tags":"rerender, useMemo, optimization"},"isInternal":false,"tokens":217,"sizeBytes":1023},{"name":"rerender-split-combined-hooks.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-split-combined-hooks.md -->","frontmatter":{"title":"Split Combined Hook Computations","impact":"MEDIUM","impactDescription":"avoids recomputing independent steps","tags":"rerender, useMemo, useEffect, dependencies, optimization"},"isInternal":false,"tokens":421,"sizeBytes":1857},{"name":"rerender-transitions.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-transitions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-transitions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-transitions.md -->","frontmatter":{"title":"Use Transitions for Non-Urgent Updates","impact":"MEDIUM","impactDescription":"maintains UI responsiveness","tags":"rerender, transitions, startTransition, performance"},"isInternal":false,"tokens":241,"sizeBytes":1067},{"name":"rerender-use-deferred-value.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-deferred-value.md -->","frontmatter":{"title":"Use useDeferredValue for Expensive Derived Renders","impact":"MEDIUM","impactDescription":"keeps input responsive during heavy computation","tags":"rerender, useDeferredValue, optimization, concurrent"},"isInternal":false,"tokens":427,"sizeBytes":1862},{"name":"rerender-use-ref-transient-values.md","path":".agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-ref-transient-values.md -->","frontmatter":{"title":"Use useRef for Transient Values","impact":"MEDIUM","impactDescription":"avoids unnecessary re-renders on frequent updates","tags":"rerender, useref, state, performance"},"isInternal":false,"tokens":437,"sizeBytes":1756},{"name":"server-after-nonblocking.md","path":".agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-after-nonblocking.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-after-nonblocking.md -->","frontmatter":{"title":"Use after() for Non-Blocking Operations","impact":"MEDIUM","impactDescription":"faster response times","tags":"server, async, logging, analytics, side-effects"},"isInternal":false,"tokens":453,"sizeBytes":2014},{"name":"server-auth-actions.md","path":".agents/skills/vercel-react-best-practices/rules/server-auth-actions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-auth-actions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-auth-actions.md -->","frontmatter":{"title":"Authenticate Server Actions Like API Routes","impact":"CRITICAL","impactDescription":"prevents unauthorized access to server mutations","tags":"server, server-actions, authentication, security, authorization"},"isInternal":false,"tokens":578,"sizeBytes":2656},{"name":"server-cache-lru.md","path":".agents/skills/vercel-react-best-practices/rules/server-cache-lru.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-cache-lru.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-lru.md -->","frontmatter":{"title":"Cross-Request LRU Caching","impact":"HIGH","impactDescription":"caches across requests","tags":"server, cache, lru, cross-request"},"isInternal":false,"tokens":325,"sizeBytes":1359},{"name":"server-cache-react.md","path":".agents/skills/vercel-react-best-practices/rules/server-cache-react.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-cache-react.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-react.md -->","frontmatter":{"title":"Per-Request Deduplication with React.cache()","impact":"MEDIUM","impactDescription":"deduplicates within request","tags":"server, cache, react-cache, deduplication"},"isInternal":false,"tokens":509,"sizeBytes":2240},{"name":"server-dedup-props.md","path":".agents/skills/vercel-react-best-practices/rules/server-dedup-props.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-dedup-props.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-dedup-props.md -->","frontmatter":{"title":"Avoid Duplicate Serialization in RSC Props","impact":"LOW","impactDescription":"reduces network payload by avoiding duplicate serialization","tags":"server, rsc, serialization, props, client-components"},"isInternal":false,"tokens":501,"sizeBytes":2065},{"name":"server-hoist-static-io.md","path":".agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-hoist-static-io.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-hoist-static-io.md -->","frontmatter":{"title":"Hoist Static I/O to Module Level","impact":"HIGH","impactDescription":"avoids repeated file/network I/O per request","tags":"server, io, performance, next.js, route-handlers, og-image"},"isInternal":false,"tokens":1020,"sizeBytes":4371},{"name":"server-parallel-fetching.md","path":".agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-parallel-fetching.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-parallel-fetching.md -->","frontmatter":{"title":"Parallel Data Fetching with Component Composition","impact":"CRITICAL","impactDescription":"eliminates server-side waterfalls","tags":"server, rsc, parallel-fetching, composition"},"isInternal":false,"tokens":365,"sizeBytes":1569},{"name":"server-serialization.md","path":".agents/skills/vercel-react-best-practices/rules/server-serialization.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/rules/server-serialization.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-serialization.md -->","frontmatter":{"title":"Minimize Serialization at RSC Boundaries","impact":"HIGH","impactDescription":"reduces data transfer size","tags":"server, rsc, serialization, props"},"isInternal":false,"tokens":242,"sizeBytes":1006},{"name":"SKILL.md","path":".agents/skills/vercel-react-best-practices/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/vercel-react-best-practices/SKILL.md","title":"vercel-react-best-practices","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fvercel-react-best-practices%2FSKILL.md -->","frontmatter":{"name":"vercel-react-best-practices","description":"React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.","license":"MIT","metadata":{"author":"vercel","version":"1.0.0"}},"isInternal":false,"tokens":1654,"sizeBytes":6883},{"name":"SKILL.md","path":".agents/skills/web-design-guidelines/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.agents/skills/web-design-guidelines/SKILL.md","title":"web-design-guidelines","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.agents%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","frontmatter":{"name":"web-design-guidelines","description":"Review UI code for Web Interface Guidelines compliance. Use when asked to \"review my UI\", \"check accessibility\", \"audit design\", \"review UX\", or \"check my site against best practices\".","metadata":{"author":"vercel","version":"1.0.0","argument-hint":"<file-or-pattern>"}},"isInternal":false,"tokens":268,"sizeBytes":1232},{"name":"theming-guide.md","path":".claude/skills/kiranism-shadcn-dashboard/references/theming-guide.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/kiranism-shadcn-dashboard/references/theming-guide.md","title":"References Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fkiranism-shadcn-dashboard%2Freferences%2Ftheming-guide.md -->","isInternal":false,"tokens":1559,"sizeBytes":5290},{"name":"SKILL.md","path":".claude/skills/kiranism-shadcn-dashboard/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/kiranism-shadcn-dashboard/SKILL.md","title":"kiranism-shadcn-dashboard","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fkiranism-shadcn-dashboard%2FSKILL.md -->","frontmatter":{"name":"kiranism-shadcn-dashboard","description":"Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention \"dashboard\" explicitly — if they're adding UI, pages, or features to this project, use this skill.\n"},"isInternal":false,"tokens":2930,"sizeBytes":12335},{"name":"async-patterns.md","path":".claude/skills/next-best-practices/async-patterns.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/async-patterns.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fasync-patterns.md -->","isInternal":false,"tokens":413,"sizeBytes":1677},{"name":"bundling.md","path":".claude/skills/next-best-practices/bundling.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/bundling.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fbundling.md -->","isInternal":false,"tokens":1131,"sizeBytes":4772},{"name":"data-patterns.md","path":".claude/skills/next-best-practices/data-patterns.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/data-patterns.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdata-patterns.md -->","isInternal":false,"tokens":1636,"sizeBytes":6880},{"name":"debug-tricks.md","path":".claude/skills/next-best-practices/debug-tricks.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/debug-tricks.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdebug-tricks.md -->","isInternal":false,"tokens":781,"sizeBytes":2970},{"name":"directives.md","path":".claude/skills/next-best-practices/directives.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/directives.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fdirectives.md -->","isInternal":false,"tokens":351,"sizeBytes":1486},{"name":"error-handling.md","path":".claude/skills/next-best-practices/error-handling.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/error-handling.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ferror-handling.md -->","isInternal":false,"tokens":1197,"sizeBytes":4762},{"name":"file-conventions.md","path":".claude/skills/next-best-practices/file-conventions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/file-conventions.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffile-conventions.md -->","isInternal":false,"tokens":979,"sizeBytes":3767},{"name":"font.md","path":".claude/skills/next-best-practices/font.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/font.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffont.md -->","isInternal":false,"tokens":1317,"sizeBytes":5206},{"name":"functions.md","path":".claude/skills/next-best-practices/functions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/functions.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Ffunctions.md -->","isInternal":false,"tokens":1103,"sizeBytes":5961},{"name":"hydration-error.md","path":".claude/skills/next-best-practices/hydration-error.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/hydration-error.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fhydration-error.md -->","isInternal":false,"tokens":431,"sizeBytes":1791},{"name":"image.md","path":".claude/skills/next-best-practices/image.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/image.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fimage.md -->","isInternal":false,"tokens":1027,"sizeBytes":3885},{"name":"metadata.md","path":".claude/skills/next-best-practices/metadata.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/metadata.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fmetadata.md -->","isInternal":false,"tokens":1809,"sizeBytes":7490},{"name":"parallel-routes.md","path":".claude/skills/next-best-practices/parallel-routes.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/parallel-routes.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fparallel-routes.md -->","isInternal":false,"tokens":1840,"sizeBytes":7452},{"name":"route-handlers.md","path":".claude/skills/next-best-practices/route-handlers.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/route-handlers.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Froute-handlers.md -->","isInternal":false,"tokens":836,"sizeBytes":3449},{"name":"rsc-boundaries.md","path":".claude/skills/next-best-practices/rsc-boundaries.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/rsc-boundaries.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Frsc-boundaries.md -->","isInternal":false,"tokens":1062,"sizeBytes":4707},{"name":"runtime-selection.md","path":".claude/skills/next-best-practices/runtime-selection.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/runtime-selection.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fruntime-selection.md -->","isInternal":false,"tokens":220,"sizeBytes":986},{"name":"scripts.md","path":".claude/skills/next-best-practices/scripts.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/scripts.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fscripts.md -->","isInternal":false,"tokens":882,"sizeBytes":3657},{"name":"self-hosting.md","path":".claude/skills/next-best-practices/self-hosting.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/self-hosting.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fself-hosting.md -->","isInternal":false,"tokens":2173,"sizeBytes":8817},{"name":"SKILL.md","path":".claude/skills/next-best-practices/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/SKILL.md","title":"next-best-practices","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2FSKILL.md -->","frontmatter":{"name":"next-best-practices","description":"Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling","user-invocable":false},"isInternal":false,"tokens":1011,"sizeBytes":4022},{"name":"suspense-boundaries.md","path":".claude/skills/next-best-practices/suspense-boundaries.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/next-best-practices/suspense-boundaries.md","title":"Next-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fnext-best-practices%2Fsuspense-boundaries.md -->","isInternal":false,"tokens":340,"sizeBytes":1482},{"name":"AGENTS.md","path":".claude/skills/vercel-composition-patterns/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/AGENTS.md","title":"Vercel-composition-patterns Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FAGENTS.md -->","isInternal":false,"tokens":5017,"sizeBytes":22552},{"name":"README.md","path":".claude/skills/vercel-composition-patterns/README.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/README.md","title":"Vercel-composition-patterns Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FREADME.md -->","isInternal":false,"tokens":476,"sizeBytes":2140},{"name":"_sections.md","path":".claude/skills/vercel-composition-patterns/rules/_sections.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/_sections.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2F_sections.md -->","isInternal":false,"tokens":176,"sizeBytes":829},{"name":"_template.md","path":".claude/skills/vercel-composition-patterns/rules/_template.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/_template.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2F_template.md -->","frontmatter":{"title":"Rule Title Here","impact":"MEDIUM","impactDescription":"brief description of impact","tags":"composition, components"},"isInternal":false,"tokens":80,"sizeBytes":329},{"name":"architecture-avoid-boolean-props.md","path":".claude/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/architecture-avoid-boolean-props.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-avoid-boolean-props.md -->","frontmatter":{"title":"Avoid Boolean Prop Proliferation","impact":"CRITICAL","impactDescription":"prevents unmaintainable component variants","tags":"composition, props, architecture"},"isInternal":false,"tokens":501,"sizeBytes":2216},{"name":"architecture-compound-components.md","path":".claude/skills/vercel-composition-patterns/rules/architecture-compound-components.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/architecture-compound-components.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Farchitecture-compound-components.md -->","frontmatter":{"title":"Use Compound Components","impact":"HIGH","impactDescription":"enables flexible composition without prop drilling","tags":"composition, compound-components, architecture"},"isInternal":false,"tokens":570,"sizeBytes":2583},{"name":"patterns-children-over-render-props.md","path":".claude/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/patterns-children-over-render-props.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-children-over-render-props.md -->","frontmatter":{"title":"Prefer Composing Children Over Render Props","impact":"MEDIUM","impactDescription":"cleaner composition, better readability","tags":"composition, children, render-props"},"isInternal":false,"tokens":425,"sizeBytes":1889},{"name":"patterns-explicit-variants.md","path":".claude/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/patterns-explicit-variants.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fpatterns-explicit-variants.md -->","frontmatter":{"title":"Create Explicit Component Variants","impact":"MEDIUM","impactDescription":"self-documenting code, no hidden conditionals","tags":"composition, variants, architecture"},"isInternal":false,"tokens":495,"sizeBytes":2388},{"name":"react19-no-forwardref.md","path":".claude/skills/vercel-composition-patterns/rules/react19-no-forwardref.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/react19-no-forwardref.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Freact19-no-forwardref.md -->","frontmatter":{"title":"React 19 API Changes","impact":"MEDIUM","impactDescription":"cleaner component definitions and context usage","tags":"react19, refs, context, hooks"},"isInternal":false,"tokens":255,"sizeBytes":958},{"name":"state-context-interface.md","path":".claude/skills/vercel-composition-patterns/rules/state-context-interface.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/state-context-interface.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-context-interface.md -->","frontmatter":{"title":"Define Generic Context Interfaces for Dependency Injection","impact":"HIGH","impactDescription":"enables dependency-injectable state across use-cases","tags":"composition, context, state, typescript, dependency-injection"},"isInternal":false,"tokens":1076,"sizeBytes":4996},{"name":"state-decouple-implementation.md","path":".claude/skills/vercel-composition-patterns/rules/state-decouple-implementation.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/state-decouple-implementation.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-decouple-implementation.md -->","frontmatter":{"title":"Decouple State Management from UI","impact":"MEDIUM","impactDescription":"enables swapping state implementations without changing UI","tags":"composition, state, architecture"},"isInternal":false,"tokens":564,"sizeBytes":2651},{"name":"state-lift-state.md","path":".claude/skills/vercel-composition-patterns/rules/state-lift-state.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/rules/state-lift-state.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2Frules%2Fstate-lift-state.md -->","frontmatter":{"title":"Lift State into Provider Components","impact":"HIGH","impactDescription":"enables state sharing outside component boundaries","tags":"composition, state, context, providers"},"isInternal":false,"tokens":686,"sizeBytes":3242},{"name":"SKILL.md","path":".claude/skills/vercel-composition-patterns/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-composition-patterns/SKILL.md","title":"vercel-composition-patterns","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-composition-patterns%2FSKILL.md -->","frontmatter":{"name":"vercel-composition-patterns","description":"React composition patterns that scale. Use when refactoring components with boolean prop proliferation, building flexible component libraries, or designing reusable APIs. Triggers on tasks involving compound components, render props, context providers, or component architecture. Includes React 19 API changes.","license":"MIT","metadata":{"author":"vercel","version":"1.0.0"}},"isInternal":false,"tokens":629,"sizeBytes":2884},{"name":"AGENTS.md","path":".claude/skills/vercel-react-best-practices/AGENTS.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/AGENTS.md","title":"Vercel-react-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FAGENTS.md -->","isInternal":false,"tokens":11951,"sizeBytes":50060},{"name":"README.md","path":".claude/skills/vercel-react-best-practices/README.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/README.md","title":"Vercel-react-best-practices Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FREADME.md -->","isInternal":false,"tokens":852,"sizeBytes":3369},{"name":"_sections.md","path":".claude/skills/vercel-react-best-practices/rules/_sections.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/_sections.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2F_sections.md -->","isInternal":false,"tokens":318,"sizeBytes":1554},{"name":"_template.md","path":".claude/skills/vercel-react-best-practices/rules/_template.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/_template.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2F_template.md -->","frontmatter":{"title":"Rule Title Here","impact":"MEDIUM","impactDescription":"Optional description of impact (e.g., \"20-50% improvement\")","tags":"tag1, tag2"},"isInternal":false,"tokens":143,"sizeBytes":633},{"name":"advanced-event-handler-refs.md","path":".claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/advanced-event-handler-refs.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-event-handler-refs.md -->","frontmatter":{"title":"Store Event Handlers in Refs","impact":"LOW","impactDescription":"stable subscriptions","tags":"advanced, hooks, refs, event-handlers, optimization"},"isInternal":false,"tokens":337,"sizeBytes":1498},{"name":"advanced-init-once.md","path":".claude/skills/vercel-react-best-practices/rules/advanced-init-once.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/advanced-init-once.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-init-once.md -->","frontmatter":{"title":"Initialize App Once, Not Per Mount","impact":"LOW-MEDIUM","impactDescription":"avoids duplicate init in development","tags":"initialization, useEffect, app-startup, side-effects"},"isInternal":false,"tokens":230,"sizeBytes":967},{"name":"advanced-use-latest.md","path":".claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/advanced-use-latest.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fadvanced-use-latest.md -->","frontmatter":{"title":"useEffectEvent for Stable Callback Refs","impact":"LOW","impactDescription":"prevents effect re-runs","tags":"advanced, hooks, useEffectEvent, refs, optimization"},"isInternal":false,"tokens":253,"sizeBytes":1081},{"name":"async-api-routes.md","path":".claude/skills/vercel-react-best-practices/rules/async-api-routes.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/async-api-routes.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-api-routes.md -->","frontmatter":{"title":"Prevent Waterfall Chains in API Routes","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"api-routes, server-actions, waterfalls, parallelization"},"isInternal":false,"tokens":237,"sizeBytes":1122},{"name":"async-defer-await.md","path":".claude/skills/vercel-react-best-practices/rules/async-defer-await.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/async-defer-await.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-defer-await.md -->","frontmatter":{"title":"Defer Await Until Needed","impact":"HIGH","impactDescription":"avoids blocking unused code paths","tags":"async, await, conditional, optimization"},"isInternal":false,"tokens":427,"sizeBytes":2024},{"name":"async-dependencies.md","path":".claude/skills/vercel-react-best-practices/rules/async-dependencies.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/async-dependencies.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-dependencies.md -->","frontmatter":{"title":"Dependency-Based Parallelization","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"async, parallelization, dependencies, better-all"},"isInternal":false,"tokens":290,"sizeBytes":1303},{"name":"async-parallel.md","path":".claude/skills/vercel-react-best-practices/rules/async-parallel.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/async-parallel.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-parallel.md -->","frontmatter":{"title":"Promise.all() for Independent Operations","impact":"CRITICAL","impactDescription":"2-10× improvement","tags":"async, parallelization, promises, waterfalls"},"isInternal":false,"tokens":140,"sizeBytes":650},{"name":"async-suspense-boundaries.md","path":".claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/async-suspense-boundaries.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fasync-suspense-boundaries.md -->","frontmatter":{"title":"Strategic Suspense Boundaries","impact":"HIGH","impactDescription":"faster initial paint","tags":"async, suspense, streaming, layout-shift"},"isInternal":false,"tokens":590,"sizeBytes":2517},{"name":"bundle-barrel-imports.md","path":".claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/bundle-barrel-imports.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-barrel-imports.md -->","frontmatter":{"title":"Avoid Barrel File Imports","impact":"CRITICAL","impactDescription":"200-800ms import cost, slow builds","tags":"bundle, imports, tree-shaking, barrel-files, performance"},"isInternal":false,"tokens":595,"sizeBytes":2379},{"name":"bundle-conditional.md","path":".claude/skills/vercel-react-best-practices/rules/bundle-conditional.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/bundle-conditional.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-conditional.md -->","frontmatter":{"title":"Conditional Module Loading","impact":"HIGH","impactDescription":"loads large data only when needed","tags":"bundle, conditional-loading, lazy-loading"},"isInternal":false,"tokens":220,"sizeBytes":965},{"name":"bundle-defer-third-party.md","path":".claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/bundle-defer-third-party.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-defer-third-party.md -->","frontmatter":{"title":"Defer Non-Critical Third-Party Libraries","impact":"MEDIUM","impactDescription":"loads after hydration","tags":"bundle, third-party, analytics, defer"},"isInternal":false,"tokens":221,"sizeBytes":923},{"name":"bundle-dynamic-imports.md","path":".claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/bundle-dynamic-imports.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-dynamic-imports.md -->","frontmatter":{"title":"Dynamic Imports for Heavy Components","impact":"CRITICAL","impactDescription":"directly affects TTI and LCP","tags":"bundle, dynamic-import, code-splitting, next-dynamic"},"isInternal":false,"tokens":197,"sizeBytes":794},{"name":"bundle-preload.md","path":".claude/skills/vercel-react-best-practices/rules/bundle-preload.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/bundle-preload.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fbundle-preload.md -->","frontmatter":{"title":"Preload Based on User Intent","impact":"MEDIUM","impactDescription":"reduces perceived latency","tags":"bundle, preload, user-intent, hover"},"isInternal":false,"tokens":259,"sizeBytes":1126},{"name":"client-event-listeners.md","path":".claude/skills/vercel-react-best-practices/rules/client-event-listeners.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/client-event-listeners.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-event-listeners.md -->","frontmatter":{"title":"Deduplicate Global Event Listeners","impact":"LOW","impactDescription":"single listener for N components","tags":"client, swr, event-listeners, subscription"},"isInternal":false,"tokens":470,"sizeBytes":2003},{"name":"client-localstorage-schema.md","path":".claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/client-localstorage-schema.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-localstorage-schema.md -->","frontmatter":{"title":"Version and Minimize localStorage Data","impact":"MEDIUM","impactDescription":"prevents schema conflicts, reduces storage size","tags":"client, localStorage, storage, versioning, data-minimization"},"isInternal":false,"tokens":442,"sizeBytes":1986},{"name":"client-passive-event-listeners.md","path":".claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/client-passive-event-listeners.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-passive-event-listeners.md -->","frontmatter":{"title":"Use Passive Event Listeners for Scrolling Performance","impact":"MEDIUM","impactDescription":"eliminates scroll delay caused by event listeners","tags":"client, event-listeners, scrolling, performance, touch, wheel"},"isInternal":false,"tokens":357,"sizeBytes":1652},{"name":"client-swr-dedup.md","path":".claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/client-swr-dedup.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fclient-swr-dedup.md -->","frontmatter":{"title":"Use SWR for Automatic Deduplication","impact":"MEDIUM-HIGH","impactDescription":"automatic deduplication","tags":"client, swr, deduplication, data-fetching"},"isInternal":false,"tokens":310,"sizeBytes":1171},{"name":"js-batch-dom-css.md","path":".claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-batch-dom-css.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-batch-dom-css.md -->","frontmatter":{"title":"Avoid Layout Thrashing","impact":"MEDIUM","impactDescription":"prevents forced synchronous layouts and reduces performance bottlenecks","tags":"javascript, dom, css, performance, reflow, layout-thrashing"},"isInternal":false,"tokens":747,"sizeBytes":3266},{"name":"js-cache-function-results.md","path":".claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-cache-function-results.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-function-results.md -->","frontmatter":{"title":"Cache Repeated Function Calls","impact":"MEDIUM","impactDescription":"avoid redundant computation","tags":"javascript, cache, memoization, performance"},"isInternal":false,"tokens":445,"sizeBytes":1936},{"name":"js-cache-property-access.md","path":".claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-cache-property-access.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-property-access.md -->","frontmatter":{"title":"Cache Property Access in Loops","impact":"LOW-MEDIUM","impactDescription":"reduces lookups","tags":"javascript, loops, optimization, caching"},"isInternal":false,"tokens":138,"sizeBytes":536},{"name":"js-cache-storage.md","path":".claude/skills/vercel-react-best-practices/rules/js-cache-storage.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-cache-storage.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-cache-storage.md -->","frontmatter":{"title":"Cache Storage API Calls","impact":"LOW-MEDIUM","impactDescription":"reduces expensive I/O","tags":"javascript, localStorage, storage, caching, performance"},"isInternal":false,"tokens":374,"sizeBytes":1653},{"name":"js-combine-iterations.md","path":".claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-combine-iterations.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-combine-iterations.md -->","frontmatter":{"title":"Combine Multiple Array Iterations","impact":"LOW-MEDIUM","impactDescription":"reduces iterations","tags":"javascript, arrays, loops, performance"},"isInternal":false,"tokens":182,"sizeBytes":768},{"name":"js-early-exit.md","path":".claude/skills/vercel-react-best-practices/rules/js-early-exit.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-early-exit.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-early-exit.md -->","frontmatter":{"title":"Early Return from Functions","impact":"LOW-MEDIUM","impactDescription":"avoids unnecessary computation","tags":"javascript, functions, optimization, early-return"},"isInternal":false,"tokens":264,"sizeBytes":1139},{"name":"js-flatmap-filter.md","path":".claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-flatmap-filter.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-flatmap-filter.md -->","frontmatter":{"title":"Use flatMap to Map and Filter in One Pass","impact":"LOW-MEDIUM","impactDescription":"eliminates intermediate array","tags":"javascript, arrays, flatMap, filter, performance"},"isInternal":false,"tokens":343,"sizeBytes":1418},{"name":"js-hoist-regexp.md","path":".claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-hoist-regexp.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-hoist-regexp.md -->","frontmatter":{"title":"Hoist RegExp Creation","impact":"LOW-MEDIUM","impactDescription":"avoids recreation","tags":"javascript, regexp, optimization, memoization"},"isInternal":false,"tokens":281,"sizeBytes":1029},{"name":"js-index-maps.md","path":".claude/skills/vercel-react-best-practices/rules/js-index-maps.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-index-maps.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-index-maps.md -->","frontmatter":{"title":"Build Index Maps for Repeated Lookups","impact":"LOW-MEDIUM","impactDescription":"1M ops to 2K ops","tags":"javascript, map, indexing, optimization, performance"},"isInternal":false,"tokens":237,"sizeBytes":848},{"name":"js-length-check-first.md","path":".claude/skills/vercel-react-best-practices/rules/js-length-check-first.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-length-check-first.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-length-check-first.md -->","frontmatter":{"title":"Early Length Check for Array Comparisons","impact":"MEDIUM-HIGH","impactDescription":"avoids expensive operations when lengths differ","tags":"javascript, arrays, performance, optimization, comparison"},"isInternal":false,"tokens":373,"sizeBytes":1754},{"name":"js-min-max-loop.md","path":".claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-min-max-loop.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-min-max-loop.md -->","frontmatter":{"title":"Use Loop for Min/Max Instead of Sort","impact":"LOW","impactDescription":"O(n) instead of O(n log n)","tags":"javascript, arrays, performance, sorting, algorithms"},"isInternal":false,"tokens":597,"sizeBytes":2298},{"name":"js-set-map-lookups.md","path":".claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-set-map-lookups.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-set-map-lookups.md -->","frontmatter":{"title":"Use Set/Map for O(1) Lookups","impact":"LOW-MEDIUM","impactDescription":"O(n) to O(1)","tags":"javascript, set, map, data-structures, performance"},"isInternal":false,"tokens":154,"sizeBytes":532},{"name":"js-tosorted-immutable.md","path":".claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/js-tosorted-immutable.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fjs-tosorted-immutable.md -->","frontmatter":{"title":"Use toSorted() Instead of sort() for Immutability","impact":"MEDIUM-HIGH","impactDescription":"prevents mutation bugs in React state","tags":"javascript, arrays, immutability, react, state, mutation"},"isInternal":false,"tokens":436,"sizeBytes":1783},{"name":"rendering-activity.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-activity.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-activity.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-activity.md -->","frontmatter":{"title":"Use Activity Component for Show/Hide","impact":"MEDIUM","impactDescription":"preserves state/DOM","tags":"rendering, activity, visibility, state-preservation"},"isInternal":false,"tokens":131,"sizeBytes":566},{"name":"rendering-animate-svg-wrapper.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-animate-svg-wrapper.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-animate-svg-wrapper.md -->","frontmatter":{"title":"Animate SVG Wrapper Instead of SVG Element","impact":"LOW","impactDescription":"enables hardware acceleration","tags":"rendering, svg, css, animation, performance"},"isInternal":false,"tokens":274,"sizeBytes":1121},{"name":"rendering-conditional-render.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-conditional-render.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-conditional-render.md -->","frontmatter":{"title":"Use Explicit Conditional Rendering","impact":"LOW","impactDescription":"prevents rendering 0 or NaN","tags":"rendering, conditional, jsx, falsy-values"},"isInternal":false,"tokens":257,"sizeBytes":938},{"name":"rendering-content-visibility.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-content-visibility.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-content-visibility.md -->","frontmatter":{"title":"CSS content-visibility for Long Lists","impact":"HIGH","impactDescription":"faster initial render","tags":"rendering, css, content-visibility, long-lists"},"isInternal":false,"tokens":205,"sizeBytes":818},{"name":"rendering-hoist-jsx.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-hoist-jsx.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hoist-jsx.md -->","frontmatter":{"title":"Hoist Static JSX Elements","impact":"LOW","impactDescription":"avoids re-creation","tags":"rendering, jsx, static, optimization"},"isInternal":false,"tokens":227,"sizeBytes":993},{"name":"rendering-hydration-no-flicker.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-no-flicker.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-no-flicker.md -->","frontmatter":{"title":"Prevent Hydration Mismatch Without Flickering","impact":"MEDIUM","impactDescription":"avoids visual flicker and hydration errors","tags":"rendering, ssr, hydration, localStorage, flicker"},"isInternal":false,"tokens":467,"sizeBytes":2249},{"name":"rendering-hydration-suppress-warning.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-hydration-suppress-warning.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-hydration-suppress-warning.md -->","frontmatter":{"title":"Suppress Expected Hydration Mismatches","impact":"LOW-MEDIUM","impactDescription":"avoids noisy hydration warnings for known differences","tags":"rendering, hydration, ssr, nextjs"},"isInternal":false,"tokens":189,"sizeBytes":852},{"name":"rendering-resource-hints.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-resource-hints.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-resource-hints.md -->","frontmatter":{"title":"Use React DOM Resource Hints","impact":"HIGH","impactDescription":"reduces load time for critical resources","tags":"rendering, preload, preconnect, prefetch, resource-hints"},"isInternal":false,"tokens":633,"sizeBytes":2689},{"name":"rendering-script-defer-async.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-script-defer-async.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-script-defer-async.md -->","frontmatter":{"title":"Use defer or async on Script Tags","impact":"HIGH","impactDescription":"eliminates render-blocking","tags":"rendering, script, defer, async, performance"},"isInternal":false,"tokens":430,"sizeBytes":1872},{"name":"rendering-svg-precision.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-svg-precision.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-svg-precision.md -->","frontmatter":{"title":"Optimize SVG Precision","impact":"LOW","impactDescription":"reduces file size","tags":"rendering, svg, optimization, svgo"},"isInternal":false,"tokens":167,"sizeBytes":588},{"name":"rendering-usetransition-loading.md","path":".claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rendering-usetransition-loading.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frendering-usetransition-loading.md -->","frontmatter":{"title":"Use useTransition Over Manual Loading States","impact":"LOW","impactDescription":"reduces re-renders and improves code clarity","tags":"rendering, transitions, useTransition, loading, state"},"isInternal":false,"tokens":457,"sizeBytes":2090},{"name":"rerender-defer-reads.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-defer-reads.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-defer-reads.md -->","frontmatter":{"title":"Defer State Reads to Usage Point","impact":"MEDIUM","impactDescription":"avoids unnecessary subscriptions","tags":"rerender, searchParams, localStorage, optimization"},"isInternal":false,"tokens":231,"sizeBytes":983},{"name":"rerender-dependencies.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-dependencies.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-dependencies.md -->","frontmatter":{"title":"Narrow Effect Dependencies","impact":"LOW","impactDescription":"minimizes effect re-runs","tags":"rerender, useEffect, dependencies, optimization"},"isInternal":false,"tokens":214,"sizeBytes":833},{"name":"rerender-derived-state-no-effect.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state-no-effect.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state-no-effect.md -->","frontmatter":{"title":"Calculate Derived State During Rendering","impact":"MEDIUM","impactDescription":"avoids redundant renders and state drift","tags":"rerender, derived-state, useEffect, state"},"isInternal":false,"tokens":282,"sizeBytes":1211},{"name":"rerender-derived-state.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-derived-state.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-derived-state.md -->","frontmatter":{"title":"Subscribe to Derived State","impact":"MEDIUM","impactDescription":"reduces re-render frequency","tags":"rerender, derived-state, media-query, optimization"},"isInternal":false,"tokens":174,"sizeBytes":732},{"name":"rerender-functional-setstate.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-functional-setstate.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-functional-setstate.md -->","frontmatter":{"title":"Use Functional setState Updates","impact":"MEDIUM","impactDescription":"prevents stale closures and unnecessary callback recreations","tags":"react, hooks, useState, useCallback, callbacks, closures"},"isInternal":false,"tokens":663,"sizeBytes":2988},{"name":"rerender-lazy-state-init.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-lazy-state-init.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-lazy-state-init.md -->","frontmatter":{"title":"Use Lazy State Initialization","impact":"MEDIUM","impactDescription":"wasted computation on every render","tags":"react, hooks, useState, performance, initialization"},"isInternal":false,"tokens":430,"sizeBytes":2012},{"name":"rerender-memo-with-default-value.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-memo-with-default-value.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo-with-default-value.md -->","frontmatter":{"title":"Extract Default Non-primitive Parameter Value from Memoized Component to Constant","impact":"MEDIUM","impactDescription":"restores memoization by using a constant for default value","tags":"rerender, memo, optimization"},"isInternal":false,"tokens":248,"sizeBytes":1171},{"name":"rerender-memo.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-memo.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-memo.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-memo.md -->","frontmatter":{"title":"Extract to Memoized Components","impact":"MEDIUM","impactDescription":"enables early returns","tags":"rerender, memo, useMemo, optimization"},"isInternal":false,"tokens":278,"sizeBytes":1158},{"name":"rerender-move-effect-to-event.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-move-effect-to-event.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-move-effect-to-event.md -->","frontmatter":{"title":"Put Interaction Logic in Event Handlers","impact":"MEDIUM","impactDescription":"avoids effect re-runs and duplicate side effects","tags":"rerender, useEffect, events, side-effects, dependencies"},"isInternal":false,"tokens":285,"sizeBytes":1278},{"name":"rerender-no-inline-components.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-no-inline-components.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-no-inline-components.md -->","frontmatter":{"title":"Don't Define Components Inside Components","impact":"HIGH","impactDescription":"prevents remount on every render","tags":"rerender, components, remount, performance"},"isInternal":false,"tokens":511,"sizeBytes":2100},{"name":"rerender-simple-expression-in-memo.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-simple-expression-in-memo.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-simple-expression-in-memo.md -->","frontmatter":{"title":"Do not wrap a simple expression with a primitive result type in useMemo","impact":"LOW-MEDIUM","impactDescription":"wasted computation on every render","tags":"rerender, useMemo, optimization"},"isInternal":false,"tokens":217,"sizeBytes":1023},{"name":"rerender-split-combined-hooks.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-split-combined-hooks.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-split-combined-hooks.md -->","frontmatter":{"title":"Split Combined Hook Computations","impact":"MEDIUM","impactDescription":"avoids recomputing independent steps","tags":"rerender, useMemo, useEffect, dependencies, optimization"},"isInternal":false,"tokens":421,"sizeBytes":1857},{"name":"rerender-transitions.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-transitions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-transitions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-transitions.md -->","frontmatter":{"title":"Use Transitions for Non-Urgent Updates","impact":"MEDIUM","impactDescription":"maintains UI responsiveness","tags":"rerender, transitions, startTransition, performance"},"isInternal":false,"tokens":241,"sizeBytes":1067},{"name":"rerender-use-deferred-value.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-use-deferred-value.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-deferred-value.md -->","frontmatter":{"title":"Use useDeferredValue for Expensive Derived Renders","impact":"MEDIUM","impactDescription":"keeps input responsive during heavy computation","tags":"rerender, useDeferredValue, optimization, concurrent"},"isInternal":false,"tokens":427,"sizeBytes":1862},{"name":"rerender-use-ref-transient-values.md","path":".claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/rerender-use-ref-transient-values.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Frerender-use-ref-transient-values.md -->","frontmatter":{"title":"Use useRef for Transient Values","impact":"MEDIUM","impactDescription":"avoids unnecessary re-renders on frequent updates","tags":"rerender, useref, state, performance"},"isInternal":false,"tokens":437,"sizeBytes":1756},{"name":"server-after-nonblocking.md","path":".claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-after-nonblocking.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-after-nonblocking.md -->","frontmatter":{"title":"Use after() for Non-Blocking Operations","impact":"MEDIUM","impactDescription":"faster response times","tags":"server, async, logging, analytics, side-effects"},"isInternal":false,"tokens":453,"sizeBytes":2014},{"name":"server-auth-actions.md","path":".claude/skills/vercel-react-best-practices/rules/server-auth-actions.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-auth-actions.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-auth-actions.md -->","frontmatter":{"title":"Authenticate Server Actions Like API Routes","impact":"CRITICAL","impactDescription":"prevents unauthorized access to server mutations","tags":"server, server-actions, authentication, security, authorization"},"isInternal":false,"tokens":578,"sizeBytes":2656},{"name":"server-cache-lru.md","path":".claude/skills/vercel-react-best-practices/rules/server-cache-lru.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-cache-lru.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-lru.md -->","frontmatter":{"title":"Cross-Request LRU Caching","impact":"HIGH","impactDescription":"caches across requests","tags":"server, cache, lru, cross-request"},"isInternal":false,"tokens":325,"sizeBytes":1359},{"name":"server-cache-react.md","path":".claude/skills/vercel-react-best-practices/rules/server-cache-react.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-cache-react.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-cache-react.md -->","frontmatter":{"title":"Per-Request Deduplication with React.cache()","impact":"MEDIUM","impactDescription":"deduplicates within request","tags":"server, cache, react-cache, deduplication"},"isInternal":false,"tokens":509,"sizeBytes":2240},{"name":"server-dedup-props.md","path":".claude/skills/vercel-react-best-practices/rules/server-dedup-props.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-dedup-props.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-dedup-props.md -->","frontmatter":{"title":"Avoid Duplicate Serialization in RSC Props","impact":"LOW","impactDescription":"reduces network payload by avoiding duplicate serialization","tags":"server, rsc, serialization, props, client-components"},"isInternal":false,"tokens":501,"sizeBytes":2065},{"name":"server-hoist-static-io.md","path":".claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-hoist-static-io.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-hoist-static-io.md -->","frontmatter":{"title":"Hoist Static I/O to Module Level","impact":"HIGH","impactDescription":"avoids repeated file/network I/O per request","tags":"server, io, performance, next.js, route-handlers, og-image"},"isInternal":false,"tokens":1020,"sizeBytes":4371},{"name":"server-parallel-fetching.md","path":".claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-parallel-fetching.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-parallel-fetching.md -->","frontmatter":{"title":"Parallel Data Fetching with Component Composition","impact":"CRITICAL","impactDescription":"eliminates server-side waterfalls","tags":"server, rsc, parallel-fetching, composition"},"isInternal":false,"tokens":365,"sizeBytes":1569},{"name":"server-serialization.md","path":".claude/skills/vercel-react-best-practices/rules/server-serialization.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/rules/server-serialization.md","title":"Rules Skill","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2Frules%2Fserver-serialization.md -->","frontmatter":{"title":"Minimize Serialization at RSC Boundaries","impact":"HIGH","impactDescription":"reduces data transfer size","tags":"server, rsc, serialization, props"},"isInternal":false,"tokens":242,"sizeBytes":1006},{"name":"SKILL.md","path":".claude/skills/vercel-react-best-practices/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/vercel-react-best-practices/SKILL.md","title":"vercel-react-best-practices","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fvercel-react-best-practices%2FSKILL.md -->","frontmatter":{"name":"vercel-react-best-practices","description":"React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.","license":"MIT","metadata":{"author":"vercel","version":"1.0.0"}},"isInternal":false,"tokens":1654,"sizeBytes":6883},{"name":"SKILL.md","path":".claude/skills/web-design-guidelines/SKILL.md","rawUrl":"https://raw.githubusercontent.com/Kiranism/next-shadcn-dashboard-starter/HEAD/.claude/skills/web-design-guidelines/SKILL.md","title":"web-design-guidelines","category":"anthropic-skill","format":"markdown","content":"<!-- Skill available on-demand via /api/skills/Kiranism/next-shadcn-dashboard-starter?path=.claude%2Fskills%2Fweb-design-guidelines%2FSKILL.md -->","frontmatter":{"name":"web-design-guidelines","description":"Review UI code for Web Interface Guidelines compliance. Use when asked to \"review my UI\", \"check accessibility\", \"audit design\", \"review UX\", or \"check my site against best practices\".","metadata":{"author":"vercel","version":"1.0.0","argument-hint":"<file-or-pattern>"}},"isInternal":false,"tokens":268,"sizeBytes":1232}],"systemPromptSnippet":"<yakaai_skills repo=\"Kiranism/next-shadcn-dashboard-starter\">\n<!-- File: AGENTS.md (Tokens: ~6586 | Category: root-instruction) -->\n# AGENTS.md - AI Coding Agent Reference\n\nThis file provides essential information for AI coding agents working on this project. It contains project-specific details, conventions, and guidelines that complement the README.\n\n---\n\n## Project Overview\n\n**Next.js Admin Dashboard Starter** is a production-ready admin dashboard template built with:\n\n- **Framework**: Next.js 16 (App Router)\n- **Language**: TypeScript 7\n- **Styling**: Tailwind CSS v4\n- **UI Components**: shadcn/ui (base-nova style, Base UI primitives)\n- **Authentication**: Clerk (with Organizations/Billing support)\n- **Error Tracking**: Sentry\n- **Charts**: Recharts\n- **Containerization**: Docker (Node.js & Bun Dockerfiles)\n- **Package Manager**: Bun (preferred) or npm\n\nThe project follows a feature-based folder structure designed for scalability in SaaS applications, internal tools, and admin panels.\n\n---\n\n## Technology Stack Details\n\n### Core Framework & Runtime\n\n- Next.js 16.4 with App Router\n- React 19.3\n- TypeScript 7 (native compiler) with strict mode enabled\n\n### Styling & UI\n\n- Tailwind CSS v4 (using `@import 'tailwindcss'` syntax)\n- PostCSS with `@tailwindcss/postcss` plugin\n- shadcn/ui component library (Base UI primitives)\n- CSS custom properties for theming (OKLCH color format)\n\n### State Management\n\n- Zustand 5.x for local UI state in the stateful demo features\n- Nuqs for URL search params state management\n- TanStack Form + Zod for form handling (`createFormHook` + shadcn `Field`-anatomy components)\n\n### Data Fetching & Caching\n\n- TanStack React Query for data fetching, caching, and mutations\n- Server-side prefetching with `HydrationBoundary` + `dehydrate`\n- Client-side `useQuery` + nuqs `shallow: true` for tables (no RSC round-trips on pagination/filter)\n- `useMutation` + `invalidateQueries` for form submissions\n- Query client singleton in `src/lib/query-client.ts`\n\n### Authentication & Authorization\n\n- Clerk for authentication and user management\n- Clerk Organizations for multi-tenant workspaces\n- Clerk Billing for subscription management (B2B)\n- Client-side RBAC for navigation visibility\n\n### Data & APIs\n\n- TanStack Table for data tables\n- TanStack React Query for data fetching and mutations\n- Recharts for analytics/charts\n- Service layer per feature (`api/types.ts` → `api/service.ts` → `api/queries.ts`)\n- Route handlers at `src/app/api/` (for Route Handler or BFF patterns)\n- Mock data in `src/constants/mock-api*.ts` (default, swap via service layer)\n- API client utility in `src/lib/api-client.ts` (for fetch-based patterns)\n\n### Development Tools\n\n- ESLint 8.x with Next.js core-web-vitals config\n- Prettier 3.x with prettier-plugin-tailwindcss\n- Husky for git hooks\n- lint-staged for pre-commit formatting\n\n---\n\n## Project Structure\n\n```\n/src\n├── app/                    # Next.js App Router\n│   ├── auth/              # Authentication routes (sign-in, sign-up)\n│   ├── dashboard/         # Dashboard routes\n│   │   ├── overview/      # Parallel routes (@area_stats, @bar_stats, etc.)\n│   │   ├── product/       # Product management pages\n│   │   ├── kanban/        # Kanban board page\n│   │   ├── chat/          # Messaging page\n│   │   ├── ai-chat/       # AI chat streaming demo\n│   │   ├── notifications/ # Notifications page\n│   │   ├── workspaces/    # Organization management\n│   │   ├── billing/       # Subscription billing\n│   │   ├── exclusive/     # Pro plan feature example\n│   │   └── profile/       # User profile\n│   ├── api/               # API routes (if any)\n│   ├── layout.tsx         # Root layout with providers\n│   ├── page.tsx           # Landing page\n│   ├── global-error.tsx   # Global error boundary\n│   └── not-found.tsx      # 404 page\n│\n├── components/\n│   ├── ui/                # shadcn/ui components (50+ components)\n│   ├── layout/            # Layout components (sidebar, header, etc.)\n│   ├── forms/             # Field components (shadcn TanStack Form anatomy) + demos\n│   ├── themes/            # Theme system components\n│   ├── kbar/              # Command+K search bar\n│   ├── icons.tsx          # Icon registry\n│   └── ...\n│\n├── features/              # Feature-based modules\n│   ├── auth/              # Authentication components\n│   ├── overview/          # Dashboard analytics\n│   ├── products/          # Product management (React Query + nuqs)\n│   │   ├── api/\n│   │   │   ├── types.ts   # Type contract (response shapes, filters, payloads)\n│   │   │   ├── service.ts # Data access layer (swap for your backend)\n│   │   │   └── queries.ts # React Query options + key factories\n│   │   ├── components/    # Listing, form, table components\n│   │   ├── schemas/       # Zod schemas\n│   │   └── constants/     # Filter options\n│   ├── users/             # User management (React Query + nuqs)\n│   │   ├── api/           # Same pattern: types.ts → service.ts → queries.ts\n│   │   └── components/    # Listing, table components\n│   ├── react-query-demo/  # React Query showcase (Pokemon API)\n│   ├── kanban/            # Kanban board with dnd-kit\n│   ├── chat/              # Messaging UI (conversations, bubbles, composer)\n│   ├── ai-chat/           # Scripted useChat streaming demo\n│   ├── notifications/     # Notification center & store\n│   └── profile/           # Profile management\n│\n├── config/                # Configuration files\n│   ├── nav-config.ts      # Navigation with RBAC\n│   └── ...\n│\n├── hooks/                 # Custom React hooks\n│   ├── use-nav.ts         # RBAC navigation filtering\n│   ├── use-data-table.ts  # Data table state\n│   └── ...\n│\n├── lib/                   # Utility functions\n│   ├── utils.ts           # cn() and formatters\n│   ├── searchparams.ts    # Search param utilities\n│   └── ...\n│\n├── types/                 # TypeScript type definitions\n│   └── index.ts           # Core types (NavItem, etc.)\n│\n└── styles/                # Global styles\n    ├── globals.css        # Tailwind imports + view transitions\n    ├── theme.css          # Theme imports\n    └── themes/            # Individual theme files\n\n/docs                      # Documentation\n│   ├── clerk_setup.md     # Clerk configuration guide\n│   ├── nav-rbac.md        # Navigation RBAC documentation\n│   └── themes.md          # Theme customization guide\n\n/scripts                   # Dev tooling\n    ├── cleanup.js         # Feature removal, run via `bun run cleanup` (templates in cleanup-templates/, typechecked)\n    └── cleanup-templates/ # Replacement files cleanup.js copies into the repo\n\nDockerfile                 # Node.js production Dockerfile\nDockerfile.bun             # Bun production Dockerfile\n.dockerignore              # Docker build exclusions\n```\n\n---\n\n## Build & Development Commands\n\n```bash\n# Install dependencies\nbun install\n\n# Development server\nbun run dev          # Starts at http://localhost:3000\n\n# Build for production\nbun run build\n\n# Start production server\nbun run start\n\n# Linting\nbun run lint         # Run ESLint\nbun run lint:fix     # Fix ESLint issues and format\nbun run lint:strict  # Zero warnings tolerance\n\nbun run typecheck    # tsc --noEmit\n\n# Formatting\nbun run format       # Format with Prettier\nbun run format:check # Check formatting\n\n# Git hooks\nbun run prepare      # Install Husky hooks\n```\n\n---\n\n## Environment Configuration\n\nCopy `env.example.txt` to `.env.local` and configure:\n\n### Required for Authentication (Clerk)\n\n```env\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_...\nCLERK_SECRET_KEY=sk_...\n\n# Redirect URLs\nNEXT_PUBLIC_CLERK_SIGN_IN_URL=\"/auth/sign-in\"\nNEXT_PUBLIC_CLERK_SIGN_UP_URL=\"/auth/sign-up\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=\"/dashboard/overview\"\nNEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=\"/dashboard/overview\"\n```\n\n### Optional for Error Tracking (Sentry)\n\n```env\nNEXT_PUBLIC_SENTRY_DSN=https://...@....ingest.sentry.io/...\nNEXT_PUBLIC_SENTRY_ORG=your-org\nNEXT_PUBLIC_SENTRY_PROJECT=your-project\nSENTRY_AUTH_TOKEN=sntrys_...\nNEXT_PUBLIC_SENTRY_DISABLED=\"false\"  # Set to \"true\" to disable in dev\n```\n\n**Note**: Clerk supports \"keyless mode\" — run `npx clerk@latest init` to provision a development instance in seconds (no account needed; keys are written to `.env.local`).\n\n---\n\n## Code Style Guidelines\n\n### TypeScript\n\n- Strict mode enabled\n- Use explicit return types for public functions\n- Prefer interface over type for object definitions\n- Use `@/*` alias for imports from src\n\n### Formatting (Prettier)\n\n```json\n{\n  \"singleQuote\": true,\n  \"jsxSingleQuote\": true,\n  \"semi\": true,\n  \"trailingComma\": \"none\",\n  \"tabWidth\": 2,\n  \"arrowParens\": \"always\"\n}\n```\n\n### ESLint Rules\n\n- `@typescript-eslint/no-unused-vars`: warn\n- `no-console`: warn\n- `react-hooks/exhaustive-deps`: warn\n- `import/no-unresolved`: off (handled by TypeScript)\n\n### Component Conventions\n\n- Use function declarations for components: `function ComponentName() {}`\n- Props interface named `{ComponentName}Props`\n- shadcn/ui components use `cn()` utility for class merging\n- Server components by default, `'use client'` only when needed\n\n---\n\n## Theming System\n\nThe project uses a sophisticated multi-theme system with 10 built-in themes:\n\n- `vercel` (default)\n- `claude`\n- `discord`\n- `supabase`\n- `mono`\n- `notebook`\n- `light-green`\n- `zen`\n- `astro-vista`\n- `whatsapp`\n\n### Theme Files\n\n- CSS files: `src/styles/themes/{theme-name}.css`\n- Theme registry: `src/components/themes/theme.config.ts`\n- Font config: `src/components/themes/font.config.ts`\n- Active theme provider: `src/components/themes/active-theme.tsx`\n\n### Adding a New Theme\n\n1. Create `src/styles/themes/your-theme.css` with `[data-theme='your-theme']` selector\n2. Import in `src/styles/theme.css`\n3. Add to `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add fonts in `font.config.ts`\n5. (Optional) Set as default in `theme.config.ts`\n\nSee `docs/themes.md` for detailed theming guide.\n\n---\n\n## Navigation & RBAC System\n\n### Navigation Configuration\n\nNavigation is organized into groups in `src/config/nav-config.ts`:\n\n```typescript\nimport { NavGroup } from '@/types';\n\nexport const navGroups: NavGroup[] = [\n  {\n    label: 'Overview',\n    items: [\n      {\n        title: 'Dashboard',\n        url: '/dashboard/overview',\n        icon: 'dashboard',\n        shortcut: ['d', 'd'],\n        items: [],\n        access: { requireOrg: true } // RBAC check\n      }\n    ]\n  }\n];\n```\n\n### Access Control Properties\n\n- `requireOrg: boolean` - Requires active organization\n- `permission: string` - Requires specific permission\n- `role: string` - Requires specific role\n- `plan: string` - Requires specific subscription plan\n- `feature: string` - Requires specific feature\n\n### Client-Side Filtering\n\nThe `useFilteredNavItems()` hook in `src/hooks/use-nav.ts` filters navigation client-side using Clerk's `useOrganization()` and `useUser()` hooks. This is for UX only - actual security checks must happen server-side.\n\n---\n\n## Authentication Patterns\n\n### Protected Routes\n\nRoute protection is resource-based (Clerk's recommended pattern): `src/proxy.ts` runs a bare `clerkMiddleware()` to attach the auth context, and every dashboard `layout`/`page` and API route handler performs its own check.\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nexport default async function Page() {\n  await auth.protect(); // redirects signed-out users to sign-in\n  // ...\n}\n```\n\nAPI route handlers return 401 instead of redirecting:\n\n```tsx\nconst { userId } = await auth();\nif (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });\n```\n\nPages that require an organization:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\nimport { redirect } from 'next/navigation';\n\nexport default async function Page() {\n  await auth.protect();\n  const { orgId } = await auth();\n  if (!orgId) redirect('/dashboard/workspaces');\n  // ...\n}\n```\n\n### Plan/Feature Protection\n\nUse Clerk's `<Show>` component for client-side:\n\n```tsx\nimport { Show } from '@clerk/nextjs';\n\n<Show when={{ plan: 'pro' }} fallback={<UpgradePrompt />}>\n  <PremiumContent />\n</Show>;\n```\n\nUse `has()` function for server-side checks:\n\n```tsx\nimport { auth } from '@clerk/nextjs/server';\n\nconst { has } = await auth();\nconst hasFeature = has({ feature: 'premium_access' });\n```\n\n---\n\n## Data Fetching Patterns\n\n### Service Layer Architecture\n\nEach feature has a three-file API layer:\n\n```\nsrc/features/<name>/api/\n  types.ts      ← Type contract (response shapes, filters, payloads)\n  service.ts    ← Data access functions (the ONE file to swap for your backend)\n  queries.ts    ← React Query options + query key factories (stable, never changes)\n```\n\n**`service.ts` is the only file you modify when connecting to a real backend.** Queries and components import from it — they never change.\n\n#### Backend Patterns\n\n| Pattern                                            | How to implement                                                                            |\n| -------------------------------------------------- | ------------------------------------------------------------------------------------------- |\n| **Server Actions + ORM** (Prisma/Drizzle/Supabase) | Add `'use server'` at top of `service.ts`, call ORM directly                                |\n| **Route Handlers + ORM**                           | `service.ts` calls `/api/` routes via `apiClient`, route handlers call ORM                  |\n| **BFF** (Next.js proxies to Laravel/Go/etc.)       | `service.ts` calls `/api/` routes via `apiClient`, route handlers proxy to external backend |\n| **Direct external API** (frontend-only)            | `service.ts` calls external URL via `fetch()`                                               |\n| **Mock** (default)                                 | `service.ts` calls in-memory fake data stores                                               |\n\nRoute handlers at `src/app/api/` are ready for patterns 2 and 3. `src/lib/api-client.ts` provides a typed `fetch` wrapper.\n\n### Query Key Factories\n\nEach feature defines a key factory in `queries.ts` for type-safe, hierarchical cache invalidation:\n\n```tsx\nexport const entityKeys = {\n  all: ['entities'] as const,\n  list: (filters: EntityFilters) => [...entityKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...entityKeys.all, 'detail', id] as const\n};\n\n// Usage in queryOptions\nqueryKey: entityKeys.list(filters);\n\n// Usage in mutations — invalidate all entity queries\nqueryClient.invalidateQueries({ queryKey: entityKeys.all });\n```\n\n### React Query (Default for all new pages)\n\nThe project uses TanStack React Query with server-side prefetching and client-side cache management:\n\n1. **Query options** defined in `queries.ts` — shared between server prefetch and client hooks\n2. **Server prefetch** using `void queryClient.prefetchQuery()` + `HydrationBoundary` + `dehydrate` — `void` (fire-and-forget) is the standard TanStack pattern for Next.js App Router\n3. **Client fetch** using `useSuspenseQuery()` — integrates with React Suspense so prefetched data streams in without showing a loading skeleton on first load\n4. **Suspense boundary** wraps the client component — shows a fallback skeleton only on subsequent client-side navigations when cache is empty\n\n```tsx\n// Server component: prefetch + dehydrate\nconst queryClient = getQueryClient();\nvoid queryClient.prefetchQuery(entitiesQueryOptions(filters)); // void, not await\n\nreturn (\n  <HydrationBoundary state={dehydrate(queryClient)}>\n    <Suspense fallback={<Skeleton />}>\n      <EntityTable />\n    </Suspense>\n  </HydrationBoundary>\n);\n\n// Client component: useSuspenseQuery (not useQuery)\nconst { data } = useSuspenseQuery(entitiesQueryOptions(filters));\n```\n\n**Why `void` + `useSuspenseQuery`:**\n\n- `void` fires the prefetch without blocking the server component\n- `useSuspenseQuery` integrates with React Suspense — the pending query streams in via Next.js streaming SSR\n- With `<Suspense fallback={<Skeleton />}>`: skeleton shows immediately while data streams in — this is expected behavior, the skeleton IS the Suspense fallback during streaming\n- Without `<Suspense>` wrapper: no skeleton, but the previous page stays visible until data fully resolves (feels like a slow navigation)\n- Once data is cached (within `staleTime`), subsequent visits are instant — no skeleton\n\n**Why NOT `useQuery`:**\n\n- `useQuery` doesn't integrate with Suspense — returns `isLoading: true` and you must handle loading state manually\n- Hydrated pending queries from `void` prefetch won't prevent the loading state\n- Results in skeleton flash even when data is prefetched\n\n### Mutations\n\nComponents import service functions for mutations. Use query key factories for invalidation:\n\n```tsx\nimport { createEntity } from '../api/service';\nimport { entityKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createEntity(data),\n  onSuccess: () => {\n    queryClient.invalidateQueries({ queryKey: entityKeys.all });\n    toast.success('Created');\n  }\n});\n```\n\n### URL State Management\n\nUse `nuqs` for search params state:\n\n- `searchParamsCache` (server) — reads params in server components\n- `useQueryState` (client) — reads/writes params in client components with `shallow: true`\n\n### Data Tables\n\nTables use TanStack Table v9 (DiceUI data table) with React Query:\n\n- Query options in `features/*/api/queries.ts`\n- Column definitions in `features/*/components/*-tables/columns.tsx`, typed `ColumnDef<DataTableFeatures, T>[]`\n- Table component in `src/components/data-table/data-table.tsx`\n- Feature set + `DataTableFeatures` type in `src/lib/data-table-features.ts`; URL parsers in `src/lib/data-table-parsers.ts`\n- Column pinning via `initialState.columnPinning` (`{ start: [], end: ['actions'] }`) in `useDataTable`\n\n---\n\n## Error Handling & Monitoring\n\n### Sentry Integration\n\nSentry is configured for both client and server:\n\n- Client config: `src/instrumentation-client.ts`\n- Server config: `src/instrumentation.ts`\n- Global error: `src/app/global-error.tsx`\n\nTo disable Sentry in development:\n\n```env\nNEXT_PUBLIC_SENTRY_DISABLED=\"true\"\n```\n\n### Error Boundaries\n\n- `global-error.tsx` - Catches all errors, reports to Sentry\n- Parallel route `error.tsx` files for specific sections\n\n---\n\n## Testing Strategy\n\n**Note**: This project does not include a test suite by default. Consider adding:\n\n- **Unit tests**: Vitest or Jest for utilities and hooks\n- **Component tests**: React Testing Library for UI components\n- **E2E tests**: Playwright for critical user flows\n\nRecommended test locations:\n\n```\n/src\n  /__tests__           # Unit tests\n  /features/*/tests    # Feature tests\n/e2e                   # Playwright tests\n```\n\n---\n\n## Deployment\n\nCanonical guide: [docs/deployment.md](./docs/deployment.md) (Vercel, production environment variables, Docker).\n\n### Vercel (Recommended)\n\n1. Connect repository to Vercel\n2. Add environment variables in dashboard\n3. Deploy\n\n### Environment Variables for Production\n\nEnsure these are set in your deployment platform:\n\n- `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY`\n- `CLERK_SECRET_KEY`\n- All `NEXT_PUBLIC_*` variables for client-side access\n- `SENTRY_*` variables if using error tracking\n\n### Docker\n\nProduction-ready Dockerfiles are included:\n\n- `Dockerfile` — Node.js-based\n- `Dockerfile.bun` — Bun-based\n\nBoth use `output: 'standalone'` in `next.config.ts`. Pass `NEXT_PUBLIC_*` vars as `--build-arg` at build time, and runtime secrets via `-e` at run time.\n\n### Build Considerations\n\n- Output: `standalone` (optimized for Docker/self-hosting)\n- Images: Configured for `api.slingacademy.com`, `img.clerk.com`, `clerk.com`\n- Sentry source maps uploaded automatically in CI\n\n---\n\n## Feature Cleanup System\n\nA single `scripts/cleanup.js` file handles removal of optional features:\n\n```bash\n# Interactive mode — prompts for each feature\nnode scripts/cleanup.js --interactive\n\n# Remove specific features\nnode scripts/cleanup.js clerk           # Remove auth/org/billing\nnode scripts/cleanup.js kanban          # Remove kanban board\nnode scripts/cleanup.js chat            # Remove messaging UI\nnode scripts/cleanup.js ai-chat         # Remove AI chat demo\nnode scripts/cleanup.js notifications   # Remove notification center\nnode scripts/cleanup.js themes          # Keep one theme, remove rest\nnode scripts/cleanup.js sentry          # Remove error tracking\n\n# Remove multiple at once\nnode scripts/cleanup.js kanban chat notifications\n\n# Preview without changing files\nnode scripts/cleanup.js --dry-run kanban\n\n# List all features\nnode scripts/cleanup.js --list\n```\n\n**Safety**: Script requires git repository with at least one commit. Use `--force` to skip.\n\nReplacement files live in `scripts/cleanup-templates/` as real `.ts`/`.tsx` files typechecked by `tsc` and `next build`, so template rot fails loudly instead of shipping broken code.\n\nAfter cleanup, delete `scripts/cleanup.js` and `scripts/cleanup-templates/` — the dev server message auto-cleans on next start.\n\n---\n\n## Icon System\n\n**All icons come from a single source: `src/components/icons.tsx`.**\n\nThe project uses `@tabler/icons-react` as the sole icon package. Every icon is re-exported through a centralized `Icons` object — **never import directly from `@tabler/icons-react` or any other icon package**.\n\n### Usage\n\n```tsx\nimport { Icons } from '@/components/icons';\n\n// In JSX\n<Icons.search className='h-4 w-4' />\n<Icons.chevronRight className='h-4 w-4' />\n\n// Passing as a prop\nicon={Icons.check}\n```\n\n### Adding a New Icon\n\n1. Import the tabler icon in `src/components/icons.tsx`\n2. Add a semantic key to the `Icons` object\n3. Use `Icons.yourKey` everywhere — never the raw import\n\n```tsx\n// In src/components/icons.tsx\nimport { IconNewIcon } from '@tabler/icons-react';\n\nexport const Icons = {\n  // ...existing icons\n  newIcon: IconNewIcon\n};\n```\n\n### Available Icon Categories\n\n| Category        | Example Keys                                                                  |\n| --------------- | ----------------------------------------------------------------------------- |\n| General         | `check`, `close`, `search`, `settings`, `trash`, `spinner`, `info`, `warning` |\n| Navigation      | `chevronDown`, `chevronLeft`, `chevronRight`, `chevronUp`, `chevronsUpDown`   |\n| Layout          | `dashboard`, `kanban`, `panelLeft`                                            |\n| User            | `user`, `account`, `profile`, `teams`                                         |\n| Communication   | `chat`, `notification`, `phone`, `video`, `send`                              |\n| Files           | `page`, `post`, `media`, `fileTypePdf`, `fileTypeDoc`                         |\n| Actions         | `add`, `edit`, `upload`, `share`, `login`, `logout`                           |\n| Theme           | `sun`, `moon`, `brightness`, `laptop`, `palette`                              |\n| Text formatting | `bold`, `italic`, `underline`, `text`                                         |\n| Data / Charts   | `trendingUp`, `trendingDown`, `eyeOff`, `adjustments`                         |\n\n### Icon Showcase Page\n\nBrowse all available icons at `/dashboard/elements/icons` — a searchable grid of every icon in the registry.\n\n### Why This Pattern?\n\n- **Single source of truth** — swap icon packages by editing one file\n- **Semantic naming** — `Icons.trash` is clearer than `IconTrash` scattered across files\n- **Discoverability** — autocomplete on `Icons.` shows every available icon\n- **No direct dependencies** — components never couple to a specific icon package\n\n---\n\n## Common Development Tasks\n\n### Adding a New Feature (End-to-End)\n\n1. Create `src/features/<name>/api/types.ts` — response types, filter types, mutation payloads\n2. Create `src/features/<name>/api/service.ts` — data access functions (mock by default)\n3. Create `src/features/<name>/api/queries.ts` — query key factory + `queryOptions`\n4. Create page route: `src/app/dashboard/<name>/page.tsx`\n5. Create feature components in `src/features/<name>/components/`\n6. Add navigation item in `src/config/nav-config.ts`\n7. (Optional) Add route handlers in `src/app/api/<name>/` for REST API patterns\n8. (Optional) Register new icon in `src/components/icons.tsx`\n\n### Adding a New API Route\n\n1. Create: `src/app/api/my-route/route.ts`\n2. Export HTTP method handlers: `GET`, `POST`, etc.\n3. For BFF pattern: proxy requests to your external backend\n\n### Adding a shadcn Component\n\n```bash\nnpx shadcn add component-name\n```\n\n### Adding a New Theme\n\nSee \"Theming System\" section above or `docs/themes.md`.\n\n---\n\n## Troubleshooting\n\n### Common Issues\n\n**Build fails with Tailwind errors**\n\n- Ensure using Tailwind CSS v4 syntax (`@import 'tailwindcss'`)\n- Check `postcss.config.js` uses `@tailwindcss/postcss`\n\n**Clerk keyless mode popup**\n\n- Run `npx clerk@latest init` to provision a dev instance in seconds (no account needed)\n- It writes keys to `.env.local`; later you can claim application or set env variables\n\n**Theme not applying**\n\n- Check theme name matches in CSS `[data-theme]` and `theme.config.ts`\n- Verify theme CSS is imported in `theme.css`\n\n**Navigation items not showing**\n\n- Check `access` property in nav config\n- Verify user has required org/permission/role\n\n---\n\n## External Documentation\n\n- [Next.js App Router](https://nextjs.org/docs/app)\n- [Clerk Next.js SDK](https://clerk.com/docs/references/nextjs)\n- [shadcn/ui](https://ui.shadcn.com/docs)\n- [Tailwind CSS v4](https://tailwindcss.com/docs)\n- [TanStack Table](https://tanstack.com/table/latest)\n- [Sentry Next.js](https://docs.sentry.io/platforms/javascript/guides/nextjs/)\n\n---\n\n## Notes for AI Agents\n\n1. **Always use `cn()` for className merging** - never concatenate strings manually\n2. **Respect the feature-based structure** - put new feature code in `src/features/`\n3. **Server components by default** - only add `'use client'` when using browser APIs or React hooks\n4. **Type safety first** - avoid `any`, prefer explicit types\n5. **Follow existing patterns** - look at similar components before creating new ones\n6. **Environment variables** - prefix with `NEXT_PUBLIC_` for client-side access\n7. **shadcn components** - don't modify files in `src/components/ui/` directly; extend them instead\n8. **Icons** - NEVER import icons directly from `@tabler/icons-react` or any other icon package. All icons must be registered in `src/components/icons.tsx` and imported as `import { Icons } from '@/components/icons'`. To add a new icon: add the tabler import to `icons.tsx`, add a semantic key to the `Icons` object, then use `Icons.keyName` in your component.\n9. **Page headers** - Always use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`) for page headers. Never import `<Heading>` manually in pages — `PageContainer` handles that internally.\n10. **Forms** - Use `useAppForm` from `@/lib/form` with `form.AppField` rendering the shared field components (`field.TextField`, `field.SelectField`, …) from `@/components/forms/fields`. Each component follows the official shadcn TanStack Form anatomy; drop down to raw `form.Field` render props for one-off custom fields. Never use `useState` inside a render prop — extract stateful controls into components.\n11. **Button loading** - Use `<Button isLoading={isPending}>` for loading states. Uses CSS Grid overlap trick for zero layout shift. When `isLoading` is not passed, button behaves as default shadcn. `SubmitButton` in forms handles this automatically via form `isSubmitting` state.\n12. **Data layer** - Always go through the service layer: `types.ts` → `service.ts` → `queries.ts`. Components import types from `types.ts`, functions from `service.ts`, query options from `queries.ts`. Never import from `@/constants/mock-api*` directly in components.\n\n<!-- File: CLAUDE.md (Tokens: ~527 | Category: claude-rule) -->\n# CLAUDE.md\n\nThis is a Next.js 16 + shadcn/ui admin dashboard starter kit.\n\n## Key References\n\n- **[AGENTS.md](./AGENTS.md)** — Full project overview, tech stack, structure, conventions, data fetching patterns, deployment\n- **[docs/forms.md](./docs/forms.md)** — Form system: TanStack Form + Zod, composable fields, validation, multi-step, sheet/dialog forms\n- **[docs/themes.md](./docs/themes.md)** — Theme system: OKLCH colors, adding themes, font config\n- **[docs/nav-rbac.md](./docs/nav-rbac.md)** — Navigation RBAC: access control, Clerk integration\n- **[docs/clerk_setup.md](./docs/clerk_setup.md)** — Clerk auth setup: organizations, billing, environment variables\n- **[docs/deployment.md](./docs/deployment.md)** — Deployment: Vercel, production environment variables, Docker\n\n## Critical Conventions\n\n- **React Query** for all data fetching — `void prefetchQuery()` on server + `useSuspenseQuery` on client (standard TanStack pattern), `useMutation` for forms, `HydrationBoundary` + `dehydrate` for hydration, `<Suspense fallback>` for streaming\n- **API layer** per feature — `api/types.ts` → `api/service.ts` → `api/queries.ts`; queries use key factories (`entityKeys.all/list/detail`); components import from service and queries, never from mock APIs directly\n- **nuqs** for URL search params — `searchParamsCache` on server, `useQueryStates` on client, use `getSortingStateParser` for sort (same parser as `useDataTable`)\n- **Icons** — only import from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` from `@/lib/form` (TanStack `createFormHook`) + `form.AppField` rendering the field components in `@/components/forms/fields` (`field.TextField`, `field.SelectField`, …); each component is the shadcn TanStack Form doc anatomy; raw `form.Field` for one-off custom fields; form-level Zod `onSubmit` validators\n- **Page headers** — use `PageContainer` props (`pageTitle`, `pageDescription`, `pageHeaderAction`), never import `<Heading>` manually\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n\n<!-- File: .agents/skills/kiranism-shadcn-dashboard/references/charts-guide.md (Tokens: ~2884 | Category: anthropic-skill) -->\n# Charts & Analytics Guide\n\n## Table of Contents\n\n1. [Overview Architecture](#overview-architecture)\n2. [Parallel Routes Pattern](#parallel-routes-pattern)\n3. [Chart Components](#chart-components)\n4. [Stats Cards](#stats-cards)\n5. [Skeleton Loading](#skeleton-loading)\n6. [Adding a New Chart Section](#adding-a-new-chart-section)\n\n---\n\n## Overview Architecture\n\nThe analytics dashboard at `/dashboard/overview` uses **Next.js parallel routes** to load multiple chart sections independently. Each chart slot streams in as its data becomes ready — no waterfall, no blocking.\n\n**File structure:**\n\n```\nsrc/app/dashboard/overview/\n├── layout.tsx              # Composes all slots into a grid\n├── @area_stats/\n│   ├── page.tsx            # Async server component (fetches data)\n│   ├── loading.tsx         # Skeleton shown while streaming\n│   └── error.tsx           # Error boundary if fetch fails\n├── @bar_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n├── @pie_stats/\n│   ├── page.tsx\n│   ├── loading.tsx\n│   └── error.tsx\n└── @sales/\n    ├── page.tsx\n    ├── loading.tsx\n    └── error.tsx\n\nsrc/features/overview/components/\n├── area-graph.tsx          # Client chart component\n├── area-graph-skeleton.tsx # Matching skeleton\n├── bar-graph.tsx\n├── bar-graph-skeleton.tsx\n├── pie-graph.tsx\n├── pie-graph-skeleton.tsx\n├── recent-sales.tsx\n└── recent-sales-skeleton.tsx\n```\n\n---\n\n## Parallel Routes Pattern\n\n### Layout (`layout.tsx`)\n\nThe layout receives each parallel route as a prop and arranges them in a grid:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats\n}: {\n  sales: React.ReactNode;\n  pie_stats: React.ReactNode;\n  bar_stats: React.ReactNode;\n  area_stats: React.ReactNode;\n}) {\n  return (\n    <PageContainer pageTitle='Dashboard' pageDescription='Overview analytics.'>\n      {/* Stats cards row */}\n      <div className='grid gap-4 md:grid-cols-2 lg:grid-cols-4'>\n        <Card>\n          <CardHeader className='flex flex-row items-center justify-between pb-2'>\n            <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n            <Icons.billing className='h-4 w-4 text-muted-foreground' />\n          </CardHeader>\n          <CardContent>\n            <div className='text-2xl font-bold'>$45,231.89</div>\n            <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n          </CardContent>\n        </Card>\n        {/* ...more stat cards */}\n      </div>\n\n      {/* Charts grid — each slot loads independently */}\n      <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n        <div className='col-span-4'>{area_stats}</div>\n        <div className='col-span-3'>{sales}</div>\n        <div className='col-span-4'>{bar_stats}</div>\n        <div className='col-span-3'>{pie_stats}</div>\n      </div>\n    </PageContainer>\n  );\n}\n```\n\n### Slot Page (`@area_stats/page.tsx`)\n\nEach slot is an async server component that fetches data then renders the chart:\n\n```tsx\nimport { delay } from '@/constants/mock-api';\nimport { AreaGraph } from '@/features/overview/components/area-graph';\n\nexport default async function AreaStatsPage() {\n  await delay(2000); // Simulates API fetch\n  return <AreaGraph />;\n}\n```\n\n### Slot Loading (`@area_stats/loading.tsx`)\n\n```tsx\nimport { AreaGraphSkeleton } from '@/features/overview/components/area-graph-skeleton';\n\nexport default function Loading() {\n  return <AreaGraphSkeleton />;\n}\n```\n\n### Slot Error (`@area_stats/error.tsx`)\n\n```tsx\n'use client';\nimport { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';\nimport { Icons } from '@/components/icons';\n\nexport default function AreaStatsError({ error }: { error: Error }) {\n  return (\n    <Alert variant='destructive'>\n      <Icons.alertCircle className='h-4 w-4' />\n      <AlertTitle>Error</AlertTitle>\n      <AlertDescription>Failed to load area stats: {error.message}</AlertDescription>\n    </Alert>\n  );\n}\n```\n\nEach slot can fail independently without affecting others.\n\n---\n\n## Chart Components\n\nAll chart components are `'use client'` and use **Recharts** wrapped in shadcn's `ChartContainer`.\n\n### Chart Config\n\nEvery chart defines a config object mapping data keys to labels and theme colors:\n\n```tsx\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n```\n\nTheme colors `--chart-1` through `--chart-5` are defined in each theme's CSS file and automatically adapt to light/dark mode.\n\n### Area Chart Example\n\n```tsx\n'use client';\nimport { Area, AreaChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\nimport { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';\nimport { Badge } from '@/components/ui/badge';\nimport { Icons } from '@/components/icons';\n\nconst chartData = [\n  { month: 'January', desktop: 186, mobile: 80 },\n  { month: 'February', desktop: 305, mobile: 200 }\n  // ...more months\n];\n\nconst chartConfig = {\n  desktop: { label: 'Desktop', color: 'var(--chart-1)' },\n  mobile: { label: 'Mobile', color: 'var(--chart-2)' }\n} satisfies ChartConfig;\n\nexport function AreaGraph() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <CardTitle>Area Chart - Stacked</CardTitle>\n        <Badge variant='outline'>\n          <Icons.trendingUp className='mr-1 h-3 w-3' /> +12.5%\n        </Badge>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <AreaChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis\n              dataKey='month'\n              tickLine={false}\n              axisLine={false}\n              tickFormatter={(value) => value.slice(0, 3)}\n            />\n            <ChartTooltip content={<ChartTooltipContent indicator='dot' />} />\n            <Area\n              dataKey='mobile'\n              type='natural'\n              fill='var(--color-mobile)'\n              stroke='var(--color-mobile)'\n              stackId='a'\n            />\n            <Area\n              dataKey='desktop'\n              type='natural'\n              fill='var(--color-desktop)'\n              stroke='var(--color-desktop)'\n              stackId='a'\n            />\n          </AreaChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### Bar Chart Pattern\n\nSame structure, using `BarChart` + `Bar`:\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <BarChart data={chartData}>\n    <CartesianGrid vertical={false} />\n    <XAxis dataKey='month' tickLine={false} axisLine={false} />\n    <ChartTooltip content={<ChartTooltipContent />} />\n    <Bar dataKey='desktop' fill='var(--color-desktop)' radius={4} />\n    <Bar dataKey='mobile' fill='var(--color-mobile)' radius={4} />\n  </BarChart>\n</ChartContainer>\n```\n\n### Pie/Donut Chart Pattern\n\n```tsx\n<ChartContainer config={chartConfig}>\n  <PieChart>\n    <ChartTooltip content={<ChartTooltipContent hideLabel />} />\n    <Pie data={chartData} dataKey='visitors' nameKey='browser' innerRadius={30}>\n      <LabelList dataKey='visitors' className='fill-background' />\n    </Pie>\n  </PieChart>\n</ChartContainer>\n```\n\n---\n\n## Stats Cards\n\nStats cards are simple server-rendered `Card` components at the top of the layout — no parallel routes needed since they render instantly:\n\n```tsx\n<Card>\n  <CardHeader className='flex flex-row items-center justify-between space-y-0 pb-2'>\n    <CardTitle className='text-sm font-medium'>Total Revenue</CardTitle>\n    <Icons.billing className='h-4 w-4 text-muted-foreground' />\n  </CardHeader>\n  <CardContent>\n    <div className='text-2xl font-bold'>$45,231.89</div>\n    <p className='text-xs text-muted-foreground'>+20.1% from last month</p>\n  </CardContent>\n</Card>\n```\n\nFor dynamic stats that need data fetching, wrap in their own Suspense boundary or parallel route slot.\n\n---\n\n## Skeleton Loading\n\nEach chart has a matching skeleton component. Pattern:\n\n```tsx\nimport { Card, CardContent, CardHeader } from '@/components/ui/card';\nimport { Skeleton } from '@/components/ui/skeleton';\n\nexport function AreaGraphSkeleton() {\n  return (\n    <Card className='@container/card'>\n      <CardHeader>\n        <Skeleton className='h-5 w-[140px]' />\n        <Skeleton className='h-4 w-[80px]' />\n      </CardHeader>\n      <CardContent>\n        <Skeleton className='h-[250px] w-full rounded-md' />\n      </CardContent>\n    </Card>\n  );\n}\n```\n\nMatch the skeleton dimensions to the actual chart for smooth visual transitions.\n\n---\n\n## Adding a New Chart Section\n\nTo add a new chart (e.g., line chart for user growth):\n\n### 1. Create the chart component\n\n`src/features/overview/components/line-graph.tsx`:\n\n```tsx\n'use client';\nimport { Line, LineChart, CartesianGrid, XAxis } from 'recharts';\nimport {\n  type ChartConfig,\n  ChartContainer,\n  ChartTooltip,\n  ChartTooltipContent\n} from '@/components/ui/chart';\n\nconst chartConfig = {\n  users: { label: 'Users', color: 'var(--chart-3)' }\n} satisfies ChartConfig;\n\nconst chartData = [\n  /* monthly user data */\n];\n\nexport function LineGraph() {\n  return (\n    <Card>\n      <CardHeader>\n        <CardTitle>User Growth</CardTitle>\n      </CardHeader>\n      <CardContent>\n        <ChartContainer config={chartConfig} className='aspect-auto h-[250px] w-full'>\n          <LineChart data={chartData}>\n            <CartesianGrid vertical={false} />\n            <XAxis dataKey='month' tickLine={false} axisLine={false} />\n            <ChartTooltip content={<ChartTooltipContent />} />\n            <Line dataKey='users' type='monotone' stroke='var(--color-users)' strokeWidth={2} />\n          </LineChart>\n        </ChartContainer>\n      </CardContent>\n    </Card>\n  );\n}\n```\n\n### 2. Create matching skeleton\n\n`src/features/overview/components/line-graph-skeleton.tsx`\n\n### 3. Create parallel route slot\n\n```\nsrc/app/dashboard/overview/@line_stats/\n├── page.tsx     → async, fetches data, returns <LineGraph />\n├── loading.tsx  → returns <LineGraphSkeleton />\n├── error.tsx    → error alert\n└── default.tsx  → return null (fallback when route doesn't match)\n```\n\n`default.tsx` is required for parallel routes — return `null` or a fallback:\n\n```tsx\nexport default function Default() {\n  return null;\n}\n```\n\n### 4. Add slot to layout\n\nUpdate `src/app/dashboard/overview/layout.tsx`:\n\n```tsx\nexport default function OverviewLayout({\n  sales,\n  pie_stats,\n  bar_stats,\n  area_stats,\n  line_stats // ← add new slot\n}: {\n  /* ...types */\n}) {\n  return (\n    <div className='grid grid-cols-1 gap-4 md:grid-cols-2 lg:grid-cols-7'>\n      {/* existing charts */}\n      <div className='col-span-4'>{line_stats}</div>\n    </div>\n  );\n}\n```\n\n### Available Recharts Components\n\nCommon chart types to use with `ChartContainer`:\n\n- `AreaChart` + `Area` — filled area charts (stacked or standalone)\n- `BarChart` + `Bar` — vertical/horizontal bars\n- `LineChart` + `Line` — line/trend charts\n- `PieChart` + `Pie` — pie/donut charts\n- `RadarChart` + `Radar` — radar/spider charts\n- `RadialBarChart` + `RadialBar` — radial progress bars\n\nAll support `ChartTooltip`, `ChartLegend`, and theme-aware colors via `var(--chart-N)`.\n\n<!-- File: .agents/skills/kiranism-shadcn-dashboard/references/forms-guide.md (Tokens: ~1928 | Category: anthropic-skill) -->\n# Forms Guide\n\n## Table of Contents\n\n1. [Architecture](#architecture)\n2. [Field Types](#field-types)\n3. [Usage Patterns](#usage-patterns)\n4. [Validation Strategies](#validation-strategies)\n5. [Sheet/Dialog Forms](#sheetdialog-forms)\n6. [Multi-Step Forms](#multi-step-forms)\n7. [Advanced Patterns](#advanced-patterns)\n\n---\n\n## Architecture\n\nThe form system is built on **TanStack Form + Zod** with a composable field layer.\n\n**Key files:**\n\n- `src/components/ui/tanstack-form.tsx` — exports `useAppForm`, `useFormFields<T>()`, composed fields\n- `src/components/ui/form-context.tsx` — contexts, `createFormField`, structural components\n- `src/components/forms/fields/*.tsx` — 8 field type implementations\n\n**Key exports:**\n\n```tsx\nimport { useAppForm, useFormFields } from '@/components/ui/tanstack-form';\n```\n\n- `useAppForm(config)` — creates a form instance with `defaultValues`, `validators`, `onSubmit`\n- `useFormFields<T>()` — returns all 8 typed field components with name autocomplete from `T`\n- `form.AppForm` — context provider wrapper\n- `form.Form` — `<form>` element that handles submit\n- `form.SubmitButton` — auto-disabled when form is invalid or submitting\n- `form.AppField` — low-level render prop for custom fields\n\n---\n\n## Field Types\n\nAll fields accept: `name`, `label`, `description`, `required`, `disabled`, `validators`, `listeners`, `className`.\n\n| Component             | Props                                                                                         | Notes                                   |\n| --------------------- | --------------------------------------------------------------------------------------------- | --------------------------------------- |\n| `FormTextField`       | `type` (text/email/number/password/tel/url), `placeholder`, `min`, `max`, `step`, `maxLength` | For numbers use `type='number'`         |\n| `FormTextareaField`   | `placeholder`, `rows`, `maxLength`                                                            | Multiline text                          |\n| `FormSelectField`     | `options: {value, label}[]`, `placeholder`                                                    | Single select dropdown                  |\n| `FormCheckboxField`   | `options?: {value, label}[]`                                                                  | Single checkbox or multi-checkbox group |\n| `FormSwitchField`     | —                                                                                             | Toggle switch                           |\n| `FormRadioGroupField` | `options: {value, label}[]`, `orientation`                                                    | Radio button group                      |\n| `FormSliderField`     | `min`, `max`, `step`                                                                          | Range slider                            |\n| `FormFileUploadField` | `maxSize`, `maxFiles`, `accept`                                                               | Drag-and-drop with preview              |\n\n---\n\n## Usage Patterns\n\n### Pattern 1: `useFormFields<T>()` (Recommended)\n\nType-safe field components with name autocomplete:\n\n```tsx\nconst { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n<FormTextField name='customer' label='Customer' required placeholder='Name'\n  validators={{ onBlur: z.string().min(2) }} />\n\n<FormSelectField name='status' label='Status' required options={STATUS_OPTIONS}\n  validators={{ onBlur: z.string().min(1) }} />\n```\n\n### Pattern 2: `form.AppField` render prop\n\nFull control for custom field rendering:\n\n```tsx\n<form.AppField name='framework'>\n  {(field) => (\n    <field.FieldSet>\n      <field.Field>\n        <field.TextField label='Framework' />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Pattern 3: Direct import (no type safety)\n\nFor quick prototyping:\n\n```tsx\nimport { FormTextField } from '@/components/ui/tanstack-form';\n<FormTextField name='name' label='Name' />;\n```\n\n---\n\n## Validation Strategies\n\n### Field-level (recommended for UX)\n\n```tsx\n<FormTextField\n  name='email'\n  label='Email'\n  validators={{\n    onBlur: z.string().email('Invalid email') // Validates when field loses focus\n  }}\n/>\n```\n\n### Form-level (catch-all on submit)\n\n```tsx\nconst form = useAppForm({\n  validators: { onSubmit: orderSchema }, // Validates entire form on submit\n  onSubmit: async ({ value }) => {\n    /* ... */\n  }\n});\n```\n\n### Async validation (server-side checks)\n\n```tsx\n<FormTextField\n  name='username'\n  label='Username'\n  validators={{\n    onChangeAsync: async ({ value }) => {\n      const exists = await checkUsername(value);\n      return exists ? 'Username taken' : undefined;\n    }\n  }}\n  asyncDebounceMs={500}\n/>\n```\n\n### Linked field validation\n\nFor dependent fields (e.g., confirm password):\n\n```tsx\n<FormTextField\n  name='confirmPassword'\n  label='Confirm Password'\n  validators={{\n    onChangeListenTo: ['password'],\n    onChange: ({ value, fieldApi }) => {\n      const password = fieldApi.form.getFieldValue('password');\n      return value !== password ? 'Passwords must match' : undefined;\n    }\n  }}\n/>\n```\n\n---\n\n## Sheet/Dialog Forms\n\nThe key pattern for forms inside sheets or dialogs: give the `<form.Form>` an `id`, and use that `id` on the submit button's `form` attribute. This allows the submit button to live outside the form element (e.g., in `SheetFooter`).\n\n```tsx\n<form.AppForm>\n  <form.Form id='my-sheet-form' className='space-y-4'>\n    {/* fields */}\n  </form.Form>\n</form.AppForm>;\n\n{\n  /* In SheetFooter — button is outside the <form> but still submits it */\n}\n<SheetFooter>\n  <Button type='submit' form='my-sheet-form'>\n    Save\n  </Button>\n</SheetFooter>;\n```\n\nOn success, call `onOpenChange(false)` to close the sheet and `form.reset()` for create forms.\n\n---\n\n## Multi-Step Forms\n\nUse `withFieldGroup` + `useAppForm` with `StepButton`:\n\n```tsx\n// Define field groups for each step\nconst Step1 = withFieldGroup({\n  fields: ['name', 'email'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='name' label='Name' />\n        <FormTextField name='email' label='Email' />\n        <form.StepButton direction='next' label='Next' />\n      </>\n    );\n  }\n});\n\nconst Step2 = withFieldGroup({\n  fields: ['address', 'city'],\n  render: ({ form }) => {\n    const { FormTextField } = useFormFields<FormValues>();\n    return (\n      <>\n        <FormTextField name='address' label='Address' />\n        <FormTextField name='city' label='City' />\n        <form.StepButton direction='prev' label='Back' />\n        <form.SubmitButton label='Submit' />\n      </>\n    );\n  }\n});\n```\n\nUse the `useStepper` hook from `src/hooks/use-stepper.tsx` to manage step state.\n\n---\n\n## Advanced Patterns\n\n### Nested objects (dot notation)\n\n```tsx\n<FormTextField name='address.street' label='Street' />\n<FormTextField name='address.city' label='City' />\n```\n\n### Dynamic array rows\n\n```tsx\n<form.AppField name='items' mode='array'>\n  {(field) => (\n    <>\n      {field.state.value.map((_, i) => (\n        <form.AppField key={i} name={`items[${i}].name`}>\n          {(subField) => <subField.TextField label={`Item ${i + 1}`} />}\n        </form.AppField>\n      ))}\n      <Button onClick={() => field.pushValue({ name: '' })}>Add Row</Button>\n    </>\n  )}\n</form.AppField>\n```\n\n### Side effects with listeners\n\n```tsx\n<FormSelectField\n  name='country'\n  label='Country'\n  options={countryOptions}\n  listeners={{\n    onChange: ({ value }) => {\n      // Reset city when country changes\n      form.setFieldValue('city', '');\n    }\n  }}\n/>\n```\n\n### Custom field with `form.AppField`\n\nFor fields not covered by the built-in 8 types:\n\n```tsx\n<form.AppField name='color'>\n  {(field) => (\n    <field.FieldSet>\n      <Label>Pick a color</Label>\n      <field.Field>\n        <input\n          type='color'\n          value={field.state.value}\n          onChange={(e) => field.handleChange(e.target.value)}\n        />\n      </field.Field>\n      <field.FieldError />\n    </field.FieldSet>\n  )}\n</form.AppField>\n```\n\n### Form-level errors\n\nDisplay errors that apply to the whole form (e.g., server errors):\n\n```tsx\nimport { FormErrors } from '@/components/ui/form-context';\n\n<form.AppForm>\n  <form.Form>\n    <FormErrors /> {/* Renders form-level validation errors */}\n    {/* fields... */}\n  </form.Form>\n</form.AppForm>;\n```\n\n<!-- File: .agents/skills/kiranism-shadcn-dashboard/references/mock-api-guide.md (Tokens: ~1721 | Category: anthropic-skill) -->\n# Mock API Guide\n\n## Table of Contents\n\n1. [Structure](#structure)\n2. [Full Template](#full-template)\n3. [Key Patterns](#key-patterns)\n4. [Integrating with React Query](#integrating-with-react-query)\n\n---\n\n## Structure\n\nEach mock API file lives in `src/constants/mock-api-<name>.ts` and is a self-contained in-memory database. It uses:\n\n- **faker** for generating sample data\n- **match-sorter** for fuzzy search across fields\n- **delay** (from `./mock-api`) to simulate network latency\n\nThe `delay` function is exported from `src/constants/mock-api.ts`:\n\n```tsx\nexport async function delay(ms: number) {\n  return new Promise((resolve) => setTimeout(resolve, ms));\n}\n```\n\n---\n\n## Full Template\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\n// 1. Define the entity type\nexport type Order = {\n  id: number;\n  customer: string;\n  email: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\n// 2. Create the fake database object\nexport const fakeOrders = {\n  records: [] as Order[],\n\n  // 3. Initialize with faker data\n  initialize() {\n    const statuses = ['pending', 'processing', 'completed', 'cancelled'];\n    for (let i = 1; i <= 20; i++) {\n      this.records.push({\n        id: i,\n        customer: faker.person.fullName(),\n        email: faker.internet.email(),\n        status: faker.helpers.arrayElement(statuses),\n        total: parseFloat(faker.commerce.price({ min: 10, max: 500 })),\n        created_at: faker.date.between({ from: '2023-01-01', to: Date.now() }).toISOString(),\n        updated_at: faker.date.recent().toISOString()\n      });\n    }\n  },\n\n  // 4. Get all with optional search (used internally)\n  async getAll({ search }: { search?: string } = {}) {\n    let items = [...this.records];\n    if (search) {\n      items = matchSorter(items, search, {\n        keys: ['customer', 'email']\n      });\n    }\n    return items;\n  },\n\n  // 5. Paginated list with filtering and sorting\n  async getOrders(params: {\n    page?: number;\n    limit?: number;\n    search?: string;\n    statuses?: string;\n    sort?: string;\n  }) {\n    await delay(800);\n    const { page = 1, limit = 10, search, statuses, sort } = params;\n\n    let items = await this.getAll({ search });\n\n    // Filter by comma-separated values\n    if (statuses) {\n      const statusList = statuses.split('.');\n      items = items.filter((item) => statusList.includes(item.status));\n    }\n\n    // Sort by column\n    if (sort) {\n      const parsedSort = JSON.parse(sort) as { id: string; desc: boolean }[];\n      if (parsedSort.length > 0) {\n        const { id, desc } = parsedSort[0];\n        items.sort((a, b) => {\n          const aVal = a[id as keyof Order];\n          const bVal = b[id as keyof Order];\n          if (aVal < bVal) return desc ? 1 : -1;\n          if (aVal > bVal) return desc ? -1 : 1;\n          return 0;\n        });\n      }\n    }\n\n    // Paginate\n    const total_items = items.length;\n    items = items.slice((page - 1) * limit, page * limit);\n\n    return { items, total_items };\n  },\n\n  // 6. Get single record by ID\n  async getOrderById(id: number) {\n    await delay(800);\n    return this.records.find((r) => r.id === id) || null;\n  },\n\n  // 7. Create\n  async createOrder(data: Omit<Order, 'id' | 'created_at' | 'updated_at'>) {\n    await delay(800);\n    const newRecord: Order = {\n      ...data,\n      id: this.records.length + 1,\n      created_at: new Date().toISOString(),\n      updated_at: new Date().toISOString()\n    };\n    this.records.push(newRecord);\n    return newRecord;\n  },\n\n  // 8. Update\n  async updateOrder(id: number, data: Partial<Order>) {\n    await delay(800);\n    const idx = this.records.findIndex((r) => r.id === id);\n    if (idx === -1) return null;\n    this.records[idx] = {\n      ...this.records[idx],\n      ...data,\n      updated_at: new Date().toISOString()\n    };\n    return this.records[idx];\n  },\n\n  // 9. Delete\n  async deleteOrder(id: number) {\n    await delay(800);\n    this.records = this.records.filter((r) => r.id !== id);\n    return true;\n  }\n};\n\n// 10. Auto-initialize on import\nfakeOrders.initialize();\n```\n\n---\n\n## Key Patterns\n\n### Search with match-sorter\n\nAlways specify which fields to search across:\n\n```tsx\nmatchSorter(items, search, { keys: ['customer', 'email', 'status'] });\n```\n\n### Comma-separated filter values\n\nFor multi-select filters (roles, statuses), the URL param uses `.` as delimiter:\n\n```tsx\nif (statuses) {\n  const list = statuses.split('.');\n  items = items.filter((item) => list.includes(item.status));\n}\n```\n\n### Computed column sorting\n\nWhen a table has a computed column (e.g., combining first_name + last_name into \"name\"), handle it in the sort logic:\n\n```tsx\nif (id === 'name') {\n  const aName = `${a.first_name} ${a.last_name}`;\n  const bName = `${b.first_name} ${b.last_name}`;\n  return desc ? bName.localeCompare(aName) : aName.localeCompare(bName);\n}\n```\n\n### Return shape\n\nList methods must return `{ items, total_items }` (or `{ products, total }` etc. — match the query option expectations). The total is the count **before** pagination, used for `pageCount` calculation.\n\n---\n\n## Integrating with the API Layer\n\nThe mock API is only imported in `service.ts`. Queries and components import from the service and types files:\n\n```\nmock-api-orders.ts  →  api/service.ts  →  api/queries.ts  →  components\n(data source)          (data access)       (key factory +     (useSuspenseQuery\n                                            queryOptions)       + useMutation)\n```\n\n**service.ts** imports from the mock API:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\n```\n\n**queries.ts** imports from service, uses key factories:\n\n```tsx\nimport { getOrders } from './service';\nimport type { OrderFilters } from './types';\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({ queryKey: orderKeys.list(filters), queryFn: () => getOrders(filters) });\n```\n\n**Mutations** in components use service functions + key factories:\n\n```tsx\nimport { createOrder } from '../api/service';\nimport { orderKeys } from '../api/queries';\n\nconst mutation = useMutation({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => queryClient.invalidateQueries({ queryKey: orderKeys.all })\n});\n```\n\n<!-- File: .agents/skills/kiranism-shadcn-dashboard/references/query-abstractions.md (Tokens: ~1177 | Category: anthropic-skill) -->\n# TanStack Query Abstractions (v5)\n\nThe core insight: **`queryOptions` and `mutationOptions` are the right abstraction — not custom hooks.**\n\n---\n\n## Query Abstraction\n\n### The Pattern\n\n```ts\n// queries/invoice.ts\nimport { queryOptions } from '@tanstack/react-query';\n\nexport function invoiceOptions(id: number) {\n  return queryOptions({\n    queryKey: ['invoice', id],\n    queryFn: () => fetchInvoice(id)\n  });\n}\n\nexport function invoiceListOptions(filters: InvoiceFilters) {\n  return queryOptions({\n    queryKey: ['invoices', filters],\n    queryFn: () => fetchInvoices(filters),\n    staleTime: 30_000\n  });\n}\n```\n\n### Usage — always compose at the call site\n\n```ts\n// basic\nconst { data } = useQuery(invoiceOptions(id));\n\n// with suspense — same options, different hook\nconst { data } = useSuspenseQuery(invoiceOptions(id));\n\n// with extra options spread on top — full type inference, no TS pain\nconst { data } = useQuery({\n  ...invoiceOptions(id),\n  select: (invoice) => invoice.createdAt, // data infers as string | undefined\n  enabled: !!id\n});\n\n// prefetch in a route loader (works outside React — this is why hooks are wrong)\nawait queryClient.prefetchQuery(invoiceOptions(id));\n\n// read from cache imperatively — queryKey is typed via DataTag symbol\nconst invoice = queryClient.getQueryData(invoiceOptions(id).queryKey);\n\n// invalidate\nqueryClient.invalidateQueries({ queryKey: invoiceOptions(id).queryKey });\n```\n\n### Why NOT a custom hook\n\nCustom hooks like `useInvoice(id)` have three critical problems:\n\n1. **Hooks only work in components/hooks** — but queries are now used in route loaders, server prefetching, event handlers, and server components. `queryOptions` is just a plain function — works anywhere.\n2. **They share logic, not configuration** — what you actually want to share is the `queryKey` + `queryFn` config. Hooks are the wrong primitive for that.\n3. **They lock you to one hook** — you can't use `useInvoice()` with `useSuspenseQuery`, `useQueries`, or imperative `queryClient` methods.\n\n### Why NOT `UseQueryOptions` type directly\n\n```ts\n// BAD — data becomes unknown\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions>) { ... }\n\n// STILL BAD — select breaks with TS error\nfunction useInvoice(id: number, options?: Partial<UseQueryOptions<Invoice>>) { ... }\n// select: (invoice) => invoice.createdAt\n// Error: Type 'string' is not assignable to type 'Invoice'\n```\n\n`queryOptions` solves this via a `DataTag` symbol on the queryKey — full inference, zero manual generics.\n\n### Custom hooks are still fine on top\n\nIf a component always uses the same composition, a hook is fine — but build it _on top of_ `queryOptions`:\n\n```ts\n// OK — hook built on queryOptions\nfunction useInvoice(id: number) {\n  return useQuery(invoiceOptions(id));\n}\n\n// OK — hook that adds per-feature defaults\nfunction useInvoiceWithSuspense(id: number) {\n  return useSuspenseQuery(invoiceOptions(id));\n}\n```\n\n---\n\n## Mutation Abstraction\n\n### The Pattern\n\n```ts\n// mutations/invoice.ts\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\n\nexport const createInvoiceMutation = mutationOptions({\n  mutationFn: (data: CreateInvoiceInput) => createInvoice(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n\nexport const updateInvoiceMutation = mutationOptions({\n  mutationFn: ({ id, ...data }: UpdateInvoiceInput) => updateInvoice(id, data),\n  onSuccess: (updated) => {\n    const qc = getQueryClient();\n    qc.setQueryData(invoiceOptions(updated.id).queryKey, updated);\n    qc.invalidateQueries({ queryKey: ['invoices'] });\n  }\n});\n```\n\n> **Note on queryClient**: Import `getQueryClient()` directly — do NOT pass `queryClient` as a function argument. The `getQueryClient()` pattern handles both SSR (fresh per request) and client (singleton) correctly.\n\n### Usage\n\n```ts\n// basic\nconst { mutate } = useMutation(createInvoiceMutation);\n\n// composed — add per-usage callbacks on top\nconst { mutate } = useMutation({\n  ...createInvoiceMutation,\n  onError: (err) => toast.error(err.message),\n  onSuccess: (data) => {\n    // this runs AFTER the shared onSuccess above\n    router.push(`/invoices/${data.id}`);\n  }\n});\n```\n\n---\n\n## Rules Summary\n\n| Rule                                                          | Reason                                                                 |\n| ------------------------------------------------------------- | ---------------------------------------------------------------------- |\n| Use `queryOptions()` not custom hooks as the base abstraction | Works everywhere — loaders, server, imperative calls                   |\n| Keep options factories lean — no extra config params          | Best abstractions are not configurable                                 |\n| Compose extra options at the call site via spread             | Full TS inference without manual generics                              |\n| Import `getQueryClient()` in mutation files                   | Handles SSR/client correctly without prop drilling                     |\n| Co-locate `queryKey` inside `queryOptions`                    | Typed key reuse in `invalidateQueries`, `setQueryData`, `getQueryData` |\n| Custom hooks are fine — but built ON TOP of `queryOptions`    | Hooks for component convenience, `queryOptions` for sharing config     |\n\n<!-- File: .agents/skills/kiranism-shadcn-dashboard/references/theming-guide.md (Tokens: ~1559 | Category: anthropic-skill) -->\n# Theme Creation Guide\n\n## Table of Contents\n\n1. [Create Theme CSS](#1-create-theme-css)\n2. [Import Theme](#2-import-theme)\n3. [Register Theme](#3-register-theme)\n4. [Add Custom Fonts](#4-add-custom-fonts-optional)\n5. [Set as Default](#5-set-as-default-optional)\n6. [Required Tokens](#required-tokens)\n7. [Color Format Reference](#color-format-reference)\n\n---\n\n## 1. Create Theme CSS\n\nCreate `src/styles/themes/<name>.css`:\n\n```css\n/* Light mode */\n[data-theme='your-theme'] {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.145 0 0);\n  --card: oklch(...);\n  --card-foreground: oklch(...);\n  --popover: oklch(...);\n  --popover-foreground: oklch(...);\n  --primary: oklch(...);\n  --primary-foreground: oklch(...);\n  --secondary: oklch(...);\n  --secondary-foreground: oklch(...);\n  --muted: oklch(...);\n  --muted-foreground: oklch(...);\n  --accent: oklch(...);\n  --accent-foreground: oklch(...);\n  --destructive: oklch(...);\n  --destructive-foreground: oklch(...);\n  --border: oklch(...);\n  --input: oklch(...);\n  --ring: oklch(...);\n  --chart-1: oklch(...);\n  --chart-2: oklch(...);\n  --chart-3: oklch(...);\n  --chart-4: oklch(...);\n  --chart-5: oklch(...);\n  --sidebar: oklch(...);\n  --sidebar-foreground: oklch(...);\n  --sidebar-primary: oklch(...);\n  --sidebar-primary-foreground: oklch(...);\n  --sidebar-accent: oklch(...);\n  --sidebar-accent-foreground: oklch(...);\n  --sidebar-border: oklch(...);\n  --sidebar-ring: oklch(...);\n  --font-sans: 'Font Name', sans-serif;\n  --font-mono: 'Mono Font', monospace;\n  --radius: 0.5rem;\n  --spacing: 0.25rem;\n}\n\n/* Dark mode */\n[data-theme='your-theme'].dark {\n  --background: oklch(0.145 0 0);\n  --foreground: oklch(0.985 0 0);\n  /* ... all tokens with dark values */\n}\n\n/* Tailwind integration (required) */\n[data-theme='your-theme'] {\n  @theme inline {\n    --color-background: var(--background);\n    --color-foreground: var(--foreground);\n    --color-card: var(--card);\n    --color-card-foreground: var(--card-foreground);\n    --color-popover: var(--popover);\n    --color-popover-foreground: var(--popover-foreground);\n    --color-primary: var(--primary);\n    --color-primary-foreground: var(--primary-foreground);\n    --color-secondary: var(--secondary);\n    --color-secondary-foreground: var(--secondary-foreground);\n    --color-muted: var(--muted);\n    --color-muted-foreground: var(--muted-foreground);\n    --color-accent: var(--accent);\n    --color-accent-foreground: var(--accent-foreground);\n    --color-destructive: var(--destructive);\n    --color-destructive-foreground: var(--destructive-foreground);\n    --color-border: var(--border);\n    --color-input: var(--input);\n    --color-ring: var(--ring);\n    --color-chart-1: var(--chart-1);\n    --color-chart-2: var(--chart-2);\n    --color-chart-3: var(--chart-3);\n    --color-chart-4: var(--chart-4);\n    --color-chart-5: var(--chart-5);\n    --color-sidebar: var(--sidebar);\n    --color-sidebar-foreground: var(--sidebar-foreground);\n    --color-sidebar-primary: var(--sidebar-primary);\n    --color-sidebar-primary-foreground: var(--sidebar-primary-foreground);\n    --color-sidebar-accent: var(--sidebar-accent);\n    --color-sidebar-accent-foreground: var(--sidebar-accent-foreground);\n    --color-sidebar-border: var(--sidebar-border);\n    --color-sidebar-ring: var(--sidebar-ring);\n    --font-sans: var(--font-sans);\n    --font-mono: var(--font-mono);\n    --font-serif: var(--font-serif);\n    --radius-sm: calc(var(--radius) - 4px);\n    --radius-md: calc(var(--radius) - 2px);\n    --radius-lg: var(--radius);\n    --radius-xl: calc(var(--radius) + 4px);\n  }\n}\n```\n\n## 2. Import Theme\n\nAdd to `src/styles/theme.css`:\n\n```css\n@import './themes/your-theme.css';\n```\n\n## 3. Register Theme\n\nAdd to `THEMES` array in `src/components/themes/theme.config.ts`:\n\n```typescript\n{ name: 'Your Theme', value: 'your-theme' }\n```\n\nThe `value` must exactly match the `data-theme` attribute in your CSS.\n\n## 4. Add Custom Fonts (Optional)\n\nOnly if using a Google Font not already loaded.\n\nIn `src/components/themes/font.config.ts`:\n\n```typescript\nimport { Your_Font } from 'next/font/google';\n\nconst fontYourName = Your_Font({\n  subsets: ['latin'],\n  weight: ['400', '500', '700'],\n  variable: '--font-your-name'\n});\n\nexport const fontVariables = cn(\n  // ... existing fonts\n  fontYourName.variable\n);\n```\n\nIn your theme CSS, use the font's **display name** (not the CSS variable):\n\n```css\n--font-sans: 'Your Font', sans-serif;\n```\n\n## 5. Set as Default (Optional)\n\nIn `src/components/themes/theme.config.ts`:\n\n```typescript\nexport const DEFAULT_THEME = 'your-theme';\n```\n\n## Required Tokens\n\nMinimum required: `--background`, `--foreground`, `--card` & `--card-foreground`, `--popover` & `--popover-foreground`, `--primary` & `--primary-foreground`, `--secondary` & `--secondary-foreground`, `--muted` & `--muted-foreground`, `--accent` & `--accent-foreground`, `--destructive` & `--destructive-foreground`, `--border`, `--input`, `--ring`, `--radius`.\n\nOptional: `--chart-*`, `--sidebar-*`, `--font-*`, `--shadow-*`, `--tracking-normal`, `--spacing`.\n\n## Color Format Reference\n\nOKLCH: `oklch(lightness chroma hue)`\n\n- Lightness: 0-1 (0=black, 1=white)\n- Chroma: 0+ (0=gray, higher=saturated)\n- Hue: 0-360 (0=red, 120=green, 240=blue)\n\nSee `src/styles/themes/claude.css` for a complete example.\n\n<!-- File: .agents/skills/kiranism-shadcn-dashboard/SKILL.md (Tokens: ~5634 | Category: anthropic-skill) -->\n---\nname: kiranism-shadcn-dashboard\ndescription: |\n  Guide for building features, pages, tables, forms, themes, and navigation in this Next.js 16 shadcn dashboard template. Use this skill whenever the user wants to add a new page, create a feature module, build a data table, add a form, configure navigation items, add a theme, set up RBAC access control, or work with the dashboard's patterns and conventions. Also triggers when adding routes under /dashboard, working with Clerk auth/orgs/billing, creating mock APIs, or modifying the sidebar. Even if the user doesn't mention \"dashboard\" explicitly — if they're adding UI, pages, or features to this project, use this skill.\n---\n\n# Dashboard Development Guide\n\nThis skill encodes the exact patterns and conventions used in this Next.js 16 + shadcn/ui admin dashboard template.\n\n## Quick Reference: What Goes Where\n\n| Task                  | Location                                |\n| --------------------- | --------------------------------------- |\n| New page              | `src/app/dashboard/<name>/page.tsx`     |\n| New feature module    | `src/features/<name>/`                  |\n| Feature components    | `src/features/<name>/components/`       |\n| API types             | `src/features/<name>/api/types.ts`      |\n| Service layer         | `src/features/<name>/api/service.ts`    |\n| Query options         | `src/features/<name>/api/queries.ts`    |\n| Mutation options      | `src/features/<name>/api/mutations.ts`  |\n| Zod schemas           | `src/features/<name>/schemas/<name>.ts` |\n| Filter/select options | `src/features/<name>/constants/`        |\n| Nav config            | `src/config/nav-config.ts`              |\n| Types                 | `src/types/index.ts`                    |\n| Mock data             | `src/constants/mock-api-<name>.ts`      |\n| Search params         | `src/lib/searchparams.ts`               |\n| Query client          | `src/lib/query-client.ts`               |\n| Theme CSS             | `src/styles/themes/<name>.css`          |\n| Theme registry        | `src/components/themes/theme.config.ts` |\n| Custom hook           | `src/hooks/`                            |\n| Icons registry        | `src/components/icons.tsx`              |\n\n---\n\n## Adding a New Feature (End-to-End)\n\nWhen a user asks to add a feature (e.g., \"add an orders page\"), follow these steps in order. Each step below shows the minimal pattern — see reference files for full templates.\n\n### Step 1: Mock API (`src/constants/mock-api-<name>.ts`)\n\nSee [references/mock-api-guide.md](references/mock-api-guide.md) for the complete template. Key structure:\n\n```tsx\nimport { faker } from '@faker-js/faker';\nimport { matchSorter } from 'match-sorter';\nimport { delay } from './mock-api';\n\nexport type Order = {\n  id: number;\n  customer: string;\n  status: string;\n  total: number;\n  created_at: string;\n  updated_at: string;\n};\n\nexport const fakeOrders = {\n  records: [] as Order[],\n  initialize() {\n    /* generate with faker */\n  },\n  async getOrders({ page, limit, search, sort }) {\n    /* filter, sort, paginate, return { items, total_items } */\n  },\n  async getOrderById(id: number) {\n    /* find by id */\n  },\n  async createOrder(data) {\n    /* push to records */\n  },\n  async updateOrder(id, data) {\n    /* merge into record */\n  },\n  async deleteOrder(id) {\n    /* filter out */\n  }\n};\nfakeOrders.initialize();\n```\n\nEvery method should call `await delay(800)` to simulate network latency. Use `matchSorter` for search. Return `{ items, total_items }` from list methods.\n\n### Step 2: API Layer (`src/features/<name>/api/`)\n\nEach feature has 4 API files: **types** → **service** → **queries** → **mutations**.\n\n**Types** (`api/types.ts`) — re-export the entity type from mock API, plus filter/response/payload types:\n\n```tsx\nexport type { Order } from '@/constants/mock-api-orders';\nexport type OrderFilters = { page?: number; limit?: number; search?: string; sort?: string };\nexport type OrdersResponse = { items: Order[]; total_items: number };\nexport type OrderMutationPayload = { customer: string; status: string; total: number };\n```\n\n**Service** (`api/service.ts`) — data access layer. One exported function per operation:\n\n```tsx\nimport { fakeOrders } from '@/constants/mock-api-orders';\nimport type { OrderFilters, OrdersResponse, OrderMutationPayload } from './types';\n\nexport async function getOrders(filters: OrderFilters): Promise<OrdersResponse> {\n  return fakeOrders.getOrders(filters);\n}\nexport async function getOrderById(id: number) {\n  return fakeOrders.getOrderById(id);\n}\nexport async function createOrder(data: OrderMutationPayload) {\n  return fakeOrders.createOrder(data);\n}\nexport async function updateOrder(id: number, data: OrderMutationPayload) {\n  return fakeOrders.updateOrder(id, data);\n}\nexport async function deleteOrder(id: number) {\n  return fakeOrders.deleteOrder(id);\n}\n```\n\n**Queries** (`api/queries.ts`) — query key factory + query options:\n\n```tsx\nimport { queryOptions } from '@tanstack/react-query';\nimport { getOrders, getOrderById } from './service';\nimport type { Order, OrderFilters } from './types';\n\nexport type { Order };\n\nexport const orderKeys = {\n  all: ['orders'] as const,\n  list: (filters: OrderFilters) => [...orderKeys.all, 'list', filters] as const,\n  detail: (id: number) => [...orderKeys.all, 'detail', id] as const\n};\n\nexport const ordersQueryOptions = (filters: OrderFilters) =>\n  queryOptions({\n    queryKey: orderKeys.list(filters),\n    queryFn: () => getOrders(filters)\n  });\n\nexport const orderByIdOptions = (id: number) =>\n  queryOptions({\n    queryKey: orderKeys.detail(id),\n    queryFn: () => getOrderById(id)\n  });\n```\n\n**Mutations** (`api/mutations.ts`) — use `mutationOptions` + `getQueryClient()` (not custom hooks with `useQueryClient()`):\n\n```tsx\nimport { mutationOptions } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { createOrder, updateOrder, deleteOrder } from './service';\nimport { orderKeys } from './queries';\nimport type { OrderMutationPayload } from './types';\n\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data: OrderMutationPayload) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const updateOrderMutation = mutationOptions({\n  mutationFn: ({ id, values }: { id: number; values: OrderMutationPayload }) =>\n    updateOrder(id, values),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\nexport const deleteOrderMutation = mutationOptions({\n  mutationFn: (id: number) => deleteOrder(id),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n```\n\n`mutationOptions` is the right abstraction because it works outside React (event handlers, tests, utilities), composes via spread at the call site, and uses `getQueryClient()` which handles both SSR (fresh per request) and client (singleton) correctly. See [references/query-abstractions.md](references/query-abstractions.md) for the full rationale.\n\n### Step 3: Zod Schema (`src/features/<name>/schemas/<name>.ts`)\n\n```tsx\nimport { z } from 'zod';\n\nexport const orderSchema = z.object({\n  customer: z.string().min(2, 'Customer name must be at least 2 characters'),\n  status: z.string().min(1, 'Please select a status'),\n  total: z.number({ message: 'Total is required' })\n});\n\nexport type OrderFormValues = z.infer<typeof orderSchema>;\n```\n\n### Step 4: Feature Components\n\nCreate `src/features/<name>/components/` with:\n\n**Listing page** (server component — `<name>-listing.tsx`):\n\n```tsx\nimport { HydrationBoundary, dehydrate } from '@tanstack/react-query';\nimport { getQueryClient } from '@/lib/query-client';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport { ordersQueryOptions } from '../api/queries';\nimport { OrderTable, OrderTableSkeleton } from './orders-table';\nimport { Suspense } from 'react';\n\nexport default function OrderListingPage() {\n  const page = searchParamsCache.get('page');\n  const search = searchParamsCache.get('name');\n  const pageLimit = searchParamsCache.get('perPage');\n  const sort = searchParamsCache.get('sort');\n\n  const filters = {\n    page,\n    limit: pageLimit,\n    ...(search && { search }),\n    ...(sort.length > 0 && { sort: JSON.stringify(sort) })\n  };\n\n  const queryClient = getQueryClient();\n  void queryClient.prefetchQuery(ordersQueryOptions(filters));\n\n  return (\n    <HydrationBoundary state={dehydrate(queryClient)}>\n      <Suspense fallback={<OrderTableSkeleton />}>\n        <OrderTable />\n      </Suspense>\n    </HydrationBoundary>\n  );\n}\n```\n\n**Table + skeleton** (client component — `orders-table/index.tsx`):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { parseAsInteger, parseAsString, useQueryStates } from 'nuqs';\nimport { getSortingStateParser } from '@/lib/data-table-parsers';\nimport { useDataTable } from '@/hooks/use-data-table';\nimport { DataTable } from '@/components/data-table/data-table';\nimport { DataTableToolbar } from '@/components/data-table/data-table-toolbar';\nimport { Skeleton } from '@/components/ui/skeleton';\nimport { ordersQueryOptions } from '../../api/queries';\nimport { columns } from './columns';\n\nexport function OrderTable() {\n  const [params] = useQueryStates({\n    page: parseAsInteger.withDefault(1),\n    perPage: parseAsInteger.withDefault(10),\n    name: parseAsString,\n    sort: getSortingStateParser().withDefault([]) // same parser as searchParamsCache\n  });\n\n  const filters = {\n    page: params.page,\n    limit: params.perPage,\n    ...(params.name && { search: params.name }),\n    ...(params.sort.length > 0 && { sort: JSON.stringify(params.sort) })\n  };\n\n  const { data } = useSuspenseQuery(ordersQueryOptions(filters));\n\n  const { table } = useDataTable({\n    data: data.items,\n    columns,\n    pageCount: Math.ceil(data.total_items / params.perPage),\n    shallow: true,\n    debounceMs: 500,\n    initialState: { columnPinning: { start: [], end: ['actions'] } }\n  });\n\n  return (\n    <DataTable table={table}>\n      <DataTableToolbar table={table} />\n    </DataTable>\n  );\n}\n\nexport function OrderTableSkeleton() {\n  return (\n    <div className='space-y-4 p-4'>\n      <Skeleton className='h-10 w-full' />\n      <Skeleton className='h-96 w-full' />\n    </div>\n  );\n}\n```\n\n**Column definitions** (`orders-table/columns.tsx`):\n\nEach column needs `id`, `accessorKey` (or `accessorFn`), `header` with `DataTableColumnHeader`, and optionally `meta` for filtering + `enableColumnFilter: true`. Type columns as `ColumnDef<DataTableFeatures, Order>[]` (TanStack Table v9; `DataTableFeatures` from `@/lib/data-table-features` types `meta`).\n\n```tsx\nexport const columns: ColumnDef<DataTableFeatures, Order>[] = [\n  {\n    id: 'customer',\n    accessorKey: 'customer',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Customer' />,\n    meta: { label: 'Customer', placeholder: 'Search...', variant: 'text', icon: Icons.text },\n    enableColumnFilter: true\n  },\n  {\n    id: 'status',\n    accessorKey: 'status',\n    header: ({ column }) => <DataTableColumnHeader column={column} label='Status' />,\n    cell: ({ cell }) => (\n      <Badge variant='outline' className='capitalize'>\n        {cell.getValue<string>()}\n      </Badge>\n    ),\n    enableColumnFilter: true,\n    meta: { label: 'Status', variant: 'multiSelect', options: STATUS_OPTIONS }\n  },\n  { id: 'actions', cell: ({ row }) => <CellAction data={row.original} /> }\n];\n```\n\nFilter `meta.variant` options: `text`, `number`, `range`, `date`, `dateRange`, `select`, `multiSelect`, `boolean`. For multiSelect, provide `options: { value, label, icon? }[]`.\n\n**Cell actions** (`orders-table/cell-action.tsx`):\n\nPattern: `DropdownMenu` with edit/delete items + `AlertModal` for delete confirmation + `useMutation` for the delete API call.\n\n```tsx\nimport { deleteOrderMutation } from '../../api/mutations';\n\nexport const CellAction: React.FC<{ data: Order }> = ({ data }) => {\n  const [deleteOpen, setDeleteOpen] = useState(false);\n  const deleteMutation = useMutation({\n    ...deleteOrderMutation,\n    onSuccess: () => {\n      toast.success('Deleted');\n      setDeleteOpen(false);\n    }\n  });\n  return (\n    <>\n      <AlertModal\n        isOpen={deleteOpen}\n        onClose={() => setDeleteOpen(false)}\n        onConfirm={() => deleteMutation.mutate(data.id)}\n        loading={deleteMutation.isPending}\n      />\n      <DropdownMenu modal={false}>\n        <DropdownMenuTrigger asChild>\n          <Button variant='ghost' className='h-8 w-8 p-0'>\n            <Icons.ellipsis className='h-4 w-4' />\n          </Button>\n        </DropdownMenuTrigger>\n        <DropdownMenuContent align='end'>\n          <DropdownMenuLabel>Actions</DropdownMenuLabel>\n          <DropdownMenuItem onClick={() => router.push(`/dashboard/orders/${data.id}`)}>\n            <Icons.edit className='mr-2 h-4 w-4' /> Edit\n          </DropdownMenuItem>\n          <DropdownMenuItem onClick={() => setDeleteOpen(true)}>\n            <Icons.trash className='mr-2 h-4 w-4' /> Delete\n          </DropdownMenuItem>\n        </DropdownMenuContent>\n      </DropdownMenu>\n    </>\n  );\n};\n```\n\nFor **sheet-based editing** (like Users), replace `router.push` with opening a `<FormSheet>` — see the Forms section below.\n\n### Step 5: Page Route (`src/app/dashboard/<name>/page.tsx`)\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderListingPage from '@/features/orders/components/order-listing';\nimport { searchParamsCache } from '@/lib/searchparams';\nimport type { SearchParams } from 'nuqs/server';\n\nexport const metadata = { title: 'Dashboard: Orders' };\ntype PageProps = { searchParams: Promise<SearchParams> };\n\nexport default async function Page(props: PageProps) {\n  const searchParams = await props.searchParams;\n  searchParamsCache.parse(searchParams);\n\n  return (\n    <PageContainer\n      scrollable={false}\n      pageTitle='Orders'\n      pageDescription='Manage your orders.'\n      pageHeaderAction={/* Add button — Link or SheetTrigger */}\n    >\n      <OrderListingPage />\n    </PageContainer>\n  );\n}\n```\n\n**PageContainer props**: `scrollable`, `pageTitle`, `pageDescription`, `pageHeaderAction` (React node for the top-right button), `infoContent` (help sidebar), `access` + `accessFallback` (RBAC gating).\n\n**Detail/Edit page** (`src/app/dashboard/<name>/[id]/page.tsx`):\n\n```tsx\nimport PageContainer from '@/components/layout/page-container';\nimport OrderViewPage from '@/features/orders/components/order-view-page';\n\nexport const metadata = { title: 'Dashboard: Order Details' };\ntype PageProps = { params: Promise<{ id: string }> };\n\nexport default async function Page(props: PageProps) {\n  const { id } = await props.params;\n  return (\n    <PageContainer scrollable pageTitle='Order Details'>\n      <OrderViewPage orderId={id} />\n    </PageContainer>\n  );\n}\n```\n\n**View page component** (client — handles new vs edit):\n\n```tsx\n'use client';\nimport { useSuspenseQuery } from '@tanstack/react-query';\nimport { notFound } from 'next/navigation';\nimport { orderByIdOptions } from '../api/queries';\nimport OrderForm from './order-form';\n\nexport default function OrderViewPage({ orderId }: { orderId: string }) {\n  if (orderId === 'new') return <OrderForm initialData={null} pageTitle='Create Order' />;\n  const { data } = useSuspenseQuery(orderByIdOptions(Number(orderId)));\n  if (!data) notFound();\n  return <OrderForm initialData={data} pageTitle='Edit Order' />;\n}\n```\n\n### Step 6: Search Params (`src/lib/searchparams.ts`)\n\nAdd any new filter keys. Existing params: `page`, `perPage`, `name`, `gender`, `category`, `role`, `sort`.\n\n### Step 7: Navigation (`src/config/nav-config.ts`)\n\n```tsx\n{ title: 'Orders', url: '/dashboard/orders', icon: 'product', items: [] }\n```\n\n### Step 8: Icons (`src/components/icons.tsx`)\n\nTo register a new icon, import from `@tabler/icons-react` and add to the `Icons` object:\n\n```tsx\nimport { IconShoppingCart } from '@tabler/icons-react';\nexport const Icons = { /* ...existing */ cart: IconShoppingCart };\n```\n\nNever import `@tabler/icons-react` anywhere else. Always use `Icons.keyName`.\n\n**Existing icon keys** (partial): `dashboard`, `product`, `kanban`, `chat`, `forms`, `user`, `teams`, `billing`, `settings`, `add`, `edit`, `trash`, `search`, `check`, `close`, `clock`, `ellipsis`, `text`, `calendar`, `upload`, `spinner`, `chevronDown/Left/Right/Up`, `sun`, `moon`, `palette`, `pro`, `workspace`, `notification`.\n\n---\n\n## Forms\n\nForms use **TanStack Form + Zod** with `useAppForm` + `useFormFields<T>()` and `useMutation` for submission. See [references/forms-guide.md](references/forms-guide.md) for all field types, validation strategies, multi-step forms, and advanced patterns.\n\n### Page Form (Create/Edit on a dedicated route)\n\nThe full pattern is shown in Steps 1-4 above. The key structure:\n\n1. **Schema** — Zod schema + inferred type in `schemas/<name>.ts`\n2. **Form component** — `useAppForm({ defaultValues, validators: { onSubmit: schema }, onSubmit })` + `useFormFields<T>()` for typed fields\n3. **Mutations** — `useMutation({ ...createOrderMutation, onSuccess: () => { toast(); router.push() } })`, spread shared mutation options from `api/mutations.ts` and layer on UI callbacks\n4. **View page** — client component that checks `id === 'new'` for create vs `useSuspenseQuery(byIdOptions)` for edit\n\n### Sheet Form (Inline create/edit in a side panel)\n\nFor features where a separate page is overkill (like Users). The sheet manages open state; the form uses a `form` attribute to connect to the sheet footer's submit button.\n\n```tsx\n'use client';\nimport { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';\n\nexport function OrderFormSheet({\n  order,\n  open,\n  onOpenChange\n}: {\n  order?: Order;\n  open: boolean;\n  onOpenChange: (open: boolean) => void;\n}) {\n  const isEdit = !!order;\n  const mutation = useMutation({\n    ...(isEdit ? updateOrderMutation : createOrderMutation),\n    onSuccess: () => {\n      onOpenChange(false);\n    }\n  });\n  const form = useAppForm({\n    defaultValues: { customer: order?.customer ?? '' /* ... */ } as OrderFormValues,\n    validators: { onSubmit: orderSchema },\n    onSubmit: async ({ value }) => {\n      await mutation.mutateAsync(value);\n    }\n  });\n  const { FormTextField, FormSelectField } = useFormFields<OrderFormValues>();\n\n  return (\n    <Sheet open={open} onOpenChange={onOpenChange}>\n      <SheetContent className='flex flex-col'>\n        <SheetHeader>\n          <SheetTitle>{isEdit ? 'Edit' : 'New'} Order</SheetTitle>\n        </SheetHeader>\n        <div className='flex-1 overflow-auto'>\n          <form.AppForm>\n            <form.Form id='order-sheet-form' className='space-y-4'>\n              <FormTextField name='customer' label='Customer' required />\n              <FormSelectField name='status' label='Status' required options={STATUS_OPTIONS} />\n            </form.Form>\n          </form.AppForm>\n        </div>\n        <SheetFooter>\n          <Button variant='outline' onClick={() => onOpenChange(false)}>\n            Cancel\n          </Button>\n          <Button type='submit' form='order-sheet-form' disabled={mutation.isPending}>\n            {mutation.isPending ? 'Saving...' : isEdit ? 'Update' : 'Create'}\n          </Button>\n        </SheetFooter>\n      </SheetContent>\n    </Sheet>\n  );\n}\n```\n\nFor cell actions, add `const [editOpen, setEditOpen] = useState(false)` and render `<OrderFormSheet order={data} open={editOpen} onOpenChange={setEditOpen} />` with a `<DropdownMenuItem onClick={() => setEditOpen(true)}>`. For the page header \"Add\" button, create a trigger component that manages `open` state and renders the sheet.\n\n**Available field components** from `useFormFields<T>()`: `FormTextField`, `FormTextareaField`, `FormSelectField`, `FormCheckboxField`, `FormSwitchField`, `FormRadioGroupField`, `FormSliderField`, `FormFileUploadField`.\n\n---\n\n## Data Fetching with React Query\n\nThe pattern is: server prefetch → HydrationBoundary → client useSuspenseQuery.\n\n1. **Server**: `void queryClient.prefetchQuery(options)` — fire-and-forget during SSR streaming\n2. **Client**: `useSuspenseQuery(options)` — picks up dehydrated data, suspends until resolved\n3. **HydrationBoundary + dehydrate**: bridges server cache → client cache\n4. **Suspense fallback**: skeleton shown while data streams\n\n**Why `useSuspenseQuery` not `useQuery`:** `useQuery` doesn't integrate with Suspense — it shows loading even when data is prefetched. `useSuspenseQuery` picks up the dehydrated pending query. Once cached (within `staleTime: 60s`), subsequent visits are instant.\n\n**Mutations** use `mutationOptions` + `getQueryClient()` in `mutations.ts`, composed via spread at the call site:\n\n```tsx\n// In mutations.ts — shared config\nexport const createOrderMutation = mutationOptions({\n  mutationFn: (data) => createOrder(data),\n  onSuccess: () => {\n    getQueryClient().invalidateQueries({ queryKey: orderKeys.all });\n  }\n});\n\n// In component — spread + layer UI callbacks\nconst mutation = useMutation({\n  ...createOrderMutation,\n  onSuccess: () => toast.success('Created')\n});\n```\n\nSee [references/query-abstractions.md](references/query-abstractions.md) for why `mutationOptions`/`queryOptions` are the right abstraction over custom hooks.\n\n---\n\n## Navigation & RBAC\n\nConfigure in `src/config/nav-config.ts`. Items are filtered client-side in `src/hooks/use-nav.ts` using Clerk.\n\n**Access control properties** on nav items:\n\n- `requireOrg: boolean` — requires active Clerk organization\n- `permission: string` — requires specific Clerk permission\n- `role: string` — requires specific Clerk role\n- `plan: string` — requires subscription plan (server-side)\n- `feature: string` — requires feature flag (server-side)\n\nItems without `access` are visible to everyone. All client-side checks are synchronous — no loading states.\n\n---\n\n## Themes\n\nSee [references/theming-guide.md](references/theming-guide.md) for the complete guide. Quick steps:\n\n1. Create `src/styles/themes/<name>.css` with OKLCH color tokens + `@theme inline` block\n2. Import in `src/styles/theme.css`\n3. Register in `THEMES` array in `src/components/themes/theme.config.ts`\n4. (Optional) Add Google Fonts in `src/components/themes/font.config.ts`\n\n---\n\n## Code Conventions\n\n- **`cn()`** for class merging — never concatenate className strings\n- **Server components by default** — only add `'use client'` when needed\n- **React Query** — `void prefetchQuery()` on server + `useSuspenseQuery` on client\n- **API layer** — `types.ts` → `service.ts` → `queries.ts` → `mutations.ts` per feature; `queryOptions`/`mutationOptions` as base abstractions (not custom hooks); `getQueryClient()` in mutations (not `useQueryClient()`); key factories (`entityKeys.all/list/detail`); components never import mock APIs directly\n- **nuqs** — `searchParamsCache` on server, `useQueryStates` on client with `shallow: true`\n- **Icons** — only from `@/components/icons`, never from `@tabler/icons-react` directly\n- **Forms** — `useAppForm` + `useFormFields<T>()` from `@/components/ui/tanstack-form`\n- **Page headers** — `PageContainer` props, never import `<Heading>` manually\n- **Sort parser** — use `getSortingStateParser` from `@/lib/data-table-parsers` (same parser as `useDataTable`)\n- **Formatting** — single quotes, JSX single quotes, no trailing comma, 2-space indent\n\n<!-- File: .agents/skills/next-best-practices/async-patterns.md (Tokens: ~413 | Category: anthropic-skill) -->\n# Async Patterns\n\nIn Next.js 15+, `params`, `searchParams`, `cookies()`, and `headers()` are asynchronous.\n\n## Async Params and SearchParams\n\nAlways type them as `Promise<...>` and await them.\n\n### Pages and Layouts\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Page({ params }: Props) {\n  const { slug } = await params;\n}\n```\n\n### Route Handlers\n\n```tsx\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n### SearchParams\n\n```tsx\ntype Props = {\n  params: Promise<{ slug: string }>;\n  searchParams: Promise<{ query?: string }>;\n};\n\nexport default async function Page({ params, searchParams }: Props) {\n  const { slug } = await params;\n  const { query } = await searchParams;\n}\n```\n\n### Synchronous Components\n\nUse `React.use()` for non-async components:\n\n```tsx\nimport { use } from 'react';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default function Page({ params }: Props) {\n  const { slug } = use(params);\n}\n```\n\n### generateMetadata\n\n```tsx\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  return { title: slug };\n}\n```\n\n## Async Cookies and Headers\n\n```tsx\nimport { cookies, headers } from 'next/headers';\n\nexport default async function Page() {\n  const cookieStore = await cookies();\n  const headersList = await headers();\n\n  const theme = cookieStore.get('theme');\n  const userAgent = headersList.get('user-agent');\n}\n```\n\n## Migration Codemod\n\n```bash\nnpx @next/codemod@latest next-async-request-api .\n```\n\n<!-- File: .agents/skills/next-best-practices/bundling.md (Tokens: ~1131 | Category: anthropic-skill) -->\n# Bundling\n\nFix common bundling issues with third-party packages.\n\n## Server-Incompatible Packages\n\nSome packages use browser APIs (`window`, `document`, `localStorage`) and fail in Server Components.\n\n### Error Signs\n\n```\nReferenceError: window is not defined\nReferenceError: document is not defined\nReferenceError: localStorage is not defined\nModule not found: Can't resolve 'fs'\n```\n\n### Solution 1: Mark as Client-Only\n\nIf the package is only needed on client:\n\n```tsx\n// Bad: Fails - package uses window\nimport SomeChart from 'some-chart-library';\n\nexport default function Page() {\n  return <SomeChart />;\n}\n\n// Good: Use dynamic import with ssr: false\nimport dynamic from 'next/dynamic';\n\nconst SomeChart = dynamic(() => import('some-chart-library'), {\n  ssr: false\n});\n\nexport default function Page() {\n  return <SomeChart />;\n}\n```\n\n### Solution 2: Externalize from Server Bundle\n\nFor packages that should run on server but have bundling issues:\n\n```js\n// next.config.js\nmodule.exports = {\n  serverExternalPackages: ['problematic-package']\n};\n```\n\nUse this for:\n\n- Packages with native bindings (sharp, bcrypt)\n- Packages that don't bundle well (some ORMs)\n- Packages with circular dependencies\n\n### Solution 3: Client Component Wrapper\n\nWrap the entire usage in a client component:\n\n```tsx\n// components/ChartWrapper.tsx\n'use client';\n\nimport { Chart } from 'chart-library';\n\nexport function ChartWrapper(props) {\n  return <Chart {...props} />;\n}\n\n// app/page.tsx (server component)\nimport { ChartWrapper } from '@/components/ChartWrapper';\n\nexport default function Page() {\n  return <ChartWrapper data={data} />;\n}\n```\n\n## CSS Imports\n\nImport CSS files instead of using `<link>` tags. Next.js handles bundling and optimization.\n\n```tsx\n// Bad: Manual link tag\n<link rel='stylesheet' href='/styles.css' />;\n\n// Good: Import CSS\nimport './styles.css';\n\n// Good: CSS Modules\nimport styles from './Button.module.css';\n```\n\n## Polyfills\n\nNext.js includes common polyfills automatically. Don't load redundant ones from polyfill.io or similar CDNs.\n\nAlready included: `Array.from`, `Object.assign`, `Promise`, `fetch`, `Map`, `Set`, `Symbol`, `URLSearchParams`, and 50+ others.\n\n```tsx\n// Bad: Redundant polyfills\n<script src='https://polyfill.io/v3/polyfill.min.js?features=fetch,Promise,Array.from' />\n\n// Good: Next.js includes these automatically\n```\n\n## ESM/CommonJS Issues\n\n### Error Signs\n\n```\nSyntaxError: Cannot use import statement outside a module\nError: require() of ES Module\nModule not found: ESM packages need to be imported\n```\n\n### Solution: Transpile Package\n\n```js\n// next.config.js\nmodule.exports = {\n  transpilePackages: ['some-esm-package', 'another-package']\n};\n```\n\n## Common Problematic Packages\n\n| Package         | Issue           | Solution                                                        |\n| --------------- | --------------- | --------------------------------------------------------------- |\n| `sharp`         | Native bindings | `serverExternalPackages: ['sharp']`                             |\n| `bcrypt`        | Native bindings | `serverExternalPackages: ['bcrypt']` or use `bcryptjs`          |\n| `canvas`        | Native bindings | `serverExternalPackages: ['canvas']`                            |\n| `recharts`      | Uses window     | `dynamic(() => import('recharts'), { ssr: false })`             |\n| `react-quill`   | Uses document   | `dynamic(() => import('react-quill'), { ssr: false })`          |\n| `mapbox-gl`     | Uses window     | `dynamic(() => import('mapbox-gl'), { ssr: false })`            |\n| `monaco-editor` | Uses window     | `dynamic(() => import('@monaco-editor/react'), { ssr: false })` |\n| `lottie-web`    | Uses document   | `dynamic(() => import('lottie-react'), { ssr: false })`         |\n\n## Bundle Analysis\n\nAnalyze bundle size with the built-in analyzer (Next.js 16.1+):\n\n```bash\nnext experimental-analyze\n```\n\nThis opens an interactive UI to:\n\n- Filter by route, environment (client/server), and type\n- Inspect module sizes and import chains\n- View treemap visualization\n\nSave output for comparison:\n\n```bash\nnext experimental-analyze --output\n# Output saved to .next/diagnostics/analyze\n```\n\nReference: https://nextjs.org/docs/app/guides/package-bundling\n\n## Migrating from Webpack to Turbopack\n\nTurbopack is the default bundler in Next.js 15+. If you have custom webpack config, migrate to Turbopack-compatible alternatives:\n\n```js\n// next.config.js\nmodule.exports = {\n  // Good: Works with Turbopack\n  serverExternalPackages: ['package'],\n  transpilePackages: ['package'],\n\n  // Bad: Webpack-only - migrate away from this\n  webpack: (config) => {\n    // custom webpack config\n  }\n};\n```\n\nReference: https://nextjs.org/docs/app/building-your-application/upgrading/from-webpack-to-turbopack\n\n<!-- File: .agents/skills/next-best-practices/data-patterns.md (Tokens: ~1636 | Category: anthropic-skill) -->\n# Data Patterns\n\nChoose the right data fetching pattern for each use case.\n\n## Decision Tree\n\n```\nNeed to fetch data?\n├── From a Server Component?\n│   └── Use: Fetch directly (no API needed)\n│\n├── From a Client Component?\n│   ├── Is it a mutation (POST/PUT/DELETE)?\n│   │   └── Use: Server Action\n│   └── Is it a read (GET)?\n│       └── Use: Route Handler OR pass from Server Component\n│\n├── Need external API access (webhooks, third parties)?\n│   └── Use: Route Handler\n│\n└── Need REST API for mobile app / external clients?\n    └── Use: Route Handler\n```\n\n## Pattern 1: Server Components (Preferred for Reads)\n\nFetch data directly in Server Components - no API layer needed.\n\n```tsx\n// app/users/page.tsx\nasync function UsersPage() {\n  // Direct database access - no API round-trip\n  const users = await db.user.findMany();\n\n  // Or fetch from external API\n  const posts = await fetch('https://api.example.com/posts').then((r) => r.json());\n\n  return (\n    <ul>\n      {users.map((user) => (\n        <li key={user.id}>{user.name}</li>\n      ))}\n    </ul>\n  );\n}\n```\n\n**Benefits**:\n\n- No API to maintain\n- No client-server waterfall\n- Secrets stay on server\n- Direct database access\n\n## Pattern 2: Server Actions (Preferred for Mutations)\n\nServer Actions are the recommended way to handle mutations.\n\n```tsx\n// app/actions.ts\n'use server';\n\nimport { revalidatePath } from 'next/cache';\n\nexport async function createPost(formData: FormData) {\n  const title = formData.get('title') as string;\n\n  await db.post.create({ data: { title } });\n\n  revalidatePath('/posts');\n}\n\nexport async function deletePost(id: string) {\n  await db.post.delete({ where: { id } });\n\n  revalidateTag('posts');\n}\n```\n\n```tsx\n// app/posts/new/page.tsx\nimport { createPost } from '@/app/actions';\n\nexport default function NewPost() {\n  return (\n    <form action={createPost}>\n      <input name='title' required />\n      <button type='submit'>Create</button>\n    </form>\n  );\n}\n```\n\n**Benefits**:\n\n- End-to-end type safety\n- Progressive enhancement (works without JS)\n- Automatic request handling\n- Integrated with React transitions\n\n**Constraints**:\n\n- POST only (no GET caching semantics)\n- Internal use only (no external access)\n- Cannot return non-serializable data\n\n## Pattern 3: Route Handlers (APIs)\n\nUse Route Handlers when you need a REST API.\n\n```tsx\n// app/api/posts/route.ts\nimport { NextRequest, NextResponse } from 'next/server';\n\n// GET is cacheable\nexport async function GET(request: NextRequest) {\n  const posts = await db.post.findMany();\n  return NextResponse.json(posts);\n}\n\n// POST for mutations\nexport async function POST(request: NextRequest) {\n  const body = await request.json();\n  const post = await db.post.create({ data: body });\n  return NextResponse.json(post, { status: 201 });\n}\n```\n\n**When to use**:\n\n- External API access (mobile apps, third parties)\n- Webhooks from external services\n- GET endpoints that need HTTP caching\n- OpenAPI/Swagger documentation needed\n\n**When NOT to use**:\n\n- Internal data fetching (use Server Components)\n- Mutations from your UI (use Server Actions)\n\n## Avoiding Data Waterfalls\n\n### Problem: Sequential Fetches\n\n```tsx\n// Bad: Sequential waterfalls\nasync function Dashboard() {\n  const user = await getUser(); // Wait...\n  const posts = await getPosts(); // Then wait...\n  const comments = await getComments(); // Then wait...\n\n  return <div>...</div>;\n}\n```\n\n### Solution 1: Parallel Fetching with Promise.all\n\n```tsx\n// Good: Parallel fetching\nasync function Dashboard() {\n  const [user, posts, comments] = await Promise.all([getUser(), getPosts(), getComments()]);\n\n  return <div>...</div>;\n}\n```\n\n### Solution 2: Streaming with Suspense\n\n```tsx\n// Good: Show content progressively\nimport { Suspense } from 'react';\n\nasync function Dashboard() {\n  return (\n    <div>\n      <Suspense fallback={<UserSkeleton />}>\n        <UserSection />\n      </Suspense>\n      <Suspense fallback={<PostsSkeleton />}>\n        <PostsSection />\n      </Suspense>\n    </div>\n  );\n}\n\nasync function UserSection() {\n  const user = await getUser(); // Fetches independently\n  return <div>{user.name}</div>;\n}\n\nasync function PostsSection() {\n  const posts = await getPosts(); // Fetches independently\n  return <PostList posts={posts} />;\n}\n```\n\n### Solution 3: Preload Pattern\n\n```tsx\n// lib/data.ts\nimport { cache } from 'react';\n\nexport const getUser = cache(async (id: string) => {\n  return db.user.findUnique({ where: { id } });\n});\n\nexport const preloadUser = (id: string) => {\n  void getUser(id); // Fire and forget\n};\n```\n\n```tsx\n// app/user/[id]/page.tsx\nimport { getUser, preloadUser } from '@/lib/data';\n\nexport default async function UserPage({ params }) {\n  const { id } = await params;\n\n  // Start fetching early\n  preloadUser(id);\n\n  // Do other work...\n\n  // Data likely ready by now\n  const user = await getUser(id);\n  return <div>{user.name}</div>;\n}\n```\n\n## Client Component Data Fetching\n\nWhen Client Components need data:\n\n### Option 1: Pass from Server Component (Preferred)\n\n```tsx\n// Server Component\nasync function Page() {\n  const data = await fetchData();\n  return <ClientComponent initialData={data} />;\n}\n\n// Client Component\n('use client');\nfunction ClientComponent({ initialData }) {\n  const [data, setData] = useState(initialData);\n  // ...\n}\n```\n\n### Option 2: Fetch on Mount (When Necessary)\n\n```tsx\n'use client';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    fetch('/api/data')\n      .then((r) => r.json())\n      .then(setData);\n  }, []);\n\n  if (!data) return <Loading />;\n  return <div>{data.value}</div>;\n}\n```\n\n### Option 3: Server Action for Reads (Works But Not Ideal)\n\nServer Actions can be called from Client Components for reads, but this is not their intended purpose:\n\n```tsx\n'use client';\nimport { getData } from './actions';\nimport { useEffect, useState } from 'react';\n\nfunction ClientComponent() {\n  const [data, setData] = useState(null);\n\n  useEffect(() => {\n    getData().then(setData);\n  }, []);\n\n  return <div>{data?.value}</div>;\n}\n```\n\n**Note**: Server Actions always use POST, so no HTTP caching. Prefer Route Handlers for cacheable reads.\n\n## Quick Reference\n\n| Pattern                | Use Case                    | HTTP Method | Caching              |\n| ---------------------- | --------------------------- | ----------- | -------------------- |\n| Server Component fetch | Internal reads              | Any         | Full Next.js caching |\n| Server Action          | Mutations, form submissions | POST only   | No                   |\n| Route Handler          | External APIs, webhooks     | Any         | GET can be cached    |\n| Client fetch to API    | Client-side reads           | Any         | HTTP cache headers   |\n\n<!-- File: .agents/skills/next-best-practices/debug-tricks.md (Tokens: ~781 | Category: anthropic-skill) -->\n# Debug Tricks\n\nTricks to speed up debugging Next.js applications.\n\n## MCP Endpoint (Dev Server)\n\nNext.js exposes a `/_next/mcp` endpoint in development for AI-assisted debugging via MCP (Model Context Protocol).\n\n- **Next.js 16+**: Enabled by default, use `next-devtools-mcp`\n- **Next.js < 16**: Requires `experimental.mcpServer: true` in next.config.js\n\nReference: https://nextjs.org/docs/app/guides/mcp\n\n**Important**: Find the actual port of the running Next.js dev server (check terminal output or `package.json` scripts). Don't assume port 3000.\n\n### Request Format\n\nThe endpoint uses JSON-RPC 2.0 over HTTP POST:\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\n    \"jsonrpc\": \"2.0\",\n    \"id\": \"1\",\n    \"method\": \"tools/call\",\n    \"params\": {\n      \"name\": \"<tool-name>\",\n      \"arguments\": {}\n    }\n  }'\n```\n\n### Available Tools\n\n#### `get_errors`\n\nGet current errors from dev server (build errors, runtime errors with source-mapped stacks):\n\n```json\n{ \"name\": \"get_errors\", \"arguments\": {} }\n```\n\n#### `get_routes`\n\nDiscover all routes by scanning filesystem:\n\n```json\n{ \"name\": \"get_routes\", \"arguments\": {} }\n// Optional: { \"name\": \"get_routes\", \"arguments\": { \"routerType\": \"app\" } }\n```\n\nReturns: `{ \"appRouter\": [\"/\", \"/api/users/[id]\", ...], \"pagesRouter\": [...] }`\n\n#### `get_project_metadata`\n\nGet project path and dev server URL:\n\n```json\n{ \"name\": \"get_project_metadata\", \"arguments\": {} }\n```\n\nReturns: `{ \"projectPath\": \"/path/to/project\", \"devServerUrl\": \"http://localhost:3000\" }`\n\n#### `get_page_metadata`\n\nGet runtime metadata about current page render (requires active browser session):\n\n```json\n{ \"name\": \"get_page_metadata\", \"arguments\": {} }\n```\n\nReturns segment trie data showing layouts, boundaries, and page components.\n\n#### `get_logs`\n\nGet path to Next.js development log file:\n\n```json\n{ \"name\": \"get_logs\", \"arguments\": {} }\n```\n\nReturns path to `<distDir>/logs/next-development.log`\n\n#### `get_server_action_by_id`\n\nLocate a Server Action by ID:\n\n```json\n{ \"name\": \"get_server_action_by_id\", \"arguments\": { \"actionId\": \"<action-id>\" } }\n```\n\n### Example: Get Errors\n\n```bash\ncurl -X POST http://localhost:<port>/_next/mcp \\\n  -H \"Content-Type: application/json\" \\\n  -H \"Accept: application/json, text/event-stream\" \\\n  -d '{\"jsonrpc\":\"2.0\",\"id\":\"1\",\"method\":\"tools/call\",\"params\":{\"name\":\"get_errors\",\"arguments\":{}}}'\n```\n\n## Rebuild Specific Routes (Next.js 16+)\n\nUse `--debug-build-paths` to rebuild only specific routes instead of the entire app:\n\n```bash\n# Rebuild a specific route\nnext build --debug-build-paths \"/dashboard\"\n\n# Rebuild routes matching a glob\nnext build --debug-build-paths \"/api/*\"\n\n# Dynamic routes\nnext build --debug-build-paths \"/blog/[slug]\"\n```\n\nUse this to:\n\n- Quickly verify a build fix without full rebuild\n- Debug static generation issues for specific pages\n- Iterate faster on build errors\n\n<!-- File: .agents/skills/next-best-practices/directives.md (Tokens: ~351 | Category: anthropic-skill) -->\n# Directives\n\n## React Directives\n\nThese are React directives, not Next.js specific.\n\n### `'use client'`\n\nMarks a component as a Client Component. Required for:\n\n- React hooks (`useState`, `useEffect`, etc.)\n- Event handlers (`onClick`, `onChange`)\n- Browser APIs (`window`, `localStorage`)\n\n```tsx\n'use client';\n\nimport { useState } from 'react';\n\nexport function Counter() {\n  const [count, setCount] = useState(0);\n  return <button onClick={() => setCount(count + 1)}>{count}</button>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-client\n\n### `'use server'`\n\nMarks a function as a Server Action. Can be passed to Client Components.\n\n```tsx\n'use server';\n\nexport async function submitForm(formData: FormData) {\n  // Runs on server\n}\n```\n\nOr inline within a Server Component:\n\n```tsx\nexport default function Page() {\n  async function submit() {\n    'use server';\n    // Runs on server\n  }\n  return <form action={submit}>...</form>;\n}\n```\n\nReference: https://react.dev/reference/rsc/use-server\n\n---\n\n## Next.js Directive\n\n### `'use cache'`\n\nMarks a function or component for caching. Part of Next.js Cache Components.\n\n```tsx\n'use cache';\n\nexport async function getCachedData() {\n  return await fetchData();\n}\n```\n\nRequires `cacheComponents: true` in `next.config.ts`.\n\nFor detailed usage including cache profiles, `cacheLife()`, `cacheTag()`, and `updateTag()`, see the `next-cache-components` skill.\n\nReference: https://nextjs.org/docs/app/api-reference/directives/use-cache\n\n<!-- File: .agents/skills/next-best-practices/error-handling.md (Tokens: ~1197 | Category: anthropic-skill) -->\n# Error Handling\n\nHandle errors gracefully in Next.js applications.\n\nReference: https://nextjs.org/docs/app/getting-started/error-handling\n\n## Error Boundaries\n\n### `error.tsx`\n\nCatches errors in a route segment and its children:\n\n```tsx\n'use client';\n\nexport default function Error({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <div>\n      <h2>Something went wrong!</h2>\n      <button onClick={() => reset()}>Try again</button>\n    </div>\n  );\n}\n```\n\n**Important:** `error.tsx` must be a Client Component.\n\n### `global-error.tsx`\n\nCatches errors in root layout:\n\n```tsx\n'use client';\n\nexport default function GlobalError({\n  error,\n  reset\n}: {\n  error: Error & { digest?: string };\n  reset: () => void;\n}) {\n  return (\n    <html>\n      <body>\n        <h2>Something went wrong!</h2>\n        <button onClick={() => reset()}>Try again</button>\n      </body>\n    </html>\n  );\n}\n```\n\n**Important:** Must include `<html>` and `<body>` tags.\n\n## Server Actions: Navigation API Gotcha\n\n**Do NOT wrap navigation APIs in try-catch.** They throw special errors that Next.js handles internally.\n\nReference: https://nextjs.org/docs/app/api-reference/functions/redirect#behavior\n\n```tsx\n'use server'\n\nimport { redirect } from 'next/navigation'\nimport { notFound } from 'next/navigation'\n\n// Bad: try-catch catches the navigation \"error\"\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)  // This throws!\n  } catch (error) {\n    // redirect() throw is caught here - navigation fails!\n    return { error: 'Failed to create post' }\n  }\n}\n\n// Good: Call navigation APIs outside try-catch\nasync function createPost(formData: FormData) {\n  let post\n  try {\n    post = await db.post.create({ ... })\n  } catch (error) {\n    return { error: 'Failed to create post' }\n  }\n  redirect(`/posts/${post.id}`)  // Outside try-catch\n}\n\n// Good: Re-throw navigation errors\nasync function createPost(formData: FormData) {\n  try {\n    const post = await db.post.create({ ... })\n    redirect(`/posts/${post.id}`)\n  } catch (error) {\n    if (error instanceof Error && error.message === 'NEXT_REDIRECT') {\n      throw error  // Re-throw navigation errors\n    }\n    return { error: 'Failed to create post' }\n  }\n}\n```\n\nSame applies to:\n\n- `redirect()` - 307 temporary redirect\n- `permanentRedirect()` - 308 permanent redirect\n- `notFound()` - 404 not found\n- `forbidden()` - 403 forbidden\n- `unauthorized()` - 401 unauthorized\n\nUse `unstable_rethrow()` to re-throw these errors in catch blocks:\n\n```tsx\nimport { unstable_rethrow } from 'next/navigation';\n\nasync function action() {\n  try {\n    // ...\n    redirect('/success');\n  } catch (error) {\n    unstable_rethrow(error); // Re-throws Next.js internal errors\n    return { error: 'Something went wrong' };\n  }\n}\n```\n\n## Redirects\n\n```tsx\nimport { redirect, permanentRedirect } from 'next/navigation';\n\n// 307 Temporary - use for most cases\nredirect('/new-path');\n\n// 308 Permanent - use for URL migrations (cached by browsers)\npermanentRedirect('/new-url');\n```\n\n## Auth Errors\n\nTrigger auth-related error pages:\n\n```tsx\nimport { forbidden, unauthorized } from 'next/navigation';\n\nasync function Page() {\n  const session = await getSession();\n\n  if (!session) {\n    unauthorized(); // Renders unauthorized.tsx (401)\n  }\n\n  if (!session.hasAccess) {\n    forbidden(); // Renders forbidden.tsx (403)\n  }\n\n  return <Dashboard />;\n}\n```\n\nCreate corresponding error pages:\n\n```tsx\n// app/forbidden.tsx\nexport default function Forbidden() {\n  return <div>You don't have access to this resource</div>;\n}\n\n// app/unauthorized.tsx\nexport default function Unauthorized() {\n  return <div>Please log in to continue</div>;\n}\n```\n\n## Not Found\n\n### `not-found.tsx`\n\nCustom 404 page for a route segment:\n\n```tsx\nexport default function NotFound() {\n  return (\n    <div>\n      <h2>Not Found</h2>\n      <p>Could not find the requested resource</p>\n    </div>\n  );\n}\n```\n\n### Triggering Not Found\n\n```tsx\nimport { notFound } from 'next/navigation';\n\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const post = await getPost(id);\n\n  if (!post) {\n    notFound(); // Renders closest not-found.tsx\n  }\n\n  return <div>{post.title}</div>;\n}\n```\n\n## Error Hierarchy\n\nErrors bubble up to the nearest error boundary:\n\n```\napp/\n├── error.tsx           # Catches errors from all children\n├── blog/\n│   ├── error.tsx       # Catches errors in /blog/*\n│   └── [slug]/\n│       ├── error.tsx   # Catches errors in /blog/[slug]\n│       └── page.tsx\n└── layout.tsx          # Errors here go to global-error.tsx\n```\n\n<!-- File: .agents/skills/next-best-practices/file-conventions.md (Tokens: ~979 | Category: anthropic-skill) -->\n# File Conventions\n\nNext.js App Router uses file-based routing with special file conventions.\n\n## Project Structure\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure\n\n```\napp/\n├── layout.tsx          # Root layout (required)\n├── page.tsx            # Home page (/)\n├── loading.tsx         # Loading UI\n├── error.tsx           # Error UI\n├── not-found.tsx       # 404 UI\n├── global-error.tsx    # Global error UI\n├── route.ts            # API endpoint\n├── template.tsx        # Re-rendered layout\n├── default.tsx         # Parallel route fallback\n├── blog/\n│   ├── page.tsx        # /blog\n│   └── [slug]/\n│       └── page.tsx    # /blog/:slug\n└── (group)/            # Route group (no URL impact)\n    └── page.tsx\n```\n\n## Special Files\n\n| File            | Purpose                                  |\n| --------------- | ---------------------------------------- |\n| `page.tsx`      | UI for a route segment                   |\n| `layout.tsx`    | Shared UI for segment and children       |\n| `loading.tsx`   | Loading UI (Suspense boundary)           |\n| `error.tsx`     | Error UI (Error boundary)                |\n| `not-found.tsx` | 404 UI                                   |\n| `route.ts`      | API endpoint                             |\n| `template.tsx`  | Like layout but re-renders on navigation |\n| `default.tsx`   | Fallback for parallel routes             |\n\n## Route Segments\n\n```\napp/\n├── blog/               # Static segment: /blog\n├── [slug]/             # Dynamic segment: /:slug\n├── [...slug]/          # Catch-all: /a/b/c\n├── [[...slug]]/        # Optional catch-all: / or /a/b/c\n└── (marketing)/        # Route group (ignored in URL)\n```\n\n## Parallel Routes\n\n```\napp/\n├── @analytics/\n│   └── page.tsx\n├── @sidebar/\n│   └── page.tsx\n└── layout.tsx          # Receives { analytics, sidebar } as props\n```\n\n## Intercepting Routes\n\n```\napp/\n├── feed/\n│   └── page.tsx\n├── @modal/\n│   └── (.)photo/[id]/  # Intercepts /photo/[id] from /feed\n│       └── page.tsx\n└── photo/[id]/\n    └── page.tsx\n```\n\nConventions:\n\n- `(.)` - same level\n- `(..)` - one level up\n- `(..)(..)` - two levels up\n- `(...)` - from root\n\n## Private Folders\n\n```\napp/\n├── _components/        # Private folder (not a route)\n│   └── Button.tsx\n└── page.tsx\n```\n\nPrefix with `_` to exclude from routing.\n\n## Middleware / Proxy\n\n### Next.js 14-15: `middleware.ts`\n\n```ts\n// middleware.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function middleware(request: NextRequest) {\n  // Auth, redirects, rewrites, etc.\n  return NextResponse.next();\n}\n\nexport const config = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n### Next.js 16+: `proxy.ts`\n\nRenamed for clarity - same capabilities, different names:\n\n```ts\n// proxy.ts (root of project)\nimport { NextResponse } from 'next/server';\nimport type { NextRequest } from 'next/server';\n\nexport function proxy(request: NextRequest) {\n  // Same logic as middleware\n  return NextResponse.next();\n}\n\nexport const proxyConfig = {\n  matcher: ['/dashboard/:path*', '/api/:path*']\n};\n```\n\n| Version | File            | Export         | Config        |\n| ------- | --------------- | -------------- | ------------- |\n| v14-15  | `middleware.ts` | `middleware()` | `config`      |\n| v16+    | `proxy.ts`      | `proxy()`      | `proxyConfig` |\n\n**Migration**: Run `npx @next/codemod@latest upgrade` to auto-rename.\n\n## File Conventions Reference\n\nReference: https://nextjs.org/docs/app/api-reference/file-conventions\n\n<!-- File: .agents/skills/next-best-practices/font.md (Tokens: ~1317 | Category: anthropic-skill) -->\n# Font Optimization\n\nUse `next/font` for automatic font optimization with zero layout shift.\n\n## Google Fonts\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({ subsets: ['latin'] });\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={inter.className}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Multiple Fonts\n\n```tsx\nimport { Inter, Roboto_Mono } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nconst robotoMono = Roboto_Mono({\n  subsets: ['latin'],\n  variable: '--font-roboto-mono'\n});\n\nexport default function RootLayout({ children }: { children: React.ReactNode }) {\n  return (\n    <html lang='en' className={`${inter.variable} ${robotoMono.variable}`}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\nUse in CSS:\n\n```css\nbody {\n  font-family: var(--font-inter);\n}\n\ncode {\n  font-family: var(--font-roboto-mono);\n}\n```\n\n## Font Weights and Styles\n\n```tsx\n// Single weight\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: '400'\n});\n\n// Multiple weights\nconst inter = Inter({\n  subsets: ['latin'],\n  weight: ['400', '500', '700']\n});\n\n// Variable font (recommended) - includes all weights\nconst inter = Inter({\n  subsets: ['latin']\n  // No weight needed - variable fonts support all weights\n});\n\n// With italic\nconst inter = Inter({\n  subsets: ['latin'],\n  style: ['normal', 'italic']\n});\n```\n\n## Local Fonts\n\n```tsx\nimport localFont from 'next/font/local';\n\nconst myFont = localFont({\n  src: './fonts/MyFont.woff2'\n});\n\n// Multiple files for different weights\nconst myFont = localFont({\n  src: [\n    {\n      path: './fonts/MyFont-Regular.woff2',\n      weight: '400',\n      style: 'normal'\n    },\n    {\n      path: './fonts/MyFont-Bold.woff2',\n      weight: '700',\n      style: 'normal'\n    }\n  ]\n});\n\n// Variable font\nconst myFont = localFont({\n  src: './fonts/MyFont-Variable.woff2',\n  variable: '--font-my-font'\n});\n```\n\n## Tailwind CSS Integration\n\n```tsx\n// app/layout.tsx\nimport { Inter } from 'next/font/google';\n\nconst inter = Inter({\n  subsets: ['latin'],\n  variable: '--font-inter'\n});\n\nexport default function RootLayout({ children }) {\n  return (\n    <html lang='en' className={inter.variable}>\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n```js\n// tailwind.config.js\nmodule.exports = {\n  theme: {\n    extend: {\n      fontFamily: {\n        sans: ['var(--font-inter)']\n      }\n    }\n  }\n};\n```\n\n## Preloading Subsets\n\nOnly load needed character subsets:\n\n```tsx\n// Latin only (most common)\nconst inter = Inter({ subsets: ['latin'] });\n\n// Multiple subsets\nconst inter = Inter({ subsets: ['latin', 'latin-ext', 'cyrillic'] });\n```\n\n## Display Strategy\n\nControl font loading behavior:\n\n```tsx\nconst inter = Inter({\n  subsets: ['latin'],\n  display: 'swap' // Default - shows fallback, swaps when loaded\n});\n\n// Options:\n// 'auto' - browser decides\n// 'block' - short block period, then swap\n// 'swap' - immediate fallback, swap when ready (recommended)\n// 'fallback' - short block, short swap, then fallback\n// 'optional' - short block, no swap (use if font is optional)\n```\n\n## Don't Use Manual Font Links\n\nAlways use `next/font` instead of `<link>` tags for Google Fonts.\n\n```tsx\n// Bad: Manual link tag (blocks rendering, no optimization)\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Bad: Missing display and preconnect\n<link href=\"https://fonts.googleapis.com/css2?family=Inter\" rel=\"stylesheet\" />\n\n// Good: Use next/font (self-hosted, zero layout shift)\nimport { Inter } from 'next/font/google'\n\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Importing font in every component\n// components/Button.tsx\nimport { Inter } from 'next/font/google'\nconst inter = Inter({ subsets: ['latin'] }) // Creates new instance each time!\n\n// Good: Import once in layout, use CSS variable\n// app/layout.tsx\nconst inter = Inter({ subsets: ['latin'], variable: '--font-inter' })\n\n// Bad: Using @import in CSS (blocks rendering)\n/* globals.css */\n@import url('https://fonts.googleapis.com/css2?family=Inter');\n\n// Good: Use next/font (self-hosted, no network request)\nimport { Inter } from 'next/font/google'\n\n// Bad: Loading all weights when only using a few\nconst inter = Inter({ subsets: ['latin'] }) // Loads all weights\n\n// Good: Specify only needed weights (for non-variable fonts)\nconst inter = Inter({ subsets: ['latin'], weight: ['400', '700'] })\n\n// Bad: Missing subset - loads all characters\nconst inter = Inter({})\n\n// Good: Always specify subset\nconst inter = Inter({ subsets: ['latin'] })\n```\n\n## Font in Specific Components\n\n```tsx\n// For component-specific fonts, export from a shared file\n// lib/fonts.ts\nimport { Inter, Playfair_Display } from 'next/font/google';\n\nexport const inter = Inter({ subsets: ['latin'], variable: '--font-inter' });\nexport const playfair = Playfair_Display({ subsets: ['latin'], variable: '--font-playfair' });\n\n// components/Heading.tsx\nimport { playfair } from '@/lib/fonts';\n\nexport function Heading({ children }) {\n  return <h1 className={playfair.className}>{children}</h1>;\n}\n```\n\n<!-- File: .agents/skills/next-best-practices/functions.md (Tokens: ~1103 | Category: anthropic-skill) -->\n# Functions\n\nNext.js function APIs.\n\nReference: https://nextjs.org/docs/app/api-reference/functions\n\n## Navigation Hooks (Client)\n\n| Hook                        | Purpose                                                        | Reference                                                                                |\n| --------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------- |\n| `useRouter`                 | Programmatic navigation (`push`, `replace`, `back`, `refresh`) | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-router)                   |\n| `usePathname`               | Get current pathname                                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-pathname)                 |\n| `useSearchParams`           | Read URL search parameters                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-search-params)            |\n| `useParams`                 | Access dynamic route parameters                                | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-params)                   |\n| `useSelectedLayoutSegment`  | Active child segment (one level)                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segment)  |\n| `useSelectedLayoutSegments` | All active segments below layout                               | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-selected-layout-segments) |\n| `useLinkStatus`             | Check link prefetch status                                     | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-link-status)              |\n| `useReportWebVitals`        | Report Core Web Vitals metrics                                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/use-report-web-vitals)        |\n\n## Server Functions\n\n| Function     | Purpose                                      | Reference                                                              |\n| ------------ | -------------------------------------------- | ---------------------------------------------------------------------- |\n| `cookies`    | Read/write cookies                           | [Docs](https://nextjs.org/docs/app/api-reference/functions/cookies)    |\n| `headers`    | Read request headers                         | [Docs](https://nextjs.org/docs/app/api-reference/functions/headers)    |\n| `draftMode`  | Enable preview of unpublished CMS content    | [Docs](https://nextjs.org/docs/app/api-reference/functions/draft-mode) |\n| `after`      | Run code after response finishes streaming   | [Docs](https://nextjs.org/docs/app/api-reference/functions/after)      |\n| `connection` | Wait for connection before dynamic rendering | [Docs](https://nextjs.org/docs/app/api-reference/functions/connection) |\n| `userAgent`  | Parse User-Agent header                      | [Docs](https://nextjs.org/docs/app/api-reference/functions/userAgent)  |\n\n## Generate Functions\n\n| Function                | Purpose                                 | Reference                                                                           |\n| ----------------------- | --------------------------------------- | ----------------------------------------------------------------------------------- |\n| `generateStaticParams`  | Pre-render dynamic routes at build time | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-static-params)  |\n| `generateMetadata`      | Dynamic metadata                        | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-metadata)       |\n| `generateViewport`      | Dynamic viewport config                 | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-viewport)       |\n| `generateSitemaps`      | Multiple sitemaps for large sites       | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-sitemaps)       |\n| `generateImageMetadata` | Multiple OG images per route            | [Docs](https://nextjs.org/docs/app/api-reference/functions/generate-image-metadata) |\n\n## Request/Response\n\n| Function        | Purpose                        | Reference                                                                  |\n| --------------- | ------------------------------ | -------------------------------------------------------------------------- |\n| `NextRequest`   | Extended Request with helpers  | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-request)   |\n| `NextResponse`  | Extended Response with helpers | [Docs](https://nextjs.org/docs/app/api-reference/functions/next-response)  |\n| `ImageResponse` | Generate OG images             | [Docs](https://nextjs.org/docs/app/api-reference/functions/image-response) |\n\n## Common Examples\n\n### Navigation\n\nUse `next/link` for internal navigation instead of `<a>` tags.\n\n```tsx\n// Bad: Plain anchor tag\n<a href='/about'>About</a>;\n\n// Good: Next.js Link\nimport Link from 'next/link';\n\n<Link href='/about'>About</Link>;\n```\n\nActive link styling:\n\n```tsx\n'use client';\n\nimport Link from 'next/link';\nimport { usePathname } from 'next/navigation';\n\nexport function NavLink({ href, children }) {\n  const pathname = usePathname();\n\n  return (\n    <Link href={href} className={pathname === href ? 'active' : ''}>\n      {children}\n    </Link>\n  );\n}\n```\n\n### Static Generation\n\n```tsx\n// app/blog/[slug]/page.tsx\nexport async function generateStaticParams() {\n  const posts = await getPosts();\n  return posts.map((post) => ({ slug: post.slug }));\n}\n```\n\n### After Response\n\n```tsx\nimport { after } from 'next/server';\n\nexport async function POST(request: Request) {\n  const data = await processRequest(request);\n\n  after(async () => {\n    await logAnalytics(data);\n  });\n\n  return Response.json({ success: true });\n}\n```\n\n<!-- File: .agents/skills/next-best-practices/hydration-error.md (Tokens: ~431 | Category: anthropic-skill) -->\n# Hydration Errors\n\nDiagnose and fix React hydration mismatch errors.\n\n## Error Signs\n\n- \"Hydration failed because the initial UI does not match\"\n- \"Text content does not match server-rendered HTML\"\n\n## Debugging\n\nIn development, click the hydration error to see the server/client diff.\n\n## Common Causes and Fixes\n\n### Browser-only APIs\n\n```tsx\n// Bad: Causes mismatch - window doesn't exist on server\n<div>{window.innerWidth}</div>;\n\n// Good: Use client component with mounted check\n('use client');\nimport { useState, useEffect } from 'react';\n\nexport function ClientOnly({ children }: { children: React.ReactNode }) {\n  const [mounted, setMounted] = useState(false);\n  useEffect(() => setMounted(true), []);\n  return mounted ? children : null;\n}\n```\n\n### Date/Time Rendering\n\nServer and client may be in different timezones:\n\n```tsx\n// Bad: Causes mismatch\n<span>{new Date().toLocaleString()}</span>;\n\n// Good: Render on client only\n('use client');\nconst [time, setTime] = useState<string>();\nuseEffect(() => setTime(new Date().toLocaleString()), []);\n```\n\n### Random Values or IDs\n\n```tsx\n// Bad: Random values differ between server and client\n<div id={Math.random().toString()}>\n\n// Good: Use useId hook\nimport { useId } from 'react'\n\nfunction Input() {\n  const id = useId()\n  return <input id={id} />\n}\n```\n\n### Invalid HTML Nesting\n\n```tsx\n// Bad: Invalid - div inside p\n<p><div>Content</div></p>\n\n// Bad: Invalid - p inside p\n<p><p>Nested</p></p>\n\n// Good: Valid nesting\n<div><p>Content</p></div>\n```\n\n### Third-party Scripts\n\nScripts that modify DOM during hydration.\n\n```tsx\n// Good: Use next/script with afterInteractive\nimport Script from 'next/script';\n\nexport default function Page() {\n  return <Script src='https://example.com/script.js' strategy='afterInteractive' />;\n}\n```\n\n<!-- File: .agents/skills/next-best-practices/image.md (Tokens: ~1027 | Category: anthropic-skill) -->\n# Image Optimization\n\nUse `next/image` for automatic image optimization.\n\n## Always Use next/image\n\n```tsx\n// Bad: Avoid native img\n<img src='/hero.png' alt='Hero' />;\n\n// Good: Use next/image\nimport Image from 'next/image';\n<Image src='/hero.png' alt='Hero' width={800} height={400} />;\n```\n\n## Required Props\n\nImages need explicit dimensions to prevent layout shift:\n\n```tsx\n// Local images - dimensions inferred automatically\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" />\n\n// Remote images - must specify width/height\n<Image src=\"https://example.com/image.jpg\" alt=\"Hero\" width={800} height={400} />\n\n// Or use fill for parent-relative sizing\n<div style={{ position: 'relative', width: '100%', height: 400 }}>\n  <Image src=\"/hero.png\" alt=\"Hero\" fill style={{ objectFit: 'cover' }} />\n</div>\n```\n\n## Remote Images Configuration\n\nRemote domains must be configured in `next.config.js`:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    remotePatterns: [\n      {\n        protocol: 'https',\n        hostname: 'example.com',\n        pathname: '/images/**'\n      },\n      {\n        protocol: 'https',\n        hostname: '*.cdn.com' // Wildcard subdomain\n      }\n    ]\n  }\n};\n```\n\n## Responsive Images\n\nUse `sizes` to tell the browser which size to download:\n\n```tsx\n// Full-width hero\n<Image\n  src=\"/hero.png\"\n  alt=\"Hero\"\n  fill\n  sizes=\"100vw\"\n/>\n\n// Responsive grid (3 columns on desktop, 1 on mobile)\n<Image\n  src=\"/card.png\"\n  alt=\"Card\"\n  fill\n  sizes=\"(max-width: 768px) 100vw, 33vw\"\n/>\n\n// Fixed sidebar image\n<Image\n  src=\"/avatar.png\"\n  alt=\"Avatar\"\n  width={200}\n  height={200}\n  sizes=\"200px\"\n/>\n```\n\n## Blur Placeholder\n\nPrevent layout shift with placeholders:\n\n```tsx\n// Local images - automatic blur hash\nimport heroImage from './hero.png'\n<Image src={heroImage} alt=\"Hero\" placeholder=\"blur\" />\n\n// Remote images - provide blurDataURL\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"blur\"\n  blurDataURL=\"data:image/jpeg;base64,/9j/4AAQSkZJRg...\"\n/>\n\n// Or use color placeholder\n<Image\n  src=\"https://example.com/image.jpg\"\n  alt=\"Hero\"\n  width={800}\n  height={400}\n  placeholder=\"empty\"\n  style={{ backgroundColor: '#e0e0e0' }}\n/>\n```\n\n## Priority Loading\n\nUse `priority` for above-the-fold images (LCP):\n\n```tsx\n// Hero image - loads immediately\n<Image src=\"/hero.png\" alt=\"Hero\" fill priority />\n\n// Below-fold images - lazy loaded by default (no priority needed)\n<Image src=\"/card.png\" alt=\"Card\" width={400} height={300} />\n```\n\n## Common Mistakes\n\n```tsx\n// Bad: Missing sizes with fill - downloads largest image\n<Image src=\"/hero.png\" alt=\"Hero\" fill />\n\n// Good: Add sizes for proper responsive behavior\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" />\n\n// Bad: Using width/height for aspect ratio only\n<Image src=\"/hero.png\" alt=\"Hero\" width={16} height={9} />\n\n// Good: Use actual display dimensions or fill with sizes\n<Image src=\"/hero.png\" alt=\"Hero\" fill sizes=\"100vw\" style={{ objectFit: 'cover' }} />\n\n// Bad: Remote image without config\n<Image src=\"https://untrusted.com/image.jpg\" alt=\"Image\" width={400} height={300} />\n// Error: Invalid src prop, hostname not configured\n\n// Good: Add hostname to next.config.js remotePatterns\n```\n\n## Static Export\n\nWhen using `output: 'export'`, use `unoptimized` or custom loader:\n\n```tsx\n// Option 1: Disable optimization\n<Image src='/hero.png' alt='Hero' width={800} height={400} unoptimized />;\n\n// Option 2: Global config\n// next.config.js\nmodule.exports = {\n  output: 'export',\n  images: { unoptimized: true }\n};\n\n// Option 3: Custom loader (Cloudinary, Imgix, etc.)\nconst cloudinaryLoader = ({ src, width, quality }) => {\n  return `https://res.cloudinary.com/demo/image/upload/w_${width},q_${quality || 75}/${src}`;\n};\n\n<Image loader={cloudinaryLoader} src='sample.jpg' alt='Sample' width={800} height={400} />;\n```\n\n<!-- File: .agents/skills/next-best-practices/metadata.md (Tokens: ~1809 | Category: anthropic-skill) -->\n# Metadata\n\nAdd SEO metadata to Next.js pages using the Metadata API.\n\n## Important: Server Components Only\n\nThe `metadata` object and `generateMetadata` function are **only supported in Server Components**. They cannot be used in Client Components.\n\nIf the target page has `'use client'`:\n\n1. Remove `'use client'` if possible, move client logic to child components\n2. Or extract metadata to a parent Server Component layout\n3. Or split the file: Server Component with metadata imports Client Components\n\n## Static Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\nexport const metadata: Metadata = {\n  title: 'Page Title',\n  description: 'Page description for search engines'\n};\n```\n\n## Dynamic Metadata\n\n```tsx\nimport type { Metadata } from 'next';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport async function generateMetadata({ params }: Props): Promise<Metadata> {\n  const { slug } = await params;\n  const post = await getPost(slug);\n  return { title: post.title, description: post.description };\n}\n```\n\n## Avoid Duplicate Fetches\n\nUse React `cache()` when the same data is needed for both metadata and page:\n\n```tsx\nimport { cache } from 'react';\n\nexport const getPost = cache(async (slug: string) => {\n  return await db.posts.findFirst({ where: { slug } });\n});\n```\n\n## Viewport\n\nSeparate from metadata for streaming support:\n\n```tsx\nimport type { Viewport } from 'next';\n\nexport const viewport: Viewport = {\n  width: 'device-width',\n  initialScale: 1,\n  themeColor: '#000000'\n};\n\n// Or dynamic\nexport function generateViewport({ params }): Viewport {\n  return { themeColor: getThemeColor(params) };\n}\n```\n\n## Title Templates\n\nIn root layout for consistent naming:\n\n```tsx\nexport const metadata: Metadata = {\n  title: { default: 'Site Name', template: '%s | Site Name' }\n};\n```\n\n## Metadata File Conventions\n\nReference: https://nextjs.org/docs/app/getting-started/project-structure#metadata-file-conventions\n\nPlace these files in `app/` directory (or route segments):\n\n| File                            | Purpose                                       |\n| ------------------------------- | --------------------------------------------- |\n| `favicon.ico`                   | Favicon                                       |\n| `icon.png` / `icon.svg`         | App icon                                      |\n| `apple-icon.png`                | Apple app icon                                |\n| `opengraph-image.png`           | OG image                                      |\n| `twitter-image.png`             | Twitter card image                            |\n| `sitemap.ts` / `sitemap.xml`    | Sitemap (use `generateSitemaps` for multiple) |\n| `robots.ts` / `robots.txt`      | Robots directives                             |\n| `manifest.ts` / `manifest.json` | Web app manifest                              |\n\n## SEO Best Practice: Static Files Are Often Enough\n\nFor most sites, **static metadata files provide excellent SEO coverage**:\n\n```\napp/\n├── favicon.ico\n├── opengraph-image.png     # Works for both OG and Twitter\n├── sitemap.ts\n├── robots.ts\n└── layout.tsx              # With title/description metadata\n```\n\n**Tips:**\n\n- A single `opengraph-image.png` covers both Open Graph and Twitter (Twitter falls back to OG)\n- Static `title` and `description` in layout metadata is sufficient for most pages\n- Only use dynamic `generateMetadata` when content varies per page\n\n---\n\n# OG Image Generation\n\nGenerate dynamic Open Graph images using `next/og`.\n\n## Important Rules\n\n1. **Use `next/og`** - not `@vercel/og` (it's built into Next.js)\n2. **No searchParams** - OG images can't access search params, use route params instead\n3. **Avoid Edge runtime** - Use default Node.js runtime\n\n```tsx\n// Good\nimport { ImageResponse } from 'next/og';\n\n// Bad\n// import { ImageResponse } from '@vercel/og'\n// export const runtime = 'edge'\n```\n\n## Basic OG Image\n\n```tsx\n// app/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Site Name';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\nexport default function Image() {\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 128,\n        background: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        alignItems: 'center',\n        justifyContent: 'center'\n      }}\n    >\n      Hello World\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Dynamic OG Image\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport const alt = 'Blog Post';\nexport const size = { width: 1200, height: 630 };\nexport const contentType = 'image/png';\n\ntype Props = { params: Promise<{ slug: string }> };\n\nexport default async function Image({ params }: Props) {\n  const { slug } = await params;\n  const post = await getPost(slug);\n\n  return new ImageResponse(\n    <div\n      style={{\n        fontSize: 48,\n        background: 'linear-gradient(to bottom, #1a1a1a, #333)',\n        color: 'white',\n        width: '100%',\n        height: '100%',\n        display: 'flex',\n        flexDirection: 'column',\n        alignItems: 'center',\n        justifyContent: 'center',\n        padding: 48\n      }}\n    >\n      <div style={{ fontSize: 64, fontWeight: 'bold' }}>{post.title}</div>\n      <div style={{ marginTop: 24, opacity: 0.8 }}>{post.description}</div>\n    </div>,\n    { ...size }\n  );\n}\n```\n\n## Custom Fonts\n\n```tsx\nimport { ImageResponse } from 'next/og';\nimport { join } from 'path';\nimport { readFile } from 'fs/promises';\n\nexport default async function Image() {\n  const fontPath = join(process.cwd(), 'assets/fonts/Inter-Bold.ttf');\n  const fontData = await readFile(fontPath);\n\n  return new ImageResponse(\n    <div style={{ fontFamily: 'Inter', fontSize: 64 }}>Custom Font Text</div>,\n    {\n      width: 1200,\n      height: 630,\n      fonts: [{ name: 'Inter', data: fontData, style: 'normal' }]\n    }\n  );\n}\n```\n\n## File Naming\n\n- `opengraph-image.tsx` - Open Graph (Facebook, LinkedIn)\n- `twitter-image.tsx` - Twitter/X cards (optional, falls back to OG)\n\n## Styling Notes\n\nImageResponse uses Flexbox layout:\n\n- Use `display: 'flex'`\n- No CSS Grid support\n- Styles must be inline objects\n\n## Multiple OG Images\n\nUse `generateImageMetadata` for multiple images per route:\n\n```tsx\n// app/blog/[slug]/opengraph-image.tsx\nimport { ImageResponse } from 'next/og';\n\nexport async function generateImageMetadata({ params }) {\n  const images = await getPostImages(params.slug);\n  return images.map((img, idx) => ({\n    id: idx,\n    alt: img.alt,\n    size: { width: 1200, height: 630 },\n    contentType: 'image/png'\n  }));\n}\n\nexport default async function Image({ params, id }) {\n  const images = await getPostImages(params.slug);\n  const image = images[id];\n  return new ImageResponse(/* ... */);\n}\n```\n\n## Multiple Sitemaps\n\nUse `generateSitemaps` for large sites:\n\n```tsx\n// app/sitemap.ts\nimport type { MetadataRoute } from 'next';\n\nexport async function generateSitemaps() {\n  // Return array of sitemap IDs\n  return [{ id: 0 }, { id: 1 }, { id: 2 }];\n}\n\nexport default async function sitemap({ id }: { id: number }): Promise<MetadataRoute.Sitemap> {\n  const start = id * 50000;\n  const end = start + 50000;\n  const products = await getProducts(start, end);\n\n  return products.map((product) => ({\n    url: `https://example.com/product/${product.id}`,\n    lastModified: product.updatedAt\n  }));\n}\n```\n\nGenerates `/sitemap/0.xml`, `/sitemap/1.xml`, etc.\n\n<!-- File: .agents/skills/next-best-practices/parallel-routes.md (Tokens: ~1840 | Category: anthropic-skill) -->\n# Parallel & Intercepting Routes\n\nParallel routes render multiple pages in the same layout. Intercepting routes show a different UI when navigating from within your app vs direct URL access. Together they enable modal patterns.\n\n## File Structure\n\n```\napp/\n├── @modal/                    # Parallel route slot\n│   ├── default.tsx            # Required! Returns null\n│   ├── (.)photos/             # Intercepts /photos/*\n│   │   └── [id]/\n│   │       └── page.tsx       # Modal content\n│   └── [...]catchall/         # Optional: catch unmatched\n│       └── page.tsx\n├── photos/\n│   └── [id]/\n│       └── page.tsx           # Full page (direct access)\n├── layout.tsx                 # Renders both children and @modal\n└── page.tsx\n```\n\n## Step 1: Root Layout with Slot\n\n```tsx\n// app/layout.tsx\nexport default function RootLayout({\n  children,\n  modal\n}: {\n  children: React.ReactNode;\n  modal: React.ReactNode;\n}) {\n  return (\n    <html>\n      <body>\n        {children}\n        {modal}\n      </body>\n    </html>\n  );\n}\n```\n\n## Step 2: Default File (Critical!)\n\n**Every parallel route slot MUST have a `default.tsx`** to prevent 404s on hard navigation.\n\n```tsx\n// app/@modal/default.tsx\nexport default function Default() {\n  return null;\n}\n```\n\nWithout this file, refreshing any page will 404 because Next.js can't determine what to render in the `@modal` slot.\n\n## Step 3: Intercepting Route (Modal)\n\nThe `(.)` prefix intercepts routes at the same level.\n\n```tsx\n// app/@modal/(.)photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoModal({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Step 4: Full Page (Direct Access)\n\n```tsx\n// app/photos/[id]/page.tsx\nexport default async function PhotoPage({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  return (\n    <div className='full-page'>\n      <img src={photo.url} alt={photo.title} />\n      <h1>{photo.title}</h1>\n    </div>\n  );\n}\n```\n\n## Step 5: Modal Component with Correct Closing\n\n**Critical: Use `router.back()` to close modals, NOT `router.push()` or `<Link>`.**\n\n```tsx\n// components/modal.tsx\n'use client';\n\nimport { useRouter } from 'next/navigation';\nimport { useCallback, useEffect, useRef } from 'react';\n\nexport function Modal({ children }: { children: React.ReactNode }) {\n  const router = useRouter();\n  const overlayRef = useRef<HTMLDivElement>(null);\n\n  // Close on escape key\n  useEffect(() => {\n    function onKeyDown(e: KeyboardEvent) {\n      if (e.key === 'Escape') {\n        router.back(); // Correct\n      }\n    }\n    document.addEventListener('keydown', onKeyDown);\n    return () => document.removeEventListener('keydown', onKeyDown);\n  }, [router]);\n\n  // Close on overlay click\n  const handleOverlayClick = useCallback(\n    (e: React.MouseEvent) => {\n      if (e.target === overlayRef.current) {\n        router.back(); // Correct\n      }\n    },\n    [router]\n  );\n\n  return (\n    <div\n      ref={overlayRef}\n      onClick={handleOverlayClick}\n      className='fixed inset-0 bg-black/50 flex items-center justify-center z-50'\n    >\n      <div className='bg-white rounded-lg p-6 max-w-2xl w-full mx-4'>\n        <button\n          onClick={() => router.back()} // Correct!\n          className='absolute top-4 right-4'\n        >\n          Close\n        </button>\n        {children}\n      </div>\n    </div>\n  );\n}\n```\n\n### Why NOT `router.push('/')` or `<Link href=\"/\">`?\n\nUsing `push` or `Link` to \"close\" a modal:\n\n1. Adds a new history entry (back button shows modal again)\n2. Doesn't properly clear the intercepted route\n3. Can cause the modal to flash or persist unexpectedly\n\n`router.back()` correctly:\n\n1. Removes the intercepted route from history\n2. Returns to the previous page\n3. Properly unmounts the modal\n\n## Route Matcher Reference\n\nMatchers match **route segments**, not filesystem paths:\n\n| Matcher    | Matches       | Example                                                               |\n| ---------- | ------------- | --------------------------------------------------------------------- |\n| `(.)`      | Same level    | `@modal/(.)photos` intercepts `/photos`                               |\n| `(..)`     | One level up  | `@modal/(..)settings` from `/dashboard/@modal` intercepts `/settings` |\n| `(..)(..)` | Two levels up | Rarely used                                                           |\n| `(...)`    | From root     | `@modal/(...)photos` intercepts `/photos` from anywhere               |\n\n**Common mistake**: Thinking `(..)` means \"parent folder\" - it means \"parent route segment\".\n\n## Handling Hard Navigation\n\nWhen users directly visit `/photos/123` (bookmark, refresh, shared link):\n\n- The intercepting route is bypassed\n- The full `photos/[id]/page.tsx` renders\n- Modal doesn't appear (expected behavior)\n\nIf you want the modal to appear on direct access too, you need additional logic:\n\n```tsx\n// app/photos/[id]/page.tsx\nimport { Modal } from '@/components/modal';\n\nexport default async function PhotoPage({ params }) {\n  const { id } = await params;\n  const photo = await getPhoto(id);\n\n  // Option: Render as modal on direct access too\n  return (\n    <Modal>\n      <img src={photo.url} alt={photo.title} />\n    </Modal>\n  );\n}\n```\n\n## Common Gotchas\n\n### 1. Missing `default.tsx` → 404 on Refresh\n\nEvery `@slot` folder needs a `default.tsx` that returns `null` (or appropriate content).\n\n### 2. Modal Persists After Navigation\n\nYou're using `router.push()` instead of `router.back()`.\n\n### 3. Nested Parallel Routes Need Defaults Too\n\nIf you have `@modal` inside a route group, each level needs its own `default.tsx`:\n\n```\napp/\n├── (marketing)/\n│   ├── @modal/\n│   │   └── default.tsx     # Needed!\n│   └── layout.tsx\n└── layout.tsx\n```\n\n### 4. Intercepted Route Shows Wrong Content\n\nCheck your matcher:\n\n- `(.)photos` intercepts `/photos` from the same route level\n- If your `@modal` is in `app/dashboard/@modal`, use `(.)photos` to intercept `/dashboard/photos`, not `/photos`\n\n### 5. TypeScript Errors with `params`\n\nIn Next.js 15+, `params` is a Promise:\n\n```tsx\n// Correct\nexport default async function Page({ params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n}\n```\n\n## Complete Example: Photo Gallery Modal\n\n```\napp/\n├── @modal/\n│   ├── default.tsx\n│   └── (.)photos/\n│       └── [id]/\n│           └── page.tsx\n├── photos/\n│   ├── page.tsx           # Gallery grid\n│   └── [id]/\n│       └── page.tsx       # Full photo page\n├── layout.tsx\n└── page.tsx\n```\n\nLinks in the gallery:\n\n```tsx\n// app/photos/page.tsx\nimport Link from 'next/link';\n\nexport default async function Gallery() {\n  const photos = await getPhotos();\n\n  return (\n    <div className='grid grid-cols-3 gap-4'>\n      {photos.map((photo) => (\n        <Link key={photo.id} href={`/photos/${photo.id}`}>\n          <img src={photo.thumbnail} alt={photo.title} />\n        </Link>\n      ))}\n    </div>\n  );\n}\n```\n\nClicking a photo → Modal opens (intercepted)\nDirect URL → Full page renders\nRefresh while modal open → Full page renders\n\n<!-- File: .agents/skills/next-best-practices/route-handlers.md (Tokens: ~836 | Category: anthropic-skill) -->\n# Route Handlers\n\nCreate API endpoints with `route.ts` files.\n\n## Basic Usage\n\n```tsx\n// app/api/users/route.ts\nexport async function GET() {\n  const users = await getUsers();\n  return Response.json(users);\n}\n\nexport async function POST(request: Request) {\n  const body = await request.json();\n  const user = await createUser(body);\n  return Response.json(user, { status: 201 });\n}\n```\n\n## Supported Methods\n\n`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`, `OPTIONS`\n\n## GET Handler Conflicts with page.tsx\n\n**A `route.ts` and `page.tsx` cannot coexist in the same folder.**\n\n```\napp/\n├── api/\n│   └── users/\n│       └── route.ts    # /api/users\n└── users/\n    ├── page.tsx        # /users (page)\n    └── route.ts        # Warning: Conflicts with page.tsx!\n```\n\nIf you need both a page and an API at the same path, use different paths:\n\n```\napp/\n├── users/\n│   └── page.tsx        # /users (page)\n└── api/\n    └── users/\n        └── route.ts    # /api/users (API)\n```\n\n## Environment Behavior\n\nRoute handlers run in a **Server Component-like environment**:\n\n- Yes: Can use `async/await`\n- Yes: Can access `cookies()`, `headers()`\n- Yes: Can use Node.js APIs\n- No: Cannot use React hooks\n- No: Cannot use React DOM APIs\n- No: Cannot use browser APIs\n\n```tsx\n// Bad: This won't work - no React DOM in route handlers\nimport { renderToString } from 'react-dom/server';\n\nexport async function GET() {\n  const html = renderToString(<Component />); // Error!\n  return new Response(html);\n}\n```\n\n## Dynamic Route Handlers\n\n```tsx\n// app/api/users/[id]/route.ts\nexport async function GET(request: Request, { params }: { params: Promise<{ id: string }> }) {\n  const { id } = await params;\n  const user = await getUser(id);\n\n  if (!user) {\n    return Response.json({ error: 'Not found' }, { status: 404 });\n  }\n\n  return Response.json(user);\n}\n```\n\n## Request Helpers\n\n```tsx\nexport async function GET(request: Request) {\n  // URL and search params\n  const { searchParams } = new URL(request.url);\n  const query = searchParams.get('q');\n\n  // Headers\n  const authHeader = request.headers.get('authorization');\n\n  // Cookies (Next.js helper)\n  const cookieStore = await cookies();\n  const token = cookieStore.get('token');\n\n  return Response.json({ query, token });\n}\n```\n\n## Response Helpers\n\n```tsx\n// JSON response\nreturn Response.json({ data });\n\n// With status\nreturn Response.json({ error: 'Not found' }, { status: 404 });\n\n// With headers\nreturn Response.json(data, {\n  headers: {\n    'Cache-Control': 'max-age=3600'\n  }\n});\n\n// Redirect\nreturn Response.redirect(new URL('/login', request.url));\n\n// Stream\nreturn new Response(stream, {\n  headers: { 'Content-Type': 'text/event-stream' }\n});\n```\n\n## When to Use Route Handlers vs Server Actions\n\n| Use Case                 | Route Handlers | Server Actions |\n| ------------------------ | -------------- | -------------- |\n| Form submissions         | No             | Yes            |\n| Data mutations from UI   | No             | Yes            |\n| Third-party webhooks     | Yes            | No             |\n| External API consumption | Yes            | No             |\n| Public REST API          | Yes            | No             |\n| File uploads             | Both work      | Both work      |\n\n**Prefer Server Actions** for mutations triggered from your UI.\n**Use Route Handlers** for external integrations and public APIs.\n\n<!-- File: .agents/skills/next-best-practices/rsc-boundaries.md (Tokens: ~1062 | Category: anthropic-skill) -->\n# RSC Boundaries\n\nDetect and prevent invalid patterns when crossing Server/Client component boundaries.\n\n## Detection Rules\n\n### 1. Async Client Components Are Invalid\n\nClient components **cannot** be async functions. Only Server Components can be async.\n\n**Detect:** File has `'use client'` AND component is `async function` or returns `Promise`\n\n```tsx\n// Bad: async client component\n'use client';\nexport default async function UserProfile() {\n  const user = await getUser(); // Cannot await in client component\n  return <div>{user.name}</div>;\n}\n\n// Good: Remove async, fetch data in parent server component\n// page.tsx (server component - no 'use client')\nexport default async function Page() {\n  const user = await getUser();\n  return <UserProfile user={user} />;\n}\n\n// UserProfile.tsx (client component)\n('use client');\nexport function UserProfile({ user }: { user: User }) {\n  return <div>{user.name}</div>;\n}\n```\n\n```tsx\n// Bad: async arrow function client component\n'use client';\nconst Dashboard = async () => {\n  const data = await fetchDashboard();\n  return <div>{data}</div>;\n};\n\n// Good: Fetch in server component, pass data down\n```\n\n### 2. Non-Serializable Props to Client Components\n\nProps passed from Server → Client must be JSON-serializable.\n\n**Detect:** Server component passes these to a client component:\n\n- Functions (except Server Actions with `'use server'`)\n- `Date` objects\n- `Map`, `Set`, `WeakMap`, `WeakSet`\n- Class instances\n- `Symbol` (unless globally registered)\n- Circular references\n\n```tsx\n// Bad: Function prop\n// page.tsx (server)\nexport default function Page() {\n  const handleClick = () => console.log('clicked');\n  return <ClientButton onClick={handleClick} />;\n}\n\n// Good: Define function inside client component\n// ClientButton.tsx\n('use client');\nexport function ClientButton() {\n  const handleClick = () => console.log('clicked');\n  return <button onClick={handleClick}>Click</button>;\n}\n```\n\n```tsx\n// Bad: Date object (silently becomes string, then crashes)\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt} />; // Date object\n}\n\n// PostCard.tsx (client) - will crash on .getFullYear()\n('use client');\nexport function PostCard({ createdAt }: { createdAt: Date }) {\n  return <span>{createdAt.getFullYear()}</span>; // Runtime error!\n}\n\n// Good: Serialize to string on server\n// page.tsx (server)\nexport default async function Page() {\n  const post = await getPost();\n  return <PostCard createdAt={post.createdAt.toISOString()} />;\n}\n\n// PostCard.tsx (client)\n('use client');\nexport function PostCard({ createdAt }: { createdAt: string }) {\n  const date = new Date(createdAt);\n  return <span>{date.getFullYear()}</span>;\n}\n```\n\n```tsx\n// Bad: Class instance\nconst user = new UserModel(data)\n<ClientProfile user={user} /> // Methods will be stripped\n\n// Good: Pass plain object\nconst user = await getUser()\n<ClientProfile user={{ id: user.id, name: user.name }} />\n```\n\n```tsx\n// Bad: Map/Set\n<ClientComponent items={new Map([['a', 1]])} />\n\n// Good: Convert to array/object\n<ClientComponent items={Object.fromEntries(map)} />\n<ClientComponent items={Array.from(set)} />\n```\n\n### 3. Server Actions Are the Exception\n\nFunctions marked with `'use server'` CAN be passed to client components.\n\n```tsx\n// Valid: Server Action can be passed\n// actions.ts\n'use server';\nexport async function submitForm(formData: FormData) {\n  // server-side logic\n}\n\n// page.tsx (server)\nimport { submitForm } from './actions';\nexport default function Page() {\n  return <ClientForm onSubmit={submitForm} />; // OK!\n}\n\n// ClientForm.tsx (client)\n('use client');\nexport function ClientForm({ onSubmit }: { onSubmit: (data: FormData) => Promise<void> }) {\n  return <form action={onSubmit}>...</form>;\n}\n```\n\n## Quick Reference\n\n| Pattern                           | Valid? | Fix                                   |\n| --------------------------------- | ------ | ------------------------------------- |\n| `'use client'` + `async function` | No     | Fetch in server parent, pass data     |\n| Pass `() => {}` to client         | No     | Define in client or use server action |\n| Pass `new Date()` to client       | No     | Use `.toISOString()`                  |\n| Pass `new Map()` to client        | No     | Convert to object/array               |\n| Pass class instance to client     | No     | Pass plain object                     |\n| Pass server action to client      | Yes    | -                                     |\n| Pass `string/number/boolean`      | Yes    | -                                     |\n| Pass plain object/array           | Yes    | -                                     |\n\n<!-- File: .agents/skills/next-best-practices/runtime-selection.md (Tokens: ~220 | Category: anthropic-skill) -->\n# Runtime Selection\n\n## Use Node.js Runtime by Default\n\nUse the default Node.js runtime for new routes and pages. Only use Edge runtime if the project already uses it or there's a specific requirement.\n\n```tsx\n// Good: Default - no runtime config needed (uses Node.js)\nexport default function Page() { ... }\n\n// Caution: Only if already used in project or specifically required\nexport const runtime = 'edge'\n```\n\n## When to Use Each\n\n### Node.js Runtime (Default)\n\n- Full Node.js API support\n- File system access (`fs`)\n- Full `crypto` support\n- Database connections\n- Most npm packages work\n\n### Edge Runtime\n\n- Only for specific edge-location latency requirements\n- Limited API (no `fs`, limited `crypto`)\n- Smaller cold start\n- Geographic distribution needs\n\n## Detection\n\n**Before adding `runtime = 'edge'`**, check:\n\n1. Does the project already use Edge runtime?\n2. Is there a specific latency requirement?\n3. Are all dependencies Edge-compatible?\n\nIf unsure, use Node.js runtime.\n\n<!-- File: .agents/skills/next-best-practices/scripts.md (Tokens: ~882 | Category: anthropic-skill) -->\n# Scripts\n\nLoading third-party scripts in Next.js.\n\n## Use next/script\n\nAlways use `next/script` instead of native `<script>` tags for better performance.\n\n```tsx\n// Bad: Native script tag\n<script src='https://example.com/script.js'></script>;\n\n// Good: Next.js Script component\nimport Script from 'next/script';\n\n<Script src='https://example.com/script.js' />;\n```\n\n## Inline Scripts Need ID\n\nInline scripts require an `id` attribute for Next.js to track them.\n\n```tsx\n// Bad: Missing id\n<Script dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Has id\n<Script id=\"my-script\" dangerouslySetInnerHTML={{ __html: 'console.log(\"hi\")' }} />\n\n// Good: Inline with id\n<Script id=\"show-banner\">\n  {`document.getElementById('banner').classList.remove('hidden')`}\n</Script>\n```\n\n## Don't Put Script in Head\n\n`next/script` should not be placed inside `next/head`. It handles its own positioning.\n\n```tsx\n// Bad: Script inside Head\nimport Head from 'next/head'\nimport Script from 'next/script'\n\n<Head>\n  <Script src=\"/analytics.js\" />\n</Head>\n\n// Good: Script outside Head\n<Head>\n  <title>Page</title>\n</Head>\n<Script src=\"/analytics.js\" />\n```\n\n## Loading Strategies\n\n```tsx\n// afterInteractive (default) - Load after page is interactive\n<Script src=\"/analytics.js\" strategy=\"afterInteractive\" />\n\n// lazyOnload - Load during idle time\n<Script src=\"/widget.js\" strategy=\"lazyOnload\" />\n\n// beforeInteractive - Load before page is interactive (use sparingly)\n// Only works in app/layout.tsx or pages/_document.js\n<Script src=\"/critical.js\" strategy=\"beforeInteractive\" />\n\n// worker - Load in web worker (experimental)\n<Script src=\"/heavy.js\" strategy=\"worker\" />\n```\n\n## Google Analytics\n\nUse `@next/third-parties` instead of inline GA scripts.\n\n```tsx\n// Bad: Inline GA script\n<Script src=\"https://www.googletagmanager.com/gtag/js?id=G-XXXXX\" />\n<Script id=\"ga-init\">\n  {`window.dataLayer = window.dataLayer || [];\n    function gtag(){dataLayer.push(arguments);}\n    gtag('js', new Date());\n    gtag('config', 'G-XXXXX');`}\n</Script>\n\n// Good: Next.js component\nimport { GoogleAnalytics } from '@next/third-parties/google'\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <body>{children}</body>\n      <GoogleAnalytics gaId=\"G-XXXXX\" />\n    </html>\n  )\n}\n```\n\n## Google Tag Manager\n\n```tsx\nimport { GoogleTagManager } from '@next/third-parties/google';\n\nexport default function Layout({ children }) {\n  return (\n    <html>\n      <GoogleTagManager gtmId='GTM-XXXXX' />\n      <body>{children}</body>\n    </html>\n  );\n}\n```\n\n## Other Third-Party Scripts\n\n```tsx\n// YouTube embed\nimport { YouTubeEmbed } from '@next/third-parties/google';\n\n<YouTubeEmbed videoid='dQw4w9WgXcQ' />;\n\n// Google Maps\nimport { GoogleMapsEmbed } from '@next/third-parties/google';\n\n<GoogleMapsEmbed apiKey='YOUR_API_KEY' mode='place' q='Brooklyn+Bridge,New+York,NY' />;\n```\n\n## Quick Reference\n\n| Pattern                                       | Issue                      | Fix                       |\n| --------------------------------------------- | -------------------------- | ------------------------- |\n| `<script src=\"...\">`                          | No optimization            | Use `next/script`         |\n| `<Script>` without id                         | Can't track inline scripts | Add `id` attribute        |\n| `<Script>` inside `<Head>`                    | Wrong placement            | Move outside Head         |\n| Inline GA/GTM scripts                         | No optimization            | Use `@next/third-parties` |\n| `strategy=\"beforeInteractive\"` outside layout | Won't work                 | Only use in root layout   |\n\n<!-- File: .agents/skills/next-best-practices/self-hosting.md (Tokens: ~2173 | Category: anthropic-skill) -->\n# Self-Hosting Next.js\n\nDeploy Next.js outside of Vercel with confidence.\n\n## Quick Start: Standalone Output\n\nFor Docker or any containerized deployment, use standalone output:\n\n```js\n// next.config.js\nmodule.exports = {\n  output: 'standalone'\n};\n```\n\nThis creates a minimal `standalone` folder with only production dependencies:\n\n```\n.next/\n├── standalone/\n│   ├── server.js          # Entry point\n│   ├── node_modules/      # Only production deps\n│   └── .next/             # Build output\n└── static/                # Must be copied separately\n```\n\n## Docker Deployment\n\n### Dockerfile\n\n```dockerfile\nFROM node:20-alpine AS base\n\n# Install dependencies\nFROM base AS deps\nWORKDIR /app\nCOPY package.json package-lock.json* ./\nRUN npm ci\n\n# Build\nFROM base AS builder\nWORKDIR /app\nCOPY --from=deps /app/node_modules ./node_modules\nCOPY . .\nRUN npm run build\n\n# Production\nFROM base AS runner\nWORKDIR /app\n\nENV NODE_ENV=production\n\n# Create non-root user\nRUN addgroup --system --gid 1001 nodejs\nRUN adduser --system --uid 1001 nextjs\n\n# Copy standalone output\nCOPY --from=builder /app/.next/standalone ./\nCOPY --from=builder /app/.next/static ./.next/static\nCOPY --from=builder /app/public ./public\n\nUSER nextjs\n\nEXPOSE 3000\nENV PORT=3000\nENV HOSTNAME=\"0.0.0.0\"\n\nCMD [\"node\", \"server.js\"]\n```\n\n### Docker Compose\n\n```yaml\nversion: '3.8'\n\nservices:\n  web:\n    build: .\n    ports:\n      - '3000:3000'\n    environment:\n      - NODE_ENV=production\n    restart: unless-stopped\n    healthcheck:\n      test: ['CMD', 'wget', '-q', '--spider', 'http://localhost:3000/api/health']\n      interval: 30s\n      timeout: 10s\n      retries: 3\n```\n\n## PM2 Deployment\n\nFor traditional server deployments:\n\n```js\n// ecosystem.config.js\nmodule.exports = {\n  apps: [\n    {\n      name: 'nextjs',\n      script: '.next/standalone/server.js',\n      instances: 'max',\n      exec_mode: 'cluster',\n      env: {\n        NODE_ENV: 'production',\n        PORT: 3000\n      }\n    }\n  ]\n};\n```\n\n```bash\nnpm run build\npm2 start ecosystem.config.js\n```\n\n## ISR and Cache Handlers\n\n### The Problem\n\nISR (Incremental Static Regeneration) uses filesystem caching by default. This **breaks with multiple instances**:\n\n- Instance A regenerates page → saves to its local disk\n- Instance B serves stale page → doesn't see Instance A's cache\n- Load balancer sends users to random instances → inconsistent content\n\n### Solution: Custom Cache Handler\n\nNext.js 14+ supports custom cache handlers for shared storage:\n\n```js\n// next.config.js\nmodule.exports = {\n  cacheHandler: require.resolve('./cache-handler.js'),\n  cacheMaxMemorySize: 0 // Disable in-memory cache\n};\n```\n\n#### Redis Cache Handler Example\n\n```js\n// cache-handler.js\nconst Redis = require('ioredis');\n\nconst redis = new Redis(process.env.REDIS_URL);\nconst CACHE_PREFIX = 'nextjs:';\n\nmodule.exports = class CacheHandler {\n  constructor(options) {\n    this.options = options;\n  }\n\n  async get(key) {\n    const data = await redis.get(CACHE_PREFIX + key);\n    if (!data) return null;\n\n    const parsed = JSON.parse(data);\n    return {\n      value: parsed.value,\n      lastModified: parsed.lastModified\n    };\n  }\n\n  async set(key, data, ctx) {\n    const cacheData = {\n      value: data,\n      lastModified: Date.now()\n    };\n\n    // Set TTL based on revalidate option\n    if (ctx?.revalidate) {\n      await redis.setex(CACHE_PREFIX + key, ctx.revalidate, JSON.stringify(cacheData));\n    } else {\n      await redis.set(CACHE_PREFIX + key, JSON.stringify(cacheData));\n    }\n  }\n\n  async revalidateTag(tags) {\n    // Implement tag-based invalidation\n    // This requires tracking which keys have which tags\n  }\n};\n```\n\n#### S3 Cache Handler Example\n\n```js\n// cache-handler.js\nconst { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');\n\nconst s3 = new S3Client({ region: process.env.AWS_REGION });\nconst BUCKET = process.env.CACHE_BUCKET;\n\nmodule.exports = class CacheHandler {\n  async get(key) {\n    try {\n      const response = await s3.send(\n        new GetObjectCommand({\n          Bucket: BUCKET,\n          Key: `cache/${key}`\n        })\n      );\n      const body = await response.Body.transformToString();\n      return JSON.parse(body);\n    } catch (err) {\n      if (err.name === 'NoSuchKey') return null;\n      throw err;\n    }\n  }\n\n  async set(key, data, ctx) {\n    await s3.send(\n      new PutObjectCommand({\n        Bucket: BUCKET,\n        Key: `cache/${key}`,\n        Body: JSON.stringify({\n          value: data,\n          lastModified: Date.now()\n        }),\n        ContentType: 'application/json'\n      })\n    );\n  }\n};\n```\n\n## What Works vs What Needs Setup\n\n| Feature              | Single Instance | Multi-Instance      | Notes                       |\n| -------------------- | --------------- | ------------------- | --------------------------- |\n| SSR                  | Yes             | Yes                 | No special setup            |\n| SSG                  | Yes             | Yes                 | Built at deploy time        |\n| ISR                  | Yes             | Needs cache handler | Filesystem cache breaks     |\n| Image Optimization   | Yes             | Yes                 | CPU-intensive, consider CDN |\n| Middleware           | Yes             | Yes                 | Runs on Node.js             |\n| Edge Runtime         | Limited         | Limited             | Some features Node-only     |\n| `revalidatePath/Tag` | Yes             | Needs cache handler | Must share cache            |\n| `next/font`          | Yes             | Yes                 | Fonts bundled at build      |\n| Draft Mode           | Yes             | Yes                 | Cookie-based                |\n\n## Image Optimization\n\nNext.js Image Optimization works out of the box but is CPU-intensive.\n\n### Option 1: Built-in (Simple)\n\nWorks automatically, but consider:\n\n- Set `deviceSizes` and `imageSizes` in config to limit variants\n- Use `minimumCacheTTL` to reduce regeneration\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    minimumCacheTTL: 60 * 60 * 24, // 24 hours\n    deviceSizes: [640, 750, 1080, 1920] // Limit sizes\n  }\n};\n```\n\n### Option 2: External Loader (Recommended for Scale)\n\nOffload to Cloudinary, Imgix, or similar:\n\n```js\n// next.config.js\nmodule.exports = {\n  images: {\n    loader: 'custom',\n    loaderFile: './lib/image-loader.js'\n  }\n};\n```\n\n```js\n// lib/image-loader.js\nexport default function cloudinaryLoader({ src, width, quality }) {\n  const params = ['f_auto', 'c_limit', `w_${width}`, `q_${quality || 'auto'}`];\n  return `https://res.cloudinary.com/demo/image/upload/${params.join(',')}${src}`;\n}\n```\n\n## Environment Variables\n\n### Build-time vs Runtime\n\n```js\n// Available at build time only (baked into bundle)\nNEXT_PUBLIC_API_URL=https://api.example.com\n\n// Available at runtime (server-side only)\nDATABASE_URL=postgresql://...\nAPI_SECRET=...\n```\n\n### Runtime Configuration\n\nFor truly dynamic config, don't use `NEXT_PUBLIC_*`. Instead:\n\n```tsx\n// app/api/config/route.ts\nexport async function GET() {\n  return Response.json({\n    apiUrl: process.env.API_URL,\n    features: process.env.FEATURES?.split(',')\n  });\n}\n```\n\n## OpenNext: Serverless Without Vercel\n\n[OpenNext](https://open-next.js.org/) adapts Next.js for AWS Lambda, Cloudflare Workers, etc.\n\n```bash\nnpx create-sst@latest\n# or\nnpx @opennextjs/aws build\n```\n\nSupports:\n\n- AWS Lambda + CloudFront\n- Cloudflare Workers\n- Netlify Functions\n- Deno Deploy\n\n## Health Check Endpoint\n\nAlways include a health check for load balancers:\n\n```tsx\n// app/api/health/route.ts\nexport async function GET() {\n  try {\n    // Optional: check database connection\n    // await db.$queryRaw`SELECT 1`;\n\n    return Response.json({ status: 'healthy' }, { status: 200 });\n  } catch (error) {\n    return Response.json({ status: 'unhealthy' }, { status: 503 });\n  }\n}\n```\n\n## Pre-Deployment Checklist\n\n1. **Build locally first**: `npm run build` - catch errors before deploy\n2. **Test standalone output**: `node .next/standalone/server.js`\n3. **Set `output: 'standalone'`** for Docker\n4. **Configure cache handler** for multi-instance ISR\n5. **Set `HOSTNAME=\"0.0.0.0\"`** for containers\n6. **Copy `public/` and `.next/static/`** - not included in standalone\n7. **Add health check endpoint**\n8. **Test ISR revalidation** after deployment\n9. **Monitor memory usage** - Node.js defaults may need tuning\n\n## Testing Cache Handler\n\n**Critical**: Test your cache handler on every Next.js upgrade:\n\n```bash\n# Start multiple instances\nPORT=3001 node .next/standalone/server.js &\nPORT=3002 node .next/standalone/server.js &\n\n# Trigger ISR revalidation\ncurl http://localhost:3001/api/revalidate?path=/posts\n\n# Verify both instances see the update\ncurl http://localhost:3001/posts\ncurl http://localhost:3002/posts\n# Should return identical content\n```\n\n<!-- File: .agents/skills/next-best-practices/SKILL.md (Tokens: ~1011 | Category: anthropic-skill) -->\n---\nname: next-best-practices\ndescription: Next.js best practices - file conventions, RSC boundaries, data patterns, async APIs, metadata, error handling, route handlers, image/font optimization, bundling\nuser-invocable: false\n---\n\n# Next.js Best Practices\n\nApply these rules when writing or reviewing Next.js code.\n\n## File Conventions\n\nSee [file-conventions.md](./file-conventions.md) for:\n\n- Project structure and special files\n- Route segments (dynamic, catch-all, groups)\n- Parallel and intercepting routes\n- Middleware rename in v16 (middleware → proxy)\n\n## RSC Boundaries\n\nDetect invalid React Server Component patterns.\n\nSee [rsc-boundaries.md](./rsc-boundaries.md) for:\n\n- Async client component detection (invalid)\n- Non-serializable props detection\n- Server Action exceptions\n\n## Async Patterns\n\nNext.js 15+ async API changes.\n\nSee [async-patterns.md](./async-patterns.md) for:\n\n- Async `params` and `searchParams`\n- Async `cookies()` and `headers()`\n- Migration codemod\n\n## Runtime Selection\n\nSee [runtime-selection.md](./runtime-selection.md) for:\n\n- Default to Node.js runtime\n- When Edge runtime is appropriate\n\n## Directives\n\nSee [directives.md](./directives.md) for:\n\n- `'use client'`, `'use server'` (React)\n- `'use cache'` (Next.js)\n\n## Functions\n\nSee [functions.md](./functions.md) for:\n\n- Navigation hooks: `useRouter`, `usePathname`, `useSearchParams`, `useParams`\n- Server functions: `cookies`, `headers`, `draftMode`, `after`\n- Generate functions: `generateStaticParams`, `generateMetadata`\n\n## Error Handling\n\nSee [error-handling.md](./error-handling.md) for:\n\n- `error.tsx`, `global-error.tsx`, `not-found.tsx`\n- `redirect`, `permanentRedirect`, `notFound`\n- `forbidden`, `unauthorized` (auth errors)\n- `unstable_rethrow` for catch blocks\n\n## Data Patterns\n\nSee [data-patterns.md](./data-patterns.md) for:\n\n- Server Components vs Server Actions vs Route Handlers\n- Avoiding data waterfalls (`Promise.all`, Suspense, preload)\n- Client component data fetching\n\n## Route Handlers\n\nSee [route-handlers.md](./route-handlers.md) for:\n\n- `route.ts` basics\n- GET handler conflicts with `page.tsx`\n- Environment behavior (no React DOM)\n- When to use vs Server Actions\n\n## Metadata & OG Images\n\nSee [metadata.md](./metadata.md) for:\n\n- Static and dynamic metadata\n- `generateMetadata` function\n- OG image generation with `next/og`\n- File-based metadata conventions\n\n## Image Optimization\n\nSee [image.md](./image.md) for:\n\n- Always use `next/image` over `<img>`\n- Remote images configuration\n- Responsive `sizes` attribute\n- Blur placeholders\n- Priority loading for LCP\n\n## Font Optimization\n\nSee [font.md](./font.md) for:\n\n- `next/font` setup\n- Google Fonts, local fonts\n- Tailwind CSS integration\n- Preloading subsets\n\n## Bundling\n\nSee [bundling.md](./bundling.md) for:\n\n- Server-incompatible packages\n- CSS imports (not link tags)\n- Polyfills (already included)\n- ESM/CommonJS issues\n- Bundle analysis\n\n## Scripts\n\nSee [scripts.md](./scripts.md) for:\n\n- `next/script` vs native script tags\n- Inline scripts need `id`\n- Loading strategies\n- Google Analytics with `@next/third-parties`\n\n## Hydration Errors\n\nSee [hydration-error.md](./hydration-error.md) for:\n\n- Common causes (browser APIs, dates, invalid HTML)\n- Debugging with error overlay\n- Fixes for each cause\n\n## Suspense Boundaries\n\nSee [suspense-boundaries.md](./suspense-boundaries.md) for:\n\n- CSR bailout with `useSearchParams` and `usePathname`\n- Which hooks require Suspense boundaries\n\n## Parallel & Intercepting Routes\n\nSee [parallel-routes.md](./parallel-routes.md) for:\n\n- Modal patterns with `@slot` and `(.)` interceptors\n- `default.tsx` for fallbacks\n- Closing modals correctly with `router.back()`\n\n## Self-Hosting\n\nSee [self-hosting.md](./self-hosting.md) for:\n\n- `output: 'standalone'` for Docker\n- Cache handlers for multi-instance ISR\n- What works vs needs extra setup\n\n## Debug Tricks\n\nSee [debug-tricks.md](./debug-tricks.md) for:\n\n- MCP endpoint for AI-assisted debugging\n- Rebuild specific routes with `--debug-build-paths`\n\n<!-- File: .agents/skills/next-best-practices/suspense-boundaries.md (Tokens: ~340 | Category: anthropic-skill) -->\n# Suspense Boundaries\n\nClient hooks that cause CSR bailout without Suspense boundaries.\n\n## useSearchParams\n\nAlways requires Suspense boundary in static routes. Without it, the entire page becomes client-side rendered.\n\n```tsx\n// Bad: Entire page becomes CSR\n'use client';\n\nimport { useSearchParams } from 'next/navigation';\n\nexport default function SearchBar() {\n  const searchParams = useSearchParams();\n  return <div>Query: {searchParams.get('q')}</div>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\nimport { Suspense } from 'react';\nimport SearchBar from './search-bar';\n\nexport default function Page() {\n  return (\n    <Suspense fallback={<div>Loading...</div>}>\n      <SearchBar />\n    </Suspense>\n  );\n}\n```\n\n## usePathname\n\nRequires Suspense boundary when route has dynamic parameters.\n\n```tsx\n// In dynamic route [slug]\n// Bad: No Suspense\n'use client';\nimport { usePathname } from 'next/navigation';\n\nexport function Breadcrumb() {\n  const pathname = usePathname();\n  return <nav>{pathname}</nav>;\n}\n```\n\n```tsx\n// Good: Wrap in Suspense\n<Suspense fallback={<BreadcrumbSkeleton />}>\n  <Breadcrumb />\n</Suspense>\n```\n\nIf you use `generateStaticParams`, Suspense is optional.\n\n## Quick Reference\n\n| Hook                | Suspense Required    |\n| ------------------- | -------------------- |\n| `useSearchParams()` | Yes                  |\n| `usePathname()`     | Yes (dynamic routes) |\n| `useParams()`       | No                   |\n| `useRouter()`       | No                   |\n\n<!-- File: .agents/skills/shadcn/cli.md (Tokens: ~3862 | Category: anthropic-skill) -->\n# shadcn CLI Reference\n\nConfiguration is read from `components.json`.\n\n> **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.\n\n> **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.\n\n## Contents\n\n- Commands: init, apply, add (dry-run, smart merge), search, view, docs, info, build\n- Templates: next, vite, start, react-router, astro\n- Presets: named, code, URL formats and fields\n- Switching presets\n\n---\n\n## Commands\n\n### `init` — Initialize or create a project\n\n```bash\nnpx shadcn@latest init [components...] [options]\n```\n\nInitializes shadcn/ui in an existing project or creates a new project (when `--name` is provided). Optionally installs components in the same step.\n\n| Flag                    | Short | Description                                               | Default |\n| ----------------------- | ----- | --------------------------------------------------------- | ------- |\n| `--template <template>` | `-t`  | Template (next, start, vite, next-monorepo, react-router) | —       |\n| `--preset [name]`       | `-p`  | Preset configuration (named, code, or URL)                | —       |\n| `--yes`                 | `-y`  | Skip confirmation prompt                                  | `true`  |\n| `--defaults`            | `-d`  | Use defaults (`--template=next --preset=base-nova`)       | `false` |\n| `--force`               | `-f`  | Force overwrite existing configuration                    | `false` |\n| `--cwd <cwd>`           | `-c`  | Working directory                                         | current |\n| `--name <name>`         | `-n`  | Name for new project                                      | —       |\n| `--silent`              | `-s`  | Mute output                                               | `false` |\n| `--rtl`                 |       | Enable RTL support                                        | —       |\n| `--reinstall`           |       | Re-install existing UI components                         | `false` |\n| `--monorepo`            |       | Scaffold a monorepo project                               | —       |\n| `--no-monorepo`         |       | Skip the monorepo prompt                                  | —       |\n\n`npx shadcn@latest create` is an alias for `npx shadcn@latest init`.\n\n### `apply` — Apply a preset to an existing project\n\n```bash\nnpx shadcn@latest apply [preset] [options]\n```\n\nApplies a preset to an existing project, overwriting preset-driven config, fonts, CSS variables, and detected UI components.\n\n| Flag                | Short | Description                                | Default |\n| ------------------- | ----- | ------------------------------------------ | ------- |\n| `--preset <preset>` | —     | Preset configuration (named, code, or URL) | —       |\n| `--yes`             | `-y`  | Skip confirmation prompt                   | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                          | current |\n| `--silent`          | `-s`  | Mute output                                | `false` |\n\n`[preset]` is a shorthand for `--preset <preset>`. If both are provided, they must match.\nIf no preset is provided, the CLI offers to open the custom preset builder on `ui.shadcn.com/create`.\n\n### `add` — Add components\n\n> **IMPORTANT:** To compare local components against upstream or to preview changes, ALWAYS use `npx shadcn@latest add <component> --dry-run`, `--diff`, or `--view`. NEVER fetch raw files from GitHub or other sources manually. The CLI handles registry resolution, file paths, and CSS diffing automatically.\n\n```bash\nnpx shadcn@latest add [components...] [options]\n```\n\nAccepts component names, registry-prefixed names (`@magicui/shimmer-button`),\nGitHub item addresses (`owner/repo/item`), URLs, or local paths.\n\n| Flag            | Short | Description                                                                                                          | Default |\n| --------------- | ----- | -------------------------------------------------------------------------------------------------------------------- | ------- |\n| `--yes`         | `-y`  | Skip confirmation prompt                                                                                             | `false` |\n| `--overwrite`   | `-o`  | Overwrite existing files                                                                                             | `false` |\n| `--cwd <cwd>`   | `-c`  | Working directory                                                                                                    | current |\n| `--all`         | `-a`  | Add all available components                                                                                         | `false` |\n| `--path <path>` | `-p`  | Target path for the component                                                                                        | —       |\n| `--silent`      | `-s`  | Mute output                                                                                                          | `false` |\n| `--dry-run`     |       | Preview all changes without writing files                                                                            | `false` |\n| `--diff [path]` |       | Show diffs. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`)         | —       |\n| `--view [path]` |       | Show file contents. Without a path, shows the first 5 files. With a path, shows that file only (implies `--dry-run`) | —       |\n\n#### Dry-Run Mode\n\nUse `--dry-run` to preview what `add` would do without writing any files. `--diff` and `--view` both imply `--dry-run`.\n\n```bash\n# Preview all changes.\nnpx shadcn@latest add button --dry-run\n\n# Show diffs for all files (top 5).\nnpx shadcn@latest add button --diff\n\n# Show the diff for a specific file.\nnpx shadcn@latest add button --diff button.tsx\n\n# Show contents for all files (top 5).\nnpx shadcn@latest add button --view\n\n# Show the full content of a specific file.\nnpx shadcn@latest add button --view button.tsx\n\n# Works with URLs too.\nnpx shadcn@latest add https://api.npoint.io/abc123 --dry-run\n\n# Works with public GitHub registries too.\nnpx shadcn@latest add owner/repo/item --dry-run\n\n# CSS diffs.\nnpx shadcn@latest add button --diff globals.css\n```\n\n**When to use dry-run:**\n\n- When the user asks \"what files will this add?\" or \"what will this change?\" — use `--dry-run`.\n- Before overwriting existing components — use `--diff` to preview the changes first.\n- When the user wants to inspect component source code without installing — use `--view`.\n- When checking what CSS changes would be made to `globals.css` — use `--diff globals.css`.\n- When the user asks to review or audit third-party registry code before installing — use `--view` to inspect the source.\n\n> **`npx shadcn@latest add --dry-run` vs `npx shadcn@latest view`:** Prefer `npx shadcn@latest add --dry-run/--diff/--view` over `npx shadcn@latest view` when the user wants to preview changes to their project. `npx shadcn@latest view` only shows raw registry metadata. `npx shadcn@latest add --dry-run` shows exactly what would happen in the user's project: resolved file paths, diffs against existing files, and CSS updates. Use `npx shadcn@latest view` only when the user wants to browse registry info without a project context.\n\n#### Smart Merge from Upstream\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full workflow.\n\n### `search` — Search registries\n\n```bash\nnpx shadcn@latest search [registries...] [options]\n```\n\nFuzzy search across registries. Also aliased as `npx shadcn@latest list`.\nSupports namespaces (`@acme`), public GitHub registry sources (`owner/repo`),\nand registry catalog URLs. Without `-q`, lists all items. When no registries are\npassed, searches every registry configured in `components.json`.\n\n| Flag                | Short | Description                                       | Default |\n| ------------------- | ----- | ------------------------------------------------- | ------- |\n| `--query <query>`   | `-q`  | Search query                                      | —       |\n| `--type <type>`     | `-t`  | Filter by item type (e.g. `ui`, `block`, `hook`); comma-separated | —       |\n| `--limit <number>`  | `-l`  | Max items to display                              | `100`   |\n| `--offset <number>` | `-o`  | Items to skip                                     | `0`     |\n| `--json`            |       | Output as JSON                                    | `false` |\n| `--cwd <cwd>`       | `-c`  | Working directory                                 | current |\n\n### `view` — View item details\n\n```bash\nnpx shadcn@latest view <items...> [options]\n```\n\nDisplays item info including file contents. Examples:\n`npx shadcn@latest view @shadcn/button`,\n`npx shadcn@latest view owner/repo/item`.\n\n### `docs` — Get component documentation URLs\n\n```bash\nnpx shadcn@latest docs <components...> [options]\n```\n\nOutputs resolved URLs for component documentation, examples, and API references. Accepts one or more component names. Fetch the URLs to get the actual content.\n\nExample output for `npx shadcn@latest docs input button`:\n\n```\nbase  radix\n\ninput\n  docs      https://ui.shadcn.com/docs/components/radix/input\n  examples  https://raw.githubusercontent.com/.../examples/input-example.tsx\n\nbutton\n  docs      https://ui.shadcn.com/docs/components/radix/button\n  examples  https://raw.githubusercontent.com/.../examples/button-example.tsx\n```\n\nSome components include an `api` link to the underlying library (e.g. `cmdk` for the command component).\n\n### `diff` — Check for updates\n\nDo not use this command. Use `npx shadcn@latest add --diff` instead.\n\n### `info` — Project information\n\n```bash\nnpx shadcn@latest info [options]\n```\n\nDisplays project info and `components.json` configuration. Run this first to discover the project's framework, aliases, Tailwind version, and resolved paths.\n\n| Flag          | Short | Description       | Default |\n| ------------- | ----- | ----------------- | ------- |\n| `--cwd <cwd>` | `-c`  | Working directory | current |\n\n**Project Info fields:**\n\n| Field                | Type      | Meaning                                                            |\n| -------------------- | --------- | ------------------------------------------------------------------ |\n| `framework`          | `string`  | Detected framework (`next`, `vite`, `react-router`, `start`, etc.) |\n| `frameworkVersion`   | `string`  | Framework version (e.g. `15.2.4`)                                  |\n| `isSrcDir`           | `boolean` | Whether the project uses a `src/` directory                        |\n| `isRSC`              | `boolean` | Whether React Server Components are enabled                        |\n| `isTsx`              | `boolean` | Whether the project uses TypeScript                                |\n| `tailwindVersion`    | `string`  | `\"v3\"` or `\"v4\"`                                                   |\n| `tailwindConfigFile` | `string`  | Path to the Tailwind config file                                   |\n| `tailwindCssFile`    | `string`  | Path to the global CSS file                                        |\n| `aliasPrefix`        | `string`  | Import alias prefix (e.g. `@`, `~`, `@/`)                          |\n| `packageManager`     | `string`  | Detected package manager (`npm`, `pnpm`, `yarn`, `bun`)            |\n\n**Components.json fields:**\n\n| Field                | Type      | Meaning                                                                                    |\n| -------------------- | --------- | ------------------------------------------------------------------------------------------ |\n| `base`               | `string`  | Primitive library (`radix` or `base`) — determines component APIs and available props      |\n| `style`              | `string`  | Visual style (e.g. `nova`, `vega`)                                                         |\n| `rsc`                | `boolean` | RSC flag from config                                                                       |\n| `tsx`                | `boolean` | TypeScript flag                                                                            |\n| `tailwind.config`    | `string`  | Tailwind config path                                                                       |\n| `tailwind.css`       | `string`  | Global CSS path — this is where custom CSS variables go                                    |\n| `iconLibrary`        | `string`  | Icon library — determines icon import package (e.g. `lucide-react`, `@tabler/icons-react`) |\n| `aliases.components` | `string`  | Component import alias (e.g. `@/components`)                                               |\n| `aliases.utils`      | `string`  | Utils import alias (e.g. `@/lib/utils`)                                                    |\n| `aliases.ui`         | `string`  | UI component alias (e.g. `@/components/ui`)                                                |\n| `aliases.lib`        | `string`  | Lib alias (e.g. `@/lib`)                                                                   |\n| `aliases.hooks`      | `string`  | Hooks alias (e.g. `@/hooks`)                                                               |\n| `resolvedPaths`      | `object`  | Absolute file-system paths for each alias                                                  |\n| `registries`         | `object`  | Configured custom registries                                                               |\n\n**Links fields:**\n\nThe `info` output includes a **Links** section with templated URLs for component docs, source, and examples. For resolved URLs, use `npx shadcn@latest docs <component>` instead.\n\n### `build` — Build a custom registry\n\n```bash\nnpx shadcn@latest build [registry] [options]\n```\n\nBuilds `registry.json` into individual JSON files for distribution. Default input: `./registry.json`, default output: `./public/r`.\n\nFor authoring rules, `include`, item definitions, `registryDependencies`, and\nGitHub registry behavior, see [registry.md](./registry.md).\n\n| Flag              | Short | Description       | Default      |\n| ----------------- | ----- | ----------------- | ------------ |\n| `--output <path>` | `-o`  | Output directory  | `./public/r` |\n| `--cwd <cwd>`     | `-c`  | Working directory | current      |\n\n---\n\n## Templates\n\n| Value          | Framework      | Monorepo support |\n| -------------- | -------------- | ---------------- |\n| `next`         | Next.js        | Yes              |\n| `vite`         | Vite           | Yes              |\n| `start`        | TanStack Start | Yes              |\n| `react-router` | React Router   | Yes              |\n| `astro`        | Astro          | Yes              |\n| `laravel`      | Laravel        | No               |\n\nAll templates support monorepo scaffolding via the `--monorepo` flag. When passed, the CLI uses a monorepo-specific template directory (e.g. `next-monorepo`, `vite-monorepo`). When neither `--monorepo` nor `--no-monorepo` is passed, the CLI prompts interactively. Laravel does not support monorepo scaffolding.\n\n---\n\n## Presets\n\nThree ways to specify a preset via `--preset`:\n\n1. **Named:** `--preset nova` or `--preset lyra`\n2. **Code:** `--preset a2r6bw` (version-prefixed base62 string, e.g. `a2r6bw` or `b0`)\n3. **URL:** `--preset \"https://ui.shadcn.com/init?base=radix&style=nova&...\"`\n\n> **IMPORTANT:** Never try to decode, fetch, or resolve preset codes manually. Preset codes are opaque — pass them directly to `npx shadcn@latest init --preset <code>` and let the CLI handle resolution.\n> Use `npx shadcn@latest apply --preset <code>` when overwriting an existing project's preset.\n\n## Switching Presets\n\nAsk the user first: **overwrite**, **merge**, or **skip** existing components?\n\n- **Overwrite / Re-install** → `npx shadcn@latest apply --preset <code>`. Overwrites all detected component files with the new preset styles. Use when the user hasn't customized components.\n- **Merge** → `npx shadcn@latest init --preset <code> --force --no-reinstall`, then run `npx shadcn@latest info` to get the list of installed components and use the [smart merge workflow](./SKILL.md#updating-components) to update them one by one, preserving local changes. Use when the user has customized components.\n- **Skip** → `npx shadcn@latest init --preset <code> --force --no-reinstall`. Only updates config and CSS variables, leaves existing components as-is.\n\nAlways 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 <current-base>` explicitly — preset codes do not encode the base.\n\n<!-- File: .agents/skills/shadcn/customization.md (Tokens: ~1487 | Category: anthropic-skill) -->\n# Customization & Theming\n\nComponents reference semantic CSS variable tokens. Change the variables to change every component.\n\n## Contents\n\n- How it works (CSS variables → Tailwind utilities → components)\n- Color variables and OKLCH format\n- Dark mode setup\n- Changing the theme (presets, CSS variables)\n- Adding custom colors (Tailwind v3 and v4)\n- Border radius\n- Customizing components (variants, className, wrappers)\n- Checking for updates\n\n---\n\n## How It Works\n\n1. CSS variables defined in `:root` (light) and `.dark` (dark mode).\n2. Tailwind maps them to utilities: `bg-primary`, `text-muted-foreground`, etc.\n3. Components use these utilities — changing a variable changes all components that reference it.\n\n---\n\n## Color Variables\n\nEvery color follows the `name` / `name-foreground` convention. The base variable is for backgrounds, `-foreground` is for text/icons on that background.\n\n| Variable                                     | Purpose                          |\n| -------------------------------------------- | -------------------------------- |\n| `--background` / `--foreground`              | Page background and default text |\n| `--card` / `--card-foreground`               | Card surfaces                    |\n| `--primary` / `--primary-foreground`         | Primary buttons and actions      |\n| `--secondary` / `--secondary-foreground`     | Secondary actions                |\n| `--muted` / `--muted-foreground`             | Muted/disabled states            |\n| `--accent` / `--accent-foreground`           | Hover and accent states          |\n| `--destructive` / `--destructive-foreground` | Error and destructive actions    |\n| `--border`                                   | Default border color             |\n| `--input`                                    | Form input borders               |\n| `--ring`                                     | Focus ring color                 |\n| `--chart-1` through `--chart-5`              | Chart/data visualization         |\n| `--sidebar-*`                                | Sidebar-specific colors          |\n| `--surface` / `--surface-foreground`         | Secondary surface                |\n\nColors use OKLCH: `--primary: oklch(0.205 0 0)` where values are lightness (0–1), chroma (0 = gray), and hue (0–360).\n\n---\n\n## Dark Mode\n\nClass-based toggle via `.dark` on the root element. In Next.js, use `next-themes`:\n\n```tsx\nimport { ThemeProvider } from \"next-themes\"\n\n<ThemeProvider attribute=\"class\" defaultTheme=\"system\" enableSystem>\n  {children}\n</ThemeProvider>\n```\n\n---\n\n## Changing the Theme\n\n```bash\n# Apply a preset code from ui.shadcn.com.\nnpx shadcn@latest apply --preset a2r6bw\n\n# Positional shorthand also works.\nnpx shadcn@latest apply a2r6bw\n\n# Switch to a named preset and overwrite existing components.\nnpx shadcn@latest apply --preset nova\n\n# Preserve existing components instead.\nnpx shadcn@latest init --preset nova --force --no-reinstall\n\n# Use a custom theme URL.\nnpx shadcn@latest apply --preset \"https://ui.shadcn.com/init?base=radix&style=nova&theme=blue&...\"\n```\n\nOr edit CSS variables directly in `globals.css`.\n\n---\n\n## Adding Custom Colors\n\nAdd variables to the file at `tailwindCssFile` from `npx shadcn@latest info` (typically `globals.css`). Never create a new CSS file for this.\n\n```css\n/* 1. Define in the global CSS file. */\n:root {\n  --warning: oklch(0.84 0.16 84);\n  --warning-foreground: oklch(0.28 0.07 46);\n}\n.dark {\n  --warning: oklch(0.41 0.11 46);\n  --warning-foreground: oklch(0.99 0.02 95);\n}\n```\n\n```css\n/* 2a. Register with Tailwind v4 (@theme inline). */\n@theme inline {\n  --color-warning: var(--warning);\n  --color-warning-foreground: var(--warning-foreground);\n}\n```\n\nWhen `tailwindVersion` is `\"v3\"` (check via `npx shadcn@latest info`), register in `tailwind.config.js` instead:\n\n```js\n// 2b. Register with Tailwind v3 (tailwind.config.js).\nmodule.exports = {\n  theme: {\n    extend: {\n      colors: {\n        warning: \"oklch(var(--warning) / <alpha-value>)\",\n        \"warning-foreground\":\n          \"oklch(var(--warning-foreground) / <alpha-value>)\",\n      },\n    },\n  },\n}\n```\n\n```tsx\n// 3. Use in components.\n<div className=\"bg-warning text-warning-foreground\">Warning</div>\n```\n\n---\n\n## Border Radius\n\n`--radius` controls border radius globally. Components derive values from it (`rounded-lg` = `var(--radius)`, `rounded-md` = `calc(var(--radius) - 2px)`).\n\n---\n\n## Customizing Components\n\nSee also: [rules/styling.md](./rules/styling.md) for Incorrect/Correct examples.\n\nPrefer these approaches in order:\n\n### 1. Built-in variants\n\n```tsx\n<Button variant=\"outline\" size=\"sm\">\n  Click\n</Button>\n```\n\n### 2. Tailwind classes via `className`\n\n```tsx\n<Card className=\"mx-auto max-w-md\">...</Card>\n```\n\n### 3. Add a new variant\n\nEdit the component source to add a variant via `cva`:\n\n```tsx\n// components/ui/button.tsx\nwarning: \"bg-warning text-warning-foreground hover:bg-warning/90\",\n```\n\n### 4. Wrapper components\n\nCompose shadcn/ui primitives into higher-level components:\n\n```tsx\nexport function ConfirmDialog({ title, description, onConfirm, children }) {\n  return (\n    <AlertDialog>\n      <AlertDialogTrigger asChild>{children}</AlertDialogTrigger>\n      <AlertDialogContent>\n        <AlertDialogHeader>\n          <AlertDialogTitle>{title}</AlertDialogTitle>\n          <AlertDialogDescription>{description}</AlertDialogDescription>\n        </AlertDialogHeader>\n        <AlertDialogFooter>\n          <AlertDialogCancel>Cancel</AlertDialogCancel>\n          <AlertDialogAction onClick={onConfirm}>Confirm</AlertDialogAction>\n        </AlertDialogFooter>\n      </AlertDialogContent>\n    </AlertDialog>\n  )\n}\n```\n\n---\n\n## Checking for Updates\n\n```bash\nnpx shadcn@latest add button --diff\n```\n\nTo preview exactly what would change before updating, use `--dry-run` and `--diff`:\n\n```bash\nnpx shadcn@latest add button --dry-run        # see all affected files\nnpx shadcn@latest add button --diff button.tsx # see the diff for a specific file\n```\n\nSee [Updating Components in SKILL.md](./SKILL.md#updating-components) for the full smart merge workflow.\n\n<!-- ... +204 more modular skills available via /api/skills/Kiranism/next-shadcn-dashboard-starter?path={path} -->\n</yakaai_skills>"}