mirror of
https://github.com/makeplane/plane.git
synced 2025-12-16 20:07:56 +01:00
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:
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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,25 +198,22 @@ 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">
|
||||
</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}
|
||||
@@ -278,10 +276,10 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
||||
<Loader.Item height="80px" width="100%" />
|
||||
</Loader>
|
||||
)}
|
||||
</>
|
||||
</Tabs.Content>
|
||||
)}
|
||||
</Tab.Panel>
|
||||
<Tab.Panel className="mt-4 h-full w-full space-y-4">
|
||||
|
||||
<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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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 }) => (
|
||||
<>
|
||||
<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) => (
|
||||
<Tab
|
||||
<Tabs.Trigger
|
||||
key={tab.key}
|
||||
type="button"
|
||||
value={tab.key}
|
||||
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
|
||||
>
|
||||
{t(tab.i18n_label)}
|
||||
</Tab>
|
||||
</Tabs.Trigger>
|
||||
))}
|
||||
{/* 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>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./tabs";
|
||||
export * from "./tab-list";
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user