diff --git a/app/components/common/Tabs.tsx b/app/components/common/Tabs.tsx index d9348dd1..60fd7893 100644 --- a/app/components/common/Tabs.tsx +++ b/app/components/common/Tabs.tsx @@ -1,4 +1,5 @@ -import { NavLink } from 'react-router'; +import { NavLink, useLocation } from 'react-router'; +import { useLayoutEffect, useRef, useState } from 'react'; export interface TabItem { label: string; @@ -13,50 +14,98 @@ interface TabsProps { className?: string; } +const SIDE_MARGIN_PX = 16; + export default function Tabs({ tabs, activeTab, onTabChange, className = "" }: TabsProps) { + const location = useLocation(); + const hasPath = tabs.some((t) => t.path); + const currentActiveTab = hasPath + ? tabs.find((t) => t.path && location.pathname.startsWith(t.path))?.value || tabs[0]?.value + : activeTab; + + const activeIndex = Math.max( + 0, + tabs.findIndex((t) => t.value === currentActiveTab) + ); + + const wrapRef = useRef(null); + const [wrapWidth, setWrapWidth] = useState(0); + + useLayoutEffect(() => { + const el = wrapRef.current; + if (!el) return; + + const measure = () => setWrapWidth(el.clientWidth); + measure(); + + const ro = new ResizeObserver(() => measure()); + ro.observe(el); + + return () => ro.disconnect(); + }, []); + + const pxToRem = (px: number) => { + if (typeof window === "undefined") return `${px / 16}rem`; + const root = window.getComputedStyle(document.documentElement).fontSize; + const base = Number.parseFloat(root) || 16; + return `${px / base}rem`; + }; + + const trackWidth = Math.max(0, wrapWidth - SIDE_MARGIN_PX * 2); + const tabWidth = trackWidth / tabs.length; + const indicatorWidth = tabWidth; + const indicatorX = SIDE_MARGIN_PX + tabWidth * activeIndex; + return ( -
- {tabs.map((tab) => { - const isSelected = activeTab === tab.value; +
+
+ {tabs.map((tab) => { + const isSelected = currentActiveTab === tab.value; - // Mode 1: Link-based Tab - if (tab.path) { + // Link-based Tab + if (tab.path) { + return ( + + {({ isActive }) => ( +
+ {tab.label} +
+ )} +
+ ); + } + + // Button-based Tab return ( - onTabChange?.(tab.value)} + className={`flex-1 h-full flex items-center justify-center text-title2 transition-colors ${isSelected ? 'text-(--color-core-1)' : 'text-text-gray3' + }`} > - {({ isActive }) => ( -
- {tab.label} - {isActive && ( -
- )} -
- )} - + {tab.label} + ); - } - - // Mode 2: Button-based Tab (State) - return ( - - ); - })} + })} +
+ +
+ +
); } diff --git a/app/routes/matching/brand/components/BrandFilterBar.tsx b/app/routes/matching/brand/components/BrandFilterBar.tsx index 77077ff8..f8067b05 100644 --- a/app/routes/matching/brand/components/BrandFilterBar.tsx +++ b/app/routes/matching/brand/components/BrandFilterBar.tsx @@ -16,10 +16,10 @@ export default function BrandFilterBar({ category, onCategoryChange, searchKeywo