commit 2ea4ca5d526c2bc9f729b93e8b08aa3688fa53f2 Author: Clanker Date: Fri Apr 17 23:26:01 2026 +0000 Initial commit diff --git a/.claude/agents/app-implementer.md b/.claude/agents/app-implementer.md new file mode 100644 index 0000000..f48839a --- /dev/null +++ b/.claude/agents/app-implementer.md @@ -0,0 +1,245 @@ +--- +name: app-implementer +description: Use this agent when the user wants to implement a new application or feature on top of this Nuxt/PocketBase starter template. Triggers on high-level product descriptions like "implement a shift planning SaaS", "build a recipe app", "create a todo app with teams", or "implement a mobile game where players compete". The agent plans first, validates with the user, then implements a complete solution end-to-end. Examples: + + +Context: User is working in the nuxt-pocketbase-starter repository and wants to build a new app on top of it. +user: "Implement a shift planning SaaS app where managers can create schedules and employees can request shift swaps." +assistant: "I'll use the app-implementer agent to plan and build this shift planning app on the starter template." + +This is a high-level product description — the agent should plan the full data model, UI, and feature set, validate it with the user, then implement end-to-end. + + + + +Context: User describes a mobile-first or community app idea. +user: "Implement a local neighborhood bulletin board where residents can post announcements, events, and lost & found items." +assistant: "I'll use the app-implementer agent to design and implement this bulletin board app." + +Clear product vision with implicit requirements (categories, auth, posting, browsing). Agent should surface assumptions and confirm before building. + + + + +Context: User wants to build a game or interactive experience. +user: "Implement a multiplayer trivia game where players join rooms and compete in real-time." +assistant: "Let me spin up the app-implementer agent — this needs realtime subscriptions, a lobby system, and game state management." + +Complex multi-feature idea that benefits from planning, subagent parallelism, and Playwright validation. + + + +model: inherit +color: green +--- + +You are a full-stack implementation agent for the **nuxt-pocketbase-starter** template. Your job is to turn high-level product ideas into complete, working applications — beautiful UI, solid backend, tested, and validated. + +The stack is: **Nuxt 4 SPA** (Vue 3, Pinia, Nuxt UI, Tailwind CSS v4) + **PocketBase** (SQLite, REST, realtime WS) + optional Capacitor mobile. SSR is disabled — pure SPA only. No Nuxt server routes. + +--- + +## Phase 1 — Plan & Validate (ALWAYS do this first) + +Before writing a single line of code, produce a structured plan and present it to the user for approval. + +**Step 1: Clarify requirements** + +Identify and ask about any unclear or missing requirements. Focus on: +- Who are the users / roles (e.g. admin, employee, guest)? +- What are the core entities and their relationships? +- What actions can each role perform? +- Any specific UI preferences (dark mode, mobile-first, etc.)? +- Any integrations needed (email, push notifications, OAuth)? +- Is mobile (Capacitor) in scope? + +Batch your questions — ask them all at once, not one by one. + +**Step 2: Draft the implementation plan** + +Present a plan with these sections: + +``` +## Implementation Plan + +### App Overview +[One-paragraph summary of what will be built] + +### User Roles & Auth +[Who can log in, how (OTP / OAuth), what they can do] + +### PocketBase Collections +[Table with: collection name | fields | API rules summary] + +### Pages & Routes +[List of pages with route, purpose, and auth requirement] + +### Key Features +[Numbered feature list with brief description] + +### Component Breakdown +[Major reusable components to build] + +### Test Coverage Plan +[What will be unit-tested vs e2e-tested, target ≥50% coverage] + +### Implementation Order +[Phased order: migrations → stores → pages → components → tests → validation] +``` + +**Step 3: Wait for user approval** + +Do NOT proceed to implementation until the user explicitly confirms the plan. If they request changes, revise and re-present. + +--- + +## Phase 2 — Implementation + +Once approved, implement in this order. Use **parallel subagents** for independent tasks (e.g. writing multiple migrations + stores simultaneously). + +### 2a. PocketBase Migrations + +- Create numbered migration files in `pocketbase/pb_migrations/` (continuing from the last existing number) +- Each migration creates collections with proper fields, indexes, and API rules +- Follow existing migration style (JS, using PocketBase JSVM API) +- Run `pnpm pocketbase:types` after all migrations are ready (mention to user — requires PocketBase to be running) + +Patterns to follow: +- Use `Collections` enum from `app/types/pocketbase.types.ts` for type safety +- API rules use PocketBase filter syntax: `@request.auth.id != ""` for auth-required +- Relations use collection name as field type + +### 2b. PocketBase Hooks (if needed) + +- Custom endpoints via `routerAdd()` in `.pb.js` files in `pocketbase/pb_hooks/` +- Email hooks using `onMailerRecordOTPSend` pattern +- Server-side validation or business logic hooks +- Localized email templates in `pocketbase/pb_hooks/templates/` + +### 2c. Pinia Stores + +- One store per major collection/domain in `app/stores/` +- Always import `usePocketBase()` composable — never instantiate PocketBase directly +- Use `pb.collection('x').subscribe('*', callback)` for realtime — always `unsubscribe()` on unmount +- Use typed collection names from `Collections` enum +- Handle loading, error, and empty states + +Pattern: +```ts +const { pb } = usePocketBase() +const items = ref([]) +const loading = ref(false) + +async function fetchItems() { + loading.value = true + try { + items.value = await pb.collection(Collections.MyCollection).getFullList() + } finally { + loading.value = false + } +} +``` + +### 2d. Pages + +- File-based routing in `app/pages/` +- Use `definePageMeta({ middleware: 'auth' })` for protected pages +- All pages use Nuxt UI components — never raw HTML for common UI patterns +- Mobile-responsive layouts using Nuxt UI's grid/flex utilities +- Use ``, ``, ``, ``, ``, ``, etc. +- Handle loading skeletons (``) and empty states + +### 2e. Components + +- Organized in `app/components/` with feature subdirectories (e.g. `Shift/`, `Trivia/`) +- Follow Vue 3 Composition API with ` + + +``` + +> For all form components and validation patterns, see [references/components.md](references/components.md#form) + +## Overlays + +```vue + + + + + + + + + + + + + + + + + + + + +``` + +> For all overlay components, see [references/components.md](references/components.md#overlay) + +## Layouts + +Nuxt UI provides components to compose full page layouts. Load the reference matching your use case: + +| Layout | Description | Reference | +|---|---|---| +| Page | Landing, blog, changelog, pricing — public-facing pages | [layouts/page.md](references/layouts/page.md) | +| Dashboard | Admin UI with resizable sidebar and panels | [layouts/dashboard.md](references/layouts/dashboard.md) | +| Docs | Documentation with sidebar nav and TOC | [layouts/docs.md](references/layouts/docs.md) | +| Chat | AI chat with messages and prompt | [layouts/chat.md](references/layouts/chat.md) | +| Editor | Rich text editor with toolbars | [layouts/editor.md](references/layouts/editor.md) | + +## Templates + +Official starter templates at [github.com/nuxt-ui-templates](https://github.com/nuxt-ui-templates): + +| Template | Framework | GitHub | +|---|---|---| +| Starter | Nuxt | [nuxt-ui-templates/starter](https://github.com/nuxt-ui-templates/starter) | +| Starter | Vue | [nuxt-ui-templates/starter-vue](https://github.com/nuxt-ui-templates/starter-vue) | +| Dashboard | Nuxt | [nuxt-ui-templates/dashboard](https://github.com/nuxt-ui-templates/dashboard) | +| Dashboard | Vue | [nuxt-ui-templates/dashboard-vue](https://github.com/nuxt-ui-templates/dashboard-vue) | +| SaaS | Nuxt | [nuxt-ui-templates/saas](https://github.com/nuxt-ui-templates/saas) | +| Landing | Nuxt | [nuxt-ui-templates/landing](https://github.com/nuxt-ui-templates/landing) | +| Docs | Nuxt | [nuxt-ui-templates/docs](https://github.com/nuxt-ui-templates/docs) | +| Portfolio | Nuxt | [nuxt-ui-templates/portfolio](https://github.com/nuxt-ui-templates/portfolio) | +| Chat | Nuxt | [nuxt-ui-templates/chat](https://github.com/nuxt-ui-templates/chat) | +| Editor | Nuxt | [nuxt-ui-templates/editor](https://github.com/nuxt-ui-templates/editor) | +| Changelog | Nuxt | [nuxt-ui-templates/changelog](https://github.com/nuxt-ui-templates/changelog) | +| Starter | Laravel | [nuxt-ui-templates/starter-laravel](https://github.com/nuxt-ui-templates/starter-laravel) | +| Starter | AdonisJS | [nuxt-ui-templates/starter-adonis](https://github.com/nuxt-ui-templates/starter-adonis) | + +> When starting a new project, clone the matching template instead of setting up from scratch. + +## Additional references + +Load based on your task — **do not load all at once**: + +- [references/theming.md](references/theming.md) — CSS variables, custom colors, component theme overrides +- [references/components.md](references/components.md) — all 125+ components by category with props and usage +- [references/composables.md](references/composables.md) — useToast, useOverlay, defineShortcuts +- Generated theme files — all slots, variants, and default classes for any component (Nuxt: `.nuxt/ui/.ts`, Vue: `node_modules/.nuxt-ui/ui/.ts`) diff --git a/.claude/skills/nuxt-ui/references/components.md b/.claude/skills/nuxt-ui/references/components.md new file mode 100644 index 0000000..cd7cc57 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/components.md @@ -0,0 +1,377 @@ +# Components + +125+ Vue components powered by Tailwind CSS and Reka UI. For any component's theme slots, read the generated theme file (Nuxt: `.nuxt/ui/.ts`, Vue: `node_modules/.nuxt-ui/ui/.ts`). + +## Layout + +Core structural components for organizing your application's layout. + +| Component | Purpose | +|---|---| +| `UApp` | **Required** root wrapper for toasts, tooltips, overlays | +| `UHeader` | Responsive header with mobile menu (`#title`, `#default`, `#right`, `#body`) | +| `UFooter` | Footer (`#left`, `#default`, `#right`, `#top`, `#bottom`) | +| `UFooterColumns` | Multi-column footer with link groups | +| `UMain` | Main content area (respects `--ui-header-height`) | +| `UContainer` | Centered max-width container (`--ui-container`) | + +## Element + +Essential UI building blocks. + +| Component | Key props | +|---|---| +| `UButton` | `label`, `icon`, `color`, `variant`, `size`, `loading`, `disabled`, `to` | +| `UBadge` | `label`, `color`, `variant`, `size` | +| `UAvatar` | `src`, `alt`, `icon`, `text`, `size` | +| `UAvatarGroup` | `max`, `size` — wraps multiple `UAvatar` | +| `UIcon` | `name`, `size` | +| `UCard` | `variant` — slots: `#header`, `#default`, `#footer` | +| `UAlert` | `title`, `description`, `icon`, `color`, `variant`, `close` | +| `UBanner` | `title`, `icon`, `close` — sticky top banner | +| `UChip` | `color`, `size`, `position` — notification dot on children | +| `UKbd` | `value` — keyboard key display | +| `USeparator` | `label`, `icon`, `orientation`, `type` | +| `USkeleton` | `class` — loading placeholder | +| `UProgress` | `value`, `max`, `color`, `size` | +| `UCalendar` | `v-model`, `range` (boolean), `multiple` (boolean) | +| `UCollapsible` | `v-model:open` — animated expand/collapse | +| `UFieldGroup` | Groups form inputs horizontally/vertically | + +## Form + +Comprehensive form components for user input. + +| Component | Key props | +|---|---| +| `UInput` | `v-model`, `type`, `placeholder`, `icon`, `loading` | +| `UTextarea` | `v-model`, `rows`, `autoresize`, `maxrows` | +| `USelect` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `placeholder` | +| `USelectMenu` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `searchable`, `multiple` | +| `UInputMenu` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `searchable` — autocomplete | +| `UInputNumber` | `v-model`, `min`, `max`, `step` | +| `UInputDate` | `v-model`, `range` (boolean for range selection), `locale` | +| `UInputTime` | `v-model`, `hour-cycle` (12/24), `granularity` | +| `UInputTags` | `v-model`, `max`, `placeholder` | +| `UPinInput` | `v-model`, `length`, `type`, `mask` | +| `UCheckbox` | `v-model`, `label`, `description` | +| `UCheckboxGroup` | `v-model`, `items`, `orientation` | +| `URadioGroup` | `v-model`, `items`, `orientation` | +| `USwitch` | `v-model`, `label`, `on-icon`, `off-icon` | +| `USlider` | `v-model`, `min`, `max`, `step` | +| `UColorPicker` | `v-model`, `format` (hex/rgb/hsl/cmyk/lab), `size` | +| `UFileUpload` | `v-model`, `accept`, `multiple`, `variant` (area/button) | +| `UForm` | `schema`, `state`, `@submit` — validation wrapper | +| `UFormField` | `name`, `label`, `description`, `hint`, `required` | + +### Form validation + +Uses Standard Schema — works with Zod, Valibot, Yup, or Joi. + +```vue + + + +``` + +With Valibot: + +```vue + +``` + +### File upload + +```vue + + + +``` + +## Data + +Components for displaying and organizing data. + +| Component | Key props | +|---|---| +| `UTable` | `data`, `columns`, `loading`, `sticky` | +| `UAccordion` | `items`, `type` (single/multiple), `collapsible` | +| `UCarousel` | `items`, `orientation`, `arrows`, `dots` | +| `UTimeline` | `items` — vertical timeline | +| `UTree` | `items` — hierarchical tree | +| `UUser` | `name`, `description`, `avatar` — user display | +| `UEmpty` | `icon`, `title`, `description` — empty state | +| `UMarquee` | `repeat`, `reverse`, `orientation`, `pauseOnHover` — infinite scroll | +| `UScrollArea` | Custom scrollbar wrapper | + +## Navigation + +Components for user navigation and wayfinding. + +| Component | Key props | +|---|---| +| `UNavigationMenu` | `items` (flat `T[]` or grouped `T[][]`), `orientation` (horizontal/vertical) | +| `UBreadcrumb` | `items` | +| `UTabs` | `items`, `orientation`, `variant` | +| `UStepper` | `items`, `orientation`, `color` | +| `UPagination` | `v-model`, `total`, `items-per-page` | +| `ULink` | `to`, `active`, `inactive` — styled NuxtLink | +| `UCommandPalette` | `v-model:open`, `groups` (`{ id, label, items }[]`), `placeholder` | + +## Overlay + +Floating UI elements that appear above the main content. **All require `` wrapper.** + +| Component | Key props | +|---|---| +| `UModal` | `v-model:open`, `title`, `description`, `fullscreen`, `scrollable` | +| `USlideover` | `v-model:open`, `title`, `side` (left/right/top/bottom) | +| `UDrawer` | `v-model:open`, `title`, `handle` | +| `UPopover` | `arrow`, `content: { side, align }`, `openDelay`, `closeDelay` | +| `UTooltip` | `text`, `content: { side }`, `delayDuration` | +| `UDropdownMenu` | `items` (flat `T[]` or grouped `T[][]` with separators, supports nested `children`) | +| `UContextMenu` | `items` (flat `T[]` or grouped `T[][]`) — right-click menu | +| `UToast` | Used via `useToast()` composable | + +### Modal + +```vue + + + + +``` + +Slots: `#content`, `#header`, `#body`, `#footer` + +### Slideover + +```vue + + + +``` + +### Drawer + +```vue + + + +``` + +### DropdownMenu + +Items accept a flat array or a nested array (each sub-array is rendered as a group separated by dividers): + +```vue + + + + + + + + + +``` + +### Toast + +```ts +const toast = useToast() + +toast.add({ + title: 'Success', + description: 'Changes saved', + color: 'success', + icon: 'i-lucide-check-circle', + duration: 5000, + actions: [{ label: 'Undo', onClick: () => undo() }] +}) +``` + +### Programmatic overlays + +```ts +const overlay = useOverlay() + +// create() returns a reusable instance +const confirmDialog = overlay.create(ConfirmDialog) + +// open() returns an object with .result (a Promise) +const { result } = confirmDialog.open({ + title: 'Delete?', + message: 'This cannot be undone.' +}) + +if (await result) { + // User confirmed +} + +// Inside the overlay component, emit close with a value: +// emit('close', true) or emit('close', false) +``` + +### CommandPalette + +```vue + + + +``` + +## Page + +Pre-built sections for marketing and content pages. + +| Component | Purpose | +|---|---| +| `UPage` | Multi-column grid (`#left`, `#default`, `#right`) | +| `UPageAside` | Sticky sidebar wrapper (visible from `lg`) | +| `UPageHero` | Hero section with title, description, links, media | +| `UPageSection` | Content section with headline, features grid | +| `UPageCTA` | Call to action block | +| `UPageHeader` | Page title and description | +| `UPageBody` | Main content area with prose styling | +| `UPageFeature` | Individual feature item | +| `UPageGrid` | Grid layout for cards | +| `UPageColumns` | Multi-column layout | +| `UPageCard` | Content card for grids | +| `UPageLogos` | Logo wall | +| `UPageAnchors` | Anchor links (simpler TOC) | +| `UPageLinks` | Related resource links | +| `UPageList` | List items | +| `UBlogPosts` | Responsive grid of blog posts (`orientation`) | +| `UBlogPost` | Individual blog post card | +| `UChangelogVersions` | Changelog version list | +| `UChangelogVersion` | Individual changelog entry | +| `UPricingPlans` | Pricing plan cards | +| `UPricingTable` | Feature comparison table | + +## Dashboard + +Specialized components for admin interfaces with resizable panels and sidebars. + +| Component | Purpose | +|---|---| +| `UDashboardGroup` | Root wrapper — manages sidebar state | +| `UDashboardSidebar` | Resizable/collapsible sidebar (`#header`, `#default`, `#footer`) | +| `UDashboardPanel` | Content panel (`#header`, `#body`, `#footer`) | +| `UDashboardNavbar` | Panel navbar (`#left`, `#default`, `#right`) | +| `UDashboardToolbar` | Toolbar for filters/actions | +| `UDashboardSearch` | Command palette for dashboards | +| `UDashboardSearchButton` | Search trigger button | +| `UDashboardSidebarToggle` | Mobile sidebar toggle | +| `UDashboardSidebarCollapse` | Desktop collapse button | +| `UDashboardResizeHandle` | Custom resize handle | + +## Chat + +Components for conversational AI interfaces, powered by [Vercel AI SDK](https://ai-sdk.dev/). + +| Component | Purpose | +|---|---| +| `UChatMessages` | Scrollable message list with auto-scroll | +| `UChatMessage` | Individual message display | +| `UChatPrompt` | Enhanced textarea for prompts | +| `UChatPromptSubmit` | Submit button with status handling | +| `UChatPalette` | Chat layout for overlays | + +## Editor + +Rich text editor powered by [TipTap](https://tiptap.dev/). + +| Component | Purpose | +|---|---| +| `UEditor` | Editor (`v-model`, `content-type`: json/html/markdown) | +| `UEditorToolbar` | Toolbar (`layout`: fixed/bubble/floating) | +| `UEditorDragHandle` | Block drag-and-drop | +| `UEditorSuggestionMenu` | Slash command menu | +| `UEditorMentionMenu` | @ mention menu | +| `UEditorEmojiMenu` | Emoji picker | + +## Content + +Components integrating with `@nuxt/content`. + +| Component | Purpose | +|---|---| +| `UContentNavigation` | Sidebar navigation tree | +| `UContentToc` | Table of contents | +| `UContentSurround` | Prev/next links | +| `UContentSearch` | Search command palette | +| `UContentSearchButton` | Search trigger button | + +## Color Mode + +| Component | Purpose | +|---|---| +| `UColorModeButton` | Toggle light/dark button | +| `UColorModeSwitch` | Toggle light/dark switch | +| `UColorModeSelect` | Dropdown selector | +| `UColorModeAvatar` | Avatar with different src per mode | +| `UColorModeImage` | Image with different src per mode | diff --git a/.claude/skills/nuxt-ui/references/composables.md b/.claude/skills/nuxt-ui/references/composables.md new file mode 100644 index 0000000..b79fbc1 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/composables.md @@ -0,0 +1,127 @@ +# Composables + +## useToast + +Show notifications. Requires `` wrapper. + +```ts +const toast = useToast() + +toast.add({ + title: 'Success', + description: 'Item saved', + color: 'success', // primary, success, error, warning, info + icon: 'i-lucide-check-circle', + duration: 5000, // 0 = never dismiss + actions: [{ label: 'Undo', onClick: () => {} }] +}) + +toast.remove('toast-id') +toast.clear() +``` + +## useOverlay + +Programmatically create modals, slideovers, drawers. + +```ts +const overlay = useOverlay() + +// create() returns a reusable instance with open(), close(), patch() +const modal = overlay.create(MyComponent) + +// open() accepts props and returns an object with .result (a Promise) +const { result } = modal.open({ title: 'Confirm' }) + +if (await result) { + // User confirmed +} + +// Inside the overlay component, emit close with a value: +// emit('close', true) or emit('close', false) + +// You can also close from outside: +modal.close(false) +``` + +## defineShortcuts + +Define keyboard shortcuts. + +```ts +defineShortcuts({ + meta_k: () => openSearch(), // Cmd+K (Mac) / Ctrl+K (Win) + meta_shift_p: () => openPalette(), // Cmd+Shift+P + escape: () => close(), + ctrl_s: () => save(), + + // With condition + meta_enter: { + handler: () => submit(), + whenever: [isFormValid] + } +}) +``` + +| Key | Meaning | +|---|---| +| `meta` | Cmd (Mac) / Ctrl (Windows) | +| `ctrl` | Ctrl key | +| `alt` | Alt / Option key | +| `shift` | Shift key | +| `_` | Key separator | + +## defineLocale / extendLocale + +i18n locale definition. + +```ts +import { fr } from '@nuxt/ui/locale' + +// Use a built-in locale (50+ available) +// + +// Define custom locale +const locale = defineLocale({ + name: 'Español', + code: 'es', + dir: 'ltr', + messages: { + select: { placeholder: 'Seleccionar...' } + } +}) + +// Extend existing locale +import { en } from '@nuxt/ui/locale' + +const customEn = extendLocale(en, { + messages: { commandPalette: { placeholder: 'Search a component...' } } +}) +``` + +```vue + +``` + +## extractShortcuts + +Extract shortcut keys from a list of items (e.g., dropdown menu items) into a shortcuts map for `defineShortcuts`. + +```ts +const items = [ + { label: 'New file', kbds: ['meta', 'n'], onSelect: () => newFile() }, + { label: 'Save', kbds: ['meta', 's'], onSelect: () => save() } +] + +defineShortcuts(extractShortcuts(items)) +``` + +## Quick reference + +| Composable | Purpose | +|---|---| +| `useToast` | Show notifications | +| `useOverlay` | Programmatic modals/slideovers | +| `defineShortcuts` | Keyboard shortcuts | +| `defineLocale` / `extendLocale` | i18n locale | +| `extractShortcuts` | Parse shortcut definitions | diff --git a/.claude/skills/nuxt-ui/references/layouts/chat.md b/.claude/skills/nuxt-ui/references/layouts/chat.md new file mode 100644 index 0000000..2980aa5 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/layouts/chat.md @@ -0,0 +1,271 @@ +# Chat Layout + +Build AI chat interfaces with message streams, reasoning, tool calling, and Vercel AI SDK integration. + +## Component tree + +``` +UApp +└── NuxtLayout (dashboard) + └── UDashboardGroup + ├── UDashboardSidebar (conversations) + └── NuxtPage + └── UDashboardPanel + ├── #header → UDashboardNavbar + ├── #body → UContainer → UChatMessages + │ ├── #content → UChatReasoning, UChatTool, MDC + │ └── #indicator (loading) + └── #footer → UContainer → UChatPrompt + └── UChatPromptSubmit +``` + +## Setup + +### Install AI SDK + +```bash +pnpm add ai @ai-sdk/gateway @ai-sdk/vue +``` + +### Server endpoint + +```ts [server/api/chat.post.ts] +import { streamText, convertToModelMessages } from 'ai' +import { gateway } from '@ai-sdk/gateway' + +export default defineEventHandler(async (event) => { + const { messages } = await readBody(event) + + return streamText({ + model: gateway('anthropic/claude-sonnet-4.6'), + system: 'You are a helpful assistant.', + messages: await convertToModelMessages(messages) + }).toUIMessageStreamResponse() +}) +``` + +## Full page chat + +```vue [pages/chat/[id].vue] + + + +``` + +## Key components + +### ChatMessages + +Scrollable message list with auto-scroll and loading indicator. + +| Prop | Description | +|---|---| +| `messages` | Array of AI SDK messages | +| `status` | `'submitted'`, `'streaming'`, `'ready'`, `'error'` | + +Slots: `#content` (receives `{ message }`), `#actions` (per-message), `#indicator` (loading) + +### ChatMessage + +Individual message bubble with avatar, actions, and slots. + +| Prop | Description | +|---|---| +| `message` | AI SDK UIMessage object | +| `side` | `'left'` (default), `'right'` | + +### ChatReasoning + +Collapsible block for AI reasoning / thinking process. Auto-opens during streaming, auto-closes when done. + +| Prop | Description | +|---|---| +| `text` | Reasoning text (displayed inside collapsible content) | +| `streaming` | Whether reasoning is actively streaming | +| `open` | Controlled open state | + +Use `isPartStreaming(part)` from `@nuxt/ui/utils/ai` to determine streaming state. + +### ChatTool + +Collapsible block for AI tool invocation status. + +| Prop | Description | +|---|---| +| `text` | Tool status text (displayed in trigger) | +| `icon` | Icon name | +| `loading` | Show loading spinner on icon | +| `streaming` | Whether tool is actively running | +| `suffix` | Secondary text after label | +| `variant` | `'inline'` (default), `'card'` | +| `chevron` | `'trailing'` (default), `'leading'` | + +Use `isToolStreaming(part)` from `@nuxt/ui/utils/ai` to determine if a tool is still running. + +### ChatShimmer + +Text shimmer animation for streaming states. Automatically used by ChatReasoning and ChatTool when streaming. + +### ChatPrompt + +Enhanced textarea form for prompts. Accepts all Textarea props. + +| Prop | Description | +|---|---| +| `v-model` | Input text binding | +| `error` | Error from chat instance | +| `variant` | `'outline'` (default), `'subtle'`, `'soft'`, `'ghost'`, `'none'` | + +Slots: `#default` (submit button), `#footer` (below input, e.g. model selector) + +### ChatPromptSubmit + +Submit button with automatic status handling (send/stop/reload). + +### ChatPalette + +Layout wrapper for chat inside overlays (Modal, Slideover, Drawer). + +## Chat in a modal + +```vue + + + +``` + +## With model selector + +```vue + + + +``` + +## Conversation sidebar + +```vue [layouts/dashboard.vue] + +``` diff --git a/.claude/skills/nuxt-ui/references/layouts/dashboard.md b/.claude/skills/nuxt-ui/references/layouts/dashboard.md new file mode 100644 index 0000000..e7933a2 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/layouts/dashboard.md @@ -0,0 +1,220 @@ +# Dashboard Layout + +Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars. + +## Component tree + +``` +UApp +└── NuxtLayout (dashboard) + └── UDashboardGroup + ├── UDashboardSidebar + │ ├── #header (logo, search button) + │ ├── #default (navigation) — receives { collapsed } slot prop + │ └── #footer (user menu) + └── NuxtPage + └── UDashboardPanel + ├── #header → UDashboardNavbar + UDashboardToolbar + ├── #body (scrollable content) + └── #footer (optional) +``` + +## Layout + +```vue [layouts/dashboard.vue] + + + +``` + +## Page + +```vue [pages/dashboard/index.vue] + + + +``` + +## Key components + +### DashboardGroup + +Root layout wrapper. Manages sidebar state and persistence. + +| Prop | Default | Description | +|---|---|---| +| `storage` | `'cookie'` | State persistence: `'cookie'`, `'localStorage'`, `false` | +| `storage-key` | `'dashboard'` | Storage key name | +| `unit` | `'percentages'` | Size unit: `'percentages'` or `'pixels'` | + +### DashboardSidebar + +Resizable, collapsible sidebar. Must be inside `DashboardGroup`. + +| Prop | Default | Description | +|---|---|---| +| `resizable` | `false` | Enable resize by dragging | +| `collapsible` | `false` | Enable collapse when dragged to edge | +| `side` | `'left'` | `'left'` or `'right'` | +| `mode` | `'slideover'` | Mobile menu mode: `'modal'`, `'slideover'`, `'drawer'` | + +Slots receive `{ collapsed }` prop. Control state: `v-model:collapsed`, `v-model:open` (mobile). + +### DashboardPanel + +Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw) slots. + +| Prop | Default | Description | +|---|---|---| +| `id` | `—` | Unique ID (required for multi-panel) | +| `resizable` | `false` | Enable resize by dragging | + +### DashboardNavbar / DashboardToolbar + +Navbar has `#left`, `#default`, `#right` slots and a `title` prop. Toolbar has the same slots for filters/actions below the navbar. + +## Multi-panel (list-detail) + +```vue [pages/dashboard/inbox.vue] + + + +``` + +## With toolbar + +```vue + + + +``` + +## With search + +```vue [layouts/dashboard.vue] + +``` + +## Right sidebar + +```vue + + + + + + + + + + + +``` diff --git a/.claude/skills/nuxt-ui/references/layouts/docs.md b/.claude/skills/nuxt-ui/references/layouts/docs.md new file mode 100644 index 0000000..042f331 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/layouts/docs.md @@ -0,0 +1,141 @@ +# Docs Layout + +Build documentation sites with sidebar navigation, table of contents, and surround links. + +> Requires `@nuxt/content` module for navigation, search, and TOC. + +## Component tree + +``` +UApp +├── UHeader +├── UMain +│ └── NuxtLayout (docs) +│ └── UPage +│ ├── #left → UPageAside → UContentNavigation +│ └── NuxtPage +│ ├── UPageHeader +│ ├── UPageBody → ContentRenderer + UContentSurround +│ └── #right → UContentToc +└── UFooter +``` + +## App shell + +```vue [app.vue] + + + +``` + +## Layout + +```vue [layouts/docs.vue] + + + +``` + +## Page + +```vue [pages/docs/[...slug].vue] + + + +``` + +> The outer `UPage` in the layout handles the left sidebar. The inner `UPage` in the page handles the right sidebar. They nest correctly. + +## Key components + +- `UPage` — Multi-column grid layout with `#left`, `#default`, `#right` slots +- `UPageAside` — Sticky sidebar wrapper (visible from `lg` breakpoint) +- `UPageHeader` — Page title and description +- `UPageBody` — Main content area +- `UContentNavigation` — Sidebar navigation tree +- `UContentToc` — Table of contents +- `UContentSurround` — Prev/next links +- `UContentSearch` / `UContentSearchButton` — Search command palette +- `UPageAnchors` — Simpler alternative to full TOC +- `UPageLinks` — Related resource links diff --git a/.claude/skills/nuxt-ui/references/layouts/editor.md b/.claude/skills/nuxt-ui/references/layouts/editor.md new file mode 100644 index 0000000..aae5d75 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/layouts/editor.md @@ -0,0 +1,168 @@ +# Editor Layout + +Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop. + +## Component tree + +``` +UApp +├── UHeader +├── UMain +│ └── NuxtPage +│ └── UContainer +│ └── UEditor +│ ├── UEditorToolbar (fixed / bubble / floating) +│ ├── UEditorDragHandle +│ ├── UEditorSuggestionMenu +│ ├── UEditorMentionMenu +│ └── UEditorEmojiMenu +└── UFooter +``` + +## Page + +```vue [pages/editor.vue] + + + +``` + +> If you encounter prosemirror-related errors, add prosemirror packages to `vite.optimizeDeps.include` in `nuxt.config.ts`. + +## Key components + +- `UEditor` — Rich text editor (`v-model` accepts JSON, HTML, or markdown via `content-type` prop) +- `UEditorToolbar` — Toolbar with `layout`: `'fixed'` (default), `'bubble'` (on selection), `'floating'` (on empty lines) +- `UEditorDragHandle` — Block drag-and-drop handle +- `UEditorSuggestionMenu` — Slash command menu +- `UEditorMentionMenu` — @ mention menu +- `UEditorEmojiMenu` — Emoji picker + +## Toolbar modes + +```vue + + + + + + + + + + + + + + +``` + +## Content types + +```vue + + + + + + + + +``` + +## With document sidebar + +Combine with Dashboard components for a multi-document editor with a sidebar. + +```vue [layouts/editor.vue] + +``` + +```vue [pages/editor/[id].vue] + + + +``` diff --git a/.claude/skills/nuxt-ui/references/layouts/page.md b/.claude/skills/nuxt-ui/references/layouts/page.md new file mode 100644 index 0000000..fac18ae --- /dev/null +++ b/.claude/skills/nuxt-ui/references/layouts/page.md @@ -0,0 +1,260 @@ +# Page Layout + +Build public-facing pages — landing, blog, changelog, pricing — using the Header + Main + Footer shell with Page components. + +## App shell + +```vue [app.vue] + + + +``` + +## Landing page + +```vue [pages/index.vue] + +``` + +## Blog listing + +```vue [pages/blog/index.vue] + + + +``` + +## Blog article + +```vue [pages/blog/[slug].vue] + + + +``` + +## Changelog + +```vue [pages/changelog.vue] + + + +``` + +## Key components + +### Page sections + +- `UPageHero` — Hero with title, description, links, and optional media (`orientation`: horizontal/vertical) +- `UPageSection` — Content section with headline, title, description, and `features` grid +- `UPageCTA` — Call to action block +- `UPageHeader` — Page title and description +- `UPageBody` — Main content area with prose styling + +### Grids & cards + +- `UPageGrid` / `UPageColumns` — Grid layouts +- `UPageCard` — Content card for grids +- `UPageFeature` — Individual feature item +- `UPageLogos` — Logo wall + +### Blog & changelog + +- `UBlogPosts` — Responsive grid of posts (`orientation`: horizontal/vertical) +- `UBlogPost` — Individual post card +- `UChangelogVersions` / `UChangelogVersion` — Changelog entries + +### Pricing + +- `UPricingPlans` — Pricing plan cards +- `UPricingTable` — Feature comparison table + +### Footer + +- `UFooterColumns` — Multi-column footer with link groups + +## Variations + +### Alternating sections + +```vue + + + + + + + +``` + +### Feature grid + +```vue + + + + + +``` + +### Blog with sidebar + +```vue [layouts/blog.vue] + +``` diff --git a/.claude/skills/nuxt-ui/references/theming.md b/.claude/skills/nuxt-ui/references/theming.md new file mode 100644 index 0000000..6f90300 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/theming.md @@ -0,0 +1,427 @@ +# Theming + +## Semantic colors + +| Color | Default | Purpose | +|---|---|---| +| `primary` | green | CTAs, active states, brand | +| `secondary` | blue | Secondary actions | +| `success` | green | Success messages | +| `info` | blue | Informational | +| `warning` | yellow | Warnings | +| `error` | red | Errors, destructive actions | +| `neutral` | slate | Text, borders, disabled | + +## Configuring colors + +```ts +// Nuxt — app.config.ts +export default defineAppConfig({ + ui: { + colors: { + primary: 'indigo', + secondary: 'violet', + success: 'emerald', + error: 'rose', + neutral: 'zinc' + } + } +}) +``` + +```ts +// Vue — vite.config.ts +ui({ + ui: { + colors: { primary: 'indigo', secondary: 'violet', neutral: 'zinc' } + } +}) +``` + +You can only use colors that exist in your theme — either [Tailwind's default colors](https://tailwindcss.com/docs/colors) or custom colors defined with `@theme`. + +## Adding custom colors + +1. Define all 11 shades in CSS: + +```css +/* assets/css/main.css */ +@theme static { + --color-brand-50: #fef2f2; + --color-brand-100: #fee2e2; + --color-brand-200: #fecaca; + --color-brand-300: #fca5a5; + --color-brand-400: #f87171; + --color-brand-500: #ef4444; + --color-brand-600: #dc2626; + --color-brand-700: #b91c1c; + --color-brand-800: #991b1b; + --color-brand-900: #7f1d1d; + --color-brand-950: #450a0a; +} +``` + +2. Assign it as a semantic color value: `ui: { colors: { primary: 'brand' } }` + +You can only use colors that have all shades defined — either from Tailwind's defaults or custom `@theme` definitions. + +### Extending with new semantic color names + +If you need a new semantic color beyond the defaults (e.g., `tertiary`), register it in `theme.colors`: + +```ts +// Nuxt — nuxt.config.ts +export default defineNuxtConfig({ + ui: { + theme: { + colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error'] + } + } +}) +``` + +```ts +// Vue — vite.config.ts +ui({ + theme: { + colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error'] + } +}) +``` + +Then assign it: `ui: { colors: { tertiary: 'indigo' } }` and use it via the `color` prop: ``. + +## CSS utilities + +### Text + +| Class | Use | Light value | Dark value | +|---|---|---|---| +| `text-default` | Body text | `neutral-700` | `neutral-200` | +| `text-muted` | Secondary text | `neutral-500` | `neutral-400` | +| `text-dimmed` | Placeholders, hints | `neutral-400` | `neutral-500` | +| `text-toned` | Subtitles | `neutral-600` | `neutral-300` | +| `text-highlighted` | Headings, emphasis | `neutral-900` | `white` | +| `text-inverted` | On dark/light backgrounds | `white` | `neutral-900` | + +### Background + +| Class | Use | Light value | Dark value | +|---|---|---|---| +| `bg-default` | Page background | `white` | `neutral-900` | +| `bg-muted` | Subtle sections | `neutral-50` | `neutral-800` | +| `bg-elevated` | Cards, modals | `neutral-100` | `neutral-800` | +| `bg-accented` | Hover states | `neutral-200` | `neutral-700` | +| `bg-inverted` | Inverted sections | `neutral-900` | `white` | + +### Border + +| Class | Use | Light value | Dark value | +|---|---|---|---| +| `border-default` | Default borders | `neutral-200` | `neutral-800` | +| `border-muted` | Subtle borders | `neutral-200` | `neutral-700` | +| `border-accented` | Emphasized borders | `neutral-300` | `neutral-700` | +| `border-inverted` | Inverted borders | `neutral-900` | `white` | + +### Semantic color utilities + +Each semantic color (`primary`, `secondary`, `success`, `info`, `warning`, `error`) is available as a Tailwind utility: `text-primary`, `bg-primary`, `border-primary`, `ring-primary`, etc. + +They resolve to shade **500** in light mode and shade **400** in dark mode (via `--ui-` CSS variables). This is generated at runtime by the colors plugin — you don't need to write dark-mode variants manually. + +To adjust which shade is used, override `--ui-primary` (or any semantic color) in your `main.css`: + +```css +:root { --ui-primary: var(--ui-color-primary-600); } +.dark { --ui-primary: var(--ui-color-primary-300); } +``` + +### CSS variables + +All customizable in `main.css`: + +```css +:root { + --ui-radius: 0.25rem; /* base radius for all components */ + --ui-container: 80rem; /* UContainer max-width */ + --ui-header-height: 4rem; /* UHeader height */ + --ui-primary: var(--ui-color-primary-500); /* adjust shade used */ +} + +.dark { + --ui-primary: var(--ui-color-primary-400); +} +``` + +### Solid colors (black/white) + +```css +:root { --ui-primary: black; } +.dark { --ui-primary: white; } +``` + +## Component theme customization + +### How it works + +Components are styled with [Tailwind Variants](https://www.tailwind-variants.org/). The theme defines: + +- **`slots`** — named style targets (e.g., `root`, `base`, `label`, `leadingIcon`) +- **`variants`** — styles applied based on props (e.g., `color`, `variant`, `size`) +- **`compoundVariants`** — styles for specific prop combinations (e.g., `color: 'primary'` + `variant: 'outline'`) +- **`defaultVariants`** — default prop values when none are specified + +### Override priority + +**`ui` prop / `class` prop > global config > theme defaults** + +The `ui` prop overrides slots **after** variants are computed. If the `size: 'md'` variant applies `size-5` to `trailingIcon`, and you set `:ui="{ trailingIcon: 'size-3' }"`, the `size-3` wins. + +Tailwind Variants uses [tailwind-merge](https://github.com/dcastil/tailwind-merge) under the hood so conflicting classes are resolved automatically. + +### Understanding the generated theme + +Every component's full resolved theme is generated at build time. Always read this file before customizing a component — it shows exactly what classes are applied where. + +- **Nuxt**: `.nuxt/ui/.ts` +- **Vue**: `node_modules/.nuxt-ui/ui/.ts` + +For example, the card theme: + +```ts +{ + slots: { + root: "rounded-lg overflow-hidden", + header: "p-4 sm:px-6", + body: "p-4 sm:p-6", + footer: "p-4 sm:px-6" + }, + variants: { + variant: { + outline: { root: "bg-default ring ring-default divide-y divide-default" }, + soft: { root: "bg-elevated/50 divide-y divide-default" } + } + }, + defaultVariants: { variant: "outline" } +} +``` + +### Global config + +Override the theme for all instances of a component: + +```ts +// Nuxt — app.config.ts +export default defineAppConfig({ + ui: { + button: { + slots: { + base: 'font-bold rounded-full' + }, + variants: { + size: { + md: { leadingIcon: 'size-4' } + } + }, + compoundVariants: [{ + color: 'neutral', + variant: 'outline', + class: { base: 'ring-2' } + }], + defaultVariants: { + color: 'neutral', + variant: 'outline' + } + } + } +}) +``` + +```ts +// Vue — vite.config.ts +ui({ + ui: { + button: { + slots: { base: 'font-bold rounded-full' }, + defaultVariants: { color: 'neutral', variant: 'outline' } + } + } +}) +``` + +### Per-instance (`ui` prop) + +Overrides slots after variant computation: + +```vue + + +``` + +### Per-instance (`class` prop) + +Overrides the `root` or `base` slot: + +```vue +Square +``` + +Components without slots (e.g., `UContainer`, `USkeleton`, `UMain`) only have the `class` prop. + +### Theme structure patterns + +**Slots-based** (most components — `slots` is an object in the generated theme): + +```ts +// global config +ui: { + button: { + slots: { base: 'font-bold' } + } +} +// per instance + +``` + +**Flat base** (`base` is a top-level string in the generated theme): + +```ts +// global config +ui: { + container: { + base: 'max-w-lg' + } +} +// per instance — class prop only + +``` + +Always check the generated theme file to see which pattern applies. + +## Dark mode + +```ts +const colorMode = useColorMode() +colorMode.preference = 'dark' // 'light', 'dark', 'system' +``` + +```vue + + +``` + +## Fonts + +```css +/* assets/css/main.css */ +@theme { + --font-sans: 'Public Sans', system-ui, sans-serif; + --font-mono: 'JetBrains Mono', monospace; +} +``` + +In Nuxt, fonts defined with `@theme` are automatically loaded by the `@nuxt/fonts` module. + +## Brand customization playbook + +Follow these steps to fully rebrand Nuxt UI (e.g., "make a Ghibli theme", "match our corporate brand"): + +### Step 1 — Define the color palette + +Pick colors that match the brand. Map them to semantic roles: + +```ts +// app.config.ts (Nuxt) or vite.config.ts (Vue) +ui: { + colors: { + primary: 'emerald', // brand accent + secondary: 'amber', // secondary accent + success: 'green', + info: 'sky', + warning: 'orange', + error: 'rose', + neutral: 'stone' // affects all text, borders, backgrounds + } +} +``` + +If no Tailwind default color fits, define custom shades in CSS (see [Adding custom colors](#adding-custom-colors)): + +```css +@theme static { + --color-forest-50: #f0fdf4; + /* ... all 11 shades (50–950) ... */ + --color-forest-950: #052e16; +} +``` + +Then use it: `primary: 'forest'`. + +### Step 2 — Set fonts + +```css +/* assets/css/main.css */ +@theme { + --font-sans: 'Quicksand', system-ui, sans-serif; +} +``` + +### Step 3 — Adjust CSS variables + +```css +:root { + --ui-radius: 0.75rem; /* rounder = softer/playful, smaller = sharper/corporate */ + --ui-primary: var(--ui-color-primary-600); /* adjust which shade is used */ +} + +.dark { + --ui-primary: var(--ui-color-primary-400); +} +``` + +### Step 4 — Override key components globally + +Read the generated theme files to find slot names, then apply global overrides: + +```ts +// app.config.ts (Nuxt) or vite.config.ts (Vue) +ui: { + // ... colors from Step 1 + button: { + slots: { + base: 'rounded-full font-semibold' + }, + defaultVariants: { + variant: 'soft' + } + }, + card: { + slots: { + root: 'rounded-2xl shadow-lg' + } + }, + badge: { + slots: { + base: 'rounded-full' + } + } +} +``` + +> **Tip**: Read `.nuxt/ui/button.ts` (Nuxt) or `node_modules/.nuxt-ui/ui/button.ts` (Vue) to see all available slots and variants before overriding. + +### Step 5 — Verify dark mode + +Check that both modes look correct. Adjust `--ui-primary` shade per mode and test contrast. Use `useColorMode()` to toggle during development. + +### Quick checklist + +| Step | What to change | Where | +|---|---|---| +| Colors | `primary`, `secondary`, `neutral` | `app.config.ts` / `vite.config.ts` | +| Custom palette | 11 shades per color | `main.css` (`@theme static`) | +| Fonts | `--font-sans`, `--font-mono` | `main.css` (`@theme`) | +| Radius | `--ui-radius` | `main.css` (`:root`) | +| Primary shade | `--ui-primary` | `main.css` (`:root` + `.dark`) | +| Component shapes | Global slot overrides | `app.config.ts` / `vite.config.ts` | +| Dark mode | Verify contrast, adjust variables | `main.css` (`.dark`) | diff --git a/.claude/skills/nuxt/GENERATION.md b/.claude/skills/nuxt/GENERATION.md new file mode 100644 index 0000000..6299090 --- /dev/null +++ b/.claude/skills/nuxt/GENERATION.md @@ -0,0 +1,5 @@ +# Generation Info + +- **Source:** `sources/nuxt` +- **Git SHA:** `c9fed804b9bef362276033b03ca43730c6efa7dc` +- **Generated:** 2026-01-28 diff --git a/.claude/skills/nuxt/SKILL.md b/.claude/skills/nuxt/SKILL.md new file mode 100644 index 0000000..62e497e --- /dev/null +++ b/.claude/skills/nuxt/SKILL.md @@ -0,0 +1,55 @@ +--- +name: nuxt +description: Nuxt full-stack Vue framework with SSR, auto-imports, and file-based routing. Use when working with Nuxt apps, server routes, useFetch, middleware, or hybrid rendering. +metadata: + author: Anthony Fu + version: "2026.1.28" + source: Generated from https://github.com/nuxt/nuxt, scripts located at https://github.com/antfu/skills +--- + +Nuxt is a full-stack Vue framework that provides server-side rendering, file-based routing, auto-imports, and a powerful module system. It uses Nitro as its server engine for universal deployment across Node.js, serverless, and edge platforms. + +> The skill is based on Nuxt 3.x, generated at 2026-01-28. + +## Core + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Directory Structure | Project folder structure, conventions, file organization | [core-directory-structure](references/core-directory-structure.md) | +| Configuration | nuxt.config.ts, app.config.ts, runtime config, environment variables | [core-config](references/core-config.md) | +| CLI Commands | Dev server, build, generate, preview, and utility commands | [core-cli](references/core-cli.md) | +| Routing | File-based routing, dynamic routes, navigation, middleware, layouts | [core-routing](references/core-routing.md) | +| Data Fetching | useFetch, useAsyncData, $fetch, caching, refresh | [core-data-fetching](references/core-data-fetching.md) | +| Modules | Creating and using Nuxt modules, Nuxt Kit utilities | [core-modules](references/core-modules.md) | +| Deployment | Platform-agnostic deployment with Nitro, Vercel, Netlify, Cloudflare | [core-deployment](references/core-deployment.md) | + +## Features + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Composables Auto-imports | Vue APIs, Nuxt composables, custom composables, utilities | [features-composables](references/features-composables.md) | +| Components Auto-imports | Component naming, lazy loading, hydration strategies | [features-components-autoimport](references/features-components-autoimport.md) | +| Built-in Components | NuxtLink, NuxtPage, NuxtLayout, ClientOnly, and more | [features-components](references/features-components.md) | +| State Management | useState composable, SSR-friendly state, Pinia integration | [features-state](references/features-state.md) | +| Server Routes | API routes, server middleware, Nitro server engine | [features-server](references/features-server.md) | + +## Rendering + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Rendering Modes | Universal (SSR), client-side (SPA), hybrid rendering, route rules | [rendering-modes](references/rendering-modes.md) | + +## Best Practices + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Data Fetching Patterns | Efficient fetching, caching, parallel requests, error handling | [best-practices-data-fetching](references/best-practices-data-fetching.md) | +| SSR & Hydration | Avoiding context leaks, hydration mismatches, composable patterns | [best-practices-ssr](references/best-practices-ssr.md) | + +## Advanced + +| Topic | Description | Reference | +|-------|-------------|-----------| +| Layers | Extending applications with reusable layers | [advanced-layers](references/advanced-layers.md) | +| Lifecycle Hooks | Build-time, runtime, and server hooks | [advanced-hooks](references/advanced-hooks.md) | +| Module Authoring | Creating publishable Nuxt modules with Nuxt Kit | [advanced-module-authoring](references/advanced-module-authoring.md) | diff --git a/.claude/skills/nuxt/references/advanced-hooks.md b/.claude/skills/nuxt/references/advanced-hooks.md new file mode 100644 index 0000000..b61d1ed --- /dev/null +++ b/.claude/skills/nuxt/references/advanced-hooks.md @@ -0,0 +1,289 @@ +--- +name: lifecycle-hooks +description: Nuxt and Nitro hooks for extending build-time and runtime behavior +--- + +# Lifecycle Hooks + +Nuxt provides hooks to tap into the build process, application lifecycle, and server runtime. + +## Build-time Hooks (Nuxt) + +Used in `nuxt.config.ts` or modules: + +### In nuxt.config.ts + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + hooks: { + 'build:before': () => { + console.log('Build starting...') + }, + 'pages:extend': (pages) => { + // Add custom pages + pages.push({ + name: 'custom', + path: '/custom', + file: '~/pages/custom.vue', + }) + }, + 'components:dirs': (dirs) => { + // Add component directories + dirs.push({ path: '~/extra-components' }) + }, + }, +}) +``` + +### In Modules + +```ts +// modules/my-module.ts +export default defineNuxtModule({ + setup(options, nuxt) { + nuxt.hook('ready', async (nuxt) => { + console.log('Nuxt is ready') + }) + + nuxt.hook('close', async (nuxt) => { + console.log('Nuxt is closing') + }) + + nuxt.hook('modules:done', () => { + console.log('All modules loaded') + }) + }, +}) +``` + +### Common Build Hooks + +| Hook | When | +|------|------| +| `ready` | Nuxt initialization complete | +| `close` | Nuxt is closing | +| `modules:done` | All modules installed | +| `build:before` | Before build starts | +| `build:done` | Build complete | +| `pages:extend` | Pages routes resolved | +| `components:dirs` | Component dirs being resolved | +| `imports:extend` | Auto-imports being resolved | +| `nitro:config` | Before Nitro config finalized | +| `vite:extend` | Vite context created | +| `vite:extendConfig` | Before Vite config finalized | + +## App Hooks (Runtime) + +Used in plugins and composables: + +### In Plugins + +```ts +// plugins/lifecycle.ts +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('app:created', (vueApp) => { + console.log('Vue app created') + }) + + nuxtApp.hook('app:mounted', (vueApp) => { + console.log('App mounted') + }) + + nuxtApp.hook('page:start', () => { + console.log('Page navigation starting') + }) + + nuxtApp.hook('page:finish', () => { + console.log('Page navigation finished') + }) + + nuxtApp.hook('page:loading:start', () => { + console.log('Page loading started') + }) + + nuxtApp.hook('page:loading:end', () => { + console.log('Page loading ended') + }) +}) +``` + +### Common App Hooks + +| Hook | When | +|------|------| +| `app:created` | Vue app created | +| `app:mounted` | Vue app mounted (client only) | +| `app:error` | Fatal error occurred | +| `page:start` | Page navigation starting | +| `page:finish` | Page navigation finished | +| `page:loading:start` | Loading indicator should show | +| `page:loading:end` | Loading indicator should hide | +| `link:prefetch` | Link is being prefetched | + +### Using Runtime Hooks + +```ts +// composables/usePageTracking.ts +export function usePageTracking() { + const nuxtApp = useNuxtApp() + + nuxtApp.hook('page:finish', () => { + trackPageView(useRoute().path) + }) +} +``` + +## Server Hooks (Nitro) + +Used in server plugins: + +```ts +// server/plugins/hooks.ts +export default defineNitroPlugin((nitroApp) => { + // Modify HTML before sending + nitroApp.hooks.hook('render:html', (html, { event }) => { + html.head.push('') + html.bodyAppend.push('') + }) + + // Modify response + nitroApp.hooks.hook('render:response', (response, { event }) => { + console.log('Sending response:', response.statusCode) + }) + + // Before request + nitroApp.hooks.hook('request', (event) => { + console.log('Request:', event.path) + }) + + // After response + nitroApp.hooks.hook('afterResponse', (event) => { + console.log('Response sent') + }) +}) +``` + +### Common Nitro Hooks + +| Hook | When | +|------|------| +| `request` | Request received | +| `beforeResponse` | Before sending response | +| `afterResponse` | After response sent | +| `render:html` | Before HTML is sent | +| `render:response` | Before response is finalized | +| `error` | Error occurred | + +## Custom Hooks + +### Define Custom Hook Types + +```ts +// types/hooks.d.ts +import type { HookResult } from '@nuxt/schema' + +declare module '#app' { + interface RuntimeNuxtHooks { + 'my-app:event': (data: MyEventData) => HookResult + } +} + +declare module '@nuxt/schema' { + interface NuxtHooks { + 'my-module:init': () => HookResult + } +} + +declare module 'nitropack/types' { + interface NitroRuntimeHooks { + 'my-server:event': (data: any) => void + } +} +``` + +### Call Custom Hooks + +```ts +// In a plugin +export default defineNuxtPlugin((nuxtApp) => { + // Call custom hook + nuxtApp.callHook('my-app:event', { type: 'custom' }) +}) + +// In a module +export default defineNuxtModule({ + setup(options, nuxt) { + nuxt.callHook('my-module:init') + }, +}) +``` + +## useRuntimeHook + +Call hooks at runtime from components: + +```vue + +``` + +## Hook Examples + +### Page View Tracking + +```ts +// plugins/analytics.client.ts +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('page:finish', () => { + const route = useRoute() + analytics.track('pageview', { + path: route.path, + title: document.title, + }) + }) +}) +``` + +### Performance Monitoring + +```ts +// plugins/performance.client.ts +export default defineNuxtPlugin((nuxtApp) => { + let navigationStart: number + + nuxtApp.hook('page:start', () => { + navigationStart = performance.now() + }) + + nuxtApp.hook('page:finish', () => { + const duration = performance.now() - navigationStart + console.log(`Navigation took ${duration}ms`) + }) +}) +``` + +### Inject HTML + +```ts +// server/plugins/inject.ts +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('render:html', (html) => { + html.head.push(` + + `) + }) +}) +``` + + diff --git a/.claude/skills/nuxt/references/advanced-layers.md b/.claude/skills/nuxt/references/advanced-layers.md new file mode 100644 index 0000000..94b4ae0 --- /dev/null +++ b/.claude/skills/nuxt/references/advanced-layers.md @@ -0,0 +1,299 @@ +--- +name: nuxt-layers +description: Extending Nuxt applications with layers for code sharing and reusability +--- + +# Nuxt Layers + +Layers allow sharing and reusing partial Nuxt applications across projects. They can include components, composables, pages, layouts, and configuration. + +## Using Layers + +### From npm Package + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + extends: [ + '@my-org/base-layer', + '@nuxtjs/ui-layer', + ], +}) +``` + +### From Git Repository + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + extends: [ + 'github:username/repo', + 'github:username/repo/base', // Subdirectory + 'github:username/repo#v1.0', // Specific tag + 'github:username/repo#dev', // Branch + 'gitlab:username/repo', + 'bitbucket:username/repo', + ], +}) +``` + +### From Local Directory + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + extends: [ + '../base-layer', + './layers/shared', + ], +}) +``` + +### Auto-scanned Layers + +Place in `layers/` directory for automatic discovery: + +``` +my-app/ +├── layers/ +│ ├── base/ +│ │ └── nuxt.config.ts +│ └── ui/ +│ └── nuxt.config.ts +└── nuxt.config.ts +``` + +## Creating a Layer + +Minimal layer structure: + +``` +my-layer/ +├── nuxt.config.ts # Required +├── app/ +│ ├── components/ # Auto-merged +│ ├── composables/ # Auto-merged +│ ├── layouts/ # Auto-merged +│ ├── middleware/ # Auto-merged +│ ├── pages/ # Auto-merged +│ ├── plugins/ # Auto-merged +│ └── app.config.ts # Merged +├── server/ # Auto-merged +└── package.json +``` + +### Layer nuxt.config.ts + +```ts +// my-layer/nuxt.config.ts +export default defineNuxtConfig({ + // Layer configuration + app: { + head: { + title: 'My Layer App', + }, + }, + // Shared modules + modules: ['@nuxt/ui'], +}) +``` + +### Layer Components + +```vue + + +``` + +Use in consuming project: + +```vue + +``` + +### Layer Composables + +```ts +// my-layer/app/composables/useTheme.ts +export function useTheme() { + const isDark = useState('theme-dark', () => false) + const toggle = () => isDark.value = !isDark.value + return { isDark, toggle } +} +``` + +## Layer Priority + +Override order (highest to lowest): +1. Your project files +2. Auto-scanned layers (alphabetically, Z > A) +3. `extends` array (first > last) + +Control order with prefixes: + +``` +layers/ +├── 1.base/ # Lower priority +└── 2.theme/ # Higher priority +``` + +## Layer Aliases + +Access layer files: + +```ts +// Auto-scanned layers get aliases +import Component from '#layers/base/components/Component.vue' +``` + +Named aliases: + +```ts +// my-layer/nuxt.config.ts +export default defineNuxtConfig({ + $meta: { + name: 'my-layer', + }, +}) +``` + +```ts +// In consuming project +import { something } from '#layers/my-layer/utils' +``` + +## Publishing Layers + +### As npm Package + +```json +{ + "name": "my-nuxt-layer", + "version": "1.0.0", + "type": "module", + "main": "./nuxt.config.ts", + "dependencies": { + "@nuxt/ui": "^2.0.0" + }, + "devDependencies": { + "nuxt": "^3.0.0" + } +} +``` + +### Private Layers + +For private git repos: + +```bash +export GIGET_AUTH= +``` + +## Layer Best Practices + +### Use Resolved Paths + +```ts +// my-layer/nuxt.config.ts +import { fileURLToPath } from 'node:url' +import { dirname, join } from 'node:path' + +const currentDir = dirname(fileURLToPath(import.meta.url)) + +export default defineNuxtConfig({ + css: [ + join(currentDir, './assets/main.css'), + ], +}) +``` + +### Install Dependencies + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + extends: [ + ['github:user/layer', { install: true }], + ], +}) +``` + +### Disable Layer Modules + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + extends: ['./base-layer'], + // Disable modules from layer + image: false, // Disables @nuxt/image + pinia: false, // Disables @pinia/nuxt +}) +``` + +## Starter Template + +Create a new layer: + +```bash +npx nuxi init --template layer my-layer +``` + +## Example: Theme Layer + +``` +theme-layer/ +├── nuxt.config.ts +├── app/ +│ ├── app.config.ts +│ ├── components/ +│ │ ├── ThemeButton.vue +│ │ └── ThemeCard.vue +│ ├── composables/ +│ │ └── useTheme.ts +│ └── assets/ +│ └── theme.css +└── package.json +``` + +```ts +// theme-layer/nuxt.config.ts +export default defineNuxtConfig({ + css: ['~/assets/theme.css'], +}) +``` + +```ts +// theme-layer/app/app.config.ts +export default defineAppConfig({ + theme: { + primaryColor: '#00dc82', + darkMode: false, + }, +}) +``` + +```ts +// consuming-app/nuxt.config.ts +export default defineNuxtConfig({ + extends: ['theme-layer'], +}) + +// consuming-app/app/app.config.ts +export default defineAppConfig({ + theme: { + primaryColor: '#ff0000', // Override + }, +}) +``` + + diff --git a/.claude/skills/nuxt/references/advanced-module-authoring.md b/.claude/skills/nuxt/references/advanced-module-authoring.md new file mode 100644 index 0000000..e08525d --- /dev/null +++ b/.claude/skills/nuxt/references/advanced-module-authoring.md @@ -0,0 +1,554 @@ +--- +name: module-authoring +description: Complete guide to creating publishable Nuxt modules with best practices +--- + +# Module Authoring + +This guide covers creating publishable Nuxt modules with proper structure, type safety, and best practices. + +## Module Structure + +Recommended structure for a publishable module: + +``` +my-nuxt-module/ +├── src/ +│ ├── module.ts # Module entry +│ └── runtime/ +│ ├── components/ # Vue components +│ ├── composables/ # Composables +│ ├── plugins/ # Nuxt plugins +│ └── server/ # Server handlers +├── playground/ # Development app +├── package.json +└── tsconfig.json +``` + +## Module Definition + +### Basic Module with Type-safe Options + +```ts +// src/module.ts +import { defineNuxtModule, createResolver, addPlugin, addComponent, addImports } from '@nuxt/kit' + +export interface ModuleOptions { + prefix?: string + apiKey: string + enabled?: boolean +} + +export default defineNuxtModule({ + meta: { + name: 'my-module', + configKey: 'myModule', + compatibility: { + nuxt: '>=3.0.0', + }, + }, + defaults: { + prefix: 'My', + enabled: true, + }, + setup(options, nuxt) { + if (!options.enabled) return + + const { resolve } = createResolver(import.meta.url) + + // Module setup logic here + }, +}) +``` + +### Using `.with()` for Strict Type Inference + +When you need TypeScript to infer that default values are always present: + +```ts +import { defineNuxtModule } from '@nuxt/kit' + +interface ModuleOptions { + apiKey: string + baseURL: string + timeout?: number +} + +export default defineNuxtModule().with({ + meta: { + name: '@nuxtjs/my-api', + configKey: 'myApi', + }, + defaults: { + baseURL: 'https://api.example.com', + timeout: 5000, + }, + setup(resolvedOptions, nuxt) { + // resolvedOptions.baseURL is guaranteed to be string (not undefined) + // resolvedOptions.timeout is guaranteed to be number (not undefined) + }, +}) +``` + +## Adding Runtime Assets + +### Components + +```ts +import { addComponent, addComponentsDir, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const { resolve } = createResolver(import.meta.url) + + // Single component + addComponent({ + name: 'MyButton', + filePath: resolve('./runtime/components/MyButton.vue'), + }) + + // Component directory with prefix + addComponentsDir({ + path: resolve('./runtime/components'), + prefix: 'My', + pathPrefix: false, + }) + }, +}) +``` + +### Composables and Auto-imports + +```ts +import { addImports, addImportsDir, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const { resolve } = createResolver(import.meta.url) + + // Single import + addImports({ + name: 'useMyUtil', + from: resolve('./runtime/composables/useMyUtil'), + }) + + // Directory of composables + addImportsDir(resolve('./runtime/composables')) + }, +}) +``` + +### Plugins + +```ts +import { addPlugin, addPluginTemplate, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options) { + const { resolve } = createResolver(import.meta.url) + + // Static plugin file + addPlugin({ + src: resolve('./runtime/plugins/myPlugin'), + mode: 'client', // 'client', 'server', or 'all' + }) + + // Dynamic plugin with generated code + addPluginTemplate({ + filename: 'my-module-plugin.mjs', + getContents: () => ` +import { defineNuxtPlugin } from '#app/nuxt' + +export default defineNuxtPlugin({ + name: 'my-module', + setup() { + const config = ${JSON.stringify(options)} + // Plugin logic + } +})`, + }) + }, +}) +``` + +## Server Extensions + +### Server Handlers + +```ts +import { addServerHandler, addServerScanDir, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const { resolve } = createResolver(import.meta.url) + + // Single handler + addServerHandler({ + route: '/api/my-endpoint', + handler: resolve('./runtime/server/api/my-endpoint'), + }) + + // Scan entire server directory (api/, routes/, middleware/, utils/) + addServerScanDir(resolve('./runtime/server')) + }, +}) +``` + +### Server Composables + +```ts +import { addServerImports, addServerImportsDir, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const { resolve } = createResolver(import.meta.url) + + // Single server import + addServerImports({ + name: 'useServerUtil', + from: resolve('./runtime/server/utils/useServerUtil'), + }) + + // Server composables directory + addServerImportsDir(resolve('./runtime/server/composables')) + }, +}) +``` + +### Nitro Plugin + +```ts +import { addServerPlugin, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const { resolve } = createResolver(import.meta.url) + addServerPlugin(resolve('./runtime/server/plugin')) + }, +}) +``` + +```ts +// runtime/server/plugin.ts +import { defineNitroPlugin } from 'nitropack/runtime' + +export default defineNitroPlugin((nitroApp) => { + nitroApp.hooks.hook('request', (event) => { + console.log('Request:', event.path) + }) +}) +``` + +## Templates and Virtual Files + +### Generate Virtual Files + +```ts +import { addTemplate, addTypeTemplate, addServerTemplate, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup(options, nuxt) { + const { resolve } = createResolver(import.meta.url) + + // Client/build virtual file (accessible via #build/my-config.mjs) + addTemplate({ + filename: 'my-config.mjs', + getContents: () => `export default ${JSON.stringify(options)}`, + }) + + // Type declarations + addTypeTemplate({ + filename: 'types/my-module.d.ts', + getContents: () => ` +declare module '#my-module' { + export interface Config { + apiKey: string + } +}`, + }) + + // Nitro virtual file (accessible in server routes) + addServerTemplate({ + filename: '#my-module/config.mjs', + getContents: () => `export const config = ${JSON.stringify(options)}`, + }) + }, +}) +``` + +### Access Virtual Files + +```ts +// In runtime plugin +// @ts-expect-error - virtual file +import config from '#build/my-config.mjs' + +// In server routes +import { config } from '#my-module/config.js' +``` + +## Extending Pages and Routes + +```ts +import { extendPages, extendRouteRules, addRouteMiddleware, createResolver } from '@nuxt/kit' + +export default defineNuxtModule({ + setup() { + const { resolve } = createResolver(import.meta.url) + + // Add pages + extendPages((pages) => { + pages.push({ + name: 'my-page', + path: '/my-route', + file: resolve('./runtime/pages/MyPage.vue'), + }) + }) + + // Add route rules (caching, redirects, etc.) + extendRouteRules('/api/**', { + cache: { maxAge: 60 }, + }) + + // Add middleware + addRouteMiddleware({ + name: 'my-middleware', + path: resolve('./runtime/middleware/myMiddleware'), + global: true, + }) + }, +}) +``` + +## Module Dependencies + +Declare dependencies on other modules with version constraints: + +```ts +export default defineNuxtModule({ + meta: { + name: 'my-module', + }, + moduleDependencies: { + '@nuxtjs/tailwindcss': { + version: '>=6.0.0', + // Set defaults (user can override) + defaults: { + exposeConfig: true, + }, + // Force specific options + overrides: { + viewer: false, + }, + }, + '@nuxtjs/i18n': { + optional: true, // Won't fail if not installed + defaults: { + defaultLocale: 'en', + }, + }, + }, + setup() { + // Dependencies are guaranteed to be set up before this runs + }, +}) +``` + +### Dynamic Dependencies + +```ts +moduleDependencies(nuxt) { + const deps: Record = { + '@nuxtjs/tailwindcss': { version: '>=6.0.0' }, + } + + if (nuxt.options.ssr) { + deps['@nuxtjs/html-validator'] = { optional: true } + } + + return deps +} +``` + +## Lifecycle Hooks + +Requires `meta.name` and `meta.version`: + +```ts +export default defineNuxtModule({ + meta: { + name: 'my-module', + version: '1.2.0', + }, + onInstall(nuxt) { + // First-time setup + console.log('Module installed for the first time') + }, + onUpgrade(nuxt, options, previousVersion) { + // Version upgrade migrations + console.log(`Upgrading from ${previousVersion}`) + }, + setup(options, nuxt) { + // Regular setup runs every build + }, +}) +``` + +## Extending Configuration + +```ts +export default defineNuxtModule({ + setup(options, nuxt) { + // Add CSS + nuxt.options.css.push('my-module/styles.css') + + // Add runtime config + nuxt.options.runtimeConfig.public.myModule = { + apiUrl: options.apiUrl, + } + + // Extend Vite config + nuxt.options.vite.optimizeDeps ||= {} + nuxt.options.vite.optimizeDeps.include ||= [] + nuxt.options.vite.optimizeDeps.include.push('some-package') + + // Add build transpile + nuxt.options.build.transpile.push('my-package') + }, +}) +``` + +## Using Hooks + +```ts +export default defineNuxtModule({ + // Declarative hooks + hooks: { + 'components:dirs': (dirs) => { + dirs.push({ path: '~/extra' }) + }, + }, + + setup(options, nuxt) { + // Programmatic hooks + nuxt.hook('pages:extend', (pages) => { + // Modify pages + }) + + nuxt.hook('imports:extend', (imports) => { + imports.push({ name: 'myHelper', from: 'my-package' }) + }) + + nuxt.hook('nitro:config', (config) => { + // Modify Nitro config + }) + + nuxt.hook('vite:extendConfig', (config) => { + // Modify Vite config + }) + }, +}) +``` + +## Path Resolution + +```ts +import { createResolver, resolvePath, findPath } from '@nuxt/kit' + +export default defineNuxtModule({ + async setup(options, nuxt) { + // Resolver relative to module + const { resolve } = createResolver(import.meta.url) + + const pluginPath = resolve('./runtime/plugin') + + // Resolve with extensions and aliases + const entrypoint = await resolvePath('@some/package') + + // Find first existing file + const configPath = await findPath([ + resolve('./config.ts'), + resolve('./config.js'), + ]) + }, +}) +``` + +## Module Package.json + +```json +{ + "name": "my-nuxt-module", + "version": "1.0.0", + "type": "module", + "exports": { + ".": { + "import": "./dist/module.mjs", + "require": "./dist/module.cjs" + } + }, + "main": "./dist/module.cjs", + "module": "./dist/module.mjs", + "types": "./dist/types.d.ts", + "files": ["dist"], + "scripts": { + "dev": "nuxi dev playground", + "build": "nuxt-module-build build", + "prepare": "nuxt-module-build build --stub" + }, + "dependencies": { + "@nuxt/kit": "^3.0.0" + }, + "devDependencies": { + "@nuxt/module-builder": "latest", + "nuxt": "^3.0.0" + } +} +``` + +## Disabling Modules + +Users can disable a module via config key: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + // Disable entirely + myModule: false, + + // Or with options + myModule: { + enabled: false, + }, +}) +``` + +## Development Workflow + +1. **Create module**: `npx nuxi init -t module my-module` +2. **Develop**: `npm run dev` (runs playground) +3. **Build**: `npm run build` +4. **Test**: `npm run test` + +## Best Practices + +- Use `createResolver(import.meta.url)` for all path resolution +- Prefix components to avoid naming conflicts +- Make options type-safe with `ModuleOptions` interface +- Use `moduleDependencies` instead of `installModule` +- Provide sensible defaults for all options +- Add compatibility requirements in `meta.compatibility` +- Use virtual files for dynamic configuration +- Separate client/server plugins appropriately + + diff --git a/.claude/skills/nuxt/references/best-practices-data-fetching.md b/.claude/skills/nuxt/references/best-practices-data-fetching.md new file mode 100644 index 0000000..ded5d2e --- /dev/null +++ b/.claude/skills/nuxt/references/best-practices-data-fetching.md @@ -0,0 +1,357 @@ +--- +name: data-fetching-best-practices +description: Patterns and best practices for efficient data fetching in Nuxt +--- + +# Data Fetching Best Practices + +Effective data fetching patterns for SSR-friendly, performant Nuxt applications. + +## Choose the Right Tool + +| Scenario | Use | +|----------|-----| +| Component initial data | `useFetch` or `useAsyncData` | +| User interactions (clicks, forms) | `$fetch` | +| Third-party SDK/API | `useAsyncData` with custom function | +| Multiple parallel requests | `useAsyncData` with `Promise.all` | + +## Await vs Non-Await Usage + +The `await` keyword controls whether data fetching **blocks navigation**: + +### With `await` - Blocking Navigation + +```vue + +``` + +- **Server**: Fetches data and includes it in the payload +- **Client hydration**: Uses payload data, no re-fetch +- **Client navigation**: Blocks until data is ready + +### Without `await` - Non-Blocking (Lazy) + +```vue + + + +``` + +Equivalent to using `useLazyFetch`: + +```vue + +``` + +### When to Use Each + +| Pattern | Use Case | +|---------|----------| +| `await useFetch()` | Critical data needed for SEO/initial render | +| `useFetch({ lazy: true })` | Non-critical data, better perceived performance | +| `await useLazyFetch()` | Same as lazy, await only ensures initialization | + +## Avoid Double Fetching + +### ❌ Wrong: Using $fetch Alone in Setup + +```vue + +``` + +### ✅ Correct: Use useFetch + +```vue + +``` + +## Use Explicit Cache Keys + +### ❌ Avoid: Auto-generated Keys + +```vue + +``` + +### ✅ Better: Explicit Keys + +```vue + +``` + +## Handle Loading States Properly + +```vue + + + +``` + +## Use Lazy Fetching for Non-critical Data + +```vue + + + +``` + +## Minimize Payload Size + +### Use `pick` for Simple Filtering + +```vue + +``` + +### Use `transform` for Complex Transformations + +```vue + +``` + +## Parallel Fetching + +### Fetch Independent Data with useAsyncData + +```vue + +``` + +### Multiple useFetch Calls + +```vue + +``` + +## Efficient Refresh Patterns + +### Watch Reactive Dependencies + +```vue + +``` + +### Manual Refresh + +```vue + +``` + +### Conditional Fetching + +```vue + +``` + +## Server-only Fetching + +```vue + +``` + +## Error Handling + +```vue + + + +``` + +## Shared Data Across Components + +```vue + + + + + +``` + +## Avoid useAsyncData for Side Effects + +### ❌ Wrong: Side Effects in useAsyncData + +```vue + +``` + +### ✅ Correct: Use callOnce for Side Effects + +```vue + +``` + + diff --git a/.claude/skills/nuxt/references/best-practices-ssr.md b/.claude/skills/nuxt/references/best-practices-ssr.md new file mode 100644 index 0000000..2befef0 --- /dev/null +++ b/.claude/skills/nuxt/references/best-practices-ssr.md @@ -0,0 +1,355 @@ +--- +name: ssr-best-practices +description: Avoiding SSR context leaks, hydration mismatches, and proper composable usage +--- + +# SSR Best Practices + +Patterns for avoiding common SSR pitfalls: context leaks, hydration mismatches, and composable errors. + +## The "Nuxt Instance Unavailable" Error + +This error occurs when calling Nuxt composables outside the proper context. + +### ❌ Wrong: Composable Outside Setup + +```ts +// composables/bad.ts +// Called at module level - no Nuxt context! +const config = useRuntimeConfig() + +export function useMyComposable() { + return config.public.apiBase +} +``` + +### ✅ Correct: Composable Inside Function + +```ts +// composables/good.ts +export function useMyComposable() { + // Called inside the composable - has context + const config = useRuntimeConfig() + return config.public.apiBase +} +``` + +### Valid Contexts for Composables + +Nuxt composables work in: +- ` +``` + +### ✅ Correct: Use SSR-safe Alternatives + +```vue + +``` + +### ❌ Wrong: Random/Time-based Values + +```vue + +``` + +### ✅ Correct: Use useState for Consistency + +```vue + + + +``` + +### ❌ Wrong: Conditional Rendering on Client State + +```vue + +``` + +### ✅ Correct: Use CSS or ClientOnly + +```vue + +``` + +## Browser-only Code + +### Use `import.meta.client` + +```vue + +``` + +### Use `onMounted` for DOM Access + +```vue + +``` + +### Dynamic Imports for Browser Libraries + +```vue + +``` + +## Server-only Code + +### Use `import.meta.server` + +```vue + +``` + +### Server Components + +```vue + + + + +``` + +## Async Composable Patterns + +### ❌ Wrong: Await Before Composable + +```vue + +``` + +### ✅ Correct: Get Context First + +```vue + +``` + +## Plugin Best Practices + +### Client-only Plugins + +```ts +// plugins/analytics.client.ts +export default defineNuxtPlugin(() => { + // Only runs on client + initAnalytics() +}) +``` + +### Server-only Plugins + +```ts +// plugins/server-init.server.ts +export default defineNuxtPlugin(() => { + // Only runs on server + initServerConnections() +}) +``` + +### Provide/Inject Pattern + +```ts +// plugins/api.ts +export default defineNuxtPlugin(() => { + const api = createApiClient() + + return { + provide: { + api, + }, + } +}) +``` + +```vue + +``` + +## Third-party Library Integration + +### ❌ Wrong: Import at Top Level + +```vue + +``` + +### ✅ Correct: Dynamic Import + +```vue + +``` + +### Use ClientOnly Component + +```vue + +``` + +## Debugging SSR Issues + +### Check Rendering Context + +```vue + +``` + +### Use Nuxt DevTools + +DevTools shows payload data and hydration state. + +### Common Error Messages + +| Error | Cause | +|-------|-------| +| "Nuxt instance unavailable" | Composable called outside setup context | +| "Hydration mismatch" | Server/client HTML differs | +| "window is not defined" | Browser API used during SSR | +| "document is not defined" | DOM access during SSR | + + diff --git a/.claude/skills/nuxt/references/core-cli.md b/.claude/skills/nuxt/references/core-cli.md new file mode 100644 index 0000000..1487836 --- /dev/null +++ b/.claude/skills/nuxt/references/core-cli.md @@ -0,0 +1,263 @@ +--- +name: cli-commands +description: Nuxt CLI commands for development, building, and project management +--- + +# CLI Commands + +Nuxt provides CLI commands via `nuxi` (or `npx nuxt`) for development, building, and project management. + +## Project Initialization + +### Create New Project + +```bash +# Interactive project creation +npx nuxi@latest init my-app + +# With specific package manager +npx nuxi@latest init my-app --packageManager pnpm + +# With modules +npx nuxi@latest init my-app --modules "@nuxt/ui,@nuxt/image" + +# From template +npx nuxi@latest init my-app --template v3 + +# Skip module selection prompt +npx nuxi@latest init my-app --no-modules +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `-t, --template` | Template name | +| `--packageManager` | npm, pnpm, yarn, or bun | +| `-M, --modules` | Modules to install (comma-separated) | +| `--gitInit` | Initialize git repository | +| `--no-install` | Skip installing dependencies | + +## Development + +### Start Dev Server + +```bash +# Start development server (default: http://localhost:3000) +npx nuxt dev + +# Custom port +npx nuxt dev --port 4000 + +# Open in browser +npx nuxt dev --open + +# Listen on all interfaces (for mobile testing) +npx nuxt dev --host 0.0.0.0 + +# With HTTPS +npx nuxt dev --https + +# Clear console on restart +npx nuxt dev --clear + +# Create public tunnel +npx nuxt dev --tunnel +``` + +**Options:** +| Option | Description | +|--------|-------------| +| `-p, --port` | Port to listen on | +| `-h, --host` | Host to listen on | +| `-o, --open` | Open in browser | +| `--https` | Enable HTTPS | +| `--tunnel` | Create public tunnel (via untun) | +| `--qr` | Show QR code for mobile | +| `--clear` | Clear console on restart | + +**Environment Variables:** +- `NUXT_PORT` or `PORT` - Default port +- `NUXT_HOST` or `HOST` - Default host + +## Building + +### Production Build + +```bash +# Build for production +npx nuxt build + +# Build with prerendering +npx nuxt build --prerender + +# Build with specific preset +npx nuxt build --preset node-server +npx nuxt build --preset cloudflare-pages +npx nuxt build --preset vercel + +# Build with environment +npx nuxt build --envName staging +``` + +Output is created in `.output/` directory. + +### Static Generation + +```bash +# Generate static site (prerenders all routes) +npx nuxt generate +``` + +Equivalent to `nuxt build --prerender`. Creates static HTML files for deployment to static hosting. + +### Preview Production Build + +```bash +# Preview after build +npx nuxt preview + +# Custom port +npx nuxt preview --port 4000 +``` + +## Utilities + +### Prepare (Type Generation) + +```bash +# Generate TypeScript types and .nuxt directory +npx nuxt prepare +``` + +Run after cloning or when types are missing. + +### Type Check + +```bash +# Run TypeScript type checking +npx nuxt typecheck +``` + +### Analyze Bundle + +```bash +# Analyze production bundle +npx nuxt analyze +``` + +Opens visual bundle analyzer. + +### Cleanup + +```bash +# Remove generated files (.nuxt, .output, node_modules/.cache) +npx nuxt cleanup +``` + +### Info + +```bash +# Show environment info (useful for bug reports) +npx nuxt info +``` + +### Upgrade + +```bash +# Upgrade Nuxt to latest version +npx nuxt upgrade + +# Upgrade to nightly release +npx nuxt upgrade --nightly +``` + +## Module Commands + +### Add Module + +```bash +# Add a Nuxt module +npx nuxt module add @nuxt/ui +npx nuxt module add @nuxt/image +``` + +Installs and adds to `nuxt.config.ts`. + +### Build Module (for module authors) + +```bash +# Build a Nuxt module +npx nuxt build-module +``` + +## DevTools + +```bash +# Enable DevTools globally +npx nuxt devtools enable + +# Disable DevTools +npx nuxt devtools disable +``` + +## Common Workflows + +### Development + +```bash +# Install dependencies and start dev +pnpm install +pnpm dev # or npx nuxt dev +``` + +### Production Deployment + +```bash +# Build and preview locally +pnpm build +pnpm preview + +# Or for static hosting +pnpm generate +``` + +### After Cloning + +```bash +# Install deps and prepare types +pnpm install +npx nuxt prepare +``` + +## Environment-specific Builds + +```bash +# Development build +npx nuxt build --envName development + +# Staging build +npx nuxt build --envName staging + +# Production build (default) +npx nuxt build --envName production +``` + +Corresponds to `$development`, `$env.staging`, `$production` in `nuxt.config.ts`. + +## Layer Extension + +```bash +# Dev with additional layer +npx nuxt dev --extends ./base-layer + +# Build with layer +npx nuxt build --extends ./base-layer +``` + + diff --git a/.claude/skills/nuxt/references/core-config.md b/.claude/skills/nuxt/references/core-config.md new file mode 100644 index 0000000..ebce5d5 --- /dev/null +++ b/.claude/skills/nuxt/references/core-config.md @@ -0,0 +1,162 @@ +--- +name: configuration +description: Nuxt configuration files including nuxt.config.ts, app.config.ts, and runtime configuration +--- + +# Nuxt Configuration + +Nuxt uses configuration files to customize application behavior. The main configuration options are `nuxt.config.ts` for build-time settings and `app.config.ts` for runtime settings. + +## nuxt.config.ts + +The main configuration file at the root of your project: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + // Configuration options + devtools: { enabled: true }, + modules: ['@nuxt/ui'], +}) +``` + +### Environment Overrides + +Configure environment-specific settings: + +```ts +export default defineNuxtConfig({ + $production: { + routeRules: { + '/**': { isr: true }, + }, + }, + $development: { + // Development-specific config + }, + $env: { + staging: { + // Staging environment config + }, + }, +}) +``` + +Use `--envName` flag to select environment: `nuxt build --envName staging` + +## Runtime Config + +For values that need to be overridden via environment variables: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + runtimeConfig: { + // Server-only keys + apiSecret: '123', + // Keys within public are exposed to client + public: { + apiBase: '/api', + }, + }, +}) +``` + +Override with environment variables: + +```ini +# .env +NUXT_API_SECRET=api_secret_token +NUXT_PUBLIC_API_BASE=https://api.example.com +``` + +Access in components/composables: + +```vue + +``` + +## App Config + +For public tokens determined at build time (not overridable via env vars): + +```ts +// app/app.config.ts +export default defineAppConfig({ + title: 'Hello Nuxt', + theme: { + dark: true, + colors: { + primary: '#ff0000', + }, + }, +}) +``` + +Access in components: + +```vue + +``` + +## runtimeConfig vs app.config + +| Feature | runtimeConfig | app.config | +|---------|--------------|------------| +| Client-side | Hydrated | Bundled | +| Environment variables | Yes | No | +| Reactive | Yes | Yes | +| Hot module replacement | No | Yes | +| Non-primitive JS types | No | Yes | + +**Use runtimeConfig** for secrets and values that change per environment. +**Use app.config** for public tokens, theme settings, and non-sensitive config. + +## External Tool Configuration + +Nuxt uses `nuxt.config.ts` as single source of truth. Configure external tools within it: + +```ts +export default defineNuxtConfig({ + // Nitro configuration + nitro: { + // nitro options + }, + // Vite configuration + vite: { + // vite options + vue: { + // @vitejs/plugin-vue options + }, + }, + // PostCSS configuration + postcss: { + // postcss options + }, +}) +``` + +## Vue Configuration + +Enable Vue experimental features: + +```ts +export default defineNuxtConfig({ + vue: { + propsDestructure: true, + }, +}) +``` + + diff --git a/.claude/skills/nuxt/references/core-data-fetching.md b/.claude/skills/nuxt/references/core-data-fetching.md new file mode 100644 index 0000000..d662efb --- /dev/null +++ b/.claude/skills/nuxt/references/core-data-fetching.md @@ -0,0 +1,236 @@ +--- +name: data-fetching +description: useFetch, useAsyncData, and $fetch for SSR-friendly data fetching +--- + +# Data Fetching + +Nuxt provides composables for SSR-friendly data fetching that prevent double-fetching and handle hydration. + +## Overview + +- `$fetch` - Basic fetch utility (use for client-side events) +- `useFetch` - SSR-safe wrapper around $fetch (use for component data) +- `useAsyncData` - SSR-safe wrapper for any async function + +## useFetch + +Primary composable for fetching data in components: + +```vue + + + +``` + +### With Options + +```ts +const { data } = await useFetch('/api/posts', { + // Query parameters + query: { page: 1, limit: 10 }, + // Request body (for POST/PUT) + body: { title: 'New Post' }, + // HTTP method + method: 'POST', + // Only pick specific fields + pick: ['id', 'title'], + // Transform response + transform: (posts) => posts.map(p => ({ ...p, slug: slugify(p.title) })), + // Custom key for caching + key: 'posts-list', + // Don't fetch on server + server: false, + // Don't block navigation + lazy: true, + // Don't fetch immediately + immediate: false, + // Default value + default: () => [], +}) +``` + +### Reactive Parameters + +```vue + +``` + +### Computed URL + +```vue + +``` + +## useAsyncData + +For wrapping any async function: + +```vue + +``` + +### Multiple Requests + +```vue + +``` + +## $fetch + +For client-side events (form submissions, button clicks): + +```vue + +``` + +**Important**: Don't use `$fetch` alone in setup for initial data - it will fetch twice (server + client). Use `useFetch` or `useAsyncData` instead. + +## Return Values + +All composables return: + +| Property | Type | Description | +|----------|------|-------------| +| `data` | `Ref` | Fetched data | +| `error` | `Ref` | Error if request failed | +| `status` | `Ref<'idle' \| 'pending' \| 'success' \| 'error'>` | Request status | +| `refresh` | `() => Promise` | Refetch data | +| `execute` | `() => Promise` | Alias for refresh | +| `clear` | `() => void` | Reset data and error | + +## Lazy Fetching + +Don't block navigation: + +```vue + +``` + +## Refresh & Watch + +```vue + +``` + +## Caching + +Data is cached by key. Share data across components: + +```vue + +``` + +Refresh cached data globally: + +```ts +// Refresh specific key +await refreshNuxtData('current-user') + +// Refresh all data +await refreshNuxtData() + +// Clear cached data +clearNuxtData('current-user') +``` + +## Interceptors + +```ts +const { data } = await useFetch('/api/auth', { + onRequest({ options }) { + options.headers.set('Authorization', `Bearer ${token}`) + }, + onRequestError({ error }) { + console.error('Request failed:', error) + }, + onResponse({ response }) { + // Process response + }, + onResponseError({ response }) { + if (response.status === 401) { + navigateTo('/login') + } + }, +}) +``` + +## Passing Headers (SSR) + +`useFetch` automatically proxies cookies/headers from client to server. For `$fetch`: + +```vue + +``` + + diff --git a/.claude/skills/nuxt/references/core-deployment.md b/.claude/skills/nuxt/references/core-deployment.md new file mode 100644 index 0000000..6fca597 --- /dev/null +++ b/.claude/skills/nuxt/references/core-deployment.md @@ -0,0 +1,224 @@ +--- +name: deployment +description: Deploying Nuxt applications to various hosting platforms +--- + +# Deployment + +Nuxt is platform-agnostic thanks to [Nitro](https://nitro.build), its server engine. You can deploy to almost any platform with minimal configuration—Node.js servers, static hosting, serverless functions, or edge networks. + +> **Full list of supported platforms:** https://nitro.build/deploy + +## Deployment Modes + +### Node.js Server + +```bash +# Build for Node.js +nuxt build + +# Run production server +node .output/server/index.mjs +``` + +Environment variables: +- `PORT` or `NITRO_PORT` (default: 3000) +- `HOST` or `NITRO_HOST` (default: 0.0.0.0) + +### Static Generation + +```bash +# Generate static site +nuxt generate +``` + +Output in `.output/public/` - deploy to any static host. + +### Preset Configuration + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + nitro: { + preset: 'vercel', // or 'netlify', 'cloudflare-pages', etc. + }, +}) +``` + +Or via environment variable: + +```bash +NITRO_PRESET=vercel nuxt build +``` + +--- + +## Recommended Platforms + +When helping users choose a deployment platform, consider their needs: + +### Vercel + +**Best for:** Projects wanting zero-config deployment with excellent DX + +```bash +# Install Vercel CLI +npm i -g vercel + +# Deploy +vercel +``` + +**Pros:** +- Zero configuration for Nuxt (auto-detects) +- Excellent preview deployments for PRs +- Built-in analytics and speed insights +- Edge Functions support +- Great free tier for personal projects + +**Cons:** +- Can get expensive at scale (bandwidth costs) +- Vendor lock-in concerns +- Limited build minutes on free tier + +**Recommended when:** User wants fastest setup, values DX, building SaaS or marketing sites. + +--- + +### Netlify + +**Best for:** JAMstack sites, static-heavy apps, teams needing forms/identity + +```bash +# Install Netlify CLI +npm i -g netlify-cli + +# Deploy +netlify deploy --prod +``` + +**Pros:** +- Great free tier with generous bandwidth +- Built-in forms, identity, and functions +- Excellent for static sites with some dynamic features +- Good preview deployments +- Split testing built-in + +**Cons:** +- SSR/serverless functions can be slower than Vercel +- Less optimized for full SSR apps +- Build minutes can run out on free tier + +**Recommended when:** User has static-heavy site, needs built-in forms/auth, or prefers Netlify ecosystem. + +--- + +### Cloudflare Pages + +**Best for:** Global performance, edge computing, cost-conscious projects + +```bash +# Build with Cloudflare preset +NITRO_PRESET=cloudflare-pages nuxt build +``` + +**Pros:** +- Unlimited bandwidth on free tier +- Excellent global edge network (fastest TTFB) +- Workers for edge computing +- Very cost-effective at scale +- D1, KV, R2 for data storage + +**Cons:** +- Workers have execution limits (CPU time) +- Some Node.js APIs not available in Workers +- Less mature than Vercel/Netlify for frameworks + +**Recommended when:** User prioritizes performance, global reach, or cost at scale. + +--- + +### GitHub Actions + Self-hosted/VPS + +**Best for:** Full control, existing infrastructure, CI/CD customization + +```yaml +# .github/workflows/deploy.yml +name: Deploy +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - run: npm ci + - run: npm run build + + # Deploy to your server (example: rsync to VPS) + - name: Deploy to server + run: rsync -avz .output/ user@server:/app/ +``` + +**Pros:** +- Full control over build and deployment +- No vendor lock-in +- Can deploy anywhere (VPS, Docker, Kubernetes) +- Free CI/CD minutes for public repos +- Customizable workflows + +**Cons:** +- Requires more setup and maintenance +- Need to manage your own infrastructure +- No built-in preview deployments +- SSL, scaling, monitoring are your responsibility + +**Recommended when:** User has existing infrastructure, needs full control, or deploying to private/enterprise environments. + +--- + +## Quick Decision Guide + +| Need | Recommendation | +|------|----------------| +| Fastest setup, small team | **Vercel** | +| Static site with forms | **Netlify** | +| Cost-sensitive at scale | **Cloudflare Pages** | +| Full control / enterprise | **GitHub Actions + VPS** | +| Docker/Kubernetes | **GitHub Actions + Container Registry** | +| Serverless APIs | **Vercel** or **AWS Lambda** | + +## Docker Deployment + +```dockerfile +FROM node:20-alpine AS builder +WORKDIR /app +COPY package*.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM node:20-alpine +WORKDIR /app +COPY --from=builder /app/.output .output +ENV PORT=3000 +EXPOSE 3000 +CMD ["node", ".output/server/index.mjs"] +``` + +```bash +docker build -t my-nuxt-app . +docker run -p 3000:3000 my-nuxt-app +``` + + diff --git a/.claude/skills/nuxt/references/core-directory-structure.md b/.claude/skills/nuxt/references/core-directory-structure.md new file mode 100644 index 0000000..415e112 --- /dev/null +++ b/.claude/skills/nuxt/references/core-directory-structure.md @@ -0,0 +1,269 @@ +--- +name: directory-structure +description: Nuxt project folder structure, conventions, and file organization +--- + +# Directory Structure + +Nuxt uses conventions-based directory structure. Understanding it is key to effective development. + +## Standard Project Structure + +``` +my-nuxt-app/ +├── app/ # Application source (can be at root level) +│ ├── app.vue # Root component +│ ├── app.config.ts # App configuration (runtime) +│ ├── error.vue # Error page +│ ├── components/ # Auto-imported Vue components +│ ├── composables/ # Auto-imported composables +│ ├── layouts/ # Layout components +│ ├── middleware/ # Route middleware +│ ├── pages/ # File-based routing +│ ├── plugins/ # Vue plugins +│ └── utils/ # Auto-imported utilities +├── assets/ # Build-processed assets (CSS, images) +├── public/ # Static assets (served as-is) +├── server/ # Server-side code +│ ├── api/ # API routes (/api/*) +│ ├── routes/ # Server routes +│ ├── middleware/ # Server middleware +│ ├── plugins/ # Nitro plugins +│ └── utils/ # Server utilities (auto-imported) +├── content/ # Content files (@nuxt/content) +├── layers/ # Local layers (auto-scanned) +├── modules/ # Local modules +├── nuxt.config.ts # Nuxt configuration +├── package.json +└── tsconfig.json +``` + +## Key Directories + +### `app/` Directory + +Contains all application code. Can also be at root level (without `app/` folder). + +```ts +// nuxt.config.ts - customize source directory +export default defineNuxtConfig({ + srcDir: 'src/', // Change from 'app/' to 'src/' +}) +``` + +### `app/components/` + +Vue components auto-imported by name: + +``` +components/ +├── Button.vue →