Skip to content

Commit 609124d

Browse files
committed
feat: add TabBar component and update primitives index
1 parent 870aaf3 commit 609124d

2 files changed

Lines changed: 133 additions & 0 deletions

File tree

packages/primitives/src/TabBar.tsx

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
"use client";
2+
3+
import clsx from "clsx";
4+
import {forwardRef} from "react";
5+
import {LayoutGroup, motion} from "framer-motion";
6+
7+
// ─── TabBar Container ────────────────────────────────────────────────────────
8+
9+
export interface TabBarProps extends React.HTMLAttributes<HTMLDivElement> {
10+
/** Unique layout group ID for framer-motion animation coordination */
11+
layoutId?: string;
12+
/** Element rendered after the tab list (e.g. a "new tab" button) */
13+
trailing?: React.ReactNode;
14+
}
15+
16+
export const TabBar = forwardRef<HTMLDivElement, TabBarProps>(
17+
({layoutId = "tab-bar", trailing, className, children, ...props}, ref) => (
18+
<div
19+
ref={ref}
20+
className={clsx(
21+
"mx-2 flex h-9 shrink-0 items-center gap-1 rounded-full px-1",
22+
"bg-app-box/80 shadow-sm backdrop-blur-sm",
23+
className,
24+
)}
25+
{...props}
26+
>
27+
<LayoutGroup id={layoutId}>
28+
<div className="flex min-w-0 flex-1 items-center gap-1">
29+
{children}
30+
</div>
31+
</LayoutGroup>
32+
{trailing}
33+
</div>
34+
),
35+
);
36+
37+
TabBar.displayName = "TabBar";
38+
39+
// ─── TabBarItem ──────────────────────────────────────────────────────────────
40+
41+
export interface TabBarItemProps
42+
extends Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, "children"> {
43+
/** Whether this tab is currently active */
44+
active?: boolean;
45+
/** Tab label */
46+
label: string;
47+
/** Called when the close button is clicked */
48+
onClose?: () => void;
49+
/** Show close button (default: true when onClose is provided) */
50+
closable?: boolean;
51+
/** Close button element — override to provide your own icon */
52+
closeIcon?: React.ReactNode;
53+
}
54+
55+
export const TabBarItem = forwardRef<HTMLButtonElement, TabBarItemProps>(
56+
(
57+
{
58+
active,
59+
label,
60+
onClose,
61+
closable,
62+
closeIcon,
63+
className,
64+
...props
65+
},
66+
ref,
67+
) => {
68+
const showClose = closable ?? !!onClose;
69+
70+
return (
71+
<button
72+
ref={ref}
73+
className={clsx(
74+
"group relative flex min-w-0 flex-1 items-center justify-center rounded-full py-1.5 text-[13px]",
75+
active
76+
? "text-ink"
77+
: "text-ink-dull hover:text-ink hover:bg-app-hover/50",
78+
className,
79+
)}
80+
{...props}
81+
>
82+
{active && (
83+
<motion.div
84+
layoutId="activeTab"
85+
className="absolute inset-0 rounded-full bg-app-selected shadow-sm"
86+
initial={false}
87+
transition={{
88+
type: "spring",
89+
stiffness: 500,
90+
damping: 35,
91+
}}
92+
/>
93+
)}
94+
{showClose && (
95+
<span
96+
onClick={(e) => {
97+
e.stopPropagation();
98+
onClose?.();
99+
}}
100+
className={clsx(
101+
"absolute left-1.5 z-10 flex size-5 cursor-pointer items-center justify-center rounded-full transition-all",
102+
active
103+
? "opacity-60 hover:bg-app-hover hover:opacity-100"
104+
: "opacity-0 hover:bg-app-hover hover:!opacity-100 group-hover:opacity-60",
105+
)}
106+
title="Close tab"
107+
>
108+
{closeIcon ?? (
109+
<svg
110+
width="10"
111+
height="10"
112+
viewBox="0 0 10 10"
113+
fill="none"
114+
stroke="currentColor"
115+
strokeWidth="1.5"
116+
strokeLinecap="round"
117+
>
118+
<path d="M2 2l6 6M8 2l-6 6" />
119+
</svg>
120+
)}
121+
</span>
122+
)}
123+
<span className="relative z-10 truncate px-6">{label}</span>
124+
</button>
125+
);
126+
},
127+
);
128+
129+
TabBarItem.displayName = "TabBarItem";

packages/primitives/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,10 @@ export type { CircleButtonProps } from "./CircleButton";
9393
export { CircleButtonGroup } from "./CircleButtonGroup";
9494
export type { CircleButtonGroupProps } from "./CircleButtonGroup";
9595

96+
// TabBar
97+
export { TabBar, TabBarItem } from "./TabBar";
98+
export type { TabBarProps, TabBarItemProps } from "./TabBar";
99+
96100
// Backward-compat aliases
97101
export { CircleButton as TopBarButton } from "./CircleButton";
98102
export { CircleButtonGroup as TopBarButtonGroup } from "./CircleButtonGroup";

0 commit comments

Comments
 (0)