diff --git a/packages/helpers/hooks/index.ts b/packages/helpers/hooks/index.ts index c7a8f4c06b8..c07642907fe 100644 --- a/packages/helpers/hooks/index.ts +++ b/packages/helpers/hooks/index.ts @@ -1 +1,2 @@ +export * from "./use-local-storage"; export * from "./use-outside-click-detector"; diff --git a/packages/helpers/hooks/use-local-storage.tsx b/packages/helpers/hooks/use-local-storage.tsx new file mode 100644 index 00000000000..f04e0e71ba7 --- /dev/null +++ b/packages/helpers/hooks/use-local-storage.tsx @@ -0,0 +1,59 @@ +import { useState, useEffect, useCallback } from "react"; + +export const getValueFromLocalStorage = (key: string, defaultValue: any) => { + if (typeof window === undefined || typeof window === "undefined") + return defaultValue; + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : defaultValue; + } catch (error) { + window.localStorage.removeItem(key); + return defaultValue; + } +}; + +export const setValueIntoLocalStorage = (key: string, value: any) => { + if (typeof window === undefined || typeof window === "undefined") + return false; + try { + window.localStorage.setItem(key, JSON.stringify(value)); + return true; + } catch (error) { + return false; + } +}; + +export const useLocalStorage = (key: string, initialValue: T) => { + const [storedValue, setStoredValue] = useState(() => + getValueFromLocalStorage(key, initialValue) + ); + + const setValue = useCallback( + (value: T) => { + window.localStorage.setItem(key, JSON.stringify(value)); + setStoredValue(value); + window.dispatchEvent(new Event(`local-storage:${key}`)); + }, + [key] + ); + + const clearValue = useCallback(() => { + window.localStorage.removeItem(key); + setStoredValue(null); + window.dispatchEvent(new Event(`local-storage:${key}`)); + }, [key]); + + const reHydrate = useCallback(() => { + const data = getValueFromLocalStorage(key, initialValue); + setStoredValue(data); + }, [key, initialValue]); + + useEffect(() => { + window.addEventListener(`local-storage:${key}`, reHydrate); + return () => { + window.removeEventListener(`local-storage:${key}`, reHydrate); + }; + }, [key, reHydrate]); + + return { storedValue, setValue, clearValue } as const; +}; diff --git a/packages/ui/src/collapsible/collapsible-button.tsx b/packages/ui/src/collapsible/collapsible-button.tsx index a56a724b4cb..2a141aa41cb 100644 --- a/packages/ui/src/collapsible/collapsible-button.tsx +++ b/packages/ui/src/collapsible/collapsible-button.tsx @@ -8,12 +8,27 @@ type Props = { hideChevron?: boolean; indicatorElement?: React.ReactNode; actionItemElement?: React.ReactNode; + className?: string; + titleClassName?: string; }; export const CollapsibleButton: FC = (props) => { - const { isOpen, title, hideChevron = false, indicatorElement, actionItemElement } = props; + const { + isOpen, + title, + hideChevron = false, + indicatorElement, + actionItemElement, + className = "", + titleClassName = "", + } = props; return ( -
+
{!hideChevron && ( @@ -23,7 +38,7 @@ export const CollapsibleButton: FC = (props) => { })} /> )} - {title} + {title}
{indicatorElement && indicatorElement}
diff --git a/packages/ui/src/icons/comment-fill-icon.tsx b/packages/ui/src/icons/comment-fill-icon.tsx new file mode 100644 index 00000000000..8825fbc7933 --- /dev/null +++ b/packages/ui/src/icons/comment-fill-icon.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const CommentFillIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + +); diff --git a/packages/ui/src/icons/epic-icon.tsx b/packages/ui/src/icons/epic-icon.tsx new file mode 100644 index 00000000000..c78a98261ba --- /dev/null +++ b/packages/ui/src/icons/epic-icon.tsx @@ -0,0 +1,28 @@ +import * as React from "react"; + +export type Props = { + className?: string; + width?: string | number; + height?: string | number; + color?: string; +}; + +export const EpicIcon: React.FC = ({ width = "16", height = "16", className }) => ( + + + + +); diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts index 91ae0e2f190..1402dedb004 100644 --- a/packages/ui/src/icons/index.ts +++ b/packages/ui/src/icons/index.ts @@ -7,12 +7,15 @@ export * from "./blocker-icon"; export * from "./calendar-after-icon"; export * from "./calendar-before-icon"; export * from "./center-panel-icon"; +export * from "./comment-fill-icon"; export * from "./create-icon"; export * from "./dice-icon"; export * from "./discord-icon"; +export * from "./epic-icon"; export * from "./full-screen-panel-icon"; export * from "./github-icon"; export * from "./gitlab-icon"; +export * from "./info-icon"; export * from "./layer-stack"; export * from "./layers-icon"; export * from "./monospace-icon"; diff --git a/packages/ui/src/icons/info-fill-icon.tsx b/packages/ui/src/icons/info-fill-icon.tsx new file mode 100644 index 00000000000..c7416e45d6a --- /dev/null +++ b/packages/ui/src/icons/info-fill-icon.tsx @@ -0,0 +1,14 @@ +import * as React from "react"; + +import { ISvgIcons } from "./type"; + +export const InfoFillIcon: React.FC = ({ className = "text-current", ...rest }) => ( + + + +); diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 0af3b0469d1..279e19a3e34 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -28,3 +28,4 @@ export * from "./row"; export * from "./content-wrapper"; export * from "./card"; export * from "./tag"; +export * from "./tabs"; diff --git a/packages/ui/src/progress/linear-progress-indicator.tsx b/packages/ui/src/progress/linear-progress-indicator.tsx index de8af4cbcc8..e32087d49c7 100644 --- a/packages/ui/src/progress/linear-progress-indicator.tsx +++ b/packages/ui/src/progress/linear-progress-indicator.tsx @@ -6,7 +6,9 @@ type Props = { data: any; noTooltip?: boolean; inPercentage?: boolean; - size?: "sm" | "md" | "lg"; + size?: "sm" | "md" | "lg" | "xl"; + className?: string; + barClassName?: string; }; export const LinearProgressIndicator: React.FC = ({ @@ -14,6 +16,8 @@ export const LinearProgressIndicator: React.FC = ({ noTooltip = false, inPercentage = false, size = "sm", + className = "", + barClassName = "", }) => { const total = data.reduce((acc: any, cur: any) => acc + cur.value, 0); // eslint-disable-next-line @typescript-eslint/no-unused-vars @@ -31,7 +35,7 @@ export const LinearProgressIndicator: React.FC = ({ else return ( -
+
); }); @@ -42,13 +46,12 @@ export const LinearProgressIndicator: React.FC = ({ "h-2": size === "sm", "h-3": size === "md", "h-3.5": size === "lg", + "h-[14px]": size === "xl", })} > - {total === 0 ? ( -
{bars}
- ) : ( -
{bars}
- )} +
+ {bars} +
); }; diff --git a/packages/ui/src/tabs/index.ts b/packages/ui/src/tabs/index.ts new file mode 100644 index 00000000000..811d3d4a725 --- /dev/null +++ b/packages/ui/src/tabs/index.ts @@ -0,0 +1 @@ +export * from "./tabs"; diff --git a/packages/ui/src/tabs/tabs.tsx b/packages/ui/src/tabs/tabs.tsx new file mode 100644 index 00000000000..e2fe8602c17 --- /dev/null +++ b/packages/ui/src/tabs/tabs.tsx @@ -0,0 +1,94 @@ +import React, { FC, Fragment } from "react"; +import { Tab } from "@headlessui/react"; +import { LucideProps } from "lucide-react"; +// helpers +import { useLocalStorage } from "@plane/helpers"; +import { cn } from "../../helpers"; + +type TabItem = { + key: string; + icon?: FC; + label?: React.ReactNode; + content: React.ReactNode; + disabled?: boolean; +}; + +type TTabsProps = { + tabs: TabItem[]; + storageKey: string; + actions?: React.ReactNode; + defaultTab?: string; + containerClassName?: string; + tabListContainerClassName?: string; + tabListClassName?: string; + tabClassName?: string; + tabPanelClassName?: string; +}; + +export const Tabs: FC = (props: TTabsProps) => { + const { + tabs, + storageKey, + actions, + defaultTab = tabs[0]?.key, + containerClassName = "", + tabListContainerClassName = "", + tabListClassName = "", + tabClassName = "", + tabPanelClassName = "", + } = props; + // local storage + const { storedValue, setValue } = useLocalStorage(`tab-${storageKey}`, defaultTab); + + const currentTabIndex = (tabKey: string): number => tabs.findIndex((tab) => tab.key === tabKey); + + return ( +
+ +
+
+ + {tabs.map((tab) => ( + + cn( + `flex items-center justify-center p-1 min-w-fit w-full font-medium text-custom-text-100 outline-none focus:outline-none cursor-pointer transition-all rounded`, + 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", + tabClassName + ) + } + key={tab.key} + onClick={() => { + if (!tab.disabled) setValue(tab.key); + }} + disabled={tab.disabled} + > + {tab.icon && } + {tab.label} + + ))} + + {actions &&
{actions}
} +
+ + {tabs.map((tab) => ( + + {tab.content} + + ))} + +
+
+
+ ); +};