This commit is contained in:
DCCONSTRUCTIONS
2026-04-18 18:39:25 +03:00
commit 3ba092b60c
4944 changed files with 497564 additions and 0 deletions
@@ -0,0 +1,8 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./popover";
export * from "./popover-menu";
@@ -0,0 +1,44 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { PopoverMenu } from "./popover-menu";
type TPopoverMenu = {
id: number;
name: string;
};
const meta: Meta<typeof PopoverMenu<TPopoverMenu>> = {
title: "Components/PopoverMenu",
component: PopoverMenu,
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
popperPosition: "bottom-start",
panelClassName: "rounded-sm bg-gray-100 p-2",
data: [
{ id: 1, name: "John Doe" },
{ id: 2, name: "Jane Doe" },
{ id: 3, name: "John Smith" },
{ id: 4, name: "Jane Smith" },
],
keyExtractor: (item, index: number) => `${item.id}-${index}`,
render: (item: TPopoverMenu) => (
<div className="text-gray-600 hover:text-gray-700 hover:bg-gray-200 cursor-pointer rounded-xs px-1.5 py-0.5 text-13 capitalize transition-all">
{item.name}
</div>
),
},
};
export default meta;
type Story = StoryObj<typeof PopoverMenu<TPopoverMenu>>;
export const Default: Story = {};
@@ -0,0 +1,51 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { Fragment } from "react";
// components
import { cn } from "../utils";
import { Popover } from "./popover";
// helpers
// types
import type { TPopoverMenu } from "./types";
export function PopoverMenu<T>(props: TPopoverMenu<T>) {
const {
popperPosition = "bottom-end",
popperPadding = 0,
buttonClassName = "",
button,
disabled,
panelClassName = "",
data,
popoverClassName = "",
keyExtractor,
render,
popoverButtonRef,
} = props;
return (
<Popover
popperPosition={popperPosition}
popperPadding={popperPadding}
buttonClassName={buttonClassName}
button={button}
disabled={disabled}
panelClassName={cn(
"my-1 w-48 rounded-sm border-[0.5px] border-strong bg-surface-1 px-2 py-2 text-11 shadow-raised-200 focus:outline-none",
panelClassName
)}
popoverClassName={popoverClassName}
popoverButtonRef={popoverButtonRef}
>
<Fragment>
{data.map((item, index) => (
<Fragment key={keyExtractor(item, index)}>{render(item, index)}</Fragment>
))}
</Fragment>
</Popover>
);
}
@@ -0,0 +1,60 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Meta, StoryObj } from "@storybook/react";
import React from "react";
import { Popover } from "./popover";
const meta: Meta<typeof Popover> = {
title: "Popover",
component: Popover,
};
export default meta;
// types
type Story = StoryObj<typeof Popover>;
// data
// components
const RenderCustomPopoverComponent = (
<div className="space-y-2">
<div className="text-gray-500 text-13 font-medium">Your custom component</div>
<div>
{["option1", "option2", "option3"].map((option) => (
<div
key={option}
className="text-gray-600 hover:text-gray-700 hover:bg-gray-200 cursor-pointer rounded-xs px-1.5 py-0.5 text-13 capitalize transition-all"
>
{option}
</div>
))}
</div>
</div>
);
// stories
export const Default: Story = {
args: {
popperPosition: "bottom-start",
panelClassName: "rounded-sm bg-gray-100 p-2",
children: RenderCustomPopoverComponent,
},
};
export const CustomMenuButton: Story = {
args: {
popperPosition: "bottom-start",
button: (
<div className="bg-gray-100 hover:bg-gray-200 rounded-sm p-2 text-13 font-medium transition-all">
Custom Menu Button
</div>
),
panelClassName: "rounded-sm bg-gray-100 p-2",
children: RenderCustomPopoverComponent,
},
};
@@ -0,0 +1,85 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { Popover as HeadlessReactPopover, Transition } from "@headlessui/react";
import { EllipsisVertical } from "lucide-react";
import type { Ref } from "react";
import React, { Fragment, useState } from "react";
import { usePopper } from "react-popper";
// helpers
import { cn } from "../utils";
// types
import type { TPopover } from "./types";
export function Popover(props: TPopover) {
const {
popperPosition = "bottom-end",
popperPadding = 0,
buttonClassName = "",
popoverClassName = "",
button,
disabled = false,
panelClassName = "",
children,
popoverButtonRef,
buttonRefClassName = "",
} = props;
// states
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
// react-popper derived values
const { styles, attributes } = usePopper(referenceElement, popperElement, {
placement: popperPosition,
modifiers: [
{
name: "preventOverflow",
options: {
padding: popperPadding,
},
},
],
});
return (
<HeadlessReactPopover className={cn("relative flex h-full w-full items-center justify-center", popoverClassName)}>
<div ref={setReferenceElement} className={cn("w-full", buttonRefClassName)}>
<HeadlessReactPopover.Button
ref={popoverButtonRef as Ref<HTMLButtonElement>}
className={cn(
{
"flex h-6 w-6 items-center justify-center rounded-sm bg-surface-2 text-14 transition-all hover:bg-layer-1":
!button,
},
buttonClassName
)}
disabled={disabled}
>
{button ? button : <EllipsisVertical className="h-3 w-3" />}
</HeadlessReactPopover.Button>
</div>
<Transition
as={Fragment}
enter="transition ease-out duration-200"
enterFrom="opacity-0 translate-y-1"
enterTo="opacity-100 translate-y-0"
leave="transition ease-in duration-150"
leaveFrom="opacity-100 translate-y-0"
leaveTo="opacity-0 translate-y-1"
>
<HeadlessReactPopover.Panel
ref={setPopperElement}
style={styles.popper}
{...attributes.popper}
className={cn("absolute top-full left-0 z-20 mt-2 w-screen max-w-xs", panelClassName)}
>
{children}
</HeadlessReactPopover.Panel>
</Transition>
</HeadlessReactPopover>
);
}
@@ -0,0 +1,37 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import type { Placement } from "@popperjs/core";
import type { MutableRefObject, ReactNode } from "react";
export type TPopoverButtonDefaultOptions = {
// button and button styling
button?: ReactNode;
buttonClassName?: string;
buttonRefClassName?: string;
disabled?: boolean;
};
export type TPopoverDefaultOptions = TPopoverButtonDefaultOptions & {
// popper styling
popperPosition?: Placement | undefined;
popperPadding?: number | undefined;
// panel styling
panelClassName?: string;
popoverClassName?: string;
popoverButtonRef?: MutableRefObject<HTMLButtonElement | null>;
};
export type TPopover = TPopoverDefaultOptions & {
// children
children: ReactNode;
};
export type TPopoverMenu<T> = TPopoverDefaultOptions & {
data: T[];
keyExtractor: (item: T, index: number) => string;
render: (item: T, index: number) => ReactNode;
};