mirror of
https://github.com/makeplane/plane.git
synced 2025-12-23 14:20:40 -06: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 (
|
return (
|
||||||
<Tab.Group as={React.Fragment}>
|
<div className="flex flex-col gap-14 overflow-y-auto p-6">
|
||||||
<div className="flex flex-col gap-14 overflow-y-auto p-6">
|
<TotalInsights analyticsType="work-items" peekView={!fullScreen} />
|
||||||
<TotalInsights analyticsType="work-items" peekView={!fullScreen} />
|
<CreatedVsResolved />
|
||||||
<CreatedVsResolved />
|
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
|
||||||
<CustomizedInsights peekView={!fullScreen} isEpic={isEpic} />
|
<WorkItemsInsightTable />
|
||||||
<WorkItemsInsightTable />
|
</div>
|
||||||
</div>
|
|
||||||
</Tab.Group>
|
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import { useDropzone } from "react-dropzone";
|
|||||||
import type { Control } from "react-hook-form";
|
import type { Control } from "react-hook-form";
|
||||||
import { Controller } from "react-hook-form";
|
import { Controller } from "react-hook-form";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { Tab, Popover } from "@headlessui/react";
|
import { Popover } from "@headlessui/react";
|
||||||
// plane imports
|
// plane imports
|
||||||
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
|
import { ACCEPTED_COVER_IMAGE_MIME_TYPES_FOR_REACT_DROPZONE, MAX_FILE_SIZE } from "@plane/constants";
|
||||||
import { useOutsideClickDetector } from "@plane/hooks";
|
import { useOutsideClickDetector } from "@plane/hooks";
|
||||||
import { Button } from "@plane/propel/button";
|
import { Button } from "@plane/propel/button";
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
import { TOAST_TYPE, setToast } from "@plane/propel/toast";
|
||||||
import { EFileAssetType } from "@plane/types";
|
import { EFileAssetType } from "@plane/types";
|
||||||
import { Input, Loader } from "@plane/ui";
|
import { Input, Loader } from "@plane/ui";
|
||||||
@@ -197,91 +198,88 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
|||||||
ref={imagePickerRef}
|
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]"
|
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>
|
<Tabs defaultValue={tabOptions[0].key} className={"h-full overflow-hidden"}>
|
||||||
<Tab.List as="span" className="inline-block rounded bg-custom-background-80 p-1">
|
<Tabs.List className="p-1 w-fit">
|
||||||
{tabOptions.map((tab) => (
|
{tabOptions.map((tab) => (
|
||||||
<Tab
|
<Tabs.Trigger
|
||||||
key={tab.key}
|
key={tab.key}
|
||||||
className={({ selected }) =>
|
value={tab.key}
|
||||||
`rounded px-4 py-1 text-center text-sm outline-none transition-colors ${
|
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"
|
||||||
selected ? "bg-custom-primary text-white" : "text-custom-text-100"
|
|
||||||
}`
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
{tab.title}
|
{tab.title}
|
||||||
</Tab>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tabs.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">
|
<div className="mt-4 flex-1 overflow-auto">
|
||||||
{(unsplashImages || !unsplashError) && (
|
{(unsplashImages || !unsplashError) && (
|
||||||
<>
|
<Tabs.Content className="h-full w-full space-y-4" value="unsplash">
|
||||||
<div className="flex gap-x-2">
|
<div className="flex gap-x-2">
|
||||||
<Controller
|
<Controller
|
||||||
control={control}
|
control={control}
|
||||||
name="search"
|
name="search"
|
||||||
render={({ field: { value, ref } }) => (
|
render={({ field: { value, ref } }) => (
|
||||||
<Input
|
<Input
|
||||||
id="search"
|
id="search"
|
||||||
name="search"
|
name="search"
|
||||||
type="text"
|
type="text"
|
||||||
onKeyDown={(e) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setSearchParams(formData.search);
|
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 })}
|
<img
|
||||||
ref={ref}
|
src={image.urls.small}
|
||||||
placeholder="Search for images"
|
alt={image.alt_description}
|
||||||
className="w-full text-sm"
|
className="absolute left-0 top-0 h-full w-full cursor-pointer rounded object-cover"
|
||||||
/>
|
/>
|
||||||
)}
|
</div>
|
||||||
/>
|
))}
|
||||||
<Button variant="primary" onClick={() => setSearchParams(formData.search)} size="sm">
|
</div>
|
||||||
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>
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
<Loader className="grid grid-cols-4 gap-4">
|
<p className="pt-7 text-center text-xs text-custom-text-300">No images found.</p>
|
||||||
<Loader.Item height="80px" width="100%" />
|
)
|
||||||
<Loader.Item height="80px" width="100%" />
|
) : (
|
||||||
<Loader.Item height="80px" width="100%" />
|
<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.Item height="80px" width="100%" />
|
<Loader.Item height="80px" width="100%" />
|
||||||
</Loader>
|
<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">
|
</Tabs.Content>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Tabs.Content className="h-full w-full space-y-4" value="images">
|
||||||
<div className="grid grid-cols-4 gap-4">
|
<div className="grid grid-cols-4 gap-4">
|
||||||
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
|
{Object.values(STATIC_COVER_IMAGES).map((imageUrl, index) => (
|
||||||
<div
|
<div
|
||||||
@@ -297,8 +295,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
<Tab.Panel className="mt-4 h-full w-full">
|
|
||||||
|
<Tabs.Content className="h-full w-full" value="upload">
|
||||||
<div className="flex h-full w-full flex-col gap-y-2">
|
<div className="flex h-full w-full flex-col gap-y-2">
|
||||||
<div className="flex w-full flex-1 items-center gap-3">
|
<div className="flex w-full flex-1 items-center gap-3">
|
||||||
<div
|
<div
|
||||||
@@ -365,9 +364,9 @@ export const ImagePickerPopover = observer(function ImagePickerPopover(props: Pr
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
</Tab.Panels>
|
</div>
|
||||||
</Tab.Group>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</Popover.Panel>
|
</Popover.Panel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
import type { TWorkItemFilterCondition } from "@plane/shared-state";
|
import type { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||||
import type { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
|
import type { TCycleDistribution, TCycleEstimateDistribution, TCyclePlotType } from "@plane/types";
|
||||||
import { cn, toFilterArray } from "@plane/utils";
|
import { cn, toFilterArray } from "@plane/utils";
|
||||||
@@ -54,8 +54,6 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
|
|||||||
`cycle-analytics-tab-${cycleId}`,
|
`cycle-analytics-tab-${cycleId}`,
|
||||||
"stat-assignees"
|
"stat-assignees"
|
||||||
);
|
);
|
||||||
// derived values
|
|
||||||
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
|
|
||||||
const currentDistribution = distribution as TCycleDistribution;
|
const currentDistribution = distribution as TCycleDistribution;
|
||||||
const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
|
const currentEstimateDistribution = distribution as TCycleEstimateDistribution;
|
||||||
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
|
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
|
||||||
@@ -116,9 +114,8 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
|
<Tabs defaultValue={currentTab ?? "stat-assignees"} onValueChange={(value) => setCycleTab(value)}>
|
||||||
<Tab.List
|
<Tabs.List
|
||||||
as="div"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
|
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
|
||||||
roundedTab ? `rounded-3xl` : `rounded-md`,
|
roundedTab ? `rounded-3xl` : `rounded-md`,
|
||||||
@@ -127,23 +124,22 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PROGRESS_STATS.map((stat) => (
|
{PROGRESS_STATS.map((stat) => (
|
||||||
<Tab
|
<Tabs.Trigger
|
||||||
|
key={stat.key}
|
||||||
|
value={stat.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
|
`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`,
|
roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`,
|
||||||
stat.key === currentTab
|
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-300",
|
||||||
? "bg-custom-background-100 text-custom-text-300"
|
"text-custom-text-400 hover:text-custom-text-300"
|
||||||
: "text-custom-text-400 hover:text-custom-text-300"
|
|
||||||
)}
|
)}
|
||||||
key={stat.key}
|
|
||||||
onClick={() => setCycleTab(stat.key)}
|
|
||||||
>
|
>
|
||||||
{t(stat.i18n_title)}
|
{t(stat.i18n_title)}
|
||||||
</Tab>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tabs.List>
|
||||||
<Tab.Panels className="py-3 text-custom-text-200">
|
<div className="py-3 text-custom-text-200">
|
||||||
<Tab.Panel key={"stat-states"}>
|
<Tabs.Content value="stat-states">
|
||||||
<StateGroupStatComponent
|
<StateGroupStatComponent
|
||||||
distribution={distributionStateData}
|
distribution={distributionStateData}
|
||||||
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
|
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
|
||||||
@@ -151,25 +147,25 @@ export const CycleProgressStats = observer(function CycleProgressStats(props: TC
|
|||||||
selectedStateGroups={selectedStateGroups}
|
selectedStateGroups={selectedStateGroups}
|
||||||
totalIssuesCount={totalIssuesCount}
|
totalIssuesCount={totalIssuesCount}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
<Tab.Panel key={"stat-assignees"}>
|
<Tabs.Content value="stat-assignees">
|
||||||
<AssigneeStatComponent
|
<AssigneeStatComponent
|
||||||
distribution={distributionAssigneeData}
|
distribution={distributionAssigneeData}
|
||||||
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
|
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
selectedAssigneeIds={selectedAssigneeIds}
|
selectedAssigneeIds={selectedAssigneeIds}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
<Tab.Panel key={"stat-labels"}>
|
<Tabs.Content value="stat-labels">
|
||||||
<LabelStatComponent
|
<LabelStatComponent
|
||||||
distribution={distributionLabelData}
|
distribution={distributionLabelData}
|
||||||
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
|
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
selectedLabelIds={selectedLabelIds}
|
selectedLabelIds={selectedLabelIds}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
</Tab.Panels>
|
</div>
|
||||||
</Tab.Group>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import type { FC } from "react";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { CheckCircle } from "lucide-react";
|
import { CheckCircle } from "lucide-react";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
// helpers
|
// helpers
|
||||||
import type { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types";
|
import type { EProductSubscriptionEnum, TBillingFrequency, TSubscriptionPrice } from "@plane/types";
|
||||||
import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui";
|
import { getSubscriptionBackgroundColor, getUpgradeCardVariantStyle } from "@plane/ui";
|
||||||
@@ -39,32 +39,27 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("flex flex-col py-6 px-3", upgradeCardVariantStyle)}>
|
<div className={cn("flex flex-col py-6 px-3", upgradeCardVariantStyle)}>
|
||||||
<Tab.Group selectedIndex={selectedPlan === "month" ? 0 : 1}>
|
<Tabs value={selectedPlan} onValueChange={(value) => setSelectedPlan(value as TBillingFrequency)}>
|
||||||
<div className="flex w-full justify-center h-9">
|
<div className="flex w-full justify-center">
|
||||||
<Tab.List
|
<Tabs.List className={cn("flex rounded-md w-60", getSubscriptionBackgroundColor(planVariant, "50"))}>
|
||||||
className={cn("flex space-x-1 rounded-md p-0.5 w-60", getSubscriptionBackgroundColor(planVariant, "50"))}
|
|
||||||
>
|
|
||||||
{prices.map((price: TSubscriptionPrice) => (
|
{prices.map((price: TSubscriptionPrice) => (
|
||||||
<Tab
|
<Tabs.Trigger
|
||||||
key={price.key}
|
key={price.key}
|
||||||
className={({ selected }) =>
|
value={price.recurring}
|
||||||
cn(
|
className={cn(
|
||||||
"w-full rounded py-1 text-sm font-medium leading-5",
|
"w-full rounded text-sm font-medium leading-5 py-2",
|
||||||
selected
|
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-100 data-[selected]:shadow",
|
||||||
? "bg-custom-background-100 text-custom-text-100 shadow"
|
"text-custom-text-300 hover:text-custom-text-200"
|
||||||
: "text-custom-text-300 hover:text-custom-text-200"
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
onClick={() => setSelectedPlan(price.recurring)}
|
|
||||||
>
|
>
|
||||||
{renderPriceContent(price)}
|
{renderPriceContent(price)}
|
||||||
</Tab>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tabs.List>
|
||||||
</div>
|
</div>
|
||||||
<Tab.Panels>
|
<div>
|
||||||
{prices.map((price: TSubscriptionPrice) => (
|
{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="pt-6 text-center">
|
||||||
<div className="text-xl font-medium">Plane {planeName}</div>
|
<div className="text-xl font-medium">Plane {planeName}</div>
|
||||||
{renderActionButton(price)}
|
{renderActionButton(price)}
|
||||||
@@ -88,10 +83,10 @@ export const BasePaidPlanCard = observer(function BasePaidPlanCard(props: TBaseP
|
|||||||
</ul>
|
</ul>
|
||||||
{extraFeatures && <div>{extraFeatures}</div>}
|
{extraFeatures && <div>{extraFeatures}</div>}
|
||||||
</div>
|
</div>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
))}
|
))}
|
||||||
</Tab.Panels>
|
</div>
|
||||||
</Tab.Group>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -73,7 +73,12 @@ export const PlanUpgradeCard = observer(function PlanUpgradeCard(props: PlanUpgr
|
|||||||
<>
|
<>
|
||||||
Yearly
|
Yearly
|
||||||
{yearlyDiscount > 0 && (
|
{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}%
|
-{yearlyDiscount}%
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -114,7 +114,7 @@ export const ModuleAnalyticsProgress = observer(function ModuleAnalyticsProgress
|
|||||||
|
|
||||||
if (!moduleDetails) return <></>;
|
if (!moduleDetails) return <></>;
|
||||||
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}>
|
<Disclosure defaultOpen={isModuleDateValid ? true : false}>
|
||||||
{({ open }) => (
|
{({ open }) => (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
import type { TWorkItemFilterCondition } from "@plane/shared-state";
|
import type { TWorkItemFilterCondition } from "@plane/shared-state";
|
||||||
import type { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
|
import type { TModuleDistribution, TModuleEstimateDistribution, TModulePlotType } from "@plane/types";
|
||||||
import { cn, toFilterArray } from "@plane/utils";
|
import { cn, toFilterArray } from "@plane/utils";
|
||||||
@@ -52,8 +52,6 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
|
|||||||
`module-analytics-tab-${moduleId}`,
|
`module-analytics-tab-${moduleId}`,
|
||||||
"stat-assignees"
|
"stat-assignees"
|
||||||
);
|
);
|
||||||
// derived values
|
|
||||||
const currentTabIndex = (tab: string): number => PROGRESS_STATS.findIndex((stat) => stat.key === tab);
|
|
||||||
const currentDistribution = distribution as TModuleDistribution;
|
const currentDistribution = distribution as TModuleDistribution;
|
||||||
const currentEstimateDistribution = distribution as TModuleEstimateDistribution;
|
const currentEstimateDistribution = distribution as TModuleEstimateDistribution;
|
||||||
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
|
const selectedAssigneeIds = toFilterArray(selectedFilters?.assignees?.value || []) as string[];
|
||||||
@@ -114,9 +112,8 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Tab.Group defaultIndex={currentTabIndex(currentTab ? currentTab : "stat-assignees")}>
|
<Tabs defaultValue={currentTab ?? "stat-assignees"} onValueChange={(value) => setModuleTab(value)}>
|
||||||
<Tab.List
|
<Tabs.List
|
||||||
as="div"
|
|
||||||
className={cn(
|
className={cn(
|
||||||
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
|
`flex w-full items-center justify-between gap-2 rounded-md p-1`,
|
||||||
roundedTab ? `rounded-3xl` : `rounded-md`,
|
roundedTab ? `rounded-3xl` : `rounded-md`,
|
||||||
@@ -125,39 +122,38 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
|
|||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{PROGRESS_STATS.map((stat) => (
|
{PROGRESS_STATS.map((stat) => (
|
||||||
<Tab
|
<Tabs.Trigger
|
||||||
|
key={stat.key}
|
||||||
|
value={stat.key}
|
||||||
className={cn(
|
className={cn(
|
||||||
`p-1 w-full text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all`,
|
`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`,
|
roundedTab ? `rounded-3xl border border-custom-border-200` : `rounded`,
|
||||||
stat.key === currentTab
|
"data-[selected]:bg-custom-background-100 data-[selected]:text-custom-text-300",
|
||||||
? "bg-custom-background-100 text-custom-text-300"
|
"text-custom-text-400 hover:text-custom-text-300"
|
||||||
: "text-custom-text-400 hover:text-custom-text-300"
|
|
||||||
)}
|
)}
|
||||||
key={stat.key}
|
|
||||||
onClick={() => setModuleTab(stat.key)}
|
|
||||||
>
|
>
|
||||||
{t(stat.i18n_title)}
|
{t(stat.i18n_title)}
|
||||||
</Tab>
|
</Tabs.Trigger>
|
||||||
))}
|
))}
|
||||||
</Tab.List>
|
</Tabs.List>
|
||||||
<Tab.Panels className="py-3 text-custom-text-200">
|
<div className="py-3 text-custom-text-200">
|
||||||
<Tab.Panel key={"stat-assignees"}>
|
<Tabs.Content value="stat-assignees">
|
||||||
<AssigneeStatComponent
|
<AssigneeStatComponent
|
||||||
distribution={distributionAssigneeData}
|
distribution={distributionAssigneeData}
|
||||||
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
|
handleAssigneeFiltersUpdate={handleAssigneeFiltersUpdate}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
selectedAssigneeIds={selectedAssigneeIds}
|
selectedAssigneeIds={selectedAssigneeIds}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
<Tab.Panel key={"stat-labels"}>
|
<Tabs.Content value="stat-labels">
|
||||||
<LabelStatComponent
|
<LabelStatComponent
|
||||||
distribution={distributionLabelData}
|
distribution={distributionLabelData}
|
||||||
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
|
handleLabelFiltersUpdate={handleLabelFiltersUpdate}
|
||||||
isEditable={isEditable}
|
isEditable={isEditable}
|
||||||
selectedLabelIds={selectedLabelIds}
|
selectedLabelIds={selectedLabelIds}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
<Tab.Panel key={"stat-states"}>
|
<Tabs.Content value="stat-states">
|
||||||
<StateGroupStatComponent
|
<StateGroupStatComponent
|
||||||
distribution={distributionStateData}
|
distribution={distributionStateData}
|
||||||
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
|
handleStateGroupFiltersUpdate={handleStateGroupFiltersUpdate}
|
||||||
@@ -165,9 +161,9 @@ export const ModuleProgressStats = observer(function ModuleProgressStats(props:
|
|||||||
selectedStateGroups={selectedStateGroups}
|
selectedStateGroups={selectedStateGroups}
|
||||||
totalIssuesCount={totalIssuesCount}
|
totalIssuesCount={totalIssuesCount}
|
||||||
/>
|
/>
|
||||||
</Tab.Panel>
|
</Tabs.Content>
|
||||||
</Tab.Panels>
|
</div>
|
||||||
</Tab.Group>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ import React, { useCallback } from "react";
|
|||||||
import { observer } from "mobx-react";
|
import { observer } from "mobx-react";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { ArrowRightCircle } from "lucide-react";
|
import { ArrowRightCircle } from "lucide-react";
|
||||||
import { Tab } from "@headlessui/react";
|
|
||||||
// plane imports
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
import { Tooltip } from "@plane/propel/tooltip";
|
import { Tooltip } from "@plane/propel/tooltip";
|
||||||
// hooks
|
// hooks
|
||||||
import { useQueryParams } from "@/hooks/use-query-params";
|
import { useQueryParams } from "@/hooks/use-query-params";
|
||||||
@@ -20,7 +20,6 @@ import { PageNavigationPaneTabsList } from "./tabs-list";
|
|||||||
import type { INavigationPaneExtension } from "./types/extensions";
|
import type { INavigationPaneExtension } from "./types/extensions";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PAGE_NAVIGATION_PANE_TAB_KEYS,
|
|
||||||
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM,
|
||||||
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
|
PAGE_NAVIGATION_PANE_VERSION_QUERY_PARAM,
|
||||||
PAGE_NAVIGATION_PANE_WIDTH,
|
PAGE_NAVIGATION_PANE_WIDTH,
|
||||||
@@ -49,7 +48,6 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
|
|||||||
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
|
PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM
|
||||||
) as TPageNavigationPaneTab | null;
|
) as TPageNavigationPaneTab | null;
|
||||||
const activeTab: TPageNavigationPaneTab = navigationPaneQueryParam || "outline";
|
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
|
// Check if any extension is currently active based on query parameters
|
||||||
const ActiveExtension = extensions.find(function ActiveExtension(extension) {
|
const ActiveExtension = extensions.find(function ActiveExtension(extension) {
|
||||||
@@ -69,8 +67,8 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
const handleTabChange = useCallback(
|
||||||
(index: number) => {
|
(value: string) => {
|
||||||
const updatedTab = PAGE_NAVIGATION_PANE_TAB_KEYS[index];
|
const updatedTab = value as TPageNavigationPaneTab;
|
||||||
const isUpdatedTabInfo = updatedTab === "info";
|
const isUpdatedTabInfo = updatedTab === "info";
|
||||||
const updatedRoute = updateQueryParams({
|
const updatedRoute = updateQueryParams({
|
||||||
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
|
paramsToAdd: { [PAGE_NAVIGATION_PANE_TABS_QUERY_PARAM]: updatedTab },
|
||||||
@@ -106,10 +104,10 @@ export const PageNavigationPaneRoot = observer(function PageNavigationPaneRoot(p
|
|||||||
{ActiveExtension ? (
|
{ActiveExtension ? (
|
||||||
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
|
<ActiveExtension.component page={page} extensionData={ActiveExtension.data} storeType={storeType} />
|
||||||
) : showNavigationTabs ? (
|
) : showNavigationTabs ? (
|
||||||
<Tab.Group as={React.Fragment} selectedIndex={selectedIndex} onChange={handleTabChange}>
|
<Tabs value={activeTab} onValueChange={handleTabChange}>
|
||||||
<PageNavigationPaneTabsList />
|
<PageNavigationPaneTabsList />
|
||||||
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
|
<PageNavigationPaneTabPanelsRoot page={page} versionHistory={versionHistory} />
|
||||||
</Tab.Group>
|
</Tabs>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</aside>
|
</aside>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { Tab } from "@headlessui/react";
|
// plane imports
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
// components
|
// components
|
||||||
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
|
import type { TPageRootHandlers } from "@/components/pages/editor/page-root";
|
||||||
// plane web imports
|
// plane web imports
|
||||||
@@ -21,19 +22,19 @@ export function PageNavigationPaneTabPanelsRoot(props: Props) {
|
|||||||
const { page, versionHistory } = props;
|
const { page, versionHistory } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.Panels as={React.Fragment}>
|
<>
|
||||||
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
|
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
|
||||||
<Tab.Panel
|
<Tabs.Content
|
||||||
key={tab.key}
|
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"
|
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 === "outline" && <PageNavigationPaneOutlineTabPanel page={page} />}
|
||||||
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
|
{tab.key === "info" && <PageNavigationPaneInfoTabPanel page={page} versionHistory={versionHistory} />}
|
||||||
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
|
{tab.key === "assets" && <PageNavigationPaneAssetsTabPanel page={page} />}
|
||||||
<PageNavigationPaneAdditionalTabPanelsRoot activeTab={tab.key} 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
|
// plane imports
|
||||||
import { useTranslation } from "@plane/i18n";
|
import { useTranslation } from "@plane/i18n";
|
||||||
|
import { Tabs } from "@plane/propel/tabs";
|
||||||
// plane web components
|
// plane web components
|
||||||
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
import { ORDERED_PAGE_NAVIGATION_TABS_LIST } from "@/plane-web/components/pages/navigation-pane";
|
||||||
|
|
||||||
@@ -9,29 +9,19 @@ export function PageNavigationPaneTabsList() {
|
|||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tab.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80 mx-3.5">
|
<div className="mx-3.5">
|
||||||
{({ selectedIndex }) => (
|
<Tabs.List className="relative flex items-center p-[2px] rounded-md bg-custom-background-80">
|
||||||
<>
|
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
|
||||||
{ORDERED_PAGE_NAVIGATION_TABS_LIST.map((tab) => (
|
<Tabs.Trigger
|
||||||
<Tab
|
key={tab.key}
|
||||||
key={tab.key}
|
value={tab.key}
|
||||||
type="button"
|
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
|
||||||
className="relative z-[1] flex-1 py-1.5 text-sm font-semibold outline-none"
|
>
|
||||||
>
|
{t(tab.i18n_label)}
|
||||||
{t(tab.i18n_label)}
|
</Tabs.Trigger>
|
||||||
</Tab>
|
))}
|
||||||
))}
|
<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" />
|
||||||
{/* active tab indicator */}
|
</Tabs.List>
|
||||||
<div
|
</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>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export * from "./scroll-area";
|
|||||||
export * from "./sortable";
|
export * from "./sortable";
|
||||||
export * from "./spinners";
|
export * from "./spinners";
|
||||||
export * from "./tables";
|
export * from "./tables";
|
||||||
export * from "./tabs";
|
|
||||||
export * from "./tag";
|
export * from "./tag";
|
||||||
export * from "./tooltip";
|
export * from "./tooltip";
|
||||||
export * from "./typography";
|
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