nvim
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# Auth Forms
|
||||
|
||||
## UAuthForm (recommended)
|
||||
|
||||
`UAuthForm` provides a complete auth form with fields, providers, validation, and submit — no manual `UForm` + `UFormField` wiring needed. Wrap it in `UPageCard` for a polished look.
|
||||
|
||||
```vue [pages/login.vue]
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent, AuthFormField } from '@nuxt/ui'
|
||||
|
||||
const fields: AuthFormField[] = [{
|
||||
name: 'email',
|
||||
type: 'email',
|
||||
label: 'Email',
|
||||
placeholder: 'Enter your email',
|
||||
required: true
|
||||
}, {
|
||||
name: 'password',
|
||||
label: 'Password',
|
||||
type: 'password',
|
||||
placeholder: 'Enter your password',
|
||||
required: true
|
||||
}, {
|
||||
name: 'remember',
|
||||
label: 'Remember me',
|
||||
type: 'checkbox'
|
||||
}]
|
||||
|
||||
const providers = [{
|
||||
label: 'Google',
|
||||
icon: 'i-simple-icons-google',
|
||||
onClick: () => navigateTo('/auth/google', { external: true })
|
||||
}, {
|
||||
label: 'GitHub',
|
||||
icon: 'i-simple-icons-github',
|
||||
onClick: () => navigateTo('/auth/github', { external: true })
|
||||
}]
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email('Invalid email'),
|
||||
password: z.string('Password is required').min(8, 'Must be at least 8 characters')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
|
||||
function onSubmit(payload: FormSubmitEvent<Schema>) {
|
||||
// payload.data contains validated fields
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-dvh items-center justify-center">
|
||||
<UPageCard class="w-full max-w-md">
|
||||
<UAuthForm
|
||||
:schema="schema"
|
||||
:fields="fields"
|
||||
:providers="providers"
|
||||
title="Welcome back!"
|
||||
description="Sign in to your account."
|
||||
icon="i-lucide-lock"
|
||||
@submit="onSubmit"
|
||||
>
|
||||
<template #password-hint>
|
||||
<ULink to="/forgot-password" class="text-primary font-medium">Forgot password?</ULink>
|
||||
</template>
|
||||
<template #footer>
|
||||
Don't have an account? <ULink to="/signup" class="text-primary font-medium">Sign up</ULink>.
|
||||
</template>
|
||||
</UAuthForm>
|
||||
</UPageCard>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### UAuthForm key props
|
||||
|
||||
| Prop | Purpose |
|
||||
| ------------------------------ | ------------------------------------------------------------------------------- |
|
||||
| `title`, `description`, `icon` | Header content |
|
||||
| `fields` | `AuthFormField[]` — each has `name`, `type`, `label`, `placeholder`, `required` |
|
||||
| `providers` | `ButtonProps[]` — social login buttons shown above/below the form |
|
||||
| `schema` | Zod/Valibot schema for validation |
|
||||
| `submit` | Customize submit button: `{ label: 'Sign in', block: true }` |
|
||||
| `separator` | Text between providers and fields (default: `'or'`) |
|
||||
|
||||
### UAuthForm key slots
|
||||
|
||||
| Slot | Purpose |
|
||||
| ---------------- | --------------------------------------------- |
|
||||
| `#description` | Override description (e.g., add sign-up link) |
|
||||
| `#password-hint` | "Forgot password?" link on password field |
|
||||
| `#validation` | Custom error display (e.g., `UAlert`) |
|
||||
| `#footer` | Terms of service, sign-up link |
|
||||
| `#<field>-field` | Override a specific field's rendering |
|
||||
|
||||
## Custom auth layout
|
||||
|
||||
For layouts where `UAuthForm` is too opinionated, use `UCard` + `UForm` + `UFormField` directly.
|
||||
|
||||
```vue [pages/login.vue]
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = z.object({
|
||||
email: z.email('Invalid email'),
|
||||
password: z.string().min(1, 'Password is required')
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
const state = reactive<Partial<Schema>>({ email: '', password: '' })
|
||||
|
||||
async function onSubmit(event: FormSubmitEvent<Schema>) {
|
||||
// event.data contains validated fields
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex min-h-dvh items-center justify-center">
|
||||
<UCard class="w-full max-w-sm">
|
||||
<template #header>
|
||||
<div class="text-center">
|
||||
<h1 class="text-xl font-semibold text-default">Welcome back</h1>
|
||||
<p class="mt-1 text-sm text-muted">Sign in to your account</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<UForm :schema="schema" :state="state" class="space-y-4" @submit="onSubmit">
|
||||
<UFormField name="email" label="Email">
|
||||
<UInput v-model="state.email" type="email" placeholder="you@example.com" />
|
||||
</UFormField>
|
||||
|
||||
<UFormField name="password" label="Password">
|
||||
<template #hint>
|
||||
<NuxtLink to="/forgot-password" class="text-sm text-primary">Forgot password?</NuxtLink>
|
||||
</template>
|
||||
<UInput v-model="state.password" type="password" />
|
||||
</UFormField>
|
||||
|
||||
<UButton type="submit" label="Sign in" block />
|
||||
</UForm>
|
||||
|
||||
<template #footer>
|
||||
<p class="text-center text-sm text-muted">
|
||||
Don't have an account?
|
||||
<NuxtLink to="/signup" class="text-primary font-medium">Sign up</NuxtLink>
|
||||
</p>
|
||||
</template>
|
||||
</UCard>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Prefer `UAuthForm` with `UPageCard` for standard auth pages — handles layout, providers, validation, and submit
|
||||
- Use `import * as z from 'zod'` and `z.email()` (Zod 4 syntax)
|
||||
- Type the submit handler: `function onSubmit(event: FormSubmitEvent<Schema>)` — access validated data via `event.data`
|
||||
- Center auth forms with `flex min-h-dvh items-center justify-center`
|
||||
- Place "Forgot password?" link as `#password-hint` slot on `UAuthForm`, or `#hint` slot on `UFormField`
|
||||
- Social login buttons: use `providers` prop on `UAuthForm`, or add manually with `<USeparator label="or" />`
|
||||
@@ -0,0 +1,225 @@
|
||||
# Data Tables
|
||||
|
||||
Complete patterns for displaying and managing tabular data.
|
||||
|
||||
## Basic table
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
|
||||
const data = ref([
|
||||
{ name: 'Alice', email: 'alice@example.com', role: 'Admin' },
|
||||
{ name: 'Bob', email: 'bob@example.com', role: 'Editor' }
|
||||
])
|
||||
|
||||
const columns: TableColumn<typeof data.value[number]>[] = [{
|
||||
accessorKey: 'name',
|
||||
header: 'Name'
|
||||
}, {
|
||||
accessorKey: 'email',
|
||||
header: 'Email'
|
||||
}, {
|
||||
accessorKey: 'role',
|
||||
header: 'Role'
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data" :columns="columns" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## With search and filters (dashboard)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
|
||||
const search = ref('')
|
||||
const roleFilter = ref('All')
|
||||
|
||||
const rows = ref([
|
||||
{ name: 'Alice', email: 'alice@example.com', role: 'Admin', status: 'Active' },
|
||||
{ name: 'Bob', email: 'bob@example.com', role: 'Editor', status: 'Inactive' }
|
||||
])
|
||||
|
||||
const columns: TableColumn[] = [
|
||||
{ accessorKey: 'name', header: 'Name' },
|
||||
{ accessorKey: 'email', header: 'Email' },
|
||||
{ accessorKey: 'role', header: 'Role' },
|
||||
{ accessorKey: 'status', header: 'Status' },
|
||||
{ id: 'actions' }
|
||||
]
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
return rows.value.filter(row => {
|
||||
const matchesSearch = !search.value || row.name.toLowerCase().includes(search.value.toLowerCase())
|
||||
const matchesRole = roleFilter.value === 'All' || row.role === roleFilter.value
|
||||
return matchesSearch && matchesRole
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UDashboardPanel>
|
||||
<template #header>
|
||||
<UDashboardNavbar title="Users" />
|
||||
|
||||
<UDashboardToolbar>
|
||||
<template #left>
|
||||
<UInput v-model="search" icon="i-lucide-search" placeholder="Search users..." />
|
||||
</template>
|
||||
<template #right>
|
||||
<USelect v-model="roleFilter" :items="['All', 'Admin', 'Editor', 'Viewer']" />
|
||||
</template>
|
||||
</UDashboardToolbar>
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UTable :data="filteredRows" :columns="columns">
|
||||
<template #status-cell="{ row }">
|
||||
<UBadge :color="row.original.status === 'Active' ? 'success' : 'neutral'" :label="row.original.status" variant="subtle" />
|
||||
</template>
|
||||
|
||||
<template #actions-cell="{ row }">
|
||||
<UDropdownMenu
|
||||
:items="[
|
||||
[{ label: 'Edit', icon: 'i-lucide-pencil', onSelect: () => edit(row.original) }],
|
||||
[{ label: 'Delete', icon: 'i-lucide-trash', color: 'error', onSelect: () => remove(row.original) }]
|
||||
]"
|
||||
>
|
||||
<UButton icon="i-lucide-ellipsis" color="neutral" variant="ghost" />
|
||||
</UDropdownMenu>
|
||||
</template>
|
||||
</UTable>
|
||||
</template>
|
||||
</UDashboardPanel>
|
||||
</template>
|
||||
```
|
||||
|
||||
## With row selection
|
||||
|
||||
Row selection uses TanStack Table's `rowSelection` state — a `Record<string, boolean>` keyed by row index.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const table = useTemplateRef('table')
|
||||
const rowSelection = ref<Record<string, boolean>>({})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable ref="table" v-model:row-selection="rowSelection" :data="data" :columns="columns" />
|
||||
|
||||
<div class="px-4 py-3.5 text-sm text-muted">
|
||||
{{ table?.tableApi?.getFilteredSelectedRowModel().rows.length || 0 }} of
|
||||
{{ table?.tableApi?.getFilteredRowModel().rows.length || 0 }} row(s) selected.
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
Add a checkbox column using the `h` function. Use tri-state `modelValue` (`true`, `false`, or `'indeterminate'`) for the "select all" header:
|
||||
|
||||
```ts
|
||||
import { h } from 'vue'
|
||||
|
||||
const UCheckbox = resolveComponent('UCheckbox')
|
||||
|
||||
const columns: TableColumn[] = [{
|
||||
id: 'select',
|
||||
header: ({ table }) => h(UCheckbox, {
|
||||
'modelValue': table.getIsSomePageRowsSelected() ? 'indeterminate' : table.getIsAllPageRowsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => table.toggleAllPageRowsSelected(!!value),
|
||||
'aria-label': 'Select all'
|
||||
}),
|
||||
cell: ({ row }) => h(UCheckbox, {
|
||||
'modelValue': row.getIsSelected(),
|
||||
'onUpdate:modelValue': (value: boolean | 'indeterminate') => row.toggleSelected(!!value),
|
||||
'aria-label': 'Select row'
|
||||
})
|
||||
},
|
||||
// ... other columns
|
||||
]
|
||||
```
|
||||
|
||||
## With pagination
|
||||
|
||||
Use `v-model:pagination` on `UTable` with TanStack's `getPaginationRowModel`, then wire `UPagination` to the table API. `UPagination`'s `total` is total **items** (not pages) — it calculates page count from `total / items-per-page`.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { getPaginationRowModel } from '@tanstack/vue-table'
|
||||
|
||||
const table = useTemplateRef('table')
|
||||
|
||||
const pagination = ref({
|
||||
pageIndex: 0,
|
||||
pageSize: 5
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable
|
||||
ref="table"
|
||||
v-model:pagination="pagination"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:pagination-options="{ getPaginationRowModel: getPaginationRowModel() }"
|
||||
/>
|
||||
|
||||
<div class="flex justify-end p-4">
|
||||
<UPagination
|
||||
:page="(table?.tableApi?.getState().pagination.pageIndex || 0) + 1"
|
||||
:items-per-page="table?.tableApi?.getState().pagination.pageSize"
|
||||
:total="table?.tableApi?.getFilteredRowModel().rows.length"
|
||||
@update:page="(p) => table?.tableApi?.setPageIndex(p - 1)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## With async data (Nuxt)
|
||||
|
||||
Use `status === 'pending' || status === 'idle'` for loading state — `idle` covers the initial render before `useLazyFetch` starts.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const { data, status } = useLazyFetch('/api/users', { server: false })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data" :columns="columns" :loading="status === 'pending' || status === 'idle'" />
|
||||
</template>
|
||||
```
|
||||
|
||||
For server-side pagination:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const page = ref(1)
|
||||
|
||||
const { data, status } = await useAsyncData(
|
||||
'users',
|
||||
() => $fetch('/api/users', { query: { page: page.value } }),
|
||||
{ watch: [page] }
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data?.items" :columns="columns" :loading="status === 'pending'" />
|
||||
|
||||
<div class="flex justify-end p-4">
|
||||
<UPagination v-model="page" :total="data?.total" :items-per-page="data?.pageSize" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tips
|
||||
|
||||
- Table is built on [TanStack Table v8](https://tanstack.com/table/v8) — columns use `ColumnDef` format with `accessorKey`, `header`, `cell`
|
||||
- Use `#<column>-cell` and `#<column>-header` template slots to customize rendering with Vue templates
|
||||
- Alternatively, use the `h` function inside `header` and `cell` column properties for inline rendering
|
||||
- Row data in slots is accessed via `row.original` (not `row` directly)
|
||||
- Use `v-model:row-selection` for selection, `v-model:sorting` for sort state
|
||||
- Wrap tables in `UDashboardPanel` with `#header` toolbar for the dashboard pattern
|
||||
- For empty states, use the `#empty` slot
|
||||
@@ -0,0 +1,90 @@
|
||||
# Navigation
|
||||
|
||||
Patterns for headers, sidebars, breadcrumbs, and tab navigation.
|
||||
|
||||
## Header with mobile menu
|
||||
|
||||
`UHeader` default slot is desktop nav, `#body` is the mobile menu. Without `#body`, mobile users have no navigation.
|
||||
|
||||
```vue
|
||||
<UHeader>
|
||||
<template #title>
|
||||
<Logo class="h-6 w-auto" />
|
||||
</template>
|
||||
|
||||
<UNavigationMenu :items="items" />
|
||||
|
||||
<template #right>
|
||||
<UColorModeButton />
|
||||
<UButton label="Sign in" color="neutral" variant="ghost" />
|
||||
</template>
|
||||
|
||||
<template #body>
|
||||
<UNavigationMenu :items="items" orientation="vertical" class="-mx-2.5" />
|
||||
</template>
|
||||
</UHeader>
|
||||
```
|
||||
|
||||
> Full app shell example in [landing layout](../layouts/landing.md).
|
||||
|
||||
## Sidebar navigation (dashboard)
|
||||
|
||||
See [dashboard layout](../layouts/dashboard.md) for the full sidebar pattern with `UDashboardSidebar` + `UNavigationMenu`. Key points:
|
||||
|
||||
- Pass `:collapsed="collapsed"` to `UNavigationMenu` inside collapsible sidebars
|
||||
- Use `NavigationMenuItem[][]` (nested arrays) for separate nav groups
|
||||
- Use `#footer` slot for user menu with `UDropdownMenu`
|
||||
|
||||
## Breadcrumbs
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const route = useRoute()
|
||||
|
||||
const breadcrumbs = computed(() => {
|
||||
const segments = route.path.split('/').filter(Boolean)
|
||||
return segments.map((segment, index) => ({
|
||||
label: segment.charAt(0).toUpperCase() + segment.slice(1),
|
||||
to: '/' + segments.slice(0, index + 1).join('/')
|
||||
}))
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UBreadcrumb :items="breadcrumbs" />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Tab navigation (within a page)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const items = [{
|
||||
label: 'Overview',
|
||||
icon: 'i-lucide-layout-dashboard',
|
||||
slot: 'overview' as const
|
||||
}, {
|
||||
label: 'Activity',
|
||||
icon: 'i-lucide-activity',
|
||||
slot: 'activity' as const
|
||||
}, {
|
||||
label: 'Members',
|
||||
icon: 'i-lucide-users',
|
||||
slot: 'members' as const
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTabs :items="items">
|
||||
<template #overview>
|
||||
<!-- Overview content -->
|
||||
</template>
|
||||
<template #activity>
|
||||
<!-- Activity feed -->
|
||||
</template>
|
||||
<template #members>
|
||||
<!-- Members list -->
|
||||
</template>
|
||||
</UTabs>
|
||||
</template>
|
||||
```
|
||||
@@ -0,0 +1,172 @@
|
||||
# Overlays
|
||||
|
||||
Patterns for modals, slideovers, drawers, and command palettes.
|
||||
|
||||
## Confirmation dialog
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
|
||||
function confirmDelete() {
|
||||
// perform delete
|
||||
isOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Delete" color="error" variant="soft" @click="isOpen = true" />
|
||||
|
||||
<UModal v-model:open="isOpen" title="Delete item" description="This action cannot be undone. Are you sure?" :ui="{ footer: 'justify-end' }">
|
||||
<template #footer="{ close }">
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
|
||||
<UButton label="Delete" color="error" @click="confirmDelete" />
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Programmatic confirmation (useOverlay)
|
||||
|
||||
Reusable pattern — no template state needed at the call site.
|
||||
|
||||
```vue [components/ConfirmModal.vue]
|
||||
<script setup lang="ts">
|
||||
defineProps<{
|
||||
title: string
|
||||
description?: string
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: [confirmed: boolean]
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UModal :close="{ onClick: () => emit('close', false) }" :title="title" :description="description">
|
||||
<template #footer>
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="emit('close', false)" />
|
||||
<UButton label="Confirm" color="error" @click="emit('close', true)" />
|
||||
</template>
|
||||
</UModal>
|
||||
</template>
|
||||
```
|
||||
|
||||
```ts
|
||||
// Usage anywhere
|
||||
const overlay = useOverlay()
|
||||
const confirm = overlay.create(ConfirmModal)
|
||||
|
||||
async function deleteItem(item) {
|
||||
const instance = confirm.open({
|
||||
title: 'Delete item',
|
||||
description: `Are you sure you want to delete "${item.name}"?`
|
||||
})
|
||||
|
||||
if (await instance.result) {
|
||||
// user confirmed
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Form in a slideover
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import * as z from 'zod'
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
const schema = z.object({
|
||||
name: z.string().min(1),
|
||||
email: z.email()
|
||||
})
|
||||
|
||||
type Schema = z.output<typeof schema>
|
||||
const state = reactive<Partial<Schema>>({})
|
||||
|
||||
function onSave() {
|
||||
// save user
|
||||
isOpen.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Add user" @click="isOpen = true" />
|
||||
|
||||
<USlideover v-model:open="isOpen" title="Add user" description="Fill in the details below.">
|
||||
<template #body>
|
||||
<UForm id="user-form" :schema="schema" :state="state" class="space-y-4" @submit="onSave">
|
||||
<UFormField name="name" label="Name">
|
||||
<UInput v-model="state.name" />
|
||||
</UFormField>
|
||||
<UFormField name="email" label="Email">
|
||||
<UInput v-model="state.email" type="email" />
|
||||
</UFormField>
|
||||
</UForm>
|
||||
</template>
|
||||
|
||||
<template #footer="{ close }">
|
||||
<UButton label="Cancel" color="neutral" variant="outline" @click="close" />
|
||||
<UButton type="submit" form="user-form" label="Save" />
|
||||
</template>
|
||||
</USlideover>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Command palette
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
|
||||
defineShortcuts({
|
||||
meta_k: () => { isOpen.value = true }
|
||||
})
|
||||
|
||||
const groups = [{
|
||||
id: 'actions',
|
||||
label: 'Actions',
|
||||
items: [
|
||||
{ label: 'New file', icon: 'i-lucide-file-plus', kbds: ['meta', 'n'], onSelect: () => newFile() },
|
||||
{ label: 'New folder', icon: 'i-lucide-folder-plus', onSelect: () => newFolder() }
|
||||
]
|
||||
}, {
|
||||
id: 'navigation',
|
||||
label: 'Navigation',
|
||||
items: [
|
||||
{ label: 'Dashboard', icon: 'i-lucide-house', to: '/dashboard' },
|
||||
{ label: 'Settings', icon: 'i-lucide-settings', to: '/settings' }
|
||||
]
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Search..." icon="i-lucide-search" color="neutral" variant="outline" @click="isOpen = true" />
|
||||
|
||||
<UCommandPalette v-model:open="isOpen" :groups="groups" placeholder="Type a command or search..." />
|
||||
</template>
|
||||
```
|
||||
|
||||
## Drawer (bottom sheet)
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
const isOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Options" @click="isOpen = true" />
|
||||
|
||||
<UDrawer v-model:open="isOpen" title="Options">
|
||||
<template #body>
|
||||
<div class="space-y-2 p-4">
|
||||
<UButton label="Share" icon="i-lucide-share" block variant="ghost" />
|
||||
<UButton label="Export" icon="i-lucide-download" block variant="ghost" />
|
||||
<USeparator />
|
||||
<UButton label="Delete" icon="i-lucide-trash" block variant="ghost" color="error" />
|
||||
</div>
|
||||
</template>
|
||||
</UDrawer>
|
||||
</template>
|
||||
```
|
||||
Reference in New Issue
Block a user