diff --git a/packages/ui/src/avatar/avatar.stories.tsx b/packages/ui/src/avatar/avatar.stories.tsx index e19f4c2627f..c3053cd738d 100644 --- a/packages/ui/src/avatar/avatar.stories.tsx +++ b/packages/ui/src/avatar/avatar.stories.tsx @@ -1,5 +1,4 @@ import type { Meta, StoryObj } from "@storybook/react"; -import { fn } from "@storybook/test"; import { Avatar } from "./avatar"; const meta: Meta = { diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 59e7d609f47..34a0de26c92 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -21,3 +21,4 @@ export * from "./drop-indicator"; export * from "./favorite-star"; export * from "./loader"; export * from "./collapsible"; +export * from "./popovers"; diff --git a/packages/ui/src/popovers/index.ts b/packages/ui/src/popovers/index.ts new file mode 100644 index 00000000000..4a83d311474 --- /dev/null +++ b/packages/ui/src/popovers/index.ts @@ -0,0 +1,2 @@ +export * from "./popover"; +export * from "./popover-menu"; diff --git a/packages/ui/src/popovers/popover-menu.stories.tsx b/packages/ui/src/popovers/popover-menu.stories.tsx new file mode 100644 index 00000000000..14508d16810 --- /dev/null +++ b/packages/ui/src/popovers/popover-menu.stories.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import type { Meta, StoryObj } from "@storybook/react"; +import { PopoverMenu } from "./popover-menu"; + +const meta: Meta = { + title: "PopoverMenu", + component: PopoverMenu, +}; + +export default meta; + +// types +type TPopoverMenu = { + id: number; + name: string; +}; + +type Story = StoryObj>; + +// data +const data: TPopoverMenu[] = [ + { id: 1, name: "John Doe" }, + { id: 2, name: "Jane Doe" }, + { id: 3, name: "John Smith" }, + { id: 4, name: "Jane Smith" }, +]; + +// components +const PopoverMenuItemRender = (item: TPopoverMenu) => ( +
+ {item.name} +
+); + +// stories +export const Default: Story = { + args: { + popperPosition: "bottom-start", + panelClassName: "rounded bg-gray-100 p-2", + data: data, + keyExtractor: (item, index: number) => `${item.id}-${index}`, + render: (item) => PopoverMenuItemRender(item), + }, +}; diff --git a/packages/ui/src/popovers/popover-menu.tsx b/packages/ui/src/popovers/popover-menu.tsx new file mode 100644 index 00000000000..e1b44f09b97 --- /dev/null +++ b/packages/ui/src/popovers/popover-menu.tsx @@ -0,0 +1,39 @@ +import React, { Fragment } from "react"; +// components +import { Popover } from "./popover"; +// helpers +import { cn } from "../../helpers"; +// types +import { TPopoverMenu } from "./types"; + +export const PopoverMenu = (props: TPopoverMenu) => { + const { + popperPosition = "bottom-end", + popperPadding = 0, + buttonClassName = "", + button, + panelClassName = "", + data, + keyExtractor, + render, + } = props; + + return ( + + + {data.map((item, index) => ( + {render(item, index)} + ))} + + + ); +}; diff --git a/packages/ui/src/popovers/popover.stories.tsx b/packages/ui/src/popovers/popover.stories.tsx new file mode 100644 index 00000000000..241ff7ee6c4 --- /dev/null +++ b/packages/ui/src/popovers/popover.stories.tsx @@ -0,0 +1,54 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import React from "react"; +import { Popover } from "./popover"; + +const meta: Meta = { + title: "Popover", + component: Popover, +}; + +export default meta; + +// types +type Story = StoryObj; + +// data + +// components +const RenderCustomPopoverComponent = ( +
+
Your custom component
+
+ {["option1", "option2", "option3"].map((option) => ( +
+ {option} +
+ ))} +
+
+); + +// stories +export const Default: Story = { + args: { + popperPosition: "bottom-start", + panelClassName: "rounded bg-gray-100 p-2", + children: RenderCustomPopoverComponent, + }, +}; + +export const CustomMenuButton: Story = { + args: { + popperPosition: "bottom-start", + button: ( +
+ Custom Menu Button +
+ ), + panelClassName: "rounded bg-gray-100 p-2", + children: RenderCustomPopoverComponent, + }, +}; diff --git a/packages/ui/src/popovers/popover.tsx b/packages/ui/src/popovers/popover.tsx new file mode 100644 index 00000000000..90d4bea7627 --- /dev/null +++ b/packages/ui/src/popovers/popover.tsx @@ -0,0 +1,73 @@ +import React, { Fragment, useState } from "react"; +import { usePopper } from "react-popper"; +import { Popover as HeadlessReactPopover, Transition } from "@headlessui/react"; +// helpers +import { cn } from "../../helpers"; +// types +import { TPopover } from "./types"; +import { EllipsisVertical } from "lucide-react"; + +export const Popover = (props: TPopover) => { + const { + popperPosition = "bottom-end", + popperPadding = 0, + buttonClassName = "", + button, + panelClassName = "", + children, + } = props; + // states + const [referenceElement, setReferenceElement] = useState(null); + const [popperElement, setPopperElement] = useState(null); + + // react-popper derived values + const { styles, attributes } = usePopper(referenceElement, popperElement, { + placement: popperPosition, + modifiers: [ + { + name: "preventOverflow", + options: { + padding: popperPadding, + }, + }, + ], + }); + + return ( + + + {button ? ( + button + ) : ( +
+ +
+ )} +
+ + + + {children} + + +
+ ); +}; diff --git a/packages/ui/src/popovers/types.ts b/packages/ui/src/popovers/types.ts new file mode 100644 index 00000000000..be232036047 --- /dev/null +++ b/packages/ui/src/popovers/types.ts @@ -0,0 +1,27 @@ +import { ReactNode } from "react"; +import { Placement } from "@popperjs/core"; + +export type TPopoverButtonDefaultOptions = { + // button and button styling + button?: ReactNode; + buttonClassName?: string; +}; + +export type TPopoverDefaultOptions = TPopoverButtonDefaultOptions & { + // popper styling + popperPosition?: Placement | undefined; + popperPadding?: number | undefined; + // panel styling + panelClassName?: string; +}; + +export type TPopover = TPopoverDefaultOptions & { + // children + children: ReactNode; +}; + +export type TPopoverMenu = TPopoverDefaultOptions & { + data: T[]; + keyExtractor: (item: T, index: number) => string; + render: (item: T, index: number) => ReactNode; +}; diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx index ac14c5019c4..8553d86aa51 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/layout.tsx @@ -56,7 +56,7 @@ const ProjectSettingLayout: FC = observer((props) => {
-
+
{children}
diff --git a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx index 50b0c0abcaf..4132662bf02 100644 --- a/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx +++ b/web/app/[workspaceSlug]/(projects)/projects/(detail)/[projectId]/settings/states/page.tsx @@ -18,7 +18,7 @@ const StatesSettingsPage = observer(() => { return ( <> -
+

States

diff --git a/web/core/components/project-states/create-update/form.tsx b/web/core/components/project-states/create-update/form.tsx index 6a26d7cdd2d..73d638ff219 100644 --- a/web/core/components/project-states/create-update/form.tsx +++ b/web/core/components/project-states/create-update/form.tsx @@ -1,10 +1,9 @@ "use client"; -import { FormEvent, FC, useEffect, useState, Fragment } from "react"; +import { FormEvent, FC, useEffect, useState, useMemo } from "react"; import { TwitterPicker } from "react-color"; -import { Popover, Transition } from "@headlessui/react"; import { IState } from "@plane/types"; -import { Button, Input } from "@plane/ui"; +import { Button, Popover, Input } from "@plane/ui"; type TStateForm = { data: Partial; @@ -47,30 +46,24 @@ export const StateForm: FC = (props) => { } }; + const PopoverButton = useMemo( + () => ( +
+ ), + [formData?.color] + ); + return (
{/* color */}
- - - - - handleFormData("color", value.hex)} /> - - + + handleFormData("color", value.hex)} />