Files
garage-ui/frontend/src/components/ui/tabs.tsx
T
2025-11-24 18:16:25 +01:00

119 lines
3.2 KiB
TypeScript

import * as React from 'react';
import { cn } from '@/lib/utils';
interface TabsContextValue {
value: string;
onValueChange: (value: string) => void;
}
const TabsContext = React.createContext<TabsContextValue | undefined>(undefined);
function useTabs() {
const context = React.useContext(TabsContext);
if (!context) {
throw new Error('useTabs must be used within a Tabs');
}
return context;
}
interface TabsProps {
defaultValue?: string;
value?: string;
onValueChange?: (value: string) => void;
children: React.ReactNode;
className?: string;
}
const Tabs: React.FC<TabsProps> = ({
defaultValue,
value: controlledValue,
onValueChange,
children,
className,
}) => {
const [internalValue, setInternalValue] = React.useState(defaultValue || '');
const value = controlledValue !== undefined ? controlledValue : internalValue;
const handleValueChange = (newValue: string) => {
if (controlledValue === undefined) {
setInternalValue(newValue);
}
onValueChange?.(newValue);
};
return (
<TabsContext.Provider value={{ value, onValueChange: handleValueChange }}>
<div className={className}>{children}</div>
</TabsContext.Provider>
);
};
const TabsList = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
'inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground',
className
)}
{...props}
/>
)
);
TabsList.displayName = 'TabsList';
interface TabsTriggerProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
}
const TabsTrigger = React.forwardRef<HTMLButtonElement, TabsTriggerProps>(
({ className, value: triggerValue, onClick, ...props }, ref) => {
const { value, onValueChange } = useTabs();
const isActive = value === triggerValue;
return (
<button
ref={ref}
className={cn(
'inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none',
isActive
? 'bg-background text-foreground shadow-sm'
: 'hover:bg-accent hover:text-accent-foreground',
className
)}
onClick={(e) => {
onValueChange(triggerValue);
onClick?.(e);
}}
{...props}
/>
);
}
);
TabsTrigger.displayName = 'TabsTrigger';
interface TabsContentProps extends React.HTMLAttributes<HTMLDivElement> {
value: string;
}
const TabsContent = React.forwardRef<HTMLDivElement, TabsContentProps>(
({ className, value: contentValue, ...props }, ref) => {
const { value } = useTabs();
if (value !== contentValue) return null;
return (
<div
ref={ref}
className={cn(
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
className
)}
{...props}
/>
);
}
);
TabsContent.displayName = 'TabsContent';
export { Tabs, TabsList, TabsTrigger, TabsContent };