This commit is contained in:
2026-09-27 11:51:31 +03:00
parent dfe330905c
commit ea5202f080
119 changed files with 79192 additions and 5 deletions
@@ -0,0 +1,263 @@
# Chat Layout
Build AI chat interfaces with message streams, reasoning, tool calling, and Vercel AI SDK integration.
## When to use
- AI chatbot interfaces
- Customer support chat
- Any conversational UI with streaming responses
## Setup
### Install dependencies
**Nuxt:**
```bash
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/nuxt
```
**Vue (Vite):**
```bash
pnpm add ai @ai-sdk/gateway @ai-sdk/vue @comark/vue
```
### Register Comark module
**Nuxt:**
```ts [nuxt.config.ts]
export default defineNuxtConfig({
modules: [
'@nuxt/ui',
'@comark/nuxt'
]
})
```
**Vue (Vite):** No module registration needed, import directly from `@comark/vue`.
> `@comark/nuxt` (or `@comark/vue` for Vue projects) provides the `Comark` component used to render AI responses as streaming Markdown, it incrementally renders tokens as they arrive and automatically enables Nuxt UI's prose components.
### Dark mode for syntax highlighting
When using the `highlight` plugin, add the following CSS to your stylesheet:
```css [main.css]
html.dark .shiki span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
```
### Server endpoint
Using [Vercel AI Gateway](https://vercel.com/ai-gateway) (recommended):
```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { gateway } from '@ai-sdk/gateway'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const result = streamText({
model: gateway('anthropic/claude-sonnet-5'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
```
Or with a direct provider (e.g., `pnpm add @ai-sdk/openai`):
```ts [server/api/chat.post.ts]
import { streamText, convertToModelMessages, toUIMessageStream, createUIMessageStreamResponse } from 'ai'
import { openai } from '@ai-sdk/openai'
export default defineEventHandler(async (event) => {
const { messages } = await readBody(event)
const result = streamText({
model: openai('gpt-5-nano'),
instructions: 'You are a helpful assistant.',
messages: await convertToModelMessages(messages)
})
const stream = toUIMessageStream({ stream: result.stream })
return createUIMessageStreamResponse({ stream })
})
```
## Component tree
```
UDashboardPanel
├── #header → UDashboardNavbar
├── #body → UContainer → UChatMessages
│ ├── #content → UChatReasoning, UChatTool, Comark
│ └── #indicator (loading)
└── #footer → UContainer → UChatPrompt
└── UChatPromptSubmit
```
## Full page chat
```vue [pages/chat/[id].vue]
<script setup lang="ts">
import { isReasoningUIPart, isTextUIPart, isToolUIPart, getToolName } from 'ai'
import { useChat } from '@ai-sdk/vue'
import { isPartStreaming, isToolStreaming } from '@nuxt/ui/utils/ai'
import highlight from '@comark/nuxt/plugins/highlight'
definePageMeta({ layout: 'dashboard' })
const input = ref('')
const { messages, status, error, sendMessage, stop, regenerate } = useChat({
onError(error) {
console.error(error)
}
})
function onSubmit() {
if (!input.value.trim()) return
sendMessage({ text: input.value })
input.value = ''
}
</script>
<template>
<UDashboardPanel>
<template #header>
<UDashboardNavbar title="Chat" />
</template>
<template #body>
<UContainer>
<UChatMessages :messages="messages" :status="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)"
>
<Comark
:markdown="part.text"
:streaming="isPartStreaming(part)"
:plugins="[highlight()]"
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)">
<Comark
v-if="message.role === 'assistant'"
:markdown="part.text"
:streaming="isPartStreaming(part)"
:plugins="[highlight()]"
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="error" @submit="onSubmit">
<UChatPromptSubmit :status="status" @stop="stop()" @reload="regenerate()" />
</UChatPrompt>
</UContainer>
</template>
</UDashboardPanel>
</template>
```
## Key components
- `UChatMessages` — scrollable message list with auto-scroll. Props: `messages`, `status`. Slots: `#content` (per message), `#actions`, `#indicator`.
- `UChatMessage` — individual bubble. Props: `message`, `side` (`'left'`/`'right'`).
- `UChatReasoning` — collapsible reasoning block. Auto-opens during streaming, auto-closes when done. Use `isPartStreaming(part)` from `@nuxt/ui/utils/ai`.
- `UChatTool` — tool invocation status. Use `isToolStreaming(part)`. Variants: `'inline'` (default), `'card'`.
- `UChatPrompt` — enhanced textarea for chat. Its typed API exposes a selected subset of `UTextarea` props, forwards additional attributes to the underlying textarea, and adds chat-specific props such as `error` and `submitOnEnter`.
- `UChatPromptSubmit` — submit button with automatic status handling (send/stop/reload).
- `UChatPalette` — layout wrapper for chat inside overlays.
## Chat in a modal
```vue
<UModal v-model:open="isOpen">
<template #content>
<UChatPalette>
<UChatMessages :messages="messages" :status="status" />
<template #prompt>
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="status" />
</UChatPrompt>
</template>
</UChatPalette>
</template>
</UModal>
```
## With model selector
```vue
<UChatPrompt v-model="input" @submit="onSubmit">
<UChatPromptSubmit :status="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>
```
## Conversation sidebar
Combine with dashboard layout for a ChatGPT-like interface:
```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>
```
@@ -0,0 +1,233 @@
# Dashboard Layout
Build admin interfaces with resizable sidebars, multi-panel layouts, and toolbars.
## When to use
- Admin panels, back-office UIs
- Email clients, project management tools
- Any app with a persistent sidebar and content panels
- Combine with chat or editor layouts for specialized dashboards
## 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
:collapsed="collapsed"
:items="items"
orientation="vertical"
/>
</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 #leading>
<UDashboardSidebarCollapse />
</template>
<template #right>
<UButton icon="i-lucide-plus" label="New" />
</template>
</UDashboardNavbar>
</template>
<template #body>
<!-- Page content -->
</template>
</UDashboardPanel>
</template>
```
### Common mistakes
- Forgetting `definePageMeta({ layout: 'dashboard' })` — the page won't use the dashboard layout without it.
- Putting content directly in `UDashboardPanel` without using `#body` slot — content won't scroll properly.
- Not handling the `collapsed` slot prop — sidebar content should adapt when collapsed (hide labels, center icons).
## Key components
### DashboardGroup
Root wrapper. Manages sidebar state and persistence.
| Prop | Default | Purpose |
| ------------- | ------------- | ------------------------------------- |
| `storage` | `'cookie'` | `'cookie'`, `'localStorage'`, `false` |
| `storage-key` | `'dashboard'` | Storage key name |
### DashboardSidebar
Resizable, collapsible sidebar. Must be inside `DashboardGroup`.
| Prop | Default | Purpose |
| ------------- | ------------- | -------------------------------------------- |
| `resizable` | `false` | Drag to resize |
| `collapsible` | `false` | Collapse when dragged to edge |
| `side` | `'left'` | `'left'` or `'right'` |
| `mode` | `'slideover'` | Mobile: `'modal'`, `'slideover'`, `'drawer'` |
All slots receive `{ collapsed, collapse }` — `collapsed` is the boolean state, `collapse(value)` toggles it programmatically. Use `v-model:collapsed` and `v-model:open` (mobile) for state control.
### DashboardPanel
Content panel with `#header`, `#body` (scrollable), `#footer`, and `#default` (raw, no scroll) slots.
### DashboardNavbar / DashboardToolbar
Navbar: `#leading`, `#left`, `#default`, `#right` slots + `title` prop. Use `UDashboardSidebarCollapse` in `#leading` to toggle sidebar on mobile.
Toolbar: same slots, sits below navbar for filters/actions.
### UNavigationMenu in sidebar
Always pass `:collapsed="collapsed"` to `UNavigationMenu` inside a collapsible sidebar — it auto-hides labels and centers icons. Use `NavigationMenuItem[][]` (array of arrays) for separate groups (main nav + footer links).
## 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>
```
@@ -0,0 +1,152 @@
# Docs Layout
Build documentation sites with sidebar navigation, table of contents, and surround links.
## When to use
- Technical documentation sites
- Knowledge bases, help centers
- Any content-heavy site with hierarchical navigation
> Requires `@nuxt/content` — see [conventions](../guidelines/conventions.md#content-module-integration) for setup (module order + `@source`).
## 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>
```
### How nesting works
The outer `UPage` in the layout handles the **left sidebar**. The inner `UPage` in the page handles the **right sidebar**. They nest correctly — this is intentional.
### Common mistakes
- Not providing navigation via `provide`/`inject` — the layout needs it from the app shell.
- Forgetting `UContentSearch` in app.vue — search won't work without it.
- Using `UContentSearchButton` without `UContentSearch` — the button opens search, but the search component must exist.
## Key components
- `UPage` — multi-column grid with `#left`, `#default`, `#right` slots
- `UPageAside` — sticky sidebar wrapper (visible from `lg` breakpoint)
- `UContentNavigation` — sidebar navigation tree from Nuxt Content
- `UContentToc` — table of contents from page headings
- `UContentSurround` — prev/next links
- `UContentSearch` / `UContentSearchButton` — search command palette
- `UPageAnchors` — simpler alternative to full TOC
@@ -0,0 +1,153 @@
# Editor Layout
Build a rich text editor with toolbars, slash commands, mentions, and drag-and-drop.
## When to use
- Note-taking apps, CMS editors
- Collaborative editing interfaces
- Any rich text editing need (supports JSON, HTML, and Markdown)
## Component tree
```
UEditor
├── UEditorToolbar (fixed / bubble / floating)
├── UEditorDragHandle
├── UEditorSuggestionMenu
├── UEditorMentionMenu
└── UEditorEmojiMenu
```
## Basic 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>
<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>
</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 (default), HTML, or Markdown via `content-type` prop. Default slot provides `{ editor, handlers }` — `editor` is the Tiptap instance, `handlers` contains action functions for toolbar/menus.
- `UEditorToolbar` — toolbar with `layout`: `'fixed'` (default), `'bubble'` (on selection), `'floating'` (on empty lines).
- `UEditorDragHandle` — block drag-and-drop handle.
- `UEditorSuggestionMenu` — slash command menu (type `/` to open).
- `UEditorMentionMenu` — `@` mention menu.
- `UEditorEmojiMenu` — emoji picker (type `:` to open).
## Toolbar modes
```vue
<!-- Fixed (default) — always visible at top -->
<UEditorToolbar :editor="editor" />
<!-- Bubble — appears on text selection -->
<UEditorToolbar :editor="editor" layout="bubble" />
<!-- Floating — appears on empty lines -->
<UEditorToolbar :editor="editor" layout="floating" />
```
## 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 layout for a multi-document editor:
```vue [layouts/editor.vue]
<template>
<UDashboardGroup>
<UDashboardSidebar collapsible resizable>
<template #header>
<UButton icon="i-lucide-plus" label="New document" block />
</template>
<template #default="{ collapsed }">
<UNavigationMenu
:collapsed="collapsed"
: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>
```
@@ -0,0 +1,185 @@
# Landing Page Layout
Build public-facing pages — landing, blog, changelog, pricing — using the Header + Main + Footer shell with Page components.
## When to use
- Marketing sites, product pages, company sites
- Blog and content pages
- Pricing, changelog, portfolio pages
- Any public-facing page that isn't a dashboard or documentation
## 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>
```
### Common mistakes
- Forgetting the `#body` slot on `UHeader` — this is the mobile menu content. Without it, mobile users have no navigation.
- Using `variant="solid"` for both header and hero buttons — the header button should be lower weight than the hero CTA.
## 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>
```
## Key components
- `UPageHero` — hero with title, description, links, and optional media. Use `orientation="horizontal"` for side-by-side layout.
- `UPageSection` — content section with headline, title, description, and `features` grid. Use `id` for anchor links.
- `UPageCTA` — call to action block.
- `UPageGrid` / `UPageCard` — card grid for features, testimonials, etc.
- `UPageFeature` — individual feature item.
- `UPageLogos` — logo wall for social proof.
- `UPricingPlans` / `UPricingTable` — pricing cards and comparison tables.
- `UFooterColumns` — multi-column footer with link groups (used inside `UFooter`).
## Variations
### Alternating feature 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>
```
### 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." />
<UPageBody>
<UContainer>
<UBlogPosts>
<UBlogPost v-for="post in posts" :key="post.path" v-bind="post" :to="post.path" />
</UBlogPosts>
</UContainer>
</UPageBody>
</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 in versions" :key="version.path" v-bind="version" />
</UChangelogVersions>
</UContainer>
</UPageBody>
</UPage>
</template>
```