mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat(ui): migrate Tabs component away from tailwind
This commit is contained in:
@@ -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
|
||||
})
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from './Tabs'
|
||||
Reference in New Issue
Block a user