129 lines
4.7 KiB
TypeScript
129 lines
4.7 KiB
TypeScript
"use client";
|
|
|
|
/* eslint-disable react-refresh/only-export-components */
|
|
|
|
import { type VariantProps, cva } from "class-variance-authority";
|
|
import { motion } from "motion/react";
|
|
import { Tabs as TabsPrimitive } from "radix-ui";
|
|
import * as React from "react";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
|
|
const TabsContext = React.createContext<{ value?: string; id: string }>({
|
|
id: "",
|
|
});
|
|
|
|
export function Tabs({
|
|
className,
|
|
orientation = "horizontal",
|
|
value,
|
|
defaultValue,
|
|
onValueChange,
|
|
...props
|
|
}: React.ComponentProps<typeof TabsPrimitive.Root>): React.ReactElement {
|
|
const [internal, setInternal] = React.useState(defaultValue ?? "");
|
|
const current = value ?? internal;
|
|
const id = React.useId();
|
|
|
|
return (
|
|
<TabsContext.Provider value={{ value: current, id }}>
|
|
<TabsPrimitive.Root
|
|
data-slot="tabs"
|
|
data-orientation={orientation}
|
|
value={current}
|
|
onValueChange={(v) => {
|
|
setInternal(v);
|
|
onValueChange?.(v);
|
|
}}
|
|
className={cn(
|
|
"gap-2 group/tabs flex data-[orientation=horizontal]:flex-col",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
</TabsContext.Provider>
|
|
);
|
|
}
|
|
|
|
export const tabsListVariants = cva(
|
|
"rounded-4xl p-[3px] group-data-horizontal/tabs:h-9 group-data-vertical/tabs:rounded-2xl data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col",
|
|
{
|
|
variants: {
|
|
variant: {
|
|
default: "bg-muted",
|
|
line: "gap-1 bg-transparent",
|
|
},
|
|
},
|
|
defaultVariants: {
|
|
variant: "default",
|
|
},
|
|
},
|
|
);
|
|
|
|
export function TabsList({
|
|
className,
|
|
variant = "default",
|
|
...props
|
|
}: React.ComponentProps<typeof TabsPrimitive.List> &
|
|
VariantProps<typeof tabsListVariants>): React.ReactElement {
|
|
return (
|
|
<TabsPrimitive.List
|
|
data-slot="tabs-list"
|
|
data-variant={variant}
|
|
className={cn(tabsListVariants({ variant }), className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export function TabsTrigger({
|
|
className,
|
|
value,
|
|
children,
|
|
...props
|
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>): React.ReactElement {
|
|
const ctx = React.useContext(TabsContext);
|
|
const isActive = ctx.value === value;
|
|
|
|
return (
|
|
<TabsPrimitive.Trigger
|
|
data-slot="tabs-trigger"
|
|
value={value}
|
|
className={cn(
|
|
"gap-1.5 rounded-xl corner-squircle border border-transparent px-2 py-1 text-sm font-medium group-data-vertical/tabs:px-2.5 group-data-vertical/tabs:py-1.5 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-colors group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
|
"group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent",
|
|
"data-active:text-foreground dark:data-active:text-foreground",
|
|
"after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100",
|
|
className,
|
|
)}
|
|
{...props}
|
|
>
|
|
{isActive && (
|
|
<motion.span
|
|
layoutId={`tab-bg-${ctx.id}`}
|
|
className="absolute inset-0 rounded-xl bg-background dark:bg-input/30 dark:border dark:border-input"
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 500,
|
|
damping: 35,
|
|
mass: 0.5,
|
|
}}
|
|
/>
|
|
)}
|
|
<span className="relative z-10">{children}</span>
|
|
</TabsPrimitive.Trigger>
|
|
);
|
|
}
|
|
|
|
export function TabsContent({
|
|
className,
|
|
...props
|
|
}: React.ComponentProps<typeof TabsPrimitive.Content>): React.ReactElement {
|
|
return (
|
|
<TabsPrimitive.Content
|
|
data-slot="tabs-content"
|
|
className={cn("text-sm flex-1 outline-none", className)}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|