diff --git a/packages/propel/src/tabs/index.ts b/packages/propel/src/tabs/index.ts index e69de29bb..811d3d4a7 100644 --- a/packages/propel/src/tabs/index.ts +++ b/packages/propel/src/tabs/index.ts @@ -0,0 +1 @@ +export * from "./tabs"; diff --git a/packages/propel/src/tabs/list.tsx b/packages/propel/src/tabs/list.tsx deleted file mode 100644 index cdb177dbf..000000000 --- a/packages/propel/src/tabs/list.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import React, { FC } from "react"; -import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; -import { LucideProps } from "lucide-react"; -// helpers -import { cn } from "../utils/classname"; - -export type TabListItem = { - key: string; - icon?: FC; - label?: React.ReactNode; - disabled?: boolean; - onClick?: () => void; -}; - -type TTabListProps = { - tabs: TabListItem[]; - tabListClassName?: string; - tabClassName?: string; - size?: "sm" | "md" | "lg"; - selectedTab?: string; -}; - -export const TabList: FC = ({ tabs, tabListClassName, tabClassName, size = "md", selectedTab }) => ( - - {tabs.map((tab) => ( - - cn( - "flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded", - (selectedTab ? selectedTab === tab.key : selected) - ? "bg-custom-background-100 text-custom-text-100 shadow-sm" - : tab.disabled - ? "text-custom-text-400 cursor-not-allowed" - : "text-custom-text-400 hover:text-custom-text-300 hover:bg-custom-background-80/60", - { - "text-xs": size === "sm", - "text-sm": size === "md", - "text-base": size === "lg", - }, - tabClassName - ) - } - key={tab.key} - disabled={tab.disabled} - > - {tab.icon && } - {tab.label} - - ))} - - - -); diff --git a/packages/propel/src/tabs/tabs.stories.tsx b/packages/propel/src/tabs/tabs.stories.tsx new file mode 100644 index 000000000..28e7dc11e --- /dev/null +++ b/packages/propel/src/tabs/tabs.stories.tsx @@ -0,0 +1,66 @@ +import { Fragment } from "react"; +import type { Meta, StoryObj } from "@storybook/react-vite"; +import { Tabs } from "./tabs"; + +const meta: Meta = { + title: "Components/Tabs", + component: Tabs, + parameters: { + layout: "centered", + }, +}; + +export default meta; +type Story = StoryObj; + +export const Basic: Story = { + render: () => ( +
+ + + Overview + Settings + + + + Overview settings go here + + + Settings settings go here + + +
+ ), +}; + +export const Sizes: Story = { + render: () => { + const sizes = ["sm", "md", "lg"] as const; + const labels = { + sm: "Small", + md: "Medium", + lg: "Large", + }; + + return ( +
+ {sizes.map((size, index) => ( + + {index > 0 &&
} +
{labels[size]}
+ + + + Overview + + + Settings + + + + + ))} +
+ ); + }, +}; diff --git a/packages/propel/src/tabs/tabs.tsx b/packages/propel/src/tabs/tabs.tsx index 3984ad665..5bd148ba4 100644 --- a/packages/propel/src/tabs/tabs.tsx +++ b/packages/propel/src/tabs/tabs.tsx @@ -1,89 +1,96 @@ -import React, { FC, useEffect, useState } from "react"; -import { Tabs as BaseTabs } from "@base-ui-components/react/tabs"; -import { useLocalStorage } from "@plane/hooks"; +import * as React from "react"; +import { Tabs as TabsPrimitive } from "@base-ui-components/react/tabs"; import { cn } from "../utils/classname"; -import { TabList, TabListItem } from "./list"; -export type TabContent = { - content: React.ReactNode; +type TabsCompound = React.ForwardRefExoticComponent< + React.ComponentProps & React.RefAttributes> +> & { + List: React.ForwardRefExoticComponent< + React.ComponentProps & React.RefAttributes> + >; + Trigger: React.ForwardRefExoticComponent< + React.ComponentProps & { size?: "sm" | "md" | "lg" } & React.RefAttributes< + React.ElementRef + > + >; + Content: React.ForwardRefExoticComponent< + React.ComponentProps & React.RefAttributes> + >; + Indicator: React.ForwardRefExoticComponent & React.RefAttributes>; }; -export type TabItem = TabListItem & TabContent; +const TabsRoot = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => ( + +)); -type TTabsProps = { - tabs: TabItem[]; - storageKey?: string; - actions?: React.ReactNode; - defaultTab?: string; - containerClassName?: string; - tabListContainerClassName?: string; - tabListClassName?: string; - tabClassName?: string; - tabPanelClassName?: string; - size?: "sm" | "md" | "lg"; - storeInLocalStorage?: boolean; -}; +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => ( + +)); -export const Tabs: FC = (props: TTabsProps) => { - const { - tabs, - storageKey, - actions, - defaultTab = tabs[0]?.key, - containerClassName = "", - tabListContainerClassName = "", - tabListClassName = "", - tabClassName = "", - tabPanelClassName = "", - size = "md", - storeInLocalStorage = true, - } = props; +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentProps & { size?: "sm" | "md" | "lg" } +>(({ className, size = "md", ...props }, ref) => ( + +)); - const { storedValue, setValue } = useLocalStorage( - storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`, - defaultTab - ); +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentProps +>(({ className, ...props }, ref) => ( + +)); +const TabsIndicator = React.forwardRef>(({ className, ...props }, ref) => ( +
+)); - const [activeIndex, setActiveIndex] = useState(() => { - const initialTab = storedValue ?? defaultTab; - return tabs.findIndex((tab) => tab.key === initialTab); - }); +export const Tabs = Object.assign(TabsRoot, { + List: TabsList, + Trigger: TabsTrigger, + Content: TabsContent, + Indicator: TabsIndicator, +}) satisfies TabsCompound; - useEffect(() => { - if (storeInLocalStorage && tabs[activeIndex]) { - setValue(tabs[activeIndex].key); - } - }, [activeIndex, setValue, storeInLocalStorage, tabs]); - - const handleTabChange = (index: number) => { - setActiveIndex(index); - if (!tabs[index].disabled) { - tabs[index].onClick?.(); - } - }; - - return ( - -
- - {actions &&
{actions}
} -
- - {tabs.map((tab) => ( - - {tab.content} - - ))} -
- ); -}; +export { TabsList, TabsTrigger, TabsContent, TabsIndicator }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 44425b5d7..d4aece68e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -854,7 +854,7 @@ importers: version: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-react: specifier: ^7.33.2 - version: 7.37.3(eslint@8.57.1) + version: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: specifier: ^5.2.0 version: 5.2.0(eslint@8.57.1) @@ -1076,7 +1076,7 @@ importers: version: 0.5.16(tailwindcss@3.4.17(ts-node@10.9.2(@swc/core@1.13.3(@swc/helpers@0.5.17))(@types/node@22.17.2)(typescript@5.8.3))) autoprefixer: specifier: ^10.4.14 - version: 10.4.20(postcss@8.5.6) + version: 10.4.21(postcss@8.5.6) postcss: specifier: ^8.4.38 version: 8.5.6 @@ -1248,7 +1248,7 @@ importers: version: 18.3.1 autoprefixer: specifier: ^10.4.19 - version: 10.4.20(postcss@8.5.6) + version: 10.4.21(postcss@8.5.6) postcss-cli: specifier: ^11.0.0 version: 11.0.1(jiti@1.21.7)(postcss@8.5.6) @@ -3970,8 +3970,8 @@ packages: peerDependencies: postcss: ^8.1.0 - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + autoprefixer@10.4.21: + resolution: {integrity: sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: @@ -4854,8 +4854,8 @@ packages: peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 - eslint-plugin-react@7.37.3: - resolution: {integrity: sha512-DomWuTQPFYZwF/7c9W2fkKkStqZmBd3uugfqBYLdkZ3Hii23WzZuOLUskGxB8qkSKqftxEeGL1TB2kMhrce0jA==} + eslint-plugin-react@7.37.5: + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 @@ -10893,7 +10893,7 @@ snapshots: postcss: 8.5.6 postcss-value-parser: 4.2.0 - autoprefixer@10.4.20(postcss@8.5.6): + autoprefixer@10.4.21(postcss@8.5.6): dependencies: browserslist: 4.25.2 caniuse-lite: 1.0.30001735 @@ -11788,7 +11788,7 @@ snapshots: eslint-import-resolver-typescript: 3.10.1(eslint-plugin-import@2.31.0)(eslint@8.57.1) eslint-plugin-import: 2.31.0(@typescript-eslint/parser@8.40.0(eslint@8.57.1)(typescript@5.8.3))(eslint-import-resolver-typescript@3.10.1)(eslint@8.57.1) eslint-plugin-jsx-a11y: 6.10.2(eslint@8.57.1) - eslint-plugin-react: 7.37.3(eslint@8.57.1) + eslint-plugin-react: 7.37.5(eslint@8.57.1) eslint-plugin-react-hooks: 5.0.0-canary-7118f5dd7-20230705(eslint@8.57.1) optionalDependencies: typescript: 5.8.3 @@ -11896,7 +11896,7 @@ snapshots: dependencies: eslint: 8.57.1 - eslint-plugin-react@7.37.3(eslint@8.57.1): + eslint-plugin-react@7.37.5(eslint@8.57.1): dependencies: array-includes: 3.1.9 array.prototype.findlast: 1.2.5 @@ -14308,7 +14308,6 @@ snapshots: '@rollup/rollup-win32-ia32-msvc': 4.50.0 '@rollup/rollup-win32-x64-msvc': 4.50.0 fsevents: 2.3.3 - optional: true rope-sequence@1.3.4: {} @@ -15285,7 +15284,7 @@ snapshots: fdir: 6.4.6(picomatch@4.0.3) picomatch: 4.0.3 postcss: 8.5.6 - rollup: 4.45.1 + rollup: 4.50.0 tinyglobby: 0.2.14 optionalDependencies: '@types/node': 22.17.2