feat(ui): migrate Tabs component away from tailwind

This commit is contained in:
melvinchia3636
2026-03-28 10:37:44 +08:00
parent 48515a306f
commit cba70ab340
4 changed files with 125 additions and 25 deletions

View File

@@ -0,0 +1,41 @@
import { style } from '@vanilla-extract/css'
import { createSprinkles } from '@vanilla-extract/sprinkles'
import { themeColorProperties, vars } from '@/system'
const sprinkles = createSprinkles(themeColorProperties)
export const tab = style({
cursor: 'pointer',
borderStyle: 'none',
borderBottomStyle: 'solid',
borderBottomWidth: '2px',
letterSpacing: '0.1em',
whiteSpace: 'nowrap',
textTransform: 'uppercase',
transition: 'all 0.2s'
})
export const activeTab = sprinkles({
color: { base: 'custom-500' },
borderColor: { base: 'custom-500' }
})
export const inactiveTab = sprinkles({
color: {
base: 'bg-400',
hover: 'bg-800',
dark: 'bg-500',
darkHover: 'bg-200'
},
borderColor: {
base: 'bg-400',
hover: 'bg-800',
dark: 'bg-500',
darkHover: 'bg-200'
}
})
export const amount = style({
fontSize: vars.fontSize.sm
})

View File

@@ -2,7 +2,9 @@ import type { Meta, StoryObj } from '@storybook/react-vite'
import { useState } from 'react'
import colors from 'tailwindcss/colors'
import Tabs from './Tabs'
import { Box } from '@components/primitives'
import Tabs from '../Tabs'
const meta = {
component: Tabs,
@@ -42,14 +44,14 @@ export const Default: Story = {
>('overview')
return (
<div className="w-[60vw]">
<Box width="60vw">
<Tabs
currentTab={currentTab}
enabled={['overview', 'settings', 'profile']}
items={BASIC_TABS}
onTabChange={setCurrentTab}
/>
</div>
</Box>
)
}
}
@@ -82,14 +84,14 @@ export const WithAmounts: Story = {
>('all')
return (
<div className="w-[60vw]">
<Box width="60vw">
<Tabs
currentTab={active}
enabled={['all', 'active', 'completed', 'archived']}
items={TABS_WITH_AMOUNTS}
onTabChange={setActive}
/>
</div>
</Box>
)
}
}
@@ -119,14 +121,14 @@ export const WithColors: Story = {
const [active, setActive] = useState<'red' | 'green' | 'blue'>('red')
return (
<div className="w-[60vw]">
<Box width="60vw">
<Tabs
currentTab={active}
enabled={['red', 'green', 'blue']}
items={COLORED_TABS}
onTabChange={setActive}
/>
</div>
</Box>
)
}
}
@@ -147,14 +149,44 @@ export const PartiallyEnabled: Story = {
)
return (
<div className="w-[60vw]">
<Box width="60vw">
<Tabs
currentTab={active}
enabled={['overview', 'settings', 'profile']}
items={BASIC_TABS}
onTabChange={setActive}
/>
</div>
</Box>
)
}
}
/**
* A large number of tabs to demonstrate horizontal scrolling behavior.
*/
export const ALotOfTabs: Story = {
args: {
items: [],
enabled: [],
currentTab: 'tab1',
onTabChange: () => {}
},
render: () => {
const [active, setActive] = useState<string>('tab1')
return (
<Box width="60vw">
<Tabs
currentTab={active}
enabled={Array.from({ length: 20 }, (_, i) => `tab${i + 1}`)}
items={Array.from({ length: 20 }, (_, i) => ({
id: `tab${i + 1}`,
name: `Tab ${i + 1}`,
icon: 'tabler:star'
}))}
onTabChange={setActive}
/>
</Box>
)
}
}

View File

@@ -1,6 +1,10 @@
import { Icon } from '@iconify/react'
import clsx from 'clsx'
import { Box, Flex, Text } from '@components/primitives'
import * as styles from './Tabs.css'
interface TabsProps<
T,
TKey = T extends ReadonlyArray<{ readonly id: infer U }> ? U : never
@@ -31,20 +35,27 @@ function Tabs<
TKey = T extends ReadonlyArray<{ readonly id: infer U }> ? U : never
>({ items, enabled, currentTab, onTabChange, className }: TabsProps<T, TKey>) {
return (
<div className={clsx('flex flex-wrap items-center gap-y-2', className)}>
<Flex align="center" className={className} gapY="sm" wrap="wrap">
{items
.filter(({ id }) => enabled.includes(id as TKey))
.map(({ name, icon, id, color }) => (
<button
<Flex
key={id}
align="center"
as="button"
bg="transparent"
className={clsx(
'flex flex-1 cursor-pointer items-center justify-center gap-2 border-b-2 p-4 tracking-widest whitespace-nowrap uppercase transition-all',
currentTab === id
? `${
!color ? 'border-custom-500 text-custom-500' : ''
} font-medium`
: 'border-bg-400 text-bg-400 hover:border-bg-800 hover:text-bg-800 dark:border-bg-500 dark:text-bg-500 dark:hover:border-bg-200 dark:hover:text-bg-200'
styles.tab,
currentTab !== id
? styles.inactiveTab
: !color
? styles.activeTab
: undefined
)}
flex="1 1 0%"
gap="sm"
justify="center"
p="md"
style={
color && currentTab === id
? {
@@ -57,16 +68,31 @@ function Tabs<
onTabChange(id as TKey)
}}
>
{icon && <Icon className="size-5 shrink-0" icon={icon} />}
<span className="block">{name}</span>
{items.find(item => item.name === name)?.amount !== undefined && (
<span className="hidden text-sm sm:block">
({items.find(item => item.name === name)?.amount})
</span>
{icon && (
<Icon
icon={icon}
style={{ width: '1.25rem', height: '1.25rem', flexShrink: 0 }}
/>
)}
</button>
<Text
as="span"
display="block"
weight={currentTab === id ? 'medium' : 'normal'}
>
{name}
</Text>
{items.find(item => item.name === name)?.amount !== undefined && (
<Box
as="span"
className={styles.amount}
display={{ base: 'none', sm: 'block' }}
>
({items.find(item => item.name === name)?.amount})
</Box>
)}
</Flex>
))}
</div>
</Flex>
)
}

View File

@@ -0,0 +1 @@
export { default } from './Tabs'