mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-01-15 20:48:12 +01:00
Compare commits
5 Commits
feat/init-
...
feat/1058
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b19aaa7bbb | ||
|
|
aa16084dfe | ||
|
|
b99245dc3f | ||
|
|
51088c4c73 | ||
|
|
606b4867da |
46
CHANGELOG.md
46
CHANGELOG.md
@@ -1,51 +1,5 @@
|
||||
# Changelog
|
||||
|
||||
## [3.2.0](https://github.com/nuxt/ui/compare/v3.1.3...v3.2.0) (2025-06-25)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
* **useOverlay:** correct spelling of `unmount` function (#4051)
|
||||
|
||||
### Features
|
||||
|
||||
* **Avatar:** add `chip` prop ([#4224](https://github.com/nuxt/ui/issues/4224)) ([03ac395](https://github.com/nuxt/ui/commit/03ac395164c02c964361c68743268b1bc90aae59))
|
||||
* **Carousel:** allow customization of active dot color ([#4229](https://github.com/nuxt/ui/issues/4229)) ([2ee1c5a](https://github.com/nuxt/ui/commit/2ee1c5ac2e20ab9ce2f4037a8e8c64e561b0428b))
|
||||
* **CommandPalette:** handle `children` in items ([#4226](https://github.com/nuxt/ui/issues/4226)) ([59c26ec](https://github.com/nuxt/ui/commit/59c26ec1230375a24fbaf8a630a696ae854700c7))
|
||||
* **extendLocale:** new composable ([0f558fc](https://github.com/nuxt/ui/commit/0f558fc0d014d51549222accfc50286d1770d1aa)), closes [#3729](https://github.com/nuxt/ui/issues/3729)
|
||||
* **Form:** expose loading state to default slot ([#4247](https://github.com/nuxt/ui/issues/4247)) ([ea0c459](https://github.com/nuxt/ui/commit/ea0c459306be585bacaaf5b433114d072550c824))
|
||||
* **InputTags:** new component ([#4261](https://github.com/nuxt/ui/issues/4261)) ([54bb228](https://github.com/nuxt/ui/commit/54bb2282c58d3bf5a7dde4cdee687c68efd934a0))
|
||||
* **locale:** add Luxembourgish language ([#4264](https://github.com/nuxt/ui/issues/4264)) ([43cbb94](https://github.com/nuxt/ui/commit/43cbb94ee25106b414fc8fe979fa65ebaa9ccc76))
|
||||
* **Modal/Slideover:** add `actions` slot ([#4358](https://github.com/nuxt/ui/issues/4358)) ([8156971](https://github.com/nuxt/ui/commit/81569713e9da9d5531ecdf4614660b84c686fa81))
|
||||
* **Modal/Slideover:** add `close` method in slots ([#4219](https://github.com/nuxt/ui/issues/4219)) ([5835eb5](https://github.com/nuxt/ui/commit/5835eb5f0f835b5f03646dec78f85b2f556a109b))
|
||||
* **Select/SelectMenu/Tabs:** expose trigger refs ([7a2bd4e](https://github.com/nuxt/ui/commit/7a2bd4e6179373902ba6f285903ea896fd1d378f)), closes [#4292](https://github.com/nuxt/ui/issues/4292)
|
||||
* **Select/SelectMenu:** handle dynamic `autofocus` ([1a4de49](https://github.com/nuxt/ui/commit/1a4de49c1665c9ef65279315be0393d6272447b9)), closes [#4324](https://github.com/nuxt/ui/issues/4324)
|
||||
* **Table:** add `body-top` / `body-bottom` slots ([#4354](https://github.com/nuxt/ui/issues/4354)) ([595fc64](https://github.com/nuxt/ui/commit/595fc64515613fe82c3a56fc5518f2e3fcce6e19))
|
||||
* **Timeline:** add `reverse` prop ([#4316](https://github.com/nuxt/ui/issues/4316)) ([5170cfd](https://github.com/nuxt/ui/commit/5170cfd7eb44a25c64673cf12979f9ca1049695f))
|
||||
* **Timeline:** new component ([#4215](https://github.com/nuxt/ui/issues/4215)) ([8017767](https://github.com/nuxt/ui/commit/80177679f2aa0d7f0e39e639a02d527a06e6172c))
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* **Card/Drawer/Modal:** prevent scrollbars overflow ([#4368](https://github.com/nuxt/ui/issues/4368)) ([c3adc38](https://github.com/nuxt/ui/commit/c3adc381c90dad7152e27fc303ee678efc7c4c94))
|
||||
* **components:** remove default `md` size on buttons ([#4357](https://github.com/nuxt/ui/issues/4357)) ([be41aed](https://github.com/nuxt/ui/commit/be41aed1f3d3476801e1840dbb8766926bc93c05))
|
||||
* **defineShortcuts:** allow `meta_-` shortcut ([#4321](https://github.com/nuxt/ui/issues/4321)) ([4e7c1c9](https://github.com/nuxt/ui/commit/4e7c1c9c305b45dd76d4c238e70a6aeedae78c8b))
|
||||
* **Form:** conditionally type form data via `transform` prop ([#4188](https://github.com/nuxt/ui/issues/4188)) ([37abcc6](https://github.com/nuxt/ui/commit/37abcc6a5b0a678be626673af5067956657a50d6))
|
||||
* **Form:** expose reactive fields ([#4386](https://github.com/nuxt/ui/issues/4386)) ([1a8feb7](https://github.com/nuxt/ui/commit/1a8feb751e6827c414ef82fe9fb259ba7dcc7e08))
|
||||
* **InputMenu/SelectMenu:** dynamic `empty` size ([ba3c6e8](https://github.com/nuxt/ui/commit/ba3c6e8788ed75d86d4406749797da52d7816b84)), closes [#4377](https://github.com/nuxt/ui/issues/4377)
|
||||
* **InputTags:** extend emits interface ([8781a07](https://github.com/nuxt/ui/commit/8781a079096def0d3bae5b8d896db0df6ce37e23))
|
||||
* **Modal/Slideover:** don't emit `close:prevent` on `closeAutoFocus` ([150b334](https://github.com/nuxt/ui/commit/150b334b1d242c6dc132193e23359c03e6f35666))
|
||||
* **NavigationMenu:** nested accordion context at every level ([#4363](https://github.com/nuxt/ui/issues/4363)) ([2fa8db6](https://github.com/nuxt/ui/commit/2fa8db64ddf4c92a19e73774143518d87d001b72))
|
||||
* **NavigationMenu:** set content `max-height` in `horizontal` orientation ([62bc7b2](https://github.com/nuxt/ui/commit/62bc7b25a2d205d8dffb47a109196f91ff3e823a)), closes [#4208](https://github.com/nuxt/ui/issues/4208)
|
||||
* **Pagination:** match default button `size` ([#4350](https://github.com/nuxt/ui/issues/4350)) ([4dd56c8](https://github.com/nuxt/ui/commit/4dd56c8111e5a224105b82d541b7742b46abb34a))
|
||||
* **Select/SelectMenu:** display falsy values ([7df7ee3](https://github.com/nuxt/ui/commit/7df7ee336a925d7ee07f866551dad9350785c9fc))
|
||||
* **Select/SelectMenu:** prevent empty string display when multiple ([483e473](https://github.com/nuxt/ui/commit/483e473e3f5681cc97c3766ea47283dc95f76345))
|
||||
* **SelectMenu:** dynamic input size ([b0364b9](https://github.com/nuxt/ui/commit/b0364b96b73b9e543781a35962c03b5a983352c4))
|
||||
* **Table:** use `tr` as separator ([#4083](https://github.com/nuxt/ui/issues/4083)) ([edca3bc](https://github.com/nuxt/ui/commit/edca3bcb743c7eb63e6abbaa801d3858342a8777))
|
||||
* **Toast:** calc height on next tick ([3bf5acb](https://github.com/nuxt/ui/commit/3bf5acb683f0ad09735b2417d265d6fcfd901b11)), closes [#4265](https://github.com/nuxt/ui/issues/4265)
|
||||
* **Toaster:** smoother visibility transition for stacked toasts ([#4367](https://github.com/nuxt/ui/issues/4367)) ([abfd0ed](https://github.com/nuxt/ui/commit/abfd0ede036fa2953f9abc841d77ac71bbd3bba9))
|
||||
* **useOverlay:** correct spelling of `unmount` function ([#4051](https://github.com/nuxt/ui/issues/4051)) ([546df57](https://github.com/nuxt/ui/commit/546df572fca60325315bed17c9be3367052fb7a9))
|
||||
* **useOverlay:** set props to original props when `defaultOpen` is set ([#4308](https://github.com/nuxt/ui/issues/4308)) ([66355ba](https://github.com/nuxt/ui/commit/66355ba301d569b9f44527bafc5f8f09bcda63c0))
|
||||
* **useOverlay:** use original props when not provided to `open` ([#4269](https://github.com/nuxt/ui/issues/4269)) ([bf56e15](https://github.com/nuxt/ui/commit/bf56e15a2eed7d51199d5641649a822e91ca41ba))
|
||||
|
||||
## [3.1.3](https://github.com/nuxt/ui/compare/v3.1.2...v3.1.3) (2025-05-26)
|
||||
|
||||
### ⚠ BREAKING CHANGES
|
||||
|
||||
@@ -147,8 +147,7 @@ const test = ({ name, prose, content }) => {
|
||||
? undefined
|
||||
: `
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import ${upperName} from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
|
||||
import type { ${upperName}Props, ${upperName}Slots } from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
|
||||
import ${upperName}, { type ${upperName}Props, type ${upperName}Slots } from '../../${content ? '../' : ''}src/runtime/components/${content ? 'content/' : ''}${upperName}.vue'
|
||||
import ComponentRender from '../${content ? '../' : ''}component-render'
|
||||
|
||||
describe('${upperName}', () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<template>
|
||||
<UBanner
|
||||
id="ui3-launch"
|
||||
title="Nuxt UI v3 is officially released!"
|
||||
icon="i-lucide-rocket"
|
||||
:actions="[
|
||||
{
|
||||
@@ -11,5 +10,9 @@
|
||||
}
|
||||
]"
|
||||
close
|
||||
/>
|
||||
>
|
||||
<template #title>
|
||||
<span class="font-semibold">Nuxt UI v3</span> is officially released.
|
||||
</template>
|
||||
</UBanner>
|
||||
</template>
|
||||
|
||||
@@ -1,78 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const groups = [
|
||||
{
|
||||
id: 'actions',
|
||||
items: [
|
||||
{
|
||||
label: 'Add new file',
|
||||
suffix: 'Create a new file in the current directory',
|
||||
icon: 'i-lucide-file-plus',
|
||||
kbds: ['meta', 'N']
|
||||
},
|
||||
{
|
||||
label: 'Add new folder',
|
||||
suffix: 'Create a new folder in the current directory',
|
||||
icon: 'i-lucide-folder-plus',
|
||||
kbds: ['meta', 'F']
|
||||
},
|
||||
{
|
||||
label: 'Search files',
|
||||
suffix: 'Search across all files in the project',
|
||||
icon: 'i-lucide-search',
|
||||
kbds: ['meta', 'P']
|
||||
},
|
||||
{
|
||||
label: 'Settings',
|
||||
suffix: 'Open application settings',
|
||||
icon: 'i-lucide-settings',
|
||||
kbds: ['meta', ',']
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'recent',
|
||||
label: 'Recent',
|
||||
items: [
|
||||
{
|
||||
label: 'project.vue',
|
||||
suffix: 'components/',
|
||||
icon: 'i-vscode-icons-file-type-vue'
|
||||
},
|
||||
{
|
||||
label: 'readme.md',
|
||||
suffix: 'docs/',
|
||||
icon: 'i-vscode-icons-file-type-markdown'
|
||||
},
|
||||
{
|
||||
label: 'package.json',
|
||||
suffix: 'root/',
|
||||
icon: 'i-vscode-icons-file-type-node'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UCommandPalette :groups="groups" class="flex-1 h-80">
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<UIcon name="i-simple-icons-nuxtdotjs" class="size-5 text-dimmed ml-1" />
|
||||
<div class="flex items-center gap-1">
|
||||
<UButton color="neutral" variant="ghost" label="Open Command" class="text-dimmed" size="xs">
|
||||
<template #trailing>
|
||||
<UKbd value="enter" />
|
||||
</template>
|
||||
</UButton>
|
||||
<USeparator orientation="vertical" class="h-4" />
|
||||
<UButton color="neutral" variant="ghost" label="Actions" class="text-dimmed" size="xs">
|
||||
<template #trailing>
|
||||
<UKbd value="meta" />
|
||||
<UKbd value="k" />
|
||||
</template>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UCommandPalette>
|
||||
</template>
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { object, string, nonempty, refine } from 'superstruct'
|
||||
import type { Infer } from 'superstruct'
|
||||
import { object, string, nonempty, refine, type Infer } from 'superstruct'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = object({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { object, string } from 'yup'
|
||||
import type { InferType } from 'yup'
|
||||
import { object, string, type InferType } from 'yup'
|
||||
import type { FormSubmitEvent } from '@nuxt/ui'
|
||||
|
||||
const schema = object({
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const open = ref(false)
|
||||
const anchor = ref({ x: 0, y: 0 })
|
||||
|
||||
const reference = computed(() => ({
|
||||
getBoundingClientRect: () =>
|
||||
({
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: anchor.value.x,
|
||||
right: anchor.value.x,
|
||||
top: anchor.value.y,
|
||||
bottom: anchor.value.y,
|
||||
...anchor.value
|
||||
} as DOMRect)
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UPopover
|
||||
:open="open"
|
||||
:reference="reference"
|
||||
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
|
||||
@pointerenter="open = true"
|
||||
@pointerleave="open = false"
|
||||
@pointermove="(ev) => {
|
||||
anchor.x = ev.clientX
|
||||
anchor.y = ev.clientY
|
||||
}"
|
||||
>
|
||||
Hover me
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
{{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</template>
|
||||
@@ -1,106 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
date: string
|
||||
status: 'paid' | 'failed' | 'refunded'
|
||||
email: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
const data = ref<Payment[]>([{
|
||||
id: '4600',
|
||||
date: '2024-03-11T15:30:00',
|
||||
status: 'paid',
|
||||
email: 'james.anderson@example.com',
|
||||
amount: 594
|
||||
}, {
|
||||
id: '4599',
|
||||
date: '2024-03-11T10:10:00',
|
||||
status: 'failed',
|
||||
email: 'mia.white@example.com',
|
||||
amount: 276
|
||||
}, {
|
||||
id: '4598',
|
||||
date: '2024-03-11T08:50:00',
|
||||
status: 'refunded',
|
||||
email: 'william.brown@example.com',
|
||||
amount: 315
|
||||
}, {
|
||||
id: '4597',
|
||||
date: '2024-03-10T19:45:00',
|
||||
status: 'paid',
|
||||
email: 'emma.davis@example.com',
|
||||
amount: 529
|
||||
}, {
|
||||
id: '4596',
|
||||
date: '2024-03-10T15:55:00',
|
||||
status: 'paid',
|
||||
email: 'ethan.harris@example.com',
|
||||
amount: 639
|
||||
}])
|
||||
|
||||
const columns: TableColumn<Payment>[] = [{
|
||||
accessorKey: 'id',
|
||||
header: '#',
|
||||
cell: ({ row }) => `#${row.getValue('id')}`
|
||||
}, {
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue('date')).toLocaleString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => {
|
||||
const color = ({
|
||||
paid: 'success' as const,
|
||||
failed: 'error' as const,
|
||||
refunded: 'neutral' as const
|
||||
})[row.getValue('status') as string]
|
||||
|
||||
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'email',
|
||||
header: 'Email'
|
||||
}, {
|
||||
accessorKey: 'amount',
|
||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||
footer: ({ column }) => {
|
||||
const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')), 0)
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(total)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const amount = Number.parseFloat(row.getValue('amount'))
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, formatted)
|
||||
}
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTable :data="data" :columns="columns" class="flex-1" />
|
||||
</template>
|
||||
@@ -1,8 +1,7 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import type { TableColumn } from '@nuxt/ui'
|
||||
import { getGroupedRowModel } from '@tanstack/vue-table'
|
||||
import type { GroupingOptions } from '@tanstack/vue-table'
|
||||
import { getGroupedRowModel, type GroupingOptions } from '@tanstack/vue-table'
|
||||
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
|
||||
|
||||
@@ -1,159 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import type { ContextMenuItem, TableColumn, TableRow } from '@nuxt/ui'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
const UCheckbox = resolveComponent('UCheckbox')
|
||||
|
||||
const toast = useToast()
|
||||
const { copy } = useClipboard()
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
date: string
|
||||
status: 'paid' | 'failed' | 'refunded'
|
||||
email: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
const data = ref<Payment[]>([{
|
||||
id: '4600',
|
||||
date: '2024-03-11T15:30:00',
|
||||
status: 'paid',
|
||||
email: 'james.anderson@example.com',
|
||||
amount: 594
|
||||
}, {
|
||||
id: '4599',
|
||||
date: '2024-03-11T10:10:00',
|
||||
status: 'failed',
|
||||
email: 'mia.white@example.com',
|
||||
amount: 276
|
||||
}, {
|
||||
id: '4598',
|
||||
date: '2024-03-11T08:50:00',
|
||||
status: 'refunded',
|
||||
email: 'william.brown@example.com',
|
||||
amount: 315
|
||||
}, {
|
||||
id: '4597',
|
||||
date: '2024-03-10T19:45:00',
|
||||
status: 'paid',
|
||||
email: 'emma.davis@example.com',
|
||||
amount: 529
|
||||
}, {
|
||||
id: '4596',
|
||||
date: '2024-03-10T15:55:00',
|
||||
status: 'paid',
|
||||
email: 'ethan.harris@example.com',
|
||||
amount: 639
|
||||
}])
|
||||
|
||||
const columns: TableColumn<Payment>[] = [{
|
||||
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'
|
||||
})
|
||||
}, {
|
||||
accessorKey: 'id',
|
||||
header: '#',
|
||||
cell: ({ row }) => `#${row.getValue('id')}`
|
||||
}, {
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue('date')).toLocaleString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => {
|
||||
const color = ({
|
||||
paid: 'success' as const,
|
||||
failed: 'error' as const,
|
||||
refunded: 'neutral' as const
|
||||
})[row.getValue('status') as string]
|
||||
|
||||
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'email',
|
||||
header: 'Email'
|
||||
}, {
|
||||
accessorKey: 'amount',
|
||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||
cell: ({ row }) => {
|
||||
const amount = Number.parseFloat(row.getValue('amount'))
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, formatted)
|
||||
}
|
||||
}]
|
||||
|
||||
const items = ref<ContextMenuItem[]>([])
|
||||
|
||||
function getRowItems(row: TableRow<Payment>) {
|
||||
return [{
|
||||
type: 'label' as const,
|
||||
label: 'Actions'
|
||||
}, {
|
||||
label: 'Copy payment ID',
|
||||
onSelect() {
|
||||
copy(row.original.id)
|
||||
|
||||
toast.add({
|
||||
title: 'Payment ID copied to clipboard!',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check'
|
||||
})
|
||||
}
|
||||
}, {
|
||||
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
|
||||
onSelect() {
|
||||
row.toggleExpanded()
|
||||
}
|
||||
}, {
|
||||
type: 'separator' as const
|
||||
}, {
|
||||
label: 'View customer'
|
||||
}, {
|
||||
label: 'View payment details'
|
||||
}]
|
||||
}
|
||||
|
||||
function onContextmenu(_e: Event, row: TableRow<Payment>) {
|
||||
items.value = getRowItems(row)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UContextMenu :items="items">
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
class="flex-1"
|
||||
@contextmenu="onContextmenu"
|
||||
>
|
||||
<template #expanded="{ row }">
|
||||
<pre>{{ row.original }}</pre>
|
||||
</template>
|
||||
</UTable>
|
||||
</UContextMenu>
|
||||
</template>
|
||||
@@ -1,157 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { h, resolveComponent } from 'vue'
|
||||
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||
|
||||
const UBadge = resolveComponent('UBadge')
|
||||
const UCheckbox = resolveComponent('UCheckbox')
|
||||
|
||||
type Payment = {
|
||||
id: string
|
||||
date: string
|
||||
status: 'paid' | 'failed' | 'refunded'
|
||||
email: string
|
||||
amount: number
|
||||
}
|
||||
|
||||
const data = ref<Payment[]>([{
|
||||
id: '4600',
|
||||
date: '2024-03-11T15:30:00',
|
||||
status: 'paid',
|
||||
email: 'james.anderson@example.com',
|
||||
amount: 594
|
||||
}, {
|
||||
id: '4599',
|
||||
date: '2024-03-11T10:10:00',
|
||||
status: 'failed',
|
||||
email: 'mia.white@example.com',
|
||||
amount: 276
|
||||
}, {
|
||||
id: '4598',
|
||||
date: '2024-03-11T08:50:00',
|
||||
status: 'refunded',
|
||||
email: 'william.brown@example.com',
|
||||
amount: 315
|
||||
}, {
|
||||
id: '4597',
|
||||
date: '2024-03-10T19:45:00',
|
||||
status: 'paid',
|
||||
email: 'emma.davis@example.com',
|
||||
amount: 529
|
||||
}, {
|
||||
id: '4596',
|
||||
date: '2024-03-10T15:55:00',
|
||||
status: 'paid',
|
||||
email: 'ethan.harris@example.com',
|
||||
amount: 639
|
||||
}])
|
||||
|
||||
const columns: TableColumn<Payment>[] = [{
|
||||
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'
|
||||
})
|
||||
}, {
|
||||
accessorKey: 'id',
|
||||
header: '#',
|
||||
cell: ({ row }) => `#${row.getValue('id')}`
|
||||
}, {
|
||||
accessorKey: 'date',
|
||||
header: 'Date',
|
||||
cell: ({ row }) => {
|
||||
return new Date(row.getValue('date')).toLocaleString('en-US', {
|
||||
day: 'numeric',
|
||||
month: 'short',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
})
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'status',
|
||||
header: 'Status',
|
||||
cell: ({ row }) => {
|
||||
const color = ({
|
||||
paid: 'success' as const,
|
||||
failed: 'error' as const,
|
||||
refunded: 'neutral' as const
|
||||
})[row.getValue('status') as string]
|
||||
|
||||
return h(UBadge, { class: 'capitalize', variant: 'subtle', color }, () => row.getValue('status'))
|
||||
}
|
||||
}, {
|
||||
accessorKey: 'email',
|
||||
header: 'Email'
|
||||
}, {
|
||||
accessorKey: 'amount',
|
||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||
cell: ({ row }) => {
|
||||
const amount = Number.parseFloat(row.getValue('amount'))
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(amount)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, formatted)
|
||||
}
|
||||
}]
|
||||
|
||||
const anchor = ref({ x: 0, y: 0 })
|
||||
|
||||
const reference = computed(() => ({
|
||||
getBoundingClientRect: () =>
|
||||
({
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: anchor.value.x,
|
||||
right: anchor.value.x,
|
||||
top: anchor.value.y,
|
||||
bottom: anchor.value.y,
|
||||
...anchor.value
|
||||
} as DOMRect)
|
||||
}))
|
||||
|
||||
const open = ref(false)
|
||||
const openDebounced = refDebounced(open, 10)
|
||||
const selectedRow = ref<TableRow<Payment> | null>(null)
|
||||
|
||||
function onHover(_e: Event, row: TableRow<Payment> | null) {
|
||||
selectedRow.value = row
|
||||
|
||||
open.value = !!row
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full flex-1 gap-1">
|
||||
<UTable
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
class="flex-1"
|
||||
@pointermove="(ev: PointerEvent) => {
|
||||
anchor.x = ev.clientX
|
||||
anchor.y = ev.clientY
|
||||
}"
|
||||
@hover="onHover"
|
||||
/>
|
||||
|
||||
<UPopover
|
||||
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
|
||||
:open="openDebounced"
|
||||
:reference="reference"
|
||||
>
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
{{ selectedRow?.original?.id }}
|
||||
</div>
|
||||
</template>
|
||||
</UPopover>
|
||||
</div>
|
||||
</template>
|
||||
@@ -112,7 +112,7 @@ function onSelect(row: TableRow<Payment>, e?: Event) {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-full flex-1 gap-1">
|
||||
<div class=" flex w-full flex-1 gap-1">
|
||||
<div class="flex-1">
|
||||
<UTable
|
||||
ref="table"
|
||||
@@ -1,16 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const toast = useToast()
|
||||
|
||||
function showToast() {
|
||||
toast.add({
|
||||
title: 'Uh oh! Something went wrong.',
|
||||
description: 'There was a problem with your request.',
|
||||
icon: 'i-lucide-wifi',
|
||||
progress: false
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UButton label="Show toast" color="neutral" variant="outline" @click="showToast" />
|
||||
</template>
|
||||
@@ -1,41 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
const open = ref(false)
|
||||
const anchor = ref({ x: 0, y: 0 })
|
||||
|
||||
const reference = computed(() => ({
|
||||
getBoundingClientRect: () =>
|
||||
({
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: anchor.value.x,
|
||||
right: anchor.value.x,
|
||||
top: anchor.value.y,
|
||||
bottom: anchor.value.y,
|
||||
...anchor.value
|
||||
} as DOMRect)
|
||||
}))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<UTooltip
|
||||
:open="open"
|
||||
:reference="reference"
|
||||
:content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }"
|
||||
>
|
||||
<div
|
||||
class="flex items-center justify-center rounded-md border border-dashed border-accented text-sm aspect-video w-72"
|
||||
@pointerenter="open = true"
|
||||
@pointerleave="open = false"
|
||||
@pointermove="(ev) => {
|
||||
anchor.x = ev.clientX
|
||||
anchor.y = ev.clientY
|
||||
}"
|
||||
>
|
||||
Hover me
|
||||
</div>
|
||||
|
||||
<template #content>
|
||||
{{ anchor.x.toFixed(0) }} - {{ anchor.y.toFixed(0) }}
|
||||
</template>
|
||||
</UTooltip>
|
||||
</template>
|
||||
@@ -107,10 +107,6 @@ export function useLinks() {
|
||||
to: 'https://github.com/Justineo/tempad-dev-plugin-nuxt-ui',
|
||||
target: '_blank'
|
||||
}]
|
||||
}, {
|
||||
label: 'Blog',
|
||||
icon: 'i-lucide-file-text',
|
||||
to: '/blog'
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-lucide-rocket',
|
||||
|
||||
@@ -57,10 +57,6 @@ export function useSearchLinks() {
|
||||
description: 'Meet the team behind Nuxt UI.',
|
||||
icon: 'i-lucide-users',
|
||||
to: '/team'
|
||||
}, {
|
||||
label: 'Blog',
|
||||
icon: 'i-lucide-file-text',
|
||||
to: '/blog'
|
||||
}, {
|
||||
label: 'Releases',
|
||||
icon: 'i-lucide-rocket',
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
seo:
|
||||
title: Nuxt UI Blog
|
||||
description: Read the latest news, tutorials, and updates about Nuxt UI.
|
||||
title: Nuxt [UI]{.text-primary} Blog
|
||||
navigation.title: Blog
|
||||
description: Read the latest news, tutorials, and updates about Nuxt UI.
|
||||
navigation.icon: i-lucide-newspaper
|
||||
@@ -1,183 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
import { kebabCase } from 'scule'
|
||||
|
||||
const route = useRoute()
|
||||
|
||||
const [{ data: page }, { data: surround }] = await Promise.all([
|
||||
useAsyncData(kebabCase(route.path), () => queryCollection('blog').path(route.path).first()),
|
||||
useAsyncData(`${kebabCase(route.path)}-surround`, () => {
|
||||
return queryCollectionItemSurroundings('blog', route.path, {
|
||||
fields: ['description']
|
||||
}).order('date', 'DESC')
|
||||
})
|
||||
])
|
||||
|
||||
if (!page.value) {
|
||||
throw createError({ statusCode: 404, statusMessage: 'Article not found', fatal: true })
|
||||
}
|
||||
|
||||
const title = page.value.seo?.title || page.value.title
|
||||
const description = page.value.seo?.description || page.value.description
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogDescription: description,
|
||||
ogTitle: title
|
||||
})
|
||||
|
||||
if (page.value.image) {
|
||||
defineOgImage({ url: page.value.image })
|
||||
} else {
|
||||
defineOgImageComponent('Docs', {
|
||||
headline: 'Blog',
|
||||
title,
|
||||
description
|
||||
})
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
}).toUpperCase()
|
||||
}
|
||||
|
||||
const getCategoryVariant = (category: string) => {
|
||||
switch (category?.toLowerCase()) {
|
||||
case 'release': return 'solid'
|
||||
case 'tutorial': return 'soft'
|
||||
case 'improvement': return 'soft'
|
||||
default: return 'soft'
|
||||
}
|
||||
}
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category?.toLowerCase()) {
|
||||
case 'release': return 'i-lucide-rocket'
|
||||
case 'tutorial': return 'i-lucide-book-open'
|
||||
case 'improvement': return 'i-lucide-trending-up'
|
||||
default: return 'i-lucide-file-text'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="page" class="min-h-screen">
|
||||
<div class="border-b border-default">
|
||||
<UContainer class="py-4">
|
||||
<ULink to="/blog" class="flex items-center gap-2 text-sm">
|
||||
<UIcon name="i-lucide-chevron-left" class="size-4" />
|
||||
Back to Blog
|
||||
</ULink>
|
||||
</UContainer>
|
||||
</div>
|
||||
|
||||
<div class="py-16 sm:pt-20 pb-10">
|
||||
<UContainer class="max-w-4xl">
|
||||
<div class="text-center space-y-6">
|
||||
<div class="flex items-center justify-center gap-4 text-sm">
|
||||
<UBadge
|
||||
v-if="page.category"
|
||||
:variant="getCategoryVariant(page.category)"
|
||||
size="sm"
|
||||
class="font-mono text-xs gap-2"
|
||||
>
|
||||
<UIcon :name="getCategoryIcon(page.category)" class="size-3" />
|
||||
{{ page.category?.toUpperCase() }}
|
||||
</UBadge>
|
||||
|
||||
<span class="text-muted font-mono text-xs">
|
||||
{{ formatDate(page.date) }}
|
||||
</span>
|
||||
|
||||
<span v-if="page.minRead" class="text-muted font-mono text-xs">
|
||||
{{ page.minRead }} MIN READ
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ duration: 0.6 }"
|
||||
>
|
||||
<h1 class="text-3xl sm:text-4xl lg:text-5xl font-bold text-highlighted leading-tight">
|
||||
{{ page.title }}
|
||||
</h1>
|
||||
</Motion>
|
||||
|
||||
<Motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ delay: 0.1, duration: 0.6 }"
|
||||
>
|
||||
<p class="text-lg text-muted max-w-2xl mx-auto leading-relaxed">
|
||||
{{ page.description }}
|
||||
</p>
|
||||
</Motion>
|
||||
|
||||
<Motion
|
||||
v-if="page.authors?.length"
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ delay: 0.2, duration: 0.6 }"
|
||||
class="flex justify-center"
|
||||
>
|
||||
<UAvatarGroup>
|
||||
<ULink
|
||||
v-for="(author, index) in page.authors"
|
||||
:key="index"
|
||||
:to="author.to"
|
||||
raw
|
||||
>
|
||||
<UAvatar v-bind="author.avatar" />
|
||||
</ULink>
|
||||
</UAvatarGroup>
|
||||
</Motion>
|
||||
</div>
|
||||
</UContainer>
|
||||
</div>
|
||||
|
||||
<div v-if="page.image" class="py-4">
|
||||
<UContainer class="max-w-6xl">
|
||||
<Motion
|
||||
:initial="{ opacity: 0, y: 30 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ delay: 0.3, duration: 0.8 }"
|
||||
>
|
||||
<NuxtImg
|
||||
:src="page.image"
|
||||
:alt="page.title"
|
||||
class="w-full max-h-[400px] object-cover object-center max-w-5xl mx-auto"
|
||||
/>
|
||||
</Motion>
|
||||
</UContainer>
|
||||
</div>
|
||||
|
||||
<div class="py-12 sm:py-16">
|
||||
<UContainer class="max-w-3xl">
|
||||
<Motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ delay: 0.4, duration: 0.6 }"
|
||||
>
|
||||
<ContentRenderer
|
||||
v-if="page.body"
|
||||
:value="page"
|
||||
/>
|
||||
</Motion>
|
||||
|
||||
<div v-if="surround?.length" class="mt-16 pt-8 border-t border-default">
|
||||
<Motion
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ delay: 0.6, duration: 0.6 }"
|
||||
>
|
||||
<UContentSurround :surround="surround" />
|
||||
</Motion>
|
||||
</div>
|
||||
</UContainer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -1,255 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
// @ts-expect-error - yaml import not typed
|
||||
import page from '.blog.yml'
|
||||
|
||||
const { data: posts } = await useAsyncData('blogs', () =>
|
||||
queryCollection('blog').order('date', 'DESC').all()
|
||||
)
|
||||
|
||||
const title = page.seo?.title || page.title
|
||||
const description = page.seo?.description || page.description
|
||||
|
||||
useSeoMeta({
|
||||
title,
|
||||
description,
|
||||
ogTitle: title,
|
||||
ogDescription: description
|
||||
})
|
||||
|
||||
const selectedFilter = ref('all')
|
||||
const searchQuery = ref('')
|
||||
|
||||
const availableFilters = computed(() => {
|
||||
if (!posts.value?.length) return [{ key: 'all', label: 'ALL', count: 0 }]
|
||||
|
||||
const postsData = posts.value
|
||||
const categories = new Set(postsData.map(post => post.category?.toLowerCase()).filter(Boolean))
|
||||
|
||||
const filters = [
|
||||
{ key: 'all', label: 'ALL', count: postsData.length }
|
||||
]
|
||||
|
||||
categories.forEach((category) => {
|
||||
const count = postsData.filter(p => p.category?.toLowerCase() === category).length
|
||||
const label = category.replace(/\b\w/g, l => l.toUpperCase()).replace(/([a-z])([A-Z])/g, '$1 $2')
|
||||
|
||||
filters.push({
|
||||
key: category,
|
||||
label: label,
|
||||
count
|
||||
})
|
||||
})
|
||||
|
||||
return filters.sort((a, b) => {
|
||||
if (a.key === 'all') return -1
|
||||
if (b.key === 'all') return 1
|
||||
return b.count - a.count
|
||||
})
|
||||
})
|
||||
|
||||
const filteredPosts = computed(() => {
|
||||
if (!posts.value) return []
|
||||
|
||||
let filtered = posts.value
|
||||
|
||||
if (selectedFilter.value !== 'all') {
|
||||
filtered = filtered.filter(post => post.category?.toLowerCase() === selectedFilter.value)
|
||||
}
|
||||
|
||||
if (searchQuery.value) {
|
||||
const query = searchQuery.value.toLowerCase()
|
||||
filtered = filtered.filter(post =>
|
||||
post.title?.toLowerCase().includes(query)
|
||||
|| post.description?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
|
||||
return filtered
|
||||
})
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString('en-US', {
|
||||
month: 'short',
|
||||
day: '2-digit'
|
||||
}).toUpperCase()
|
||||
}
|
||||
|
||||
const getCategoryVariant = (category: string) => {
|
||||
switch (category?.toLowerCase()) {
|
||||
case 'release': return 'solid'
|
||||
case 'tutorial': return 'soft'
|
||||
case 'improvement': return 'soft'
|
||||
default: return 'soft'
|
||||
}
|
||||
}
|
||||
|
||||
const getCategoryIcon = (category: string) => {
|
||||
switch (category?.toLowerCase()) {
|
||||
case 'release': return 'i-lucide-rocket'
|
||||
case 'tutorial': return 'i-lucide-book-open'
|
||||
case 'improvement': return 'i-lucide-trending-up'
|
||||
default: return 'i-lucide-file-text'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="page" class="relative grid grid-rows-[auto_auto_1fr] min-h-[calc(100vh-150px)]">
|
||||
<UPageHero :links="page.links" :ui="{ container: 'relative py-10 sm:py-16 lg:py-24' }">
|
||||
<LazyStarsBg />
|
||||
|
||||
<div aria-hidden="true" class="absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
|
||||
<template #title>
|
||||
<MDC :value="page.title" unwrap="p" cache-key="pro-templates-hero-title" />
|
||||
</template>
|
||||
|
||||
<template #description>
|
||||
<MDC :value="page.description" unwrap="p" cache-key="pro-templates-hero-description" />
|
||||
</template>
|
||||
</UPageHero>
|
||||
|
||||
<UPageBody class="!my-0 !py-0 border-y border-default">
|
||||
<UContainer>
|
||||
<div class="border-x border-default px-4 sm:px-6 py-6 sm:py-8">
|
||||
<div class="flex flex-col lg:flex-row lg:items-center justify-between gap-6 lg:gap-8 sm:mb-6">
|
||||
<div class="flex flex-wrap items-center gap-2 sm:gap-3">
|
||||
<Motion
|
||||
v-for="(filter, index) in availableFilters"
|
||||
:key="filter.key"
|
||||
:initial="{ opacity: 0, y: 10 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
:transition="{ delay: index * 0.1 }"
|
||||
>
|
||||
<UButton
|
||||
:variant="selectedFilter === filter.key ? 'solid' : 'ghost'"
|
||||
:color="selectedFilter === filter.key ? 'primary' : 'neutral'"
|
||||
size="sm"
|
||||
class="font-medium transition-all duration-200 hover:scale-105 focus:scale-100 rounded-none text-xs sm:text-sm"
|
||||
:leading-icon="selectedFilter === filter.key ? 'i-lucide-check' : 'i-lucide-circle'"
|
||||
:label="filter.label"
|
||||
@click="selectedFilter = filter.key"
|
||||
>
|
||||
<template #trailing>
|
||||
<UBadge
|
||||
:variant="selectedFilter === filter.key ? 'solid' : 'soft'"
|
||||
size="xs"
|
||||
>
|
||||
{{ filter.count }}
|
||||
</UBadge>
|
||||
</template>
|
||||
</UButton>
|
||||
</Motion>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col sm:flex-row items-stretch sm:items-center gap-2 sm:gap-3">
|
||||
<div class="relative">
|
||||
<UInput
|
||||
v-model="searchQuery"
|
||||
placeholder="Search posts..."
|
||||
icon="i-lucide-search"
|
||||
class="w-full sm:w-64"
|
||||
:ui="{
|
||||
base: 'rounded-none'
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<UButton
|
||||
variant="ghost"
|
||||
class="rounded-none whitespace-nowrap"
|
||||
icon="i-lucide-external-link"
|
||||
label="Follow @nuxt_js on X"
|
||||
to="https://x.com/nuxt_js"
|
||||
target="_blank"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-x border-t border-default !gap-0">
|
||||
<Motion
|
||||
v-for="(post, index) in filteredPosts"
|
||||
:key="post.path"
|
||||
:initial="{ opacity: 0, x: -20 }"
|
||||
:animate="{ opacity: 1, x: 0 }"
|
||||
:transition="{ delay: index * 0.05, type: 'spring', stiffness: 300, damping: 30 }"
|
||||
class="group border-b border-default last:border-b-0"
|
||||
>
|
||||
<ULink
|
||||
:to="post.path"
|
||||
class="flex flex-col sm:flex-row sm:items-center justify-between p-4 sm:p-6 hover:bg-muted/30 transition-all duration-200 gap-4 sm:gap-6"
|
||||
>
|
||||
<div class="flex flex-col sm:flex-row sm:items-center gap-3 sm:gap-6 flex-1 min-w-0">
|
||||
<div class="text-xs text-muted font-mono shrink-0 sm:min-w-[60px]">
|
||||
{{ formatDate(post.date) }}
|
||||
</div>
|
||||
|
||||
<UBadge
|
||||
:variant="getCategoryVariant(post.category)"
|
||||
size="sm"
|
||||
class="font-mono text-xs justify-center gap-2 shrink-0 self-start sm:self-center"
|
||||
>
|
||||
<UIcon :name="getCategoryIcon(post.category)" class="size-3" />
|
||||
{{ post.category?.toUpperCase() || 'POST' }}
|
||||
</UBadge>
|
||||
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="font-medium text-highlighted group-hover:text-primary transition-colors duration-200 truncate sm:text-base">
|
||||
{{ post.title }}
|
||||
</h3>
|
||||
<p class="text-sm text-muted mt-1 line-clamp-2 sm:line-clamp-1">
|
||||
{{ post.description }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-between sm:justify-end gap-3 sm:gap-2 shrink-0">
|
||||
<UAvatarGroup v-if="post.authors?.length" size="sm" class="sm:size-sm">
|
||||
<UAvatar
|
||||
v-for="author in post.authors.slice(0, 3)"
|
||||
:key="author.name"
|
||||
:src="author.avatar?.src"
|
||||
:alt="author.name"
|
||||
size="sm"
|
||||
/>
|
||||
</UAvatarGroup>
|
||||
|
||||
<UIcon
|
||||
name="i-lucide-chevron-right"
|
||||
class="size-4 text-muted group-hover:text-highlighted transition-colors duration-200 shrink-0"
|
||||
/>
|
||||
</div>
|
||||
</ULink>
|
||||
</Motion>
|
||||
|
||||
<Motion
|
||||
v-if="filteredPosts.length === 0"
|
||||
:initial="{ opacity: 0, y: 20 }"
|
||||
:animate="{ opacity: 1, y: 0 }"
|
||||
class="text-center py-12 sm:py-16 px-4 sm:px-6"
|
||||
>
|
||||
<UIcon name="i-lucide-search-x" class="size-10 sm:size-12 text-muted mx-auto mb-4" />
|
||||
<h3 class="text-lg font-medium mb-2">
|
||||
No posts found
|
||||
</h3>
|
||||
<p class="text-muted mb-4 text-sm sm:text-base">
|
||||
{{ searchQuery ? `No posts match "${searchQuery}"` : 'No posts in this category yet' }}
|
||||
</p>
|
||||
<UButton
|
||||
v-if="selectedFilter !== 'all' || searchQuery"
|
||||
variant="outline"
|
||||
label="Clear filters"
|
||||
class="rounded-none"
|
||||
@click="selectedFilter = 'all'; searchQuery = ''"
|
||||
/>
|
||||
</Motion>
|
||||
</div>
|
||||
</UContainer>
|
||||
</UPageBody>
|
||||
|
||||
<UContainer class="relative min-h-24">
|
||||
<div aria-hidden="true" class="absolute z-[-1] border-x border-default inset-0 mx-4 sm:mx-6 lg:mx-8" />
|
||||
</UContainer>
|
||||
</div>
|
||||
</template>
|
||||
@@ -16,12 +16,12 @@ function handleMessage(message) {
|
||||
async function handleFormatMessage(message) {
|
||||
if (!globalThis.prettier) {
|
||||
await Promise.all([
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/standalone.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/babel.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/estree.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/html.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/markdown.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.6.2/plugins/typescript.js')
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/standalone.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/babel.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/estree.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/html.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/markdown.js'),
|
||||
import('https://cdn.jsdelivr.net/npm/prettier@3.5.2/plugins/typescript.js')
|
||||
])
|
||||
}
|
||||
|
||||
|
||||
@@ -13,22 +13,6 @@ const Button = z.object({
|
||||
target: z.enum(['_blank', '_self']).optional()
|
||||
})
|
||||
|
||||
const Image = z.object({
|
||||
src: z.string(),
|
||||
alt: z.string(),
|
||||
width: z.number().optional(),
|
||||
height: z.number().optional()
|
||||
})
|
||||
|
||||
const Author = z.object({
|
||||
name: z.string(),
|
||||
description: z.string().optional(),
|
||||
username: z.string().optional(),
|
||||
twitter: z.string().optional(),
|
||||
to: z.string().optional(),
|
||||
avatar: Image.optional()
|
||||
})
|
||||
|
||||
const schema = z.object({
|
||||
category: z.enum(['layout', 'form', 'element', 'navigation', 'data', 'overlay']).optional(),
|
||||
framework: z.string().optional(),
|
||||
@@ -91,18 +75,5 @@ export const collections = {
|
||||
})
|
||||
}))
|
||||
})
|
||||
}),
|
||||
blog: defineCollection({
|
||||
type: 'page',
|
||||
source: 'blog/*',
|
||||
schema: z.object({
|
||||
image: z.string().editor({ input: 'media' }),
|
||||
authors: z.array(Author),
|
||||
date: z.string().date(),
|
||||
minRead: z.number(),
|
||||
draft: z.boolean().optional(),
|
||||
category: z.enum(['Release', 'Tutorial', 'Announcement', 'Article']),
|
||||
tags: z.array(z.string())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -32,12 +32,6 @@ props:
|
||||
You can use any name from the <https://icones.js.org> collection.
|
||||
::
|
||||
|
||||
::warning
|
||||
When using collections with a dash (`-`), you need to separate the icon name from the collection name with a colon (`:`) as `@iconify/vue` does not handle this case like `@nuxt/icon`. For example, instead of `i-simple-icons-github` you need to write `i-simple-icons:github` or `simple-icons:github`.
|
||||
|
||||
Learn more about the [Iconify naming convention](https://iconify.design/docs/icon-components/vue/#icon).
|
||||
::
|
||||
|
||||
### Component Props
|
||||
|
||||
Some components also have an `icon` prop to display an icon, like the [Button](/components/button) for example:
|
||||
|
||||
@@ -125,7 +125,7 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
|
||||
|
||||
::
|
||||
|
||||
### Extend locale :badge{label="New" class="align-text-top"}
|
||||
### Extend locale :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ Look at the `code` parameter, there you need to pass the iso code of the languag
|
||||
|
||||
::
|
||||
|
||||
### Extend locale :badge{label="New" class="align-text-top"}
|
||||
### Extend locale :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can customize an existing locale by overriding its `messages` or `code` using the `extendLocale` composable:
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/CheckboxGroup.vue
|
||||
navigation.badge: New
|
||||
---
|
||||
|
||||
|
||||
|
||||
@@ -156,7 +156,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Variant
|
||||
### Variant :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `variant` prop to change the variant of the Checkbox.
|
||||
|
||||
@@ -190,7 +190,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Indicator
|
||||
### Indicator :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
||||
|
||||
|
||||
@@ -52,8 +52,8 @@ Each group contains an `items` array of objects that define the commands. Each i
|
||||
- `loading?: boolean`{lang="ts-type"}
|
||||
- `disabled?: boolean`{lang="ts-type"}
|
||||
- [`slot?: string`{lang="ts-type"}](#with-custom-slot)
|
||||
- `placeholder?: string`{lang="ts-type"}
|
||||
- `children?: CommandPaletteItem[]`{lang="ts-type"}
|
||||
- `placeholder?: string`{lang="ts-type"} :badge{label="Soon"}
|
||||
- `children?: CommandPaletteItem[]`{lang="ts-type"} :badge{label="Soon"}
|
||||
- `onSelect?(e?: Event): void`{lang="ts-type"}
|
||||
- `class?: any`{lang="ts-type"}
|
||||
- `ui?: { item?: ClassNameValue, itemLeadingIcon?: ClassNameValue, itemLeadingAvatarSize?: ClassNameValue, itemLeadingAvatar?: ClassNameValue, itemLeadingChipSize?: ClassNameValue, itemLeadingChip?: ClassNameValue, itemLabel?: ClassNameValue, itemLabelPrefix?: ClassNameValue, itemLabelBase?: ClassNameValue, itemLabelSuffix?: ClassNameValue, itemTrailing?: ClassNameValue, itemTrailingKbds?: ClassNameValue, itemTrailingKbdsSize?: ClassNameValue, itemTrailingHighlightedIcon?: ClassNameValue, itemTrailingIcon?: ClassNameValue }`{lang="ts-type"}
|
||||
@@ -327,7 +327,7 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.ch
|
||||
:::
|
||||
::
|
||||
|
||||
### Trailing Icon :badge{label="New" class="align-text-top"}
|
||||
### Trailing Icon :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Use the `trailing-icon` prop to customize the trailing [Icon](/components/icon) when an item has children. Defaults to `i-lucide-chevron-right`.
|
||||
|
||||
@@ -565,7 +565,7 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
|
||||
:::
|
||||
::
|
||||
|
||||
### Back :badge{label="New" class="align-text-top"}
|
||||
### Back :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Use the `back` prop to customize or hide the back button (with `false` value) displayed when navigating into a submenu.
|
||||
|
||||
@@ -604,7 +604,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Back Icon :badge{label="New" class="align-text-top"}
|
||||
### Back Icon :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Use the `back-icon` prop to customize the back button [Icon](/components/icon). Defaults to `i-lucide-arrow-left`.
|
||||
|
||||
@@ -717,7 +717,7 @@ props:
|
||||
This example uses the `@update:model-value` event to reset the search term when an item is selected.
|
||||
::
|
||||
|
||||
### With children in items :badge{label="New" class="align-text-top"}
|
||||
### With children in items :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can create hierarchical menus by using the `children` property in items. When an item has children, it will automatically display a chevron icon and enable navigation into a submenu.
|
||||
|
||||
@@ -877,20 +877,6 @@ props:
|
||||
This can be useful when using the CommandPalette inside a [`Modal`](/components/modal) for example.
|
||||
::
|
||||
|
||||
### With footer slot :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Use the `#footer` slot to add custom content at the bottom of the CommandPalette, such as keyboard shortcuts help or additional actions.
|
||||
|
||||
::component-example
|
||||
---
|
||||
collapse: true
|
||||
name: 'command-palette-footer-slot-example'
|
||||
class: '!p-0'
|
||||
props:
|
||||
autofocus: false
|
||||
---
|
||||
::
|
||||
|
||||
### With custom slot
|
||||
|
||||
Use the `slot` property to customize a specific item or group.
|
||||
|
||||
@@ -9,7 +9,7 @@ links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/InputTags.vue
|
||||
navigation.badge: New
|
||||
navigation.badge: Soon
|
||||
---
|
||||
|
||||
## Usage
|
||||
@@ -51,17 +51,6 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Max Length :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Use the `max-length` prop to set the maximum number of characters allowed in a tag.
|
||||
|
||||
::component-code
|
||||
---
|
||||
props:
|
||||
maxLength: 4
|
||||
---
|
||||
::
|
||||
|
||||
### Color
|
||||
|
||||
Use the `color` prop to change the ring color when the InputTags is focused.
|
||||
|
||||
@@ -889,7 +889,7 @@ You can inspect the DOM to see each item's content being rendered.
|
||||
|
||||
## Examples
|
||||
|
||||
### With tooltip in items
|
||||
### With tooltip in items :badge{label="New" class="align-text-top"}
|
||||
|
||||
When orientation is `vertical` and the menu is `collapsed`, you can set the `tooltip` prop to `true` to display a [Tooltip](/components/tooltip) around items with their label but you can also use the `tooltip` property on each item to override the default tooltip.
|
||||
|
||||
@@ -994,7 +994,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### With popover in items
|
||||
### With popover in items :badge{label="New" class="align-text-top"}
|
||||
|
||||
When orientation is `vertical` and the menu is `collapsed`, you can set the `popover` prop to `true` to display a [Popover](/components/popover) around items with their children but you can also use the `popover` property on each item to override the default popover.
|
||||
|
||||
|
||||
@@ -202,17 +202,7 @@ name: 'popover-command-palette-example'
|
||||
---
|
||||
::
|
||||
|
||||
### With following cursor :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can make the Popover follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger) prop:
|
||||
|
||||
::component-example
|
||||
---
|
||||
name: 'popover-cursor-example'
|
||||
---
|
||||
::
|
||||
|
||||
### With anchor slot
|
||||
### With anchor slot :badge{label="New" class="align-text-top"}
|
||||
|
||||
You can use the `#anchor` slot to position the Popover against a custom element.
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Variant
|
||||
### Variant :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `variant` prop to change the variant of the RadioGroup.
|
||||
|
||||
@@ -240,7 +240,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Indicator
|
||||
### Indicator :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `indicator` prop to change the position or hide the indicator. Defaults to `start`.
|
||||
|
||||
|
||||
@@ -136,7 +136,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Tooltip
|
||||
### Tooltip :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `tooltip` prop to display a [Tooltip](/components/tooltip) around the Slider thumbs with the current value. You can set it to `true` for default behavior or pass an object to customize it with any property from the [Tooltip](/components/tooltip#props) component.
|
||||
|
||||
|
||||
@@ -77,7 +77,6 @@ Use the `columns` prop as an array of [ColumnDef](https://tanstack.com/table/lat
|
||||
|
||||
- `accessorKey`: [The key of the row object to use when extracting the value for the column.]{class="text-muted"}
|
||||
- `header`: [The header to display for the column. If a string is passed, it can be used as a default for the column ID. If a function is passed, it will be passed a props object for the header and should return the rendered header value (the exact type depends on the adapter being used).]{class="text-muted"}
|
||||
- `footer`: [The footer to display for the column. Works exactly like header, but is displayed under the table.]{class="text-muted"}
|
||||
- `cell`: [The cell to display each row for the column. If a function is passed, it will be passed a props object for the cell and should return the rendered cell value (the exact type depends on the adapter being used).]{class="text-muted"}
|
||||
- `meta`: [Extra properties for the column.]{class="text-muted"}
|
||||
- `class`:
|
||||
@@ -162,7 +161,7 @@ props:
|
||||
|
||||
### Sticky
|
||||
|
||||
Use the `sticky` prop to make the header or footer sticky.
|
||||
Use the `sticky` prop to make the header sticky.
|
||||
|
||||
::component-code
|
||||
---
|
||||
@@ -173,10 +172,6 @@ ignore:
|
||||
- class
|
||||
external:
|
||||
- data
|
||||
items:
|
||||
sticky:
|
||||
- true
|
||||
- false
|
||||
props:
|
||||
sticky: true
|
||||
data:
|
||||
@@ -271,8 +266,8 @@ You can group rows based on a given column value and show/hide sub rows via some
|
||||
|
||||
#### Important parts:
|
||||
|
||||
* Add `grouping` prop with an array of column ids you want to group by.
|
||||
* Add `grouping-options` prop. It must include `getGroupedRowModel`, you can import it from `@tanstack/vue-table` or implement your own.
|
||||
* Add prop `grouping` to `UTable` component with an array of column ids you want to group by.
|
||||
* Add prop `grouping-options` to `UTable`. It must include `getGroupedRowModel`, you can import it from `@tanstack/vue-table` or implement your own.
|
||||
* Expand rows via `row.toggleExpanded()` method on any cell of the row. Keep in mind, it also toggles `#expanded` slot.
|
||||
* Use `aggregateFn` on column definition to define how to aggregate the rows.
|
||||
* `agregatedCell` renderer on column definition only works if there is no `cell` renderer.
|
||||
@@ -309,86 +304,22 @@ class: '!p-0'
|
||||
You can use the `row-selection` prop to control the selection state of the rows (can be binded with `v-model`).
|
||||
::
|
||||
|
||||
### With row select event
|
||||
### With `@select` event
|
||||
|
||||
You can add a `@select` listener to make rows clickable with or without a checkbox column.
|
||||
You can add a `@select` listener to make rows clickable. The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
|
||||
|
||||
::note
|
||||
The handler function receives the `TableRow` instance as the first argument and an optional `Event` as the second argument.
|
||||
::
|
||||
|
||||
::component-example
|
||||
---
|
||||
prettier: true
|
||||
collapse: true
|
||||
name: 'table-row-select-event-example'
|
||||
highlights:
|
||||
- 123
|
||||
- 130
|
||||
class: '!p-0'
|
||||
---
|
||||
::
|
||||
|
||||
::tip
|
||||
You can use this to navigate to a page, open a modal or even to select the row manually.
|
||||
::
|
||||
|
||||
### With row context menu event :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can add a `@contextmenu` listener to make rows right clickable and wrap the Table in a [ContextMenu](/components/context-menu) component to display row actions for example.
|
||||
|
||||
::note
|
||||
The handler function receives the `Event` and `TableRow` instance as the first and second arguments respectively.
|
||||
::
|
||||
|
||||
::component-example
|
||||
---
|
||||
prettier: true
|
||||
collapse: true
|
||||
name: 'table-row-context-menu-event-example'
|
||||
name: 'table-row-selection-event-example'
|
||||
highlights:
|
||||
- 123
|
||||
- 130
|
||||
- 170
|
||||
class: '!p-0'
|
||||
---
|
||||
::
|
||||
|
||||
### With row hover event :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can add a `@hover` listener to make rows hoverable and use a [Popover](/components/popover) or a [Tooltip](/components/tooltip) component to display row details for example.
|
||||
|
||||
::note
|
||||
The handler function receives the `Event` and `TableRow` instance as the first and second arguments respectively.
|
||||
::
|
||||
|
||||
::component-example
|
||||
---
|
||||
prettier: true
|
||||
collapse: true
|
||||
name: 'table-row-hover-event-example'
|
||||
highlights:
|
||||
- 126
|
||||
- 149
|
||||
class: '!p-0'
|
||||
---
|
||||
::
|
||||
|
||||
::note
|
||||
This example is similar as the Popover [with following cursor example](/components/popover#with-following-cursor) and uses a [`refDebounced`](https://vueuse.org/shared/refDebounced/#refdebounced) to prevent the Popover from opening and closing too quickly when moving the cursor from one row to another.
|
||||
::
|
||||
|
||||
### With column footer :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can add a `footer` property to the column definition to render a footer for the column.
|
||||
|
||||
::component-example
|
||||
---
|
||||
prettier: true
|
||||
collapse: true
|
||||
name: 'table-column-footer-example'
|
||||
highlights:
|
||||
- 94
|
||||
- 108
|
||||
class: '!p-0'
|
||||
---
|
||||
::
|
||||
|
||||
@@ -124,7 +124,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Icon
|
||||
### Icon :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `icon` prop to show an [Icon](/components/icon) inside the Textarea.
|
||||
|
||||
@@ -157,7 +157,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Avatar
|
||||
### Avatar :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `avatar` prop to show an [Avatar](/components/avatar) inside the Textarea.
|
||||
|
||||
@@ -176,7 +176,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Loading
|
||||
### Loading :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `loading` prop to show a loading icon on the Textarea.
|
||||
|
||||
@@ -192,7 +192,7 @@ props:
|
||||
---
|
||||
::
|
||||
|
||||
### Loading Icon
|
||||
### Loading Icon :badge{label="New" class="align-text-top"}
|
||||
|
||||
Use the `loading-icon` prop to customize the loading icon. Defaults to `i-lucide-loader-circle`.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ links:
|
||||
- label: GitHub
|
||||
icon: i-simple-icons-github
|
||||
to: https://github.com/nuxt/ui/tree/v3/src/runtime/components/Timeline.vue
|
||||
navigation.badge: New
|
||||
navigation.badge: Soon
|
||||
---
|
||||
|
||||
## Usage
|
||||
|
||||
@@ -107,7 +107,7 @@ name: 'toast-color-example'
|
||||
|
||||
### Close
|
||||
|
||||
Pass a `close` field to customize or hide the close [Button](/components/button) (with `false` value).
|
||||
Pass a `close` field to customize or hide the close button (with `false` value).
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -143,7 +143,7 @@ You can customize this icon globally in your `vite.config.ts` under `ui.icons.cl
|
||||
|
||||
### Actions
|
||||
|
||||
Pass an `actions` field to add some [Button](/components/button) actions to the Toast.
|
||||
Pass an `actions` field to add some [Button](/components/button) actions to the Alert.
|
||||
|
||||
::component-example
|
||||
---
|
||||
@@ -155,23 +155,9 @@ name: 'toast-actions-example'
|
||||
---
|
||||
::
|
||||
|
||||
### Progress :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
Pass a `progress` field to customize or hide the [Progress](/components/progress) bar (with `false` value).
|
||||
|
||||
::tip
|
||||
The Progress bar inherits the Toast color by default, but you can override it using the `progress.color` field.
|
||||
::
|
||||
|
||||
::component-example
|
||||
---
|
||||
name: 'toast-progress-example'
|
||||
---
|
||||
::
|
||||
|
||||
### Orientation
|
||||
|
||||
Pass an `orientation` field to the `toast.add` method to change the orientation of the Toast.
|
||||
Use the `orientation` prop to change the orientation of the Toast.
|
||||
|
||||
::component-example
|
||||
---
|
||||
|
||||
@@ -186,16 +186,6 @@ name: 'tooltip-open-example'
|
||||
In this example, leveraging [`defineShortcuts`](/composables/define-shortcuts), you can toggle the Tooltip by pressing :kbd{value="O"}.
|
||||
::
|
||||
|
||||
### With following cursor :badge{label="Soon" class="align-text-top"}
|
||||
|
||||
You can make the Tooltip follow the cursor when hovering over an element using the [`reference`](https://reka-ui.com/docs/components/tooltip#trigger) prop:
|
||||
|
||||
::component-example
|
||||
---
|
||||
name: 'tooltip-cursor-example'
|
||||
---
|
||||
::
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
title: Nuxt UI v3
|
||||
description: Nuxt UI v3 is out! After 1500+ commits, this major redesign brings
|
||||
improved accessibility, Tailwind CSS v4 support, and full Vue compatibility
|
||||
navigation: false
|
||||
image: /assets/blog/nuxt-ui-v3.png
|
||||
minRead: 7
|
||||
authors:
|
||||
- name: Benjamin Canac
|
||||
avatar:
|
||||
src: https://github.com/benjamincanac.png
|
||||
to: https://x.com/benjamincanac
|
||||
- name: Sébastien Chopin
|
||||
avatar:
|
||||
src: https://github.com/atinux.png
|
||||
to: https://x.com/atinux
|
||||
- name: Hugo Richard
|
||||
avatar:
|
||||
src: https://github.com/hugorcd.png
|
||||
to: https://x.com/hugorcd__
|
||||
date: 2025-03-12T10:00:00.000Z
|
||||
category: Release
|
||||
---
|
||||
|
||||
We are thrilled to announce the release of Nuxt UI v3, a complete redesign of our UI library that brings significant improvements in accessibility, performance, and developer experience. This major update represents over 1500 commits of hard work, collaboration, and innovation from our team and the community.
|
||||
|
||||
## 🚀 Reimagined from the Ground Up
|
||||
|
||||
Nuxt UI v3 represents a major leap forward in our journey to provide the most comprehensive UI solution for Vue and Nuxt developers. This version has been rebuilt from the ground up with modern technologies and best practices in mind.
|
||||
|
||||
### **From HeadlessUI to Reka UI**
|
||||
|
||||
With Reka UI at its core, Nuxt UI v3 delivers:
|
||||
|
||||
• Proper keyboard navigation across all interactive components
|
||||
|
||||
• ARIA attributes automatically handled for you
|
||||
|
||||
• Focus management that just works
|
||||
|
||||
• Screen reader friendly components out of the box
|
||||
|
||||
This means you can build applications that work for everyone without becoming an accessibility expert.
|
||||
|
||||
### **Tailwind CSS v4 Integration**
|
||||
|
||||
The integration with Tailwind CSS v4 brings huge performance improvements:
|
||||
|
||||
• **5x faster runtime** with optimized component rendering
|
||||
|
||||
• **100x faster build times** thanks to the new CSS-first engine
|
||||
|
||||
• Smaller bundle sizes with more efficient styling
|
||||
|
||||
Your applications will feel snappier, build quicker, and load faster for your users.
|
||||
|
||||
## 🎨 A Brand New Design System
|
||||
|
||||
```html
|
||||
<!-- Before: Inconsistent color usage with duplicate dark mode classes -->
|
||||
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<h2 class="text-gray-900 dark:text-white text-xl mb-2">User Profile</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">Account settings and preferences</p>
|
||||
<button class="bg-blue-500 text-white px-3 py-1 rounded mt-2">Edit Profile</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- After: Semantic design tokens with automatic dark mode support -->
|
||||
<div class="bg-muted p-4 rounded-lg">
|
||||
<h2 class="text-highlighted text-xl mb-2">User Profile</h2>
|
||||
<p class="text-muted">Account settings and preferences</p>
|
||||
<UButton color="primary" size="sm" class="mt-2">Edit Profile</UButton>
|
||||
</div>
|
||||
```
|
||||
|
||||
Our new color system includes 7 semantic color aliases:
|
||||
|
||||
| Color | Default | Description |
|
||||
|-----------------------------------|----------|--------------------------------------------------|
|
||||
| :code[primary]{.text-primary} | `blue` | Primary color to represent the brand.
|
||||
| :code[secondary]{.text-secondary} | `blue` | Secondary color to complement the primary color.
|
||||
| :code[success]{.text-success} | `green` | Used for success states.
|
||||
| :code[info]{.text-info} | `blue` | Used for informational states.
|
||||
| :code[warning]{.text-warning} | `yellow` | Used for warning states.
|
||||
| :code[error]{.text-error} | `red` | Used for form error validation states. |
|
||||
| `neutral` | `slate` | Neutral color for backgrounds, text, etc. |
|
||||
|
||||
This approach makes your codebase more maintainable and your UI more consistent—especially when working in teams. With these semantic tokens, light and dark mode transitions become effortless, as the system automatically handles the appropriate color values for each theme without requiring duplicate class definitions.
|
||||
|
||||
## 💚 Complete Vue Compatibility
|
||||
|
||||
We're really happy to expand the scope of Nuxt UI beyond the Nuxt framework. With v3, both Nuxt UI and Nuxt UI Pro now work seamlessly in any Vue project, this means you can:
|
||||
|
||||
• Use the same components across all your Vue projects
|
||||
|
||||
• Benefit from Nuxt UI's theming system in any Vue application
|
||||
|
||||
• Enjoy auto-imports and TypeScript support outside of Nuxt
|
||||
|
||||
• Leverage both basic components and advanced Pro components in any Vue project
|
||||
|
||||
```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()
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## 📦 Components for Every Need
|
||||
|
||||
With 54 core components, 50 Pro components, and 42 Prose components, Nuxt UI v3 provides solutions for virtually any UI challenge:
|
||||
|
||||
• **Data Display**: Tables, charts, and visualizations that adapt to your data
|
||||
|
||||
• **Navigation**: Menus, tabs, and breadcrumbs that guide users intuitively
|
||||
|
||||
• **Feedback**: Toasts, alerts, and modals that communicate clearly
|
||||
|
||||
• **Forms**: Inputs, selectors, and validation that simplify data collection
|
||||
|
||||
• **Layout**: Grids, containers, and responsive systems that organize content beautifully
|
||||
|
||||
Each component is designed to be both beautiful out of the box and deeply customizable when needed.
|
||||
|
||||
## 🔷 Improved TypeScript Integration
|
||||
|
||||
We've completely revamped our TypeScript integration, with features that make you more productive:
|
||||
|
||||
- Complete type safety with helpful autocompletion
|
||||
- Generic-based components for flexible APIs
|
||||
- Type-safe theming through a clear, consistent API
|
||||
|
||||
```ts
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
// Your IDE will show all available options
|
||||
slots: {
|
||||
base: 'font-bold rounded-lg'
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
color: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## ⬆️ Upgrading to v3
|
||||
|
||||
We've prepared a comprehensive [migration](https://ui.nuxt.com/getting-started/migration) guide to help you upgrade from v2 to v3. While there are breaking changes due to our complete overhaul, we've worked hard to make the transition as smooth as possible.
|
||||
|
||||
## 🎯 Getting Started
|
||||
|
||||
Whether you're starting a new project or upgrading an existing one, getting started with Nuxt UI v3 is easy:
|
||||
|
||||
```bash
|
||||
# Create a new Nuxt project with Nuxt UI
|
||||
npx nuxi@latest init my-app -t ui
|
||||
```
|
||||
|
||||
::code-group{sync="pm"}
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui@latest
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @nuxt/ui@latest
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @nuxt/ui@latest
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui@latest
|
||||
```
|
||||
::
|
||||
|
||||
::warning
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss` in your project's root directory.
|
||||
::
|
||||
|
||||
Visit our [documentation](https://ui.nuxt.com/getting-started) to explore all the components and features available in Nuxt UI v3.
|
||||
|
||||
## 🙏 Thank You
|
||||
|
||||
This release represents thousands of hours of work from our team and the community. We'd like to thank everyone who contributed to making Nuxt UI v3 a reality.
|
||||
|
||||
We're excited to see what you'll build with Nuxt UI v3!
|
||||
@@ -1,198 +0,0 @@
|
||||
---
|
||||
title: Nuxt UI
|
||||
description: Nuxt UI v3 is out! After 1500+ commits, this major redesign brings
|
||||
improved accessibility, Tailwind CSS v4 support, and full Vue compatibility
|
||||
navigation: false
|
||||
image: /assets/blog/nuxt-ui-v3.png
|
||||
minRead: 7
|
||||
authors:
|
||||
- name: Benjamin Canac
|
||||
avatar:
|
||||
src: https://github.com/benjamincanac.png
|
||||
to: https://x.com/benjamincanac
|
||||
- name: Sébastien Chopin
|
||||
avatar:
|
||||
src: https://github.com/atinux.png
|
||||
to: https://x.com/atinux
|
||||
- name: Hugo Richard
|
||||
avatar:
|
||||
src: https://github.com/hugorcd.png
|
||||
to: https://x.com/hugorcd__
|
||||
date: 2025-03-12T09:00:00.000Z
|
||||
category: improvement
|
||||
---
|
||||
|
||||
We are thrilled to announce the release of Nuxt UI v3, a complete redesign of our UI library that brings significant improvements in accessibility, performance, and developer experience. This major update represents over 1500 commits of hard work, collaboration, and innovation from our team and the community.
|
||||
|
||||
## 🚀 Reimagined from the Ground Up
|
||||
|
||||
Nuxt UI v3 represents a major leap forward in our journey to provide the most comprehensive UI solution for Vue and Nuxt developers. This version has been rebuilt from the ground up with modern technologies and best practices in mind.
|
||||
|
||||
### **From HeadlessUI to Reka UI**
|
||||
|
||||
With Reka UI at its core, Nuxt UI v3 delivers:
|
||||
|
||||
• Proper keyboard navigation across all interactive components
|
||||
|
||||
• ARIA attributes automatically handled for you
|
||||
|
||||
• Focus management that just works
|
||||
|
||||
• Screen reader friendly components out of the box
|
||||
|
||||
This means you can build applications that work for everyone without becoming an accessibility expert.
|
||||
|
||||
### **Tailwind CSS v4 Integration**
|
||||
|
||||
The integration with Tailwind CSS v4 brings huge performance improvements:
|
||||
|
||||
• **5x faster runtime** with optimized component rendering
|
||||
|
||||
• **100x faster build times** thanks to the new CSS-first engine
|
||||
|
||||
• Smaller bundle sizes with more efficient styling
|
||||
|
||||
Your applications will feel snappier, build quicker, and load faster for your users.
|
||||
|
||||
## 🎨 A Brand New Design System
|
||||
|
||||
```html
|
||||
<!-- Before: Inconsistent color usage with duplicate dark mode classes -->
|
||||
<div class="bg-gray-100 dark:bg-gray-800 p-4 rounded-lg">
|
||||
<h2 class="text-gray-900 dark:text-white text-xl mb-2">User Profile</h2>
|
||||
<p class="text-gray-600 dark:text-gray-300">Account settings and preferences</p>
|
||||
<button class="bg-blue-500 text-white px-3 py-1 rounded mt-2">Edit Profile</button>
|
||||
</div>
|
||||
```
|
||||
|
||||
```html
|
||||
<!-- After: Semantic design tokens with automatic dark mode support -->
|
||||
<div class="bg-muted p-4 rounded-lg">
|
||||
<h2 class="text-highlighted text-xl mb-2">User Profile</h2>
|
||||
<p class="text-muted">Account settings and preferences</p>
|
||||
<UButton color="primary" size="sm" class="mt-2">Edit Profile</UButton>
|
||||
</div>
|
||||
```
|
||||
|
||||
Our new color system includes 7 semantic color aliases:
|
||||
|
||||
| Color | Default | Description |
|
||||
|-----------------------------------|----------|--------------------------------------------------|
|
||||
| :code[primary]{.text-primary} | `blue` | Primary color to represent the brand.
|
||||
| :code[secondary]{.text-secondary} | `blue` | Secondary color to complement the primary color.
|
||||
| :code[success]{.text-success} | `green` | Used for success states.
|
||||
| :code[info]{.text-info} | `blue` | Used for informational states.
|
||||
| :code[warning]{.text-warning} | `yellow` | Used for warning states.
|
||||
| :code[error]{.text-error} | `red` | Used for form error validation states. |
|
||||
| `neutral` | `slate` | Neutral color for backgrounds, text, etc. |
|
||||
|
||||
This approach makes your codebase more maintainable and your UI more consistent—especially when working in teams. With these semantic tokens, light and dark mode transitions become effortless, as the system automatically handles the appropriate color values for each theme without requiring duplicate class definitions.
|
||||
|
||||
## 💚 Complete Vue Compatibility
|
||||
|
||||
We're really happy to expand the scope of Nuxt UI beyond the Nuxt framework. With v3, both Nuxt UI and Nuxt UI Pro now work seamlessly in any Vue project, this means you can:
|
||||
|
||||
• Use the same components across all your Vue projects
|
||||
|
||||
• Benefit from Nuxt UI's theming system in any Vue application
|
||||
|
||||
• Enjoy auto-imports and TypeScript support outside of Nuxt
|
||||
|
||||
• Leverage both basic components and advanced Pro components in any Vue project
|
||||
|
||||
```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()
|
||||
]
|
||||
})
|
||||
```
|
||||
|
||||
## 📦 Components for Every Need
|
||||
|
||||
With 54 core components, 50 Pro components, and 42 Prose components, Nuxt UI v3 provides solutions for virtually any UI challenge:
|
||||
|
||||
• **Data Display**: Tables, charts, and visualizations that adapt to your data
|
||||
|
||||
• **Navigation**: Menus, tabs, and breadcrumbs that guide users intuitively
|
||||
|
||||
• **Feedback**: Toasts, alerts, and modals that communicate clearly
|
||||
|
||||
• **Forms**: Inputs, selectors, and validation that simplify data collection
|
||||
|
||||
• **Layout**: Grids, containers, and responsive systems that organize content beautifully
|
||||
|
||||
Each component is designed to be both beautiful out of the box and deeply customizable when needed.
|
||||
|
||||
## 🔷 Improved TypeScript Integration
|
||||
|
||||
We've completely revamped our TypeScript integration, with features that make you more productive:
|
||||
|
||||
- Complete type safety with helpful autocompletion
|
||||
- Generic-based components for flexible APIs
|
||||
- Type-safe theming through a clear, consistent API
|
||||
|
||||
```ts
|
||||
export default defineAppConfig({
|
||||
ui: {
|
||||
button: {
|
||||
// Your IDE will show all available options
|
||||
slots: {
|
||||
base: 'font-bold rounded-lg'
|
||||
},
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
color: 'error'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
## ⬆️ Upgrading to v3
|
||||
|
||||
We've prepared a comprehensive [migration](https://ui.nuxt.com/getting-started/migration) guide to help you upgrade from v2 to v3. While there are breaking changes due to our complete overhaul, we've worked hard to make the transition as smooth as possible.
|
||||
|
||||
## 🎯 Getting Started
|
||||
|
||||
Whether you're starting a new project or upgrading an existing one, getting started with Nuxt UI v3 is easy:
|
||||
|
||||
```bash
|
||||
# Create a new Nuxt project with Nuxt UI
|
||||
npx nuxi@latest init my-app -t ui
|
||||
```
|
||||
|
||||
::code-group{sync="pm"}
|
||||
```bash [pnpm]
|
||||
pnpm add @nuxt/ui@latest
|
||||
```
|
||||
|
||||
```bash [yarn]
|
||||
yarn add @nuxt/ui@latest
|
||||
```
|
||||
|
||||
```bash [npm]
|
||||
npm install @nuxt/ui@latest
|
||||
```
|
||||
|
||||
```bash [bun]
|
||||
bun add @nuxt/ui@latest
|
||||
```
|
||||
::
|
||||
|
||||
::warning
|
||||
If you're using **pnpm**, ensure that you either set [`shamefully-hoist=true`](https://pnpm.io/npmrc#shamefully-hoist) in your `.npmrc` file or install `tailwindcss` in your project's root directory.
|
||||
::
|
||||
|
||||
Visit our [documentation](https://ui.nuxt.com/getting-started) to explore all the components and features available in Nuxt UI v3.
|
||||
|
||||
## 🙏 Thank You
|
||||
|
||||
This release represents thousands of hours of work from our team and the community. We'd like to thank everyone who contributed to making Nuxt UI v3 a reality.
|
||||
|
||||
We're excited to see what you'll build with Nuxt UI v3!
|
||||
@@ -11,40 +11,40 @@
|
||||
"dependencies": {
|
||||
"@ai-sdk/vue": "^1.2.12",
|
||||
"@iconify-json/logos": "^1.2.4",
|
||||
"@iconify-json/lucide": "^1.2.56",
|
||||
"@iconify-json/simple-icons": "^1.2.42",
|
||||
"@iconify-json/lucide": "^1.2.51",
|
||||
"@iconify-json/simple-icons": "^1.2.39",
|
||||
"@iconify-json/vscode-icons": "^1.2.23",
|
||||
"@nuxt/content": "^3.6.3",
|
||||
"@nuxt/content": "^3.6.1",
|
||||
"@nuxt/image": "^1.10.0",
|
||||
"@nuxt/ui": "workspace:*",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@22fdc5e",
|
||||
"@nuxt/ui-pro": "https://pkg.pr.new/@nuxt/ui-pro@55e248c",
|
||||
"@nuxthub/core": "^0.9.0",
|
||||
"@nuxtjs/plausible": "^1.2.0",
|
||||
"@octokit/rest": "^22.0.0",
|
||||
"@rollup/plugin-yaml": "^4.1.2",
|
||||
"@vueuse/integrations": "^13.5.0",
|
||||
"@vueuse/nuxt": "^13.5.0",
|
||||
"@vueuse/integrations": "^13.4.0",
|
||||
"@vueuse/nuxt": "^13.4.0",
|
||||
"ai": "^4.3.16",
|
||||
"better-sqlite3": "^12.2.0",
|
||||
"better-sqlite3": "^12.0.0",
|
||||
"capture-website": "^4.2.0",
|
||||
"joi": "^17.13.3",
|
||||
"maska": "^3.2.0",
|
||||
"motion-v": "^1.5.0",
|
||||
"nuxt": "^3.17.6",
|
||||
"nuxt-component-meta": "^0.12.1",
|
||||
"maska": "^3.1.1",
|
||||
"motion-v": "^1.3.0",
|
||||
"nuxt": "^3.17.5",
|
||||
"nuxt-component-meta": "^0.11.0",
|
||||
"nuxt-llms": "^0.1.3",
|
||||
"nuxt-og-image": "^5.1.9",
|
||||
"prettier": "^3.6.2",
|
||||
"nuxt-og-image": "^5.1.7",
|
||||
"prettier": "^3.6.0",
|
||||
"shiki-transformer-color-highlight": "^1.0.0",
|
||||
"sortablejs": "^1.15.6",
|
||||
"superstruct": "^2.0.2",
|
||||
"ufo": "^1.6.1",
|
||||
"valibot": "^1.1.0",
|
||||
"workers-ai-provider": "^0.7.1",
|
||||
"workers-ai-provider": "^0.7.0",
|
||||
"yup": "^1.6.1",
|
||||
"zod": "^3.25.75"
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.23.0"
|
||||
"wrangler": "^4.20.5"
|
||||
}
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 515 KiB |
44
package.json
44
package.json
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"name": "@nuxt/ui",
|
||||
"description": "A UI Library for Modern Web Apps, powered by Vue & Tailwind CSS.",
|
||||
"version": "3.2.0",
|
||||
"packageManager": "pnpm@10.12.4",
|
||||
"version": "3.1.3",
|
||||
"packageManager": "pnpm@10.12.2",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/nuxt/ui.git"
|
||||
@@ -98,9 +98,9 @@
|
||||
"prepack": "pnpm build",
|
||||
"dev": "nuxt dev playground --uiDev",
|
||||
"dev:build": "nuxt build playground",
|
||||
"dev:vue": "pnpm --filter playground-vue dev -- --uiDev",
|
||||
"dev:vue:build": "pnpm --filter playground-vue build",
|
||||
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare docs && pnpm dev:vue:build",
|
||||
"dev:vue": "vite playground-vue -- --uiDev",
|
||||
"dev:vue:build": "vite build playground-vue",
|
||||
"dev:prepare": "nuxt-module-build build --stub && nuxt-module-build prepare && nuxt prepare playground && nuxt prepare docs && vite build playground-vue",
|
||||
"docs": "nuxt dev docs --uiDev",
|
||||
"docs:build": "nuxt build docs",
|
||||
"lint": "eslint .",
|
||||
@@ -115,17 +115,17 @@
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@internationalized/number": "^3.6.3",
|
||||
"@nuxt/fonts": "^0.11.4",
|
||||
"@nuxt/icon": "^1.15.0",
|
||||
"@nuxt/kit": "^3.17.6",
|
||||
"@nuxt/schema": "^3.17.6",
|
||||
"@nuxt/icon": "^1.14.0",
|
||||
"@nuxt/kit": "^3.17.5",
|
||||
"@nuxt/schema": "^3.17.5",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tailwindcss/vite": "^4.1.11",
|
||||
"@tailwindcss/postcss": "^4.1.10",
|
||||
"@tailwindcss/vite": "^4.1.10",
|
||||
"@tanstack/vue-table": "^8.21.3",
|
||||
"@unhead/vue": "^2.0.12",
|
||||
"@vueuse/core": "^13.5.0",
|
||||
"@vueuse/integrations": "^13.5.0",
|
||||
"@unhead/vue": "^2.0.10",
|
||||
"@vueuse/core": "^13.4.0",
|
||||
"@vueuse/integrations": "^13.4.0",
|
||||
"colortranslator": "^5.0.0",
|
||||
"consola": "^3.4.2",
|
||||
"defu": "^6.1.4",
|
||||
@@ -143,31 +143,31 @@
|
||||
"mlly": "^1.7.4",
|
||||
"ohash": "^2.0.11",
|
||||
"pathe": "^2.0.3",
|
||||
"reka-ui": "2.3.2",
|
||||
"reka-ui": "2.3.1",
|
||||
"scule": "^1.3.0",
|
||||
"tailwind-variants": "^1.0.0",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"tailwindcss": "^4.1.10",
|
||||
"tinyglobby": "^0.2.14",
|
||||
"unplugin": "^2.3.5",
|
||||
"unplugin-auto-import": "^19.3.0",
|
||||
"unplugin-vue-components": "^28.8.0",
|
||||
"unplugin-vue-components": "^28.7.0",
|
||||
"vaul-vue": "0.4.1",
|
||||
"vue-component-type-helpers": "^3.0.1"
|
||||
"vue-component-type-helpers": "^2.2.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@nuxt/eslint-config": "^1.5.2",
|
||||
"@nuxt/eslint-config": "^1.4.1",
|
||||
"@nuxt/module-builder": "^1.0.1",
|
||||
"@nuxt/test-utils": "^3.19.2",
|
||||
"@nuxt/test-utils": "^3.19.1",
|
||||
"@release-it/conventional-changelog": "^10.0.1",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"embla-carousel": "^8.6.0",
|
||||
"eslint": "^9.30.1",
|
||||
"eslint": "^9.29.0",
|
||||
"happy-dom": "^18.0.1",
|
||||
"nuxt": "^3.17.6",
|
||||
"nuxt": "^3.17.5",
|
||||
"release-it": "^19.0.3",
|
||||
"vitest": "^3.2.4",
|
||||
"vitest-environment-nuxt": "^1.0.1",
|
||||
"vue-tsc": "^3.0.1"
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@inertiajs/vue3": "^2.0.7",
|
||||
|
||||
@@ -13,12 +13,12 @@
|
||||
"@nuxt/ui": "workspace:*",
|
||||
"vue": "^3.5.17",
|
||||
"vue-router": "^4.5.1",
|
||||
"zod": "^3.25.75"
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"typescript": "^5.8.3",
|
||||
"vite": "^6.3.5",
|
||||
"vue-tsc": "^3.0.1"
|
||||
"vue-tsc": "^2.2.10"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,27 +166,7 @@ defineShortcuts({
|
||||
multiple
|
||||
class="sm:max-h-80"
|
||||
@update:model-value="onSelect"
|
||||
>
|
||||
<template #footer>
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<UIcon name="i-simple-icons-nuxtdotjs" class="size-5 text-dimmed ml-1" />
|
||||
<div class="flex items-center gap-1">
|
||||
<UButton color="neutral" variant="ghost" label="Open Command" class="text-dimmed" size="xs">
|
||||
<template #trailing>
|
||||
<UKbd value="enter" />
|
||||
</template>
|
||||
</UButton>
|
||||
<USeparator orientation="vertical" class="h-4" />
|
||||
<UButton color="neutral" variant="ghost" label="Actions" class="text-dimmed" size="xs">
|
||||
<template #trailing>
|
||||
<UKbd value="meta" />
|
||||
<UKbd value="k" />
|
||||
</template>
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</UCommandPalette>
|
||||
/>
|
||||
</DefineTemplate>
|
||||
|
||||
<div class="flex-1 flex flex-col gap-12 w-full max-w-lg">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { h, resolveComponent } from 'vue'
|
||||
import { upperFirst } from 'scule'
|
||||
import type { TableColumn, TableRow } from '@nuxt/ui'
|
||||
import { getPaginationRowModel } from '@tanstack/vue-table'
|
||||
import { useClipboard, refDebounced } from '@vueuse/core'
|
||||
import { useClipboard } from '@vueuse/core'
|
||||
|
||||
const UButton = resolveComponent('UButton')
|
||||
const UCheckbox = resolveComponent('UCheckbox')
|
||||
@@ -147,35 +147,6 @@ const data = ref<Payment[]>([{
|
||||
|
||||
const currentID = ref(4601)
|
||||
|
||||
function getRowItems(row: TableRow<Payment>) {
|
||||
return [{
|
||||
type: 'label' as const,
|
||||
label: 'Actions'
|
||||
}, {
|
||||
label: 'Copy payment ID',
|
||||
onSelect() {
|
||||
copy(row.original.id)
|
||||
|
||||
toast.add({
|
||||
title: 'Payment ID copied to clipboard!',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check'
|
||||
})
|
||||
}
|
||||
}, {
|
||||
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
|
||||
onSelect() {
|
||||
row.toggleExpanded()
|
||||
}
|
||||
}, {
|
||||
type: 'separator' as const
|
||||
}, {
|
||||
label: 'View customer'
|
||||
}, {
|
||||
label: 'View payment details'
|
||||
}]
|
||||
}
|
||||
|
||||
const columns: TableColumn<Payment>[] = [{
|
||||
id: 'select',
|
||||
header: ({ table }) => h(UCheckbox, {
|
||||
@@ -242,16 +213,6 @@ const columns: TableColumn<Payment>[] = [{
|
||||
}, {
|
||||
accessorKey: 'amount',
|
||||
header: () => h('div', { class: 'text-right' }, 'Amount'),
|
||||
footer: ({ column }) => {
|
||||
const total = column.getFacetedRowModel().rows.reduce((acc: number, row: TableRow<Payment>) => acc + Number.parseFloat(row.getValue('amount')), 0)
|
||||
|
||||
const formatted = new Intl.NumberFormat('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'EUR'
|
||||
}).format(total)
|
||||
|
||||
return h('div', { class: 'text-right font-medium' }, `Total: ${formatted}`)
|
||||
},
|
||||
cell: ({ row }) => {
|
||||
const amount = Number.parseFloat(row.getValue('amount'))
|
||||
|
||||
@@ -266,11 +227,38 @@ const columns: TableColumn<Payment>[] = [{
|
||||
id: 'actions',
|
||||
enableHiding: false,
|
||||
cell: ({ row }) => {
|
||||
const items = [{
|
||||
type: 'label',
|
||||
label: 'Actions'
|
||||
}, {
|
||||
label: 'Copy payment ID',
|
||||
onSelect() {
|
||||
copy(row.original.id)
|
||||
|
||||
toast.add({
|
||||
title: 'Payment ID copied to clipboard!',
|
||||
color: 'success',
|
||||
icon: 'i-lucide-circle-check'
|
||||
})
|
||||
}
|
||||
}, {
|
||||
label: row.getIsExpanded() ? 'Collapse' : 'Expand',
|
||||
onSelect() {
|
||||
row.toggleExpanded()
|
||||
}
|
||||
}, {
|
||||
type: 'separator'
|
||||
}, {
|
||||
label: 'View customer'
|
||||
}, {
|
||||
label: 'View payment details'
|
||||
}]
|
||||
|
||||
return h('div', { class: 'text-right' }, h(UDropdownMenu, {
|
||||
'content': {
|
||||
align: 'end'
|
||||
},
|
||||
'items': getRowItems(row),
|
||||
items,
|
||||
'aria-label': 'Actions dropdown'
|
||||
}, () => h(UButton, {
|
||||
'icon': 'i-lucide-ellipsis-vertical',
|
||||
@@ -308,41 +296,8 @@ function randomize() {
|
||||
data.value = data.value.sort(() => Math.random() - 0.5)
|
||||
}
|
||||
|
||||
const rowSelection = ref<Record<string, boolean>>({})
|
||||
|
||||
function onSelect(row: TableRow<Payment>) {
|
||||
row.toggleSelected(!row.getIsSelected())
|
||||
}
|
||||
|
||||
const contextmenuRow = ref<TableRow<Payment> | null>(null)
|
||||
const contextmenuItems = computed(() => contextmenuRow.value ? getRowItems(contextmenuRow.value) : [])
|
||||
|
||||
function onContextmenu(e: Event, row: TableRow<Payment>) {
|
||||
contextmenuRow.value = row
|
||||
}
|
||||
|
||||
const popoverOpen = ref(false)
|
||||
const popoverOpenDebounced = refDebounced(popoverOpen, 1)
|
||||
const popoverAnchor = ref({ x: 0, y: 0 })
|
||||
const popoverRow = ref<TableRow<Payment> | null>(null)
|
||||
|
||||
const reference = computed(() => ({
|
||||
getBoundingClientRect: () =>
|
||||
({
|
||||
width: 0,
|
||||
height: 0,
|
||||
left: popoverAnchor.value.x,
|
||||
right: popoverAnchor.value.x,
|
||||
top: popoverAnchor.value.y,
|
||||
bottom: popoverAnchor.value.y,
|
||||
...popoverAnchor.value
|
||||
} as DOMRect)
|
||||
}))
|
||||
|
||||
function onHover(_e: Event, row: TableRow<Payment> | null) {
|
||||
popoverRow.value = row
|
||||
|
||||
popoverOpen.value = !!row
|
||||
console.log(row)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
@@ -389,44 +344,27 @@ onMounted(() => {
|
||||
</UDropdownMenu>
|
||||
</div>
|
||||
|
||||
<UContextMenu :items="contextmenuItems">
|
||||
<UTable
|
||||
ref="table"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:column-pinning="columnPinning"
|
||||
:row-selection="rowSelection"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:pagination-options="{
|
||||
getPaginationRowModel: getPaginationRowModel()
|
||||
}"
|
||||
:ui="{
|
||||
tr: 'divide-x divide-default'
|
||||
}"
|
||||
sticky
|
||||
class="border border-accented rounded-sm"
|
||||
@select="onSelect"
|
||||
@contextmenu="onContextmenu"
|
||||
@pointermove="(ev: PointerEvent) => {
|
||||
popoverAnchor.x = ev.clientX
|
||||
popoverAnchor.y = ev.clientY
|
||||
}"
|
||||
@hover="onHover"
|
||||
>
|
||||
<template #expanded="{ row }">
|
||||
<pre>{{ row.original }}</pre>
|
||||
</template>
|
||||
</UTable>
|
||||
</UContextMenu>
|
||||
|
||||
<UPopover :content="{ side: 'top', sideOffset: 16, updatePositionStrategy: 'always' }" :open="popoverOpenDebounced" :reference="reference">
|
||||
<template #content>
|
||||
<div class="p-4">
|
||||
{{ popoverRow?.original?.id }}
|
||||
</div>
|
||||
<UTable
|
||||
ref="table"
|
||||
:data="data"
|
||||
:columns="columns"
|
||||
:column-pinning="columnPinning"
|
||||
:loading="loading"
|
||||
:pagination="pagination"
|
||||
:pagination-options="{
|
||||
getPaginationRowModel: getPaginationRowModel()
|
||||
}"
|
||||
:ui="{
|
||||
tr: 'divide-x divide-default'
|
||||
}"
|
||||
sticky
|
||||
class="border border-accented rounded-sm"
|
||||
@select="onSelect"
|
||||
>
|
||||
<template #expanded="{ row }">
|
||||
<pre>{{ row.original }}</pre>
|
||||
</template>
|
||||
</UPopover>
|
||||
</UTable>
|
||||
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-sm text-muted">
|
||||
|
||||
@@ -9,17 +9,17 @@
|
||||
"typecheck": "nuxt typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@iconify-json/lucide": "^1.2.56",
|
||||
"@iconify-json/simple-icons": "^1.2.42",
|
||||
"@iconify-json/lucide": "^1.2.51",
|
||||
"@iconify-json/simple-icons": "^1.2.39",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@nuxt/ui": "workspace:*",
|
||||
"@nuxthub/core": "^0.9.0",
|
||||
"nuxt": "^3.17.6",
|
||||
"zod": "^3.25.75"
|
||||
"nuxt": "^3.17.5",
|
||||
"zod": "^3.25.67"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.8.3",
|
||||
"vue-tsc": "^3.0.1"
|
||||
"vue-tsc": "^2.2.10"
|
||||
},
|
||||
"resolutions": {
|
||||
"unimport": "4.1.1"
|
||||
|
||||
3331
pnpm-lock.yaml
generated
3331
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -22,12 +22,6 @@
|
||||
"reka-ui",
|
||||
"vaul-vue"
|
||||
]
|
||||
}, {
|
||||
"groupName": "vue-tsc",
|
||||
"matchPackageNames": [
|
||||
"vue-tsc",
|
||||
"vue-component-type-helpers"
|
||||
]
|
||||
}, {
|
||||
"matchDepTypes": ["peerDependencies"],
|
||||
"enabled": false
|
||||
|
||||
@@ -11,7 +11,7 @@ import { defu } from 'defu'
|
||||
/**
|
||||
* This plugin adds all the Nuxt UI components as auto-imports.
|
||||
*/
|
||||
export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']>, extraRuntimeDir?: string }, meta: UnpluginContextMeta) {
|
||||
export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix: NonNullable<NuxtUIOptions['prefix']> }, meta: UnpluginContextMeta) {
|
||||
const components = globSync('**/*.vue', { cwd: join(runtimeDir, 'components') })
|
||||
const componentNames = new Set(components.map(c => `${options.prefix}${c.replace(/\.vue$/, '')}`))
|
||||
|
||||
@@ -50,15 +50,13 @@ export default function ComponentImportPlugin(options: NuxtUIOptions & { prefix:
|
||||
name: 'nuxt:ui:components',
|
||||
enforce: 'pre',
|
||||
resolveId(id, importer) {
|
||||
if (!importer) {
|
||||
return
|
||||
}
|
||||
if (!normalize(importer).includes(runtimeDir) && (!options.extraRuntimeDir || !normalize(importer).includes(options.extraRuntimeDir))) {
|
||||
// only apply to runtime nuxt ui components
|
||||
if (!importer || !normalize(importer).includes(runtimeDir)) {
|
||||
return
|
||||
}
|
||||
|
||||
// only apply to relative imports or nuxt ui runtime components
|
||||
if (!RELATIVE_IMPORT_RE.test(id) && !id.startsWith('@nuxt/ui/components/')) {
|
||||
// only apply to relative imports
|
||||
if (!RELATIVE_IMPORT_RE.test(id)) {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -3,8 +3,7 @@ import { normalize } from 'pathe'
|
||||
import { resolvePathSync } from 'mlly'
|
||||
import MagicString from 'magic-string'
|
||||
|
||||
import { runtimeDir } from '../unplugin'
|
||||
import type { NuxtUIOptions } from '../unplugin'
|
||||
import { runtimeDir, type NuxtUIOptions } from '../unplugin'
|
||||
|
||||
/**
|
||||
* This plugin normalises Nuxt environment (#imports) and `import.meta.client` within the Nuxt UI components.
|
||||
|
||||
@@ -4,8 +4,7 @@ import { genSafeVariableName } from 'knitwork'
|
||||
import MagicString from 'magic-string'
|
||||
import { resolvePathSync } from 'mlly'
|
||||
|
||||
import { runtimeDir } from '../unplugin'
|
||||
import type { NuxtUIOptions } from '../unplugin'
|
||||
import { runtimeDir, type NuxtUIOptions } from '../unplugin'
|
||||
|
||||
import type { UnpluginOptions } from 'unplugin'
|
||||
|
||||
|
||||
@@ -42,15 +42,14 @@ export interface ButtonSlots {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, inject } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { type Ref, computed, ref, inject } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { useForwardProps } from 'reka-ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { formLoadingInjectionKey } from '../composables/useFormField'
|
||||
import { omit, mergeClasses } from '../utils'
|
||||
import { omit } from '../utils'
|
||||
import { tv } from '../utils/tv'
|
||||
import { pickLinkProps } from '../utils/link'
|
||||
import UIcon from './Icon.vue'
|
||||
@@ -58,7 +57,11 @@ import UAvatar from './Avatar.vue'
|
||||
import ULink from './Link.vue'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
|
||||
const props = defineProps<ButtonProps>()
|
||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||
active: undefined,
|
||||
activeClass: '',
|
||||
inactiveClass: ''
|
||||
})
|
||||
const slots = defineSlots<ButtonSlots>()
|
||||
|
||||
const appConfig = useAppConfig() as Button['AppConfig']
|
||||
@@ -93,10 +96,10 @@ const ui = computed(() => tv({
|
||||
variants: {
|
||||
active: {
|
||||
true: {
|
||||
base: mergeClasses(appConfig.ui?.button?.variants?.active?.true?.base, props.activeClass)
|
||||
base: props.activeClass
|
||||
},
|
||||
false: {
|
||||
base: mergeClasses(appConfig.ui?.button?.variants?.active?.false?.base, props.inactiveClass)
|
||||
base: props.inactiveClass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@ export interface CarouselEmits {
|
||||
import { computed, ref, watch, onMounted } from 'vue'
|
||||
import useEmblaCarousel from 'embla-carousel-vue'
|
||||
import { Primitive, useForwardProps } from 'reka-ui'
|
||||
import { reactivePick } from '@vueuse/core'
|
||||
import { reactivePick, computedAsync } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useLocale } from '../composables/useLocale'
|
||||
import { tv } from '../utils/tv'
|
||||
@@ -175,45 +175,41 @@ const options = computed<EmblaOptionsType>(() => ({
|
||||
direction: dir.value === 'rtl' ? 'rtl' : 'ltr'
|
||||
}))
|
||||
|
||||
const plugins = ref<EmblaPluginType[]>([])
|
||||
|
||||
async function loadPlugins() {
|
||||
const emblaPlugins: EmblaPluginType[] = []
|
||||
const plugins = computedAsync<EmblaPluginType[]>(async () => {
|
||||
const plugins = []
|
||||
|
||||
if (props.autoplay) {
|
||||
const AutoplayPlugin = await import('embla-carousel-autoplay').then(r => r.default)
|
||||
emblaPlugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
|
||||
plugins.push(AutoplayPlugin(typeof props.autoplay === 'boolean' ? {} : props.autoplay))
|
||||
}
|
||||
|
||||
if (props.autoScroll) {
|
||||
const AutoScrollPlugin = await import('embla-carousel-auto-scroll').then(r => r.default)
|
||||
emblaPlugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
|
||||
plugins.push(AutoScrollPlugin(typeof props.autoScroll === 'boolean' ? {} : props.autoScroll))
|
||||
}
|
||||
|
||||
if (props.autoHeight) {
|
||||
const AutoHeightPlugin = await import('embla-carousel-auto-height').then(r => r.default)
|
||||
emblaPlugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
|
||||
plugins.push(AutoHeightPlugin(typeof props.autoHeight === 'boolean' ? {} : props.autoHeight))
|
||||
}
|
||||
|
||||
if (props.classNames) {
|
||||
const ClassNamesPlugin = await import('embla-carousel-class-names').then(r => r.default)
|
||||
emblaPlugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
|
||||
plugins.push(ClassNamesPlugin(typeof props.classNames === 'boolean' ? {} : props.classNames))
|
||||
}
|
||||
|
||||
if (props.fade) {
|
||||
const FadePlugin = await import('embla-carousel-fade').then(r => r.default)
|
||||
emblaPlugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
|
||||
plugins.push(FadePlugin(typeof props.fade === 'boolean' ? {} : props.fade))
|
||||
}
|
||||
|
||||
if (props.wheelGestures) {
|
||||
const { WheelGesturesPlugin } = await import('embla-carousel-wheel-gestures')
|
||||
emblaPlugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
|
||||
plugins.push(WheelGesturesPlugin(typeof props.wheelGestures === 'boolean' ? {} : props.wheelGestures))
|
||||
}
|
||||
|
||||
plugins.value = emblaPlugins
|
||||
}
|
||||
|
||||
watch(() => [props.autoplay, props.autoScroll, props.autoHeight, props.classNames, props.fade, props.wheelGestures], loadPlugins, { immediate: true })
|
||||
return plugins
|
||||
})
|
||||
|
||||
const [emblaRef, emblaApi] = useEmblaCarousel(options.value, plugins.value)
|
||||
|
||||
@@ -339,7 +335,6 @@ defineExpose({
|
||||
:aria-label="t('carousel.goto', { slide: index + 1 })"
|
||||
:class="ui.dot({ class: props.ui?.dot, active: selectedIndex === index })"
|
||||
:data-state="selectedIndex === index ? 'active' : undefined"
|
||||
:aria-current="selectedIndex === index ? true : undefined"
|
||||
@click="scrollTo(index)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
@@ -147,7 +147,6 @@ type SlotProps<T> = (props: { item: T, index: number }) => any
|
||||
|
||||
export type CommandPaletteSlots<G extends CommandPaletteGroup<T> = CommandPaletteGroup<any>, T extends CommandPaletteItem = CommandPaletteItem> = {
|
||||
'empty'(props: { searchTerm?: string }): any
|
||||
'footer'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
||||
'back'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
||||
'close'(props: { ui: { [K in keyof Required<CommandPalette['slots']>]: (props?: Record<string, any>) => string } }): any
|
||||
'item': SlotProps<T>
|
||||
@@ -445,9 +444,5 @@ function onSelect(e: Event, item: T) {
|
||||
</slot>
|
||||
</div>
|
||||
</ListboxContent>
|
||||
|
||||
<div v-if="!!slots.footer" :class="ui.footer({ class: props.ui?.footer })">
|
||||
<slot name="footer" :ui="ui" />
|
||||
</div>
|
||||
</ListboxRoot>
|
||||
</template>
|
||||
|
||||
@@ -47,6 +47,7 @@ import ULink from './Link.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UIcon from './Icon.vue'
|
||||
import UKbd from './Kbd.vue'
|
||||
// eslint-disable-next-line import/no-self-import
|
||||
import UContextMenuContent from './ContextMenuContent.vue'
|
||||
|
||||
const props = defineProps<ContextMenuContentProps<T>>()
|
||||
|
||||
@@ -53,6 +53,7 @@ import ULink from './Link.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UIcon from './Icon.vue'
|
||||
import UKbd from './Kbd.vue'
|
||||
// eslint-disable-next-line import/no-self-import
|
||||
import UDropdownMenuContent from './DropdownMenuContent.vue'
|
||||
|
||||
const props = defineProps<DropdownMenuContentProps<T>>()
|
||||
|
||||
@@ -47,8 +47,7 @@ export interface FormFieldSlots {
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, ref, inject, provide, useId } from 'vue'
|
||||
import type { Ref } from 'vue'
|
||||
import { computed, ref, inject, provide, type Ref, useId } from 'vue'
|
||||
import { Primitive, Label } from 'reka-ui'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { formFieldInjectionKey, inputIdInjectionKey } from '../composables/useFormField'
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { AcceptableValue, ComponentConfig } from '../types/utils'
|
||||
|
||||
type Input = ComponentConfig<typeof theme, AppConfig, 'input'>
|
||||
|
||||
export interface InputProps<T extends AcceptableValue = AcceptableValue> extends UseComponentIconsProps {
|
||||
export interface InputProps extends UseComponentIconsProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -38,8 +38,6 @@ export interface InputProps<T extends AcceptableValue = AcceptableValue> extends
|
||||
disabled?: boolean
|
||||
/** Highlight the ring color like a focus state. */
|
||||
highlight?: boolean
|
||||
modelValue?: T
|
||||
defaultValue?: T
|
||||
modelModifiers?: {
|
||||
string?: boolean
|
||||
number?: boolean
|
||||
@@ -67,7 +65,6 @@ export interface InputSlots {
|
||||
<script setup lang="ts" generic="T extends AcceptableValue">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useButtonGroup } from '../composables/useButtonGroup'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
@@ -79,7 +76,7 @@ import UAvatar from './Avatar.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<InputProps<T>>(), {
|
||||
const props = withDefaults(defineProps<InputProps>(), {
|
||||
type: 'text',
|
||||
autocomplete: 'off',
|
||||
autofocusDelay: 0
|
||||
@@ -87,12 +84,13 @@ const props = withDefaults(defineProps<InputProps<T>>(), {
|
||||
const emits = defineEmits<InputEmits<T>>()
|
||||
const slots = defineSlots<InputSlots>()
|
||||
|
||||
const modelValue = useVModel<InputProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue })
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const [modelValue, modelModifiers] = defineModel<T>()
|
||||
|
||||
const appConfig = useAppConfig() as Input['AppConfig']
|
||||
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps<T>>(props, { deferInputValidation: true })
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps<T>>(props)
|
||||
const { emitFormBlur, emitFormInput, emitFormChange, size: formGroupSize, color, id, name, highlight, disabled, emitFormFocus, ariaAttrs } = useFormField<InputProps>(props, { deferInputValidation: true })
|
||||
const { orientation, size: buttonGroupSize } = useButtonGroup<InputProps>(props)
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
||||
|
||||
const inputSize = computed(() => buttonGroupSize.value || formGroupSize.value)
|
||||
@@ -113,15 +111,15 @@ const inputRef = ref<HTMLInputElement | null>(null)
|
||||
|
||||
// Custom function to handle the v-model properties
|
||||
function updateInput(value: string | null) {
|
||||
if (props.modelModifiers?.trim) {
|
||||
if (modelModifiers.trim) {
|
||||
value = value?.trim() ?? null
|
||||
}
|
||||
|
||||
if (props.modelModifiers?.number || props.type === 'number') {
|
||||
if (modelModifiers.number || props.type === 'number') {
|
||||
value = looseToNumber(value)
|
||||
}
|
||||
|
||||
if (props.modelModifiers?.nullify) {
|
||||
if (modelModifiers.nullify) {
|
||||
value ||= null
|
||||
}
|
||||
|
||||
@@ -130,7 +128,7 @@ function updateInput(value: string | null) {
|
||||
}
|
||||
|
||||
function onInput(event: Event) {
|
||||
if (!props.modelModifiers?.lazy) {
|
||||
if (!modelModifiers.lazy) {
|
||||
updateInput((event.target as HTMLInputElement).value)
|
||||
}
|
||||
}
|
||||
@@ -138,12 +136,12 @@ function onInput(event: Event) {
|
||||
function onChange(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
|
||||
if (props.modelModifiers?.lazy) {
|
||||
if (modelModifiers.lazy) {
|
||||
updateInput(value)
|
||||
}
|
||||
|
||||
// Update trimmed input so that it has same behavior as native input https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
||||
if (props.modelModifiers?.trim) {
|
||||
if (modelModifiers.trim) {
|
||||
(event.target as HTMLInputElement).value = value.trim()
|
||||
}
|
||||
|
||||
|
||||
@@ -18,8 +18,6 @@ export interface InputTagsProps<T extends InputTagItem = InputTagItem> extends P
|
||||
as?: any
|
||||
/** The placeholder text when the input is empty. */
|
||||
placeholder?: string
|
||||
/** The maximum number of character allowed. */
|
||||
maxLength?: number
|
||||
/**
|
||||
* @defaultValue 'primary'
|
||||
*/
|
||||
@@ -184,7 +182,6 @@ defineExpose({
|
||||
ref="inputRef"
|
||||
v-bind="{ ...$attrs, ...ariaAttrs }"
|
||||
:placeholder="placeholder"
|
||||
:max-length="maxLength"
|
||||
:class="ui.input({ class: props.ui?.input })"
|
||||
/>
|
||||
|
||||
|
||||
@@ -88,12 +88,11 @@ export interface LinkSlots {
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { defu } from 'defu'
|
||||
import { isEqual } from 'ohash/utils'
|
||||
import { useForwardProps } from 'reka-ui'
|
||||
import { defu } from 'defu'
|
||||
import { reactiveOmit } from '@vueuse/core'
|
||||
import { useRoute, useAppConfig } from '#imports'
|
||||
import { mergeClasses } from '../utils'
|
||||
import { tv } from '../utils/tv'
|
||||
import { isPartiallyEqual } from '../utils/link'
|
||||
import ULinkBase from './LinkBase.vue'
|
||||
@@ -104,7 +103,9 @@ const props = withDefaults(defineProps<LinkProps>(), {
|
||||
as: 'button',
|
||||
type: 'button',
|
||||
ariaCurrentValue: 'page',
|
||||
active: undefined
|
||||
active: undefined,
|
||||
activeClass: '',
|
||||
inactiveClass: ''
|
||||
})
|
||||
defineSlots<LinkSlots>()
|
||||
|
||||
@@ -118,8 +119,8 @@ const ui = computed(() => tv({
|
||||
...defu({
|
||||
variants: {
|
||||
active: {
|
||||
true: mergeClasses(appConfig.ui?.link?.variants?.active?.true, props.activeClass),
|
||||
false: mergeClasses(appConfig.ui?.link?.variants?.active?.false, props.inactiveClass)
|
||||
true: props.activeClass,
|
||||
false: props.inactiveClass
|
||||
}
|
||||
}
|
||||
}, appConfig.ui?.link || {})
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import type { NavigationMenuRootProps, NavigationMenuRootEmits, NavigationMenuContentProps, NavigationMenuContentEmits, AccordionRootProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/navigation-menu'
|
||||
import type { AvatarProps, BadgeProps, LinkProps, PopoverProps, TooltipProps } from '../types'
|
||||
import type { AvatarProps, BadgeProps, ChipProps, LinkProps, PopoverProps, TooltipProps } from '../types'
|
||||
import type { ArrayOrNested, DynamicSlots, MergeTypes, NestedItem, EmitsToProps, ComponentConfig } from '../types/utils'
|
||||
|
||||
type NavigationMenu = ComponentConfig<typeof theme, AppConfig, 'navigationMenu'>
|
||||
@@ -57,6 +57,11 @@ export interface NavigationMenuItem extends Omit<LinkProps, 'type' | 'raw' | 'cu
|
||||
defaultOpen?: boolean
|
||||
open?: boolean
|
||||
onSelect?(e: Event): void
|
||||
/**
|
||||
* Display a chip on the item when the menu is collapsed with the children list.
|
||||
* @defaultValue false
|
||||
*/
|
||||
chip?: boolean | ChipProps
|
||||
class?: any
|
||||
ui?: Pick<NavigationMenu['slots'], 'item' | 'linkLeadingAvatarSize' | 'linkLeadingAvatar' | 'linkLeadingIcon' | 'linkLabel' | 'linkLabelExternalIcon' | 'linkTrailing' | 'linkTrailingBadgeSize' | 'linkTrailingBadge' | 'linkTrailingIcon' | 'label' | 'link' | 'content' | 'childList' | 'childLabel' | 'childItem' | 'childLink' | 'childLinkIcon' | 'childLinkWrapper' | 'childLinkLabel' | 'childLinkLabelExternalIcon' | 'childLinkDescription'>
|
||||
[key: string]: any
|
||||
@@ -137,6 +142,11 @@ export interface NavigationMenuProps<T extends ArrayOrNested<NavigationMenuItem>
|
||||
* @defaultValue 'label'
|
||||
*/
|
||||
labelKey?: keyof NestedItem<T>
|
||||
/**
|
||||
* Display a chip on the item when the menu is collapsed with the children list.
|
||||
* @defaultValue false
|
||||
*/
|
||||
chip?: boolean | ChipProps
|
||||
class?: any
|
||||
ui?: NavigationMenu['slots']
|
||||
}
|
||||
@@ -302,7 +312,12 @@ function getAccordionDefaultValue(list: NavigationMenuItem[], level = 0) {
|
||||
:disabled="item.disabled"
|
||||
@select="item.onSelect"
|
||||
>
|
||||
<UPopover v-if="orientation === 'vertical' && collapsed && item.children?.length && (!!props.popover || !!item.popover)" v-bind="{ ...popoverProps, ...(typeof item.popover === 'boolean' ? {} : item.popover || {}) }" :ui="{ content: ui.content({ class: [props.ui?.content, item.ui?.content] }) }">
|
||||
<UChip v-if="orientation === 'vertical' && collapsed && item.children?.length && item.chip" v-bind="typeof item.chip === 'boolean' ? {} : item.chip">
|
||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: level > 0 })">
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
</UChip>
|
||||
<UPopover v-else-if="orientation === 'vertical' && collapsed && item.children?.length && (!!props.popover || !!item.popover)" v-bind="{ ...popoverProps, ...(typeof item.popover === 'boolean' ? {} : item.popover || {}) }" :ui="{ content: ui.content({ class: [props.ui?.content, item.ui?.content] }) }">
|
||||
<ULinkBase v-bind="slotProps" :class="ui.link({ class: [props.ui?.link, item.ui?.link, item.class], active: active || item.active, disabled: !!item.disabled, level: level > 0 })">
|
||||
<ReuseLinkTemplate :item="item" :active="active || item.active" :index="index" />
|
||||
</ULinkBase>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue'
|
||||
import { useOverlay } from '../composables/useOverlay'
|
||||
import type { Overlay } from '../composables/useOverlay'
|
||||
import { useOverlay, type Overlay } from '../composables/useOverlay'
|
||||
|
||||
const { overlays, unmount, close } = useOverlay()
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverContentEmits, PopoverArrowProps, HoverCardTriggerProps } from 'reka-ui'
|
||||
import type { PopoverRootProps, HoverCardRootProps, PopoverRootEmits, PopoverContentProps, PopoverContentEmits, PopoverArrowProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/popover'
|
||||
import type { EmitsToProps, ComponentConfig } from '../types/utils'
|
||||
@@ -27,12 +27,6 @@ export interface PopoverProps extends PopoverRootProps, Pick<HoverCardRootProps,
|
||||
* @defaultValue true
|
||||
*/
|
||||
portal?: boolean | string | HTMLElement
|
||||
/**
|
||||
* The reference (or anchor) element that is being referred to for positioning.
|
||||
*
|
||||
* If not provided will use the current component as anchor.
|
||||
*/
|
||||
reference?: HoverCardTriggerProps['reference']
|
||||
/**
|
||||
* When `false`, the popover will not close when clicking outside or pressing escape.
|
||||
* @defaultValue true
|
||||
@@ -106,7 +100,7 @@ const Component = computed(() => props.mode === 'hover' ? HoverCard : Popover)
|
||||
|
||||
<template>
|
||||
<Component.Root v-slot="{ open }" v-bind="rootProps">
|
||||
<Component.Trigger v-if="!!slots.default || !!reference" as-child :reference="reference" :class="props.class">
|
||||
<Component.Trigger v-if="!!slots.default" as-child :class="props.class">
|
||||
<slot :open="open" />
|
||||
</Component.Trigger>
|
||||
|
||||
|
||||
@@ -432,7 +432,7 @@ defineExpose({
|
||||
<slot name="content-top" />
|
||||
|
||||
<ComboboxInput v-if="!!searchInput" v-model="searchTerm" :display-value="() => searchTerm" as-child>
|
||||
<UInput autofocus autocomplete="off" :size="size" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
|
||||
<UInput autofocus autocomplete="off" v-bind="searchInputProps" :class="ui.input({ class: props.ui?.input })" />
|
||||
</ComboboxInput>
|
||||
|
||||
<ComboboxEmpty :class="ui.empty({ class: props.ui?.empty })">
|
||||
|
||||
@@ -83,10 +83,10 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
|
||||
*/
|
||||
empty?: string
|
||||
/**
|
||||
* Whether the table should have a sticky header or footer. True for both, 'header' for header only, 'footer' for footer only.
|
||||
* Whether the table should have a sticky header.
|
||||
* @defaultValue false
|
||||
*/
|
||||
sticky?: boolean | 'header' | 'footer'
|
||||
sticky?: boolean
|
||||
/** Whether the table should be in loading state. */
|
||||
loading?: boolean
|
||||
/**
|
||||
@@ -165,14 +165,11 @@ export interface TableProps<T extends TableData = TableData> extends TableOption
|
||||
*/
|
||||
facetedOptions?: FacetedOptions<T>
|
||||
onSelect?: (row: TableRow<T>, e?: Event) => void
|
||||
onHover?: (e: Event, row: TableRow<T> | null) => void
|
||||
onContextmenu?: ((e: Event, row: TableRow<T>) => void) | Array<((e: Event, row: TableRow<T>) => void)>
|
||||
class?: any
|
||||
ui?: Table['slots']
|
||||
}
|
||||
|
||||
type DynamicHeaderSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-header`, (props: HeaderContext<T, unknown>) => any>
|
||||
type DynamicFooterSlots<T, K = keyof T> = Record<string, (props: HeaderContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-footer`, (props: HeaderContext<T, unknown>) => any>
|
||||
type DynamicCellSlots<T, K = keyof T> = Record<string, (props: CellContext<T, unknown>) => any> & Record<`${K extends string ? K : never}-cell`, (props: CellContext<T, unknown>) => any>
|
||||
|
||||
export type TableSlots<T extends TableData = TableData> = {
|
||||
@@ -182,7 +179,7 @@ export type TableSlots<T extends TableData = TableData> = {
|
||||
'caption': (props?: {}) => any
|
||||
'body-top': (props?: {}) => any
|
||||
'body-bottom': (props?: {}) => any
|
||||
} & DynamicHeaderSlots<T> & DynamicFooterSlots<T> & DynamicCellSlots<T>
|
||||
} & DynamicHeaderSlots<T> & DynamicCellSlots<T>
|
||||
|
||||
</script>
|
||||
|
||||
@@ -217,22 +214,6 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.table || {})
|
||||
loadingAnimation: props.loadingAnimation
|
||||
}))
|
||||
|
||||
const hasFooter = computed(() => {
|
||||
function hasFooterRecursive(columns: TableColumn<T>[]): boolean {
|
||||
for (const column of columns) {
|
||||
if ('footer' in column) {
|
||||
return true
|
||||
}
|
||||
if ('columns' in column && hasFooterRecursive(column.columns as TableColumn<T>[])) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return hasFooterRecursive(columns.value)
|
||||
})
|
||||
|
||||
const globalFilterState = defineModel<string>('globalFilter', { default: undefined })
|
||||
const columnFiltersState = defineModel<ColumnFiltersState>('columnFilters', { default: [] })
|
||||
const columnOrderState = defineModel<ColumnOrderState>('columnOrder', { default: [] })
|
||||
@@ -252,9 +233,7 @@ const tableRef = ref<HTMLTableElement | null>(null)
|
||||
const tableApi = useVueTable({
|
||||
...reactiveOmit(props, 'as', 'data', 'columns', 'caption', 'sticky', 'loading', 'loadingColor', 'loadingAnimation', 'class', 'ui'),
|
||||
data,
|
||||
get columns() {
|
||||
return columns.value
|
||||
},
|
||||
columns: columns.value,
|
||||
meta: meta.value,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
...(props.globalFilterOptions || {}),
|
||||
@@ -332,7 +311,7 @@ function valueUpdater<T extends Updater<any>>(updaterOrValue: T, ref: Ref) {
|
||||
ref.value = typeof updaterOrValue === 'function' ? updaterOrValue(ref.value) : updaterOrValue
|
||||
}
|
||||
|
||||
function onRowSelect(e: Event, row: TableRow<T>) {
|
||||
function handleRowSelect(row: TableRow<T>, e: Event) {
|
||||
if (!props.onSelect) {
|
||||
return
|
||||
}
|
||||
@@ -345,30 +324,9 @@ function onRowSelect(e: Event, row: TableRow<T>) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
|
||||
// FIXME: `e` should be the first argument for consistency
|
||||
props.onSelect(row, e)
|
||||
}
|
||||
|
||||
function onRowHover(e: Event, row: TableRow<T> | null) {
|
||||
if (!props.onHover) {
|
||||
return
|
||||
}
|
||||
|
||||
props.onHover(e, row)
|
||||
}
|
||||
|
||||
function onRowContextmenu(e: Event, row: TableRow<T>) {
|
||||
if (!props.onContextmenu) {
|
||||
return
|
||||
}
|
||||
|
||||
if (Array.isArray(props.onContextmenu)) {
|
||||
props.onContextmenu.forEach(fn => fn(e, row))
|
||||
} else {
|
||||
props.onContextmenu(e, row)
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => props.data, () => {
|
||||
data.value = props.data ? [...props.data] : []
|
||||
@@ -396,7 +354,6 @@ defineExpose({
|
||||
v-for="header in headerGroup.headers"
|
||||
:key="header.id"
|
||||
:data-pinned="header.column.getIsPinned()"
|
||||
:scope="header.colSpan > 1 ? 'colgroup' : 'col'"
|
||||
:colspan="header.colSpan > 1 ? header.colSpan : undefined"
|
||||
:class="ui.th({
|
||||
class: [
|
||||
@@ -422,7 +379,7 @@ defineExpose({
|
||||
<template v-for="row in tableApi.getRowModel().rows" :key="row.id">
|
||||
<tr
|
||||
:data-selected="row.getIsSelected()"
|
||||
:data-selectable="!!props.onSelect || !!props.onHover || !!props.onContextmenu"
|
||||
:data-selectable="!!props.onSelect"
|
||||
:data-expanded="row.getIsExpanded()"
|
||||
:role="props.onSelect ? 'button' : undefined"
|
||||
:tabindex="props.onSelect ? 0 : undefined"
|
||||
@@ -432,10 +389,7 @@ defineExpose({
|
||||
typeof tableApi.options.meta?.class?.tr === 'function' ? tableApi.options.meta.class.tr(row) : tableApi.options.meta?.class?.tr
|
||||
]
|
||||
})"
|
||||
@click="onRowSelect($event, row)"
|
||||
@pointerenter="onRowHover($event, row)"
|
||||
@pointerleave="onRowHover($event, null)"
|
||||
@contextmenu="onRowContextmenu($event, row)"
|
||||
@click="handleRowSelect(row, $event)"
|
||||
>
|
||||
<td
|
||||
v-for="cell in row.getVisibleCells()"
|
||||
@@ -478,30 +432,6 @@ defineExpose({
|
||||
|
||||
<slot name="body-bottom" />
|
||||
</tbody>
|
||||
|
||||
<tfoot v-if="hasFooter" :class="ui.tfoot({ class: [props.ui?.tfoot] })">
|
||||
<tr :class="ui.separator({ class: [props.ui?.separator] })" />
|
||||
|
||||
<tr v-for="footerGroup in tableApi.getFooterGroups()" :key="footerGroup.id" :class="ui.tr({ class: [props.ui?.tr] })">
|
||||
<th
|
||||
v-for="header in footerGroup.headers"
|
||||
:key="header.id"
|
||||
:data-pinned="header.column.getIsPinned()"
|
||||
:colspan="header.colSpan > 1 ? header.colSpan : undefined"
|
||||
:class="ui.th({
|
||||
class: [
|
||||
props.ui?.th,
|
||||
typeof header.column.columnDef.meta?.class?.th === 'function' ? header.column.columnDef.meta.class.th(header) : header.column.columnDef.meta?.class?.th
|
||||
],
|
||||
pinned: !!header.column.getIsPinned()
|
||||
})"
|
||||
>
|
||||
<slot :name="`${header.id}-footer`" v-bind="header.getContext()">
|
||||
<FlexRender v-if="!header.isPlaceholder" :render="header.column.columnDef.footer" :props="header.getContext()" />
|
||||
</slot>
|
||||
</th>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</Primitive>
|
||||
</template>
|
||||
|
||||
@@ -9,7 +9,7 @@ type Textarea = ComponentConfig<typeof theme, AppConfig, 'textarea'>
|
||||
|
||||
type TextareaValue = string | number | null
|
||||
|
||||
export interface TextareaProps<T extends TextareaValue = TextareaValue> extends UseComponentIconsProps {
|
||||
export interface TextareaProps extends UseComponentIconsProps {
|
||||
/**
|
||||
* The element or component this component should render as.
|
||||
* @defaultValue 'div'
|
||||
@@ -41,11 +41,8 @@ export interface TextareaProps<T extends TextareaValue = TextareaValue> extends
|
||||
maxrows?: number
|
||||
/** Highlight the ring color like a focus state. */
|
||||
highlight?: boolean
|
||||
modelValue?: T
|
||||
defaultValue?: T
|
||||
modelModifiers?: {
|
||||
string?: boolean
|
||||
number?: boolean
|
||||
trim?: boolean
|
||||
lazy?: boolean
|
||||
nullify?: boolean
|
||||
@@ -70,7 +67,6 @@ export interface TextareaSlots {
|
||||
<script setup lang="ts" generic="T extends TextareaValue">
|
||||
import { ref, computed, onMounted, nextTick, watch } from 'vue'
|
||||
import { Primitive } from 'reka-ui'
|
||||
import { useVModel } from '@vueuse/core'
|
||||
import { useAppConfig } from '#imports'
|
||||
import { useComponentIcons } from '../composables/useComponentIcons'
|
||||
import { useFormField } from '../composables/useFormField'
|
||||
@@ -81,7 +77,7 @@ import UAvatar from './Avatar.vue'
|
||||
|
||||
defineOptions({ inheritAttrs: false })
|
||||
|
||||
const props = withDefaults(defineProps<TextareaProps<T>>(), {
|
||||
const props = withDefaults(defineProps<TextareaProps>(), {
|
||||
rows: 3,
|
||||
maxrows: 0,
|
||||
autofocusDelay: 0,
|
||||
@@ -90,11 +86,12 @@ const props = withDefaults(defineProps<TextareaProps<T>>(), {
|
||||
const emits = defineEmits<TextareaEmits<T>>()
|
||||
const slots = defineSlots<TextareaSlots>()
|
||||
|
||||
const modelValue = useVModel<TextareaProps<T>, 'modelValue', 'update:modelValue'>(props, 'modelValue', emits, { defaultValue: props.defaultValue })
|
||||
// eslint-disable-next-line vue/no-dupe-keys
|
||||
const [modelValue, modelModifiers] = defineModel<T>()
|
||||
|
||||
const appConfig = useAppConfig() as Textarea['AppConfig']
|
||||
|
||||
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps<T>>(props, { deferInputValidation: true })
|
||||
const { emitFormFocus, emitFormBlur, emitFormInput, emitFormChange, size, color, id, name, highlight, disabled, ariaAttrs } = useFormField<TextareaProps>(props, { deferInputValidation: true })
|
||||
const { isLeading, isTrailing, leadingIconName, trailingIconName } = useComponentIcons(props)
|
||||
|
||||
const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.textarea || {}) })({
|
||||
@@ -112,15 +109,15 @@ const textareaRef = ref<HTMLTextAreaElement | null>(null)
|
||||
|
||||
// Custom function to handle the v-model properties
|
||||
function updateInput(value: string | null) {
|
||||
if (props.modelModifiers?.trim) {
|
||||
if (modelModifiers.trim) {
|
||||
value = value?.trim() ?? null
|
||||
}
|
||||
|
||||
if (props.modelModifiers?.number) {
|
||||
if (modelModifiers.number) {
|
||||
value = looseToNumber(value)
|
||||
}
|
||||
|
||||
if (props.modelModifiers?.nullify) {
|
||||
if (modelModifiers.nullify) {
|
||||
value ||= null
|
||||
}
|
||||
|
||||
@@ -131,7 +128,7 @@ function updateInput(value: string | null) {
|
||||
function onInput(event: Event) {
|
||||
autoResize()
|
||||
|
||||
if (!props.modelModifiers?.lazy) {
|
||||
if (!modelModifiers.lazy) {
|
||||
updateInput((event.target as HTMLInputElement).value)
|
||||
}
|
||||
}
|
||||
@@ -139,12 +136,12 @@ function onInput(event: Event) {
|
||||
function onChange(event: Event) {
|
||||
const value = (event.target as HTMLInputElement).value
|
||||
|
||||
if (props.modelModifiers?.lazy) {
|
||||
if (modelModifiers.lazy) {
|
||||
updateInput(value)
|
||||
}
|
||||
|
||||
// Update trimmed textarea so that it has same behavior as native textarea https://github.com/vuejs/core/blob/5ea8a8a4fab4e19a71e123e4d27d051f5e927172/packages/runtime-dom/src/directives/vModel.ts#L63
|
||||
if (props.modelModifiers?.trim) {
|
||||
if (modelModifiers.trim) {
|
||||
(event.target as HTMLInputElement).value = value.trim()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ToastRootProps, ToastRootEmits } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/toast'
|
||||
import type { AvatarProps, ButtonProps, ProgressProps } from '../types'
|
||||
import type { AvatarProps, ButtonProps } from '../types'
|
||||
import type { StringOrVNode, ComponentConfig } from '../types/utils'
|
||||
|
||||
type Toast = ComponentConfig<typeof theme, AppConfig, 'toast'>
|
||||
@@ -29,6 +29,18 @@ export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open'
|
||||
* @defaultValue 'vertical'
|
||||
*/
|
||||
orientation?: Toast['variants']['orientation']
|
||||
/**
|
||||
* Whether to show the progress bar.
|
||||
* @defaultValue true
|
||||
*/
|
||||
progress?: boolean
|
||||
/**
|
||||
* Display a list of actions:
|
||||
* - under the title and description when orientation is `vertical`
|
||||
* - next to the close button when orientation is `horizontal`
|
||||
* `{ size: 'xs' }`{lang="ts-type"}
|
||||
*/
|
||||
actions?: ButtonProps[]
|
||||
/**
|
||||
* Display a close button to dismiss the toast.
|
||||
* `{ size: 'md', color: 'neutral', variant: 'link' }`{lang="ts-type"}
|
||||
@@ -41,19 +53,6 @@ export interface ToastProps extends Pick<ToastRootProps, 'defaultOpen' | 'open'
|
||||
* @IconifyIcon
|
||||
*/
|
||||
closeIcon?: string
|
||||
/**
|
||||
* Display a list of actions:
|
||||
* - under the title and description when orientation is `vertical`
|
||||
* - next to the close button when orientation is `horizontal`
|
||||
* `{ size: 'xs' }`{lang="ts-type"}
|
||||
*/
|
||||
actions?: ButtonProps[]
|
||||
/**
|
||||
* Display a progress bar showing the toast's remaining duration.
|
||||
* `{ size: 'sm' }`{lang="ts-type"}
|
||||
* @defaultValue true
|
||||
*/
|
||||
progress?: boolean | Pick<ProgressProps, 'color'>
|
||||
class?: any
|
||||
ui?: Toast['slots']
|
||||
}
|
||||
@@ -79,11 +78,10 @@ import { tv } from '../utils/tv'
|
||||
import UIcon from './Icon.vue'
|
||||
import UAvatar from './Avatar.vue'
|
||||
import UButton from './Button.vue'
|
||||
import UProgress from './Progress.vue'
|
||||
|
||||
const props = withDefaults(defineProps<ToastProps>(), {
|
||||
orientation: 'vertical',
|
||||
close: true,
|
||||
orientation: 'vertical',
|
||||
progress: true
|
||||
})
|
||||
const emits = defineEmits<ToastEmits>()
|
||||
@@ -121,7 +119,7 @@ defineExpose({
|
||||
<template>
|
||||
<ToastRoot
|
||||
ref="el"
|
||||
v-slot="{ remaining, duration, open }"
|
||||
v-slot="{ remaining, duration }"
|
||||
v-bind="rootProps"
|
||||
:data-orientation="orientation"
|
||||
:class="ui.root({ class: [props.ui?.root, props.class] })"
|
||||
@@ -186,13 +184,6 @@ defineExpose({
|
||||
</ToastClose>
|
||||
</div>
|
||||
|
||||
<UProgress
|
||||
v-if="progress && open && remaining > 0 && duration"
|
||||
:model-value="remaining / duration * 100"
|
||||
:color="color"
|
||||
v-bind="(typeof progress === 'object' ? progress as Partial<ProgressProps> : {})"
|
||||
size="sm"
|
||||
:class="ui.progress({ class: props.ui?.progress })"
|
||||
/>
|
||||
<div v-if="progress && remaining > 0 && duration" :class="ui.progress({ class: props.ui?.progress })" :style="{ width: `${remaining / duration * 100}%` }" />
|
||||
</ToastRoot>
|
||||
</template>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import type { TooltipRootProps, TooltipRootEmits, TooltipContentProps, TooltipContentEmits, TooltipArrowProps, TooltipTriggerProps } from 'reka-ui'
|
||||
import type { TooltipRootProps, TooltipRootEmits, TooltipContentProps, TooltipContentEmits, TooltipArrowProps } from 'reka-ui'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import theme from '#build/ui/tooltip'
|
||||
import type { KbdProps } from '../types'
|
||||
@@ -27,12 +27,6 @@ export interface TooltipProps extends TooltipRootProps {
|
||||
* @defaultValue true
|
||||
*/
|
||||
portal?: boolean | string | HTMLElement
|
||||
/**
|
||||
* The reference (or anchor) element that is being referred to for positioning.
|
||||
*
|
||||
* If not provided will use the current component as anchor.
|
||||
*/
|
||||
reference?: TooltipTriggerProps['reference']
|
||||
class?: any
|
||||
ui?: Tooltip['slots']
|
||||
}
|
||||
@@ -76,7 +70,7 @@ const ui = computed(() => tv({ extend: tv(theme), ...(appConfig.ui?.tooltip || {
|
||||
|
||||
<template>
|
||||
<TooltipRoot v-slot="{ open }" v-bind="rootProps">
|
||||
<TooltipTrigger v-if="!!slots.default || !!reference" v-bind="$attrs" as-child :reference="reference" :class="props.class">
|
||||
<TooltipTrigger v-if="!!slots.default" v-bind="$attrs" as-child :class="props.class">
|
||||
<slot :open="open" />
|
||||
</TooltipTrigger>
|
||||
|
||||
|
||||
@@ -36,8 +36,6 @@ interface Shortcut {
|
||||
|
||||
const chainedShortcutRegex = /^[^-]+.*-.*[^-]+$/
|
||||
const combinedShortcutRegex = /^[^_]+.*_.*[^_]+$/
|
||||
// keyboard keys which can be combined with Shift modifier (in addition to alphabet keys)
|
||||
const shiftableKeys = ['arrowleft', 'arrowright', 'arrowup', 'arrowright', 'tab', 'escape', 'enter', 'backspace']
|
||||
|
||||
export function extractShortcuts(items: any[] | any[][]) {
|
||||
const shortcuts: Record<string, Handler> = {}
|
||||
@@ -78,8 +76,7 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
|
||||
return
|
||||
}
|
||||
|
||||
const alphabetKey = /^[a-z]{1}$/i.test(e.key)
|
||||
const shiftableKey = shiftableKeys.includes(e.key.toLowerCase())
|
||||
const alphabeticalKey = /^[a-z]{1}$/i.test(e.key)
|
||||
|
||||
let chainedKey
|
||||
chainedInputs.value.push(e.key)
|
||||
@@ -112,9 +109,9 @@ export function defineShortcuts(config: MaybeRef<ShortcutsConfig>, options: Shor
|
||||
if (e.ctrlKey !== shortcut.ctrlKey) {
|
||||
continue
|
||||
}
|
||||
// shift modifier is only checked in combination with alphabet keys and some extra keys
|
||||
// (shift with special characters would change the key)
|
||||
if ((alphabetKey || shiftableKey) && e.shiftKey !== shortcut.shiftKey) {
|
||||
// shift modifier is only checked in combination with alphabetical keys
|
||||
// (shift with non-alphabetical keys would change the key)
|
||||
if (alphabeticalKey && e.shiftKey !== shortcut.shiftKey) {
|
||||
continue
|
||||
}
|
||||
// alt modifier changes the combined key anyways
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { inject, provide, computed } from 'vue'
|
||||
import type { ComputedRef, InjectionKey } from 'vue'
|
||||
import { inject, provide, computed, type ComputedRef, type InjectionKey } from 'vue'
|
||||
import type { AvatarGroupProps } from '../types'
|
||||
|
||||
export const avatarGroupInjectionKey: InjectionKey<ComputedRef<{ size: AvatarGroupProps['size'] }>> = Symbol('nuxt-ui.avatar-group')
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { computed, toValue } from 'vue'
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
import { computed, toValue, type MaybeRefOrGetter } from 'vue'
|
||||
import { useAppConfig } from '#imports'
|
||||
import type { AvatarProps } from '../types'
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
import { inject, computed, provide } from 'vue'
|
||||
import type { InjectionKey, Ref, ComputedRef } from 'vue'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import type { UseEventBusReturn } from '@vueuse/core'
|
||||
import { inject, computed, type InjectionKey, type Ref, type ComputedRef, provide } from 'vue'
|
||||
import { type UseEventBusReturn, useDebounceFn } from '@vueuse/core'
|
||||
import type { FormFieldProps } from '../types'
|
||||
import type { FormEvent, FormInputEvents, FormFieldInjectedOptions, FormInjectedOptions } from '../types/form'
|
||||
import type { GetObjectField } from '../types/utils'
|
||||
|
||||
@@ -3,34 +3,9 @@ import { reactive, markRaw, shallowReactive } from 'vue'
|
||||
import { createSharedComposable } from '@vueuse/core'
|
||||
import type { ComponentProps, ComponentEmit } from 'vue-component-type-helpers'
|
||||
|
||||
/**
|
||||
* This is a workaround for a design limitation in TypeScript.
|
||||
*
|
||||
* Conditional types only match the last function overload, not a union of all possible
|
||||
* parameter types. This workaround forces TypeScript to properly extract the 'close'
|
||||
* event argument type from component emits with multiple event signatures.
|
||||
*
|
||||
* @see https://github.com/microsoft/TypeScript/issues/32164
|
||||
*/
|
||||
type CloseEventArgType<T> = T extends {
|
||||
(event: 'close', arg_0: infer Arg, ...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
(...args: any[]): void
|
||||
} ? Arg : never
|
||||
// Extracts the first argument of the close event
|
||||
type CloseEventArgType<T> = T extends (event: 'close', args_0: infer R) => void ? R : never
|
||||
|
||||
export type OverlayOptions<OverlayAttrs = Record<string, any>> = {
|
||||
defaultOpen?: boolean
|
||||
props?: OverlayAttrs
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { inject, provide, computed } from 'vue'
|
||||
import type { Ref, InjectionKey } from 'vue'
|
||||
import { inject, provide, computed, type Ref, type InjectionKey } from 'vue'
|
||||
|
||||
export const portalTargetInjectionKey: InjectionKey<Ref<string | HTMLElement>> = Symbol('nuxt-ui.portal-target')
|
||||
|
||||
|
||||
@@ -21,11 +21,11 @@ export interface Form<S extends FormSchema> {
|
||||
blurredFields: ReadonlySet<DeepReadonly<keyof FormData<S, false>>>
|
||||
}
|
||||
|
||||
export type FormSchema<I extends object = object, O extends object = I>
|
||||
= | YupObjectSchema<I>
|
||||
| JoiSchema<I>
|
||||
| SuperstructSchema<any, any>
|
||||
| StandardSchemaV1<I, O>
|
||||
export type FormSchema<I extends object = object, O extends object = I> =
|
||||
| YupObjectSchema<I>
|
||||
| JoiSchema<I>
|
||||
| SuperstructSchema<any, any>
|
||||
| StandardSchemaV1<I, O>
|
||||
|
||||
// Define a utility type to infer the input type based on the schema type
|
||||
export type InferInput<Schema> = Schema extends StandardSchemaV1 ? StandardSchemaV1.InferInput<Schema>
|
||||
@@ -83,10 +83,10 @@ export type FormInputEvent<T extends object> = {
|
||||
eager?: boolean
|
||||
}
|
||||
|
||||
export type FormEvent<T extends object>
|
||||
= | FormInputEvent<T>
|
||||
| FormChildAttachEvent
|
||||
| FormChildDetachEvent
|
||||
export type FormEvent<T extends object> =
|
||||
| FormInputEvent<T>
|
||||
| FormChildAttachEvent
|
||||
| FormChildDetachEvent
|
||||
|
||||
export interface FormInjectedOptions {
|
||||
disabled?: boolean
|
||||
|
||||
@@ -30,8 +30,8 @@ type ComponentSlots<T extends { slots?: Record<string, any> }> = Id<{
|
||||
[K in keyof T['slots']]?: ClassValue
|
||||
}>
|
||||
|
||||
type GetComponentAppConfig<A, U extends string, K extends string>
|
||||
= A extends Record<U, Record<K, any>> ? A[U][K] : {}
|
||||
type GetComponentAppConfig<A, U extends string, K extends string> =
|
||||
A extends Record<U, Record<K, any>> ? A[U][K] : {}
|
||||
|
||||
type ComponentAppConfig<
|
||||
T,
|
||||
|
||||
@@ -44,8 +44,8 @@ export type MergeTypes<T extends object> = {
|
||||
|
||||
export type GetItemKeys<I> = keyof Extract<NestedItem<I>, object>
|
||||
|
||||
export type GetItemValue<I, VK extends GetItemKeys<I> | undefined, T extends NestedItem<I> = NestedItem<I>>
|
||||
= T extends object
|
||||
export type GetItemValue<I, VK extends GetItemKeys<I> | undefined, T extends NestedItem<I> = NestedItem<I>> =
|
||||
T extends object
|
||||
? VK extends undefined
|
||||
? T
|
||||
: VK extends keyof T
|
||||
@@ -70,10 +70,10 @@ export type GetModelValueEmits<
|
||||
'update:modelValue': [payload: GetModelValue<T, VK, M>]
|
||||
}
|
||||
|
||||
export type StringOrVNode
|
||||
= | string
|
||||
| VNode
|
||||
| (() => VNode)
|
||||
export type StringOrVNode =
|
||||
| string
|
||||
| VNode
|
||||
| (() => VNode)
|
||||
|
||||
export type EmitsToProps<T> = {
|
||||
[K in keyof T as `on${Capitalize<string & K>}`]: T[K] extends [...args: infer Args]
|
||||
|
||||
@@ -85,14 +85,3 @@ export function compare<T>(value?: T, currentValue?: T, comparator?: string | ((
|
||||
export function isArrayOfArray<A>(item: A[] | A[][]): item is A[][] {
|
||||
return Array.isArray(item[0])
|
||||
}
|
||||
|
||||
export function mergeClasses(appConfigClass?: string | string[], propClass?: string) {
|
||||
if (!appConfigClass && !propClass) {
|
||||
return ''
|
||||
}
|
||||
|
||||
return [
|
||||
...(Array.isArray(appConfigClass) ? appConfigClass : [appConfigClass]),
|
||||
propClass
|
||||
].filter(Boolean)
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { createTV } from 'tailwind-variants'
|
||||
import type { defaultConfig } from 'tailwind-variants'
|
||||
import { createTV, type defaultConfig } from 'tailwind-variants'
|
||||
import type { AppConfig } from '@nuxt/schema'
|
||||
import appConfig from '#build/app.config'
|
||||
|
||||
|
||||
@@ -158,13 +158,13 @@ import colors from 'tailwindcss/colors'
|
||||
|
||||
const icons = ${JSON.stringify(uiConfig.icons)};
|
||||
|
||||
type NeutralColor = 'slate' | 'gray' | 'zinc' | 'neutral' | 'stone'
|
||||
type NeutralColor = 'slate' | 'gray' | 'zinc' | 'neutral' | 'stone' | (string & {})
|
||||
type Color = Exclude<keyof typeof colors, 'inherit' | 'current' | 'transparent' | 'black' | 'white' | NeutralColor> | (string & {})
|
||||
|
||||
type AppConfigUI = {
|
||||
colors?: {
|
||||
${options.theme?.colors?.map(color => `'${color}'?: Color`).join('\n\t\t')}
|
||||
neutral?: NeutralColor | (string & {})
|
||||
neutral?: NeutralColor
|
||||
}
|
||||
icons?: Partial<typeof icons>
|
||||
tv?: typeof defaultConfig
|
||||
|
||||
@@ -86,51 +86,51 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
compoundVariants: [...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'solid',
|
||||
class: `text-inverted bg-${color} hover:bg-${color}/75 active:bg-${color}/75 disabled:bg-${color} aria-disabled:bg-${color} focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-${color}`
|
||||
class: `text-inverted bg-${color} hover:bg-${color}/75 disabled:bg-${color} aria-disabled:bg-${color} focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-${color}`
|
||||
})), ...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'outline',
|
||||
class: `ring ring-inset ring-${color}/50 text-${color} hover:bg-${color}/10 active:bg-${color}/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-${color}`
|
||||
class: `ring ring-inset ring-${color}/50 text-${color} hover:bg-${color}/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent focus:outline-none focus-visible:ring-2 focus-visible:ring-${color}`
|
||||
})), ...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'soft',
|
||||
class: `text-${color} bg-${color}/10 hover:bg-${color}/15 active:bg-${color}/15 focus:outline-none focus-visible:bg-${color}/15 disabled:bg-${color}/10 aria-disabled:bg-${color}/10`
|
||||
class: `text-${color} bg-${color}/10 hover:bg-${color}/15 focus:outline-none focus-visible:bg-${color}/15 disabled:bg-${color}/10 aria-disabled:bg-${color}/10`
|
||||
})), ...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'subtle',
|
||||
class: `text-${color} ring ring-inset ring-${color}/25 bg-${color}/10 hover:bg-${color}/15 active:bg-${color}/15 disabled:bg-${color}/10 aria-disabled:bg-${color}/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-${color}`
|
||||
class: `text-${color} ring ring-inset ring-${color}/25 bg-${color}/10 hover:bg-${color}/15 disabled:bg-${color}/10 aria-disabled:bg-${color}/10 focus:outline-none focus-visible:ring-2 focus-visible:ring-${color}`
|
||||
})), ...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'ghost',
|
||||
class: `text-${color} hover:bg-${color}/10 active:bg-${color}/10 focus:outline-none focus-visible:bg-${color}/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent`
|
||||
class: `text-${color} hover:bg-${color}/10 focus:outline-none focus-visible:bg-${color}/10 disabled:bg-transparent aria-disabled:bg-transparent dark:disabled:bg-transparent dark:aria-disabled:bg-transparent`
|
||||
})), ...(options.theme.colors || []).map((color: string) => ({
|
||||
color,
|
||||
variant: 'link',
|
||||
class: `text-${color} hover:text-${color}/75 active:text-${color}/75 disabled:text-${color} aria-disabled:text-${color} focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-${color}`
|
||||
class: `text-${color} hover:text-${color}/75 disabled:text-${color} aria-disabled:text-${color} focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-${color}`
|
||||
})), {
|
||||
color: 'neutral',
|
||||
variant: 'solid',
|
||||
class: 'text-inverted bg-inverted hover:bg-inverted/90 active:bg-inverted/90 disabled:bg-inverted aria-disabled:bg-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
|
||||
class: 'text-inverted bg-inverted hover:bg-inverted/90 disabled:bg-inverted aria-disabled:bg-inverted focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-inverted'
|
||||
}, {
|
||||
color: 'neutral',
|
||||
variant: 'outline',
|
||||
class: 'ring ring-inset ring-accented text-default bg-default hover:bg-elevated active:bg-elevated disabled:bg-default aria-disabled:bg-default focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
|
||||
class: 'ring ring-inset ring-accented text-default bg-default hover:bg-elevated disabled:bg-default aria-disabled:bg-default focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
|
||||
}, {
|
||||
color: 'neutral',
|
||||
variant: 'soft',
|
||||
class: 'text-default bg-elevated hover:bg-accented/75 active:bg-accented/75 focus:outline-none focus-visible:bg-accented/75 disabled:bg-elevated aria-disabled:bg-elevated'
|
||||
class: 'text-default bg-elevated hover:bg-accented/75 focus:outline-none focus-visible:bg-accented/75 disabled:bg-elevated aria-disabled:bg-elevated'
|
||||
}, {
|
||||
color: 'neutral',
|
||||
variant: 'subtle',
|
||||
class: 'ring ring-inset ring-accented text-default bg-elevated hover:bg-accented/75 active:bg-accented/75 disabled:bg-elevated aria-disabled:bg-elevated focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
|
||||
class: 'ring ring-inset ring-accented text-default bg-elevated hover:bg-accented/75 disabled:bg-elevated aria-disabled:bg-elevated focus:outline-none focus-visible:ring-2 focus-visible:ring-inverted'
|
||||
}, {
|
||||
color: 'neutral',
|
||||
variant: 'ghost',
|
||||
class: 'text-default hover:bg-elevated active:bg-elevated focus:outline-none focus-visible:bg-elevated hover:disabled:bg-transparent dark:hover:disabled:bg-transparent hover:aria-disabled:bg-transparent dark:hover:aria-disabled:bg-transparent'
|
||||
class: 'text-default hover:bg-elevated focus:outline-none focus-visible:bg-elevated hover:disabled:bg-transparent dark:hover:disabled:bg-transparent hover:aria-disabled:bg-transparent dark:hover:aria-disabled:bg-transparent'
|
||||
}, {
|
||||
color: 'neutral',
|
||||
variant: 'link',
|
||||
class: 'text-muted hover:text-default active:text-default disabled:text-muted aria-disabled:text-muted focus:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-inverted'
|
||||
class: 'text-muted hover:text-default disabled:text-muted aria-disabled:text-muted focus:outline-none focus-visible:ring-inset focus-visible:ring-2 focus-visible:ring-inverted'
|
||||
}, {
|
||||
size: 'xs',
|
||||
square: true,
|
||||
|
||||
@@ -7,7 +7,6 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
close: '',
|
||||
back: 'p-0',
|
||||
content: 'relative overflow-hidden flex flex-col',
|
||||
footer: 'p-1',
|
||||
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1 focus:outline-none',
|
||||
group: 'p-1 isolate',
|
||||
empty: 'py-6 text-center text-sm text-muted',
|
||||
|
||||
@@ -11,7 +11,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
content: 'max-h-60 w-(--reka-combobox-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-combobox-content-transform-origin) pointer-events-auto flex flex-col',
|
||||
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
|
||||
group: 'p-1 isolate',
|
||||
empty: 'text-center text-muted',
|
||||
empty: 'py-2 text-center text-sm text-muted',
|
||||
label: 'font-semibold text-highlighted',
|
||||
separator: '-mx-1 my-1 h-px bg-border',
|
||||
item: ['group relative w-full flex items-center gap-1.5 p-1.5 text-sm select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
@@ -48,8 +48,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingChipSize: 'sm',
|
||||
itemTrailingIcon: 'size-4',
|
||||
tagsItem: 'text-[10px]/3',
|
||||
tagsItemDeleteIcon: 'size-3',
|
||||
empty: 'p-1 text-xs'
|
||||
tagsItemDeleteIcon: 'size-3'
|
||||
},
|
||||
sm: {
|
||||
label: 'p-1.5 text-[10px]/3 gap-1.5',
|
||||
@@ -60,8 +59,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingChipSize: 'sm',
|
||||
itemTrailingIcon: 'size-4',
|
||||
tagsItem: 'text-[10px]/3',
|
||||
tagsItemDeleteIcon: 'size-3',
|
||||
empty: 'p-1.5 text-xs'
|
||||
tagsItemDeleteIcon: 'size-3'
|
||||
},
|
||||
md: {
|
||||
label: 'p-1.5 text-xs gap-1.5',
|
||||
@@ -72,8 +70,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingChipSize: 'md',
|
||||
itemTrailingIcon: 'size-5',
|
||||
tagsItem: 'text-xs',
|
||||
tagsItemDeleteIcon: 'size-3.5',
|
||||
empty: 'p-1.5 text-sm'
|
||||
tagsItemDeleteIcon: 'size-3.5'
|
||||
},
|
||||
lg: {
|
||||
label: 'p-2 text-xs gap-2',
|
||||
@@ -84,8 +81,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingChipSize: 'md',
|
||||
itemTrailingIcon: 'size-5',
|
||||
tagsItem: 'text-xs',
|
||||
tagsItemDeleteIcon: 'size-3.5',
|
||||
empty: 'p-2 text-sm'
|
||||
tagsItemDeleteIcon: 'size-3.5'
|
||||
},
|
||||
xl: {
|
||||
label: 'p-2 text-sm gap-2',
|
||||
@@ -96,8 +92,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingChipSize: 'lg',
|
||||
itemTrailingIcon: 'size-6',
|
||||
tagsItem: 'text-sm',
|
||||
tagsItemDeleteIcon: 'size-4',
|
||||
empty: 'p-2 text-base'
|
||||
tagsItemDeleteIcon: 'size-4'
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
content: 'max-h-60 w-(--reka-select-trigger-width) bg-default shadow-lg rounded-md ring ring-default overflow-hidden data-[state=open]:animate-[scale-in_100ms_ease-out] data-[state=closed]:animate-[scale-out_100ms_ease-in] origin-(--reka-select-content-transform-origin) pointer-events-auto flex flex-col',
|
||||
viewport: 'relative divide-y divide-default scroll-py-1 overflow-y-auto flex-1',
|
||||
group: 'p-1 isolate',
|
||||
empty: 'text-center text-muted',
|
||||
empty: 'py-2 text-center text-sm text-muted',
|
||||
label: 'font-semibold text-highlighted',
|
||||
separator: '-mx-1 my-1 h-px bg-border',
|
||||
item: ['group relative w-full flex items-center select-none outline-none before:absolute before:z-[-1] before:inset-px before:rounded-md data-disabled:cursor-not-allowed data-disabled:opacity-75 text-default data-highlighted:not-data-disabled:text-highlighted data-highlighted:not-data-disabled:before:bg-elevated/50', options.theme.transitions && 'transition-colors before:transition-colors'],
|
||||
@@ -37,8 +37,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingAvatarSize: '3xs',
|
||||
itemLeadingChip: 'size-4',
|
||||
itemLeadingChipSize: 'sm',
|
||||
itemTrailingIcon: 'size-4',
|
||||
empty: 'p-1 text-xs'
|
||||
itemTrailingIcon: 'size-4'
|
||||
},
|
||||
sm: {
|
||||
label: 'p-1.5 text-[10px]/3 gap-1.5',
|
||||
@@ -47,8 +46,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingAvatarSize: '3xs',
|
||||
itemLeadingChip: 'size-4',
|
||||
itemLeadingChipSize: 'sm',
|
||||
itemTrailingIcon: 'size-4',
|
||||
empty: 'p-1.5 text-xs'
|
||||
itemTrailingIcon: 'size-4'
|
||||
},
|
||||
md: {
|
||||
label: 'p-1.5 text-xs gap-1.5',
|
||||
@@ -57,8 +55,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingAvatarSize: '2xs',
|
||||
itemLeadingChip: 'size-5',
|
||||
itemLeadingChipSize: 'md',
|
||||
itemTrailingIcon: 'size-5',
|
||||
empty: 'p-1.5 text-sm'
|
||||
itemTrailingIcon: 'size-5'
|
||||
},
|
||||
lg: {
|
||||
label: 'p-2 text-xs gap-2',
|
||||
@@ -67,8 +64,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingAvatarSize: '2xs',
|
||||
itemLeadingChip: 'size-5',
|
||||
itemLeadingChipSize: 'md',
|
||||
itemTrailingIcon: 'size-5',
|
||||
empty: 'p-2 text-sm'
|
||||
itemTrailingIcon: 'size-5'
|
||||
},
|
||||
xl: {
|
||||
label: 'p-2 text-sm gap-2',
|
||||
@@ -77,8 +73,7 @@ export default (options: Required<ModuleOptions>) => {
|
||||
itemLeadingAvatarSize: 'xs',
|
||||
itemLeadingChip: 'size-6',
|
||||
itemLeadingChipSize: 'lg',
|
||||
itemTrailingIcon: 'size-6',
|
||||
empty: 'p-2 text-base'
|
||||
itemTrailingIcon: 'size-6'
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
caption: 'sr-only',
|
||||
thead: 'relative',
|
||||
tbody: 'divide-y divide-default [&>tr]:data-[selectable=true]:hover:bg-elevated/50 [&>tr]:data-[selectable=true]:focus-visible:outline-primary',
|
||||
tfoot: 'relative',
|
||||
tr: 'data-[selected=true]:bg-elevated/50',
|
||||
th: 'px-4 py-3.5 text-sm text-highlighted text-left rtl:text-right font-semibold [&:has([role=checkbox])]:pe-0',
|
||||
td: 'p-4 text-sm text-muted whitespace-nowrap [&:has([role=checkbox])]:pe-0',
|
||||
@@ -24,14 +23,7 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
},
|
||||
sticky: {
|
||||
true: {
|
||||
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur',
|
||||
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
|
||||
},
|
||||
header: {
|
||||
thead: 'sticky top-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
|
||||
},
|
||||
footer: {
|
||||
tfoot: 'sticky bottom-0 inset-x-0 bg-default/75 z-[1] backdrop-blur'
|
||||
}
|
||||
},
|
||||
loading: {
|
||||
|
||||
@@ -10,18 +10,20 @@ export default (options: Required<ModuleOptions>) => ({
|
||||
avatar: 'shrink-0',
|
||||
avatarSize: '2xl',
|
||||
actions: 'flex gap-1.5 shrink-0',
|
||||
progress: 'absolute inset-x-0 bottom-0',
|
||||
progress: 'absolute inset-x-0 bottom-0 h-1 z-[-1]',
|
||||
close: 'p-0'
|
||||
},
|
||||
variants: {
|
||||
color: {
|
||||
...Object.fromEntries((options.theme.colors || []).map((color: string) => [color, {
|
||||
root: `focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-${color}`,
|
||||
icon: `text-${color}`
|
||||
icon: `text-${color}`,
|
||||
progress: `bg-${color}`
|
||||
}])),
|
||||
neutral: {
|
||||
root: 'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-inverted',
|
||||
icon: 'text-highlighted'
|
||||
icon: 'text-highlighted',
|
||||
progress: 'bg-inverted'
|
||||
}
|
||||
},
|
||||
orientation: {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Accordion from '../../src/runtime/components/Accordion.vue'
|
||||
import type { AccordionProps, AccordionSlots } from '../../src/runtime/components/Accordion.vue'
|
||||
import Accordion, { type AccordionProps, type AccordionSlots } from '../../src/runtime/components/Accordion.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
|
||||
describe('Accordion', () => {
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Alert from '../../src/runtime/components/Alert.vue'
|
||||
import type { AlertProps, AlertSlots } from '../../src/runtime/components/Alert.vue'
|
||||
import Alert, { type AlertProps, type AlertSlots } from '../../src/runtime/components/Alert.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/alert'
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Avatar from '../../src/runtime/components/Avatar.vue'
|
||||
import type { AvatarProps, AvatarSlots } from '../../src/runtime/components/Avatar.vue'
|
||||
import Avatar, { type AvatarProps, type AvatarSlots } from '../../src/runtime/components/Avatar.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/avatar'
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Avatar from '../../src/runtime/components/Avatar.vue'
|
||||
import AvatarGroup from '../../src/runtime/components/AvatarGroup.vue'
|
||||
import type { AvatarGroupProps, AvatarGroupSlots } from '../../src/runtime/components/AvatarGroup.vue'
|
||||
import AvatarGroup, { type AvatarGroupProps, type AvatarGroupSlots } from '../../src/runtime/components/AvatarGroup.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/avatar-group'
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Badge from '../../src/runtime/components/Badge.vue'
|
||||
import type { BadgeProps, BadgeSlots } from '../../src/runtime/components/Badge.vue'
|
||||
import Badge, { type BadgeProps, type BadgeSlots } from '../../src/runtime/components/Badge.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/badge'
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Breadcrumb from '../../src/runtime/components/Breadcrumb.vue'
|
||||
import type { BreadcrumbProps, BreadcrumbSlots } from '../../src/runtime/components/Breadcrumb.vue'
|
||||
import Breadcrumb, { type BreadcrumbProps, type BreadcrumbSlots } from '../../src/runtime/components/Breadcrumb.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
|
||||
describe('Breadcrumb', () => {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ref } from 'vue'
|
||||
import { describe, it, expect, test } from 'vitest'
|
||||
import Button from '../../src/runtime/components/Button.vue'
|
||||
import type { ButtonProps, ButtonSlots } from '../../src/runtime/components/Button.vue'
|
||||
import Button, { type ButtonProps, type ButtonSlots } from '../../src/runtime/components/Button.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/button'
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import ButtonGroup from '../../src/runtime/components/ButtonGroup.vue'
|
||||
import type { ButtonGroupProps, ButtonGroupSlots } from '../../src/runtime/components/ButtonGroup.vue'
|
||||
import ButtonGroup, { type ButtonGroupProps, type ButtonGroupSlots } from '../../src/runtime/components/ButtonGroup.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import { UInput, UButton } from '#components'
|
||||
import buttonTheme from '#build/ui/button'
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { describe, it, expect, vi, afterAll, test } from 'vitest'
|
||||
import { mountSuspended } from '@nuxt/test-utils/runtime'
|
||||
import Calendar from '../../src/runtime/components/Calendar.vue'
|
||||
import type { CalendarProps, CalendarSlots } from '../../src/runtime/components/Calendar.vue'
|
||||
import Calendar, { type CalendarProps, type CalendarSlots } from '../../src/runtime/components/Calendar.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/calendar'
|
||||
import { CalendarDate } from '@internationalized/date'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Card from '../../src/runtime/components/Card.vue'
|
||||
import type { CardProps, CardSlots } from '../../src/runtime/components/Card.vue'
|
||||
import Card, { type CardProps, type CardSlots } from '../../src/runtime/components/Card.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/card'
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { defineComponent } from 'vue'
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Carousel from '../../src/runtime/components/Carousel.vue'
|
||||
import type { CarouselProps, CarouselSlots } from '../../src/runtime/components/Carousel.vue'
|
||||
import Carousel, { type CarouselProps, type CarouselSlots } from '../../src/runtime/components/Carousel.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
|
||||
const CarouselWrapper = defineComponent({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect, test } from 'vitest'
|
||||
import Checkbox from '../../src/runtime/components/Checkbox.vue'
|
||||
import type { CheckboxProps, CheckboxSlots } from '../../src/runtime/components/Checkbox.vue'
|
||||
import Checkbox, { type CheckboxProps, type CheckboxSlots } from '../../src/runtime/components/Checkbox.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/checkbox'
|
||||
import { renderForm } from '../utils/form'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect, test } from 'vitest'
|
||||
import CheckboxGroup from '../../src/runtime/components/CheckboxGroup.vue'
|
||||
import type { CheckboxGroupProps, CheckboxGroupSlots } from '../../src/runtime/components/CheckboxGroup.vue'
|
||||
import CheckboxGroup, { type CheckboxGroupProps, type CheckboxGroupSlots } from '../../src/runtime/components/CheckboxGroup.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/checkbox-group'
|
||||
import themeCheckbox from '#build/ui/checkbox'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import Chip from '../../src/runtime/components/Chip.vue'
|
||||
import type { ChipProps, ChipSlots } from '../../src/runtime/components/Chip.vue'
|
||||
import Chip, { type ChipProps, type ChipSlots } from '../../src/runtime/components/Chip.vue'
|
||||
import ComponentRender from '../component-render'
|
||||
import theme from '#build/ui/chip'
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user