mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-06-12 09:47:51 +02:00
217 lines
5.6 KiB
TypeScript
217 lines
5.6 KiB
TypeScript
"use client";
|
|
|
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
|
import {
|
|
AnimatePresence,
|
|
type HTMLMotionProps,
|
|
motion,
|
|
type Transition,
|
|
} from "motion/react";
|
|
import * as React from "react";
|
|
|
|
import { AutoHeight } from "@/components/ui/auto-height";
|
|
import {
|
|
Highlight,
|
|
HighlightItem,
|
|
type HighlightItemProps,
|
|
type HighlightProps,
|
|
} from "@/components/ui/highlight";
|
|
import { useControlledState } from "@/hooks/use-controlled-state";
|
|
import { getStrictContext } from "@/lib/get-strict-context";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
type TabsContextType = {
|
|
value: string | undefined;
|
|
setValue: TabsProps["onValueChange"];
|
|
};
|
|
|
|
const [TabsProvider, useTabs] =
|
|
getStrictContext<TabsContextType>("TabsContext");
|
|
|
|
type TabsProps = React.ComponentProps<typeof TabsPrimitive.Root>;
|
|
|
|
function Tabs(props: TabsProps) {
|
|
const [value, setValue] = useControlledState({
|
|
value: props.value,
|
|
defaultValue: props.defaultValue,
|
|
onChange: props.onValueChange,
|
|
});
|
|
|
|
return (
|
|
<TabsProvider value={{ value, setValue }}>
|
|
<TabsPrimitive.Root
|
|
data-slot="tabs"
|
|
{...props}
|
|
onValueChange={setValue}
|
|
/>
|
|
</TabsProvider>
|
|
);
|
|
}
|
|
|
|
type TabsHighlightProps = Omit<HighlightProps, "controlledItems" | "value">;
|
|
|
|
function TabsHighlight({
|
|
transition = { type: "spring", stiffness: 200, damping: 25 },
|
|
...props
|
|
}: TabsHighlightProps) {
|
|
const { value } = useTabs();
|
|
|
|
return (
|
|
<Highlight
|
|
data-slot="tabs-highlight"
|
|
controlledItems
|
|
value={value}
|
|
transition={transition}
|
|
click={false}
|
|
{...props}
|
|
/>
|
|
);
|
|
}
|
|
|
|
type TabsListProps = React.ComponentProps<typeof TabsPrimitive.List>;
|
|
|
|
const TabsList = React.forwardRef<
|
|
React.ElementRef<typeof TabsPrimitive.List>,
|
|
TabsListProps
|
|
>(({ className, ...props }, ref) => (
|
|
<TabsPrimitive.List
|
|
ref={ref}
|
|
data-slot="tabs-list"
|
|
className={cn(
|
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
));
|
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
|
|
|
type TabsHighlightItemProps = HighlightItemProps & {
|
|
value: string;
|
|
};
|
|
|
|
function TabsHighlightItem(props: TabsHighlightItemProps) {
|
|
return <HighlightItem data-slot="tabs-highlight-item" {...props} />;
|
|
}
|
|
|
|
type TabsTriggerProps = React.ComponentProps<typeof TabsPrimitive.Trigger>;
|
|
|
|
const TabsTrigger = React.forwardRef<
|
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
|
TabsTriggerProps
|
|
>(({ className, ...props }, ref) => (
|
|
<TabsPrimitive.Trigger
|
|
ref={ref}
|
|
data-slot="tabs-trigger"
|
|
className={cn(
|
|
"cursor-pointer 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 disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
className,
|
|
)}
|
|
{...props}
|
|
/>
|
|
));
|
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
|
|
|
type TabsContentProps = React.ComponentProps<typeof TabsPrimitive.Content> &
|
|
HTMLMotionProps<"div">;
|
|
|
|
function TabsContent({
|
|
value,
|
|
forceMount,
|
|
transition = { duration: 0.5, ease: "easeInOut" },
|
|
className,
|
|
...props
|
|
}: TabsContentProps) {
|
|
return (
|
|
<AnimatePresence mode="wait">
|
|
<TabsPrimitive.Content asChild forceMount={forceMount} value={value}>
|
|
<motion.div
|
|
data-slot="tabs-content"
|
|
layout
|
|
layoutDependency={value}
|
|
initial={{ opacity: 0, filter: "blur(4px)" }}
|
|
animate={{ opacity: 1, filter: "blur(0px)" }}
|
|
exit={{ opacity: 0, filter: "blur(4px)" }}
|
|
transition={transition}
|
|
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}
|
|
/>
|
|
</TabsPrimitive.Content>
|
|
</AnimatePresence>
|
|
);
|
|
}
|
|
|
|
type TabsContentsAutoProps = React.ComponentProps<typeof AutoHeight> & {
|
|
mode?: "auto-height";
|
|
children: React.ReactNode;
|
|
transition?: Transition;
|
|
};
|
|
|
|
type TabsContentsLayoutProps = Omit<HTMLMotionProps<"div">, "transition"> & {
|
|
mode: "layout";
|
|
children: React.ReactNode;
|
|
transition?: Transition;
|
|
};
|
|
|
|
type TabsContentsProps = TabsContentsAutoProps | TabsContentsLayoutProps;
|
|
|
|
const defaultTransition: Transition = {
|
|
type: "spring",
|
|
stiffness: 200,
|
|
damping: 30,
|
|
};
|
|
|
|
function isAutoMode(props: TabsContentsProps): props is TabsContentsAutoProps {
|
|
return !("mode" in props) || props.mode === "auto-height";
|
|
}
|
|
|
|
function TabsContents(props: TabsContentsProps) {
|
|
const { value } = useTabs();
|
|
|
|
if (isAutoMode(props)) {
|
|
const { transition = defaultTransition, ...autoProps } = props;
|
|
|
|
return (
|
|
<AutoHeight
|
|
data-slot="tabs-contents"
|
|
deps={[value]}
|
|
transition={transition}
|
|
{...autoProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const { transition = defaultTransition, style, ...layoutProps } = props;
|
|
|
|
return (
|
|
<motion.div
|
|
data-slot="tabs-contents"
|
|
layout="size"
|
|
layoutDependency={value}
|
|
style={{ overflow: "hidden", ...style }}
|
|
transition={{ layout: transition }}
|
|
{...layoutProps}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export {
|
|
Tabs,
|
|
TabsHighlight,
|
|
TabsHighlightItem,
|
|
TabsList,
|
|
TabsTrigger,
|
|
TabsContent,
|
|
TabsContents,
|
|
type TabsProps,
|
|
type TabsHighlightProps,
|
|
type TabsHighlightItemProps,
|
|
type TabsListProps,
|
|
type TabsTriggerProps,
|
|
type TabsContentProps,
|
|
type TabsContentsProps,
|
|
};
|