mirror of
https://github.com/lumina-rocks/lumina.git
synced 2026-04-12 00:16:49 +02:00
136 lines
3.5 KiB
TypeScript
136 lines
3.5 KiB
TypeScript
"use client";
|
|
|
|
import { cn } from "@/lib/utils";
|
|
import Link, { LinkProps } from "next/link";
|
|
import { usePathname, useSearchParams } from "next/navigation";
|
|
import * as React from "react";
|
|
import { Suspense } from "react";
|
|
|
|
interface Context {
|
|
defaultValue: string;
|
|
hrefFor: (value: string) => LinkProps["href"];
|
|
searchParam: string;
|
|
selected: string;
|
|
}
|
|
const TabsContext = React.createContext<Context>(null as any);
|
|
|
|
export function Tabs(props: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
defaultValue: string;
|
|
searchParam?: string;
|
|
}) {
|
|
const { children, className, searchParam = "tab", ...other } = props;
|
|
|
|
return (
|
|
<Suspense fallback={<div>Loading...</div>}>
|
|
<TabsContentWrapper {...props} />
|
|
</Suspense>
|
|
);
|
|
}
|
|
|
|
function TabsContentWrapper(props: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
defaultValue: string;
|
|
searchParam?: string;
|
|
}) {
|
|
const { children, className, searchParam = "tab", ...other } = props;
|
|
const searchParams = useSearchParams()!;
|
|
|
|
const selected = searchParams.get(searchParam) || props.defaultValue;
|
|
|
|
const pathname = usePathname();
|
|
const hrefFor: Context["hrefFor"] = React.useCallback(
|
|
(value) => {
|
|
const params = new URLSearchParams(searchParams);
|
|
if (value === props.defaultValue) {
|
|
params.delete(searchParam);
|
|
} else {
|
|
params.set(searchParam, value);
|
|
}
|
|
|
|
const asString = params.toString();
|
|
|
|
return pathname + (asString ? "?" + asString : "");
|
|
},
|
|
[searchParams, props.searchParam],
|
|
);
|
|
|
|
return (
|
|
<TabsContext.Provider value={{ ...other, hrefFor, searchParam, selected }}>
|
|
<div className={className}>{children}</div>
|
|
</TabsContext.Provider>
|
|
);
|
|
}
|
|
|
|
const useContext = () => {
|
|
const context = React.useContext(TabsContext);
|
|
if (!context) {
|
|
throw new Error(
|
|
"Tabs compound components cannot be rendered outside the Tabs component",
|
|
);
|
|
}
|
|
|
|
return context;
|
|
};
|
|
|
|
export function TabsList(props: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
}) {
|
|
return (
|
|
<div
|
|
{...props}
|
|
className={cn(
|
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
|
props.className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|
|
|
|
export const TabsTrigger = (props: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
value: string;
|
|
}) => {
|
|
const context = useContext();
|
|
|
|
return (
|
|
<Link
|
|
{...props}
|
|
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 disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
|
props.className,
|
|
)}
|
|
data-state={context.selected === props.value ? "active" : "inactive"}
|
|
href={context.hrefFor(props.value)}
|
|
scroll={false}
|
|
shallow={true}
|
|
/>
|
|
);
|
|
};
|
|
|
|
export function TabsContent(props: {
|
|
children: React.ReactNode;
|
|
className?: string;
|
|
value: string;
|
|
}) {
|
|
const context = useContext();
|
|
|
|
if (context.selected !== props.value) {
|
|
return null;
|
|
}
|
|
|
|
return (
|
|
<div
|
|
{...props}
|
|
className={cn(
|
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
|
props.className,
|
|
)}
|
|
/>
|
|
);
|
|
}
|