refactor: migrate from Headless UI Tabs to custom Tabs component

- Replaced instances of Headless UI's Tab component with a new custom Tabs component across various components, including analytics modals, image pickers, and navigation panes.
- Updated tab handling logic to align with the new Tabs API, ensuring consistent behavior and styling.
- Removed unused Tab imports and cleaned up related code for improved maintainability.
- This refactor enhances the overall structure and consistency of tab navigation within the application.
This commit is contained in:
Jayash Tripathy
2025-12-03 17:37:45 +05:30
parent 36d42856e9
commit 2acba2980b
14 changed files with 179 additions and 368 deletions

View File

@@ -69,13 +69,11 @@ export const WorkItemsModalMainContent = observer(function WorkItemsModalMainCon
);
return (
<Tab.Group as={React.Fragment}>
<div className="flex flex-col gap-14 overflow-y-auto p-6">
<TotalInsights analyticsType="work-items" peekView={!fullScreen} />
<CreatedVsResolved />
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
<WorkItemsInsightTable />
</div>
</Tab.Group>
<div className="flex flex-col gap-14 overflow-y-auto p-6">
<TotalInsights analyticsType="work-items" peekView={!fullScreen} />
<CreatedVsResolved />
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
<WorkItemsInsightTable />
</div>
);
});

View File

@@ -5,11 +5,12 @@ import { useDropzone } from "react-dropzone";
import type { Control } from "react-hook-form";
import { Controller } from "react-hook-form";
import useSWR from "swr";
import { Tab, Popover } from "@headlessui/react";
import { Popover } from "@headlessui/react";
// plane imports
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
import { useOutsideClickDetector } from "@plane/hooks";
import { Button } from "@plane/propel/button";
import { Tabs } from "@plane/propel/tabs";
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
import { EFileAssetType } from "@plane/types";
import { Input, Loader } from "@plane/ui";
@@ -197,91 +198,88 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
ref={imagePickerRef}
className="flex h-96 w-80 flex-col overflow-auto rounded border border-custom-border-300 bg-custom-background-100 p-3 shadow-2xl md:h-[28rem] md:w-[36rem]"
>
<Tab.Group>
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
<Tabs defaultValue={tabOptions[0].key} className={"h-full overflow-hidden"}>
<Tabs.List className="p-1 w-fit">
{tabOptions.map((tab) => (
<Tab
<Tabs.Trigger
key={tab.key}
className={({ selected }) =>
`rounded px-4 py-1 text-center text-sm outline-none transition-colors ${
selected ? "bg-custom-primary text-white" : "text-custom-text-100"
}`
}
value={tab.key}
className="rounded px-4 py-1 text-center text-sm outline-none transition-colors data-[selected]:bg-custom-primary data-[selected]:text-white text-custom-text-100"
>
{tab.title}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
<Tab.Panels className="vertical-scrollbar scrollbar-md h-full w-full flex-1 overflow-y-auto overflow-x-hidden">
<Tab.Panel className="mt-4 h-full w-full space-y-4">
{(unsplashImages || !unsplashError) && (
<>
<div className="flex gap-x-2">
<Controller
control={control}
name="search"
render={({ field: { value, ref } }) => (
<Input
id="search"
name="search"
type="text"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setSearchParams(formData.search);
}
</Tabs.List>
<div className="mt-4 flex-1 overflow-auto">
{(unsplashImages || !unsplashError) && (
<Tabs.Content className="h-full w-full space-y-4" value="unsplash">
<div className="flex gap-x-2">
<Controller
control={control}
name="search"
render={({ field: { value, ref } }) => (
<Input
id="search"
name="search"
type="text"
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
setSearchParams(formData.search);
}
}}
value={value}
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
ref={ref}
placeholder="Search for images"
className="w-full text-sm"
/>
)}
/>
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm">
Search
</Button>
</div>
{unsplashImages ? (
unsplashImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{unsplashImages.map((image) => (
<div
key={image.id}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
}}
value={value}
onChange={(e) => setFormData({ ...formData, search: e.target.value })}
ref={ref}
placeholder="Search for images"
className="w-full text-sm"
/>
)}
/>
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm">
Search
</Button>
</div>
{unsplashImages ? (
unsplashImages.length > 0 ? (
<div className="grid grid-cols-4 gap-4">
{unsplashImages.map((image) => (
<div
key={image.id}
className="relative col-span-2 aspect-video md:col-span-1"
onClick={() => {
setIsOpen(false);
onChange(image.urls.regular);
}}
>
<img
src={image.urls.small}
alt={image.alt_description}
className="absolute left-0 top-0 h-full w-full cursor-pointer rounded object-cover"
/>
</div>
))}
</div>
) : (
<p className="pt-7 text-center text-xs text-custom-text-300">No images found.</p>
)
>
<img
src={image.urls.small}
alt={image.alt_description}
className="absolute left-0 top-0 h-full w-full cursor-pointer rounded object-cover"
/>
</div>
))}
</div>
) : (
<Loader className="grid grid-cols-4 gap-4">
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
</Loader>
)}
</>
)}
</Tab.Panel>
<Tab.Panel className="mt-4 h-full w-full space-y-4">
<p className="pt-7 text-center text-xs text-custom-text-300">No images found.</p>
)
) : (
<Loader className="grid grid-cols-4 gap-4">
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
<Loader.Item height="80px" width="100%" />
</Loader>
)}
</Tabs.Content>
)}
<Tabs.Content className="h-full w-full space-y-4" value="images">
<div className="grid grid-cols-4 gap-4">
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
<div
@@ -297,8 +295,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
</div>
))}
</div>
</Tab.Panel>
<Tab.Panel className="mt-4 h-full w-full">
</Tabs.Content>
<Tabs.Content className="h-full w-full" value="upload">
<div className="flex h-full w-full flex-col gap-y-2">
<div className="flex w-full flex-1 items-center gap-3">
<div
@@ -365,9 +364,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
</Button>
</div>
</div>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tabs.Content>
</div>
</Tabs>
</div>
</Popover.Panel>
)}

View File

@@ -1,8 +1,8 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import type { TWorkItemFilterCondition } from "@plane/shared-state";
import type { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
@@ -54,8 +54,6 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
`cycle-analytics-tab-${cycleId}`,
"stat-assignees"
);
// derived values
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TCycleDistribution;
const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
@@ -116,9 +114,8 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.List
as="div"
<Tabs defaultValue={currentTab ?? "stat-assignees"} onValueChange={(value) => setCycleTab(value)}>
<Tabs.List
className={cn(
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
roundedTab ? `rounded-3xl` : `rounded-md`,
@@ -127,23 +124,22 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
)}
>
{PROGRESS_STATS.map((stat) => (
<Tab
<Tabs.Trigger
key={stat.key}
value={stat.key}
className={cn(
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`,
stat.key === currentTab
? "bg-custom-background-100 text-custom-text-300"
: "text-custom-text-400 hover:text-custom-text-300"
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-300",
"text-custom-text-400 hover:text-custom-text-300"
)}
key={stat.key}
onClick={() => setCycleTab(stat.key)}
>
{t(stat.i18n_title)}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
<Tab.Panels className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-states"}>
</Tabs.List>
<div className="py-3 text-custom-text-200">
<Tabs.Content value="stat-states">
<StateGroupStatComponent
distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
@@ -151,25 +147,25 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tab.Panel>
<Tab.Panel key={"stat-assignees"}>
</Tabs.Content>
<Tabs.Content value="stat-assignees">
<AssigneeStatComponent
distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-labels"}>
</Tabs.Content>
<Tabs.Content value="stat-labels">
<LabelStatComponent
distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable}
selectedLabelIds={selectedLabelIds}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tabs.Content>
</div>
</Tabs>
</div>
);
});

View File

@@ -2,8 +2,8 @@ import type { FC } from "react";
import { useState } from "react";
import { observer } from "mobx-react";
import { CheckCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports
import { Tabs } from "@plane/propel/tabs";
// helpers
import type { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types";
import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui";
@@ -39,32 +39,27 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
return (
<div className={cn("flex flex-col py-6 px-3", upgradeCardVariantStyle)}>
<Tab.Group selectedIndex={selectedPlan === "month" ? 0 : 1}>
<div className="flex w-full justify-center h-9">
<Tab.List
className={cn("flex space-x-1 rounded-md p-0.5 w-60", getSubscriptionBackgroundColor(planVariant, "50"))}
>
<Tabs value={selectedPlan} onValueChange={(value) => setSelectedPlan(value as TBillingFrequency)}>
<div className="flex w-full justify-center">
<Tabs.List className={cn("flex rounded-md w-60", getSubscriptionBackgroundColor(planVariant, "50"))}>
{prices.map((price: TSubscriptionPrice) => (
<Tab
<Tabs.Trigger
key={price.key}
className={({ selected }) =>
cn(
"w-full rounded py-1 text-sm font-medium leading-5",
selected
? "bg-custom-background-100 text-custom-text-100 shadow"
: "text-custom-text-300 hover:text-custom-text-200"
)
}
onClick={() => setSelectedPlan(price.recurring)}
value={price.recurring}
className={cn(
"w-full rounded text-sm font-medium leading-5 py-2",
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-100 data-[selected]:shadow",
"text-custom-text-300 hover:text-custom-text-200"
)}
>
{renderPriceContent(price)}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
</Tabs.List>
</div>
<Tab.Panels>
<div>
{prices.map((price: TSubscriptionPrice) => (
<Tab.Panel key={price.key}>
<Tabs.Content key={price.key} value={price.recurring}>
<div className="pt-6 text-center">
<div className="text-xl font-medium">Plane {planeName}</div>
{renderActionButton(price)}
@@ -88,10 +83,10 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
</ul>
{extraFeatures && <div>{extraFeatures}</div>}
</div>
</Tab.Panel>
</Tabs.Content>
))}
</Tab.Panels>
</Tab.Group>
</div>
</Tabs>
</div>
);
});

View File

@@ -73,7 +73,12 @@ export const PlanUpgradeCard = observer(function PlanUpgradeCard(props: PlanUpgr
<>
Yearly
{yearlyDiscount > 0 && (
<span className={cn(getDiscountPillStyle(planVariant), "rounded-full px-1.5 py-0.5 ml-1 text-xs")}>
<span
className={cn(
getDiscountPillStyle(planVariant),
"rounded-full px-1.5 ml-1 text-xs h-5 leading-tight flex items-center justify-center"
)}
>
-{yearlyDiscount}%
</span>
)}

View File

@@ -114,7 +114,7 @@ export const ModuleAnalyticsProgress = observer(function ModuleAnalyticsProgress
if (!moduleDetails) return <></>;
return (
<div className="border-t border-custom-border-200 space-y-4 py-4 px-3">
<div className="border-t border-custom-border-200 space-y-4 py-4">
<Disclosure defaultOpen={isModuleDateValid ? true : false}>
{({ open }) => (
<div className="space-y-6">

View File

@@ -1,7 +1,7 @@
import type { FC } from "react";
import { observer } from "mobx-react";
import { Tab } from "@headlessui/react";
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import type { TWorkItemFilterCondition } from "@plane/shared-state";
import type { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
import { cn, toFilterArray } from "@plane/utils";
@@ -52,8 +52,6 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
`module-analytics-tab-${moduleId}`,
"stat-assignees"
);
// derived values
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
const currentDistribution = distribution as TModuleDistribution;
const currentEstimateDistribution = distribution as TModuleEstimateDistribution;
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
@@ -114,9 +112,8 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
return (
<div>
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
<Tab.List
as="div"
<Tabs defaultValue={currentTab ?? "stat-assignees"} onValueChange={(value) => setModuleTab(value)}>
<Tabs.List
className={cn(
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
roundedTab ? `rounded-3xl` : `rounded-md`,
@@ -125,39 +122,38 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
)}
>
{PROGRESS_STATS.map((stat) => (
<Tab
<Tabs.Trigger
key={stat.key}
value={stat.key}
className={cn(
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`,
stat.key === currentTab
? "bg-custom-background-100 text-custom-text-300"
: "text-custom-text-400 hover:text-custom-text-300"
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-300",
"text-custom-text-400 hover:text-custom-text-300"
)}
key={stat.key}
onClick={() => setModuleTab(stat.key)}
>
{t(stat.i18n_title)}
</Tab>
</Tabs.Trigger>
))}
</Tab.List>
<Tab.Panels className="py-3 text-custom-text-200">
<Tab.Panel key={"stat-assignees"}>
</Tabs.List>
<div className="py-3 text-custom-text-200">
<Tabs.Content value="stat-assignees">
<AssigneeStatComponent
distribution={distributionAssigneeData}
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
isEditable={isEditable}
selectedAssigneeIds={selectedAssigneeIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-labels"}>
</Tabs.Content>
<Tabs.Content value="stat-labels">
<LabelStatComponent
distribution={distributionLabelData}
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
isEditable={isEditable}
selectedLabelIds={selectedLabelIds}
/>
</Tab.Panel>
<Tab.Panel key={"stat-states"}>
</Tabs.Content>
<Tabs.Content value="stat-states">
<StateGroupStatComponent
distribution={distributionStateData}
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
@@ -165,9 +161,9 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
selectedStateGroups={selectedStateGroups}
totalIssuesCount={totalIssuesCount}
/>
</Tab.Panel>
</Tab.Panels>
</Tab.Group>
</Tabs.Content>
</div>
</Tabs>
</div>
);
});

View File

@@ -2,9 +2,9 @@ import React, { useCallback } from "react";
import { observer } from "mobx-react";
import { useRouter, useSearchParams } from "next/navigation";
import { ArrowRightCircle } from "lucide-react";
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
import { Tooltip } from "@plane/propel/tooltip";
// hooks
import { useQueryParams } from "@/hooks/use-query-params";
@@ -20,7 +20,6 @@ import { PageNavigationPaneTabsList } from "./tabs-list";
import type { INavigationPaneExtension } from "./types/extensions";
import {
PAGE_NAVIGATION_PANE_TAB_KEYS,
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
PAGE_NAVIGATION_PANE_WIDTH,
@@ -49,7 +48,6 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
) as TPageNavigationPaneTab | null;
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
const selectedIndex = PAGE_NAVIGATION_PANE_TAB_KEYS.indexOf(activeTab);
// Check if any extension is currently active based on query parameters
const ActiveExtension = extensions.find(function ActiveExtension(extension) {
@@ -69,8 +67,8 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
const { t } = useTranslation();
const handleTabChange = useCallback(
(index: number) => {
const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index];
(value: string) => {
const updatedTab = value as TPageNavigationPaneTab;
const isUpdatedTabInfo = updatedTab === "info";
const updatedRoute = updateQueryParams({
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
@@ -106,10 +104,10 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
{ActiveExtension ? (
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
) : showNavigationTabs ? (
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
<Tabs value={activeTab} onValueChange={handleTabChange}>
<PageNavigationPaneTabsList />
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
</Tab.Group>
</Tabs>
) : null}
</div>
</aside>

View File

@@ -1,5 +1,6 @@
import React from "react";
import { Tab } from "@headlessui/react";
// plane imports
import { Tabs } from "@plane/propel/tabs";
// components
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
// plane web imports
@@ -21,19 +22,19 @@ export function PageNavigationPaneTabPanelsRoot(props: Props) {
const { page, versionHistory } = props;
return (
<Tab.Panels as={React.Fragment}>
<>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab.Panel
<Tabs.Content
key={tab.key}
as="div"
value={tab.key}
className="size-full p-3.5 pt-0 overflow-y-auto vertical-scrollbar scrollbar-sm outline-none"
>
{tab.key === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
<PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} page={page} />
</Tab.Panel>
</Tabs.Content>
))}
</Tab.Panels>
</>
);
}

View File

@@ -1,6 +1,6 @@
import { Tab } from "@headlessui/react";
// plane imports
import { useTranslation } from "@plane/i18n";
import { Tabs } from "@plane/propel/tabs";
// plane web components
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
@@ -9,29 +9,19 @@ export function PageNavigationPaneTabsList() {
const { t } = useTranslation();
return (
<Tab.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80 mx-3.5">
{({ selectedIndex }) => (
<>
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tab
key={tab.key}
type="button"
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
>
{t(tab.i18n_label)}
</Tab>
))}
{/* active tab indicator */}
<div
className="absolute top-1/2 -translate-y-1/2 bg-custom-background-90 rounded transition-all duration-500 ease-in-out pointer-events-none"
style={{
left: `calc(${(selectedIndex / ORDERED_PAGE_NAVIGATION_TABS_LIST.length) * 100}% + 2px)`,
height: "calc(100% - 4px)",
width: `calc(${100 / ORDERED_PAGE_NAVIGATION_TABS_LIST.length}% - 4px)`,
}}
/>
</>
)}
</Tab.List>
<div className="mx-3.5">
<Tabs.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80">
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
<Tabs.Trigger
key={tab.key}
value={tab.key}
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
>
{t(tab.i18n_label)}
</Tabs.Trigger>
))}
<Tabs.Indicator className="absolute top-1/2 -translate-y-1/2 bg-custom-background-90 rounded transition-all duration-500 ease-in-out pointer-events-none" />
</Tabs.List>
</div>
);
}

View File

@@ -25,7 +25,6 @@ export * from "./scroll-area";
export * from "./sortable";
export * from "./spinners";
export * from "./tables";
export * from "./tabs";
export * from "./tag";
export * from "./tooltip";
export * from "./typography";

View File

@@ -1,2 +0,0 @@
export * from "./tabs";
export * from "./tab-list";

View File

@@ -1,74 +0,0 @@
import { Tab } from "@headlessui/react";
import type { LucideProps } from "lucide-react";
import type { FC } from "react";
import React from "react";
// helpers
import { cn } from "../utils";
export type TabListItem = {
key: string;
icon?: FC<LucideProps>;
label?: React.ReactNode;
disabled?: boolean;
onClick?: () => void;
};
type TTabListProps = {
tabs: TabListItem[];
tabListClassName?: string;
tabClassName?: string;
size?: "sm" | "md" | "lg";
selectedTab?: string;
onTabChange?: (key: string) => void;
};
export function TabList({
tabs,
tabListClassName,
tabClassName,
size = "md",
selectedTab,
onTabChange,
}: TTabListProps) {
return (
<Tab.List
as="div"
className={cn(
"flex w-full min-w-fit items-center justify-between gap-1.5 rounded-md text-sm p-0.5 bg-custom-background-80/60",
tabListClassName
)}
>
{tabs.map((tab) => (
<Tab
className={({ selected }) =>
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}
onClick={() => {
if (!tab.disabled) {
onTabChange?.(tab.key);
tab.onClick?.();
}
}}
disabled={tab.disabled}
>
{tab.icon && <tab.icon className="size-4" />}
{tab.label}
</Tab>
))}
</Tab.List>
);
}

View File

@@ -1,90 +0,0 @@
import { Tab } from "@headlessui/react";
import type { FC } from "react";
import React, { Fragment, useEffect, useState } from "react";
// helpers
import { useLocalStorage } from "@plane/hooks";
import { cn } from "../utils";
// types
import type { TabListItem } from "./tab-list";
import { TabList } from "./tab-list";
export type TabContent = {
content: React.ReactNode;
};
export type TabItem = TabListItem & TabContent;
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;
};
export function Tabs(props: TTabsProps) {
const {
tabs,
storageKey,
actions,
defaultTab = tabs[0]?.key,
containerClassName = "",
tabListContainerClassName = "",
tabListClassName = "",
tabClassName = "",
tabPanelClassName = "",
size = "md",
storeInLocalStorage = true,
} = props;
// local storage
const { storedValue, setValue } = useLocalStorage(
storeInLocalStorage && storageKey ? `tab-${storageKey}` : `tab-${tabs[0]?.key}`,
defaultTab
);
// state
const [selectedTab, setSelectedTab] = useState(storedValue ?? defaultTab);
useEffect(() => {
if (storeInLocalStorage) {
setValue(selectedTab);
}
}, [selectedTab, setValue, storeInLocalStorage, storageKey]);
const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey);
const handleTabChange = (key: string) => {
setSelectedTab(key);
};
return (
<div className="flex flex-col w-full h-full">
<Tab.Group defaultIndex={currentTabIndex(selectedTab)}>
<div className={cn("flex flex-col w-full h-full gap-2", containerClassName)}>
<div className={cn("flex w-full items-center gap-4", tabListContainerClassName)}>
<TabList
tabs={tabs}
tabListClassName={tabListClassName}
tabClassName={tabClassName}
size={size}
onTabChange={handleTabChange}
/>
{actions && <div className="flex-grow">{actions}</div>}
</div>
<Tab.Panels as={Fragment}>
{tabs.map((tab) => (
<Tab.Panel key={tab.key} as="div" className={cn("relative outline-none", tabPanelClassName)}>
{tab.content}
</Tab.Panel>
))}
</Tab.Panels>
</div>
</Tab.Group>
</div>
);
}