Initial commit
This commit is contained in:
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>
|
||||
```
|
||||
Reference in New Issue
Block a user