diff --git a/.claude/skills/nuxt-ui/SKILL.md b/.claude/skills/nuxt-ui/SKILL.md new file mode 100644 index 0000000..d27f49e --- /dev/null +++ b/.claude/skills/nuxt-ui/SKILL.md @@ -0,0 +1,94 @@ +--- +name: nuxt-ui +description: Use when building styled UI with @nuxt/ui v4 components - create forms with validation, implement data tables with sorting, build modal dialogs and overlays, configure Tailwind Variants theming. Use vue skill for raw component patterns, reka-ui for headless primitives. +license: MIT +--- + +# Nuxt UI v4 + +Component library for Vue 3 and Nuxt 4+ built on Reka UI (headless) + Tailwind CSS v4 + Tailwind Variants. + +**Current stable version:** v4.4.0 (January 2026) + +## When to Use + +- Installing/configuring @nuxt/ui +- Using UI components (Button, Card, Table, Form, etc.) +- Customizing theme (colors, variants, CSS variables) +- Building forms with validation +- Using overlays (Modal, Toast, CommandPalette) +- Working with composables (useToast, useOverlay) + +**For Vue component patterns:** use `vue` skill +**For Nuxt routing/server:** use `nuxt` skill + +## Available Guidance + +| File | Topics | +| ------------------------------------------------------------ | -------------------------------------------------------------------------------- | +| **[references/installation.md](references/installation.md)** | Nuxt/Vue setup, pnpm gotchas, UApp wrapper, module options, prefix, tree-shaking | +| **[references/theming.md](references/theming.md)** | Semantic colors, CSS variables, app.config.ts, Tailwind Variants | +| **[references/components.md](references/components.md)** | Component index by category (125+ components) | +| **components/\*.md** | Per-component details (button.md, modal.md, etc.) | +| **[references/forms.md](references/forms.md)** | Form components, validation (Zod/Valibot), useFormField | +| **[references/overlays.md](references/overlays.md)** | Toast, Modal, Slideover, Drawer, CommandPalette | +| **[references/composables.md](references/composables.md)** | useToast, useOverlay, defineShortcuts, useScrollspy | + +## Loading Files + +**Consider loading these reference files based on your task:** + +- [ ] [references/installation.md](references/installation.md) - if installing or configuring @nuxt/ui +- [ ] [references/theming.md](references/theming.md) - if customizing theme, colors, or Tailwind Variants +- [ ] [references/components.md](references/components.md) - if browsing component index or finding components by category +- [ ] [references/forms.md](references/forms.md) - if building forms with validation (Zod/Valibot) +- [ ] [references/overlays.md](references/overlays.md) - if using Toast, Modal, Slideover, Drawer, or CommandPalette +- [ ] [references/composables.md](references/composables.md) - if using useToast, useOverlay, or other composables + +**DO NOT load all files at once.** Load only what's relevant to your current task. + +## Key Concepts + +| Concept | Description | +| ----------------- | ---------------------------------------------------------- | +| UApp | Required wrapper component for Toast, Tooltip, overlays | +| Tailwind Variants | Type-safe styling with slots, variants, compoundVariants | +| Semantic Colors | primary, secondary, success, error, warning, info, neutral | +| Reka UI | Headless component primitives (accessibility built-in) | + +> For headless component primitives (API details, accessibility patterns, asChild): read the **reka-ui** skill + +## Quick Reference + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@nuxt/ui'], + css: ['~/assets/css/main.css'] +}) +``` + +```css +/* assets/css/main.css */ +@import 'tailwindcss'; +@import '@nuxt/ui'; +``` + +```vue + + +``` + +## Resources + +- [Nuxt UI Docs](https://ui.nuxt.com) +- [Component Reference](https://ui.nuxt.com/components) +- [Theme Customization](https://ui.nuxt.com/getting-started/theme) + +--- + +_Token efficiency: Main skill ~300 tokens, each sub-file ~800-1200 tokens_ diff --git a/.claude/skills/nuxt-ui/components/accordion.md b/.claude/skills/nuxt-ui/components/accordion.md new file mode 100644 index 0000000..1da47dd --- /dev/null +++ b/.claude/skills/nuxt-ui/components/accordion.md @@ -0,0 +1,51 @@ +# Accordion + +A stacked set of collapsible panels. + +> Based on [Reka UI Accordion](https://reka-ui.com/docs/components/accordion) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `trailingIcon?: string`{lang="ts-type"} +- `content?: string`{lang="ts-type"} +- `value?: string`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, header?: ClassNameValue, trigger?: ClassNameValue, leadingIcon?: ClassNameValue, label?: ClassNameValue, trailingIcon?: ClassNameValue, content?: ClassNameValue, body?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- items + external: +- items + externalTypes: +- AccordionItem[] + hide: +- class + props: + class: 'px-4' + items: - label: 'Icons' + icon: 'i-lucide-smile' + content: 'You have nothing to do, @nuxt/icon will handle it automatically. +- `disabled`: +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#body` +- `#content` diff --git a/.claude/skills/nuxt-ui/components/alert.md b/.claude/skills/nuxt-ui/components/alert.md new file mode 100644 index 0000000..0d0339e --- /dev/null +++ b/.claude/skills/nuxt-ui/components/alert.md @@ -0,0 +1,24 @@ +# Alert + +A callout to draw user's attention. + +## Key Props + +- `title`: to set the title of the Alert. +- `description`: to set the description of the Alert. +- `icon`: to show an [Icon](/docs/components/icon). +- `avatar`: to show an [Avatar](/docs/components/avatar). +- `color`: to change the color of the Alert. +- `variant`: to change the variant of the Alert. +- `close`: to display a [Button](/docs/components/button) to dismiss the Alert. +- `actions`: to add some [Button](/docs/components/button) actions to the Alert. +- `orientation`: to change the orientation of the Alert. +- `class`: to override the base styles of the Alert. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/app.md b/.claude/skills/nuxt-ui/components/app.md new file mode 100644 index 0000000..4afa2ea --- /dev/null +++ b/.claude/skills/nuxt-ui/components/app.md @@ -0,0 +1,11 @@ +# App + +Wraps your app to provide global configurations and more. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/auth-form.md b/.claude/skills/nuxt-ui/components/auth-form.md new file mode 100644 index 0000000..b5bde4d --- /dev/null +++ b/.claude/skills/nuxt-ui/components/auth-form.md @@ -0,0 +1,27 @@ +# AuthForm + +A customizable Form to create login, register or password reset forms. + +## Key Props + +- `fields`: as an array of objects with the following properties: + +- `name: string`{lang="ts-type"} +- `type: 'checkbox' | 'select' | 'otp' | 'InputHTMLAttributes['type']'`{lang="ts-type"} + +Each field must include a `type` property, which determines the input component and any additional props applied: `checkbox` fields use [Checkbox](/docs/components/checkbox#props) props, `select` fields use [SelectMenu](/docs/components/select-menu#props) props, `otp` fields use [PinInput](/docs/components/pin-input#props) props, and all other types use [Input](/docs/components/input#props) props. + +- `title`: to set the title of the Form. +- `description`: to set the description of the Form. +- `icon`: to set the icon of the Form. +- `providers`: to add providers to the form. +- `separator`: to customize the [Separator](/docs/components/separator) between the providers and the fields. +- `submit`: to change the submit button of the Form. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/avatar-group.md b/.claude/skills/nuxt-ui/components/avatar-group.md new file mode 100644 index 0000000..8b96fe3 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/avatar-group.md @@ -0,0 +1,16 @@ +# AvatarGroup + +Stack multiple avatars in a group. + +## Key Props + +- `size`: to change the size of all the avatars. +- `max`: to limit the number of avatars displayed. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/avatar.md b/.claude/skills/nuxt-ui/components/avatar.md new file mode 100644 index 0000000..671e3bb --- /dev/null +++ b/.claude/skills/nuxt-ui/components/avatar.md @@ -0,0 +1,19 @@ +# Avatar + +An img element with fallback and Nuxt Image support. + +## Key Props + +- `src`: to set the image URL. +- `size`: to set the size of the Avatar. +- `icon`: to display a fallback [Icon](/docs/components/icon). +- `text`: to display a fallback text. +- `chip`: to display a chip around the Avatar. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/badge.md b/.claude/skills/nuxt-ui/components/badge.md new file mode 100644 index 0000000..6e01c1f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/badge.md @@ -0,0 +1,21 @@ +# Badge + +A short text to represent a status or a category. + +## Key Props + +- `label`: to set the label of the Badge. +- `color`: to change the color of the Badge. +- `variant`: +- `size`: to change the size of the Badge. +- `icon`: to show an [Icon](/docs/components/icon) inside the Badge. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the Badge. +- `class`: to override the base styles of the Badge. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/banner.md b/.claude/skills/nuxt-ui/components/banner.md new file mode 100644 index 0000000..9b49929 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/banner.md @@ -0,0 +1,19 @@ +# Banner + +Display a banner at the top of your website to inform users about important information. + +## Key Props + +- `title`: to display a title on the Banner. +- `icon`: to display an icon on the Banner. +- `color`: to change the color of the Banner. +- `close`: to display a [Button](/docs/components/button) to dismiss the Banner. +- `actions`: to add some [Button](/docs/components/button) actions to the Banner. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/blog-post.md b/.claude/skills/nuxt-ui/components/blog-post.md new file mode 100644 index 0000000..4c0000f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/blog-post.md @@ -0,0 +1,32 @@ +# BlogPost + +A customizable article to display in a blog page. + +## Key Props + +- `title`: to display the title of the BlogPost. +- `description`: to display the description of the BlogPost. +- `date`: to display the date of the BlogPost. +- `badge`: to display a [Badge](/docs/components/badge) in the BlogPost. +- `image`: to display an image in the BlogPost. +- `authors`: to display a list of [User](/docs/components/user) in the BlogPost as an array of objects with the following properties: + +- `name?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- `avatar?: Omit`{lang="ts-type"} +- `chip?: boolean | Omit`{lang="ts-type"} +- `size?: UserProps['size']`{lang="ts-type"} +- `orientation?: UserProps['orientation']`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `variant`: to change the style of the BlogPost. +- `orientation`: to change the BlogPost orientation. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/blog-posts.md b/.claude/skills/nuxt-ui/components/blog-posts.md new file mode 100644 index 0000000..5eae1de --- /dev/null +++ b/.claude/skills/nuxt-ui/components/blog-posts.md @@ -0,0 +1,16 @@ +# BlogPosts + +Display a list of blog posts in a responsive grid layout. + +## Key Props + +- `posts`: as an array of objects with the properties of the [BlogPost](/docs/components/blog-post#props) component. +- `orientation`: to change the orientation of the BlogPosts. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/breadcrumb.md b/.claude/skills/nuxt-ui/components/breadcrumb.md new file mode 100644 index 0000000..9afb246 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/breadcrumb.md @@ -0,0 +1,30 @@ +# Breadcrumb + +A hierarchy of links to navigate through a website. + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLabel?: ClassNameValue, separator?: ClassNameValue, separatorIcon?: ClassNameValue }`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#item` diff --git a/.claude/skills/nuxt-ui/components/button.md b/.claude/skills/nuxt-ui/components/button.md new file mode 100644 index 0000000..378882a --- /dev/null +++ b/.claude/skills/nuxt-ui/components/button.md @@ -0,0 +1,24 @@ +# Button + +A button element that can act as a link or trigger an action. + +## Key Props + +- `label`: to set the label of the Button. +- `color`: to change the color of the Button. +- `variant`: to change the variant of the Button. +- `size`: to change the size of the Button. +- `icon`: to show an [Icon](/docs/components/icon) inside the Button. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the Button. +- `loading`: to show a loading icon and disable the Button. +- `disabled`: to disable the Button. +- `class`: to override the base styles of the Button. +- `ui`: to override the slots styles of the Button. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/calendar.md b/.claude/skills/nuxt-ui/components/calendar.md new file mode 100644 index 0000000..64b4277 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/calendar.md @@ -0,0 +1,24 @@ +# Calendar + +A calendar component for selecting single dates, multiple dates or date ranges. + +> Based on [Reka UI Calendar](https://reka-ui.com/docs/components/calendar) + +## Key Props + +- `multiple`: to allow multiple selections. +- `range`: to select a range of dates. +- `color`: to change the color of the calendar. +- `variant`: to change the variant of the calendar. +- `size`: to change the size of the calendar. +- `disabled`: to disable the calendar. +- `numberOfMonths`: to change the number of months in the calendar. +- `weekNumbers`: to display week numbers (v4.4+). + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/card.md b/.claude/skills/nuxt-ui/components/card.md new file mode 100644 index 0000000..455b76c --- /dev/null +++ b/.claude/skills/nuxt-ui/components/card.md @@ -0,0 +1,15 @@ +# Card + +Display content in a card with a header, body and footer. + +## Key Props + +- `variant`: to change the variant of the Card. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/carousel.md b/.claude/skills/nuxt-ui/components/carousel.md new file mode 100644 index 0000000..6e7bd06 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/carousel.md @@ -0,0 +1,37 @@ +# Carousel + +A carousel with motion and swipe built using Embla. + +## Key Props + +- `items`: as an array and render each item using the default slot: + +## ::component-example + +name: 'carousel-items-example' +class: 'p-8' + +--- + +:: + +You can also pass an array of objects with the following properties: + +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue }`{lang="ts-type"} + +You can control how many items are visible by using the [`basis`](https://tailwindcss. + +- `orientation`: to change the orientation of the Progress. +- `arrows`: to display prev and next buttons. +- `dots`: to display a list of dots to scroll to a specific slide. +- `autoplay`: as a boolean or an object to configure the [Autoplay plugin](https://www. +- `fade`: as a boolean or an object to configure the [Fade plugin](https://www. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/changelog-version.md b/.claude/skills/nuxt-ui/components/changelog-version.md new file mode 100644 index 0000000..6de5e6c --- /dev/null +++ b/.claude/skills/nuxt-ui/components/changelog-version.md @@ -0,0 +1,31 @@ +# ChangelogVersion + +A customizable article to display in a changelog. + +## Key Props + +- `title`: to display the title of the ChangelogVersion. +- `description`: to display the description of the ChangelogVersion. +- `date`: to display the date of the ChangelogVersion. +- `badge`: to display a [Badge](/docs/components/badge) on the ChangelogVersion. +- `image`: to display an image in the BlogPost. +- `authors`: to display a list of [User](/docs/components/user) in the ChangelogVersion as an array of objects with the following properties: + +- `name?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- `avatar?: Omit`{lang="ts-type"} +- `chip?: boolean | Omit`{lang="ts-type"} +- `size?: UserProps['size']`{lang="ts-type"} +- `orientation?: UserProps['orientation']`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `indicator`: to hide the indicator dot on the left. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/changelog-versions.md b/.claude/skills/nuxt-ui/components/changelog-versions.md new file mode 100644 index 0000000..82e8cd4 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/changelog-versions.md @@ -0,0 +1,16 @@ +# ChangelogVersions + +Display a list of changelog versions in a timeline. + +## Key Props + +- `versions`: as an array of objects with the properties of the [ChangelogVersion](/docs/components/changelog-version#props) component. +- `indicator`: to hide the indicator bar on the left. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/chat-message.md b/.claude/skills/nuxt-ui/components/chat-message.md new file mode 100644 index 0000000..f828613 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/chat-message.md @@ -0,0 +1,20 @@ +# ChatMessage + +Display a chat message with icon, avatar, and actions. + +## Key Props + +- `parts`: to display the message content using the AI SDK v5 format. +- `side`: to display the message on the left or right. +- `variant`: to change style of the message. +- `icon`: to display an [Icon](/docs/components/icon) component next to the message. +- `avatar`: to display an [Avatar](/docs/components/avatar) component next to the message. +- `actions`: to display actions below the message that will be displayed when hovering over the message. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/chat-messages.md b/.claude/skills/nuxt-ui/components/chat-messages.md new file mode 100644 index 0000000..09ec47d --- /dev/null +++ b/.claude/skills/nuxt-ui/components/chat-messages.md @@ -0,0 +1,18 @@ +# ChatMessages + +Display a list of chat messages, designed to work seamlessly with Vercel AI SDK. + +## Key Props + +- `messages`: to display a list of chat messages. +- `status`: to display a visual indicator when the assistant is processing. +- `user`: to change the [ChatMessage](/docs/components/chat-message) props for `user` messages. +- `assistant`: to change the [ChatMessage](/docs/components/chat-message) props for `assistant` messages. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/chat-palette.md b/.claude/skills/nuxt-ui/components/chat-palette.md new file mode 100644 index 0000000..36383dd --- /dev/null +++ b/.claude/skills/nuxt-ui/components/chat-palette.md @@ -0,0 +1,11 @@ +# ChatPalette + +A chat palette to create a chatbot interface inside an overlay. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/chat-prompt-submit.md b/.claude/skills/nuxt-ui/components/chat-prompt-submit.md new file mode 100644 index 0000000..6a0b327 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/chat-prompt-submit.md @@ -0,0 +1,11 @@ +# ChatPromptSubmit + +A Button for submitting chat prompts with automatic status handling. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/chat-prompt.md b/.claude/skills/nuxt-ui/components/chat-prompt.md new file mode 100644 index 0000000..ab552a0 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/chat-prompt.md @@ -0,0 +1,15 @@ +# ChatPrompt + +An enhanced Textarea for submitting prompts in AI chat interfaces. + +## Key Props + +- `variant`: to change the style of the prompt. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/checkbox-group.md b/.claude/skills/nuxt-ui/components/checkbox-group.md new file mode 100644 index 0000000..8ec9efc --- /dev/null +++ b/.claude/skills/nuxt-ui/components/checkbox-group.md @@ -0,0 +1,67 @@ +# CheckboxGroup + +A set of checklist buttons to select multiple option from a list. + +> Based on [Reka UI CheckboxGroup](https://reka-ui.com/docs/components/checkbox#group-root) + +## Key Props + +- `items`: as an array of strings or numbers: + +## ::component-code + +prettier: true +ignore: + +- modelValue +- items + external: +- items +- modelValue + props: + modelValue: - 'System' + items: - 'System' - 'Light' - 'Dark' + +--- + +:: + +You can also pass an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- [`value?: string`{lang="ts-type"}](#value-key) +- `disabled?: boolean`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, icon?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- modelValue +- items + external: +- items +- modelValue + externalTypes: +- CheckboxGroupItem[] + props: + modelValue: - 'system' + items: - label: 'System' + description: 'This is the first option. +- `legend`: to set the legend of the CheckboxGroup. +- `color`: to change the color of the CheckboxGroup. +- `variant`: to change the variant of the CheckboxGroup. +- `size`: to change the size of the CheckboxGroup. +- `orientation`: to change the orientation of the CheckboxGroup. +- `indicator`: to change the position or hide the indicator. +- `disabled`: to disable the CheckboxGroup. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/checkbox.md b/.claude/skills/nuxt-ui/components/checkbox.md new file mode 100644 index 0000000..7002168 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/checkbox.md @@ -0,0 +1,24 @@ +# Checkbox + +An input element to toggle between checked and unchecked states. + +> Based on [Reka UI Checkbox](https://reka-ui.com/docs/components/checkbox) + +## Key Props + +- `label`: to set the label of the Checkbox. +- `description`: to set the description of the Checkbox. +- `icon`: to set the icon of the Checkbox when it is checked. +- `color`: to change the color of the Checkbox. +- `variant`: to change the variant of the Checkbox. +- `size`: to change the size of the Checkbox. +- `indicator`: to change the position or hide the indicator. +- `disabled`: to disable the Checkbox. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/chip.md b/.claude/skills/nuxt-ui/components/chip.md new file mode 100644 index 0000000..7514307 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/chip.md @@ -0,0 +1,20 @@ +# Chip + +An indicator of a numeric value or a state. + +## Key Props + +- `color`: to change the color of the Chip. +- `size`: to change the size of the Chip. +- `text`: to set the text of the Chip. +- `position`: to change the position of the Chip. +- `inset`: to display the Chip inside the component. +- `standalone`: alongside the `inset` prop to display the Chip inline. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/collapsible.md b/.claude/skills/nuxt-ui/components/collapsible.md new file mode 100644 index 0000000..ecc2e73 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/collapsible.md @@ -0,0 +1,21 @@ +# Collapsible + +A collapsible element to toggle visibility of its content. + +> Based on [Reka UI Collapsible](https://reka-ui.com/docs/components/collapsible) + +## Key Props + +- `disabled`: to disable the Collapsible. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` diff --git a/.claude/skills/nuxt-ui/components/color-mode-avatar.md b/.claude/skills/nuxt-ui/components/color-mode-avatar.md new file mode 100644 index 0000000..d377beb --- /dev/null +++ b/.claude/skills/nuxt-ui/components/color-mode-avatar.md @@ -0,0 +1,11 @@ +# ColorModeAvatar + +An Avatar with a different source for light and dark mode. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/color-mode-button.md b/.claude/skills/nuxt-ui/components/color-mode-button.md new file mode 100644 index 0000000..db8a153 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/color-mode-button.md @@ -0,0 +1,11 @@ +# ColorModeButton + +A Button to switch between light and dark mode. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/color-mode-image.md b/.claude/skills/nuxt-ui/components/color-mode-image.md new file mode 100644 index 0000000..9d9b3f8 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/color-mode-image.md @@ -0,0 +1,11 @@ +# ColorModeImage + +An image element with a different source for light and dark mode. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/color-mode-select.md b/.claude/skills/nuxt-ui/components/color-mode-select.md new file mode 100644 index 0000000..0b6d980 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/color-mode-select.md @@ -0,0 +1,11 @@ +# ColorModeSelect + +A Select to switch between system, dark & light mode. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/color-mode-switch.md b/.claude/skills/nuxt-ui/components/color-mode-switch.md new file mode 100644 index 0000000..abcf748 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/color-mode-switch.md @@ -0,0 +1,11 @@ +# ColorModeSwitch + +A switch to toggle between light and dark mode. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/color-picker.md b/.claude/skills/nuxt-ui/components/color-picker.md new file mode 100644 index 0000000..80fb234 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/color-picker.md @@ -0,0 +1,18 @@ +# ColorPicker + +A component to select a color. + +## Key Props + +- `format`: to set `rgb` value of the ColorPicker. +- `throttle`: to set the throttle value of the ColorPicker. +- `size`: to set the size of the ColorPicker. +- `disabled`: to disable the ColorPicker. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/command-palette.md b/.claude/skills/nuxt-ui/components/command-palette.md new file mode 100644 index 0000000..7bc9d83 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/command-palette.md @@ -0,0 +1,43 @@ +# CommandPalette + +A command palette with full-text search powered by Fuse.js for efficient fuzzy matching. + +> Based on [Reka UI CommandPalette](https://reka-ui.com/docs/components/listbox) + +## Key Props + +- `groups`: as an array of objects with the following properties: + +- `id: string`{lang="ts-type"} +- `label?: string`{lang="ts-type"} +- `slot?: string`{lang="ts-type"} +- `items?: CommandPaletteItem[]`{lang="ts-type"} +- [`ignoreFilter?: boolean`{lang="ts-type"}](#with-ignore-filter) +- [`postFilter?: (searchTerm: string, items: T[]) => T[]`{lang="ts-type"}](#with-post-filtered-items) +- `highlightedIcon?: string`{lang="ts-type"} + +::caution +You must provide an `id` for each group otherwise the group will be ignored. + +- `multiple`: to allow multiple selections. +- `placeholder`: to change the placeholder text. +- `icon`: to customize the input [Icon](/docs/components/icon). +- `loading`: to show a loading icon on the CommandPalette. +- `close`: to display a [Button](/docs/components/button) to dismiss the CommandPalette. +- `back`: to customize or hide the back button (with `false` value) displayed when navigating into a submenu. +- `disabled`: to disable the CommandPalette. +- `virtualize`: to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`. +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#footer` +- `#item` diff --git a/.claude/skills/nuxt-ui/components/container.md b/.claude/skills/nuxt-ui/components/container.md new file mode 100644 index 0000000..45a2400 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/container.md @@ -0,0 +1,11 @@ +# Container + +A container lets you center and constrain the width of your content. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/content-navigation.md b/.claude/skills/nuxt-ui/components/content-navigation.md new file mode 100644 index 0000000..adf439b --- /dev/null +++ b/.claude/skills/nuxt-ui/components/content-navigation.md @@ -0,0 +1,18 @@ +# ContentNavigation + +An accordion-style navigation component for organizing page links. + +## Key Props + +- `navigation`: with the `navigation`{lang="ts-type"} value you get when fetching the navigation of your app. +- `color`: to change the color of the navigation links. +- `variant`: to change the variant of the navigation links. +- `highlight`: to display a highlighted border for the active link. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/content-search-button.md b/.claude/skills/nuxt-ui/components/content-search-button.md new file mode 100644 index 0000000..315e5c0 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/content-search-button.md @@ -0,0 +1,16 @@ +# ContentSearchButton + +A pre-styled Button to open the ContentSearch modal. + +## Key Props + +- `collapsed`: to show the button's label and [kbds](#kbds). +- `kbds`: to display keyboard keys in the button. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/content-search.md b/.claude/skills/nuxt-ui/components/content-search.md new file mode 100644 index 0000000..caaa934 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/content-search.md @@ -0,0 +1,15 @@ +# ContentSearch + +A ready to use CommandPalette to add to your documentation. + +## Key Props + +- `shortcut`: to change the shortcut used in [defineShortcuts](/docs/composables/define-shortcuts) to open the ContentSearch component. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/content-surround.md b/.claude/skills/nuxt-ui/components/content-surround.md new file mode 100644 index 0000000..8ff1717 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/content-surround.md @@ -0,0 +1,15 @@ +# ContentSurround + +A pair of prev and next links to navigate between pages. + +## Key Props + +- `surround`: with the `surround`{lang="ts-type"} value you get when fetching a page surround. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/content-toc.md b/.claude/skills/nuxt-ui/components/content-toc.md new file mode 100644 index 0000000..93daef9 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/content-toc.md @@ -0,0 +1,18 @@ +# ContentToc + +A sticky Table of Contents with automatic active anchor link highlighting. + +## Key Props + +- `links`: with the `page?. +- `title`: to change the title of the Table of Contents. +- `color`: to change the color of the links. +- `highlight`: to display a highlighted border for the active item. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/context-menu.md b/.claude/skills/nuxt-ui/components/context-menu.md new file mode 100644 index 0000000..d2efa7f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/context-menu.md @@ -0,0 +1,43 @@ +# ContextMenu + +A menu to display actions when right-clicking on an element. + +> Based on [Reka UI ContextMenu](https://reka-ui.com/docs/components/context-menu) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- `kbds?: string[] | KbdProps[]`{lang="ts-type"} +- [`type?: "link" | "label" | "separator" | "checkbox"`{lang="ts-type"}](#with-checkbox-items) +- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"}](#with-color-items) +- [`checked?: boolean`{lang="ts-type"}](#with-checkbox-items) +- `disabled?: boolean`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `onSelect?: (e: Event) => void`{lang="ts-type"} +- [`onUpdateChecked?: (checked: boolean) => void`{lang="ts-type"}](#with-checkbox-items) +- `children?: ContextMenuItem[] | ContextMenuItem[][]`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `size`: to change the size of the ContextMenu. +- `modal`: to control whether the ContextMenu blocks interaction with outside content. +- `disabled`: to disable the ContextMenu. +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#item` diff --git a/.claude/skills/nuxt-ui/components/dashboard-group.md b/.claude/skills/nuxt-ui/components/dashboard-group.md new file mode 100644 index 0000000..5886014 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-group.md @@ -0,0 +1,11 @@ +# DashboardGroup + +A fixed layout component that provides context for dashboard components with sidebar state management and persistence. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-navbar.md b/.claude/skills/nuxt-ui/components/dashboard-navbar.md new file mode 100644 index 0000000..bb6525f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-navbar.md @@ -0,0 +1,17 @@ +# DashboardNavbar + +A responsive navbar to display in a dashboard. + +## Key Props + +- `title`: to set the title of the navbar. +- `icon`: to set the icon of the navbar. +- `toggle`: to customize the toggle button displayed on mobile that opens the [DashboardSidebar](/docs/components/dashboard-sidebar) component. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-panel.md b/.claude/skills/nuxt-ui/components/dashboard-panel.md new file mode 100644 index 0000000..f6c633d --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-panel.md @@ -0,0 +1,15 @@ +# DashboardPanel + +A resizable panel to display in a dashboard. + +## Key Props + +- `resizable`: to make the panel resizable. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-resize-handle.md b/.claude/skills/nuxt-ui/components/dashboard-resize-handle.md new file mode 100644 index 0000000..db50505 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-resize-handle.md @@ -0,0 +1,11 @@ +# DashboardResizeHandle + +A handle to resize a sidebar or panel. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-search-button.md b/.claude/skills/nuxt-ui/components/dashboard-search-button.md new file mode 100644 index 0000000..042d850 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-search-button.md @@ -0,0 +1,16 @@ +# DashboardSearchButton + +A pre-styled Button to open the DashboardSearch modal. + +## Key Props + +- `collapsed`: to hide the button's label and [kbds](#kbds). +- `kbds`: to display keyboard keys in the button. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-search.md b/.claude/skills/nuxt-ui/components/dashboard-search.md new file mode 100644 index 0000000..e3fa60c --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-search.md @@ -0,0 +1,15 @@ +# DashboardSearch + +A ready to use CommandPalette to add to your dashboard. + +## Key Props + +- `shortcut`: to change the shortcut used in [defineShortcuts](/docs/composables/define-shortcuts) to open the ContentSearch component. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-sidebar-collapse.md b/.claude/skills/nuxt-ui/components/dashboard-sidebar-collapse.md new file mode 100644 index 0000000..7b1cbc6 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-sidebar-collapse.md @@ -0,0 +1,11 @@ +# DashboardSidebarCollapse + +A Button to collapse the sidebar on desktop. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-sidebar-toggle.md b/.claude/skills/nuxt-ui/components/dashboard-sidebar-toggle.md new file mode 100644 index 0000000..f968425 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-sidebar-toggle.md @@ -0,0 +1,11 @@ +# DashboardSidebarToggle + +A Button to toggle the sidebar on mobile. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-sidebar.md b/.claude/skills/nuxt-ui/components/dashboard-sidebar.md new file mode 100644 index 0000000..69f3c13 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-sidebar.md @@ -0,0 +1,19 @@ +# DashboardSidebar + +A resizable and collapsible sidebar to display in a dashboard. + +## Key Props + +- `resizable`: to make the sidebar resizable. +- `collapsible`: to make the sidebar collapsible when dragging near the edge of the screen. +- `side`: to change the side of the sidebar. +- `mode`: to change the mode of the sidebar menu. +- `toggle`: to customize the [DashboardSidebarToggle](/docs/components/dashboard-sidebar-toggle) component displayed on mobile. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/dashboard-toolbar.md b/.claude/skills/nuxt-ui/components/dashboard-toolbar.md new file mode 100644 index 0000000..5c55a10 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dashboard-toolbar.md @@ -0,0 +1,11 @@ +# DashboardToolbar + +A toolbar to display under the navbar in a dashboard. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/drawer.md b/.claude/skills/nuxt-ui/components/drawer.md new file mode 100644 index 0000000..ced654c --- /dev/null +++ b/.claude/skills/nuxt-ui/components/drawer.md @@ -0,0 +1,29 @@ +# Drawer + +A drawer that smoothly slides in & out of the screen. + +## Key Props + +- `title`: to set the title of the Drawer's header. +- `description`: to set the description of the Drawer's header. +- `direction`: to control the direction of the Drawer. +- `inset`: to inset the Drawer from the edges. +- `handle`: to control whether the Drawer has a handle or not. +- `overlay`: to control whether the Drawer has an overlay or not. +- `modal`: to control whether the Drawer blocks interaction with outside content. +- `dismissible`: to control whether the Drawer is dismissible when clicking outside of it or pressing escape. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` +- `#header` +- `#body` +- `#footer` diff --git a/.claude/skills/nuxt-ui/components/dropdown-menu.md b/.claude/skills/nuxt-ui/components/dropdown-menu.md new file mode 100644 index 0000000..e74d566 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/dropdown-menu.md @@ -0,0 +1,45 @@ +# DropdownMenu + +A menu to display actions when clicking on an element. + +> Based on [Reka UI DropdownMenu](https://reka-ui.com/docs/components/dropdown-menu) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- `kbds?: string[] | KbdProps[]`{lang="ts-type"} +- [`type?: "link" | "label" | "separator" | "checkbox"`{lang="ts-type"}](#with-checkbox-items) +- [`color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"}](#with-color-items) +- [`checked?: boolean`{lang="ts-type"}](#with-checkbox-items) +- `disabled?: boolean`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `onSelect?: (e: Event) => void`{lang="ts-type"} +- [`onUpdateChecked?: (checked: boolean) => void`{lang="ts-type"}](#with-checkbox-items) +- `children?: DropdownMenuItem[] | DropdownMenuItem[][]`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelExternalIcon?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue }`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `content`: to control how the DropdownMenu content is rendered, like its `align` or `side` for example. +- `arrow`: to display an arrow on the DropdownMenu. +- `size`: to control the size of the DropdownMenu. +- `modal`: to control whether the DropdownMenu blocks interaction with outside content. +- `disabled`: to disable the DropdownMenu. +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#item` diff --git a/.claude/skills/nuxt-ui/components/editor-drag-handle.md b/.claude/skills/nuxt-ui/components/editor-drag-handle.md new file mode 100644 index 0000000..39ba4d6 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/editor-drag-handle.md @@ -0,0 +1,16 @@ +# EditorDragHandle + +A draggable handle for reordering and selecting blocks in the editor. + +## Key Props + +- `icon`: to customize the drag handle icon. +- `options`: to customize the positioning behavior using [Floating UI options](https://floating-ui. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/editor-emoji-menu.md b/.claude/skills/nuxt-ui/components/editor-emoji-menu.md new file mode 100644 index 0000000..6316edd --- /dev/null +++ b/.claude/skills/nuxt-ui/components/editor-emoji-menu.md @@ -0,0 +1,39 @@ +# EditorEmojiMenu + +An emoji picker menu that displays emoji suggestions when typing the : character in the editor. + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `name: string`{lang="ts-type"} +- `emoji: string`{lang="ts-type"} +- `shortcodes?: string[]`{lang="ts-type"} +- `tags?: string[]`{lang="ts-type"} +- `group?: string`{lang="ts-type"} +- `fallbackImage?: string`{lang="ts-type"} + +## ::component-example + +elevated: true +collapse: true +name: 'editor-emoji-menu-items-example' +class: 'p-8' + +--- + +:: + +::note +You can also pass an array of arrays to the `items` prop to create separated groups of items. + +- `char`: to change the trigger character. +- `options`: to customize the positioning behavior using [Floating UI options](https://floating-ui. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/editor-mention-menu.md b/.claude/skills/nuxt-ui/components/editor-mention-menu.md new file mode 100644 index 0000000..0050227 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/editor-mention-menu.md @@ -0,0 +1,38 @@ +# EditorMentionMenu + +A mention menu that displays user suggestions when typing the @ character in the editor. + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} + +## ::component-example + +elevated: true +collapse: true +name: 'editor-mention-menu-items-example' +class: 'p-8' + +--- + +:: + +::note +You can also pass an array of arrays to the `items` prop to create separated groups of items. + +- `char`: to change the trigger character. +- `options`: to customize the positioning behavior using [Floating UI options](https://floating-ui. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/editor-suggestion-menu.md b/.claude/skills/nuxt-ui/components/editor-suggestion-menu.md new file mode 100644 index 0000000..b88f69c --- /dev/null +++ b/.claude/skills/nuxt-ui/components/editor-suggestion-menu.md @@ -0,0 +1,39 @@ +# EditorSuggestionMenu + +A command menu that displays formatting and action suggestions when typing the / character in the editor. + +## Key Props + +- `items`: as an array of objects with the following properties: + +- [`kind?: "textAlign" | "heading" | "link" | "image" | "blockquote" | "bulletList" | "orderedList" | "codeBlock" | "horizontalRule" | "paragraph" | "clearFormatting" | "duplicate" | "delete" | "moveUp" | "moveDown" | "suggestion" | "mention" | "emoji"`{lang="ts-type"}](/docs/components/editor#handlers) +- `label?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `type?: "label" | "separator"`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} + +## ::component-example + +elevated: true +collapse: true +name: 'editor-suggestion-menu-items-example' +class: 'p-8' + +--- + +:: + +::note +You can also pass an array of arrays to the `items` prop to create separated groups of items. + +- `char`: to change the trigger character. +- `options`: to customize the positioning behavior using [Floating UI options](https://floating-ui. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/editor-toolbar.md b/.claude/skills/nuxt-ui/components/editor-toolbar.md new file mode 100644 index 0000000..bb4609f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/editor-toolbar.md @@ -0,0 +1,36 @@ +# EditorToolbar + +A customizable toolbar for editor actions that can be displayed as fixed, bubble, or floating menu. + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `color?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"} +- `activeColor?: "error" | "primary" | "secondary" | "success" | "info" | "warning" | "neutral"`{lang="ts-type"} +- `variant?: "solid" | "outline" | "soft" | "ghost" | "link" | "subtle"`{lang="ts-type"} +- `activeVariant?: "solid" | "outline" | "soft" | "ghost" | "link" | "subtle"`{lang="ts-type"} +- `size?: "xs" | "sm" | "md" | "lg" | "xl"`{lang="ts-type"} +- [`kind?: "mark" | "textAlign" | "heading" | "link" | "image" | "blockquote" | "bulletList" | "orderedList" | "codeBlock" | "horizontalRule" | "paragraph" | "undo" | "redo" | "clearFormatting" | "duplicate" | "delete" | "moveUp" | "moveDown" | "suggestion" | "mention" | "emoji"`{lang="ts-type"}](/docs/components/editor#handlers) +- `disabled?: boolean`{lang="ts-type"} +- `loading?: boolean`{lang="ts-type"} +- `active?: boolean`{lang="ts-type"} +- `tooltip?: TooltipProps`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-link-popover) +- `onClick?: (e: MouseEvent) => void`{lang="ts-type"} +- `items?: EditorToolbarItem[] | EditorToolbarItem[][]`{lang="ts-type"} +- `class?: any`{lang="ts-type"} + +You can pass any property from the [Button](/docs/components/button#props) component such as `color`, `variant`, `size`, etc. + +- `layout`: to change how the toolbar is displayed. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/editor.md b/.claude/skills/nuxt-ui/components/editor.md new file mode 100644 index 0000000..536fb97 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/editor.md @@ -0,0 +1,16 @@ +# Editor + +A rich text editor component based on TipTap with support for markdown, HTML, and JSON content types. + +## Key Props + +- `placeholder`: to set a placeholder text that shows in empty paragraphs. +- `handlers`: to extend or override the default handlers. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/empty.md b/.claude/skills/nuxt-ui/components/empty.md new file mode 100644 index 0000000..2d63d94 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/empty.md @@ -0,0 +1,21 @@ +# Empty + +A component to display an empty state. + +## Key Props + +- `title`: to set the title of the empty state. +- `description`: to set the description of the empty state. +- `icon`: to set the icon of the empty state. +- `avatar`: to set the avatar of the empty state. +- `actions`: to add some [Button](/docs/components/button) actions to the empty state. +- `variant`: to change the variant of the empty state. +- `size`: to change the size of the empty state. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/error.md b/.claude/skills/nuxt-ui/components/error.md new file mode 100644 index 0000000..c4cbad3 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/error.md @@ -0,0 +1,17 @@ +# Error + +A pre-built error component with NuxtError support. + +## Key Props + +- `error`: to display an error message. +- `clear`: to customize or hide the clear button (with `false` value). +- `redirect`: to redirect the user to a different page when the clear button is clicked. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/field-group.md b/.claude/skills/nuxt-ui/components/field-group.md new file mode 100644 index 0000000..63347e2 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/field-group.md @@ -0,0 +1,16 @@ +# FieldGroup + +Group multiple button-like elements together. + +## Key Props + +- `size`: to change the size of all the buttons. +- `orientation`: to change the orientation of the buttons. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/file-upload.md b/.claude/skills/nuxt-ui/components/file-upload.md new file mode 100644 index 0000000..f1b40a7 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/file-upload.md @@ -0,0 +1,28 @@ +# FileUpload + +An input element to upload files. + +## Key Props + +- `multiple`: to allow multiple files to be selected. +- `dropzone`: to enable/disable the droppable area. +- `interactive`: to enable/disable the clickable area. +- `accept`: to specify the allowed file types for the input. +- `label`: to set the label of the FileUpload. +- `description`: to set the description of the FileUpload. +- `icon`: to set the icon of the FileUpload. +- `color`: to change the color of the FileUpload. +- `variant`: to change the variant of the FileUpload. +- `size`: to change the size of the FileUpload. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#actions` diff --git a/.claude/skills/nuxt-ui/components/footer-columns.md b/.claude/skills/nuxt-ui/components/footer-columns.md new file mode 100644 index 0000000..40d7998 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/footer-columns.md @@ -0,0 +1,20 @@ +# FooterColumns + +A list of links as columns to display in your Footer. + +## Key Props + +- `columns`: as an array of objects with the following properties: + +- `label: string`{lang="ts-type"} +- `children?: FooterColumnLink[]`{lang="ts-type"} + +Each column contains a `children` array of objects that define the links. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/footer.md b/.claude/skills/nuxt-ui/components/footer.md new file mode 100644 index 0000000..da0c368 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/footer.md @@ -0,0 +1,11 @@ +# Footer + +A responsive footer component. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/form-field.md b/.claude/skills/nuxt-ui/components/form-field.md new file mode 100644 index 0000000..989aedc --- /dev/null +++ b/.claude/skills/nuxt-ui/components/form-field.md @@ -0,0 +1,20 @@ +# FormField + +A wrapper for form elements that provides validation and error handling. + +## Key Props + +- `label`: to set the label for the form control. +- `description`: to provide additional information below the label. +- `hint`: to display a hint message next to the label. +- `help`: to display a help message below the form control. +- `error`: to display an error message below the form control. +- `size`: to change the size of the FormField, the `size` is proxied to the form control. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/form.md b/.claude/skills/nuxt-ui/components/form.md new file mode 100644 index 0000000..e34a5b6 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/form.md @@ -0,0 +1,16 @@ +# Form + +A form component with built-in validation and submission handling. + +## Key Props + +- `validate`: to apply your own validation logic. +- `nested`: to nest multiple Form components and link their validation functions. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/header.md b/.claude/skills/nuxt-ui/components/header.md new file mode 100644 index 0000000..db6b4a5 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/header.md @@ -0,0 +1,18 @@ +# Header + +A responsive header component. + +## Key Props + +- `title`: to change the title of the header. +- `to`: to change the link of the title. +- `mode`: to change the mode of the header menu. +- `toggle`: to customize the toggle button displayed on mobile. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/icon.md b/.claude/skills/nuxt-ui/components/icon.md new file mode 100644 index 0000000..0bc17a6 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/icon.md @@ -0,0 +1,30 @@ +# Icon + +A component to display any icon from Iconify or another component. + +## Key Props + +- `name`: to display an icon: + +## ::component-code + +props: +name: 'i-lucide-lightbulb' +class: 'size-5' + +--- + +:: + +::framework-only +#nuxt +:::caution{to="/docs/getting-started/integrations/icons/nuxt#collections"} +It's highly recommended to install the icons collections you need, read more about this. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/input-date.md b/.claude/skills/nuxt-ui/components/input-date.md new file mode 100644 index 0000000..e4ae228 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/input-date.md @@ -0,0 +1,21 @@ +# InputDate + +An input component for date selection. + +> Based on [Reka UI InputDate](https://reka-ui.com/docs/components/date-field) + +## Key Props + +- `range`: to select a range of dates. +- `color`: to change the color of the InputDate. +- `variant`: to change the variant of the InputDate. +- `size`: to change the size of the InputDate. +- `disabled`: to disable the InputDate. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/input-menu.md b/.claude/skills/nuxt-ui/components/input-menu.md new file mode 100644 index 0000000..f8c491e --- /dev/null +++ b/.claude/skills/nuxt-ui/components/input-menu.md @@ -0,0 +1,66 @@ +# InputMenu + +An autocomplete input with real-time suggestions. + +> Based on [Reka UI InputMenu](https://reka-ui.com/docs/components/combobox) + +## Key Props + +- `items`: as an array of strings, numbers or booleans: + +## ::component-code + +prettier: true +ignore: + +- modelValue +- items + external: +- items +- modelValue + props: + modelValue: 'Backlog' + items: - Backlog - Todo - In Progress - Done + +--- + +:: + +You can also pass an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- [`type?: "label" | "separator" | "item"`{lang="ts-type"}](#with-items-type) +- [`icon?: string`{lang="ts-type"}](#with-icons-in-items) +- [`avatar?: AvatarProps`{lang="ts-type"}](#with-avatar-in-items) +- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items) +- `disabled?: boolean`{lang="ts-type"} +- `onSelect?: (e: Event) => void`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { tagsItem?: ClassNameValue, tagsItemText?: ClassNameValue, tagsItemDelete?: ClassNameValue, tagsItemDeleteIcon?: ClassNameValue, label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- modelValue. +- `multiple`: to allow multiple selections, the selected items will be displayed as tags. +- `placeholder`: to set a placeholder text. +- `content`: to control how the InputMenu content is rendered, like its `align` or `side` for example. +- `arrow`: to display an arrow on the InputMenu. +- `color`: to change the ring color when the InputMenu is focused. +- `variant`: to change the variant of the InputMenu. +- `size`: to change the size of the InputMenu. +- `icon`: to show an [Icon](/docs/components/icon) inside the InputMenu. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the InputMenu. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#leading` diff --git a/.claude/skills/nuxt-ui/components/input-number.md b/.claude/skills/nuxt-ui/components/input-number.md new file mode 100644 index 0000000..ff16598 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/input-number.md @@ -0,0 +1,21 @@ +# InputNumber + +An input for numerical values with a customizable range. + +## Key Props + +- `step`: to set the step value of the InputNumber. +- `orientation`: to change the orientation of the InputNumber. +- `placeholder`: to set a placeholder text. +- `color`: to change the ring color when the InputNumber is focused. +- `variant`: to change the variant of the InputNumber. +- `size`: to change the size of the InputNumber. +- `disabled`: to disable the InputNumber. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/input-tags.md b/.claude/skills/nuxt-ui/components/input-tags.md new file mode 100644 index 0000000..6e205db --- /dev/null +++ b/.claude/skills/nuxt-ui/components/input-tags.md @@ -0,0 +1,24 @@ +# InputTags + +An input element that displays interactive tags. + +> Based on [Reka UI InputTags](https://reka-ui.com/docs/components/tags-input) + +## Key Props + +- `placeholder`: to set a placeholder text. +- `color`: to change the ring color when the InputTags is focused. +- `variant`: to change the appearance of the InputTags. +- `size`: to adjust the size of the InputTags. +- `icon`: to show an [Icon](/docs/components/icon) inside the InputTags. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the InputTags. +- `loading`: to show a loading icon on the InputTags. +- `disabled`: to disable the InputTags. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/input-time.md b/.claude/skills/nuxt-ui/components/input-time.md new file mode 100644 index 0000000..e580550 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/input-time.md @@ -0,0 +1,22 @@ +# InputTime + +An input for selecting a time. + +> Based on [Reka UI InputTime](https://reka-ui.com/docs/components/time-field) + +## Key Props + +- `color`: to change the color of the InputTime. +- `variant`: to change the variant of the InputTime. +- `size`: to change the size of the InputTime. +- `icon`: to show an [Icon](/docs/components/icon) inside the InputTime. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the InputTime. +- `disabled`: to disable the InputTime. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/input.md b/.claude/skills/nuxt-ui/components/input.md new file mode 100644 index 0000000..8197204 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/input.md @@ -0,0 +1,28 @@ +# Input + +An input element to enter text. + +## Key Props + +- `type`: to change the input type. +- `placeholder`: to set a placeholder text. +- `color`: to change the ring color when the Input is focused. +- `variant`: to change the variant of the Input. +- `size`: to change the size of the Input. +- `icon`: to show an [Icon](/docs/components/icon) inside the Input. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the Input. +- `loading`: to show a loading icon on the Input. +- `disabled`: to disable the Input. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#trailing` +- `#default` diff --git a/.claude/skills/nuxt-ui/components/kbd.md b/.claude/skills/nuxt-ui/components/kbd.md new file mode 100644 index 0000000..847f9cf --- /dev/null +++ b/.claude/skills/nuxt-ui/components/kbd.md @@ -0,0 +1,19 @@ +# Kbd + +A kbd element to display a keyboard key. + +## Key Props + +- `value`: to set the value of the Kbd. +- `color`: to change the color of the Kbd. +- `variant`: to change the variant of the Kbd. +- `size`: to change the size of the Kbd. +- `class`: to override the base styles of the Badge. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/link.md b/.claude/skills/nuxt-ui/components/link.md new file mode 100644 index 0000000..5567839 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/link.md @@ -0,0 +1,11 @@ +# Link + +A wrapper around with extra props. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/locale-select.md b/.claude/skills/nuxt-ui/components/locale-select.md new file mode 100644 index 0000000..16ee131 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/locale-select.md @@ -0,0 +1,15 @@ +# LocaleSelect + +A Select to switch between locales. + +## Key Props + +- `locales`: with an array of locales from `@nuxt/ui/locale`. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/main.md b/.claude/skills/nuxt-ui/components/main.md new file mode 100644 index 0000000..0d80581 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/main.md @@ -0,0 +1,11 @@ +# Main + +A main element that fills the available viewport height. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/marquee.md b/.claude/skills/nuxt-ui/components/marquee.md new file mode 100644 index 0000000..b6704d6 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/marquee.md @@ -0,0 +1,18 @@ +# Marquee + +A component to create infinite scrolling content. + +## Key Props + +- `reverse`: to reverse the direction of the animation. +- `orientation`: to change the scrolling direction. +- `repeat`: to specify how many times the content should be repeated in the animation. +- `overlay`: to remove the gradient overlays on the edges of the marquee. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/modal.md b/.claude/skills/nuxt-ui/components/modal.md new file mode 100644 index 0000000..94caafb --- /dev/null +++ b/.claude/skills/nuxt-ui/components/modal.md @@ -0,0 +1,32 @@ +# Modal + +A dialog window that can be used to display a message or request user input. + +> Based on [Reka UI Modal](https://reka-ui.com/docs/components/dialog) + +## Key Props + +- `title`: to set the title of the Modal's header. +- `description`: to set the description of the Modal's header. +- `close`: to customize or hide the close button (with `false` value) displayed in the Modal's header. +- `transition`: to control whether the Modal is animated or not. +- `overlay`: to control whether the Modal has an overlay or not. +- `modal`: to control whether the Modal blocks interaction with outside content. +- `dismissible`: to control whether the Modal is dismissible when clicking outside of it or pressing escape. +- `scrollable`: to make the Modal's content scrollable within the overlay. +- `fullscreen`: to make the Modal fullscreen. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` +- `#header` +- `#body` +- `#footer` diff --git a/.claude/skills/nuxt-ui/components/navigation-menu.md b/.claude/skills/nuxt-ui/components/navigation-menu.md new file mode 100644 index 0000000..b481790 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/navigation-menu.md @@ -0,0 +1,48 @@ +# NavigationMenu + +A list of links that can be displayed horizontally or vertically. + +> Based on [Reka UI NavigationMenu](https://reka-ui.com/docs/components/navigation-menu) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- `badge?: string | number | BadgeProps`{lang="ts-type"} +- `tooltip?: TooltipProps`{lang="ts-type"} +- `trailingIcon?: string`{lang="ts-type"} +- `type?: 'label' | 'trigger' | 'link'`{lang="ts-type"} +- `defaultOpen?: boolean`{lang="ts-type"} +- `open?: boolean`{lang="ts-type"} +- `value?: string`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `onSelect?: (e: Event) => void`{lang="ts-type"} +- `children?: NavigationMenuChildItem[]`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { linkLeadingAvatarSize?: ClassNameValue, linkLeadingAvatar?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingBadgeSize?: ClassNameValue, linkTrailingBadge?: ClassNameValue, linkTrailingIcon?: ClassNameValue, label?: ClassNameValue, link?: ClassNameValue, content?: ClassNameValue, childList?: ClassNameValue, childLabel?: ClassNameValue, childItem?: ClassNameValue, childLink?: ClassNameValue, childLinkIcon?: ClassNameValue, childLinkWrapper?: ClassNameValue, childLinkLabel?: ClassNameValue, childLinkLabelExternalIcon?: ClassNameValue, childLinkDescription?: ClassNameValue }`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `orientation`: to change the orientation of the NavigationMenu. +- `highlight`: to display a highlighted border for the active item. +- `color`: to change the color of the NavigationMenu. +- `variant`: to change the variant of the NavigationMenu. +- `arrow`: to display an arrow on the NavigationMenu content when items have children. +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` +- `#item` diff --git a/.claude/skills/nuxt-ui/components/page-anchors.md b/.claude/skills/nuxt-ui/components/page-anchors.md new file mode 100644 index 0000000..aae4299 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-anchors.md @@ -0,0 +1,22 @@ +# PageAnchors + +A list of anchors to be displayed in the page. + +## Key Props + +- `links`: as an array of objects with the following properties: + +- `label: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkLeading?: ClassNameValue, linkLeadingIcon?: ClassNameValue }`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-aside.md b/.claude/skills/nuxt-ui/components/page-aside.md new file mode 100644 index 0000000..8117160 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-aside.md @@ -0,0 +1,11 @@ +# PageAside + +A sticky aside to display your page navigation. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-body.md b/.claude/skills/nuxt-ui/components/page-body.md new file mode 100644 index 0000000..71c8793 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-body.md @@ -0,0 +1,11 @@ +# PageBody + +The main content of your page. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-card.md b/.claude/skills/nuxt-ui/components/page-card.md new file mode 100644 index 0000000..06330bc --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-card.md @@ -0,0 +1,20 @@ +# PageCard + +A pre-styled card component that displays a title, description and optional link. + +## Key Props + +- `title`: to set the title of the card. +- `description`: to set the description of the card. +- `icon`: to set the icon of the card. +- `variant`: to change the style of the card. +- `orientation`: to change the orientation with the default slot. +- `reverse`: to reverse the orientation of the default slot. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-columns.md b/.claude/skills/nuxt-ui/components/page-columns.md new file mode 100644 index 0000000..8709edd --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-columns.md @@ -0,0 +1,11 @@ +# PageColumns + +A responsive multi-column layout system for organizing content side-by-side. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-cta.md b/.claude/skills/nuxt-ui/components/page-cta.md new file mode 100644 index 0000000..10b9838 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-cta.md @@ -0,0 +1,20 @@ +# PageCta + +A call to action section to display in your pages. + +## Key Props + +- `title`: to set the title of the CTA. +- `description`: to set the description of the CTA. +- `links`: to display a list of [Button](/docs/components/button) under the description. +- `variant`: to change the style of the CTA. +- `orientation`: to change the orientation with the default slot. +- `reverse`: to reverse the orientation of the default slot. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-feature.md b/.claude/skills/nuxt-ui/components/page-feature.md new file mode 100644 index 0000000..b3840f2 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-feature.md @@ -0,0 +1,18 @@ +# PageFeature + +A component to showcase key features of your application. + +## Key Props + +- `title`: to set the title of the feature. +- `description`: to set the description of the feature. +- `icon`: to set the icon of the feature. +- `orientation`: to change the orientation of the feature. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-grid.md b/.claude/skills/nuxt-ui/components/page-grid.md new file mode 100644 index 0000000..41e18ba --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-grid.md @@ -0,0 +1,11 @@ +# PageGrid + +A responsive grid system for displaying content in a flexible layout. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-header.md b/.claude/skills/nuxt-ui/components/page-header.md new file mode 100644 index 0000000..0347653 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-header.md @@ -0,0 +1,18 @@ +# PageHeader + +A responsive header for your pages. + +## Key Props + +- `title`: to display a title in the header. +- `description`: to display a description in the header. +- `headline`: to display a headline in the header. +- `links`: to display a list of [Button](/docs/components/button) in the header. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-hero.md b/.claude/skills/nuxt-ui/components/page-hero.md new file mode 100644 index 0000000..c88a1b0 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-hero.md @@ -0,0 +1,20 @@ +# PageHero + +A responsive hero for your pages. + +## Key Props + +- `title`: to set the title of the hero. +- `description`: to set the description of the hero. +- `headline`: to set the headline of the hero. +- `links`: to display a list of [Button](/docs/components/button) under the description. +- `orientation`: to change the orientation with the default slot. +- `reverse`: to reverse the orientation of the default slot. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-links.md b/.claude/skills/nuxt-ui/components/page-links.md new file mode 100644 index 0000000..739d75d --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-links.md @@ -0,0 +1,24 @@ +# PageLinks + +A list of links to be displayed in the page. + +## Key Props + +- `links`: as an array of objects with the following properties: + +- `label: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, link?: ClassNameValue, linkLabel?: ClassNameValue, linkLabelExternalIcon?: ClassNameValue, linkLeadingIcon?: ClassNameValue }`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `title`: to display a title above the links. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-list.md b/.claude/skills/nuxt-ui/components/page-list.md new file mode 100644 index 0000000..ff7b591 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-list.md @@ -0,0 +1,15 @@ +# PageList + +A vertical list layout for displaying content in a stacked format. + +## Key Props + +- `divide`: to add a divider between each child element. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-logos.md b/.claude/skills/nuxt-ui/components/page-logos.md new file mode 100644 index 0000000..f29921b --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-logos.md @@ -0,0 +1,16 @@ +# PageLogos + +A list of logos or images to display on your pages. + +## Key Props + +- `title`: to set the title above the logos. +- `marquee`: to enable a marquee effect for the logos. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page-section.md b/.claude/skills/nuxt-ui/components/page-section.md new file mode 100644 index 0000000..2667f9f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page-section.md @@ -0,0 +1,30 @@ +# PageSection + +A responsive section for your pages. + +## Key Props + +- `title`: to set the title of the section. +- `description`: to set the description of the section. +- `headline`: to set the headline of the section. +- `icon`: to set the icon of the section. +- `features`: to display a list of [PageFeature](/docs/components/page-feature) under the description as an array of objects with the following properties: + +- `title?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `orientation?: 'horizontal' | 'vertical'`{lang="ts-type"} + +You can pass any property from the [Link](/docs/components/link#props) component such as `to`, `target`, etc. + +- `links`: to display a list of [Button](/docs/components/button) under the description. +- `orientation`: to change the orientation with the default slot. +- `reverse`: to reverse the orientation of the default slot. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/page.md b/.claude/skills/nuxt-ui/components/page.md new file mode 100644 index 0000000..1dfa8e5 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/page.md @@ -0,0 +1,11 @@ +# Page + +A grid layout for your pages with left and right columns. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/pagination.md b/.claude/skills/nuxt-ui/components/pagination.md new file mode 100644 index 0000000..635a07f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/pagination.md @@ -0,0 +1,22 @@ +# Pagination + +A list of buttons or links to navigate through pages. + +> Based on [Reka UI Pagination](https://reka-ui.com/docs/components/pagination) + +## Key Props + +- `total`: to set the total number of items in the list. +- `color`: to set the color of the inactive controls. +- `variant`: to set the variant of the inactive controls. +- `size`: to set the size of the controls. +- `disabled`: to disable the pagination controls. +- `to`: to transform buttons into links. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/pin-input.md b/.claude/skills/nuxt-ui/components/pin-input.md new file mode 100644 index 0000000..5421001 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/pin-input.md @@ -0,0 +1,25 @@ +# PinInput + +An input element to enter a pin. + +> Based on [Reka UI PinInput](https://reka-ui.com/docs/components/pin-input) + +## Key Props + +- `type`: to change the input type. +- `mask`: to treat the input like a password. +- `otp`: to enable One-Time Password functionality. +- `length`: to change the amount of inputs. +- `placeholder`: to set a placeholder text. +- `color`: to change the ring color when the PinInput is focused. +- `variant`: to change the variant of the PinInput. +- `size`: to change the size of the PinInput. +- `disabled`: to disable the PinInput. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/popover.md b/.claude/skills/nuxt-ui/components/popover.md new file mode 100644 index 0000000..580a3cc --- /dev/null +++ b/.claude/skills/nuxt-ui/components/popover.md @@ -0,0 +1,25 @@ +# Popover + +A non-modal dialog that floats around a trigger element. + +> Based on [Reka UI Popover](https://reka-ui.com/docs/components/hover-card) + +## Key Props + +- `mode`: to change the mode of the Popover. +- `content`: to control how the Popover content is rendered, like its `align` or `side` for example. +- `arrow`: to display an arrow on the Popover. +- `modal`: to control whether the Popover blocks interaction with outside content. +- `dismissible`: to control whether the Popover is dismissible when clicking outside of it or pressing escape. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` diff --git a/.claude/skills/nuxt-ui/components/pricing-plan.md b/.claude/skills/nuxt-ui/components/pricing-plan.md new file mode 100644 index 0000000..44ca332 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/pricing-plan.md @@ -0,0 +1,39 @@ +# PricingPlan + +A customizable pricing plan to display in a pricing page. + +## Key Props + +- `title`: to set the title of the PricingPlan. +- `description`: to set the description of the PricingPlan. +- `badge`: to display a [Badge](/docs/components/badge) next to the title of the PricingPlan. +- `price`: to set the price of the PricingPlan. +- `discount`: to set a discounted price that will be displayed alongside the original price (which will be shown with a strikethrough). +- `features`: as an array of string to display a list of features on the PricingPlan: + +## ::component-code + +prettier: true +hide: + +- class + ignore: +- title +- description +- price +- features + props: + title: 'Solo' + description: 'For bootstrappers and indie hackers. +- `button`: with any property from the [Button](/docs/components/button) component to display a button at the bottom of the PricingPlan. +- `variant`: to change the variant of the PricingPlan. +- `orientation`: to change the orientation of the PricingPlan. +- `tagline`: to display a tagline text above the price. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/pricing-plans.md b/.claude/skills/nuxt-ui/components/pricing-plans.md new file mode 100644 index 0000000..d164c49 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/pricing-plans.md @@ -0,0 +1,18 @@ +# PricingPlans + +Display a list of pricing plans in a responsive grid layout. + +## Key Props + +- `plans`: as an array of objects with the properties of the [PricingPlan](/docs/components/pricing-plan#props) component. +- `orientation`: to change the orientation of the PricingPlans. +- `compact`: to reduce the padding between the plans when one of the plans is scaled for a better visual balance. +- `scale`: to adjust the spacing between the plans when one of the plans is scaled for a better visual balance. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/pricing-table.md b/.claude/skills/nuxt-ui/components/pricing-table.md new file mode 100644 index 0000000..df064c9 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/pricing-table.md @@ -0,0 +1,16 @@ +# PricingTable + +A responsive pricing table component that displays tiered pricing plans with feature comparisons. + +## Key Props + +- `tiers`: as an array of objects to define your pricing plans. +- `sections`: to organize features into logical groups. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/progress.md b/.claude/skills/nuxt-ui/components/progress.md new file mode 100644 index 0000000..9ef86d4 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/progress.md @@ -0,0 +1,23 @@ +# Progress + +An indicator showing the progress of a task. + +> Based on [Reka UI Progress](https://reka-ui.com/docs/components/progress) + +## Key Props + +- `max`: to set the maximum value of the Progress. +- `status`: to display the current Progress value above the bar. +- `animation`: to change the animation of the Progress to an inverse carousel, a swinging bar or an elastic bar. +- `orientation`: to change the orientation of the Progress. +- `color`: to change the color of the Slider. +- `size`: to change the size of the Slider. +- `inverted`: to visually invert the Progress. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/radio-group.md b/.claude/skills/nuxt-ui/components/radio-group.md new file mode 100644 index 0000000..73ce07e --- /dev/null +++ b/.claude/skills/nuxt-ui/components/radio-group.md @@ -0,0 +1,67 @@ +# RadioGroup + +A set of radio buttons to select a single option from a list. + +> Based on [Reka UI RadioGroup](https://reka-ui.com/docs/components/radio-group) + +## Key Props + +- `items`: as an array of strings or numbers: + +## ::component-code + +prettier: true +ignore: + +- modelValue +- items + external: +- items +- modelValue + props: + modelValue: 'System' + items: - 'System' - 'Light' - 'Dark' + +--- + +:: + +You can also pass an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `description?: string`{lang="ts-type"} +- [`value?: string`{lang="ts-type"}](#value-key) +- `disabled?: boolean`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, container?: ClassNameValue, base?: ClassNameValue, 'indicator'?: ClassNameValue, wrapper?: ClassNameValue, label?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- modelValue +- items + external: +- items +- modelValue + externalTypes: +- RadioGroupItem[] + props: + modelValue: 'system' + items: - label: 'System' + description: 'This is the first option. +- `legend`: to set the legend of the RadioGroup. +- `color`: to change the color of the RadioGroup. +- `variant`: to change the variant of the RadioGroup. +- `size`: to change the size of the RadioGroup. +- `orientation`: to change the orientation of the RadioGroup. +- `indicator`: to change the position or hide the indicator. +- `disabled`: to disable the RadioGroup. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/scroll-area.md b/.claude/skills/nuxt-ui/components/scroll-area.md new file mode 100644 index 0000000..3312d3e --- /dev/null +++ b/.claude/skills/nuxt-ui/components/scroll-area.md @@ -0,0 +1,21 @@ +# ScrollArea + +Creates scrollable containers with optional virtualization for large lists. + +> Based on [Reka UI ScrollArea](https://reka-ui.com/docs/components/scroll-area) + +## Key Props + +- `orientation`: to control scroll direction (`'vertical'` or `'horizontal'`). +- `items`: array of data to render within the scrollable area. +- `virtualize`: enables performance optimization for large datasets (renders only visible items). Supports `estimateSize`, `lanes`, `gap`. +- `as`: specifies the underlying HTML element or component (defaults to `'div'`). +- `ui`: customization object for styling root, viewport, and items. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/select-menu.md b/.claude/skills/nuxt-ui/components/select-menu.md new file mode 100644 index 0000000..4684d40 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/select-menu.md @@ -0,0 +1,68 @@ +# SelectMenu + +An advanced searchable select element. + +> Based on [Reka UI SelectMenu](https://reka-ui.com/docs/components/combobox) + +## Key Props + +- `items`: as an array of strings, numbers or booleans: + +## ::component-code + +prettier: true +ignore: + +- modelValue +- items +- class + external: +- items +- modelValue + props: + modelValue: 'Backlog' + items: - Backlog - Todo - In Progress - Done + class: 'w-48' + +--- + +:: + +You can also pass an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- [`type?: "label" | "separator" | "item"`{lang="ts-type"}](#with-items-type) +- [`icon?: string`{lang="ts-type"}](#with-icons-in-items) +- [`avatar?: AvatarProps`{lang="ts-type"}](#with-avatar-in-items) +- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items) +- `disabled?: boolean`{lang="ts-type"} +- `onSelect?: (e: Event) => void`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- modelValue. +- `multiple`: to allow multiple selections, the selected items will be separated by a comma in the trigger. +- `placeholder`: to set a placeholder text. +- `content`: to control how the SelectMenu content is rendered, like its `align` or `side` for example. +- `arrow`: to display an arrow on the SelectMenu. +- `color`: to change the ring color when the SelectMenu is focused. +- `variant`: to change the variant of the SelectMenu. +- `size`: to change the size of the SelectMenu. +- `icon`: to show an [Icon](/docs/components/icon) inside the SelectMenu. +- `avatar`: to display an [Avatar](/docs/components/avatar) inside the SelectMenu. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#leading` diff --git a/.claude/skills/nuxt-ui/components/select.md b/.claude/skills/nuxt-ui/components/select.md new file mode 100644 index 0000000..32fddd8 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/select.md @@ -0,0 +1,91 @@ +# Select + +A select element to choose from a list of options. + +> Based on [Reka UI Select](https://reka-ui.com/docs/components/select) + +## Key Props + +- `items`: as an array of strings, numbers or booleans: + +## ::component-code + +prettier: true +ignore: + +- modelValue +- items +- class + external: +- items +- modelValue + props: + modelValue: 'Backlog' + items: - Backlog - Todo - In Progress - Done + class: 'w-48' + +--- + +:: + +You can also pass an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- [`value?: string`{lang="ts-type"}](#value-key) +- [`type?: "label" | "separator" | "item"`{lang="ts-type"}](#with-items-type) +- [`icon?: string`{lang="ts-type"}](#with-icons-in-items) +- [`avatar?: AvatarProps`{lang="ts-type"}](#with-avatar-in-items) +- [`chip?: ChipProps`{lang="ts-type"}](#with-chip-in-items) +- `disabled?: boolean`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { label?: ClassNameValue, separator?: ClassNameValue, item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- modelValue +- items +- class + external: +- items +- modelValue + externalTypes: +- SelectItem[] + props: + modelValue: 'backlog' + items: - label: 'Backlog' + value: 'backlog' - label: 'Todo' + value: 'todo' - label: 'In Progress' + value: 'in_progress' - label: 'Done' + value: 'done' + class: 'w-48' + +--- + +:: + +::caution +When using objects, you need to reference the `value` property of the object in the `v-model` directive or the `default-value` prop. + +- `multiple`: to allow multiple selections, the selected items will be separated by a comma in the trigger. +- `placeholder`: to set a placeholder text. +- `content`: to control how the Select content is rendered, like its `align` or `side` for example. +- `arrow`: to display an arrow on the Select. +- `color`: to change the ring color when the Select is focused. +- `variant`: to change the variant of the Select. +- `size`: to change the size of the Select. +- `icon`: to show an [Icon](/docs/components/icon) inside the Select. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the Select. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#leading` diff --git a/.claude/skills/nuxt-ui/components/separator.md b/.claude/skills/nuxt-ui/components/separator.md new file mode 100644 index 0000000..e410974 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/separator.md @@ -0,0 +1,23 @@ +# Separator + +Separates content horizontally or vertically. + +> Based on [Reka UI Separator](https://reka-ui.com/docs/components/separator) + +## Key Props + +- `orientation`: to change the orientation of the Separator. +- `label`: to display a label in the middle of the Separator. +- `icon`: to display an icon in the middle of the Separator. +- `avatar`: to display an avatar in the middle of the Separator. +- `color`: to change the color of the Separator. +- `type`: to change the type of the Separator. +- `size`: to change the size of the Separator. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/skeleton.md b/.claude/skills/nuxt-ui/components/skeleton.md new file mode 100644 index 0000000..f843c9d --- /dev/null +++ b/.claude/skills/nuxt-ui/components/skeleton.md @@ -0,0 +1,11 @@ +# Skeleton + +A placeholder to show while content is loading. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/slideover.md b/.claude/skills/nuxt-ui/components/slideover.md new file mode 100644 index 0000000..c1b8bc2 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/slideover.md @@ -0,0 +1,31 @@ +# Slideover + +A dialog that slides in from any side of the screen. + +> Based on [Reka UI Slideover](https://reka-ui.com/docs/components/dialog) + +## Key Props + +- `title`: to set the title of the Slideover's header. +- `description`: to set the description of the Slideover's header. +- `close`: to customize or hide the close button (with `false` value) displayed in the Slideover's header. +- `side`: to set the side of the screen where the Slideover will slide in from. +- `transition`: to control whether the Slideover is animated or not. +- `overlay`: to control whether the Slideover has an overlay or not. +- `modal`: to control whether the Slideover blocks interaction with outside content. +- `dismissible`: to control whether the Slideover is dismissible when clicking outside of it or pressing escape. + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` +- `#header` +- `#body` +- `#footer` diff --git a/.claude/skills/nuxt-ui/components/slider.md b/.claude/skills/nuxt-ui/components/slider.md new file mode 100644 index 0000000..8d45cdc --- /dev/null +++ b/.claude/skills/nuxt-ui/components/slider.md @@ -0,0 +1,23 @@ +# Slider + +An input to select a numeric value within a range. + +> Based on [Reka UI Slider](https://reka-ui.com/docs/components/slider) + +## Key Props + +- `step`: to set the increment value of the Slider. +- `orientation`: to change the orientation of the Slider. +- `color`: to change the color of the Slider. +- `size`: to change the size of the Slider. +- `tooltip`: to display a [Tooltip](/docs/components/tooltip) around the Slider thumbs with the current value. +- `disabled`: to disable the Slider. +- `inverted`: to visually invert the Slider. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/stepper.md b/.claude/skills/nuxt-ui/components/stepper.md new file mode 100644 index 0000000..c05b978 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/stepper.md @@ -0,0 +1,63 @@ +# Stepper + +A set of steps that are used to indicate progress through a multi-step process. + +> Based on [Reka UI Stepper](https://reka-ui.com/docs/components/stepper) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `title?: string`{lang="ts-type"} +- `description?: AvatarProps`{lang="ts-type"} +- `content?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `value?: string | number`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, container?: ClassNameValue, trigger?: ClassNameValue, indicator?: ClassNameValue, icon?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- items +- class + external: +- items + externalTypes: +- StepperItem[] + props: + items: - title: 'Address' + description: 'Add your address here' + icon: 'i-lucide-house' - title: 'Shipping' + description: 'Set your preferred shipping method' + icon: 'i-lucide-truck' - title: 'Checkout' + description: 'Confirm your order' + class: 'w-full' + +--- + +:: + +::note +Click on the items to navigate through the steps. + +- `color`: to change the color of the Stepper. +- `size`: to change the size of the Stepper. +- `orientation`: to change the orientation of the Stepper. +- `disabled`: to disable navigation through the steps. +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` diff --git a/.claude/skills/nuxt-ui/components/switch.md b/.claude/skills/nuxt-ui/components/switch.md new file mode 100644 index 0000000..a21797e --- /dev/null +++ b/.claude/skills/nuxt-ui/components/switch.md @@ -0,0 +1,22 @@ +# Switch + +A control that toggles between two states. + +> Based on [Reka UI Switch](https://reka-ui.com/docs/components/switch) + +## Key Props + +- `label`: to set the label of the Switch. +- `description`: to set the description of the Switch. +- `loading`: to show a loading icon on the Switch. +- `color`: to change the color of the Switch. +- `size`: to change the size of the Switch. +- `disabled`: to disable the Switch. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/table.md b/.claude/skills/nuxt-ui/components/table.md new file mode 100644 index 0000000..a9e9756 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/table.md @@ -0,0 +1,20 @@ +# Table + +A responsive table element to display data in rows and columns. + +## Key Props + +- `data`: as an array of objects, the columns will be generated based on the keys of the objects. +- `columns`: as an array of [ColumnDef](https://tanstack. +- `meta`: as an object ([TableMeta](https://tanstack. +- `loading`: to display a loading state, the `loading-color` prop to change its color and the `loading-animation` prop to change its animation. +- `sticky`: to make the header or footer sticky. +- `virtualize`: to enable virtualization for large datasets as a boolean or an object with options like `{ estimateSize: 65, overscan: 12 }`. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/tabs.md b/.claude/skills/nuxt-ui/components/tabs.md new file mode 100644 index 0000000..5d4e75f --- /dev/null +++ b/.claude/skills/nuxt-ui/components/tabs.md @@ -0,0 +1,52 @@ +# Tabs + +A set of tab panels that are displayed one at a time. + +> Based on [Reka UI Tabs](https://reka-ui.com/docs/components/tabs) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `label?: string`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- `badge?: string | number | BadgeProps`{lang="ts-type"} +- `content?: string`{lang="ts-type"} +- `value?: string | number`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `class?: any`{lang="ts-type"} +- `ui?: { trigger?: ClassNameValue, leadingIcon?: ClassNameValue, leadingAvatar?: ClassNameValue, leadingAvatarSize?: ClassNameValue, label?: ClassNameValue, trailingBadge?: ClassNameValue, trailingBadgeSize?: ClassNameValue, content?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- items +- class + external: +- items + externalTypes: +- TabsItem[] + props: + items: - label: Account + icon: 'i-lucide-user' + content: 'This is the account content. +- `color`: to change the color of the Tabs. +- `variant`: to change the variant of the Tabs. +- `size`: to change the size of the Tabs. +- `orientation`: to change the orientation of the Tabs. +- `slot`: + +## Usage + +```vue + +/> +``` + +## Slots + +- `#content` diff --git a/.claude/skills/nuxt-ui/components/textarea.md b/.claude/skills/nuxt-ui/components/textarea.md new file mode 100644 index 0000000..83ca9b5 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/textarea.md @@ -0,0 +1,24 @@ +# Textarea + +A textarea element to input multi-line text. + +## Key Props + +- `rows`: to set the number of rows. +- `placeholder`: to set a placeholder text. +- `autoresize`: to enable autoresizing the height of the Textarea. +- `maxrows`: to set the maximum number of rows when autoresizing. +- `color`: to change the ring color when the Textarea is focused. +- `variant`: to change the variant of the Textarea. +- `size`: to change the size of the Textarea. +- `icon`: to show an [Icon](/docs/components/icon) inside the Textarea. +- `avatar`: to show an [Avatar](/docs/components/avatar) inside the Textarea. +- `loading`: to show a loading icon on the Textarea. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/timeline.md b/.claude/skills/nuxt-ui/components/timeline.md new file mode 100644 index 0000000..02f4aab --- /dev/null +++ b/.claude/skills/nuxt-ui/components/timeline.md @@ -0,0 +1,52 @@ +# Timeline + +A component that displays a sequence of events with dates, titles, icons or avatars. + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `date?: string`{lang="ts-type"} +- `title?: string`{lang="ts-type"} +- `description?: AvatarProps`{lang="ts-type"} +- `icon?: string`{lang="ts-type"} +- `avatar?: AvatarProps`{lang="ts-type"} +- `value?: string | number`{lang="ts-type"} +- [`slot?: string`{lang="ts-type"}](#with-custom-slot) +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, container?: ClassNameValue, indicator?: ClassNameValue, separator?: ClassNameValue, wrapper?: ClassNameValue, date?: ClassNameValue, title?: ClassNameValue, description?: ClassNameValue }`{lang="ts-type"} + +## ::component-code + +ignore: + +- items +- class +- defaultValue + external: +- items + externalTypes: +- TimelineItem[] + props: + defaultValue: 2 + items: - date: 'Mar 15, 2025' + title: 'Project Kickoff' + description: 'Kicked off the project with team alignment. +- `color`: to change the color of the active items in a Timeline. +- `size`: to change the size of the Timeline. +- `orientation`: to change the orientation of the Timeline. +- `ui`: to create a Timeline with alternating layout. +- `slot`: + +## Events + +- `@select`: Emitted when an item is selected (v4.4+) + +## Usage + +```vue + +``` diff --git a/.claude/skills/nuxt-ui/components/toast.md b/.claude/skills/nuxt-ui/components/toast.md new file mode 100644 index 0000000..21091aa --- /dev/null +++ b/.claude/skills/nuxt-ui/components/toast.md @@ -0,0 +1,13 @@ +# Toast + +A succinct message to provide information or feedback to the user. + +> Based on [Reka UI Toast](https://reka-ui.com/docs/components/toast) + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/tooltip.md b/.claude/skills/nuxt-ui/components/tooltip.md new file mode 100644 index 0000000..eaf3374 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/tooltip.md @@ -0,0 +1,21 @@ +# Tooltip + +A popup that reveals information when hovering over an element. + +> Based on [Reka UI Tooltip](https://reka-ui.com/docs/components/tooltip) + +## Key Props + +- `text`: to set the content of the Tooltip. +- `kbds`: to render [Kbd](/docs/components/kbd) components in the Tooltip. +- `content`: to control how the Tooltip content is rendered, like its `align` or `side` for example. +- `arrow`: to display an arrow on the Tooltip. +- `disabled`: to disable the Tooltip. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/tree.md b/.claude/skills/nuxt-ui/components/tree.md new file mode 100644 index 0000000..cf92167 --- /dev/null +++ b/.claude/skills/nuxt-ui/components/tree.md @@ -0,0 +1,40 @@ +# Tree + +A tree view component to display and interact with hierarchical data structures. + +> Based on [Reka UI Tree](https://reka-ui.com/docs/components/tree) + +## Key Props + +- `items`: as an array of objects with the following properties: + +- `icon?: string`{lang="ts-type"} +- `label?: string`{lang="ts-type"} +- `trailingIcon?: string`{lang="ts-type"} +- `defaultExpanded?: boolean`{lang="ts-type"} +- `disabled?: boolean`{lang="ts-type"} +- `slot?: string`{lang="ts-type"} +- `children?: TreeItem[]`{lang="ts-type"} +- `onToggle?: (e: TreeItemToggleEvent) => void`{lang="ts-type"} +- `onSelect?: (e: TreeItemSelectEvent) => void`{lang="ts-type"} +- `class?: any`{lang="ts-type"} +- `ui?: { item?: ClassNameValue, itemWithChildren?: ClassNameValue, link?: ClassNameValue, linkLeadingIcon?: ClassNameValue, linkLabel?: ClassNameValue, linkTrailing?: ClassNameValue, linkTrailingIcon?: ClassNameValue, listWithChildren?: ClassNameValue }`{lang="ts-type"} + +::note +A unique identifier is required for each item. + +- `multiple`: to allow multiple item selections. +- `nested`: to control whether the Tree is rendered with nested structure or as a flat list. +- `color`: to change the color of the Tree. +- `size`: to change the size of the Tree. +- `disabled`: to prevent any user interaction with the Tree. +- `virtualize`: to enable virtualization for large lists as a boolean or an object with options like `{ estimateSize: 32, overscan: 12 }`. +- `slot`: + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/components/user.md b/.claude/skills/nuxt-ui/components/user.md new file mode 100644 index 0000000..c22785a --- /dev/null +++ b/.claude/skills/nuxt-ui/components/user.md @@ -0,0 +1,20 @@ +# User + +Display user information with name, description and avatar. + +## Key Props + +- `name`: to display a name for the user. +- `description`: to display a description for the user. +- `avatar`: to display an [Avatar](/docs/components/avatar) component. +- `chip`: to display a [Chip](/docs/components/chip) component. +- `size`: to change the size of the user avatar and text. +- `orientation`: to change the orientation. + +## Usage + +```vue + +/> +``` diff --git a/.claude/skills/nuxt-ui/references/components.md b/.claude/skills/nuxt-ui/references/components.md new file mode 100644 index 0000000..e1215e2 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/components.md @@ -0,0 +1,161 @@ +# Components + +> Auto-generated from Nuxt UI docs. Run `npx tsx skills/nuxt-ui/scripts/generate-components.ts` to update. + +> **For headless primitives (API, accessibility):** see `reka-ui` skill + +## Data + +| Component | Description | +| --------------------------------------- | ------------------------------------------------------------------------------------ | +| [Accordion](components/accordion.md) | A stacked set of collapsible panels. | +| [Carousel](components/carousel.md) | A carousel with motion and swipe built using Embla. | +| [Empty](components/empty.md) | A component to display an empty state. (v4.1+) | +| [Marquee](components/marquee.md) | A component to create infinite scrolling content. | +| [ScrollArea](components/scroll-area.md) | A flexible scroll container with virtualization support. (v4.3+) | +| [Table](components/table.md) | A responsive table element to display data in rows and columns. | +| [Timeline](components/timeline.md) | A component that displays a sequence of events with dates, titles, icons or avatars. | +| [Tree](components/tree.md) | A tree view component to display and interact with hierarchical data structures. | +| [User](components/user.md) | Display user information with name, description and avatar. | + +## Element + +| Component | Description | +| ----------------------------------------- | ---------------------------------------------------------------------------------------- | +| [Alert](components/alert.md) | A callout to draw user's attention. | +| [Avatar](components/avatar.md) | An img element with fallback and Nuxt Image support. | +| [AvatarGroup](components/avatar-group.md) | Stack multiple avatars in a group. | +| [Badge](components/badge.md) | A short text to represent a status or a category. | +| [Banner](components/banner.md) | Display a banner at the top of your website to inform users about important information. | +| [Button](components/button.md) | A button element that can act as a link or trigger an action. | +| [Calendar](components/calendar.md) | A calendar component for selecting single dates, multiple dates or date ranges. | +| [Card](components/card.md) | Display content in a card with a header, body and footer. | +| [Chip](components/chip.md) | An indicator of a numeric value or a state. | +| [Collapsible](components/collapsible.md) | A collapsible element to toggle visibility of its content. | +| [FieldGroup](components/field-group.md) | Group multiple button-like elements together. | +| [Icon](components/icon.md) | A component to display any icon from Iconify or another component. | +| [Kbd](components/kbd.md) | A kbd element to display a keyboard key. | +| [Progress](components/progress.md) | An indicator showing the progress of a task. | +| [Separator](components/separator.md) | Separates content horizontally or vertically. | +| [Skeleton](components/skeleton.md) | A placeholder to show while content is loading. | + +## Form + +| Component | Description | +| --------------------------------------------- | ------------------------------------------------------------------------ | +| [Checkbox](components/checkbox.md) | An input element to toggle between checked and unchecked states. | +| [CheckboxGroup](components/checkbox-group.md) | A set of checklist buttons to select multiple option from a list. | +| [ColorPicker](components/color-picker.md) | A component to select a color. | +| [FileUpload](components/file-upload.md) | An input element to upload files. | +| [Form](components/form.md) | A form component with built-in validation and submission handling. | +| [FormField](components/form-field.md) | A wrapper for form elements that provides validation and error handling. | +| [Input](components/input.md) | An input element to enter text. | +| [InputDate](components/input-date.md) | An input component for date selection. (v4.2+) | +| [InputMenu](components/input-menu.md) | An autocomplete input with real-time suggestions. | +| [InputNumber](components/input-number.md) | An input for numerical values with a customizable range. | +| [InputTags](components/input-tags.md) | An input element that displays interactive tags. | +| [InputTime](components/input-time.md) | An input for selecting a time. (v4.2+) | +| [PinInput](components/pin-input.md) | An input element to enter a pin. | +| [RadioGroup](components/radio-group.md) | A set of radio buttons to select a single option from a list. | +| [Select](components/select.md) | A select element to choose from a list of options. | +| [SelectMenu](components/select-menu.md) | An advanced searchable select element. | +| [Slider](components/slider.md) | An input to select a numeric value within a range. | +| [Switch](components/switch.md) | A control that toggles between two states. | +| [Textarea](components/textarea.md) | A textarea element to input multi-line text. | + +## Layout + +| Component | Description | +| ------------------------------------ | -------------------------------------------------------------------- | +| [App](components/app.md) | Wraps your app to provide global configurations and more. | +| [Container](components/container.md) | A container lets you center and constrain the width of your content. | +| [Error](components/error.md) | A pre-built error component with NuxtError support. | +| [Footer](components/footer.md) | A responsive footer component. | +| [Header](components/header.md) | A responsive header component. | +| [Main](components/main.md) | A main element that fills the available viewport height. | + +## Navigation + +| Component | Description | +| ----------------------------------------------- | ---------------------------------------------------------------------------------------- | +| [Breadcrumb](components/breadcrumb.md) | A hierarchy of links to navigate through a website. | +| [CommandPalette](components/command-palette.md) | A command palette with full-text search powered by Fuse.js for efficient fuzzy matching. | +| [FooterColumns](components/footer-columns.md) | A list of links as columns to display in your Footer. | +| [Link](components/link.md) | A wrapper around with extra props. | +| [NavigationMenu](components/navigation-menu.md) | A list of links that can be displayed horizontally or vertically. | +| [Pagination](components/pagination.md) | A list of buttons or links to navigate through pages. | +| [Stepper](components/stepper.md) | A set of steps that are used to indicate progress through a multi-step process. | +| [Tabs](components/tabs.md) | A set of tab panels that are displayed one at a time. | + +## Other + +| Component | Description | Version | +| -------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- | ------- | +| [AuthForm](components/auth-form.md) | A customizable Form to create login, register or password reset forms. | | +| [BlogPost](components/blog-post.md) | A customizable article to display in a blog page. | | +| [BlogPosts](components/blog-posts.md) | Display a list of blog posts in a responsive grid layout. | | +| [ChangelogVersion](components/changelog-version.md) | A customizable article to display in a changelog. | | +| [ChangelogVersions](components/changelog-versions.md) | Display a list of changelog versions in a timeline. | | +| [ChatMessage](components/chat-message.md) | Display a chat message with icon, avatar, and actions. | | +| [ChatMessages](components/chat-messages.md) | Display a list of chat messages, designed to work seamlessly with Vercel AI SDK. | | +| [ChatPalette](components/chat-palette.md) | A chat palette to create a chatbot interface inside an overlay. | | +| [ChatPrompt](components/chat-prompt.md) | An enhanced Textarea for submitting prompts in AI chat interfaces. | | +| [ChatPromptSubmit](components/chat-prompt-submit.md) | A Button for submitting chat prompts with automatic status handling. | | +| [ColorModeAvatar](components/color-mode-avatar.md) | An Avatar with a different source for light and dark mode. | | +| [ColorModeButton](components/color-mode-button.md) | A Button to switch between light and dark mode. | | +| [ColorModeImage](components/color-mode-image.md) | An image element with a different source for light and dark mode. | | +| [ColorModeSelect](components/color-mode-select.md) | A Select to switch between system, dark & light mode. | | +| [ColorModeSwitch](components/color-mode-switch.md) | A switch to toggle between light and dark mode. | | +| [ContentNavigation](components/content-navigation.md) | An accordion-style navigation component for organizing page links. | | +| [ContentSearch](components/content-search.md) | A ready to use CommandPalette to add to your documentation. | | +| [ContentSearchButton](components/content-search-button.md) | A pre-styled Button to open the ContentSearch modal. | | +| [ContentSurround](components/content-surround.md) | A pair of prev and next links to navigate between pages. | | +| [ContentToc](components/content-toc.md) | A sticky Table of Contents with automatic active anchor link highlighting. | | +| [DashboardGroup](components/dashboard-group.md) | A fixed layout component that provides context for dashboard components with sidebar state management and persistence. | | +| [DashboardNavbar](components/dashboard-navbar.md) | A responsive navbar to display in a dashboard. | | +| [DashboardPanel](components/dashboard-panel.md) | A resizable panel to display in a dashboard. | | +| [DashboardResizeHandle](components/dashboard-resize-handle.md) | A handle to resize a sidebar or panel. | | +| [DashboardSearch](components/dashboard-search.md) | A ready to use CommandPalette to add to your dashboard. | | +| [DashboardSearchButton](components/dashboard-search-button.md) | A pre-styled Button to open the DashboardSearch modal. | | +| [DashboardSidebar](components/dashboard-sidebar.md) | A resizable and collapsible sidebar to display in a dashboard. | | +| [DashboardSidebarCollapse](components/dashboard-sidebar-collapse.md) | A Button to collapse the sidebar on desktop. | | +| [DashboardSidebarToggle](components/dashboard-sidebar-toggle.md) | A Button to toggle the sidebar on mobile. | | +| [DashboardToolbar](components/dashboard-toolbar.md) | A toolbar to display under the navbar in a dashboard. | | +| [Editor](components/editor.md) | A rich text editor component based on TipTap with support for markdown, HTML, and JSON content types. (v4.3+) | v4.3+ | +| [EditorDragHandle](components/editor-drag-handle.md) | A draggable handle for reordering and selecting blocks in the editor. (v4.3+) | v4.3+ | +| [EditorEmojiMenu](components/editor-emoji-menu.md) | An emoji picker menu that displays emoji suggestions when typing the : character in the editor. (v4.3+) | v4.3+ | +| [EditorMentionMenu](components/editor-mention-menu.md) | A mention menu that displays user suggestions when typing the @ character in the editor. (v4.3+) | v4.3+ | +| [EditorSuggestionMenu](components/editor-suggestion-menu.md) | A command menu that displays formatting and action suggestions when typing the / character in the editor. (v4.3+) | v4.3+ | +| [EditorToolbar](components/editor-toolbar.md) | A customizable toolbar for editor actions that can be displayed as fixed, bubble, or floating menu. (v4.3+) | v4.3+ | +| [LocaleSelect](components/locale-select.md) | A Select to switch between locales. | | +| [Page](components/page.md) | A grid layout for your pages with left and right columns. | | +| [PageAnchors](components/page-anchors.md) | A list of anchors to be displayed in the page. | | +| [PageAside](components/page-aside.md) | A sticky aside to display your page navigation. | | +| [PageBody](components/page-body.md) | The main content of your page. | | +| [PageCard](components/page-card.md) | A pre-styled card component that displays a title, description and optional link. | | +| [PageColumns](components/page-columns.md) | A responsive multi-column layout system for organizing content side-by-side. | | +| [PageCta](components/page-cta.md) | A call to action section to display in your pages. | | +| [PageFeature](components/page-feature.md) | A component to showcase key features of your application. | | +| [PageGrid](components/page-grid.md) | A responsive grid system for displaying content in a flexible layout. | | +| [PageHeader](components/page-header.md) | A responsive header for your pages. | | +| [PageHero](components/page-hero.md) | A responsive hero for your pages. | | +| [PageLinks](components/page-links.md) | A list of links to be displayed in the page. | | +| [PageList](components/page-list.md) | A vertical list layout for displaying content in a stacked format. | | +| [PageLogos](components/page-logos.md) | A list of logos or images to display on your pages. | | +| [PageSection](components/page-section.md) | A responsive section for your pages. | | +| [PricingPlan](components/pricing-plan.md) | A customizable pricing plan to display in a pricing page. | | +| [PricingPlans](components/pricing-plans.md) | Display a list of pricing plans in a responsive grid layout. | | +| [PricingTable](components/pricing-table.md) | A responsive pricing table component that displays tiered pricing plans with feature comparisons. | | + +## Overlay + +| Component | Description | +| ------------------------------------------- | ---------------------------------------------------------------------------- | +| [ContextMenu](components/context-menu.md) | A menu to display actions when right-clicking on an element. | +| [Drawer](components/drawer.md) | A drawer that smoothly slides in & out of the screen. | +| [DropdownMenu](components/dropdown-menu.md) | A menu to display actions when clicking on an element. | +| [Modal](components/modal.md) | A dialog window that can be used to display a message or request user input. | +| [Popover](components/popover.md) | A non-modal dialog that floats around a trigger element. | +| [Slideover](components/slideover.md) | A dialog that slides in from any side of the screen. | +| [Toast](components/toast.md) | A succinct message to provide information or feedback to the user. | +| [Tooltip](components/tooltip.md) | A popup that reveals information when hovering over an element. | diff --git a/.claude/skills/nuxt-ui/references/composables.md b/.claude/skills/nuxt-ui/references/composables.md new file mode 100644 index 0000000..65ae61d --- /dev/null +++ b/.claude/skills/nuxt-ui/references/composables.md @@ -0,0 +1,328 @@ +# Composables + +## useToast + +Show notifications. Requires `` wrapper. + +```ts +const toast = useToast() + +// Add toast +toast.add({ + title: 'Success', + description: 'Item saved', + color: 'success', + icon: 'i-heroicons-check-circle', + timeout: 5000 +}) + +// Remove specific toast +toast.remove('toast-id') + +// Clear all toasts +toast.clear() +``` + +See overlays.md for full toast options. + +## useOverlay + +Programmatically create modals, slidelovers, drawers. + +```ts +const overlay = useOverlay() + +// Create modal +const modal = overlay.create(MyModalComponent, { + props: { title: 'Confirm' }, + modal: true // Default: true +}) + +// Wait for result +const result = await modal.result + +// Close programmatically +modal.close(returnValue) +``` + +### Confirm Dialog Pattern + +```vue + + + + +``` + +```ts +// Usage +const overlay = useOverlay() + +async function confirmDelete() { + const modal = overlay.create(ConfirmDialog, { + props: { title: 'Delete?', message: 'This cannot be undone.' }, + events: { + confirm: () => modal.close(true), + cancel: () => modal.close(false) + } + }) + + if (await modal.result) { + // Delete item + } +} +``` + +## defineShortcuts + +Define keyboard shortcuts. + +```ts +defineShortcuts({ + // Single key + escape: () => closeModal(), + + // Modifier + key (meta = Cmd on Mac, Ctrl on Windows) + meta_k: () => openSearch(), + meta_shift_p: () => openCommandPalette(), + + // Ctrl specific + ctrl_s: () => save(), + + // Alt/Option + alt_n: () => newItem(), + + // Arrow keys + arrowup: () => navigateUp(), + arrowdown: () => navigateDown(), + + // With condition + meta_enter: { + handler: () => submit(), + whenever: [isFormValid] + } +}, { + layoutIndependent: true // Ignore keyboard layout (v4.3+) +}) +``` + +### Shortcut Syntax + +| Key | Meaning | +| ------- | -------------------------- | +| `meta` | Cmd (Mac) / Ctrl (Windows) | +| `ctrl` | Ctrl key | +| `alt` | Alt / Option key | +| `shift` | Shift key | +| `_` | Key separator | + +### Extract Shortcuts (for display) + +```ts +const shortcuts = extractShortcuts({ + meta_k: () => {}, + escape: () => {} +}) +// Returns: { meta_k: { key: 'K', metaKey: true }, ... } +``` + +## useKbd + +Detect current keyboard state. + +```ts +const kbd = useKbd() + +// Reactive state +kbd.meta // true if Cmd/Ctrl pressed +kbd.ctrl // true if Ctrl pressed +kbd.shift // true if Shift pressed +kbd.alt // true if Alt/Option pressed +``` + +## useScrollspy + +Track scroll position for anchor navigation. + +```ts +const { activeId } = useScrollspy({ + ids: ['section-1', 'section-2', 'section-3'], + offset: 100 // Pixels from top +}) + +// activeId.value = 'section-2' (currently visible) +``` + +### With Table of Contents + +```vue + + + +``` + +## useFileUpload + +Handle file uploads. + +```ts +const { files, open, reset, remove } = useFileUpload({ + accept: 'image/*', + multiple: true, + maxFiles: 5, + maxSize: 5 * 1024 * 1024 // 5MB +}) + +// Open file picker +open() + +// Files selected +files.value // FileList + +// Reset selection +reset() + +// Remove specific file +remove(index) +``` + +### With UFileUpload + +```vue + + + +``` + +## defineLocale + +Define/extend locale for i18n. + +```ts +// locales/es.ts +export default defineLocale({ + name: 'Español', + code: 'es', + messages: { + select: { + placeholder: 'Seleccionar...', + noResults: 'Sin resultados' + }, + pagination: { + previous: 'Anterior', + next: 'Siguiente' + } + } +}) +``` + +### Extend Existing Locale + +```ts +import en from '@nuxt/ui/locale/en' + +export default extendLocale(en, { + messages: { + select: { + placeholder: 'Choose an option...' + } + } +}) +``` + +### Use in App + +```vue + + + +``` + +## useFormField + +Access form field context in custom components. + +```ts +// Inside custom form component +const { name, error, disabled } = useFormField() + +// name: field name from UFormField +// error: validation error message +// disabled: if field is disabled +``` + +### Custom Input Component + +```vue + + + +``` + +## Quick Reference + +| Composable | Purpose | +| ------------------ | ------------------------------- | +| `useToast` | Show notifications | +| `useOverlay` | Programmatic modals/slidelovers | +| `defineShortcuts` | Keyboard shortcuts | +| `useKbd` | Keyboard state detection | +| `useScrollspy` | Track scroll for TOC | +| `useFileUpload` | File upload handling | +| `defineLocale` | i18n locale definition | +| `extendLocale` | Extend existing locale | +| `useFormField` | Form field context | +| `extractShortcuts` | Parse shortcut definitions | diff --git a/.claude/skills/nuxt-ui/references/forms.md b/.claude/skills/nuxt-ui/references/forms.md new file mode 100644 index 0000000..ecb4dcf --- /dev/null +++ b/.claude/skills/nuxt-ui/references/forms.md @@ -0,0 +1,328 @@ +# Forms + +## Form Components + +| Component | Purpose | Key Props | Version | +| ---------------- | ------------------- | --------------------------------------------- | ------- | +| `UInput` | Text input | `type`, `placeholder`, `icon`, `loading` | | +| `UTextarea` | Multi-line text | `rows`, `autoresize`, `maxrows` | | +| `USelect` | Native select | `options`, `placeholder` | | +| `USelectMenu` | Custom select | `options`, `searchable`, `multiple` | | +| `UInputMenu` | Autocomplete | `options`, `searchable` | | +| `UCheckbox` | Single checkbox | `label`, `description` | | +| `UCheckboxGroup` | Multiple checkboxes | `items`, `orientation` | | +| `URadioGroup` | Radio options | `items`, `orientation` | | +| `USwitch` | Toggle switch | `label`, `description`, `on-icon`, `off-icon` | | +| `USlider` | Range slider | `min`, `max`, `step` | | +| `UPinInput` | Code/OTP input | `length`, `type`, `mask` | | +| `UInputNumber` | Number input | `min`, `max`, `step`, `format-options` | | +| `UInputDate` | Date picker | `mode`, `range`, `locale` | v4.2+ | +| `UInputTime` | Time picker | `hour-cycle`, `minute-step` | v4.2+ | +| `UInputTags` | Tag input | `max`, `placeholder` | | +| `UColorPicker` | Color selection | `format`, `swatches` | | +| `UFileUpload` | File upload | `accept`, `multiple`, `max-files` | | + +## Basic Input Examples + +```vue + + + +``` + +## SelectMenu (Custom Dropdown) + +```vue + + + +``` + +### SelectMenu/InputMenu Props (v4.4+) + +```vue + + :viewport-ref="ref" +/> +``` + +## Checkbox & Radio + +```vue + + + +``` + +## Form Validation + +Uses Standard Schema (Zod, Valibot, Yup, Joi, etc.) + +### With Zod + +```vue + + + +``` + +### With Valibot + +```vue + + + +``` + +## UFormField Props + +```vue + + label="Email" + description="Your email" + hint="Optional" + required + :help="error?.message" +> + + +``` + +## UFieldGroup (Group Fields) + +```vue + + + + +``` + +## Input States + +```vue + + + + + + + + + + + +``` + +## File Upload + +```vue + + + +``` + +**Note (v4.4):** FileUpload now emits `null` when clearing files (previously emitted empty array). + +## Date & Time Pickers (v4.2+) + +### Date Picker + +```vue + + + +``` + +### Time Picker + +```vue + + + +``` + +## Common Patterns + +### Login Form + +```vue + + + + + + + + Sign in + +``` + +### Settings Form + +```vue + + + + + + + + Save + +``` + +## Best Practices + +| Do | Don't | +| --------------------------------- | -------------------------- | +| Use UForm + UFormField | Build custom form wrappers | +| Use Standard Schema (Zod/Valibot) | Write custom validation | +| Use v-model on form inputs | Manually sync form state | +| Use `required` prop for asterisks | Add asterisks manually | +| Use `description` for help text | Use separate help elements | diff --git a/.claude/skills/nuxt-ui/references/installation.md b/.claude/skills/nuxt-ui/references/installation.md new file mode 100644 index 0000000..97e8f44 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/installation.md @@ -0,0 +1,214 @@ +# Installation + +## Nuxt Installation + +```bash +pnpm add @nuxt/ui +``` + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@nuxt/ui'], + css: ['~/assets/css/main.css'] +}) +``` + +```css +/* assets/css/main.css */ +@import 'tailwindcss'; +@import '@nuxt/ui'; +``` + +**Critical**: Wrap app in UApp for Toast, Tooltip, and overlays to work: + +```vue + + +``` + +### pnpm Gotcha + +If using pnpm, either: + +1. Add `shamefully-hoist=true` to `.npmrc`, OR +2. Install tailwindcss explicitly: `pnpm add tailwindcss` + +## Vue Installation (Vite) + +```bash +pnpm add @nuxt/ui +``` + +```ts +import ui from '@nuxt/ui/vite' +import vue from '@vitejs/plugin-vue' +// vite.config.ts +import { defineConfig } from 'vite' + +export default defineConfig({ + plugins: [vue(), ui()] +}) +``` + +```ts +import ui from '@nuxt/ui/vue-plugin' +// main.ts +import { createApp } from 'vue' +import App from './App.vue' +import './assets/main.css' + +const app = createApp(App) +app.use(ui) +app.mount('#app') +``` + +```css +/* assets/main.css */ +@import 'tailwindcss'; +@import '@nuxt/ui'; +``` + +**Critical**: Add `isolate` class to root for overlay stacking: + +```vue + + +``` + +### Auto-imports + +Vue generates `auto-imports.d.ts` and `components.d.ts`. Add to `.gitignore`: + +```gitignore +auto-imports.d.ts +components.d.ts +``` + +## Module Options + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + modules: ['@nuxt/ui'], + ui: { + prefix: 'U', // Component prefix (default 'U') + fonts: true, // Enable @nuxt/fonts + colorMode: true, // Enable @nuxtjs/color-mode + theme: { + colors: ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'], + transitions: true, // transition-colors on components + defaultVariants: { + color: 'primary', + size: 'md' + }, + prefix: '' // Tailwind CSS prefix (v4.2+) - ensures prefixed utilities work + }, + mdc: false, // Force Prose components + content: false, // Force UContent* components + experimental: { + componentDetection: false // Tree-shake unused components (v4.1+) - auto-generates CSS only for used components + } + } +}) +``` + +## Vue Vite Options + +```ts +// vite.config.ts +ui({ + prefix: 'U', + colorMode: true, + inertia: true, // Inertia.js support + theme: { + colors: ['primary', 'secondary', 'success', 'info', 'warning', 'error', 'neutral'], + transitions: true, + defaultVariants: { color: 'primary', size: 'md' }, + prefix: '' + }, + ui: { + colors: { primary: 'green' }, // Runtime color config + button: { /* theme overrides */ } + } +}) +``` + +## Auto-installed Modules + +Nuxt UI automatically installs: + +- `@nuxt/icon` - Icon system +- `@nuxt/fonts` - Web fonts (if `fonts: true`) +- `@nuxtjs/color-mode` - Dark mode (if `colorMode: true`) + +## Common Issues + +| Issue | Solution | +| ------------------------- | -------------------------------------------------- | +| Tailwind not found (pnpm) | Add `shamefully-hoist=true` or install tailwindcss | +| Overlays not showing | Wrap app in `` | +| Vue overlays broken | Add `isolate` class to root element | +| Icons not loading | Check @nuxt/icon is installed | +| Dark mode not working | Ensure `colorMode: true` in config | + +## Performance Features (v4.1+) + +### Component Virtualization + +Large datasets in CommandPalette, InputMenu, SelectMenu, Table, and Tree automatically use virtualization for better performance. + +### Experimental Component Detection + +Enable `experimental.componentDetection` to auto-generate CSS only for components you actually use: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + ui: { + experimental: { + componentDetection: true + } + } +}) +``` + +**Benefits:** Smaller CSS bundle, faster builds, reduced unused styles. + +### Tailwind CSS Prefix Support (v4.2+) + +Avoid style conflicts in complex apps: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + ui: { + theme: { + prefix: 'ui-' // Prefixes all Tailwind utilities + } + } +}) +``` + +**Result:** Components use `ui-bg-primary` instead of `bg-primary`. + +## Best Practices + +| Do | Don't | +| -------------------------- | ------------------------- | +| Wrap in UApp first | Forget UApp wrapper | +| Use semantic colors | Hardcode color values | +| Import CSS correctly | Skip @nuxt/ui import | +| Check pnpm hoisting | Ignore tailwindcss errors | +| Use component detection | Ship unused component CSS | +| Use prefix in complex apps | Risk style conflicts | diff --git a/.claude/skills/nuxt-ui/references/overlays.md b/.claude/skills/nuxt-ui/references/overlays.md new file mode 100644 index 0000000..3f5cc35 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/overlays.md @@ -0,0 +1,357 @@ +# Overlays + +**Prerequisite**: All overlays require `` wrapper in app root. + +**Important:** v4.4.0 includes security fixes for XSS vulnerabilities in Banner and CommandPalette components. Upgrade recommended. + +## Toast (Notifications) + +### Basic Usage + +```vue + + + +``` + +### Toast Options + +```ts +toast.add({ + id: 'unique-id', // Custom ID (auto-generated if omitted) + title: 'Title', // Toast title + description: 'Details', // Toast body + color: 'success', // primary, success, error, warning, info + icon: 'i-heroicons-check', // Left icon + avatar: { src: '...' }, // Avatar instead of icon + timeout: 5000, // Auto-dismiss (0 = never) + actions: [{ // Action buttons + label: 'Undo', + click: () => {} + }], + callback: () => {} // Called on dismiss +}) +``` + +### Toast Types + +```ts +// Success +toast.add({ title: 'Saved', color: 'success', icon: 'i-heroicons-check-circle' }) + +// Error +toast.add({ title: 'Error', color: 'error', icon: 'i-heroicons-x-circle' }) + +// Warning +toast.add({ title: 'Warning', color: 'warning', icon: 'i-heroicons-exclamation-triangle' }) + +// Info +toast.add({ title: 'Info', color: 'info', icon: 'i-heroicons-information-circle' }) + +// Remove toast +toast.remove('toast-id') + +// Clear all +toast.clear() +``` + +## Modal + +### Basic Modal + +```vue + + + +``` + +### Modal Props + +```vue + + description="Subtitle" + :close="true" + :close-icon="'i-heroicons-x-mark'" + :overlay="true" + :transition="true" + :prevent-close="false" + fullscreen +> +``` + +### Programmatic Modal (useOverlay) + +```vue + +``` + +## Slideover + +Side panel overlay (from edge of screen). + +```vue + + + +``` + +### Slideover Props + +```vue + + :overlay="true" + :transition="true" + :prevent-close="false" +> +``` + +## Drawer + +Bottom sheet overlay (vaul-vue). + +```vue + + + +``` + +### Drawer Props + +```vue + + :should-scale-background="true" + :close-threshold="0.25" +> +``` + +## Popover + +```vue + + Open Popover + + + +``` + +### Popover Props + +```vue + + align="center" + :arrow="true" + :delay="{ open: 0, close: 0 }" +> +``` + +## Tooltip + +```vue + + + + + + + Hover me + + +``` + +## DropdownMenu + +```vue + + + +``` + +### Nested Items + +```vue + +``` + +## ContextMenu + +Right-click menu. + +```vue + +
+ Right-click here +
+
+``` + +## CommandPalette + +Search-driven command menu (Fuse.js powered). + +```vue + + + +``` + +### CommandPalette Props (v4.4+) + +```vue + + :input="{ /* props */ }" +/> +``` + +### Keyboard Shortcut + +```vue + +``` + +## Best Practices + +| Do | Don't | +| -------------------------------------- | --------------------------- | +| Use useToast for notifications | Build custom toast systems | +| Use UModal for dialogs | Use alerts for complex UI | +| Use Slideover for panels | Use modals for side content | +| Use Drawer for mobile sheets | Use slideover on mobile | +| Use CommandPalette for search | Build custom search UI | +| Use programmatic overlays for confirms | Create confirm components | diff --git a/.claude/skills/nuxt-ui/references/theming.md b/.claude/skills/nuxt-ui/references/theming.md new file mode 100644 index 0000000..6bc8ac1 --- /dev/null +++ b/.claude/skills/nuxt-ui/references/theming.md @@ -0,0 +1,323 @@ +# Theming + +## Semantic Colors + +| Color | Default | Purpose | +| ----------- | ------- | ------------------------------------------- | +| `primary` | green | CTAs, active states, brand, important links | +| `secondary` | blue | Secondary buttons, alternatives | +| `success` | green | Success messages, positive states | +| `info` | blue | Info alerts, help text | +| `warning` | yellow | Warnings, pending states | +| `error` | red | Errors, destructive actions | +| `neutral` | slate | Text, borders, disabled states | + +## Configuring Colors + +### Nuxt (app.config.ts) + +```ts +// app.config.ts +export default defineAppConfig({ + ui: { + colors: { + primary: 'indigo', + secondary: 'violet', + success: 'emerald', + error: 'rose' + } + } +}) +``` + +### Vue (vite.config.ts) + +```ts +ui({ + ui: { + colors: { + primary: 'indigo', + secondary: 'violet' + } + } +}) +``` + +## Adding Custom Colors + +1. Register in theme config: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + ui: { + theme: { + colors: ['primary', 'secondary', 'tertiary'] // Add new color + } + } +}) +``` + +2. Define in CSS (all 11 shades required): + +```css +@theme { + --color-tertiary-50: #fef2f2; + --color-tertiary-100: #fee2e2; + --color-tertiary-200: #fecaca; + --color-tertiary-300: #fca5a5; + --color-tertiary-400: #f87171; + --color-tertiary-500: #ef4444; + --color-tertiary-600: #dc2626; + --color-tertiary-700: #b91c1c; + --color-tertiary-800: #991b1b; + --color-tertiary-900: #7f1d1d; + --color-tertiary-950: #450a0a; +} +``` + +3. Assign and use: + +```ts +// app.config.ts +export default defineAppConfig({ + ui: { colors: { tertiary: 'tertiary' } } +}) +``` + +```vue +Custom Color +``` + +## CSS Variables + +### Text Utilities + +| Class | Light | Dark | Use | +| ------------------ | ----- | ---- | ------------------------- | +| `text-dimmed` | 400 | 500 | Placeholders, hints | +| `text-muted` | 500 | 400 | Secondary text | +| `text-toned` | 600 | 300 | Subtitles | +| `text-default` | 700 | 200 | Body text | +| `text-highlighted` | 900 | 100 | Headings, emphasis | +| `text-inverted` | 50 | 950 | On dark/light backgrounds | + +### Background Utilities + +| Class | Light | Dark | Use | +| ------------- | ----- | ---- | ----------------- | +| `bg-default` | white | 900 | Page background | +| `bg-muted` | 50 | 800 | Subtle sections | +| `bg-elevated` | white | 800 | Cards, modals | +| `bg-accented` | 100 | 700 | Hover states | +| `bg-inverted` | 900 | 100 | Inverted sections | + +### Border Utilities + +| Class | Light | Dark | +| ----------------- | ----- | ---- | +| `border-default` | 200 | 800 | +| `border-muted` | 100 | 800 | +| `border-accented` | 200 | 700 | +| `border-inverted` | 900 | 100 | + +### Global Variables + +```css +:root { + --ui-radius: 0.25rem; /* Base border radius */ + --ui-container: 80rem; /* Container max-width */ + --ui-header-height: 4rem; /* Header height */ +} +``` + +## Custom CSS Variables + +```css +/* assets/css/main.css */ +:root { + --ui-primary: var(--ui-color-primary-700); + --ui-radius: 0.5rem; +} +.dark { + --ui-primary: var(--ui-color-primary-200); +} +``` + +## Solid Colors (Black/White) + +Can't use `primary: 'black'` - set in CSS: + +```css +:root { + --ui-primary: black; +} +.dark { + --ui-primary: white; +} +``` + +## Tailwind Variants Override + +### Global Override (app.config.ts) + +```ts +export default defineAppConfig({ + ui: { + button: { + slots: { + base: 'font-bold rounded-full' + }, + variants: { + size: { + md: { base: 'px-6 py-3' } + } + }, + compoundVariants: [{ + color: 'neutral', + variant: 'outline', + class: { base: 'ring-2' } + }], + defaultVariants: { + color: 'neutral', + variant: 'outline' + } + } + } +}) +``` + +### Per-Component Override + +```vue + +Custom + + +Square +``` + +## Matching Theme Structure in app.config + +**CRITICAL**: Components have two theme structure patterns. Your app.config MUST match the component's theme structure. + +### Pattern 1: Slots-Based Themes (Most Components) + +Components like Button, Card, Input, Select use `slots:` in their theme: + +```ts +// Component theme (Button, Card, etc.) +export default { + slots: { + base: '...', + root: '...', + icon: '...' + } +} +``` + +**app.config usage**: + +```ts +ui: { + button: { + slots: { base: 'font-bold' } // ✅ Match slots structure + } +} +``` + +### Pattern 2: Flat Base Themes (Container, Skeleton, etc.) + +Components like Container, Skeleton, Form, Main use flat `base:` in their theme: + +```ts +// Component theme (Container, Skeleton, etc.) +export default { + base: 'w-full max-w-container' +} +``` + +**app.config usage**: + +```ts +ui: { + container: { + base: 'max-w-lg' // ✅ Match flat structure + } +} +``` + +### Common Mistake + +```ts +// ❌ WRONG - Don't use slots for flat-base components +ui: { + container: { + slots: { base: 'max-w-lg' } // TypeScript error! + } +} + +// ❌ WRONG - Don't use flat for slots-based components +ui: { + button: { + base: 'font-bold' // Won't work correctly + } +} +``` + +### How to Check Component Structure + +1. Check component docs: https://ui.nuxt.com/components/[component] +2. Look at "Theme" section - shows the structure +3. Match that structure in your app.config + +## Component Theme Structure + +```ts +// Each component has slots, variants, compoundVariants, defaultVariants +export default { + slots: { + root: 'relative', + base: 'px-4 py-2', + icon: 'size-5' + }, + variants: { + color: { + primary: { base: 'bg-primary text-inverted' }, + neutral: { base: 'bg-neutral text-default' } + }, + size: { + sm: { base: 'text-sm', icon: 'size-4' }, + md: { base: 'text-base', icon: 'size-5' }, + lg: { base: 'text-lg', icon: 'size-6' } + } + }, + defaultVariants: { + color: 'primary', + size: 'md' + } +} +``` + +## Dark Mode + +Handled by `@nuxtjs/color-mode`. Access via: + +```ts +const colorMode = useColorMode() +colorMode.preference = 'dark' // 'light', 'dark', 'system' +``` + +```vue + + +``` + +## Best Practices + +| Do | Don't | +| -------------------------------------- | ----------------------------- | +| Use semantic colors | Hardcode hex values | +| Override via app.config | Modify source theme files | +| Use CSS variables for custom colors | Skip dark mode variants | +| Define all 11 shades for custom colors | Use partial shade definitions | diff --git a/.claude/skills/nuxt-ui/scripts/generate-components.ts b/.claude/skills/nuxt-ui/scripts/generate-components.ts new file mode 100644 index 0000000..2e562c8 --- /dev/null +++ b/.claude/skills/nuxt-ui/scripts/generate-components.ts @@ -0,0 +1,256 @@ +#!/usr/bin/env npx tsx +/** + * Generates nuxt-ui component docs from Nuxt UI repo (cloned to /tmp) + * Run: npx tsx skills/nuxt-ui/scripts/generate-components.ts + * + * Creates: + * - references/components.md (index with version column for Other category) + * - components/.md (per-component details) + */ + +import { execSync } from 'node:child_process' +import { mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { basename, dirname, join } from 'node:path' +import { fileURLToPath } from 'node:url' + +const TMP_DIR = join(tmpdir(), 'nuxt-ui-docs-gen') +const REPO_URL = 'https://github.com/nuxt/ui.git' +const DOCS_PATH = 'docs/content/docs/2.components' + +interface ComponentMeta { + name: string + description: string + category: string + rekaLink?: string + version?: string +} + +// Category groupings for better organization +const CATEGORIES: Record = { + element: 'Element', + form: 'Form', + data: 'Data', + navigation: 'Navigation', + overlay: 'Overlay', + layout: 'Layout', +} + +// Version mapping for components introduced after v4.0 +const VERSION_MAP: Record = { + 'empty': 'v4.1+', + 'scroll-area': 'v4.3+', + 'input-date': 'v4.2+', + 'input-time': 'v4.2+', + 'editor': 'v4.3+', + 'editor-drag-handle': 'v4.3+', + 'editor-emoji-menu': 'v4.3+', + 'editor-mention-menu': 'v4.3+', + 'editor-suggestion-menu': 'v4.3+', + 'editor-toolbar': 'v4.3+', +} + +function parseYamlFrontmatter(content: string): { frontmatter: Record, body: string } { + const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/) + if (!match) + return { frontmatter: {}, body: content } + + const frontmatter: Record = {} + const yamlContent = match[1] + + // Simple YAML parsing for our needs + for (const line of yamlContent.split('\n')) { + const colonIdx = line.indexOf(':') + if (colonIdx > 0 && !line.startsWith(' ') && !line.startsWith('-')) { + const key = line.slice(0, colonIdx).trim() + const value = line.slice(colonIdx + 1).trim() + frontmatter[key] = value.replace(/^['"]|['"]$/g, '') + } + } + + // Parse links for Reka UI reference + if (yamlContent.includes('reka-ui.com')) { + const rekaMatch = yamlContent.match(/to:\s*(https:\/\/reka-ui\.com[^\n]+)/) + if (rekaMatch) + frontmatter.rekaLink = rekaMatch[1] + } + + return { frontmatter, body: match[2] } +} + +function generateComponentFile(name: string, meta: ComponentMeta, body: string): string { + const lines: string[] = [] + const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('') + + lines.push(`# ${displayName}`) + lines.push('') + lines.push(meta.description) + lines.push('') + + if (meta.rekaLink) { + lines.push(`> Based on [Reka UI ${displayName}](${meta.rekaLink})`) + lines.push('') + } + + // Extract key props from body text + const propMentions = body.match(/Use the `(\w+)` prop/g) + if (propMentions && propMentions.length > 0) { + lines.push('## Key Props') + lines.push('') + const uniqueProps = [...new Set(propMentions.map(m => m.match(/`(\w+)`/)?.[1]).filter(Boolean))] + for (const prop of uniqueProps.slice(0, 10)) { + // Find the description after the prop mention + const propRegex = new RegExp(`Use the \`${prop}\` prop ([^.]+\\.?)`) + const desc = body.match(propRegex)?.[1] || '' + lines.push(`- \`${prop}\`: ${desc.replace(/to\s+$/, '').trim()}`) + } + lines.push('') + } + + // Add basic usage + lines.push('## Usage') + lines.push('') + lines.push('```vue') + lines.push(``) + lines.push(`/>`) + lines.push('```') + lines.push('') + + // Add slot info if present - look for slot mentions in text + const slotPattern = /`#(\w+)`\{?/g + const slotMatches = [...body.matchAll(slotPattern)] + if (slotMatches.length > 0) { + const validSlots = ['default', 'content', 'header', 'body', 'footer', 'title', 'description', 'leading', 'trailing', 'icon', 'label', 'close', 'trigger', 'actions', 'item', 'empty'] + const uniqueSlots = [...new Set(slotMatches.map(m => m[1]))] + .filter(s => validSlots.includes(s)) + if (uniqueSlots.length > 0) { + lines.push('## Slots') + lines.push('') + for (const slot of uniqueSlots.slice(0, 8)) { + lines.push(`- \`#${slot}\``) + } + lines.push('') + } + } + + return lines.join('\n') +} + +async function main() { + const __dirname = dirname(fileURLToPath(import.meta.url)) + const baseDir = join(__dirname, '..') + const componentsDir = join(baseDir, 'components') + + // Clean previous run and clone fresh + rmSync(TMP_DIR, { recursive: true, force: true }) + console.log('Cloning nuxt/ui (sparse checkout)...') + try { + execSync(`git clone --depth 1 --filter=blob:none --sparse ${REPO_URL} ${TMP_DIR}`, { stdio: 'inherit' }) + execSync(`git sparse-checkout set ${DOCS_PATH}`, { cwd: TMP_DIR, stdio: 'inherit' }) + } + catch { + console.error(`\nFailed to clone ${REPO_URL}. Check network/GitHub status.`) + process.exit(1) + } + + const NUXT_UI_DOCS = join(TMP_DIR, DOCS_PATH) + + mkdirSync(componentsDir, { recursive: true }) + + console.log('Generating Nuxt UI component docs...') + + const files = readdirSync(NUXT_UI_DOCS).filter(f => f.endsWith('.md') && f !== '0.index.md') + const components: ComponentMeta[] = [] + + for (const file of files) { + const name = basename(file, '.md') + const content = readFileSync(join(NUXT_UI_DOCS, file), 'utf-8') + const { frontmatter, body } = parseYamlFrontmatter(content) + + const meta: ComponentMeta = { + name, + description: frontmatter.description || '', + category: frontmatter.category || 'other', + rekaLink: frontmatter.rekaLink, + version: VERSION_MAP[name], + } + components.push(meta) + + // Generate component file + const componentContent = generateComponentFile(name, meta, body) + writeFileSync(join(componentsDir, `${name}.md`), componentContent) + console.log(`✓ Generated components/${name}.md`) + } + + // Generate index + const index: string[] = [] + index.push('# Components') + index.push('') + index.push('> Auto-generated from Nuxt UI docs. Run `npx tsx skills/nuxt-ui/scripts/generate-components.ts` to update.') + index.push('') + index.push('> **For headless primitives (API, accessibility):** see `reka-ui` skill') + index.push('') + + // Group by category + const byCategory: Record = {} + for (const comp of components) { + const cat = CATEGORIES[comp.category] || 'Other' + if (!byCategory[cat]) + byCategory[cat] = [] + byCategory[cat].push(comp) + } + + // Calculate column widths for alignment + function getMaxLengths(comps: ComponentMeta[], hasVersionCol: boolean) { + let maxComp = 'Component'.length + let maxDesc = 'Description'.length + for (const comp of comps) { + const displayName = comp.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('') + const link = `[${displayName}](components/${comp.name}.md)` + if (link.length > maxComp) + maxComp = link.length + const desc = hasVersionCol ? comp.description : (comp.version ? `${comp.description} (${comp.version})` : comp.description) + if (desc.length > maxDesc) + maxDesc = desc.length + } + return { maxComp, maxDesc } + } + + for (const [cat, comps] of Object.entries(byCategory).sort((a, b) => a[0].localeCompare(b[0]))) { + index.push(`## ${cat}`) + index.push('') + const hasVersionCol = cat === 'Other' + const { maxComp, maxDesc } = getMaxLengths(comps, hasVersionCol) + + if (hasVersionCol) { + index.push(`| ${'Component'.padEnd(maxComp)} | ${'Description'.padEnd(maxDesc)} | Version |`) + index.push(`| ${'-'.repeat(maxComp)} | ${'-'.repeat(maxDesc)} | ------- |`) + } + else { + index.push(`| ${'Component'.padEnd(maxComp)} | ${'Description'.padEnd(maxDesc)} |`) + index.push(`| ${'-'.repeat(maxComp)} | ${'-'.repeat(maxDesc)} |`) + } + + for (const comp of comps.sort((a, b) => a.name.localeCompare(b.name))) { + const displayName = comp.name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join('') + const link = `[${displayName}](components/${comp.name}.md)` + if (hasVersionCol) { + const desc = comp.version ? `${comp.description} (${comp.version})` : comp.description + index.push(`| ${link.padEnd(maxComp)} | ${desc.padEnd(maxDesc)} | ${(comp.version || '').padEnd(7)} |`) + } + else { + const desc = comp.version ? `${comp.description} (${comp.version})` : comp.description + index.push(`| ${link.padEnd(maxComp)} | ${desc.padEnd(maxDesc)} |`) + } + } + index.push('') + } + + writeFileSync(join(baseDir, 'references', 'components.md'), index.join('\n')) + console.log('✓ Generated references/components.md (index)') + + console.log(`\nDone! Generated ${components.length + 1} files.`) +} + +main().catch(console.error) diff --git a/.claude/skills/nuxt/SKILL.md b/.claude/skills/nuxt/SKILL.md new file mode 100644 index 0000000..992147d --- /dev/null +++ b/.claude/skills/nuxt/SKILL.md @@ -0,0 +1,98 @@ +--- +name: nuxt +description: Use when working on Nuxt 4+ projects - provides server routes, file-based routing, middleware patterns, Nuxt-specific composables, and configuration with latest docs. Covers h3 v1 helpers (validation, WebSocket, SSE) and nitropack v2 patterns. Updated for Nuxt 4.3+. +license: MIT +--- + +# Nuxt 4+ Development + +Progressive guidance for Nuxt 4+ projects (v4.3+) with latest patterns and conventions. + +## When to Use + +Working with: + +- Server routes (API endpoints, server middleware, server utils) +- File-based routing (pages, layouts, route groups) +- Nuxt middleware (route guards, navigation) +- Nuxt plugins (app extensions) +- Nuxt-specific features (auto-imports, layers, modules) + +## Available Guidance + +Read specific files based on current work: + +- **[references/server.md](references/server.md)** - API routes, server middleware, validation (Zod), WebSocket, SSE +- **[references/routing.md](references/routing.md)** - File-based routing, route groups, typed router, definePage +- **[references/middleware-plugins.md](references/middleware-plugins.md)** - Route middleware, plugins, app lifecycle +- **[references/nuxt-composables.md](references/nuxt-composables.md)** - Nuxt composables (useRequestURL, useFetch, navigation) +- **[references/nuxt-components.md](references/nuxt-components.md)** - NuxtLink, NuxtImg, NuxtTime (prefer over HTML elements) +- **[references/nuxt-config.md](references/nuxt-config.md)** - Configuration, modules, auto-imports, layers + +**For Vue composables:** See `vue` skill composables.md (VueUse, Composition API patterns) +**For UI components:** use `nuxt-ui` skill +**For database/storage:** use `nuxthub` skill +**For content-driven sites:** use `nuxt-content` skill +**For creating modules:** use `nuxt-modules` skill +**For project scaffolding/CI:** use `ts-library` skill + +## Loading Files + +**Consider loading these reference files based on your task:** + +- [ ] [references/server.md](references/server.md) - if creating API endpoints or server middleware +- [ ] [references/routing.md](references/routing.md) - if setting up pages, layouts, or route groups +- [ ] [references/nuxt-composables.md](references/nuxt-composables.md) - if using Nuxt composables (useFetch, useRequestURL, etc.) +- [ ] [references/middleware-plugins.md](references/middleware-plugins.md) - if working with middleware or plugins +- [ ] [references/nuxt-components.md](references/nuxt-components.md) - if using Nuxt components (NuxtLink, NuxtImg, etc.) +- [ ] [references/nuxt-config.md](references/nuxt-config.md) - if editing nuxt.config.ts +- [ ] [references/project-setup.md](references/project-setup.md) - if setting up CI/ESLint/build tools + +**DO NOT load all files at once.** Load only what's relevant to your current task. + +## Quick Start + +```ts +// server/api/hello.get.ts +import { z } from 'zod' + +export default defineEventHandler(async (event) => { + const { name } = await getValidatedQuery(event, z.object({ + name: z.string().default('world'), + }).parse) + return { message: `Hello ${name}` } +}) +``` + +## Nuxt 4 vs Older Versions + +**You are working with Nuxt 4+.** Key differences: + +| Old (Nuxt 2/3) | New (Nuxt 4) | +| ----------------- | ------------------------------- | +| `` | `` | +| `context.params` | `getRouterParam(event, 'name')` | +| `window.origin` | `useRequestURL().origin` | +| String routes | Typed router with route names | +| Separate layouts/ | Parent routes with `` | + +**If you're unsure about Nuxt 4 patterns, read the relevant guidance file first.** + +## Latest Documentation + +**When to fetch latest docs:** + +- New Nuxt 4 features not covered here +- Module-specific configuration +- Breaking changes or deprecations +- Advanced use cases + +**Official sources:** + +- Nuxt: https://nuxt.com/docs +- h3 (server engine): https://v1.h3.dev/ +- Nitro: https://nitro.build/ + +## Token Efficiency + +Main skill: ~300 tokens. Each sub-file: ~800-1500 tokens. Only load files relevant to current task. diff --git a/.claude/skills/nuxt/references/middleware-plugins.md b/.claude/skills/nuxt/references/middleware-plugins.md new file mode 100644 index 0000000..1c64c79 --- /dev/null +++ b/.claude/skills/nuxt/references/middleware-plugins.md @@ -0,0 +1,278 @@ +# Nuxt Middleware & Plugins + +## When to Use + +Working with `middleware/` or `plugins/` directories, route guards, app extensions. + +## Route Middleware + +Route middleware runs before navigation. Used for auth checks, redirects, logging. + +### Global Middleware + +Runs on every route change. **REQUIRED: Use `.global.ts` suffix:** + +```ts +// middleware/auth.global.ts +export default defineNuxtRouteMiddleware((to, from) => { + const auth = useAuthStore() + + if (to.meta.requiresAuth && !auth.isAuthenticated) { + return navigateTo('/login') + } +}) +``` + +**Without `.global.ts` suffix, middleware is named (not global).** + +## Red Flags - Stop and Check Skill + +If you're thinking any of these, STOP and re-read this skill: + +- "Suffix doesn't matter, it's about where I put it" +- "I'll redirect() instead of return navigateTo()" +- "I remember Nuxt 3 middleware patterns" +- "Export default function is simpler" + +All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead. + +### Named Middleware + +Runs only when explicitly applied. No `.global` suffix: + +```ts +// middleware/admin.ts +export default defineNuxtRouteMiddleware((to, from) => { + const auth = useAuthStore() + + if (!auth.isAdmin) { + return navigateTo('/') + } +}) +``` + +Apply in page: + +```vue + +``` + +### Middleware Return Values + +```ts +export default defineNuxtRouteMiddleware((to, from) => { + // Allow navigation + return + + // Redirect + return navigateTo('/login') + + // Abort navigation + return abortNavigation() + + // Abort with error + return abortNavigation('Not authorized') +}) +``` + +### Middleware Order + +1. Global middleware (alphabetical by filename) +2. Layout middleware (if layout defines middleware) +3. Page middleware (defined in definePageMeta) + +## Plugins + +Plugins extend Vue app with global functionality. Run during app initialization. + +### Basic Plugin + +```ts +// plugins/my-plugin.ts +export default defineNuxtPlugin((nuxtApp) => { + return { + provide: { + hello: (name: string) => `Hello ${name}!` + } + } +}) +``` + +Use in components: + +```vue + +``` + +### Plugin with Vue Plugin + +```ts +import type { PluginOptions } from 'vue-toastification' +// plugins/toast.client.ts +import Toast from 'vue-toastification' +import 'vue-toastification/dist/index.css' + +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.vueApp.use(Toast, { + position: 'top-right', + timeout: 3000 + } as PluginOptions) +}) +``` + +### Plugin with Hooks + +```ts +// plugins/init.ts +export default defineNuxtPlugin((nuxtApp) => { + nuxtApp.hook('app:created', () => { + console.log('App created') + }) + + nuxtApp.hook('page:finish', () => { + console.log('Page finished loading') + }) +}) +``` + +### Client-Only or Server-Only + +Use file suffix: + +- `.client.ts` - runs only on client +- `.server.ts` - runs only on server + +```ts +// plugins/analytics.client.ts +export default defineNuxtPlugin(() => { + // Only runs in browser + if (window.analytics) { + window.analytics.init() + } +}) +``` + +### Plugin Order + +Use numeric prefix for execution order: + +``` +plugins/ +├── 01.first.ts +├── 02.second.ts +└── 03.third.ts +``` + +### Async Plugins + +```ts +// plugins/api.ts +export default defineNuxtPlugin(async (nuxtApp) => { + const config = await fetch('/api/config').then(r => r.json()) + + return { + provide: { + config + } + } +}) +``` + +## Best Practices + +**Middleware:** + +- **Return navigation or nothing** - don't mutate state heavily +- **Keep logic minimal** - delegate to composables/stores +- **Use for guards & redirects** only +- **Check meta properly** - `to.meta.requiresAuth` +- **Global = `.global.ts`** suffix required + +**Plugins:** + +- **Use for app-wide functionality** only +- **Provide via `provide`** for type safety +- **Consider client/server context** - use `.client`/`.server` +- **Minimize work** in plugin initialization +- **Use hooks** for lifecycle events + +## Common Mistakes + +| ❌ Wrong | ✅ Right | +| ------------------------------------ | ------------------------------------------------------------ | +| `export default function({ route })` | `export default defineNuxtRouteMiddleware((to, from) => {})` | +| Mutate route object | Return navigateTo() or nothing | +| `middleware/auth.ts` (not global) | `middleware/auth.global.ts` (global) | +| `redirect('/login')` | `return navigateTo('/login')` | +| Plugin without defineNuxtPlugin | Wrap in defineNuxtPlugin() | + +## Middleware Example: Auth + +```ts +// middleware/auth.global.ts +export default defineNuxtRouteMiddleware((to, from) => { + const auth = useAuthStore() + + // Public routes + const publicRoutes = ['/', '/login', '/register'] + if (publicRoutes.includes(to.path)) { + return + } + + // Check auth + if (!auth.isAuthenticated) { + return navigateTo('/login') + } + + // Check role + if (to.meta.requiresAdmin && !auth.isAdmin) { + return abortNavigation('Access denied') + } +}) +``` + +## Plugin Example: API Client + +```ts +// plugins/api.ts +export default defineNuxtPlugin((nuxtApp) => { + const config = useRuntimeConfig() + + const api = $fetch.create({ + baseURL: config.public.apiBase, + onRequest({ request, options }) { + const auth = useAuthStore() + if (auth.token) { + options.headers = { + ...options.headers, + Authorization: `Bearer ${auth.token}` + } + } + }, + onResponseError({ response }) { + if (response.status === 401) { + navigateTo('/login') + } + } + }) + + return { + provide: { + api + } + } +}) +``` + +## Resources + +- Nuxt middleware: https://nuxt.com/docs/guide/directory-structure/middleware +- Nuxt plugins: https://nuxt.com/docs/guide/directory-structure/plugins +- Route middleware: https://nuxt.com/docs/getting-started/routing#route-middleware diff --git a/.claude/skills/nuxt/references/nuxt-components.md b/.claude/skills/nuxt/references/nuxt-components.md new file mode 100644 index 0000000..db620b2 --- /dev/null +++ b/.claude/skills/nuxt/references/nuxt-components.md @@ -0,0 +1,162 @@ +# Nuxt Built-in Components + +## When to Use + +Working with images, links, or time display in templates. **Always prefer Nuxt components over HTML elements.** + +## Component Preferences + +| HTML Element | Nuxt Component | Why | +| ------------ | -------------- | -------------------------------------- | +| `` | `` | Client-side navigation, prefetching | +| `` | `` | Optimization, lazy loading, responsive | +| `` for internal links:** + +```vue + +``` + +**Props:** + +- `to` - Route path or route object +- `external` - Force external link behavior +- `target` - Link target (`_blank`, etc.) +- `prefetch` - Enable/disable prefetching (default: true) +- `noPrefetch` - Disable prefetching +- `activeClass` - Class when route matches +- `exactActiveClass` - Class when route exactly matches + +**Docs:** https://nuxt.com/docs/api/components/nuxt-link + +## NuxtImg + +**ALWAYS use `` instead of `` for images:** + +Requires `@nuxt/image` module (usually pre-installed). + +```vue + +``` + +**Props:** + +- `src` - Image source path +- `alt` - Alt text (required for accessibility) +- `width` / `height` - Dimensions +- `sizes` - Responsive sizes +- `loading` - `lazy` (default) or `eager` +- `placeholder` - Show blur placeholder while loading +- `format` - Force output format (`webp`, `avif`, etc.) +- `quality` - Image quality (1-100) +- `provider` - Image provider (cloudinary, imgix, etc.) + +**For art direction, use `` (different sources per breakpoint).** + +**Docs:** https://image.nuxt.com/usage/nuxt-img + +## NuxtTime + +**ALWAYS use `` instead of `` | `` | +| `` | `` | +| `` | `` | +| `formatTimeAgo(date)` in template | `` | +| `new Date().toLocaleDateString()` | `` | + +## Best Practices + +- **NuxtLink for all internal routes** - enables prefetching and client-side navigation +- **NuxtImg for all images** - automatic optimization, lazy loading, responsive +- **NuxtTime for all dates** - SSR-safe, automatic localization +- **Always provide alt text** for images +- **Use `loading="eager"`** for above-the-fold images +- **Use sizes prop** for responsive images + +## Resources + +- NuxtLink: https://nuxt.com/docs/api/components/nuxt-link +- NuxtImg: https://image.nuxt.com/usage/nuxt-img +- NuxtPicture: https://image.nuxt.com/usage/nuxt-picture +- NuxtTime: https://nuxt.com/docs/api/components/nuxt-time diff --git a/.claude/skills/nuxt/references/nuxt-composables.md b/.claude/skills/nuxt/references/nuxt-composables.md new file mode 100644 index 0000000..46744f7 --- /dev/null +++ b/.claude/skills/nuxt/references/nuxt-composables.md @@ -0,0 +1,323 @@ +# Nuxt Composables & Utilities + +## When to Use + +Working with Nuxt-specific composables, URL handling, navigation, or data fetching. + +## URL & Request Handling + +### useRequestURL() + +**ALWAYS use `useRequestURL()` instead of `window.origin` or `window.location`:** + +```ts +// ✅ Correct - works SSR + client +const url = useRequestURL() +console.log(url.origin) // https://example.com +console.log(url.pathname) // /users/123 +console.log(url.search) // ?tab=profile + +// ❌ Wrong - breaks on SSR, not available server-side +const origin = window.origin +const path = window.location.pathname +``` + +**Why:** `window` is undefined during SSR. `useRequestURL()` works everywhere. + +### useRequestURL() Patterns + +```ts +// Get full URL +const url = useRequestURL() +const fullUrl = url.href // https://example.com/users/123?tab=profile + +// Get origin (base URL) +const baseUrl = url.origin // https://example.com + +// Get path +const path = url.pathname // /users/123 + +// Get query params (use useRoute() instead for better typing) +const params = url.searchParams +const tab = params.get('tab') // 'profile' + +// Build absolute URL +const apiUrl = `${url.origin}/api/users` +``` + +## Navigation Composables + +### navigateTo() + +```ts +// Navigate to route +await navigateTo('/about') + +// Type-safe navigation +await navigateTo({ name: '/users/[userId]', params: { userId: '123' } }) + +// External URL +await navigateTo('https://nuxt.com', { external: true }) + +// Replace history +await navigateTo('/login', { replace: true }) + +// Open in new tab +await navigateTo('/docs', { open: { target: '_blank' } }) + +// Server-side redirect +return navigateTo('/login') // in middleware or server route +``` + +### useRouter() + +```ts +const router = useRouter() + +// Navigate +router.push({ name: '/users/[userId]', params: { userId: '123' } }) + +// Go back +router.back() + +// Go forward +router.forward() + +// Navigation guards +router.beforeEach((to, from) => { + // Guard logic +}) +``` + +### useRoute() + +```ts +// Generic route +const route = useRoute() + +// Typed route (preferred) +const route = useRoute('/users/[userId]') + +// Access params +const userId = route.params.userId + +// Access query +const tab = route.query.tab + +// Access meta +const requiresAuth = route.meta.requiresAuth +``` + +## Data Fetching + +### useFetch() + +```ts +// Basic fetch +const { data, error, pending, refresh } = await useFetch('/api/users') + +// With params +const { data } = await useFetch('/api/users', { + query: { page: 1, limit: 10 } +}) + +// With key for deduplication +const { data } = await useFetch(`/api/users/${userId}`, { + key: `user-${userId}` +}) + +// Lazy fetch (doesn't block navigation) +const { data } = await useLazyFetch('/api/users') + +// Watch and refetch +const page = ref(1) +const { data } = await useFetch('/api/users', { + query: { page }, + watch: [page] +}) + +// Cancel requests with AbortController signal (Nuxt 4.2+) +const controller = new AbortController() +const { data } = await useFetch('/api/users', { + signal: controller.signal +}) +// Later: controller.abort() to cancel the request + +// Manual cancellation via execute/refresh +const { data, execute } = await useFetch('/api/users', { immediate: false }) +const abortController = new AbortController() +await execute({ signal: abortController.signal }) +// Later: abortController.abort() to cancel +``` + +### useAsyncData() + +```ts +// Custom async logic +const { data, error, pending, refresh } = await useAsyncData('users', async () => { + const response = await $fetch('/api/users') + return response.filter(u => u.active) +}) + +// Lazy version +const { data } = await useLazyAsyncData('users', async () => { + return await $fetch('/api/users') +}) + +// Cancel with AbortController (Nuxt 4.2+) +const controller = new AbortController() +const { data } = await useAsyncData('users', async () => { + return await $fetch('/api/users', { signal: controller.signal }) +}) +// Later: controller.abort() to cancel + +// Custom cache logic with getCachedData +const { data } = await useAsyncData('users', + async () => $fetch('/api/users'), + { + getCachedData: (key) => { + // Return cached data or null/undefined to trigger fetch + const cached = useNuxtData(key) + return cached.data.value + } + } +) + +// Deep reactivity for nested objects +// Default is shallow in Nuxt 4 (was deep in Nuxt 3) +const { data } = await useAsyncData('user', + async () => $fetch('/api/user'), + { + deep: true // Makes nested properties reactive + } +) + +// Deduplication strategies (Nuxt 4.2+) +const { data } = await useAsyncData('users', + async () => $fetch('/api/users'), + { + dedupe: 'cancel' // Cancel existing requests when new one starts + // dedupe: 'defer' // Prevent new requests while one is pending + } +) + +// Manual cancellation via execute/refresh +const { data, execute } = await useAsyncData('users', + async ({ signal }) => $fetch('/api/users', { signal }), + { immediate: false } +) +const abortController = new AbortController() +await execute({ signal: abortController.signal }) +// Later: abortController.abort() to cancel +``` + +## State Management + +### useState() + +```ts +// Create shared state +const counter = useState('counter', () => 0) + +// Use in components +counter.value++ + +// With type +const user = useState('user', () => null) +``` + +## App Context + +### useNuxtApp() + +```ts +const nuxtApp = useNuxtApp() + +// Access provided values +const { $api, $hello } = nuxtApp + +// Access hooks +nuxtApp.hook('page:finish', () => { + console.log('Page loaded') +}) + +// Access Vue app +nuxtApp.vueApp.use(SomePlugin) +``` + +### useRuntimeConfig() + +```ts +// Access runtime config +const config = useRuntimeConfig() + +// Public config (client + server) +const apiBase = config.public.apiBase + +// Private config (server only) +const apiSecret = config.apiSecret // undefined on client +``` + +## Head Management + +### useHead() + +```ts +// Set page meta +useHead({ + title: 'User Profile', + meta: [ + { name: 'description', content: 'View user profile' }, + { property: 'og:title', content: 'User Profile' } + ], + link: [ + { rel: 'canonical', href: 'https://example.com/profile' } + ] +}) + +// Dynamic values +const user = ref({ name: 'John' }) +useHead({ + title: () => `${user.value.name}'s Profile` +}) +``` + +### useSeoMeta() + +```ts +// Cleaner SEO meta +useSeoMeta({ + title: 'User Profile', + description: 'View user profile', + ogTitle: 'User Profile', + ogDescription: 'View user profile', + ogImage: 'https://example.com/image.jpg', + twitterCard: 'summary_large_image' +}) +``` + +## Best Practices + +- **Use useRequestURL()** NOT window.origin/location +- **Type routes** with useRoute('/path/[param]') +- **Use useFetch** for API calls (deduplication, SSR) +- **Key your fetches** for proper caching +- **useState for shared state** across components +- **useSeoMeta** for cleaner SEO tags + +## Common Mistakes + +| ❌ Wrong | ✅ Right | +| ---------------------------- | ----------------------------------------------------- | +| `window.origin` | `useRequestURL().origin` | +| `window.location.pathname` | `useRequestURL().pathname` | +| `fetch()` in components | `useFetch()` or `useAsyncData()` | +| `router.push('/path/' + id)` | `router.push({ name: '/path/[id]', params: { id } })` | +| Duplicate fetches | Use `key` parameter | + +## Resources + +- Nuxt composables: https://nuxt.com/docs/api/composables/use-fetch +- Data fetching: https://nuxt.com/docs/getting-started/data-fetching +- useRequestURL: https://nuxt.com/docs/api/composables/use-request-url +- **For NuxtTime, NuxtLink, NuxtImg:** See nuxt-components.md diff --git a/.claude/skills/nuxt/references/nuxt-config.md b/.claude/skills/nuxt/references/nuxt-config.md new file mode 100644 index 0000000..b905e18 --- /dev/null +++ b/.claude/skills/nuxt/references/nuxt-config.md @@ -0,0 +1,419 @@ +# Nuxt Configuration + +## When to Use + +Configuring `nuxt.config.ts`, modules, auto-imports, runtime config, layers. + +## Basic Structure + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + devtools: { enabled: true }, + + modules: [ + '@nuxtjs/tailwindcss', + '@pinia/nuxt' + ], + + runtimeConfig: { + // Private (server-only) + apiSecret: process.env.API_SECRET, + + public: { + // Public (client + server) + apiBase: process.env.API_BASE || 'http://localhost:3000' + } + }, + + app: { + head: { + title: 'My App', + meta: [ + { charset: 'utf-8' }, + { name: 'viewport', content: 'width=device-width, initial-scale=1' } + ] + } + } +}) +``` + +## Runtime Config + +Access runtime config in app: + +```ts +// Server-side +const config = useRuntimeConfig() +console.log(config.apiSecret) // Available + +// Client-side +const config = useRuntimeConfig() +console.log(config.public.apiBase) // Available +console.log(config.apiSecret) // undefined (private) +``` + +### Runtime Config Validation (Recommended) + +Use `nuxt-safe-runtime-config` for type-safe runtime config with build-time validation: + +```bash +npx nuxi module add nuxt-safe-runtime-config +``` + +**Benefits:** + +- Build-time validation (catches missing env vars early) +- Optional runtime validation (validates when server starts) +- Auto-generated types (no manual type definitions needed) +- No manual env var checks required (schema handles validation) + +**Example with Valibot:** + +```ts +import { number, object, optional, string } from 'valibot' + +export default defineNuxtConfig({ + modules: ['nuxt-safe-runtime-config'], + + runtimeConfig: { + databaseUrl: process.env.DATABASE_URL, + secretKey: process.env.SECRET_KEY, + port: Number.parseInt(process.env.PORT || '3000'), + public: { + apiBase: process.env.PUBLIC_API_BASE, + appName: 'My App', + }, + }, + + safeRuntimeConfig: { + $schema: object({ + public: object({ + apiBase: string(), + appName: optional(string()), + }), + databaseUrl: string(), + secretKey: string(), + port: optional(number()), + }), + validateAtRuntime: true, // Optional: validate when server starts + }, +}) +``` + +**Usage:** + +```ts +// Auto-typed from schema - no generics needed +const config = useSafeRuntimeConfig() +// config.public.apiBase is string +// config.databaseUrl is string +``` + +**No manual env checks needed:** + +```ts +// ❌ Don't do this with nuxt-safe-runtime-config +if (!config.databaseUrl) throw new Error('Missing DATABASE_URL') + +// ✅ Schema validation handles it automatically +// If env var is missing, build fails with detailed error +``` + +Works with Zod, ArkType, or any Standard Schema library. See: https://github.com/onmax/nuxt-safe-runtime-config + +## Auto-Imports + +Nuxt auto-imports from these directories: + +- `components/` - Vue components +- `composables/` - Composition functions +- `utils/` - Utility functions +- `server/utils/` - Server utilities (server-only) + +### Custom Auto-Imports + +```ts +export default defineNuxtConfig({ + imports: { + dirs: [ + 'stores', + 'types' + ] + } +}) +``` + +### Disable Auto-Import + +```ts +export default defineNuxtConfig({ + imports: { + autoImport: false + } +}) +``` + +## Modules + +```ts +export default defineNuxtConfig({ + modules: [ + '@nuxtjs/tailwindcss', + '@pinia/nuxt', + '@vueuse/nuxt', + ['@nuxtjs/google-fonts', { + families: { + Inter: [400, 700] + } + }] + ] +}) +``` + +## App Config + +For non-sensitive config exposed to client: + +```ts +// app.config.ts +export default defineAppConfig({ + theme: { + primaryColor: '#3b82f6', + borderRadius: '0.5rem' + } +}) +``` + +Access in app: + +```ts +const appConfig = useAppConfig() +console.log(appConfig.theme.primaryColor) +``` + +## TypeScript + +```ts +export default defineNuxtConfig({ + typescript: { + strict: true, + typeCheck: true, + shim: false + } +}) +``` + +## Build Configuration + +```ts +export default defineNuxtConfig({ + build: { + transpile: ['some-package'] + }, + + vite: { + css: { + preprocessorOptions: { + scss: { + additionalData: '@use "@/assets/styles/variables" as *;' + } + } + } + } +}) +``` + +## Route Rules + +Pre-render, cache, or customize routes: + +```ts +export default defineNuxtConfig({ + routeRules: { + '/': { prerender: true }, + '/api/**': { cors: true }, + '/admin/**': { ssr: false }, + '/blog/**': { swr: 3600 } // Cache for 1 hour + } +}) +``` + +### ISR Route Rules + +Use `isr` for incremental static regeneration: + +```ts +export default defineNuxtConfig({ + routeRules: { + '/': { prerender: true }, // Static at build time + '/**': { isr: 60 }, // Regenerate every 60s + '/package/**': { isr: 60 }, // ISR for dynamic routes + '/search': { isr: false, cache: false }, // No cache + } +}) +``` + +### Route Rule Layouts (Nuxt 4.3+) + +Apply layouts via route rules for centralized layout management: + +```ts +export default defineNuxtConfig({ + routeRules: { + '/admin/**': { appLayout: 'admin' }, + '/docs/**': { appLayout: 'docs' }, + '/': { appLayout: 'default' } + } +}) +``` + +**Benefits:** Centralized layout control, no need for `setPageLayout()` in every page. + +## Inline Modules + +Add conditional logic during nuxt prepare: + +```ts +export default defineNuxtConfig({ + modules: [ + // Inline function module + function (_, nuxt) { + if (nuxt.options._prepare) { + // Disable expensive operations during prepare + nuxt.options.pwa ||= {} + nuxt.options.pwa.pwaAssets ||= { disabled: true } + } + }, + '@nuxtjs/tailwindcss', + ] +}) +``` + +## Provider-Specific Modules + +Use `std-env` to detect platform and configure accordingly: + +```ts +// modules/vercel-cache.ts +import { defineNuxtModule } from 'nuxt/kit' +import { provider } from 'std-env' + +export default defineNuxtModule({ + meta: { name: 'vercel-cache' }, + setup(_, nuxt) { + if (provider !== 'vercel') return + + nuxt.hook('nitro:config', (nitroConfig) => { + nitroConfig.storage ||= {} + nitroConfig.storage.cache = { + driver: 'vercel-runtime-cache', + ...nitroConfig.storage.cache, + } + }) + } +}) +``` + +Then register in nuxt.config.ts: + +```ts +export default defineNuxtConfig({ + modules: ['~/modules/vercel-cache'] +}) +``` + +## Experimental Features + +```ts +export default defineNuxtConfig({ + future: { + compatibilityVersion: 4 + }, + + experimental: { + typedPages: true, + viewTransition: true, + payloadExtraction: true // Enable ISR/SWR payload extraction (Nuxt 4.3+) + } +}) +``` + +**Payload extraction** (Nuxt 4.3+): Enables cached payloads during client navigation for ISR/SWR routes, improving performance. + +## Nitro Config + +Server engine configuration: + +```ts +export default defineNuxtConfig({ + nitro: { + preset: 'vercel', + compressPublicAssets: true, + routeRules: { + '/api/**': { cors: true } + } + } +}) +``` + +## Layers + +Extend or share configuration: + +```ts +export default defineNuxtConfig({ + extends: [ + './base-layer' + ] +}) +``` + +## Environment Variables + +Use `.env` file: + +```env +API_SECRET=secret123 +API_BASE=https://api.example.com +``` + +Access via runtimeConfig: + +```ts +export default defineNuxtConfig({ + runtimeConfig: { + apiSecret: process.env.API_SECRET, + public: { + apiBase: process.env.API_BASE + } + } +}) +``` + +## Best Practices + +- **Use nuxt-safe-runtime-config** for runtime config with validation +- **Public vs private** - keep secrets in private runtimeConfig +- **App config** for non-sensitive client config +- **Route rules** for performance (prerender, cache, SWR) +- **Auto-imports** for cleaner code +- **TypeScript strict mode** for better DX + +## Common Mistakes + +| ❌ Wrong | ✅ Right | +| -------------------------- | ---------------------------- | +| Hardcoded API URLs | Use runtimeConfig.public | +| Secrets in app.config | Use runtimeConfig (private) | +| Import everything manually | Let Nuxt auto-import | +| process.env in client code | Use useRuntimeConfig() | +| Manual env var validation | Use nuxt-safe-runtime-config | +| if (!config.x) throw error | Schema validation handles it | + +## Resources + +- Nuxt config: https://nuxt.com/docs/api/nuxt-config +- Runtime config: https://nuxt.com/docs/guide/going-further/runtime-config +- App config: https://nuxt.com/docs/guide/directory-structure/app-config +- Modules: https://nuxt.com/modules diff --git a/.claude/skills/nuxt/references/project-setup.md b/.claude/skills/nuxt/references/project-setup.md new file mode 100644 index 0000000..9a3160d --- /dev/null +++ b/.claude/skills/nuxt/references/project-setup.md @@ -0,0 +1,107 @@ +# Project Setup + +Standard patterns for new Nuxt projects: CI, ESLint, package scripts. + +## CI Workflow + +```yaml +# .github/workflows/ci.yml +name: CI +on: [push, pull_request] + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: {node-version: 22, cache: pnpm} + - run: pnpm install --frozen-lockfile + - run: pnpm prepare + - run: pnpm lint + - run: pnpm typecheck + - run: pnpm test # if tests exist +``` + +**With env vars:** + +```yaml +env: + DATABASE_URL: postgresql://test:test@localhost:5432/test + API_KEY: test +``` + +## ESLint Config + +```js +// eslint.config.mjs +import antfu from '@antfu/eslint-config' +import withNuxt from './.nuxt/eslint.config.mjs' + +export default withNuxt( + antfu({ + formatters: true, + vue: true, + pnpm: true, + ignores: ['.eslintcache', 'cache/**', '.claude/**', 'README.md', 'docs/**'], + }), +) +``` + +**For monorepos, add:** + +```js +ignores: ['apps/web/.nuxt/**', 'packages/**/dist/**'] +``` + +## Package Scripts + +```json +{ + "scripts": { + "dev": "nuxt dev", + "build": "nuxt build", + "preview": "nuxt preview", + "prepare": "nuxt prepare", + "lint": "eslint . --cache", + "lint:fix": "eslint . --fix --cache", + "typecheck": "nuxt typecheck" + } +} +``` + +## Key Conventions + +| Convention | Standard | +| --------------- | ----------------------------------------------------- | +| Package manager | pnpm with `--frozen-lockfile` in CI | +| Node version | 22-24 | +| ESLint base | @antfu/eslint-config | +| Formatter | Via ESLint (`formatters: true`), no separate Prettier | +| Cache | `--cache` flag on lint scripts | +| Prepare step | Required before lint/typecheck in CI | + +## NuxtHub Deployment + +```yaml +# .github/workflows/nuxthub.yml +name: Deploy to NuxtHub +on: push + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: {contents: read, id-token: write} + steps: + - uses: actions/checkout@v4 + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v4 + with: {node-version: 22, cache: pnpm} + - run: pnpm install + - uses: nuxt-hub/action@v2 + with: + project-key: your-project-key +``` + +> **For pnpm catalogs, release workflows, tsconfig patterns:** see `ts-library` skill diff --git a/.claude/skills/nuxt/references/routing.md b/.claude/skills/nuxt/references/routing.md new file mode 100644 index 0000000..6b648ad --- /dev/null +++ b/.claude/skills/nuxt/references/routing.md @@ -0,0 +1,242 @@ +# Nuxt File-Based Routing + +## When to Use + +Working with `pages/` or `layouts/` directories, file-based routing, navigation. + +## File-Based Routing Basics + +`pages/` folder structure directly maps to routes. File names determine URLs. + +## Naming Conventions + +**Key principles:** + +- **ALWAYS use descriptive params:** `[userId].vue` NOT `[id].vue` +- **Optional params:** `[[paramName]].vue` +- **Catch-all:** `[...path].vue` +- **Route groups for organization:** `(folder)/` groups files without affecting URLs + +## Red Flags - Stop and Check Skill + +If you're thinking any of these, STOP and re-read this skill: + +- "String paths are simpler than typed routes" +- "Generic param names like [id] are fine" +- "I remember how Nuxt 3 worked" + +All of these mean: You're about to use outdated patterns. Use Nuxt 4 patterns instead. + +## File Structure Example + +``` +pages/ +├── index.vue # / +├── about.vue # /about +├── [...slug].vue # catch-all for 404 +├── users.vue # parent route (layout for /users/*) +└── users/ + ├── index.vue # /users + └── [userId].vue # /users/:userId +``` + +## Route Groups for Organization + +Route groups organize files WITHOUT affecting URLs. Wrap folder names in parentheses: + +``` +pages/ +├── (marketing)/ # group folder (ignored in URL) +│ ├── about.vue # /about (not /marketing/about) +│ └── pricing.vue # /pricing +└── (admin)/ # group folder (ignored in URL) + ├── dashboard.vue # /dashboard + └── settings.vue # /settings +``` + +**Use route groups to:** + +- Organize pages by feature/team +- Group related routes without affecting URLs +- Keep large projects maintainable +- Apply middleware to specific groups (via `route.meta.groups`) + +**Access route groups in middleware:** + +```ts +// middleware/auth.global.ts +export default defineNuxtRouteMiddleware((to) => { + // Check if route is in admin group + if (to.meta.groups?.includes('admin')) { + const auth = useAuthStore() + if (!auth.isAdmin) return navigateTo('/') + } +}) +``` + +## Parent Routes (Layouts) + +Parent route = layout for nested routes: + +```vue + + +``` + +Child routes: + +``` +pages/ +├── users.vue # Parent route with +└── users/ + ├── index.vue # /users + ├── [userId].vue # /users/:userId + └── create.vue # /users/create +``` + +## definePage() for Route Customization + +```vue + + + +``` + +## Typed Router + +**ALWAYS use typed routes for navigation:** + +```ts +// ✅ Type-safe with route name +await navigateTo({ name: '/users/[userId]', params: { userId: '123' } }) + +// ❌ String-based (not type-safe, avoid) +await navigateTo('/users/123') +``` + +**REQUIRED: Check `typed-router.d.ts` for available route names and params before navigating.** + +## useRoute with Types + +Pass route name for stricter typing: + +```ts +// Generic route +const route = useRoute() + +// Typed route (preferred) +const route = useRoute('/users/[userId]') +// route.params.userId is now typed correctly +``` + +## Navigation + +```ts +// Navigate to route +await navigateTo('/about') +await navigateTo({ name: '/users/[userId]', params: { userId: '123' } }) + +// Navigate with query +await navigateTo({ path: '/search', query: { q: 'nuxt' } }) + +// External redirect +await navigateTo('https://nuxt.com', { external: true }) + +// Replace history +await navigateTo('/login', { replace: true }) + +// Open in new tab +await navigateTo('/docs', { open: { target: '_blank' } }) +``` + +## Route Meta & Middleware + +```vue + +``` + +## Dynamic Layout Switching + +Use `setPageLayout()` to switch layouts programmatically: + +```vue + +``` + +## Dynamic Routes Patterns + +``` +[userId].vue # /users/123 +[[slug]].vue # /blog or /blog/post (optional) +[...path].vue # /a/b/c (catch-all) +[[...path]].vue # / or /a/b/c (optional catch-all) +``` + +## Best Practices + +- **`index.vue` for index routes** - valid and correct for creating default routes +- **Route groups `(folder)/` for organization** - group files without affecting URLs +- **Descriptive param names** - `[userId]` not `[id]`, `[postSlug]` not `[slug]` +- **Type-safe navigation** - use route names, not strings +- **Check typed-router.d.ts** for available routes +- **Parent routes for layouts** - `users.vue` with `` +- **Use definePage** for custom paths/aliases +- **Catch-all for 404** - `[...path].vue` or `[...slug].vue` + +## Common Mistakes + +| ❌ Wrong | ✅ Right | +| ---------------------------- | ----------------------------------------------------------------- | +| `[id].vue` | `[userId].vue` or `[postId].vue` | +| `navigateTo('/users/' + id)` | `navigateTo({ name: '/users/[userId]', params: { userId: id } })` | +| `` | `` | +| Separate layouts/ folder | Parent routes with `` | + +## Resources + +- Nuxt routing: https://nuxt.com/docs/guide/directory-structure/pages +- File-based routing: https://nuxt.com/docs/getting-started/routing diff --git a/.claude/skills/nuxt/references/server.md b/.claude/skills/nuxt/references/server.md new file mode 100644 index 0000000..eeb7763 --- /dev/null +++ b/.claude/skills/nuxt/references/server.md @@ -0,0 +1,451 @@ +# Nuxt Server Patterns + +> **Versions:** Nuxt uses h3 v1 and nitropack v2. Patterns from h3 v2 or nitro v3 docs won't work. + +## When to Use + +Working with `server/` directory - API routes, server middleware, server utilities. + +## Server Directory Structure + +``` +server/ +├── api/ # API endpoints +│ ├── users.get.ts # GET /api/users +│ ├── users.post.ts # POST /api/users +│ └── users/ +│ └── [id].get.ts # GET /api/users/:id +├── routes/ # Non-API routes +│ └── healthz.get.ts # GET /healthz +├── middleware/ # Server middleware +│ └── log.ts +└── utils/ # Server utilities (auto-imported) + └── db.ts +``` + +## API Routes + +File naming determines HTTP method and route: + +- `users.get.ts` → GET /api/users +- `users.post.ts` → POST /api/users +- `users/[userId].get.ts` → GET /api/users/:userId +- `users/[userId].delete.ts` → DELETE /api/users/:userId + +**REQUIRED: Use descriptive param names:** `[userId].get.ts` NOT `[id].get.ts` + +## Red Flags - Stop and Check Skill + +If you're thinking any of these, STOP and re-read this skill: + +- "I'll use event.context.params like before" +- "Generic [id] is fine for params" +- "Don't need .get.ts suffix" +- "I remember how Nuxt 3 API routes worked" + +All of these mean: You're using outdated patterns. Use Nuxt 4 patterns instead. + +### Basic API Route + +```ts +// server/api/users.get.ts +export default defineEventHandler(async (event) => { + const users = await fetchUsers() + return users +}) +``` + +### Route with Params + +```ts +// server/api/users/[userId].get.ts +export default defineEventHandler(async (event) => { + const userId = getRouterParam(event, 'userId') + + if (!userId) { + throw createError({ + statusCode: 400, + message: 'User ID is required' + }) + } + + const user = await fetchUserById(userId) + + if (!user) { + throw createError({ + statusCode: 404, + message: 'User not found' + }) + } + + return user +}) +``` + +### Route with Query Params + +```ts +// server/api/users.get.ts +export default defineEventHandler(async (event) => { + const query = getQuery(event) + const page = Number(query.page) || 1 + const limit = Number(query.limit) || 10 + + const users = await fetchUsers({ page, limit }) + return users +}) +``` + +### Route with Body + +```ts +// server/api/users.post.ts +export default defineEventHandler(async (event) => { + const body = await readBody(event) + + // Validate body + if (!body.name || !body.email) { + throw createError({ + statusCode: 400, + message: 'Missing required fields: name, email' + }) + } + + const user = await createUser(body) + setResponseStatus(event, 201) + return user +}) +``` + +### Validation with Valibot + +Use `readValidatedBody` and `getValidatedQuery` for schema validation: + +```ts +// server/api/users.post.ts +import * as v from 'valibot' + +const UserSchema = v.object({ + name: v.pipe(v.string(), v.minLength(1)), + email: v.pipe(v.string(), v.email()) +}) + +export default defineEventHandler(async (event) => { + const body = await readValidatedBody(event, v.parser(UserSchema)) + // body is typed as { name: string, email: string } + const user = await createUser(body) + setResponseStatus(event, 201) + return user +}) +``` + +```ts +// server/api/users.get.ts +import * as v from 'valibot' + +const QuerySchema = v.object({ + page: v.optional(v.pipe(v.string(), v.transform(Number)), '1'), + limit: v.optional(v.pipe(v.string(), v.transform(Number)), '10') +}) + +export default defineEventHandler(async (event) => { + const { page, limit } = await getValidatedQuery(event, v.parser(QuerySchema)) + return fetchUsers({ page, limit }) +}) +``` + +## Error Handling + +Use `createError` for HTTP errors: + +```ts +throw createError({ + statusCode: 400, + statusMessage: 'Bad Request', + message: 'Invalid input', + data: { field: 'email' } // Optional additional data +}) +``` + +## Server Middleware + +Runs on every server request: + +```ts +// server/middleware/log.ts +export default defineEventHandler((event) => { + console.log(`${event.method} ${event.path}`) +}) +``` + +Named middleware for specific patterns: + +```ts +// server/middleware/auth.ts +export default defineEventHandler((event) => { + const token = getRequestHeader(event, 'authorization') + + if (!token) { + throw createError({ + statusCode: 401, + message: 'Unauthorized' + }) + } + + // Attach user to event context + event.context.user = await verifyToken(token) +}) +``` + +## Server Utils + +Reusable server functions (auto-imported): + +```ts +// server/utils/db.ts +import { db } from './database' + +export async function fetchUsers(options: { page: number, limit: number }) { + return await db.select().from('users').limit(options.limit).offset((options.page - 1) * options.limit) +} + +export async function fetchUserById(id: string) { + return await db.select().from('users').where({ id }).first() +} +``` + +Auto-imported in all server routes and middleware. + +**Import server utils from client (Nuxt 4.3+):** + +```ts +// Use #server alias for type-safe server-only imports +import type { User } from '#server/utils/db' +``` + +**Note:** Only types are imported; actual server code never bundles into client. + +## Cached Functions + +Use `defineCachedFunction` for caching expensive operations in server utils: + +```ts +// server/utils/github.ts +export const fetchRepo = defineCachedFunction( + async (owner: string, repo: string) => { + return await $fetch(`https://api.github.com/repos/${owner}/${repo}`) + }, + { + maxAge: 60 * 5, // Cache for 5 minutes + swr: true, // Stale-while-revalidate + name: 'github-repo', + getKey: (owner, repo) => `${owner}/${repo}`, + } +) +``` + +## Cached Event Handlers + +Use `defineCachedEventHandler` for ISR-style caching on API routes: + +```ts +// server/api/products/[productId].get.ts +export default defineCachedEventHandler( + async (event) => { + const productId = getRouterParam(event, 'productId') + return await fetchProductById(productId) + }, + { + maxAge: 3600, // Cache for 1 hour + swr: true, // Serve stale while revalidating + getKey: event => getRouterParam(event, 'productId') ?? '', + } +) +``` + +## Generic Error Handler + +Centralize error handling for H3 errors, validation errors, and fallbacks: + +```ts +// server/utils/error-handler.ts +import { isError, createError } from 'h3' +import * as v from 'valibot' + +export function handleApiError(error: unknown, fallback: { statusCode?: number, message: string }): never { + // Re-throw existing H3 errors + if (isError(error)) throw error + + // Handle Valibot validation errors + if (v.isValiError(error)) { + throw createError({ statusCode: 400, message: error.issues[0].message }) + } + + // Generic fallback + throw createError({ statusCode: fallback.statusCode ?? 502, message: fallback.message }) +} +``` + +Usage in routes: + +```ts +export default defineEventHandler(async (event) => { + try { + const data = await fetchExternalApi() + return data + } catch (error) { + handleApiError(error, { statusCode: 502, message: 'Failed to fetch data' }) + } +}) +``` + +## Request Helpers + +```ts +// Get params +const userId = getRouterParam(event, 'userId') + +// Get query +const query = getQuery(event) + +// Get body +const body = await readBody(event) + +// Get headers +const auth = getRequestHeader(event, 'authorization') + +// Get cookies +const token = getCookie(event, 'token') + +// Get method +const method = getMethod(event) + +// Get IP +const ip = getRequestIP(event) +``` + +## Response Helpers + +```ts +// Set status code +setResponseStatus(event, 201) + +// Set headers +setResponseHeader(event, 'X-Custom', 'value') +setResponseHeaders(event, { 'X-Custom': 'value', 'X-Another': 'value' }) + +// Set cookies +setCookie(event, 'token', 'value', { + httpOnly: true, + secure: true, + sameSite: 'lax', + maxAge: 60 * 60 * 24 * 7 // 1 week +}) + +// Redirect +return sendRedirect(event, '/login', 302) + +// Stream +return sendStream(event, stream) + +// No content +return sendNoContent(event) +``` + +## Background Tasks + +Use `event.waitUntil()` for async tasks that shouldn't block the response (Nuxt 4+): + +```ts +// server/api/analytics.post.ts +export default defineEventHandler(async (event) => { + const data = await readBody(event) + + // Don't block response with analytics logging + event.waitUntil( + logAnalytics(data) + ) + + return { success: true } +}) +``` + +**Use cases:** logging, caching, background processing, async cleanup. + +## Best Practices + +- **Use descriptive param names** - `[userId]` not `[id]` +- **Keep routes thin** - delegate to server utils +- **Validate input** at route level +- **Use typed errors** with createError +- **Handle errors gracefully** - don't expose internals +- **Use server utils** for DB/external APIs +- **Don't expose sensitive data** in responses +- **Set proper status codes** - 201 for created, 204 for no content +- **Use event.waitUntil()** for background tasks that shouldn't block responses + +## Common Mistakes + +| ❌ Wrong | ✅ Right | +| ------------------------- | ----------------------------- | +| `event.context.params.id` | `getRouterParam(event, 'id')` | +| `return res.json(data)` | `return data` | +| `[id].get.ts` | `[userId].get.ts` | +| `users-id.get.ts` | `users/[id].get.ts` | +| Throw generic errors | Use createError with status | + +## WebSocket + +```ts +// server/routes/_ws.ts +export default defineWebSocketHandler({ + open(peer) { + console.log('Client connected:', peer.id) + }, + message(peer, message) { + peer.send(`Echo: ${message.text()}`) + // Broadcast to all: peer.publish('channel', message) + }, + close(peer) { + console.log('Client disconnected:', peer.id) + } +}) +``` + +Enable in config: + +```ts +// nuxt.config.ts +export default defineNuxtConfig({ + nitro: { + experimental: { websocket: true } + } +}) +``` + +## Server-Sent Events (Experimental) + +```ts +// server/api/stream.get.ts +export default defineEventHandler(async (event) => { + const stream = createEventStream(event) + + const interval = setInterval(async () => { + await stream.push({ data: JSON.stringify({ time: Date.now() }) }) + }, 1000) + + stream.onClosed(() => { + clearInterval(interval) + }) + + return stream.send() +}) +``` + +## Resources + +- Nuxt server: https://nuxt.com/docs/guide/directory-structure/server +- h3 (Nitro engine): https://v1.h3.dev/ +- Nitro: https://nitro.build/ + +> **For database/storage APIs:** see `nuxthub` skill diff --git a/.claude/skills/vue/SKILL.md b/.claude/skills/vue/SKILL.md new file mode 100644 index 0000000..bf315fd --- /dev/null +++ b/.claude/skills/vue/SKILL.md @@ -0,0 +1,103 @@ +--- +name: vue +description: Use when editing .vue files, creating Vue 3 components, writing composables, or testing Vue code - provides Composition API patterns, props/emits best practices, VueUse integration, and reactive destructuring guidance +license: MIT +--- + +# Vue 3 Development + +Reference for Vue 3 Composition API patterns, component architecture, and testing practices. + +**Current stable:** Vue 3.5+ with enhanced reactivity performance (-56% memory, 10x faster array tracking), new SSR features, and improved developer experience. + +## Overview + +Progressive reference system for Vue 3 projects. Load only files relevant to current task to minimize context usage (~250 tokens base, 500-1500 per sub-file). + +## When to Use + +**Use this skill when:** + +- Writing `.vue` components +- Creating composables (`use*` functions) +- Building client-side utilities +- Testing Vue components/composables + +**Use `nuxt` skill instead for:** + +- Server routes, API endpoints +- File-based routing, middleware +- Nuxt-specific patterns + +**For styled UI components:** use `nuxt-ui` skill +**For headless accessible components:** use `reka-ui` skill +**For VueUse composables:** use `vueuse` skill + +## Quick Reference + +| Working on... | Load file | +| ------------------------ | ---------------------------- | +| `.vue` in `components/` | references/components.md | +| File in `composables/` | references/composables.md | +| File in `utils/` | references/utils-client.md | +| `.spec.ts` or `.test.ts` | references/testing.md | +| TypeScript patterns | references/typescript.md | +| Vue Router typing | references/router.md | +| Reactivity (ref, watch) | references/reactivity.md | +| Custom directives | references/directives.md | +| Provide/inject | references/provide-inject.md | +| Edge cases, vue-tsc | references/gotchas.md | + +## Loading Files + +**Consider loading these reference files based on your task:** + +- [ ] [references/components.md](references/components.md) - if working in `components/` or writing `.vue` files +- [ ] [references/composables.md](references/composables.md) - if creating composables (`use*` functions) +- [ ] [references/utils-client.md](references/utils-client.md) - if working in `utils/` or writing client utilities +- [ ] [references/testing.md](references/testing.md) - if writing `.spec.ts` or `.test.ts` files +- [ ] [references/typescript.md](references/typescript.md) - if working with Vue TypeScript patterns or generics +- [ ] [references/router.md](references/router.md) - if working with Vue Router or route typing +- [ ] [references/reactivity.md](references/reactivity.md) - if using ref, reactive, computed, watch, or watchEffect +- [ ] [references/directives.md](references/directives.md) - if creating or using custom directives +- [ ] [references/provide-inject.md](references/provide-inject.md) - if using provide/inject patterns +- [ ] [references/gotchas.md](references/gotchas.md) - if debugging edge cases or hydration issues + +**DO NOT load all files at once.** Load only what's relevant to your current task. + +## Quick Start + +```vue + + + +``` + +## Available Guidance + +**[references/components.md](references/components.md)** - Props with reactive destructuring, emits patterns, defineModel for v-model, slots shorthand + +**[references/composables.md](references/composables.md)** - Composition API structure, VueUse integration, lifecycle hooks, async patterns, reactivity gotchas + +**[references/utils-client.md](references/utils-client.md)** - Pure functions, formatters, validators, transformers, when NOT to use utils + +**[references/testing.md](references/testing.md)** - Vitest + @vue/test-utils, component testing, composable testing, router mocking + +**[references/typescript.md](references/typescript.md)** - InjectionKey for provide/inject, vue-tsc strict templates, tsconfig settings, generic components + +**[references/router.md](references/router.md)** - Route meta types, typed params with unplugin-vue-router, scroll behavior, navigation guards + +**[references/reactivity.md](references/reactivity.md)** - ref, reactive, computed, watch, watchEffect, reactivity fundamentals + +**[references/directives.md](references/directives.md)** - Custom directive hooks, v-focus, v-click-outside, v-tooltip patterns + +**[references/provide-inject.md](references/provide-inject.md)** - InjectionKey typing, app-level provide, readonly patterns + +**[references/gotchas.md](references/gotchas.md)** - Common gotchas, vue-tsc edge cases, hydration issues, race conditions (from vuejs-ai/skills) diff --git a/.claude/skills/vue/references/components.md b/.claude/skills/vue/references/components.md new file mode 100644 index 0000000..f0f6cb8 --- /dev/null +++ b/.claude/skills/vue/references/components.md @@ -0,0 +1,323 @@ +# Vue Components + +Patterns for Vue 3 components using Composition API with ` + + +``` + +### With Options + +```vue + +``` + +**⚠️ Warning:** When using `default` without parent providing a value, parent and child can de-sync (parent `undefined`, child has default). Always provide matching defaults in parent or make prop required. + +**Prevent double-emit with `required: true`:** + +```ts +// ❌ Without required - emits twice (undefined then value) +const model = defineModel() + +// ✅ With required - single emit +const model = defineModel({ required: true }) +``` + +Use `required: true` when the model should always have a value to avoid the double-emit issue during initialization. + +### Multiple Models + +Default assumes `modelValue` prop. For multiple bindings, use explicit names: + +```vue + + + + +``` + +[v-model modifiers docs](https://vuejs.org/guide/components/v-model#handling-v-model-modifiers) + +## Reusable Templates + +For typed, scoped template snippets within a component: + +```vue + + + +``` + +## Template Refs (Vue 3.5+) + +Use `useTemplateRef()` for type-safe template references with IDE support: + +```vue + + + +``` + +**Benefits over `ref()`:** + +- Type-safe with generics +- Better IDE autocomplete and refactoring +- Explicit ref name as string literal + +**Dynamic refs:** + +```vue + + + +``` + +**Component refs with generics:** + +For generic components, use `ComponentExposed` from `vue-component-type-helpers`: + +```ts +import type { ComponentExposed } from 'vue-component-type-helpers' +import MyGenericComponent from './MyGenericComponent.vue' + +// Get exposed methods/properties with correct generic types +const compRef = useTemplateRef>('comp') + +onMounted(() => { + compRef.value?.someExposedMethod() // Typed! +}) +``` + +Install: `pnpm add -D vue-component-type-helpers` + +## SSR Hydration (Vue 3.5+) + +**Suppress hydration mismatches** for values that differ between server/client: + +```vue + +``` + +**Generate SSR-stable IDs:** + +```vue + + + +``` + +## Deferred Teleport (Vue 3.5+) + +Teleport to elements rendered later in the same cycle: + +```vue + +``` + +Without `defer`, teleport to `#late-div` would fail since it doesn't exist yet. + +## Common Mistakes + +**Using `const props =` with destructured values:** + +```ts +// ❌ Wrong +const props = defineProps<{ count: number }>() +const { count } = props // Loses reactivity +``` + +**Forgetting TypeScript types:** + +```ts +// ❌ Wrong +const emit = defineEmits(['update']) + +// ✅ Correct +const emit = defineEmits<{ update: [id: number] }>() +``` + +**Components >300 lines:** Split into smaller components or extract logic to composables diff --git a/.claude/skills/vue/references/composables.md b/.claude/skills/vue/references/composables.md new file mode 100644 index 0000000..3a726a9 --- /dev/null +++ b/.claude/skills/vue/references/composables.md @@ -0,0 +1,358 @@ +# Vue Composables + +Reusable functions encapsulating stateful logic using Composition API. + +## Core Rules + +1. **VueUse first** - check [vueuse.org](https://vueuse.org) before writing custom +2. **No async composables** - lose lifecycle context when awaited in other composables +3. **Top-level only** - never call in event handlers, conditionals, or loops +4. **readonly() exports** - protect internal state from external mutation +5. **useState() for SSR** - use Nuxt's `useState()` not global refs + +## Quick Reference + +| Pattern | Example | +| --------- | ------------------------------------------------ | +| Naming | `useAuth`, `useCounter`, `useDebounce` | +| State | `const count = ref(0)` | +| Computed | `const double = computed(() => count.value * 2)` | +| Lifecycle | `onMounted(() => ...)`, `onUnmounted(() => ...)` | +| Return | `return { count, increment }` | + +## Structure + +```ts +// composables/useCounter.ts +import { readonly, ref } from 'vue' + +export function useCounter(initialValue = 0) { + const count = ref(initialValue) + + function increment() { count.value++ } + function decrement() { count.value-- } + function reset() { count.value = initialValue } + + return { + count: readonly(count), // readonly if shouldn't be mutated + increment, + decrement, + reset, + } +} +``` + +## Naming + +**Always prefix with `use`:** `useAuth`, `useLocalStorage`, `useDebounce` + +**File = function:** `useAuth.ts` exports `useAuth` + +## Best Practices + +**Do:** + +- Return object with named properties (destructuring-friendly) +- Accept options object for configuration +- Use `readonly()` for state that shouldn't mutate +- Handle cleanup (`onUnmounted`, `onScopeDispose`) +- Add JSDoc for complex functions + +## Lifecycle + +Hooks execute in component context: + +```ts +export function useEventListener(target: EventTarget, event: string, handler: Function) { + onMounted(() => target.addEventListener(event, handler)) + onUnmounted(() => target.removeEventListener(event, handler)) +} +``` + +**Watcher cleanup (Vue 3.5+):** + +```ts +import { watch, onWatcherCleanup } from 'vue' + +export function usePolling(url: Ref) { + watch(url, (newUrl) => { + const interval = setInterval(() => { + fetch(newUrl).then(/* ... */) + }, 1000) + + // Cleanup when watcher re-runs or stops + onWatcherCleanup(() => { + clearInterval(interval) + }) + }) +} +``` + +**Benefits of `onWatcherCleanup()`:** + +- Cleaner than returning cleanup functions +- Works with async watchers +- Can be called multiple times in same watcher + +## Async Pattern + +```ts +export function useAsyncData(fetcher: () => Promise) { + const data = ref(null) + const error = ref(null) + const loading = ref(false) + + async function execute() { + loading.value = true + error.value = null + try { + data.value = await fetcher() + } + catch (e) { + error.value = e as Error + } + finally { + loading.value = false + } + } + + execute() + return { data, error, loading, refetch: execute } +} +``` + +**Data fetching:** Prefer Pinia Colada queries over custom composables. + +## VueUse + +> For VueUse composable reference, use the `vueuse` skill. + +Check VueUse before writing custom composables - most patterns already implemented. + +> **For Nuxt-specific composables** (useFetch, useRequestURL): see `nuxt` skill nuxt-composables.md + +## Advanced Patterns + +### Singleton Composable + +Share state across all components using the same composable: + +```ts +import { createSharedComposable } from '@vueuse/core' + +function useMapControlsBase() { + const mapInstance = ref(null) + const flyTo = (coords: [number, number]) => mapInstance.value?.flyTo(coords) + return { mapInstance, flyTo } +} + +export const useMapControls = createSharedComposable(useMapControlsBase) +``` + +### Cancellable Fetch with AbortController + +```ts +export function useSearch() { + let abortController: AbortController | null = null + + watch(query, async (newQuery) => { + abortController?.abort() + abortController = new AbortController() + + try { + const data = await $fetch('/api/search', { + query: { q: newQuery }, + signal: abortController.signal, + }) + } + catch (e) { + if (e.name !== 'AbortError') + throw e + } + }) +} +``` + +### Step-Based State Machine + +```ts +export function useSendFlow() { + const step = ref<'input' | 'confirm' | 'success'>('input') + const amount = ref('') + + const next = () => { + if (step.value === 'input') + step.value = 'confirm' + else if (step.value === 'confirm') + step.value = 'success' + } + + return { step, amount, next } +} +``` + +### Client-Only Guards + +```ts +export function useUserLocation() { + const location = ref(null) + + if (import.meta.client) { + navigator.geolocation.getCurrentPosition(pos => location.value = pos) + } + + return { location } +} +``` + +### Custom Element Composables (Vue 3.5+) + +For custom element components, use built-in helpers: + +```ts +import { useHost, useShadowRoot } from 'vue' + +export function useCustomElement() { + const host = useHost() // Host element reference + const shadowRoot = useShadowRoot() // Shadow DOM root + + onMounted(() => { + console.log('Host:', host) + console.log('Shadow:', shadowRoot) + }) + + return { host, shadowRoot } +} +``` + +**Available in:** + +- Components using ` + + +``` + +## Directive Hooks + +```ts +const myDirective = { + // Before element attributes/listeners are applied + created(el, binding, vnode) {}, + + // Before element is inserted into DOM + beforeMount(el, binding, vnode) {}, + + // After element and children are mounted + mounted(el, binding, vnode) {}, + + // Before parent component updates + beforeUpdate(el, binding, vnode, prevVnode) {}, + + // After parent component updates + updated(el, binding, vnode, prevVnode) {}, + + // Before parent component unmounts + beforeUnmount(el, binding, vnode) {}, + + // After parent component unmounts + unmounted(el, binding, vnode) {} +} +``` + +## Hook Arguments + +```ts +interface DirectiveBinding { + value: T // v-my-dir="value" + oldValue: T // Previous value (beforeUpdate/updated only) + arg?: string // v-my-dir:arg + modifiers: Record // v-my-dir.foo.bar → { foo: true, bar: true } + instance: ComponentPublicInstance // Component using the directive + dir: ObjectDirective // Directive definition object +} +``` + +Example usage: + +```vue-html +
+``` + +```ts +// binding object: +{ + arg: 'foo', + modifiers: { bar: true }, + value: /* value of baz */, + oldValue: /* previous value */ +} +``` + +## Function Shorthand + +When you only need `mounted` and `updated` with same behavior: + +```ts +// Full form +const vColor = { + mounted(el, binding) { + el.style.color = binding.value + }, + updated(el, binding) { + el.style.color = binding.value + } +} + +// Shorthand (same behavior) +const vColor = (el: HTMLElement, binding: DirectiveBinding) => { + el.style.color = binding.value +} +``` + +## Global Registration + +```ts +// main.ts +const app = createApp(App) + +app.directive('focus', { + mounted: (el) => el.focus() +}) + +// Shorthand +app.directive('color', (el, binding) => { + el.style.color = binding.value +}) +``` + +## Object Literals + +Pass multiple values: + +```vue-html +
+``` + +```ts +const vDemo = (el: HTMLElement, binding: DirectiveBinding<{ color: string; text: string }>) => { + console.log(binding.value.color) // 'white' + console.log(binding.value.text) // 'hello' +} +``` + +## Dynamic Arguments + +```vue-html +
+``` + +## Practical Examples + +### v-click-outside + +```ts +const vClickOutside = { + mounted(el: HTMLElement, binding: DirectiveBinding<() => void>) { + el._clickOutside = (event: MouseEvent) => { + if (!el.contains(event.target as Node)) { + binding.value() + } + } + document.addEventListener('click', el._clickOutside) + }, + unmounted(el: HTMLElement) { + document.removeEventListener('click', el._clickOutside) + } +} +``` + +### v-tooltip + +```ts +const vTooltip = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + el.setAttribute('title', binding.value) + }, + updated(el: HTMLElement, binding: DirectiveBinding) { + el.setAttribute('title', binding.value) + } +} +``` + +### v-permission + +```ts +const vPermission = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + if (!hasPermission(binding.value)) { + el.parentNode?.removeChild(el) + } + } +} +``` + +## TypeScript: Global Directives + +```ts +// directives/highlight.ts +import type { Directive } from 'vue' + +export type HighlightDirective = Directive + +declare module 'vue' { + export interface ComponentCustomProperties { + vHighlight: HighlightDirective + } +} + +export default { + mounted: (el, binding) => { + el.style.backgroundColor = binding.value + } +} satisfies HighlightDirective +``` + +## Usage on Components + +⚠️ **Not recommended** - directives apply to root element, which can be unpredictable with multi-root components. + +```vue-html + + +``` + + diff --git a/.claude/skills/vue/references/gotchas.md b/.claude/skills/vue/references/gotchas.md new file mode 100644 index 0000000..0bbb885 --- /dev/null +++ b/.claude/skills/vue/references/gotchas.md @@ -0,0 +1,438 @@ +# Vue Common Gotchas & Edge Cases + +Critical Vue 3 gotchas that cause silent failures or hard-to-debug issues. + +> Based on [vuejs-ai/skills](https://github.com/vuejs-ai/skills) vue-best-practices. For comprehensive coverage (200+ rules), see the upstream repo. + +## Reactivity + +### Always Use `.value` When Accessing ref() in Scripts + +**Impact: HIGH** - Forgetting `.value` causes silent failures. + +```ts +const count = ref(0) + +// WRONG +count++ // Tries to increment the ref object +count = 5 // Reassigns variable, loses reactivity +items.push(4) // Error: push is not a function + +// CORRECT +count.value++ +count.value = 5 +items.value.push(4) + +// In templates - NO .value needed (Vue unwraps automatically) +// {{ count }} works, not {{ count.value }} +``` + +### Never Destructure reactive() Objects Directly + +**Impact: HIGH** - Destructuring breaks reactive connection. + +```ts +const state = reactive({ count: 0, name: 'Vue' }) + +// WRONG - destructured variables lose reactivity +const { count, name } = state +state.count++ +console.log(count) // Still 0! + +// CORRECT - use toRefs() +const { count, name } = toRefs(state) +state.count++ +console.log(count.value) // 1 + +// BEST - just use ref() instead of reactive() +const count = ref(0) +const name = ref('Vue') +``` + +### Proxy Identity Hazard with reactive() + +```ts +const raw = {} +const proxy = reactive(raw) + +// WRONG - comparing different objects +console.log(proxy === raw) // false + +// WRONG - creating multiple proxies +const a = reactive({}) +const b = reactive(a) // Returns same proxy +console.log(a === b) // true (same object) + +// GOTCHA - nested objects get proxied too +const nested = reactive({ obj: {} }) +console.log(nested.obj === nested.obj) // true (same proxy) +``` + +## Computed Properties + +### No Side Effects in Computed Getters + +**Impact: HIGH** - Side effects break reactivity model. + +```ts +// WRONG - mutates state +const doubled = computed(() => { + count.value++ // Side effect! + return count.value * 2 +}) + +// WRONG - async operation +const data = computed(async () => { + return await fetch('/api') // Side effect! +}) + +// CORRECT - pure computation only +const doubled = computed(() => count.value * 2) + +// For side effects, use watch: +watch(count, (newVal) => { + document.title = `Count: ${newVal}` +}) +``` + +### Computed Returns Are Read-Only + +```ts +const fullName = computed(() => `${first.value} ${last.value}`) + +// WRONG - computed values are read-only +fullName.value = 'John Doe' // Error! + +// CORRECT - use writable computed +const fullName = computed({ + get: () => `${first.value} ${last.value}`, + set: (val) => { + const [f, l] = val.split(' ') + first.value = f + last.value = l + } +}) +``` + +## Watchers + +### Clean Up Async Operations to Prevent Race Conditions + +**Impact: HIGH** - Stale requests can overwrite newer data. + +```ts +const query = ref('') +const results = ref([]) + +// WRONG - race condition +watch(query, async (q) => { + const res = await fetch(`/api?q=${q}`) + results.value = await res.json() // May overwrite newer results! +}) + +// CORRECT - use onWatcherCleanup (Vue 3.5+) +watch(query, async (q) => { + const controller = new AbortController() + onWatcherCleanup(() => controller.abort()) + + try { + const res = await fetch(`/api?q=${q}`, { signal: controller.signal }) + results.value = await res.json() + } catch (e) { + if (e.name !== 'AbortError') throw e + } +}) + +// Or use onCleanup parameter +watch(query, async (q, oldQ, onCleanup) => { + const controller = new AbortController() + onCleanup(() => controller.abort()) + // ... same as above +}) +``` + +### Deep Watch Returns Same Object Reference + +```ts +const obj = reactive({ nested: { count: 0 } }) + +// GOTCHA - oldValue === newValue for deep watches +watch(obj, (newVal, oldVal) => { + console.log(newVal === oldVal) // true! Same object +}, { deep: true }) + +// If you need old value, clone first: +watch( + () => structuredClone(obj), + (newVal, oldVal) => { /* now different */ } +) +``` + +## Props + +### Props Are Read-Only - Never Mutate + +**Impact: HIGH** - Breaks one-way data flow. + +```ts +const props = defineProps<{ count: number; user: User }>() + +// WRONG - direct mutation +props.count++ // Vue warning +props.user.name = 'New' // No warning but still wrong! + +// CORRECT - emit to parent +const emit = defineEmits(['update:count', 'update-user']) +emit('update:count', props.count + 1) +emit('update-user', { ...props.user, name: 'New' }) + +// Or create local copy +const localUser = ref({ ...props.user }) +``` + +### Destructured Props Don't Update Watchers (pre-3.5) + +```ts +// WRONG (Vue < 3.5) +const { count } = defineProps<{ count: number }>() +watch(count, () => {}) // Won't trigger! + +// CORRECT - use getter +const props = defineProps<{ count: number }>() +watch(() => props.count, () => {}) + +// Vue 3.5+ - destructuring works with reactive props +const { count } = defineProps<{ count: number }>() +watch(() => count, () => {}) // Works in 3.5+ +``` + +## Lifecycle Hooks + +### Register Hooks Synchronously During Setup + +**Impact: HIGH** - Async hooks silently fail. + +```ts +// WRONG - hook registered after await +async setup() { + const data = await fetchData() + onMounted(() => {}) // Will NEVER run! +} + +// WRONG - hook in setTimeout +setup() { + setTimeout(() => { + onMounted(() => {}) // Will NEVER run! + }, 100) +} + +// CORRECT - register synchronously, async inside +setup() { + onMounted(async () => { + const data = await fetchData() + }) +} +``` + +## Templates + +### Never Use v-if with v-for on Same Element + +**Impact: HIGH** - Vue 2/3 precedence differs. + +```vue + +
  • + + + + +
  • + + + + + +``` + +### Template Refs Are Null with v-if + +```ts +const inputRef = ref(null) + +// GOTCHA - ref is null when element hidden + + +// WRONG - may be null +inputRef.value.focus() // Error if !show + +// CORRECT - null check +inputRef.value?.focus() + +// Or use watchEffect with flush: 'post' +watchEffect(() => { + inputRef.value?.focus() +}, { flush: 'post' }) +``` + +## defineModel + +### Object Mutations Don't Emit + +```ts +const model = defineModel<{ name: string }>() + +// WRONG - mutation doesn't notify parent +model.value.name = 'New' // Parent won't know! + +// CORRECT - replace entire object +model.value = { ...model.value, name: 'New' } +``` + +### Updated Value Needs nextTick + +```ts +const model = defineModel() + +// WRONG - value not updated yet +model.value = 'new' +console.log(model.value) // Still old value! + +// CORRECT - wait for nextTick +model.value = 'new' +await nextTick() +console.log(model.value) // Now 'new' +``` + +## Component Events + +### Undeclared Emits Can Fire Twice + +```ts +// WRONG - missing emit declaration causes double firing +const emit = defineEmits([]) // 'click' not declared +