diff --git a/web/src/components/ui/segmented/RadixSegmented.tsx b/web/src/components/ui/segmented/RadixSegmented.tsx deleted file mode 100644 index 6b88f0bb9..000000000 --- a/web/src/components/ui/segmented/RadixSegmented.tsx +++ /dev/null @@ -1,107 +0,0 @@ -'use client'; - -import * as React from 'react'; -import * as Tabs from '@radix-ui/react-tabs'; -import { motion } from 'framer-motion'; -import { cn } from '@/lib/classMerge'; -import { useEffect, useState } from 'react'; - -export interface SegmentedItem { - value: string; - label: React.ReactNode; - icon?: React.ReactNode; - disabled?: boolean; -} - -interface RadixSegmentedProps { - items: SegmentedItem[]; - value?: string; - onChange?: (value: string) => void; - className?: string; -} - -export const RadixSegmented = React.forwardRef( - ({ items, value, onChange, className }, ref) => { - const tabRefs = React.useRef>(new Map()); - const [selectedValue, setSelectedValue] = useState(value || items[0]?.value); - const [hoveredValue, setHoveredValue] = useState(null); - const [gliderStyle, setGliderStyle] = useState({ - width: 0, - transform: 'translateX(0)' - }); - - useEffect(() => { - if (value !== undefined && value !== selectedValue) { - setSelectedValue(value); - } - }, [value]); - - useEffect(() => { - const selectedTab = tabRefs.current.get(selectedValue); - if (selectedTab) { - const { offsetWidth, offsetLeft } = selectedTab; - setGliderStyle({ - width: offsetWidth, - transform: `translateX(${offsetLeft}px)` - }); - } - }, [selectedValue]); - - const handleValueChange = (newValue: string) => { - setSelectedValue(newValue); - onChange?.(newValue); - }; - - return ( - - - - {items.map((item) => ( - { - if (el) tabRefs.current.set(item.value, el); - }} - onMouseEnter={() => !item.disabled && setHoveredValue(item.value)} - onMouseLeave={() => setHoveredValue(null)} - className={cn( - 'relative z-10 flex h-8 items-center justify-center gap-2 rounded-md px-3 text-sm font-medium transition-colors', - 'focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2', - selectedValue === item.value - ? 'text-gray-900' - : 'text-gray-500 hover:text-gray-900', - hoveredValue === item.value && selectedValue !== item.value && 'bg-gray-50/50', - item.disabled && 'cursor-not-allowed opacity-50 hover:text-gray-500' - )}> - {item.icon && {item.icon}} - {item.label} - - ))} - - - ); - } -); - -RadixSegmented.displayName = 'RadixSegmented'; diff --git a/web/src/components/ui/segmented/RadixSegmented.stories.tsx b/web/src/components/ui/segmented/Segmented.stories.tsx similarity index 76% rename from web/src/components/ui/segmented/RadixSegmented.stories.tsx rename to web/src/components/ui/segmented/Segmented.stories.tsx index 958739c75..402a3a20f 100644 --- a/web/src/components/ui/segmented/RadixSegmented.stories.tsx +++ b/web/src/components/ui/segmented/Segmented.stories.tsx @@ -1,9 +1,9 @@ import type { Meta, StoryObj } from '@storybook/react'; -import { RadixSegmented } from './RadixSegmented'; +import { Segmented } from './Segmented'; -const meta: Meta = { - title: 'UI/RadixSegmented', - component: RadixSegmented, +const meta: Meta = { + title: 'Base/Segmented', + component: Segmented, parameters: { layout: 'centered' }, @@ -11,11 +11,11 @@ const meta: Meta = { }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const defaultItems = [ { value: 'tab1', label: 'Tab 1' }, - { value: 'tab2', label: 'Tab 2' }, + { value: 'tab2', label: 'Tab 2', disabled: true }, { value: 'tab3', label: 'Tab 3' } ]; diff --git a/web/src/components/ui/segmented/Segmented.tsx b/web/src/components/ui/segmented/Segmented.tsx index e69de29bb..c8bd95b58 100644 --- a/web/src/components/ui/segmented/Segmented.tsx +++ b/web/src/components/ui/segmented/Segmented.tsx @@ -0,0 +1,117 @@ +'use client'; + +import * as React from 'react'; +import * as Tabs from '@radix-ui/react-tabs'; +import { motion } from 'framer-motion'; +import { cn } from '@/lib/classMerge'; +import { useEffect, useState } from 'react'; + +export interface SegmentedItem { + value: string; + label: React.ReactNode; + icon?: React.ReactNode; + disabled?: boolean; +} + +interface SegmentedProps { + items: SegmentedItem[]; + value?: string; + onChange?: (value: string) => void; + className?: string; + size?: 'default' | 'large'; +} + +export const Segmented = React.forwardRef( + ({ items, value, onChange, className, size = 'default' }, ref) => { + const tabRefs = React.useRef>(new Map()); + const [selectedValue, setSelectedValue] = useState(value || items[0]?.value); + const [hoveredValue, setHoveredValue] = useState(null); + const [gliderStyle, setGliderStyle] = useState({ + width: 0, + transform: 'translateX(0)' + }); + + const height = size === 'default' ? 'h-[28px]' : 'h-[50px]'; + const innerHeight = size === 'default' ? 'h-[28px]' : 'h-[50px]'; + const padding = size === 'default' ? 'p-[0px]' : 'p-[0px]'; + + useEffect(() => { + if (value !== undefined && value !== selectedValue) { + setSelectedValue(value); + } + }, [value]); + + useEffect(() => { + const selectedTab = tabRefs.current.get(selectedValue); + if (selectedTab) { + const { offsetWidth, offsetLeft } = selectedTab; + setGliderStyle({ + width: offsetWidth, + transform: `translateX(${offsetLeft}px)` + }); + } + }, [selectedValue]); + + const handleValueChange = (newValue: string) => { + setSelectedValue(newValue); + onChange?.(newValue); + }; + + return ( + + + + {items.map((item) => ( + { + if (el) tabRefs.current.set(item.value, el); + }} + onMouseEnter={() => !item.disabled && setHoveredValue(item.value)} + onMouseLeave={() => setHoveredValue(null)} + className={cn( + 'relative z-10 flex items-center justify-center gap-2 rounded-md px-2.5 text-sm transition-colors', + innerHeight, + 'focus-visible:ring-primary focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2', + selectedValue === item.value + ? 'text-foreground' + : 'text-gray-dark hover:text-foreground', + hoveredValue === item.value && selectedValue !== item.value && 'bg-gray-50/50', + item.disabled + ? 'text-foreground/30 hover:text-foreground/30 cursor-not-allowed' + : 'cursor-pointer' + )}> + {item.icon && {item.icon}} + {item.label} + + ))} + + + ); + } +); + +Segmented.displayName = 'Segmented';