Skip to content

Commit

Permalink
feat(Tabs): handle color and variant props
Browse files Browse the repository at this point in the history
Resolves #116
  • Loading branch information
benjamincanac committed Jun 5, 2024
1 parent 57b32ca commit 82ffc1e
Show file tree
Hide file tree
Showing 5 changed files with 247 additions and 59 deletions.
32 changes: 24 additions & 8 deletions playground/pages/tabs.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,14 @@
<script setup lang="ts">
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 color = ref(theme.defaultVariants.color)
const variant = ref(theme.defaultVariants.variant)
const orientation = ref('horizontal' as const)
const items = [{
label: 'Tab1',
avatar: {
Expand All @@ -18,15 +28,21 @@ const items = [{
</script>

<template>
<div class="flex gap-4">
<UTabs :items="[{ label: 'Monthly' }, { label: 'Yearly' }]" :content="false" />
<div class="flex flex-col items-center gap-12">
<div class="flex items-center gap-2">
<USelect v-model="color" :items="colors" placeholder="Color" />
<USelect v-model="variant" :items="variants" placeholder="Variant" />
<USelect v-model="orientation" :items="orientations" placeholder="Orientation" />
</div>

<UTabs :items="items" class="w-96">
<template #custom="{ item }">
<span class="text-gray-500 dark:text-gray-400">Custom: {{ item.content }}</span>
</template>
</UTabs>
<div class="flex gap-4">
<UTabs :color="color" :variant="variant" :orientation="orientation" :items="[{ label: 'Monthly' }, { label: 'Yearly' }]" :content="false" />

<UTabs :items="items" orientation="vertical" />
<UTabs :color="color" :variant="variant" :orientation="orientation" :items="items" class="w-96">
<template #custom="{ item }">
<span class="text-gray-500 dark:text-gray-400">Custom: {{ item.content }}</span>
</template>
</UTabs>
</div>
</div>
</template>
12 changes: 10 additions & 2 deletions src/runtime/components/Tabs.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script lang="ts">
import { tv } from 'tailwind-variants'
import { tv, type VariantProps } from 'tailwind-variants'
import type { TabsRootProps, TabsRootEmits, TabsContentProps, TabsTriggerProps } from 'radix-vue'
import type { AppConfig } from '@nuxt/schema'
import _appConfig from '#build/app.config'
Expand All @@ -19,8 +19,12 @@ export interface TabsItem extends Partial<Pick<TabsTriggerProps, 'disabled' | 'v
content?: string
}
type TabsVariants = VariantProps<typeof tabs>
export interface TabsProps<T> extends Omit<TabsRootProps, 'asChild'> {
items?: T[]
color?: TabsVariants['color']
variant?: TabsVariants['variant']
content?: boolean | Omit<TabsContentProps, 'asChild' | 'value'>
class?: any
ui?: Partial<typeof tabs.slots>
Expand Down Expand Up @@ -55,7 +59,11 @@ const slots = defineSlots<TabsSlots<T>>()
const rootProps = useForwardPropsEmits(reactivePick(props, 'as', 'defaultValue', 'orientation', 'activationMode', 'modelValue'), emits)
const contentProps = toRef(() => defu(props.content || {}, { forceMount: true }) as TabsContentProps)
const ui = computed(() => tv({ extend: tabs, slots: props.ui })({ orientation: props.orientation }))
const ui = computed(() => tv({ extend: tabs, slots: props.ui })({
color: props.color,
variant: props.variant,
orientation: props.orientation
}))
</script>

<template>
Expand Down
95 changes: 88 additions & 7 deletions src/theme/tabs.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,107 @@
export default {
export default (config: { colors: string[] }) => ({
slots: {
root: 'flex items-center gap-2',
list: 'relative flex rounded-lg bg-gray-50 dark:bg-gray-800 p-1 group',
indicator: 'absolute transition-[translate,width] duration-200 bg-white dark:bg-gray-900 rounded-md shadow-sm',
trigger: 'relative inline-flex items-center justify-center gap-1.5 shrink-0 flex-1 text-gray-500 data-[state=active]:text-gray-900 dark:text-gray-400 dark:data-[state=active]:text-white px-3 py-1.5 text-sm font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75 transition-colors ease-out focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus:outline-none',
list: 'relative flex p-1 group',
indicator: 'absolute transition-[translate,width] duration-200',
trigger: 'relative inline-flex items-center justify-center gap-1.5 shrink-0 flex-1 px-3 py-1.5 data-[state=inactive]:text-gray-500 dark:data-[state=inactive]:text-gray-400 hover:data-[state=inactive]:text-gray-700 dark:hover:data-[state=inactive]:text-gray-200 text-sm font-medium rounded-md disabled:cursor-not-allowed disabled:opacity-75 transition-colors ease-out focus-visible:ring-2 focus-visible:ring-primary-500 dark:focus-visible:ring-primary-400 focus:outline-none',
content: 'focus:outline-none',
leadingIcon: 'shrink-0 size-5',
leadingAvatar: 'shrink-0',
label: 'truncate'
},
variants: {
color: {
...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
white: '',
black: ''
},
variant: {
pill: {
list: 'bg-gray-50 dark:bg-gray-800 rounded-lg',
indicator: 'rounded-md shadow-sm'
},
link: {
list: 'border-gray-200 dark:border-gray-800',
indicator: 'rounded-full'
}
},
orientation: {
horizontal: {
root: 'flex-col',
list: 'w-full',
indicator: 'left-0 inset-y-1 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position]'
indicator: 'left-0 w-[--radix-tabs-indicator-size] translate-x-[--radix-tabs-indicator-position]'
},
vertical: {
list: 'flex-col items-center',
indicator: 'top-0 inset-x-1 h-[--radix-tabs-indicator-size] translate-y-[--radix-tabs-indicator-position]',
indicator: 'top-0 h-[--radix-tabs-indicator-size] translate-y-[--radix-tabs-indicator-position]',
content: 'flex-1'
}
}
},
compoundVariants: [{
orientation: 'horizontal',
variant: 'pill',
class: {
indicator: 'inset-y-1'
}
}, {
orientation: 'horizontal',
variant: 'link',
class: {
list: 'border-b',
indicator: '-bottom-px h-px'
}
}, {
orientation: 'vertical',
variant: 'pill',
class: {
indicator: 'inset-x-1'
}
}, {
orientation: 'vertical',
variant: 'link',
class: {
list: 'border-l',
indicator: '-left-px w-px'
}
}, ...config.colors.map((color: string) => ({
color,
variant: 'pill',
class: {
indicator: `bg-${color}-500 dark:bg-${color}-400`,
trigger: 'data-[state=active]:text-white'
}
})), {
color: 'white',
variant: 'pill',
class: {
indicator: 'bg-white dark:bg-gray-900',
trigger: 'data-[state=active]:text-gray-900 dark:data-[state=active]:text-white'
}
}, {
color: 'black',
variant: 'pill',
class: {
indicator: 'bg-gray-900 dark:bg-white',
trigger: 'data-[state=active]:text-white dark:data-[state=active]:text-gray-900'
}
}, ...config.colors.map((color: string) => ({
color,
variant: 'link',
class: {
indicator: `bg-${color}-500 dark:bg-${color}-400`,
trigger: `data-[state=active]:text-${color}-500 dark:data-[state=active]:text-${color}-400`
}
})), {
color: ['white', 'black'],
variant: 'link',
class: {
indicator: 'bg-gray-900 dark:bg-white',
trigger: 'data-[state=active]:text-gray-900 dark:data-[state=active]:text-white'
}
}],
defaultVariants: {
color: 'white',
variant: 'pill'
}
}
})
6 changes: 6 additions & 0 deletions test/components/Tabs.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
import { describe, it, expect } from 'vitest'
import Tabs, { type TabsProps, type TabsSlots } from '../../src/runtime/components/Tabs.vue'
import ComponentRender from '../component-render'
import theme from '#build/ui/tabs'

describe('Tabs', () => {
const colors = Object.keys(theme.variants.color) as any
const variants = Object.keys(theme.variants.variant) as any

const items = [{
label: 'Tab1',
avatar: {
Expand All @@ -28,6 +32,8 @@ describe('Tabs', () => {
['with modelValue', { props: { ...props, modelValue: '1' } }],
['with defaultValue', { props: { ...props, defaultValue: '1' } }],
['with orientation vertical', { props: { ...props, orientation: 'vertical' as const } }],
...colors.map((color: string) => [`with color ${color}`, { props: { ...props, color } }]),
...variants.map((variant: string) => [`with variant ${variant}`, { props: { ...props, variant } }]),
['without content', { props: { ...props, content: false } }],
['with class', { props: { ...props, class: 'w-96' } }],
['with ui', { props: { ...props, ui: { content: 'w-full ring ring-gray-200 dark:ring-gray-800' } } }],
Expand Down
Loading

0 comments on commit 82ffc1e

Please sign in to comment.