diff --git a/docs/app/components/content/examples/tabs/TabsOverflowCollapseExample.vue b/docs/app/components/content/examples/tabs/TabsOverflowCollapseExample.vue new file mode 100644 index 0000000000..0a8b027a9a --- /dev/null +++ b/docs/app/components/content/examples/tabs/TabsOverflowCollapseExample.vue @@ -0,0 +1,22 @@ + + + diff --git a/docs/app/components/content/examples/tabs/TabsOverflowScrollExample.vue b/docs/app/components/content/examples/tabs/TabsOverflowScrollExample.vue new file mode 100644 index 0000000000..9df404f919 --- /dev/null +++ b/docs/app/components/content/examples/tabs/TabsOverflowScrollExample.vue @@ -0,0 +1,23 @@ + + + diff --git a/docs/app/components/content/examples/tabs/TabsOverflowWrapExample.vue b/docs/app/components/content/examples/tabs/TabsOverflowWrapExample.vue new file mode 100644 index 0000000000..2c17996b0c --- /dev/null +++ b/docs/app/components/content/examples/tabs/TabsOverflowWrapExample.vue @@ -0,0 +1,23 @@ + + + diff --git a/docs/content/docs/2.components/tabs.md b/docs/content/docs/2.components/tabs.md index 720e79a6cd..c488bd318a 100644 --- a/docs/content/docs/2.components/tabs.md +++ b/docs/content/docs/2.components/tabs.md @@ -217,6 +217,30 @@ props: --- :: +### Overflow :badge{label="Soon" class="align-text-top"} + +Use the `overflow` prop to control how the tab list handles items that don't fit in the available space: + +- `scroll`{lang="ts-type"} enables scrolling along the list axis. +- `wrap`{lang="ts-type"} allows tabs to wrap onto multiple lines. +- `collapse`{lang="ts-type"} hides overflowing tabs behind a **More** dropdown. + +When omitted, no overflow handling is applied. + +### Scroll + +:component-example{name="tabs-overflow-scroll-example"} + +### Wrap + +:component-example{name="tabs-overflow-wrap-example"} + +### Collapse + +Use the `more-label` and `more-icon` props to customize the overflow trigger. You can also use the `#more` slot to fully customize it. + +:component-example{name="tabs-overflow-collapse-example"} + ## Examples ### Control active item diff --git a/playgrounds/nuxt/app/pages/components/tabs.vue b/playgrounds/nuxt/app/pages/components/tabs.vue index df0c013107..1322d1f57a 100644 --- a/playgrounds/nuxt/app/pages/components/tabs.vue +++ b/playgrounds/nuxt/app/pages/components/tabs.vue @@ -4,6 +4,7 @@ import theme from '#build/ui/tabs' const colors = Object.keys(theme.variants.color) const variants = Object.keys(theme.variants.variant) const orientations = Object.keys(theme.variants.orientation) +const overflows = ['none', ...Object.keys(theme.variants.overflow)] const sizes = Object.keys(theme.variants.size) const attrs = reactive({ @@ -13,6 +14,7 @@ const attrs = reactive({ }) const orientation = ref('horizontal' as keyof typeof theme.variants.orientation) +const overflow = ref<'none' | keyof typeof theme.variants.overflow>('none') const items = [{ label: 'Tab1', @@ -31,6 +33,21 @@ const items = [{ slot: 'custom' as const, badge: '300' }] + +const manyItems = [ + { label: 'Overview', icon: 'i-lucide-layout-dashboard', content: 'Overview content' }, + { label: 'Activity', icon: 'i-lucide-activity', content: 'Activity content' }, + { label: 'Settings', icon: 'i-lucide-settings', content: 'Settings content' }, + { label: 'Members', icon: 'i-lucide-users', content: 'Members content' }, + { label: 'Billing', icon: 'i-lucide-credit-card', content: 'Billing content' }, + { label: 'Integrations', icon: 'i-lucide-plug', content: 'Integrations content' }, + { label: 'Notifications', icon: 'i-lucide-bell', content: 'Notifications content' }, + { label: 'Security', icon: 'i-lucide-shield', content: 'Security content' }, + { label: 'API Keys', icon: 'i-lucide-key', content: 'API Keys content' }, + { label: 'Logs', icon: 'i-lucide-scroll-text', content: 'Logs content' } +] + +const overflowProp = computed(() => overflow.value === 'none' ? undefined : overflow.value) + + diff --git a/src/runtime/components/Tabs.vue b/src/runtime/components/Tabs.vue index 3ec9724610..496f4e0c02 100644 --- a/src/runtime/components/Tabs.vue +++ b/src/runtime/components/Tabs.vue @@ -4,7 +4,7 @@ import type { ComponentPublicInstance, VNode } from 'vue' import type { TabsRootProps, TabsRootEmits } from 'reka-ui' import type { AppConfig } from '@nuxt/schema' import theme from '#build/ui/tabs' -import type { AvatarProps, BadgeProps, IconProps } from '../types' +import type { AvatarProps, BadgeProps, IconProps, DropdownMenuItem } from '../types' import type { DynamicSlots, GetItemKeys } from '../types/utils' import type { ComponentConfig } from '../types/tv' @@ -56,10 +56,30 @@ export interface TabsProps extends Pick extends Pick {} +export interface TabsEmits extends TabsRootEmits { + 'update:modelValue': [value: string | number] +} type SlotProps = (props: { item: T, index: number, ui: Tabs['ui'] }) => VNode[] @@ -90,12 +112,13 @@ export type TabsSlots = { 'content'?: SlotProps 'list-leading'?(props?: {}): VNode[] 'list-trailing'?(props?: {}): VNode[] + 'more'?(props: { items: T[], isActive: boolean, ui: Tabs['ui'] }): VNode[] } & DynamicSlots