feat(discover): enhance Discover page with Tailwind CSS integration and routing improvements

- Added Tailwind CSS import to the entry point for styling.
- Updated the ThemeProvider to dynamically apply Tailwind themes based on user selection.
- Refactored Discover page to utilize new ROUTERS structure for better routing management.
- Simplified category handling in useDiscoverCategories hook by leveraging ROUTERS_ENTRIES.
- Introduced InternalCategory interface for better type management in Discover components.
- Cleaned up unused code and comments for improved readability.
This commit is contained in:
MyPrototypeWhat 2025-07-31 12:46:25 +08:00
parent 0d6156cc1b
commit 833ea86e82
15 changed files with 151 additions and 155 deletions

View File

@ -49,6 +49,7 @@ body {
font-family: var(--font-family); font-family: var(--font-family);
text-rendering: optimizeLegibility; text-rendering: optimizeLegibility;
transition: background-color 0.3s linear; transition: background-color 0.3s linear;
background-color: unset;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;

View File

@ -23,6 +23,12 @@ interface ThemeProviderProps extends PropsWithChildren {
defaultTheme?: ThemeMode defaultTheme?: ThemeMode
} }
const tailwindThemeChange = (theme: ThemeMode) => {
const root = window.document.documentElement
root.classList.remove('light', 'dark')
root.classList.add(theme)
}
export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => { export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
// 用户设置的主题 // 用户设置的主题
const { theme: settedTheme, setTheme: setSettedTheme } = useSettings() const { theme: settedTheme, setTheme: setSettedTheme } = useSettings()
@ -64,7 +70,7 @@ export const ThemeProvider: React.FC<ThemeProviderProps> = ({ children }) => {
useEffect(() => { useEffect(() => {
window.api.setTheme(settedTheme) window.api.setTheme(settedTheme)
// tailwindThemeChange(settedTheme) tailwindThemeChange(settedTheme)
}, [settedTheme]) }, [settedTheme])
return ( return (

View File

@ -1,5 +1,5 @@
import './assets/styles/tailwind.css'
import './assets/styles/index.scss' import './assets/styles/index.scss'
import './assets/styles/tailwind.css'
import '@ant-design/v5-patch-for-react-19' import '@ant-design/v5-patch-for-react-19'
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client'

View File

@ -1,29 +1,29 @@
import React, { Suspense } from 'react' import React, { Suspense } from 'react'
import { Navigate, Route, Routes, useLocation } from 'react-router-dom' import { Navigate, Route, Routes, useLocation } from 'react-router-dom'
import { discoverRouters, InternalCategory } from '../routers' import { ROUTERS } from '../routers'
import { InternalCategory } from '../type'
export interface DiscoverContentProps { export interface DiscoverContentProps {
activeTabId: string // This should be one of the CherryStoreType values, e.g., "Assistant" activeTabId: string
// selectedSubcategoryId: string
currentCategory: InternalCategory | undefined currentCategory: InternalCategory | undefined
} }
const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => { const DiscoverContent: React.FC<DiscoverContentProps> = ({ activeTabId, currentCategory }) => {
const location = useLocation() // To see the current path for debugging or more complex logic const location = useLocation()
if (!currentCategory || !activeTabId) { if (!currentCategory || !activeTabId) {
return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div> return <div className="p-4 text-center">Loading: Category or Tab ID missing...</div>
} }
if (!activeTabId && !location.pathname.startsWith('/discover/')) { if (!activeTabId && !location.pathname.startsWith('/discover/')) {
return <Navigate to="/discover/assistant?subcategory=all" replace /> // Fallback redirect, adjust as needed return <Navigate to="/discover/assistant?subcategory=all" replace />
} }
return ( return (
<Suspense fallback={<div>Loading...</div>}> <Suspense fallback={null}>
<Routes> <Routes>
{discoverRouters.map((_Route) => { {ROUTERS.map((_Route) => {
if (!_Route.component) return null if (!_Route.component) return null
return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} /> return <Route key={_Route.path} path={`/${_Route.path}`} element={<_Route.component />} />
})} })}

View File

@ -9,7 +9,7 @@ import {
SidebarProvider SidebarProvider
} from '@renderer/ui/sidebar' } from '@renderer/ui/sidebar'
import { InternalCategory } from '../hooks/useDiscoverCategories' import { InternalCategory } from '../type'
interface DiscoverSidebarProps { interface DiscoverSidebarProps {
activeCategory: InternalCategory | undefined activeCategory: InternalCategory | undefined

View File

@ -1,18 +1,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { CATEGORY_REGISTRY, InternalCategory } from '../routers' import { ROUTERS, ROUTERS_ENTRIES } from '../routers'
// 导出接口供其他文件使用
export type { InternalCategory }
// Helper to find category by path
const findCategoryByPath = (path: string | undefined): InternalCategory | undefined =>
CATEGORY_REGISTRY.find((cat) => cat.path === path)
// Helper to find category by id (activeTab)
const findCategoryById = (id: string | undefined): InternalCategory | undefined =>
CATEGORY_REGISTRY.find((cat) => cat.id === id)
export function useDiscoverCategories() { export function useDiscoverCategories() {
const [activeTab, setActiveTab] = useState<string>('') const [activeTab, setActiveTab] = useState<string>('')
@ -34,8 +23,8 @@ export function useDiscoverCategories() {
// 处理基础路径重定向 // 处理基础路径重定向
if (location.pathname === '/discover' || location.pathname === '/discover/') { if (location.pathname === '/discover' || location.pathname === '/discover/') {
if (CATEGORY_REGISTRY.length > 0) { if (ROUTERS.length > 0) {
const firstCategory = CATEGORY_REGISTRY[0] const firstCategory = ROUTERS[0]
navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true }) navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true })
} }
return return
@ -46,14 +35,14 @@ export function useDiscoverCategories() {
// 如果没有 category 参数,尝试从路径推断 // 如果没有 category 参数,尝试从路径推断
if (!targetCategoryId && currentCategoryPath) { if (!targetCategoryId && currentCategoryPath) {
const categoryFromPath = findCategoryByPath(currentCategoryPath) const categoryFromPath = ROUTERS_ENTRIES[currentCategoryPath]
targetCategoryId = categoryFromPath?.id || null targetCategoryId = categoryFromPath?.id || null
} }
// 处理无效分类重定向 // 处理无效分类重定向
if (!targetCategoryId || !findCategoryById(targetCategoryId)) { if (!targetCategoryId || !ROUTERS_ENTRIES[targetCategoryId]) {
if (CATEGORY_REGISTRY.length > 0) { if (ROUTERS.length > 0) {
const firstCategory = CATEGORY_REGISTRY[0] const firstCategory = ROUTERS[0]
navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true }) navigate(`/discover/${firstCategory.path}?category=${firstCategory.id}&subcategory=all`, { replace: true })
} }
return return
@ -74,7 +63,7 @@ export function useDiscoverCategories() {
}, [location.pathname, location.search, navigate]) // 故意不包含 activeTab 和 selectedSubcategory 以避免重复渲染 }, [location.pathname, location.search, navigate]) // 故意不包含 activeTab 和 selectedSubcategory 以避免重复渲染
const currentCategory = useMemo(() => { const currentCategory = useMemo(() => {
return findCategoryById(activeTab) return ROUTERS_ENTRIES[activeTab]
}, [activeTab]) }, [activeTab])
// 优化的 Tab 选择处理,使用 useCallback 避免重复渲染 // 优化的 Tab 选择处理,使用 useCallback 避免重复渲染
@ -83,7 +72,7 @@ export function useDiscoverCategories() {
(tabId: string) => { (tabId: string) => {
if (activeTab === tabId) return // 如果已经是当前 tab直接返回 if (activeTab === tabId) return // 如果已经是当前 tab直接返回
const categoryToSelect = findCategoryById(tabId) const categoryToSelect = ROUTERS_ENTRIES[tabId]
if (categoryToSelect?.path) { if (categoryToSelect?.path) {
isUserNavigationRef.current = true isUserNavigationRef.current = true
navigate(`/discover/${categoryToSelect.path}?category=${tabId}&subcategory=all`) navigate(`/discover/${categoryToSelect.path}?category=${tabId}&subcategory=all`)
@ -97,7 +86,7 @@ export function useDiscoverCategories() {
(subcategoryId: string) => { (subcategoryId: string) => {
if (selectedSubcategory === subcategoryId) return // 如果已经是当前子分类,直接返回 if (selectedSubcategory === subcategoryId) return // 如果已经是当前子分类,直接返回
const currentCatDetails = findCategoryById(activeTab) const currentCatDetails = ROUTERS_ENTRIES[activeTab]
if (currentCatDetails?.path) { if (currentCatDetails?.path) {
isUserNavigationRef.current = true isUserNavigationRef.current = true
navigate(`/discover/${currentCatDetails.path}?category=${activeTab}&subcategory=${subcategoryId}`) navigate(`/discover/${currentCatDetails.path}?category=${activeTab}&subcategory=${subcategoryId}`)
@ -107,7 +96,6 @@ export function useDiscoverCategories() {
) )
return { return {
categories: CATEGORY_REGISTRY, // 直接返回静态注册表
activeTab, activeTab,
selectedSubcategory, selectedSubcategory,
currentCategory, currentCategory,

View File

@ -6,19 +6,14 @@ import { useTranslation } from 'react-i18next'
import DiscoverMain from './components/DiscoverMain' import DiscoverMain from './components/DiscoverMain'
import DiscoverSidebar from './components/DiscoverSidebar' import DiscoverSidebar from './components/DiscoverSidebar'
import { useDiscoverCategories } from './hooks/useDiscoverCategories' import { useDiscoverCategories } from './hooks/useDiscoverCategories'
import { ROUTERS } from './routers'
export default function DiscoverPage() { export default function DiscoverPage() {
const { t } = useTranslation() const { t } = useTranslation()
const { categories, activeTab, selectedSubcategory, currentCategory, handleSelectTab, handleSelectSubcategory } = const { activeTab, selectedSubcategory, currentCategory, handleSelectTab, handleSelectSubcategory } =
useDiscoverCategories() useDiscoverCategories()
// 使用 useMemo 优化 tabs 数据,避免每次渲染都创建新数组 const tabs = useMemo(() => ROUTERS.map((router) => ({ id: router.id, label: router.title })), [])
const vercelTabsData = useMemo(() => {
return categories.map((category) => ({
id: category.id,
label: category.title
}))
}, [categories])
return ( return (
<div className="h-full w-full"> <div className="h-full w-full">
@ -29,9 +24,9 @@ export default function DiscoverPage() {
</NavbarCenter> </NavbarCenter>
</Navbar> </Navbar>
{categories.length > 0 && ( {ROUTERS.length > 0 && (
<div className="px-4 py-2"> <div className="p-2 pl-0">
<Tabs tabs={vercelTabsData} onTabChange={handleSelectTab} /> <Tabs tabs={tabs} activeTab={activeTab} onTabChange={handleSelectTab} />
</div> </div>
)} )}
@ -45,16 +40,9 @@ export default function DiscoverPage() {
/> />
</div> </div>
)} )}
{/* {!currentCategory && categories.length > 0 && (
<div className="w-64 flex-shrink-0 border-r p-4 text-muted-foreground">Select a category...</div>
)} */}
<main className="w-full overflow-hidden"> <main className="w-full overflow-hidden">
<DiscoverMain <DiscoverMain activeTabId={activeTab} currentCategory={currentCategory} />
activeTabId={activeTab}
// selectedSubcategoryId={selectedSubcategory}
currentCategory={currentCategory}
/>
</main> </main>
</div> </div>
</div> </div>

View File

@ -185,7 +185,7 @@ const AgentsPage: FC = () => {
{/* <Navbar> */} {/* <Navbar> */}
{/* <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> */} {/* <NavbarCenter style={{ borderRight: 'none', justifyContent: 'space-between' }}> */}
{/* {t('agents.title')} */} {/* {t('agents.title')} */}
<div className="p-4"> {/* <div className="flex justify-center p-2">
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
className="nodrag" className="nodrag"
@ -201,8 +201,7 @@ const AgentsPage: FC = () => {
onPressEnter={handleSearch} onPressEnter={handleSearch}
onBlur={handleSearchInputBlur} onBlur={handleSearchInputBlur}
/> />
<div style={{ width: 80 }} /> </div> */}
</div>
{/* </NavbarCenter> */} {/* </NavbarCenter> */}
{/* </Navbar> */} {/* </Navbar> */}

View File

@ -1,8 +1,6 @@
import { Navbar, NavbarMain } from '@renderer/components/app/Navbar'
import App from '@renderer/components/MinApp/MinApp' import App from '@renderer/components/MinApp/MinApp'
import Scrollbar from '@renderer/components/Scrollbar' import Scrollbar from '@renderer/components/Scrollbar'
import { useMinapps } from '@renderer/hooks/useMinapps' import { useMinapps } from '@renderer/hooks/useMinapps'
import { useNavbarPosition } from '@renderer/hooks/useSettings'
import { Button, Input } from 'antd' import { Button, Input } from 'antd'
import { Search, SettingsIcon } from 'lucide-react' import { Search, SettingsIcon } from 'lucide-react'
import React, { FC, useState } from 'react' import React, { FC, useState } from 'react'
@ -16,7 +14,7 @@ const AppsPage: FC = () => {
const { t } = useTranslation() const { t } = useTranslation()
const [search, setSearch] = useState('') const [search, setSearch] = useState('')
const { minapps } = useMinapps() const { minapps } = useMinapps()
const { isTopNavbar } = useNavbarPosition() // const { isTopNavbar } = useNavbarPosition()
const filteredApps = search const filteredApps = search
? minapps.filter( ? minapps.filter(
@ -37,40 +35,40 @@ const AppsPage: FC = () => {
return ( return (
<Container onContextMenu={handleContextMenu}> <Container onContextMenu={handleContextMenu}>
<Navbar> {/* <Navbar> */}
<NavbarMain> {/* <NavbarMain> */}
{/* {t('minapp.title')} */} {/* {t('minapp.title')} */}
{/* <div className="p-2"> */} <div className="p-2">
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
className="nodrag" className="nodrag"
style={{ style={{
width: '30%', width: '30%',
height: 28, height: 28,
borderRadius: 15, borderRadius: 15,
position: 'absolute', position: 'absolute',
left: '50vw', left: '50vw',
transform: 'translateX(-50%)' transform: 'translateX(-50%)'
}} }}
size="small" size="small"
variant="filled" variant="filled"
suffix={<Search size={18} />} suffix={<Search size={18} />}
value={search} value={search}
onChange={(e) => setSearch(e.target.value)} onChange={(e) => setSearch(e.target.value)}
/> />
<Button <Button
type="text" type="text"
className="nodrag" className="nodrag"
icon={<SettingsIcon size={18} color="var(--color-text-2)" />} icon={<SettingsIcon size={18} color="var(--color-text-2)" />}
onClick={MinappSettingsPopup.show} onClick={MinappSettingsPopup.show}
/> />
{/* </div> */} </div>
</NavbarMain> {/* </NavbarMain> */}
</Navbar> {/* </Navbar> */}
<ContentContainer id="content-container"> <ContentContainer id="content-container">
<MainContainer> <MainContainer>
<RightContainer> <RightContainer>
{isTopNavbar && ( {/* {isTopNavbar && (
<HeaderContainer> <HeaderContainer>
<Input <Input
placeholder={t('common.search')} placeholder={t('common.search')}
@ -88,7 +86,7 @@ const AppsPage: FC = () => {
onClick={() => MinappSettingsPopup.show()} onClick={() => MinappSettingsPopup.show()}
/> />
</HeaderContainer> </HeaderContainer>
)} )} */}
<AppsContainerWrapper> <AppsContainerWrapper>
<AppsContainer style={{ height: containerHeight }}> <AppsContainer style={{ height: containerHeight }}>
{filteredApps.map((app) => ( {filteredApps.map((app) => (
@ -119,15 +117,15 @@ const ContentContainer = styled.div`
height: 100%; height: 100%;
` `
const HeaderContainer = styled.div` // const HeaderContainer = styled.div`
display: flex; // display: flex;
flex-direction: row; // flex-direction: row;
justify-content: center; // justify-content: center;
align-items: center; // align-items: center;
height: 60px; // height: 60px;
width: 100%; // width: 100%;
gap: 10px; // gap: 10px;
` // `
const MainContainer = styled.div` const MainContainer = styled.div`
display: flex; display: flex;

View File

@ -2,18 +2,22 @@ import i18n from '@renderer/i18n'
import { CherryStoreType } from '@renderer/types/cherryStore' import { CherryStoreType } from '@renderer/types/cherryStore'
import { lazy } from 'react' import { lazy } from 'react'
export const discoverRouters = [ export const ROUTERS = [
{ {
id: CherryStoreType.ASSISTANT, id: CherryStoreType.ASSISTANT,
title: i18n.t('assistants.title'), title: i18n.t('assistants.title'),
path: 'assistant', path: 'assistant',
component: lazy(() => import('./pages/agents/AgentsPage')) component: lazy(() => import('./pages/agents/AgentsPage')),
hasSidebar: false, // 目前都没有侧边栏
items: [{ id: 'all', name: `All ${i18n.t('assistants.title')}` }] // 预设 "All" 子分类
}, },
{ {
id: CherryStoreType.MINI_APP, id: CherryStoreType.MINI_APP,
title: i18n.t('minapp.title'), title: i18n.t('minapp.title'),
path: 'mini-app', path: 'mini-app',
component: lazy(() => import('./pages/minapps/MinAppsPage')) component: lazy(() => import('./pages/minapps/MinAppsPage')),
hasSidebar: false, // 目前都没有侧边栏
items: [{ id: 'all', name: `All ${i18n.t('minapp.title')}` }] // 预设 "All" 子分类
} }
// { // {
// id: CherryStoreType.TRANSLATE, // id: CherryStoreType.TRANSLATE,
@ -43,20 +47,10 @@ export const discoverRouters = [
// } // }
] ]
// 静态注册表 - 避免每次渲染都重新生成 export const ROUTERS_ENTRIES = ROUTERS.reduce(
export interface InternalCategory { (acc, { id, ...rest }) => {
id: string acc[id] = rest
title: string return acc
path: string },
hasSidebar?: boolean {} as Record<(typeof ROUTERS)[number]['id'], Omit<(typeof ROUTERS)[number], 'id'>>
items: Array<{ id: string; name: string; count?: number }> )
}
// 预生成的分类注册表
export const CATEGORY_REGISTRY: InternalCategory[] = discoverRouters.map((router) => ({
id: router.id,
title: router.title,
path: router.path,
hasSidebar: false, // 目前都没有侧边栏
items: [{ id: 'all', name: `All ${router.title}` }] // 预设 "All" 子分类
}))

View File

@ -0,0 +1,7 @@
export interface InternalCategory {
id: string
title: string
path: string
hasSidebar?: boolean
items: Array<{ id: string; name: string; count?: number }>
}

View File

@ -1,13 +1,13 @@
export enum CherryStoreType { export enum CherryStoreType {
ASSISTANT = 'Assistant', ASSISTANT = 'Assistant',
MINI_APP = 'Mini-App', MINI_APP = 'Mini-App'
KNOWLEDGE = 'Knowledge', // KNOWLEDGE = 'Knowledge',
MCP_SERVER = 'MCP-Server', // MCP_SERVER = 'MCP-Server',
MODEL_PROVIDER = 'Model-Provider', // MODEL_PROVIDER = 'Model-Provider',
AGENT = 'Agent', // AGENT = 'Agent',
TRANSLATE = 'Translate', // TRANSLATE = 'Translate',
PAINTINGS = 'Paintings', // PAINTINGS = 'Paintings',
FILES = 'Files' // FILES = 'Files'
} }
export interface SubCategoryItem { export interface SubCategoryItem {

View File

@ -1,6 +1,6 @@
import { cn } from '@renderer/utils' import { cn } from '@renderer/utils'
import * as React from 'react' import * as React from 'react'
import { useEffect, useRef, useState } from 'react' import { useEffect, useMemo, useRef, useState } from 'react'
interface Tab { interface Tab {
id: string id: string
@ -13,73 +13,86 @@ interface TabsProps extends React.HTMLAttributes<HTMLDivElement> {
onTabChange?: (tabId: string) => void onTabChange?: (tabId: string) => void
} }
const Tabs = ({ // 提取常用的性能优化类
const PERFORMANCE_CLASSES = 'will-change-transform [backface-visibility:hidden] [transform-style:preserve-3d]'
const TabsComponent = ({
ref, ref,
className, className,
tabs, tabs,
activeTab: _, activeTab,
onTabChange, onTabChange,
...props ...props
}: TabsProps & { ref?: React.RefObject<HTMLDivElement | null> }) => { }: TabsProps & { ref?: React.RefObject<HTMLDivElement | null> }) => {
const [hoveredIndex, setHoveredIndex] = useState<number | null>(null) const [hoveredIndex, setHoveredIndex] = useState<number | null>(null)
const [activeIndex, setActiveIndex] = useState(0) const [hoverStyle, setHoverStyle] = useState({ transform: 'translate3d(0px, 0px, 0px)', width: '0px' })
const [hoverStyle, setHoverStyle] = useState({}) const [activeStyle, setActiveStyle] = useState({ transform: 'translate3d(0px, 0px, 0px)', width: '0px' })
const [activeStyle, setActiveStyle] = useState({ left: '0px', width: '0px' })
const tabRefs = useRef<(HTMLDivElement | null)[]>([]) const tabRefs = useRef<(HTMLDivElement | null)[]>([])
const activeIndex = useMemo(() => {
if (activeTab) {
const index = tabs.findIndex((tab) => tab.id === activeTab)
return index !== -1 ? index : 0
}
return 0
}, [activeTab, tabs])
useEffect(() => { useEffect(() => {
if (hoveredIndex !== null) { if (hoveredIndex !== null) {
const hoveredElement = tabRefs.current[hoveredIndex] const hoveredElement = tabRefs.current[hoveredIndex]
if (hoveredElement) { if (hoveredElement) {
const { offsetLeft, offsetWidth } = hoveredElement const { offsetLeft, offsetWidth } = hoveredElement
setHoverStyle({ setHoverStyle({
left: `${offsetLeft}px`, transform: `translate3d(${offsetLeft}px, 0px, 0px)`,
width: `${offsetWidth}px` width: `${offsetWidth}px`
}) })
} }
} }
}, [hoveredIndex]) }, [hoveredIndex])
useEffect(() => {
const activeElement = tabRefs.current[activeIndex]
if (activeElement) {
const { offsetLeft, offsetWidth } = activeElement
setActiveStyle({
left: `${offsetLeft}px`,
width: `${offsetWidth}px`
})
}
}, [activeIndex])
useEffect(() => { useEffect(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const firstElement = tabRefs.current[0] const activeElement = tabRefs.current[activeIndex]
if (firstElement) { if (activeElement) {
const { offsetLeft, offsetWidth } = firstElement const { offsetLeft, offsetWidth } = activeElement
setActiveStyle({ setActiveStyle({
left: `${offsetLeft}px`, transform: `translate3d(${offsetLeft}px, 0px, 0px)`,
width: `${offsetWidth}px` width: `${offsetWidth}px`
}) })
} }
}) })
}, []) }, [activeIndex]) // 使用 translate3d 强制启用硬件加速
return ( return (
<div ref={ref} className={cn('relative', className)} {...props}> <div ref={ref} className={cn('relative', className)} {...props}>
<div className="relative"> <div className="relative">
{/* Hover Highlight */} {/* Hover Highlight */}
<div <div
className="absolute flex h-[30px] items-center rounded-[6px] bg-[#0e0f1114] transition-all duration-300 ease-out dark:bg-[#ffffff1a]" className={cn(
'absolute flex h-[30px] items-center rounded-[6px]',
'bg-[#0e0f1114] dark:bg-[#ffffff1a]',
'transition-all duration-300 ease-out',
PERFORMANCE_CLASSES,
hoveredIndex !== null ? 'opacity-100' : 'opacity-0'
)}
style={{ style={{
...hoverStyle, transform: hoverStyle.transform,
opacity: hoveredIndex !== null ? 1 : 0 width: hoverStyle.width
}} }}
/> />
{/* Active Indicator */} {/* Active Indicator */}
<div <div
className="absolute bottom-[-6px] h-[2px] bg-[#0e0f11] transition-all duration-300 ease-out dark:bg-white" className={cn(
style={activeStyle} 'absolute bottom-[-6px] h-[2px]',
'bg-[#0e0f11] dark:bg-white',
'transition-all duration-300 ease-out',
PERFORMANCE_CLASSES
)}
style={{
transform: activeStyle.transform,
width: activeStyle.width
}}
/> />
{/* Tabs */} {/* Tabs */}
@ -97,7 +110,6 @@ const Tabs = ({
onMouseEnter={() => setHoveredIndex(index)} onMouseEnter={() => setHoveredIndex(index)}
onMouseLeave={() => setHoveredIndex(null)} onMouseLeave={() => setHoveredIndex(null)}
onClick={() => { onClick={() => {
setActiveIndex(index)
onTabChange?.(tab.id) onTabChange?.(tab.id)
}}> }}>
<div className="flex h-full items-center justify-center text-sm leading-5 font-medium whitespace-nowrap"> <div className="flex h-full items-center justify-center text-sm leading-5 font-medium whitespace-nowrap">
@ -110,6 +122,9 @@ const Tabs = ({
</div> </div>
) )
} }
const Tabs = React.memo(TabsComponent)
Tabs.displayName = 'Tabs' Tabs.displayName = 'Tabs'
export { Tabs } export { Tabs }