Initial commit
This commit is contained in:
334
.claude/skills/nuxt-ui/SKILL.md
Normal file
334
.claude/skills/nuxt-ui/SKILL.md
Normal file
@@ -0,0 +1,334 @@
|
||||
---
|
||||
name: nuxt-ui
|
||||
description: Build UIs with @nuxt/ui v4 — 125+ accessible Vue components with Tailwind CSS theming. Use when creating interfaces, customizing themes to match a brand, building forms, or composing layouts like dashboards, docs sites, and chat interfaces.
|
||||
---
|
||||
|
||||
# Nuxt UI
|
||||
|
||||
Vue component library built on [Reka UI](https://reka-ui.com/) + [Tailwind CSS](https://tailwindcss.com/) + [Tailwind Variants](https://www.tailwind-variants.org/). Works with Nuxt, Vue (Vite), Laravel (Inertia), and AdonisJS (Inertia).
|
||||
|
||||
## Installation
|
||||
|
||||
### Nuxt
|
||||
|
||||
```bash
|
||||
pnpm add @nuxt/ui tailwindcss
|
||||
```
|
||||
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
modules: ['@nuxt/ui'],
|
||||
css: ['~/assets/css/main.css']
|
||||
})
|
||||
```
|
||||
|
||||
```css
|
||||
/* app/assets/css/main.css */
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- app.vue -->
|
||||
<template>
|
||||
<UApp>
|
||||
<NuxtPage />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Vue (Vite)
|
||||
|
||||
```bash
|
||||
pnpm add @nuxt/ui tailwindcss
|
||||
```
|
||||
|
||||
```ts
|
||||
// vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ui()
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// src/main.ts
|
||||
import './assets/main.css'
|
||||
import { createApp } from 'vue'
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import ui from '@nuxt/ui/vue-plugin'
|
||||
import App from './App.vue'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
const router = createRouter({
|
||||
routes: [],
|
||||
history: createWebHistory()
|
||||
})
|
||||
|
||||
app.use(router)
|
||||
app.use(ui)
|
||||
app.mount('#app')
|
||||
```
|
||||
|
||||
```css
|
||||
/* assets/main.css */
|
||||
@import "tailwindcss";
|
||||
@import "@nuxt/ui";
|
||||
```
|
||||
|
||||
```vue
|
||||
<!-- src/App.vue -->
|
||||
<template>
|
||||
<UApp>
|
||||
<RouterView />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
> **Vue**: Add `class="isolate"` to your root `<div id="app">` in `index.html`.
|
||||
|
||||
> **Vue + Inertia**: Use `ui({ router: 'inertia' })` in `vite.config.ts`.
|
||||
|
||||
### UApp
|
||||
|
||||
Wrapping your app in `UApp` is **required** — it provides global config for toasts, tooltips, and programmatic overlays. It also accepts a `locale` prop for i18n (see [composables reference](references/composables.md)).
|
||||
|
||||
## Icons
|
||||
|
||||
Nuxt UI uses [Iconify](https://iconify.design/) for 200,000+ icons. In Nuxt, `@nuxt/icon` is auto-registered. In Vue, icons work out of the box via the Vite plugin.
|
||||
|
||||
### Naming convention
|
||||
|
||||
Icons use the format `i-{collection}-{name}`:
|
||||
|
||||
```vue
|
||||
<UIcon name="i-lucide-sun" class="size-5" />
|
||||
<UButton icon="i-lucide-plus" label="Add" />
|
||||
<UAlert icon="i-lucide-info" title="Heads up" />
|
||||
```
|
||||
|
||||
> Browse all icons at [icones.js.org](https://icones.js.org). The `lucide` collection is used throughout Nuxt UI defaults.
|
||||
|
||||
### Install icon collections locally
|
||||
|
||||
```bash
|
||||
pnpm i @iconify-json/lucide
|
||||
pnpm i @iconify-json/simple-icons
|
||||
```
|
||||
|
||||
### Custom local collections (Nuxt)
|
||||
|
||||
```ts
|
||||
// nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
icon: {
|
||||
customCollections: [{
|
||||
prefix: 'custom',
|
||||
dir: './app/assets/icons'
|
||||
}]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```vue
|
||||
<UIcon name="i-custom-my-icon" />
|
||||
```
|
||||
|
||||
## Theming & Branding
|
||||
|
||||
Nuxt UI ships with a default look. The goal is to adapt it to your brand so every app looks unique.
|
||||
|
||||
**Always use semantic utilities** (`text-default`, `bg-elevated`, `border-muted`), never raw Tailwind palette colors. See [references/theming.md](references/theming.md) for the full list.
|
||||
|
||||
### Colors
|
||||
|
||||
7 semantic colors (`primary`, `secondary`, `success`, `info`, `warning`, `error`, `neutral`) configurable at runtime:
|
||||
|
||||
```ts
|
||||
// Nuxt — app.config.ts
|
||||
export default defineAppConfig({
|
||||
ui: { colors: { primary: 'indigo', neutral: 'zinc' } }
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vue — vite.config.ts
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import ui from '@nuxt/ui/vite'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
ui({
|
||||
ui: { colors: { primary: 'indigo', neutral: 'zinc' } }
|
||||
})
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
### Customizing components
|
||||
|
||||
**Override priority** (highest wins): `ui` prop / `class` prop > global config > theme defaults.
|
||||
|
||||
The `ui` prop overrides a component's **slots** after variants are computed — it wins over everything:
|
||||
|
||||
```vue
|
||||
<UButton :ui="{ base: 'rounded-none', trailingIcon: 'size-3 rotate-90' }" />
|
||||
<UCard :ui="{ header: 'bg-muted', body: 'p-8' }" />
|
||||
```
|
||||
|
||||
**Read the generated theme file** to find slot names for any component:
|
||||
|
||||
- **Nuxt**: `.nuxt/ui/<component>.ts`
|
||||
- **Vue**: `node_modules/.nuxt-ui/ui/<component>.ts`
|
||||
|
||||
> For CSS variables, custom colors, global config, compound variants, and a **full brand customization playbook**, see [references/theming.md](references/theming.md)
|
||||
|
||||
## Composables
|
||||
|
||||
```ts
|
||||
// Notifications
|
||||
const toast = useToast()
|
||||
toast.add({ title: 'Saved', color: 'success', icon: 'i-lucide-check' })
|
||||
|
||||
// Programmatic overlays
|
||||
const overlay = useOverlay()
|
||||
const modal = overlay.create(MyModal)
|
||||
const { result } = modal.open({ title: 'Confirm' })
|
||||
await result
|
||||
|
||||
// Keyboard shortcuts
|
||||
defineShortcuts({
|
||||
meta_k: () => openSearch(),
|
||||
escape: () => close()
|
||||
})
|
||||
```
|
||||
|
||||
> For full composable reference, see [references/composables.md](references/composables.md)
|
||||
|
||||
## Form validation
|
||||
|
||||
Uses Standard Schema — works with Zod, Valibot, Yup, or Joi.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Min 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
const state = reactive<Partial<Schema>>({ email: '', password: '' })
|
||||
|
||||
function onSubmit() {
|
||||
// UForm validates before emitting @submit — state is valid here
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm :schema="schema" :state="state" @submit="onSubmit">
|
||||
<UFormField name="email" label="Email" required>
|
||||
<UInput v-model="state.email" type="email" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="password" label="Password" required>
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit">Sign in</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
|
||||
> For all form components and validation patterns, see [references/components.md](references/components.md#form)
|
||||
|
||||
## Overlays
|
||||
|
||||
```vue
|
||||
<!-- Modal -->
|
||||
<UModal v-model:open="isOpen" title="Edit" description="Edit your profile">
|
||||
<template #body>Content</template>
|
||||
<template #footer>
|
||||
<UButton variant="ghost" @click="isOpen = false">Cancel</UButton>
|
||||
<UButton @click="save">Save</UButton>
|
||||
</template>
|
||||
</UModal>
|
||||
|
||||
<!-- Slideover (side panel) -->
|
||||
<USlideover v-model:open="isOpen" title="Settings" side="right">
|
||||
<template #body>Content</template>
|
||||
</USlideover>
|
||||
|
||||
<!-- Dropdown menu (flat array) -->
|
||||
<UDropdownMenu :items="[
|
||||
{ label: 'Edit', icon: 'i-lucide-pencil' },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
|
||||
]">
|
||||
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
|
||||
<!-- Dropdown menu (nested array — groups with automatic separators) -->
|
||||
<UDropdownMenu :items="[
|
||||
[{ label: 'Edit', icon: 'i-lucide-pencil' }, { label: 'Duplicate', icon: 'i-lucide-copy' }],
|
||||
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }]
|
||||
]">
|
||||
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
```
|
||||
|
||||
> For all overlay components, see [references/components.md](references/components.md#overlay)
|
||||
|
||||
## Layouts
|
||||
|
||||
Nuxt UI provides components to compose full page layouts. Load the reference matching your use case:
|
||||
|
||||
| Layout | Description | Reference |
|
||||
|---|---|---|
|
||||
| Page | Landing, blog, changelog, pricing — public-facing pages | [layouts/page.md](references/layouts/page.md) |
|
||||
| Dashboard | Admin UI with resizable sidebar and panels | [layouts/dashboard.md](references/layouts/dashboard.md) |
|
||||
| Docs | Documentation with sidebar nav and TOC | [layouts/docs.md](references/layouts/docs.md) |
|
||||
| Chat | AI chat with messages and prompt | [layouts/chat.md](references/layouts/chat.md) |
|
||||
| Editor | Rich text editor with toolbars | [layouts/editor.md](references/layouts/editor.md) |
|
||||
|
||||
## Templates
|
||||
|
||||
Official starter templates at [github.com/nuxt-ui-templates](https://github.com/nuxt-ui-templates):
|
||||
|
||||
| Template | Framework | GitHub |
|
||||
|---|---|---|
|
||||
| Starter | Nuxt | [nuxt-ui-templates/starter](https://github.com/nuxt-ui-templates/starter) |
|
||||
| Starter | Vue | [nuxt-ui-templates/starter-vue](https://github.com/nuxt-ui-templates/starter-vue) |
|
||||
| Dashboard | Nuxt | [nuxt-ui-templates/dashboard](https://github.com/nuxt-ui-templates/dashboard) |
|
||||
| Dashboard | Vue | [nuxt-ui-templates/dashboard-vue](https://github.com/nuxt-ui-templates/dashboard-vue) |
|
||||
| SaaS | Nuxt | [nuxt-ui-templates/saas](https://github.com/nuxt-ui-templates/saas) |
|
||||
| Landing | Nuxt | [nuxt-ui-templates/landing](https://github.com/nuxt-ui-templates/landing) |
|
||||
| Docs | Nuxt | [nuxt-ui-templates/docs](https://github.com/nuxt-ui-templates/docs) |
|
||||
| Portfolio | Nuxt | [nuxt-ui-templates/portfolio](https://github.com/nuxt-ui-templates/portfolio) |
|
||||
| Chat | Nuxt | [nuxt-ui-templates/chat](https://github.com/nuxt-ui-templates/chat) |
|
||||
| Editor | Nuxt | [nuxt-ui-templates/editor](https://github.com/nuxt-ui-templates/editor) |
|
||||
| Changelog | Nuxt | [nuxt-ui-templates/changelog](https://github.com/nuxt-ui-templates/changelog) |
|
||||
| Starter | Laravel | [nuxt-ui-templates/starter-laravel](https://github.com/nuxt-ui-templates/starter-laravel) |
|
||||
| Starter | AdonisJS | [nuxt-ui-templates/starter-adonis](https://github.com/nuxt-ui-templates/starter-adonis) |
|
||||
|
||||
> When starting a new project, clone the matching template instead of setting up from scratch.
|
||||
|
||||
## Additional references
|
||||
|
||||
Load based on your task — **do not load all at once**:
|
||||
|
||||
- [references/theming.md](references/theming.md) — CSS variables, custom colors, component theme overrides
|
||||
- [references/components.md](references/components.md) — all 125+ components by category with props and usage
|
||||
- [references/composables.md](references/composables.md) — useToast, useOverlay, defineShortcuts
|
||||
- Generated theme files — all slots, variants, and default classes for any component (Nuxt: `.nuxt/ui/<component>.ts`, Vue: `node_modules/.nuxt-ui/ui/<component>.ts`)
|
||||
377
.claude/skills/nuxt-ui/references/components.md
Normal file
377
.claude/skills/nuxt-ui/references/components.md
Normal file
@@ -0,0 +1,377 @@
|
||||
# Components
|
||||
|
||||
125+ Vue components powered by Tailwind CSS and Reka UI. For any component's theme slots, read the generated theme file (Nuxt: `.nuxt/ui/<component>.ts`, Vue: `node_modules/.nuxt-ui/ui/<component>.ts`).
|
||||
|
||||
## Layout
|
||||
|
||||
Core structural components for organizing your application's layout.
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UApp` | **Required** root wrapper for toasts, tooltips, overlays |
|
||||
| `UHeader` | Responsive header with mobile menu (`#title`, `#default`, `#right`, `#body`) |
|
||||
| `UFooter` | Footer (`#left`, `#default`, `#right`, `#top`, `#bottom`) |
|
||||
| `UFooterColumns` | Multi-column footer with link groups |
|
||||
| `UMain` | Main content area (respects `--ui-header-height`) |
|
||||
| `UContainer` | Centered max-width container (`--ui-container`) |
|
||||
|
||||
## Element
|
||||
|
||||
Essential UI building blocks.
|
||||
|
||||
| Component | Key props |
|
||||
|---|---|
|
||||
| `UButton` | `label`, `icon`, `color`, `variant`, `size`, `loading`, `disabled`, `to` |
|
||||
| `UBadge` | `label`, `color`, `variant`, `size` |
|
||||
| `UAvatar` | `src`, `alt`, `icon`, `text`, `size` |
|
||||
| `UAvatarGroup` | `max`, `size` — wraps multiple `UAvatar` |
|
||||
| `UIcon` | `name`, `size` |
|
||||
| `UCard` | `variant` — slots: `#header`, `#default`, `#footer` |
|
||||
| `UAlert` | `title`, `description`, `icon`, `color`, `variant`, `close` |
|
||||
| `UBanner` | `title`, `icon`, `close` — sticky top banner |
|
||||
| `UChip` | `color`, `size`, `position` — notification dot on children |
|
||||
| `UKbd` | `value` — keyboard key display |
|
||||
| `USeparator` | `label`, `icon`, `orientation`, `type` |
|
||||
| `USkeleton` | `class` — loading placeholder |
|
||||
| `UProgress` | `value`, `max`, `color`, `size` |
|
||||
| `UCalendar` | `v-model`, `range` (boolean), `multiple` (boolean) |
|
||||
| `UCollapsible` | `v-model:open` — animated expand/collapse |
|
||||
| `UFieldGroup` | Groups form inputs horizontally/vertically |
|
||||
|
||||
## Form
|
||||
|
||||
Comprehensive form components for user input.
|
||||
|
||||
| Component | Key props |
|
||||
|---|---|
|
||||
| `UInput` | `v-model`, `type`, `placeholder`, `icon`, `loading` |
|
||||
| `UTextarea` | `v-model`, `rows`, `autoresize`, `maxrows` |
|
||||
| `USelect` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `placeholder` |
|
||||
| `USelectMenu` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `searchable`, `multiple` |
|
||||
| `UInputMenu` | `v-model`, `items` (flat `T[]` or grouped `T[][]`), `searchable` — autocomplete |
|
||||
| `UInputNumber` | `v-model`, `min`, `max`, `step` |
|
||||
| `UInputDate` | `v-model`, `range` (boolean for range selection), `locale` |
|
||||
| `UInputTime` | `v-model`, `hour-cycle` (12/24), `granularity` |
|
||||
| `UInputTags` | `v-model`, `max`, `placeholder` |
|
||||
| `UPinInput` | `v-model`, `length`, `type`, `mask` |
|
||||
| `UCheckbox` | `v-model`, `label`, `description` |
|
||||
| `UCheckboxGroup` | `v-model`, `items`, `orientation` |
|
||||
| `URadioGroup` | `v-model`, `items`, `orientation` |
|
||||
| `USwitch` | `v-model`, `label`, `on-icon`, `off-icon` |
|
||||
| `USlider` | `v-model`, `min`, `max`, `step` |
|
||||
| `UColorPicker` | `v-model`, `format` (hex/rgb/hsl/cmyk/lab), `size` |
|
||||
| `UFileUpload` | `v-model`, `accept`, `multiple`, `variant` (area/button) |
|
||||
| `UForm` | `schema`, `state`, `@submit` — validation wrapper |
|
||||
| `UFormField` | `name`, `label`, `description`, `hint`, `required` |
|
||||
|
||||
### Form validation
|
||||
|
||||
Uses Standard Schema — works with Zod, Valibot, Yup, or Joi.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { z } from 'zod'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.string().email('Invalid email'),
|
||||
password: z.string().min(8, 'Min 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
const state = reactive<Partial<Schema>>({ email: '', password: '' })
|
||||
const form = ref()
|
||||
|
||||
async function onSubmit() {
|
||||
await form.value.validate()
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UForm ref="form" :schema="schema" :state="state" @submit="onSubmit">
|
||||
<UFormField name="email" label="Email" required>
|
||||
<UInput v-model="state.email" type="email" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="password" label="Password" required>
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit">Submit</UButton>
|
||||
</UForm>
|
||||
</template>
|
||||
```
|
||||
|
||||
With Valibot:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import * as v from 'valibot'
|
||||
|
||||
const schema = v.object({
|
||||
email: v.pipe(v.string(), v.email('Invalid email')),
|
||||
password: v.pipe(v.string(), v.minLength(8, 'Min 8 characters'))
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
### File upload
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const files = ref<File[]>([])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UFileUpload v-model="files" accept="image/*" multiple>
|
||||
<template #actions="{ open }">
|
||||
<UButton label="Upload" icon="i-lucide-upload" color="neutral" variant="outline" @click="open()" />
|
||||
</template>
|
||||
</UFileUpload>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Data
|
||||
|
||||
Components for displaying and organizing data.
|
||||
|
||||
| Component | Key props |
|
||||
|---|---|
|
||||
| `UTable` | `data`, `columns`, `loading`, `sticky` |
|
||||
| `UAccordion` | `items`, `type` (single/multiple), `collapsible` |
|
||||
| `UCarousel` | `items`, `orientation`, `arrows`, `dots` |
|
||||
| `UTimeline` | `items` — vertical timeline |
|
||||
| `UTree` | `items` — hierarchical tree |
|
||||
| `UUser` | `name`, `description`, `avatar` — user display |
|
||||
| `UEmpty` | `icon`, `title`, `description` — empty state |
|
||||
| `UMarquee` | `repeat`, `reverse`, `orientation`, `pauseOnHover` — infinite scroll |
|
||||
| `UScrollArea` | Custom scrollbar wrapper |
|
||||
|
||||
## Navigation
|
||||
|
||||
Components for user navigation and wayfinding.
|
||||
|
||||
| Component | Key props |
|
||||
|---|---|
|
||||
| `UNavigationMenu` | `items` (flat `T[]` or grouped `T[][]`), `orientation` (horizontal/vertical) |
|
||||
| `UBreadcrumb` | `items` |
|
||||
| `UTabs` | `items`, `orientation`, `variant` |
|
||||
| `UStepper` | `items`, `orientation`, `color` |
|
||||
| `UPagination` | `v-model`, `total`, `items-per-page` |
|
||||
| `ULink` | `to`, `active`, `inactive` — styled NuxtLink |
|
||||
| `UCommandPalette` | `v-model:open`, `groups` (`{ id, label, items }[]`), `placeholder` |
|
||||
|
||||
## Overlay
|
||||
|
||||
Floating UI elements that appear above the main content. **All require `<UApp>` wrapper.**
|
||||
|
||||
| Component | Key props |
|
||||
|---|---|
|
||||
| `UModal` | `v-model:open`, `title`, `description`, `fullscreen`, `scrollable` |
|
||||
| `USlideover` | `v-model:open`, `title`, `side` (left/right/top/bottom) |
|
||||
| `UDrawer` | `v-model:open`, `title`, `handle` |
|
||||
| `UPopover` | `arrow`, `content: { side, align }`, `openDelay`, `closeDelay` |
|
||||
| `UTooltip` | `text`, `content: { side }`, `delayDuration` |
|
||||
| `UDropdownMenu` | `items` (flat `T[]` or grouped `T[][]` with separators, supports nested `children`) |
|
||||
| `UContextMenu` | `items` (flat `T[]` or grouped `T[][]`) — right-click menu |
|
||||
| `UToast` | Used via `useToast()` composable |
|
||||
|
||||
### Modal
|
||||
|
||||
```vue
|
||||
<UModal v-model:open="isOpen" title="Edit" description="Edit your profile">
|
||||
<template #body>Content</template>
|
||||
<template #footer>
|
||||
<UButton variant="ghost" @click="isOpen = false">Cancel</UButton>
|
||||
<UButton @click="save">Save</UButton>
|
||||
</template>
|
||||
</UModal>
|
||||
```
|
||||
|
||||
Slots: `#content`, `#header`, `#body`, `#footer`
|
||||
|
||||
### Slideover
|
||||
|
||||
```vue
|
||||
<USlideover v-model:open="isOpen" title="Settings" side="right">
|
||||
<template #body>Content</template>
|
||||
</USlideover>
|
||||
```
|
||||
|
||||
### Drawer
|
||||
|
||||
```vue
|
||||
<UDrawer v-model:open="isOpen" title="Options" handle>
|
||||
<template #body>Content</template>
|
||||
</UDrawer>
|
||||
```
|
||||
|
||||
### DropdownMenu
|
||||
|
||||
Items accept a flat array or a nested array (each sub-array is rendered as a group separated by dividers):
|
||||
|
||||
```vue
|
||||
<!-- Flat array -->
|
||||
<UDropdownMenu :items="[
|
||||
{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit() },
|
||||
{ type: 'separator' },
|
||||
{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }
|
||||
]">
|
||||
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
|
||||
<!-- Nested array (groups with automatic separators) -->
|
||||
<UDropdownMenu :items="[
|
||||
[{ label: 'Edit', icon: 'i-lucide-pencil' }, { label: 'Duplicate', icon: 'i-lucide-copy' }],
|
||||
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error' }]
|
||||
]">
|
||||
<UButton icon="i-lucide-ellipsis-vertical" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
```
|
||||
|
||||
### Toast
|
||||
|
||||
```ts
|
||||
const toast = useToast()
|
||||
|
||||
toast.add({
|
||||
title: 'Success',
|
||||
description: 'Changes saved',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-check-circle',
|
||||
duration: 5000,
|
||||
actions: [{ label: 'Undo', onClick: () => undo() }]
|
||||
})
|
||||
```
|
||||
|
||||
### Programmatic overlays
|
||||
|
||||
```ts
|
||||
const overlay = useOverlay()
|
||||
|
||||
// create() returns a reusable instance
|
||||
const confirmDialog = overlay.create(ConfirmDialog)
|
||||
|
||||
// open() returns an object with .result (a Promise)
|
||||
const { result } = confirmDialog.open({
|
||||
title: 'Delete?',
|
||||
message: 'This cannot be undone.'
|
||||
})
|
||||
|
||||
if (await result) {
|
||||
// User confirmed
|
||||
}
|
||||
|
||||
// Inside the overlay component, emit close with a value:
|
||||
// emit('close', true) or emit('close', false)
|
||||
```
|
||||
|
||||
### CommandPalette
|
||||
|
||||
```vue
|
||||
<script setup>
|
||||
const groups = [{
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
items: [
|
||||
{ label: 'New file', icon: 'i-lucide-file-plus', onSelect: () => {} },
|
||||
{ label: 'Settings', to: '/settings' }
|
||||
]
|
||||
}]
|
||||
|
||||
defineShortcuts({ meta_k: () => { isOpen.value = true } })
|
||||
</script>
|
||||
|
||||
<UCommandPalette v-model:open="isOpen" :groups="groups" placeholder="Search..." />
|
||||
```
|
||||
|
||||
## Page
|
||||
|
||||
Pre-built sections for marketing and content pages.
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UPage` | Multi-column grid (`#left`, `#default`, `#right`) |
|
||||
| `UPageAside` | Sticky sidebar wrapper (visible from `lg`) |
|
||||
| `UPageHero` | Hero section with title, description, links, media |
|
||||
| `UPageSection` | Content section with headline, features grid |
|
||||
| `UPageCTA` | Call to action block |
|
||||
| `UPageHeader` | Page title and description |
|
||||
| `UPageBody` | Main content area with prose styling |
|
||||
| `UPageFeature` | Individual feature item |
|
||||
| `UPageGrid` | Grid layout for cards |
|
||||
| `UPageColumns` | Multi-column layout |
|
||||
| `UPageCard` | Content card for grids |
|
||||
| `UPageLogos` | Logo wall |
|
||||
| `UPageAnchors` | Anchor links (simpler TOC) |
|
||||
| `UPageLinks` | Related resource links |
|
||||
| `UPageList` | List items |
|
||||
| `UBlogPosts` | Responsive grid of blog posts (`orientation`) |
|
||||
| `UBlogPost` | Individual blog post card |
|
||||
| `UChangelogVersions` | Changelog version list |
|
||||
| `UChangelogVersion` | Individual changelog entry |
|
||||
| `UPricingPlans` | Pricing plan cards |
|
||||
| `UPricingTable` | Feature comparison table |
|
||||
|
||||
## Dashboard
|
||||
|
||||
Specialized components for admin interfaces with resizable panels and sidebars.
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UDashboardGroup` | Root wrapper — manages sidebar state |
|
||||
| `UDashboardSidebar` | Resizable/collapsible sidebar (`#header`, `#default`, `#footer`) |
|
||||
| `UDashboardPanel` | Content panel (`#header`, `#body`, `#footer`) |
|
||||
| `UDashboardNavbar` | Panel navbar (`#left`, `#default`, `#right`) |
|
||||
| `UDashboardToolbar` | Toolbar for filters/actions |
|
||||
| `UDashboardSearch` | Command palette for dashboards |
|
||||
| `UDashboardSearchButton` | Search trigger button |
|
||||
| `UDashboardSidebarToggle` | Mobile sidebar toggle |
|
||||
| `UDashboardSidebarCollapse` | Desktop collapse button |
|
||||
| `UDashboardResizeHandle` | Custom resize handle |
|
||||
|
||||
## Chat
|
||||
|
||||
Components for conversational AI interfaces, powered by [Vercel AI SDK](https://ai-sdk.dev/).
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UChatMessages` | Scrollable message list with auto-scroll |
|
||||
| `UChatMessage` | Individual message display |
|
||||
| `UChatPrompt` | Enhanced textarea for prompts |
|
||||
| `UChatPromptSubmit` | Submit button with status handling |
|
||||
| `UChatPalette` | Chat layout for overlays |
|
||||
|
||||
## Editor
|
||||
|
||||
Rich text editor powered by [TipTap](https://tiptap.dev/).
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UEditor` | Editor (`v-model`, `content-type`: json/html/markdown) |
|
||||
| `UEditorToolbar` | Toolbar (`layout`: fixed/bubble/floating) |
|
||||
| `UEditorDragHandle` | Block drag-and-drop |
|
||||
| `UEditorSuggestionMenu` | Slash command menu |
|
||||
| `UEditorMentionMenu` | @ mention menu |
|
||||
| `UEditorEmojiMenu` | Emoji picker |
|
||||
|
||||
## Content
|
||||
|
||||
Components integrating with `@nuxt/content`.
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UContentNavigation` | Sidebar navigation tree |
|
||||
| `UContentToc` | Table of contents |
|
||||
| `UContentSurround` | Prev/next links |
|
||||
| `UContentSearch` | Search command palette |
|
||||
| `UContentSearchButton` | Search trigger button |
|
||||
|
||||
## Color Mode
|
||||
|
||||
| Component | Purpose |
|
||||
|---|---|
|
||||
| `UColorModeButton` | Toggle light/dark button |
|
||||
| `UColorModeSwitch` | Toggle light/dark switch |
|
||||
| `UColorModeSelect` | Dropdown selector |
|
||||
| `UColorModeAvatar` | Avatar with different src per mode |
|
||||
| `UColorModeImage` | Image with different src per mode |
|
||||
127
.claude/skills/nuxt-ui/references/composables.md
Normal file
127
.claude/skills/nuxt-ui/references/composables.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# Composables
|
||||
|
||||
## useToast
|
||||
|
||||
Show notifications. Requires `<UApp>` wrapper.
|
||||
|
||||
```ts
|
||||
const toast = useToast()
|
||||
|
||||
toast.add({
|
||||
title: 'Success',
|
||||
description: 'Item saved',
|
||||
color: 'success', // primary, success, error, warning, info
|
||||
icon: 'i-lucide-check-circle',
|
||||
duration: 5000, // 0 = never dismiss
|
||||
actions: [{ label: 'Undo', onClick: () => {} }]
|
||||
})
|
||||
|
||||
toast.remove('toast-id')
|
||||
toast.clear()
|
||||
```
|
||||
|
||||
## useOverlay
|
||||
|
||||
Programmatically create modals, slideovers, drawers.
|
||||
|
||||
```ts
|
||||
const overlay = useOverlay()
|
||||
|
||||
// create() returns a reusable instance with open(), close(), patch()
|
||||
const modal = overlay.create(MyComponent)
|
||||
|
||||
// open() accepts props and returns an object with .result (a Promise)
|
||||
const { result } = modal.open({ title: 'Confirm' })
|
||||
|
||||
if (await result) {
|
||||
// User confirmed
|
||||
}
|
||||
|
||||
// Inside the overlay component, emit close with a value:
|
||||
// emit('close', true) or emit('close', false)
|
||||
|
||||
// You can also close from outside:
|
||||
modal.close(false)
|
||||
```
|
||||
|
||||
## defineShortcuts
|
||||
|
||||
Define keyboard shortcuts.
|
||||
|
||||
```ts
|
||||
defineShortcuts({
|
||||
meta_k: () => openSearch(), // Cmd+K (Mac) / Ctrl+K (Win)
|
||||
meta_shift_p: () => openPalette(), // Cmd+Shift+P
|
||||
escape: () => close(),
|
||||
ctrl_s: () => save(),
|
||||
|
||||
// With condition
|
||||
meta_enter: {
|
||||
handler: () => submit(),
|
||||
whenever: [isFormValid]
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
| Key | Meaning |
|
||||
|---|---|
|
||||
| `meta` | Cmd (Mac) / Ctrl (Windows) |
|
||||
| `ctrl` | Ctrl key |
|
||||
| `alt` | Alt / Option key |
|
||||
| `shift` | Shift key |
|
||||
| `_` | Key separator |
|
||||
|
||||
## defineLocale / extendLocale
|
||||
|
||||
i18n locale definition.
|
||||
|
||||
```ts
|
||||
import { fr } from '@nuxt/ui/locale'
|
||||
|
||||
// Use a built-in locale (50+ available)
|
||||
// <UApp :locale="fr">
|
||||
|
||||
// Define custom locale
|
||||
const locale = defineLocale({
|
||||
name: 'Español',
|
||||
code: 'es',
|
||||
dir: 'ltr',
|
||||
messages: {
|
||||
select: { placeholder: 'Seleccionar...' }
|
||||
}
|
||||
})
|
||||
|
||||
// Extend existing locale
|
||||
import { en } from '@nuxt/ui/locale'
|
||||
|
||||
const customEn = extendLocale(en, {
|
||||
messages: { commandPalette: { placeholder: 'Search a component...' } }
|
||||
})
|
||||
```
|
||||
|
||||
```vue
|
||||
<UApp :locale="fr"><NuxtPage /></UApp>
|
||||
```
|
||||
|
||||
## extractShortcuts
|
||||
|
||||
Extract shortcut keys from a list of items (e.g., dropdown menu items) into a shortcuts map for `defineShortcuts`.
|
||||
|
||||
```ts
|
||||
const items = [
|
||||
{ label: 'New file', kbds: ['meta', 'n'], onSelect: () => newFile() },
|
||||
{ label: 'Save', kbds: ['meta', 's'], onSelect: () => save() }
|
||||
]
|
||||
|
||||
defineShortcuts(extractShortcuts(items))
|
||||
```
|
||||
|
||||
## Quick reference
|
||||
|
||||
| Composable | Purpose |
|
||||
|---|---|
|
||||
| `useToast` | Show notifications |
|
||||
| `useOverlay` | Programmatic modals/slideovers |
|
||||
| `defineShortcuts` | Keyboard shortcuts |
|
||||
| `defineLocale` / `extendLocale` | i18n locale |
|
||||
| `extractShortcuts` | Parse shortcut definitions |
|
||||
271
.claude/skills/nuxt-ui/references/layouts/chat.md
Normal file
271
.claude/skills/nuxt-ui/references/layouts/chat.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Chat Layout
|
||||
|
||||
Build AI chat interfaces with message streams, reasoning, tool calling, and Vercel AI SDK integration.
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UApp
|
||||
└── NuxtLayout (dashboard)
|
||||
└── UDashboardGroup
|
||||
├── UDashboardSidebar (conversations)
|
||||
└── NuxtPage
|
||||
└── UDashboardPanel
|
||||
├── #header → UDashboardNavbar
|
||||
├── #body → UContainer → UChatMessages
|
||||
│ ├── #content → UChatReasoning, UChatTool, MDC
|
||||
│ └── #indicator (loading)
|
||||
└── #footer → UContainer → UChatPrompt
|
||||
└── UChatPromptSubmit
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Install AI SDK
|
||||
|
||||
```bash
|
||||
pnpm add ai @ai-sdk/gateway @ai-sdk/vue
|
||||
```
|
||||
|
||||
### Server endpoint
|
||||
|
||||
```ts [server/api/chat.post.ts]
|
||||
import { streamText, convertToModelMessages } from 'ai'
|
||||
import { gateway } from '@ai-sdk/gateway'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const { messages } = await readBody(event)
|
||||
|
||||
return streamText({
|
||||
model: gateway('anthropic/claude-sonnet-4.6'),
|
||||
system: 'You are a helpful assistant.',
|
||||
messages: await convertToModelMessages(messages)
|
||||
}).toUIMessageStreamResponse()
|
||||
})
|
||||
```
|
||||
|
||||
## Full page chat
|
||||
|
||||
```vue [pages/chat/[id].vue]
|
||||
<script setup lang="ts">
|
||||
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
|
||||
import { Chat } from '@ai-sdk/vue'
|
||||
import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
|
||||
|
||||
definePageMeta({ layout: 'dashboard' })
|
||||
|
||||
const input = ref('')
|
||||
|
||||
const chat = new Chat({
|
||||
onError(error) {
|
||||
console.error(error)
|
||||
}
|
||||
})
|
||||
|
||||
function onSubmit() {
|
||||
if (!input.value.trim()) return
|
||||
|
||||
chat.sendMessage({ text: input.value })
|
||||
|
||||
input.value = ''
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Chat" />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UContainer>
|
||||
<UChatMessages :messages="chat.messages" :status="chat.status">
|
||||
<template #content="{ message }">
|
||||
<template v-for="(part, index) in message.parts" :key="`${message.id}-${part.type}-${index}`">
|
||||
<UChatReasoning
|
||||
v-if="isReasoningUIPart(part)"
|
||||
:text="part.text"
|
||||
:streaming="isPartStreaming(part)"
|
||||
>
|
||||
<MDC
|
||||
:value="part.text"
|
||||
:cache-key="`reasoning-${message.id}-${index}`"
|
||||
class="*:first:mt-0 *:last:mb-0"
|
||||
/>
|
||||
</UChatReasoning>
|
||||
|
||||
<UChatTool
|
||||
v-else-if="isToolUIPart(part)"
|
||||
:text="getToolName(part)"
|
||||
:streaming="isToolStreaming(part)"
|
||||
/>
|
||||
|
||||
<template v-else-if="isTextUIPart(part)">
|
||||
<MDC
|
||||
v-if="message.role === 'assistant'"
|
||||
:value="part.text"
|
||||
:cache-key="`${message.id}-${index}`"
|
||||
class="*:first:mt-0 *:last:mb-0"
|
||||
/>
|
||||
<p v-else-if="message.role === 'user'" class="whitespace-pre-wrap">
|
||||
{{ part.text }}
|
||||
</p>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
</UChatMessages>
|
||||
</UContainer>
|
||||
</template>
|
||||
|
||||
<template #footer>
|
||||
<UContainer class="pb-4 sm:pb-6">
|
||||
<UChatPrompt v-model="input" :error="chat.error" @submit="onSubmit">
|
||||
<UChatPromptSubmit :status="chat.status" @stop="chat.stop()" @reload="chat.regenerate()" />
|
||||
</UChatPrompt>
|
||||
</UContainer>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Key components
|
||||
|
||||
### ChatMessages
|
||||
|
||||
Scrollable message list with auto-scroll and loading indicator.
|
||||
|
||||
| Prop | Description |
|
||||
|---|---|
|
||||
| `messages` | Array of AI SDK messages |
|
||||
| `status` | `'submitted'`, `'streaming'`, `'ready'`, `'error'` |
|
||||
|
||||
Slots: `#content` (receives `{ message }`), `#actions` (per-message), `#indicator` (loading)
|
||||
|
||||
### ChatMessage
|
||||
|
||||
Individual message bubble with avatar, actions, and slots.
|
||||
|
||||
| Prop | Description |
|
||||
|---|---|
|
||||
| `message` | AI SDK UIMessage object |
|
||||
| `side` | `'left'` (default), `'right'` |
|
||||
|
||||
### ChatReasoning
|
||||
|
||||
Collapsible block for AI reasoning / thinking process. Auto-opens during streaming, auto-closes when done.
|
||||
|
||||
| Prop | Description |
|
||||
|---|---|
|
||||
| `text` | Reasoning text (displayed inside collapsible content) |
|
||||
| `streaming` | Whether reasoning is actively streaming |
|
||||
| `open` | Controlled open state |
|
||||
|
||||
Use `isPartStreaming(part)` from `@nuxt/ui/utils/ai` to determine streaming state.
|
||||
|
||||
### ChatTool
|
||||
|
||||
Collapsible block for AI tool invocation status.
|
||||
|
||||
| Prop | Description |
|
||||
|---|---|
|
||||
| `text` | Tool status text (displayed in trigger) |
|
||||
| `icon` | Icon name |
|
||||
| `loading` | Show loading spinner on icon |
|
||||
| `streaming` | Whether tool is actively running |
|
||||
| `suffix` | Secondary text after label |
|
||||
| `variant` | `'inline'` (default), `'card'` |
|
||||
| `chevron` | `'trailing'` (default), `'leading'` |
|
||||
|
||||
Use `isToolStreaming(part)` from `@nuxt/ui/utils/ai` to determine if a tool is still running.
|
||||
|
||||
### ChatShimmer
|
||||
|
||||
Text shimmer animation for streaming states. Automatically used by ChatReasoning and ChatTool when streaming.
|
||||
|
||||
### ChatPrompt
|
||||
|
||||
Enhanced textarea form for prompts. Accepts all Textarea props.
|
||||
|
||||
| Prop | Description |
|
||||
|---|---|
|
||||
| `v-model` | Input text binding |
|
||||
| `error` | Error from chat instance |
|
||||
| `variant` | `'outline'` (default), `'subtle'`, `'soft'`, `'ghost'`, `'none'` |
|
||||
|
||||
Slots: `#default` (submit button), `#footer` (below input, e.g. model selector)
|
||||
|
||||
### ChatPromptSubmit
|
||||
|
||||
Submit button with automatic status handling (send/stop/reload).
|
||||
|
||||
### ChatPalette
|
||||
|
||||
Layout wrapper for chat inside overlays (Modal, Slideover, Drawer).
|
||||
|
||||
## Chat in a modal
|
||||
|
||||
```vue
|
||||
<UModal v-model:open="isOpen">
|
||||
<template #content>
|
||||
<UChatPalette>
|
||||
<UChatMessages :messages="chat.messages" :status="chat.status" />
|
||||
|
||||
<template #prompt>
|
||||
<UChatPrompt v-model="input" @submit="onSubmit">
|
||||
<UChatPromptSubmit :status="chat.status" />
|
||||
</UChatPrompt>
|
||||
</template>
|
||||
</UChatPalette>
|
||||
</template>
|
||||
</UModal>
|
||||
```
|
||||
|
||||
## With model selector
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const input = ref('')
|
||||
const model = ref('claude-opus-4.6')
|
||||
const models = [
|
||||
{ label: 'Claude Opus 4.6', value: 'claude-opus-4.6', icon: 'i-simple-icons-anthropic' },
|
||||
{ label: 'Gemini 3 Pro', value: 'gemini-3-pro', icon: 'i-simple-icons-googlegemini' },
|
||||
{ label: 'GPT-5', value: 'gpt-5', icon: 'i-simple-icons-openai' }
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UChatPrompt v-model="input" @submit="onSubmit">
|
||||
<UChatPromptSubmit :status="chat.status" />
|
||||
|
||||
<template #footer>
|
||||
<USelect
|
||||
v-model="model"
|
||||
:icon="models.find(m => m.value === model)?.icon"
|
||||
placeholder="Select a model"
|
||||
variant="ghost"
|
||||
:items="models"
|
||||
/>
|
||||
</template>
|
||||
</UChatPrompt>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Conversation sidebar
|
||||
|
||||
```vue [layouts/dashboard.vue]
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<template #header>
|
||||
<UButton icon="i-lucide-plus" label="New chat" block />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<UNavigationMenu :items="conversations" orientation="vertical" />
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
220
.claude/skills/nuxt-ui/references/layouts/dashboard.md
Normal file
220
.claude/skills/nuxt-ui/references/layouts/dashboard.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# Dashboard Layout
|
||||
|
||||
Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars.
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UApp
|
||||
└── NuxtLayout (dashboard)
|
||||
└── UDashboardGroup
|
||||
├── UDashboardSidebar
|
||||
│ ├── #header (logo, search button)
|
||||
│ ├── #default (navigation) — receives { collapsed } slot prop
|
||||
│ └── #footer (user menu)
|
||||
└── NuxtPage
|
||||
└── UDashboardPanel
|
||||
├── #header → UDashboardNavbar + UDashboardToolbar
|
||||
├── #body (scrollable content)
|
||||
└── #footer (optional)
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```vue [layouts/dashboard.vue]
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [{
|
||||
label: 'Home',
|
||||
icon: 'i-lucide-house',
|
||||
to: '/dashboard'
|
||||
}, {
|
||||
label: 'Inbox',
|
||||
icon: 'i-lucide-inbox',
|
||||
to: '/dashboard/inbox'
|
||||
}, {
|
||||
label: 'Users',
|
||||
icon: 'i-lucide-users',
|
||||
to: '/dashboard/users'
|
||||
}, {
|
||||
label: 'Settings',
|
||||
icon: 'i-lucide-settings',
|
||||
to: '/dashboard/settings'
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<template #header="{ collapsed }">
|
||||
<UDashboardSearchButton :collapsed="collapsed" />
|
||||
</template>
|
||||
|
||||
<template #default="{ collapsed }">
|
||||
<UNavigationMenu
|
||||
:items="items"
|
||||
orientation="vertical"
|
||||
:ui="{ link: collapsed ? 'justify-center' : undefined }"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #footer="{ collapsed }">
|
||||
<UButton
|
||||
:icon="collapsed ? 'i-lucide-log-out' : undefined"
|
||||
:label="collapsed ? undefined : 'Sign out'"
|
||||
color="neutral"
|
||||
variant="ghost"
|
||||
block
|
||||
/>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Page
|
||||
|
||||
```vue [pages/dashboard/index.vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'dashboard' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Home">
|
||||
<template #right>
|
||||
<UButton icon="i-lucide-plus" label="New" />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<!-- Page content -->
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Key components
|
||||
|
||||
### DashboardGroup
|
||||
|
||||
Root layout wrapper. Manages sidebar state and persistence.
|
||||
|
||||
| Prop | Default | Description |
|
||||
|---|---|---|
|
||||
| `storage` | `'cookie'` | State persistence: `'cookie'`, `'localStorage'`, `false` |
|
||||
| `storage-key` | `'dashboard'` | Storage key name |
|
||||
| `unit` | `'percentages'` | Size unit: `'percentages'` or `'pixels'` |
|
||||
|
||||
### DashboardSidebar
|
||||
|
||||
Resizable, collapsible sidebar. Must be inside `DashboardGroup`.
|
||||
|
||||
| Prop | Default | Description |
|
||||
|---|---|---|
|
||||
| `resizable` | `false` | Enable resize by dragging |
|
||||
| `collapsible` | `false` | Enable collapse when dragged to edge |
|
||||
| `side` | `'left'` | `'left'` or `'right'` |
|
||||
| `mode` | `'slideover'` | Mobile menu mode: `'modal'`, `'slideover'`, `'drawer'` |
|
||||
|
||||
Slots receive `{ collapsed }` prop. Control state: `v-model:collapsed`, `v-model:open` (mobile).
|
||||
|
||||
### DashboardPanel
|
||||
|
||||
Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw) slots.
|
||||
|
||||
| Prop | Default | Description |
|
||||
|---|---|---|
|
||||
| `id` | `—` | Unique ID (required for multi-panel) |
|
||||
| `resizable` | `false` | Enable resize by dragging |
|
||||
|
||||
### DashboardNavbar / DashboardToolbar
|
||||
|
||||
Navbar has `#left`, `#default`, `#right` slots and a `title` prop. Toolbar has the same slots for filters/actions below the navbar.
|
||||
|
||||
## Multi-panel (list-detail)
|
||||
|
||||
```vue [pages/dashboard/inbox.vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'dashboard' })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel id="inbox-list" resizable>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Inbox" />
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Email list -->
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
|
||||
<UDashboardPanel id="inbox-detail" class="hidden lg:flex">
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Message" />
|
||||
</template>
|
||||
<template #body>
|
||||
<!-- Email content -->
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## With toolbar
|
||||
|
||||
```vue
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Users" />
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<UInput icon="i-lucide-search" placeholder="Search..." />
|
||||
</template>
|
||||
<template #right>
|
||||
<USelect :items="['All', 'Active', 'Inactive']" />
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
```
|
||||
|
||||
## With search
|
||||
|
||||
```vue [layouts/dashboard.vue]
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar>
|
||||
<template #header>
|
||||
<UDashboardSearchButton />
|
||||
</template>
|
||||
<!-- ... -->
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
|
||||
<UDashboardSearch :groups="searchGroups" />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Right sidebar
|
||||
|
||||
```vue
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<!-- Left sidebar -->
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
|
||||
<UDashboardSidebar side="right" resizable>
|
||||
<!-- Right sidebar -->
|
||||
</UDashboardSidebar>
|
||||
</UDashboardGroup>
|
||||
```
|
||||
141
.claude/skills/nuxt-ui/references/layouts/docs.md
Normal file
141
.claude/skills/nuxt-ui/references/layouts/docs.md
Normal file
@@ -0,0 +1,141 @@
|
||||
# Docs Layout
|
||||
|
||||
Build documentation sites with sidebar navigation, table of contents, and surround links.
|
||||
|
||||
> Requires `@nuxt/content` module for navigation, search, and TOC.
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UApp
|
||||
├── UHeader
|
||||
├── UMain
|
||||
│ └── NuxtLayout (docs)
|
||||
│ └── UPage
|
||||
│ ├── #left → UPageAside → UContentNavigation
|
||||
│ └── NuxtPage
|
||||
│ ├── UPageHeader
|
||||
│ ├── UPageBody → ContentRenderer + UContentSurround
|
||||
│ └── #right → UContentToc
|
||||
└── UFooter
|
||||
```
|
||||
|
||||
## App shell
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const { data: navigation } = await useAsyncData('navigation', () => queryCollectionNavigation('docs'))
|
||||
|
||||
provide('navigation', navigation)
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [{
|
||||
label: 'Docs',
|
||||
to: '/docs/getting-started',
|
||||
active: route.path.startsWith('/docs')
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" />
|
||||
|
||||
<template #right>
|
||||
<UContentSearchButton />
|
||||
<UColorModeButton />
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UMain>
|
||||
<NuxtLayout>
|
||||
<NuxtPage />
|
||||
</NuxtLayout>
|
||||
</UMain>
|
||||
|
||||
<UFooter />
|
||||
|
||||
<UContentSearch :navigation="navigation" />
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Layout
|
||||
|
||||
```vue [layouts/docs.vue]
|
||||
<script setup lang="ts">
|
||||
import type { ContentNavigationItem } from '@nuxt/content'
|
||||
|
||||
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<template #left>
|
||||
<UPageAside>
|
||||
<UContentNavigation :navigation="navigation" />
|
||||
</UPageAside>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Page
|
||||
|
||||
```vue [pages/docs/[...slug].vue]
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
definePageMeta({ layout: 'docs' })
|
||||
|
||||
const { data: page } = await useAsyncData(route.path, () => {
|
||||
return queryCollection('docs').path(route.path).first()
|
||||
})
|
||||
|
||||
const { data: surround } = await useAsyncData(`${route.path}-surround`, () => {
|
||||
return queryCollectionItemSurroundings('docs', route.path)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader :title="page.title" :description="page.description" />
|
||||
|
||||
<UPageBody>
|
||||
<ContentRenderer :value="page" />
|
||||
|
||||
<USeparator />
|
||||
|
||||
<UContentSurround :surround="surround" />
|
||||
</UPageBody>
|
||||
|
||||
<template #right>
|
||||
<UContentToc :links="page.body.toc.links" />
|
||||
</template>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
> The outer `UPage` in the layout handles the left sidebar. The inner `UPage` in the page handles the right sidebar. They nest correctly.
|
||||
|
||||
## Key components
|
||||
|
||||
- `UPage` — Multi-column grid layout with `#left`, `#default`, `#right` slots
|
||||
- `UPageAside` — Sticky sidebar wrapper (visible from `lg` breakpoint)
|
||||
- `UPageHeader` — Page title and description
|
||||
- `UPageBody` — Main content area
|
||||
- `UContentNavigation` — Sidebar navigation tree
|
||||
- `UContentToc` — Table of contents
|
||||
- `UContentSurround` — Prev/next links
|
||||
- `UContentSearch` / `UContentSearchButton` — Search command palette
|
||||
- `UPageAnchors` — Simpler alternative to full TOC
|
||||
- `UPageLinks` — Related resource links
|
||||
168
.claude/skills/nuxt-ui/references/layouts/editor.md
Normal file
168
.claude/skills/nuxt-ui/references/layouts/editor.md
Normal file
@@ -0,0 +1,168 @@
|
||||
# Editor Layout
|
||||
|
||||
Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop.
|
||||
|
||||
## Component tree
|
||||
|
||||
```
|
||||
UApp
|
||||
├── UHeader
|
||||
├── UMain
|
||||
│ └── NuxtPage
|
||||
│ └── UContainer
|
||||
│ └── UEditor
|
||||
│ ├── UEditorToolbar (fixed / bubble / floating)
|
||||
│ ├── UEditorDragHandle
|
||||
│ ├── UEditorSuggestionMenu
|
||||
│ ├── UEditorMentionMenu
|
||||
│ └── UEditorEmojiMenu
|
||||
└── UFooter
|
||||
```
|
||||
|
||||
## Page
|
||||
|
||||
```vue [pages/editor.vue]
|
||||
<script setup lang="ts">
|
||||
const content = ref({
|
||||
type: 'doc',
|
||||
content: [
|
||||
{
|
||||
type: 'heading',
|
||||
attrs: { level: 1 },
|
||||
content: [{ type: 'text', text: 'Hello World' }]
|
||||
},
|
||||
{
|
||||
type: 'paragraph',
|
||||
content: [{ type: 'text', text: 'Start writing...' }]
|
||||
}
|
||||
]
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader title="Editor">
|
||||
<template #actions>
|
||||
<UButton label="Save" icon="i-lucide-save" />
|
||||
</template>
|
||||
</UPageHeader>
|
||||
|
||||
<UPageBody>
|
||||
<UEditor v-slot="{ editor }" v-model="content">
|
||||
<UEditorToolbar :editor="editor" />
|
||||
<UEditorSuggestionMenu :editor="editor" />
|
||||
<UEditorMentionMenu
|
||||
:editor="editor"
|
||||
:items="[
|
||||
{ label: 'Benjamin', avatar: { src: 'https://github.com/benjamincanac.png' } },
|
||||
{ label: 'Sébastien', avatar: { src: 'https://github.com/atinux.png' } }
|
||||
]"
|
||||
/>
|
||||
<UEditorEmojiMenu :editor="editor" />
|
||||
<UEditorDragHandle :editor="editor" />
|
||||
</UEditor>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
> If you encounter prosemirror-related errors, add prosemirror packages to `vite.optimizeDeps.include` in `nuxt.config.ts`.
|
||||
|
||||
## Key components
|
||||
|
||||
- `UEditor` — Rich text editor (`v-model` accepts JSON, HTML, or markdown via `content-type` prop)
|
||||
- `UEditorToolbar` — Toolbar with `layout`: `'fixed'` (default), `'bubble'` (on selection), `'floating'` (on empty lines)
|
||||
- `UEditorDragHandle` — Block drag-and-drop handle
|
||||
- `UEditorSuggestionMenu` — Slash command menu
|
||||
- `UEditorMentionMenu` — @ mention menu
|
||||
- `UEditorEmojiMenu` — Emoji picker
|
||||
|
||||
## Toolbar modes
|
||||
|
||||
```vue
|
||||
<!-- Fixed (default) -->
|
||||
<UEditor v-model="content">
|
||||
<UEditorToolbar />
|
||||
</UEditor>
|
||||
|
||||
<!-- Bubble (appears on text selection) -->
|
||||
<UEditor v-model="content">
|
||||
<UEditorToolbar layout="bubble" />
|
||||
</UEditor>
|
||||
|
||||
<!-- Floating (appears on empty lines) -->
|
||||
<UEditor v-model="content">
|
||||
<UEditorToolbar layout="floating" />
|
||||
</UEditor>
|
||||
```
|
||||
|
||||
## Content types
|
||||
|
||||
```vue
|
||||
<!-- JSON (default) -->
|
||||
<UEditor v-model="jsonContent" />
|
||||
|
||||
<!-- HTML -->
|
||||
<UEditor v-model="htmlContent" content-type="html" />
|
||||
|
||||
<!-- Markdown -->
|
||||
<UEditor v-model="markdownContent" content-type="markdown" />
|
||||
```
|
||||
|
||||
## With document sidebar
|
||||
|
||||
Combine with Dashboard components for a multi-document editor with a sidebar.
|
||||
|
||||
```vue [layouts/editor.vue]
|
||||
<template>
|
||||
<UDashboardGroup>
|
||||
<UDashboardSidebar collapsible resizable>
|
||||
<template #header>
|
||||
<UButton icon="i-lucide-plus" label="New document" block />
|
||||
</template>
|
||||
|
||||
<template #default>
|
||||
<UNavigationMenu
|
||||
:items="documents.map(doc => ({
|
||||
label: doc.title,
|
||||
to: `/editor/${doc.id}`,
|
||||
icon: 'i-lucide-file-text'
|
||||
}))"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</template>
|
||||
</UDashboardSidebar>
|
||||
|
||||
<slot />
|
||||
</UDashboardGroup>
|
||||
</template>
|
||||
```
|
||||
|
||||
```vue [pages/editor/[id].vue]
|
||||
<script setup lang="ts">
|
||||
definePageMeta({ layout: 'editor' })
|
||||
|
||||
const content = ref({ type: 'doc', content: [] })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Editor">
|
||||
<template #right>
|
||||
<UButton label="Save" icon="i-lucide-save" />
|
||||
</template>
|
||||
</UDashboardNavbar>
|
||||
</template>
|
||||
|
||||
<UContainer class="py-8">
|
||||
<UEditor v-slot="{ editor }" v-model="content">
|
||||
<UEditorToolbar :editor="editor" />
|
||||
<UEditorSuggestionMenu :editor="editor" />
|
||||
<UEditorEmojiMenu :editor="editor" />
|
||||
<UEditorDragHandle :editor="editor" />
|
||||
</UEditor>
|
||||
</UContainer>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
260
.claude/skills/nuxt-ui/references/layouts/page.md
Normal file
260
.claude/skills/nuxt-ui/references/layouts/page.md
Normal file
@@ -0,0 +1,260 @@
|
||||
# Page Layout
|
||||
|
||||
Build public-facing pages — landing, blog, changelog, pricing — using the Header + Main + Footer shell with Page components.
|
||||
|
||||
## App shell
|
||||
|
||||
```vue [app.vue]
|
||||
<script setup lang="ts">
|
||||
import type { NavigationMenuItem } from '@nuxt/ui'
|
||||
|
||||
const items = computed<NavigationMenuItem[]>(() => [{
|
||||
label: 'Features',
|
||||
to: '#features'
|
||||
}, {
|
||||
label: 'Pricing',
|
||||
to: '/pricing'
|
||||
}, {
|
||||
label: 'Blog',
|
||||
to: '/blog'
|
||||
}])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UApp>
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" />
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<UButton label="Sign in" color="neutral" variant="ghost" />
|
||||
<UButton label="Get started" />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
|
||||
</template>
|
||||
</UHeader>
|
||||
|
||||
<UMain>
|
||||
<NuxtPage />
|
||||
</UMain>
|
||||
|
||||
<UFooter>
|
||||
<template #left>
|
||||
<p class="text-muted text-sm">Copyright © {{ new Date().getFullYear() }}</p>
|
||||
</template>
|
||||
<template #right>
|
||||
<UButton icon="i-simple-icons-github" color="neutral" variant="ghost" to="https://github.com" target="_blank" />
|
||||
</template>
|
||||
</UFooter>
|
||||
</UApp>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Landing page
|
||||
|
||||
```vue [pages/index.vue]
|
||||
<template>
|
||||
<UPageHero
|
||||
title="Build faster with Nuxt UI"
|
||||
description="A comprehensive Vue UI component library."
|
||||
:links="[
|
||||
{ label: 'Get started', to: '/docs', icon: 'i-lucide-square-play' },
|
||||
{ label: 'Learn more', color: 'neutral', variant: 'subtle', trailingIcon: 'i-lucide-arrow-right' }
|
||||
]"
|
||||
orientation="horizontal"
|
||||
>
|
||||
<img src="/hero-image.png" alt="App screenshot" class="rounded-lg shadow-2xl ring ring-default" />
|
||||
</UPageHero>
|
||||
|
||||
<UPageSection
|
||||
id="features"
|
||||
headline="Features"
|
||||
title="Everything you need"
|
||||
description="A comprehensive suite of components and utilities."
|
||||
:features="[
|
||||
{ title: 'Accessible', description: 'Built on Reka UI with full ARIA support.', icon: 'i-lucide-accessibility' },
|
||||
{ title: 'Customizable', description: 'Tailwind Variants theming with full control.', icon: 'i-lucide-palette' },
|
||||
{ title: 'Responsive', description: 'Mobile-first components.', icon: 'i-lucide-monitor-smartphone' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<UPageCTA
|
||||
title="Trusted by thousands of developers"
|
||||
description="Join the community and start building today."
|
||||
:links="[
|
||||
{ label: 'Get started', color: 'neutral' },
|
||||
{ label: 'Star on GitHub', color: 'neutral', variant: 'subtle', trailingIcon: 'i-lucide-arrow-right' }
|
||||
]"
|
||||
/>
|
||||
|
||||
<UPageSection id="pricing" headline="Pricing" title="Simple, transparent pricing">
|
||||
<UPricingPlans
|
||||
:plans="[
|
||||
{ title: 'Free', price: '$0', description: 'For personal projects', features: ['10 components', 'Community support'] },
|
||||
{ title: 'Pro', price: '$99', description: 'For teams', features: ['All components', 'Priority support'], highlight: true },
|
||||
{ title: 'Enterprise', price: 'Custom', description: 'For large teams', features: ['Custom components', 'Dedicated support'] }
|
||||
]"
|
||||
/>
|
||||
</UPageSection>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Blog listing
|
||||
|
||||
```vue [pages/blog/index.vue]
|
||||
<script setup lang="ts">
|
||||
const { data: posts } = await useAsyncData('posts', () => queryCollection('posts').all())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHero title="Blog" description="The latest news and updates from our team." />
|
||||
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UBlogPosts>
|
||||
<UBlogPost
|
||||
v-for="(post, index) in posts"
|
||||
:key="index"
|
||||
v-bind="post"
|
||||
:to="post.path"
|
||||
/>
|
||||
</UBlogPosts>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Blog article
|
||||
|
||||
```vue [pages/blog/[slug].vue]
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const { data: post } = await useAsyncData(route.path, () => {
|
||||
return queryCollection('posts').path(route.path).first()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHeader :title="post.title" :description="post.description" />
|
||||
|
||||
<UPageBody>
|
||||
<ContentRenderer :value="post" />
|
||||
</UPageBody>
|
||||
|
||||
<template #right>
|
||||
<UContentToc :links="post.body.toc.links" />
|
||||
</template>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Changelog
|
||||
|
||||
```vue [pages/changelog.vue]
|
||||
<script setup lang="ts">
|
||||
const { data: versions } = await useAsyncData('versions', () => queryCollection('changelog').all())
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPage>
|
||||
<UPageHero title="Changelog" />
|
||||
|
||||
<UPageBody>
|
||||
<UContainer>
|
||||
<UChangelogVersions>
|
||||
<UChangelogVersion v-for="(version, index) in versions" :key="index" v-bind="version" />
|
||||
</UChangelogVersions>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Key components
|
||||
|
||||
### Page sections
|
||||
|
||||
- `UPageHero` — Hero with title, description, links, and optional media (`orientation`: horizontal/vertical)
|
||||
- `UPageSection` — Content section with headline, title, description, and `features` grid
|
||||
- `UPageCTA` — Call to action block
|
||||
- `UPageHeader` — Page title and description
|
||||
- `UPageBody` — Main content area with prose styling
|
||||
|
||||
### Grids & cards
|
||||
|
||||
- `UPageGrid` / `UPageColumns` — Grid layouts
|
||||
- `UPageCard` — Content card for grids
|
||||
- `UPageFeature` — Individual feature item
|
||||
- `UPageLogos` — Logo wall
|
||||
|
||||
### Blog & changelog
|
||||
|
||||
- `UBlogPosts` — Responsive grid of posts (`orientation`: horizontal/vertical)
|
||||
- `UBlogPost` — Individual post card
|
||||
- `UChangelogVersions` / `UChangelogVersion` — Changelog entries
|
||||
|
||||
### Pricing
|
||||
|
||||
- `UPricingPlans` — Pricing plan cards
|
||||
- `UPricingTable` — Feature comparison table
|
||||
|
||||
### Footer
|
||||
|
||||
- `UFooterColumns` — Multi-column footer with link groups
|
||||
|
||||
## Variations
|
||||
|
||||
### Alternating sections
|
||||
|
||||
```vue
|
||||
<UPageSection title="Feature A" orientation="horizontal">
|
||||
<img src="/feature-a.png" />
|
||||
</UPageSection>
|
||||
|
||||
<UPageSection title="Feature B" orientation="horizontal" reverse>
|
||||
<img src="/feature-b.png" />
|
||||
</UPageSection>
|
||||
```
|
||||
|
||||
### Feature grid
|
||||
|
||||
```vue
|
||||
<UPageSection headline="Features" title="Why choose us">
|
||||
<UPageGrid>
|
||||
<UPageCard v-for="feature in features" :key="feature.title" v-bind="feature" />
|
||||
</UPageGrid>
|
||||
</UPageSection>
|
||||
```
|
||||
|
||||
### Blog with sidebar
|
||||
|
||||
```vue [layouts/blog.vue]
|
||||
<template>
|
||||
<UPage>
|
||||
<template #left>
|
||||
<UPageAside>
|
||||
<UNavigationMenu
|
||||
:items="[
|
||||
{ label: 'All posts', to: '/blog', icon: 'i-lucide-newspaper' },
|
||||
{ label: 'Tutorials', to: '/blog/tutorials', icon: 'i-lucide-graduation-cap' },
|
||||
{ label: 'Announcements', to: '/blog/announcements', icon: 'i-lucide-megaphone' }
|
||||
]"
|
||||
orientation="vertical"
|
||||
/>
|
||||
</UPageAside>
|
||||
</template>
|
||||
|
||||
<slot />
|
||||
</UPage>
|
||||
</template>
|
||||
```
|
||||
427
.claude/skills/nuxt-ui/references/theming.md
Normal file
427
.claude/skills/nuxt-ui/references/theming.md
Normal file
@@ -0,0 +1,427 @@
|
||||
# Theming
|
||||
|
||||
## Semantic colors
|
||||
|
||||
| Color | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `primary` | green | CTAs, active states, brand |
|
||||
| `secondary` | blue | Secondary actions |
|
||||
| `success` | green | Success messages |
|
||||
| `info` | blue | Informational |
|
||||
| `warning` | yellow | Warnings |
|
||||
| `error` | red | Errors, destructive actions |
|
||||
| `neutral` | slate | Text, borders, disabled |
|
||||
|
||||
## Configuring colors
|
||||
|
||||
```ts
|
||||
// Nuxt — app.config.ts
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'indigo',
|
||||
secondary: 'violet',
|
||||
success: 'emerald',
|
||||
error: 'rose',
|
||||
neutral: 'zinc'
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vue — vite.config.ts
|
||||
ui({
|
||||
ui: {
|
||||
colors: { primary: 'indigo', secondary: 'violet', neutral: 'zinc' }
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
You can only use colors that exist in your theme — either [Tailwind's default colors](https://tailwindcss.com/docs/colors) or custom colors defined with `@theme`.
|
||||
|
||||
## Adding custom colors
|
||||
|
||||
1. Define all 11 shades in CSS:
|
||||
|
||||
```css
|
||||
/* assets/css/main.css */
|
||||
@theme static {
|
||||
--color-brand-50: #fef2f2;
|
||||
--color-brand-100: #fee2e2;
|
||||
--color-brand-200: #fecaca;
|
||||
--color-brand-300: #fca5a5;
|
||||
--color-brand-400: #f87171;
|
||||
--color-brand-500: #ef4444;
|
||||
--color-brand-600: #dc2626;
|
||||
--color-brand-700: #b91c1c;
|
||||
--color-brand-800: #991b1b;
|
||||
--color-brand-900: #7f1d1d;
|
||||
--color-brand-950: #450a0a;
|
||||
}
|
||||
```
|
||||
|
||||
2. Assign it as a semantic color value: `ui: { colors: { primary: 'brand' } }`
|
||||
|
||||
You can only use colors that have all shades defined — either from Tailwind's defaults or custom `@theme` definitions.
|
||||
|
||||
### Extending with new semantic color names
|
||||
|
||||
If you need a new semantic color beyond the defaults (e.g., `tertiary`), register it in `theme.colors`:
|
||||
|
||||
```ts
|
||||
// Nuxt — nuxt.config.ts
|
||||
export default defineNuxtConfig({
|
||||
ui: {
|
||||
theme: {
|
||||
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vue — vite.config.ts
|
||||
ui({
|
||||
theme: {
|
||||
colors: ['primary', 'secondary', 'tertiary', 'info', 'success', 'warning', 'error']
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Then assign it: `ui: { colors: { tertiary: 'indigo' } }` and use it via the `color` prop: `<UButton color="tertiary">`.
|
||||
|
||||
## CSS utilities
|
||||
|
||||
### Text
|
||||
|
||||
| Class | Use | Light value | Dark value |
|
||||
|---|---|---|---|
|
||||
| `text-default` | Body text | `neutral-700` | `neutral-200` |
|
||||
| `text-muted` | Secondary text | `neutral-500` | `neutral-400` |
|
||||
| `text-dimmed` | Placeholders, hints | `neutral-400` | `neutral-500` |
|
||||
| `text-toned` | Subtitles | `neutral-600` | `neutral-300` |
|
||||
| `text-highlighted` | Headings, emphasis | `neutral-900` | `white` |
|
||||
| `text-inverted` | On dark/light backgrounds | `white` | `neutral-900` |
|
||||
|
||||
### Background
|
||||
|
||||
| Class | Use | Light value | Dark value |
|
||||
|---|---|---|---|
|
||||
| `bg-default` | Page background | `white` | `neutral-900` |
|
||||
| `bg-muted` | Subtle sections | `neutral-50` | `neutral-800` |
|
||||
| `bg-elevated` | Cards, modals | `neutral-100` | `neutral-800` |
|
||||
| `bg-accented` | Hover states | `neutral-200` | `neutral-700` |
|
||||
| `bg-inverted` | Inverted sections | `neutral-900` | `white` |
|
||||
|
||||
### Border
|
||||
|
||||
| Class | Use | Light value | Dark value |
|
||||
|---|---|---|---|
|
||||
| `border-default` | Default borders | `neutral-200` | `neutral-800` |
|
||||
| `border-muted` | Subtle borders | `neutral-200` | `neutral-700` |
|
||||
| `border-accented` | Emphasized borders | `neutral-300` | `neutral-700` |
|
||||
| `border-inverted` | Inverted borders | `neutral-900` | `white` |
|
||||
|
||||
### Semantic color utilities
|
||||
|
||||
Each semantic color (`primary`, `secondary`, `success`, `info`, `warning`, `error`) is available as a Tailwind utility: `text-primary`, `bg-primary`, `border-primary`, `ring-primary`, etc.
|
||||
|
||||
They resolve to shade **500** in light mode and shade **400** in dark mode (via `--ui-<color>` CSS variables). This is generated at runtime by the colors plugin — you don't need to write dark-mode variants manually.
|
||||
|
||||
To adjust which shade is used, override `--ui-primary` (or any semantic color) in your `main.css`:
|
||||
|
||||
```css
|
||||
:root { --ui-primary: var(--ui-color-primary-600); }
|
||||
.dark { --ui-primary: var(--ui-color-primary-300); }
|
||||
```
|
||||
|
||||
### CSS variables
|
||||
|
||||
All customizable in `main.css`:
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ui-radius: 0.25rem; /* base radius for all components */
|
||||
--ui-container: 80rem; /* UContainer max-width */
|
||||
--ui-header-height: 4rem; /* UHeader height */
|
||||
--ui-primary: var(--ui-color-primary-500); /* adjust shade used */
|
||||
}
|
||||
|
||||
.dark {
|
||||
--ui-primary: var(--ui-color-primary-400);
|
||||
}
|
||||
```
|
||||
|
||||
### Solid colors (black/white)
|
||||
|
||||
```css
|
||||
:root { --ui-primary: black; }
|
||||
.dark { --ui-primary: white; }
|
||||
```
|
||||
|
||||
## Component theme customization
|
||||
|
||||
### How it works
|
||||
|
||||
Components are styled with [Tailwind Variants](https://www.tailwind-variants.org/). The theme defines:
|
||||
|
||||
- **`slots`** — named style targets (e.g., `root`, `base`, `label`, `leadingIcon`)
|
||||
- **`variants`** — styles applied based on props (e.g., `color`, `variant`, `size`)
|
||||
- **`compoundVariants`** — styles for specific prop combinations (e.g., `color: 'primary'` + `variant: 'outline'`)
|
||||
- **`defaultVariants`** — default prop values when none are specified
|
||||
|
||||
### Override priority
|
||||
|
||||
**`ui` prop / `class` prop > global config > theme defaults**
|
||||
|
||||
The `ui` prop overrides slots **after** variants are computed. If the `size: 'md'` variant applies `size-5` to `trailingIcon`, and you set `:ui="{ trailingIcon: 'size-3' }"`, the `size-3` wins.
|
||||
|
||||
Tailwind Variants uses [tailwind-merge](https://github.com/dcastil/tailwind-merge) under the hood so conflicting classes are resolved automatically.
|
||||
|
||||
### Understanding the generated theme
|
||||
|
||||
Every component's full resolved theme is generated at build time. Always read this file before customizing a component — it shows exactly what classes are applied where.
|
||||
|
||||
- **Nuxt**: `.nuxt/ui/<component>.ts`
|
||||
- **Vue**: `node_modules/.nuxt-ui/ui/<component>.ts`
|
||||
|
||||
For example, the card theme:
|
||||
|
||||
```ts
|
||||
{
|
||||
slots: {
|
||||
root: "rounded-lg overflow-hidden",
|
||||
header: "p-4 sm:px-6",
|
||||
body: "p-4 sm:p-6",
|
||||
footer: "p-4 sm:px-6"
|
||||
},
|
||||
variants: {
|
||||
variant: {
|
||||
outline: { root: "bg-default ring ring-default divide-y divide-default" },
|
||||
soft: { root: "bg-elevated/50 divide-y divide-default" }
|
||||
}
|
||||
},
|
||||
defaultVariants: { variant: "outline" }
|
||||
}
|
||||
```
|
||||
|
||||
### Global config
|
||||
|
||||
Override the theme for all instances of a component:
|
||||
|
||||
```ts
|
||||
// Nuxt — app.config.ts
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
slots: {
|
||||
base: 'font-bold rounded-full'
|
||||
},
|
||||
variants: {
|
||||
size: {
|
||||
md: { leadingIcon: 'size-4' }
|
||||
}
|
||||
},
|
||||
compoundVariants: [{
|
||||
color: 'neutral',
|
||||
variant: 'outline',
|
||||
class: { base: 'ring-2' }
|
||||
}],
|
||||
defaultVariants: {
|
||||
color: 'neutral',
|
||||
variant: 'outline'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
```ts
|
||||
// Vue — vite.config.ts
|
||||
ui({
|
||||
ui: {
|
||||
button: {
|
||||
slots: { base: 'font-bold rounded-full' },
|
||||
defaultVariants: { color: 'neutral', variant: 'outline' }
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Per-instance (`ui` prop)
|
||||
|
||||
Overrides slots after variant computation:
|
||||
|
||||
```vue
|
||||
<UButton :ui="{ base: 'font-mono', trailingIcon: 'size-3 rotate-90' }" />
|
||||
<UCard :ui="{ root: 'shadow-xl', body: 'p-8' }" />
|
||||
```
|
||||
|
||||
### Per-instance (`class` prop)
|
||||
|
||||
Overrides the `root` or `base` slot:
|
||||
|
||||
```vue
|
||||
<UButton class="rounded-none">Square</UButton>
|
||||
```
|
||||
|
||||
Components without slots (e.g., `UContainer`, `USkeleton`, `UMain`) only have the `class` prop.
|
||||
|
||||
### Theme structure patterns
|
||||
|
||||
**Slots-based** (most components — `slots` is an object in the generated theme):
|
||||
|
||||
```ts
|
||||
// global config
|
||||
ui: {
|
||||
button: {
|
||||
slots: { base: 'font-bold' }
|
||||
}
|
||||
}
|
||||
// per instance
|
||||
<UButton :ui="{ base: 'font-bold' }" />
|
||||
```
|
||||
|
||||
**Flat base** (`base` is a top-level string in the generated theme):
|
||||
|
||||
```ts
|
||||
// global config
|
||||
ui: {
|
||||
container: {
|
||||
base: 'max-w-lg'
|
||||
}
|
||||
}
|
||||
// per instance — class prop only
|
||||
<UContainer class="max-w-lg" />
|
||||
```
|
||||
|
||||
Always check the generated theme file to see which pattern applies.
|
||||
|
||||
## Dark mode
|
||||
|
||||
```ts
|
||||
const colorMode = useColorMode()
|
||||
colorMode.preference = 'dark' // 'light', 'dark', 'system'
|
||||
```
|
||||
|
||||
```vue
|
||||
<UColorModeButton /> <!-- Toggle -->
|
||||
<UColorModeSelect /> <!-- Dropdown -->
|
||||
```
|
||||
|
||||
## Fonts
|
||||
|
||||
```css
|
||||
/* assets/css/main.css */
|
||||
@theme {
|
||||
--font-sans: 'Public Sans', system-ui, sans-serif;
|
||||
--font-mono: 'JetBrains Mono', monospace;
|
||||
}
|
||||
```
|
||||
|
||||
In Nuxt, fonts defined with `@theme` are automatically loaded by the `@nuxt/fonts` module.
|
||||
|
||||
## Brand customization playbook
|
||||
|
||||
Follow these steps to fully rebrand Nuxt UI (e.g., "make a Ghibli theme", "match our corporate brand"):
|
||||
|
||||
### Step 1 — Define the color palette
|
||||
|
||||
Pick colors that match the brand. Map them to semantic roles:
|
||||
|
||||
```ts
|
||||
// app.config.ts (Nuxt) or vite.config.ts (Vue)
|
||||
ui: {
|
||||
colors: {
|
||||
primary: 'emerald', // brand accent
|
||||
secondary: 'amber', // secondary accent
|
||||
success: 'green',
|
||||
info: 'sky',
|
||||
warning: 'orange',
|
||||
error: 'rose',
|
||||
neutral: 'stone' // affects all text, borders, backgrounds
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
If no Tailwind default color fits, define custom shades in CSS (see [Adding custom colors](#adding-custom-colors)):
|
||||
|
||||
```css
|
||||
@theme static {
|
||||
--color-forest-50: #f0fdf4;
|
||||
/* ... all 11 shades (50–950) ... */
|
||||
--color-forest-950: #052e16;
|
||||
}
|
||||
```
|
||||
|
||||
Then use it: `primary: 'forest'`.
|
||||
|
||||
### Step 2 — Set fonts
|
||||
|
||||
```css
|
||||
/* assets/css/main.css */
|
||||
@theme {
|
||||
--font-sans: 'Quicksand', system-ui, sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
### Step 3 — Adjust CSS variables
|
||||
|
||||
```css
|
||||
:root {
|
||||
--ui-radius: 0.75rem; /* rounder = softer/playful, smaller = sharper/corporate */
|
||||
--ui-primary: var(--ui-color-primary-600); /* adjust which shade is used */
|
||||
}
|
||||
|
||||
.dark {
|
||||
--ui-primary: var(--ui-color-primary-400);
|
||||
}
|
||||
```
|
||||
|
||||
### Step 4 — Override key components globally
|
||||
|
||||
Read the generated theme files to find slot names, then apply global overrides:
|
||||
|
||||
```ts
|
||||
// app.config.ts (Nuxt) or vite.config.ts (Vue)
|
||||
ui: {
|
||||
// ... colors from Step 1
|
||||
button: {
|
||||
slots: {
|
||||
base: 'rounded-full font-semibold'
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: 'soft'
|
||||
}
|
||||
},
|
||||
card: {
|
||||
slots: {
|
||||
root: 'rounded-2xl shadow-lg'
|
||||
}
|
||||
},
|
||||
badge: {
|
||||
slots: {
|
||||
base: 'rounded-full'
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> **Tip**: Read `.nuxt/ui/button.ts` (Nuxt) or `node_modules/.nuxt-ui/ui/button.ts` (Vue) to see all available slots and variants before overriding.
|
||||
|
||||
### Step 5 — Verify dark mode
|
||||
|
||||
Check that both modes look correct. Adjust `--ui-primary` shade per mode and test contrast. Use `useColorMode()` to toggle during development.
|
||||
|
||||
### Quick checklist
|
||||
|
||||
| Step | What to change | Where |
|
||||
|---|---|---|
|
||||
| Colors | `primary`, `secondary`, `neutral` | `app.config.ts` / `vite.config.ts` |
|
||||
| Custom palette | 11 shades per color | `main.css` (`@theme static`) |
|
||||
| Fonts | `--font-sans`, `--font-mono` | `main.css` (`@theme`) |
|
||||
| Radius | `--ui-radius` | `main.css` (`:root`) |
|
||||
| Primary shade | `--ui-primary` | `main.css` (`:root` + `.dark`) |
|
||||
| Component shapes | Global slot overrides | `app.config.ts` / `vite.config.ts` |
|
||||
| Dark mode | Verify contrast, adjust variables | `main.css` (`.dark`) |
|
||||
Reference in New Issue
Block a user