Files
Bookra/apps/frontend/src/components/ui/tabs.tsx
T
Tomas Dvorak 48c3e15a38 cleanup
2026-05-05 09:48:07 +02:00

139 lines
3.6 KiB
TypeScript

import { JSX, For, createSignal, createContext, useContext, ParentComponent, splitProps, children, Accessor } from "solid-js";
import type { ResolvedChildren } from "solid-js";
// Context for tab state
interface TabsContextValue {
selectedTab: Accessor<string>;
setSelectedTab: (id: string) => void;
}
const TabsContext = createContext<TabsContextValue>();
const useTabs = () => {
const context = useContext(TabsContext);
if (!context) {
throw new Error("useTabs must be used within a Tabs component");
}
return context;
};
// Tabs Root Component
interface TabsProps {
defaultValue: string;
value?: string;
onValueChange?: (value: string) => void;
children: JSX.Element;
class?: string;
}
export const Tabs: ParentComponent<TabsProps> = (props) => {
const [local, rest] = splitProps(props, ["defaultValue", "value", "onValueChange", "children", "class"]);
const [selectedTab, setSelectedTabInternal] = createSignal(local.defaultValue);
const setSelectedTab = (id: string) => {
setSelectedTabInternal(id);
local.onValueChange?.(id);
};
// If controlled, use the provided value
const currentTab = () => local.value ?? selectedTab();
const contextValue: TabsContextValue = {
selectedTab: currentTab,
setSelectedTab,
};
return (
<TabsContext.Provider value={contextValue}>
<div class={local.class} {...rest}>
{local.children}
</div>
</TabsContext.Provider>
);
};
// Tabs List Component
interface TabsListProps extends JSX.HTMLAttributes<HTMLDivElement> {
variant?: "default" | "pills" | "underline";
}
export const TabsList: ParentComponent<TabsListProps> = (props) => {
const [local, rest] = splitProps(props, ["variant", "children", "class"]);
const variantClasses = {
default: "bg-canvas-muted p-1 rounded-card",
pills: "gap-1",
underline: "border-b border-border gap-4",
};
return (
<div
class={[
"flex items-center",
variantClasses[local.variant || "default"],
local.class || "",
].join(" ")}
role="tablist"
{...rest}
>
{local.children}
</div>
);
};
// Tab Trigger Component
interface TabsTriggerProps extends JSX.ButtonHTMLAttributes<HTMLButtonElement> {
value: string;
}
export const TabsTrigger: ParentComponent<TabsTriggerProps> = (props) => {
const [local, rest] = splitProps(props, ["value", "children", "class"]);
const tabs = useTabs();
const isSelected = () => tabs.selectedTab() === local.value;
return (
<button
role="tab"
aria-selected={isSelected()}
class={[
"px-4 py-2 text-sm font-display font-medium transition-all duration-200",
"focus:outline-none focus-visible:ring-2 focus-visible:ring-accent/50 focus-visible:ring-offset-2",
"rounded-button",
isSelected()
? "bg-canvas text-ink shadow-sm"
: "text-ink-muted hover:text-ink hover:bg-canvas-subtle",
local.class || "",
].join(" ")}
onClick={() => tabs.setSelectedTab(local.value)}
{...rest}
>
{local.children}
</button>
);
};
// Tab Content Component
interface TabsContentProps extends JSX.HTMLAttributes<HTMLDivElement> {
value: string;
}
export const TabsContent: ParentComponent<TabsContentProps> = (props) => {
const [local, rest] = splitProps(props, ["value", "children", "class"]);
const tabs = useTabs();
const isSelected = () => tabs.selectedTab() === local.value;
return (
<div
role="tabpanel"
class={[
"mt-4",
isSelected() ? "block animate-fade-in" : "hidden",
local.class || "",
].join(" ")}
{...rest}
>
{local.children}
</div>
);
};