mirror of
https://github.com/Dvorinka/Bookra.git
synced 2026-06-04 04:22:59 +00:00
139 lines
3.6 KiB
TypeScript
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>
|
|
);
|
|
};
|