mirror of
https://github.com/ArthurDanjou/ui.git
synced 2026-02-01 04:37:57 +01:00
feat(Tabs): new component (#450)
This commit is contained in:
17
docs/components/content/examples/TabsExampleBasic.vue
Normal file
17
docs/components/content/examples/TabsExampleBasic.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Tab1',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Tab2',
|
||||||
|
disabled: true,
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Tab3',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" />
|
||||||
|
</template>
|
||||||
17
docs/components/content/examples/TabsExampleCard.vue
Normal file
17
docs/components/content/examples/TabsExampleCard.vue
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Tab1',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Tab2',
|
||||||
|
disabled: true,
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Tab3',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" />
|
||||||
|
</template>
|
||||||
29
docs/components/content/examples/TabsExampleDefaultSlot.vue
Normal file
29
docs/components/content/examples/TabsExampleDefaultSlot.vue
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Getting Started',
|
||||||
|
icon: 'i-heroicons-information-circle',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Installation',
|
||||||
|
icon: 'i-heroicons-arrow-down-tray',
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Theming',
|
||||||
|
icon: 'i-heroicons-eye-dropper',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #default="{ item, index, selected }">
|
||||||
|
<div class="flex items-center gap-2 relative truncate">
|
||||||
|
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0" />
|
||||||
|
|
||||||
|
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
|
||||||
|
|
||||||
|
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
16
docs/components/content/examples/TabsExampleIndex.vue
Normal file
16
docs/components/content/examples/TabsExampleIndex.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Tab1',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Tab2',
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Tab3',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" :default-index="2" />
|
||||||
|
</template>
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
slot: 'account',
|
||||||
|
label: 'Account'
|
||||||
|
}, {
|
||||||
|
slot: 'password',
|
||||||
|
label: 'Password'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||||
|
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||||
|
|
||||||
|
function onSubmitAccount () {
|
||||||
|
console.log('Submitted form:', accountForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitPassword () {
|
||||||
|
console.log('Submitted form:', passwordForm)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #account="{ item }">
|
||||||
|
<UCard @submit.prevent="onSubmitAccount">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Make changes to your account here. Click save when you're done.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormGroup label="Name" name="name" class="mb-3">
|
||||||
|
<UInput v-model="accountForm.name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Username" name="username">
|
||||||
|
<UInput v-model="accountForm.username" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton type="submit" color="black">
|
||||||
|
Save account
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #password="{ item }">
|
||||||
|
<UCard @submit.prevent="onSubmitPassword">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Change your password here. After saving, you'll be logged out.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormGroup label="Current Password" name="current" required class="mb-3">
|
||||||
|
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="New Password" name="new" required>
|
||||||
|
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton type="submit" color="black">
|
||||||
|
Save password
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
58
docs/components/content/examples/TabsExampleItemSlot.vue
Normal file
58
docs/components/content/examples/TabsExampleItemSlot.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
key: 'account',
|
||||||
|
label: 'Account',
|
||||||
|
description: 'Make changes to your account here. Click save when you\'re done.'
|
||||||
|
}, {
|
||||||
|
key: 'password',
|
||||||
|
label: 'Password',
|
||||||
|
description: 'Change your password here. After saving, you\'ll be logged out.'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||||
|
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||||
|
|
||||||
|
function onSubmit (form) {
|
||||||
|
console.log('Submitted form:', form)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : passwordForm)">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="item.key === 'account'" class="space-y-3">
|
||||||
|
<UFormGroup label="Name" name="name">
|
||||||
|
<UInput v-model="accountForm.name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Username" name="username">
|
||||||
|
<UInput v-model="accountForm.username" />
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.key === 'password'" class="space-y-3">
|
||||||
|
<UFormGroup label="Current Password" name="current" required>
|
||||||
|
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="New Password" name="new" required>
|
||||||
|
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton type="submit" color="black">
|
||||||
|
Save {{ item.key === 'account' ? 'account' : 'password' }}
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
16
docs/components/content/examples/TabsExampleVertical.vue
Normal file
16
docs/components/content/examples/TabsExampleVertical.vue
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Tab1',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Tab2',
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Tab3',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" orientation="vertical" :ui="{ wrapper: 'flex items-center gap-4', list: { width: 'w-48' } }" />
|
||||||
|
</template>
|
||||||
296
docs/content/5.navigation/4.tabs.md
Normal file
296
docs/content/5.navigation/4.tabs.md
Normal file
@@ -0,0 +1,296 @@
|
|||||||
|
---
|
||||||
|
description: A set of tab panels that are displayed one at a time.
|
||||||
|
links:
|
||||||
|
- label: GitHub
|
||||||
|
icon: i-simple-icons-github
|
||||||
|
to: https://github.com/nuxtlabs/ui/blob/dev/src/runtime/components/navigation/Tabs.vue
|
||||||
|
navigation:
|
||||||
|
badge: Edge
|
||||||
|
---
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Pass an array to the `items` prop of the Tabs component. Each item can have the following properties:
|
||||||
|
|
||||||
|
- `label` - The label of the item.
|
||||||
|
- `slot` - A key to customize the item with a slot.
|
||||||
|
- `content` - The content to display in the panel by default.
|
||||||
|
- `disabled` - Determines whether the item is disabled or not.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:tabs-example-basic{class="w-full"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Tab1',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Tab2',
|
||||||
|
disabled: true,
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Tab3',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Vertical
|
||||||
|
|
||||||
|
You can change the orientation of the tabs by setting the `orientation` prop to `vertical`.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:tabs-example-vertical{class="w-full"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" orientation="vertical" :ui="{ wrapper: 'flex items-center gap-4', list: { width: 'w-48' } }" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### Default index
|
||||||
|
|
||||||
|
You can set the default index of the tabs by setting the `default-index` prop.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:tabs-example-index{class="w-full"}
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [...]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" :default-index="2" />
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
## Slots
|
||||||
|
|
||||||
|
You can use slots to customize the buttons and items content of the Accordion.
|
||||||
|
|
||||||
|
### `default`
|
||||||
|
|
||||||
|
Use the `#default` slot to customize the content of the trigger buttons. You will have access to the `item`, `index`, `selected` and `disabled` in the slot scope.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:tabs-example-default-slot
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
label: 'Introduction',
|
||||||
|
icon: 'i-heroicons-information-circle',
|
||||||
|
content: 'This is the content shown for Tab1'
|
||||||
|
}, {
|
||||||
|
label: 'Installation',
|
||||||
|
icon: 'i-heroicons-arrow-down-tray',
|
||||||
|
content: 'And, this is the content for Tab2'
|
||||||
|
}, {
|
||||||
|
label: 'Theming',
|
||||||
|
icon: 'i-heroicons-eye-dropper',
|
||||||
|
content: 'Finally, this is the content for Tab3'
|
||||||
|
}]
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #default="{ item, index, selected }">
|
||||||
|
<div class="flex items-center gap-2 relative truncate">
|
||||||
|
<UIcon :name="item.icon" class="w-4 h-4 flex-shrink-0" />
|
||||||
|
|
||||||
|
<span class="truncate">{{ index + 1 }}. {{ item.label }}</span>
|
||||||
|
|
||||||
|
<span v-if="selected" class="absolute -right-4 w-2 h-2 rounded-full bg-primary-500 dark:bg-primary-400" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
### `item`
|
||||||
|
|
||||||
|
Use the `#item` slot to customize the items content. You will have access to the `item`, `index` and `selected` properties in the slot scope.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:tabs-example-item-slot
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
key: 'account',
|
||||||
|
label: 'Account',
|
||||||
|
description: 'Make changes to your account here. Click save when you\'re done.'
|
||||||
|
}, {
|
||||||
|
key: 'password',
|
||||||
|
label: 'Password',
|
||||||
|
description: 'Change your password here. After saving, you\'ll be logged out.'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||||
|
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||||
|
|
||||||
|
function onSubmit (form) {
|
||||||
|
console.log('Submitted form:', form)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #item="{ item }">
|
||||||
|
<UCard @submit.prevent="() => onSubmit(item.key === 'account' ? accountForm : passwordForm)">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
{{ item.description }}
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="item.key === 'account'" class="space-y-3">
|
||||||
|
<UFormGroup label="Name" name="name">
|
||||||
|
<UInput v-model="accountForm.name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Username" name="username">
|
||||||
|
<UInput v-model="accountForm.username" />
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="item.key === 'password'" class="space-y-3">
|
||||||
|
<UFormGroup label="Current Password" name="current" required>
|
||||||
|
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="New Password" name="new" required>
|
||||||
|
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton type="submit" color="black">
|
||||||
|
Save {{ item.key === 'account' ? 'account' : 'password' }}
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
You can also pass a `slot` property to customize a specific item.
|
||||||
|
|
||||||
|
::component-example
|
||||||
|
#default
|
||||||
|
:tabs-example-item-custom-slot
|
||||||
|
|
||||||
|
#code
|
||||||
|
```vue
|
||||||
|
<script setup>
|
||||||
|
const items = [{
|
||||||
|
slot: 'account',
|
||||||
|
label: 'Account'
|
||||||
|
}, {
|
||||||
|
slot: 'password',
|
||||||
|
label: 'Password'
|
||||||
|
}]
|
||||||
|
|
||||||
|
const accountForm = reactive({ name: 'Benjamin', username: 'benjamincanac' })
|
||||||
|
const passwordForm = reactive({ currentPassword: '', newPassword: '' })
|
||||||
|
|
||||||
|
function onSubmitAccount () {
|
||||||
|
console.log('Submitted form:', accountForm)
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmitPassword () {
|
||||||
|
console.log('Submitted form:', passwordForm)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<UTabs :items="items" class="w-full">
|
||||||
|
<template #account="{ item }">
|
||||||
|
<UCard @submit.prevent="onSubmitAccount">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Make changes to your account here. Click save when you're done.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormGroup label="Name" name="name" class="mb-3">
|
||||||
|
<UInput v-model="accountForm.name" />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="Username" name="username">
|
||||||
|
<UInput v-model="accountForm.username" />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton type="submit" color="black">
|
||||||
|
Save account
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #password="{ item }">
|
||||||
|
<UCard @submit.prevent="onSubmitPassword">
|
||||||
|
<template #header>
|
||||||
|
<h3 class="text-base font-semibold leading-6 text-gray-900 dark:text-white">
|
||||||
|
{{ item.label }}
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 text-sm text-gray-500 dark:text-gray-400">
|
||||||
|
Change your password here. After saving, you'll be logged out.
|
||||||
|
</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<UFormGroup label="Current Password" name="current" required class="mb-3">
|
||||||
|
<UInput v-model="passwordForm.currentPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
<UFormGroup label="New Password" name="new" required>
|
||||||
|
<UInput v-model="passwordForm.newPassword" type="password" required />
|
||||||
|
</UFormGroup>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<UButton type="submit" color="black">
|
||||||
|
Save password
|
||||||
|
</UButton>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UTabs>
|
||||||
|
</template>
|
||||||
|
```
|
||||||
|
::
|
||||||
|
|
||||||
|
## Props
|
||||||
|
|
||||||
|
:component-props
|
||||||
|
|
||||||
|
## Preset
|
||||||
|
|
||||||
|
:component-preset
|
||||||
@@ -765,6 +765,40 @@ const pagination = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const tabs = {
|
||||||
|
wrapper: 'relative space-y-2',
|
||||||
|
container: 'relative w-full',
|
||||||
|
base: 'focus:outline-none',
|
||||||
|
list: {
|
||||||
|
base: 'relative',
|
||||||
|
background: 'bg-gray-100 dark:bg-gray-800',
|
||||||
|
rounded: 'rounded-lg',
|
||||||
|
shadow: '',
|
||||||
|
padding: 'p-1',
|
||||||
|
height: 'h-10',
|
||||||
|
width: 'w-full',
|
||||||
|
marker: {
|
||||||
|
wrapper: 'absolute top-[4px] left-[4px] duration-200 ease-out focus:outline-none',
|
||||||
|
base: 'w-full h-full',
|
||||||
|
background: 'bg-white dark:bg-gray-900',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
shadow: 'shadow-sm'
|
||||||
|
},
|
||||||
|
tab: {
|
||||||
|
base: 'relative inline-flex items-center justify-center flex-shrink-0 w-full whitespace-nowrap focus:outline-none disabled:cursor-not-allowed disabled:opacity-75 transition-colors duration-200 ease-out',
|
||||||
|
background: '',
|
||||||
|
active: 'text-gray-900 dark:text-white',
|
||||||
|
inactive: 'text-gray-500 dark:text-gray-400',
|
||||||
|
height: 'h-8',
|
||||||
|
padding: 'px-3',
|
||||||
|
size: 'text-sm',
|
||||||
|
font: 'font-medium',
|
||||||
|
rounded: 'rounded-md',
|
||||||
|
shadow: ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Overlays
|
// Overlays
|
||||||
|
|
||||||
const modal = {
|
const modal = {
|
||||||
@@ -984,6 +1018,7 @@ export default {
|
|||||||
verticalNavigation,
|
verticalNavigation,
|
||||||
commandPalette,
|
commandPalette,
|
||||||
pagination,
|
pagination,
|
||||||
|
tabs,
|
||||||
modal,
|
modal,
|
||||||
slideover,
|
slideover,
|
||||||
popover,
|
popover,
|
||||||
|
|||||||
117
src/runtime/components/navigation/Tabs.vue
Normal file
117
src/runtime/components/navigation/Tabs.vue
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
<template>
|
||||||
|
<HTabGroup :vertical="orientation === 'vertical'" :default-index="defaultIndex" as="div" :class="ui.wrapper" @change="onChange">
|
||||||
|
<HTabList
|
||||||
|
:class="[ui.list.base, ui.list.background, ui.list.rounded, ui.list.shadow, ui.list.padding, ui.list.width, orientation === 'horizontal' && ui.list.height, orientation === 'horizontal' && 'inline-grid items-center']"
|
||||||
|
:style="[orientation === 'horizontal' && `grid-template-columns: repeat(${items.length}, minmax(0, 1fr))`]"
|
||||||
|
>
|
||||||
|
<div ref="markerRef" :class="ui.list.marker.wrapper">
|
||||||
|
<div :class="[ui.list.marker.base, ui.list.marker.background, ui.list.marker.rounded, ui.list.marker.shadow]" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<HTab
|
||||||
|
v-for="(item, index) of items"
|
||||||
|
:key="index"
|
||||||
|
ref="itemRefs"
|
||||||
|
v-slot="{ selected, disabled }"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
as="template"
|
||||||
|
>
|
||||||
|
<button :class="[ui.list.tab.base, ui.list.tab.background, ui.list.tab.height, ui.list.tab.padding, ui.list.tab.size, ui.list.tab.font, ui.list.tab.rounded, ui.list.tab.shadow, selected ? ui.list.tab.active : ui.list.tab.inactive]">
|
||||||
|
<slot :item="item" :index="index" :selected="selected" :disabled="disabled">
|
||||||
|
{{ item.label }}
|
||||||
|
</slot>
|
||||||
|
</button>
|
||||||
|
</HTab>
|
||||||
|
</HTabList>
|
||||||
|
|
||||||
|
<HTabPanels :class="ui.container">
|
||||||
|
<HTabPanel
|
||||||
|
v-for="(item, index) of items"
|
||||||
|
:key="index"
|
||||||
|
v-slot="{ selected }"
|
||||||
|
:class="ui.base"
|
||||||
|
>
|
||||||
|
<slot :name="item.slot || 'item'" :item="item" :index="index" :selected="selected">
|
||||||
|
{{ item.content }}
|
||||||
|
</slot>
|
||||||
|
</HTabPanel>
|
||||||
|
</HTabPanels>
|
||||||
|
</HTabGroup>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { computed, defineComponent } from 'vue'
|
||||||
|
import type { PropType } from 'vue'
|
||||||
|
import { TabGroup as HTabGroup, TabList as HTabList, Tab as HTab, TabPanels as HTabPanels, TabPanel as HTabPanel } from '@headlessui/vue'
|
||||||
|
import { defu } from 'defu'
|
||||||
|
import type { TabItem } from '../../types/tabs'
|
||||||
|
import { useAppConfig } from '#imports'
|
||||||
|
// TODO: Remove
|
||||||
|
// @ts-expect-error
|
||||||
|
import appConfig from '#build/app.config'
|
||||||
|
|
||||||
|
// const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
components: {
|
||||||
|
HTabGroup,
|
||||||
|
HTabList,
|
||||||
|
HTab,
|
||||||
|
HTabPanels,
|
||||||
|
HTabPanel
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
orientation: {
|
||||||
|
type: String as PropType<'horizontal' | 'vertical'>,
|
||||||
|
default: 'horizontal',
|
||||||
|
validator: (value: string) => ['horizontal', 'vertical'].includes(value)
|
||||||
|
},
|
||||||
|
defaultIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
items: {
|
||||||
|
type: Array as PropType<TabItem[]>,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
ui: {
|
||||||
|
type: Object as PropType<Partial<typeof appConfig.ui.tabs>>,
|
||||||
|
default: () => appConfig.ui.tabs
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
|
// TODO: Remove
|
||||||
|
const appConfig = useAppConfig()
|
||||||
|
|
||||||
|
const ui = computed<Partial<typeof appConfig.ui.tabs>>(() => defu({}, props.ui, appConfig.ui.tabs))
|
||||||
|
|
||||||
|
const itemRefs = ref<HTMLElement[]>([])
|
||||||
|
const markerRef = ref<HTMLElement>()
|
||||||
|
|
||||||
|
// Methods
|
||||||
|
|
||||||
|
function onChange (index) {
|
||||||
|
// @ts-ignore
|
||||||
|
const tab = itemRefs.value[index]?.$el
|
||||||
|
if (!tab) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
markerRef.value.style.top = `${tab.offsetTop}px`
|
||||||
|
markerRef.value.style.left = `${tab.offsetLeft}px`
|
||||||
|
markerRef.value.style.width = `${tab.offsetWidth}px`
|
||||||
|
markerRef.value.style.height = `${tab.offsetHeight}px`
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => onChange(props.defaultIndex))
|
||||||
|
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line vue/no-dupe-keys
|
||||||
|
ui,
|
||||||
|
itemRefs,
|
||||||
|
markerRef,
|
||||||
|
onChange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
1
src/runtime/types/index.d.ts
vendored
1
src/runtime/types/index.d.ts
vendored
@@ -6,4 +6,5 @@ export * from './command-palette'
|
|||||||
export * from './dropdown'
|
export * from './dropdown'
|
||||||
export * from './notification'
|
export * from './notification'
|
||||||
export * from './popper'
|
export * from './popper'
|
||||||
|
export * from './tabs'
|
||||||
export * from './vertical-navigation'
|
export * from './vertical-navigation'
|
||||||
|
|||||||
6
src/runtime/types/tabs.d.ts
vendored
Normal file
6
src/runtime/types/tabs.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export interface TabItem {
|
||||||
|
label: string
|
||||||
|
slot?: string
|
||||||
|
disabled?: boolean
|
||||||
|
content?: string
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user