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,183 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import { useState } from "react";
import type { Meta, StoryObj } from "@storybook/react-vite";
import { useArgs } from "storybook/preview-api";
import { ChevronDownIcon } from "../icons/arrows/chevron-down";
import { Collapsible } from "./collapsible";
const meta = {
title: "Components/Collapsible",
component: Collapsible.CollapsibleRoot,
subcomponents: {
CollapsibleTrigger: Collapsible.CollapsibleTrigger,
CollapsibleContent: Collapsible.CollapsibleContent,
},
parameters: {
layout: "centered",
},
tags: ["autodocs"],
args: {
children: null,
isOpen: false,
onToggle: () => {},
},
render(args) {
const [{ isOpen }, updateArgs] = useArgs();
const toggleOpen = () => updateArgs({ isOpen: !isOpen });
return (
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={toggleOpen} className="w-96">
<Collapsible.CollapsibleTrigger className="bg-gray-100 hover:bg-gray-200 flex w-full items-center justify-between rounded-md px-4 py-2">
<span className="font-semibold">Click to toggle</span>
<ChevronDownIcon className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="border-gray-200 rounded-md border p-4">
<p className="text-13">This is the collapsible content that can be shown or hidden.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
);
},
} satisfies Meta<typeof Collapsible.CollapsibleRoot>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {};
export const DefaultOpen: Story = {
args: { isOpen: true },
};
export const Controlled: Story = {
render() {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="space-y-4">
<div className="flex gap-2">
<button onClick={() => setIsOpen(true)} className="bg-blue-500 rounded-sm px-4 py-2 text-13 text-on-color">
Open
</button>
<button onClick={() => setIsOpen(false)} className="bg-gray-500 rounded-sm px-4 py-2 text-13 text-on-color">
Close
</button>
<button
onClick={() => setIsOpen(!isOpen)}
className="bg-green-500 rounded-sm px-4 py-2 text-13 text-on-color"
>
Toggle
</button>
</div>
<Collapsible.CollapsibleRoot isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
<Collapsible.CollapsibleTrigger className="bg-gray-100 hover:bg-gray-200 flex w-full items-center justify-between rounded-md px-4 py-2">
<span className="font-semibold">Controlled Collapsible</span>
<ChevronDownIcon className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="border-gray-200 rounded-md border p-4">
<p className="text-13">This collapsible is controlled by external state.</p>
<p className="mt-2 text-13">Current state: {isOpen ? "Open" : "Closed"}</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
</div>
);
},
};
export const NestedContent: Story = {
render(args) {
const [isOpen, setIsOpen] = useState(args.isOpen);
return (
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
<Collapsible.CollapsibleTrigger className="bg-gray-100 hover:bg-gray-200 flex w-full items-center justify-between rounded-md px-4 py-2">
<span className="font-semibold">Collapsible with Nested Content</span>
<ChevronDownIcon className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="border-gray-200 space-y-2 rounded-md border p-4">
<h4 className="font-semibold">Section 1</h4>
<p className="text-13">This is some content in the first section.</p>
<h4 className="font-semibold">Section 2</h4>
<p className="text-13">This is some content in the second section.</p>
<ul className="list-inside list-disc text-13">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
);
},
};
export const CustomStyling: Story = {
render(args) {
const [isOpen, setIsOpen] = useState(args.isOpen);
return (
<Collapsible.CollapsibleRoot {...args} isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} className="w-96">
<Collapsible.CollapsibleTrigger className="from-purple-500 to-pink-500 shadow-lg hover:shadow-xl flex w-full items-center justify-between rounded-lg bg-gradient-to-r px-6 py-3 text-on-color transition-all">
<span className="text-16 font-bold">Custom Styled Trigger</span>
<ChevronDownIcon className="h-5 w-5 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-4">
<div className="from-purple-100 to-pink-100 shadow-md rounded-lg bg-gradient-to-br p-6">
<p className="text-purple-900">This collapsible has custom styling with gradients, shadows, and colors.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
);
},
};
export const MultipleCollapsibles: Story = {
render() {
return (
<div className="w-96 space-y-2">
<Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleTrigger className="bg-gray-100 hover:bg-gray-200 flex w-full items-center justify-between rounded-md px-4 py-2">
<span className="font-semibold">First Item</span>
<ChevronDownIcon className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="border-gray-200 rounded-md border p-4">
<p className="text-13">Content for the first item.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleTrigger className="bg-gray-100 hover:bg-gray-200 flex w-full items-center justify-between rounded-md px-4 py-2">
<span className="font-semibold">Second Item</span>
<ChevronDownIcon className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="border-gray-200 rounded-md border p-4">
<p className="text-13">Content for the second item.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleRoot>
<Collapsible.CollapsibleTrigger className="bg-gray-100 hover:bg-gray-200 flex w-full items-center justify-between rounded-md px-4 py-2">
<span className="font-semibold">Third Item</span>
<ChevronDownIcon className="h-4 w-4 transition-transform group-data-[panel-open]:rotate-180" />
</Collapsible.CollapsibleTrigger>
<Collapsible.CollapsibleContent className="mt-2">
<div className="border-gray-200 rounded-md border p-4">
<p className="text-13">Content for the third item.</p>
</div>
</Collapsible.CollapsibleContent>
</Collapsible.CollapsibleRoot>
</div>
);
},
};
@@ -0,0 +1,108 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
import React, { useState, useEffect, useCallback, createContext, useContext } from "react";
import { Collapsible as BaseCollapsible } from "@base-ui-components/react/collapsible";
import clsx from "clsx";
// Types
type CollapsibleContextType = {
isOpen: boolean;
onToggle: () => void;
};
type RootProps = {
children: React.ReactNode;
className?: string;
isOpen?: boolean;
onToggle?: () => void;
defaultOpen?: boolean;
};
type TriggerProps = {
children: React.ReactNode;
className?: string;
buttonRef?: React.RefObject<HTMLButtonElement>;
};
type ContentProps = {
children: React.ReactNode;
className?: string;
};
// Context
const CollapsibleContext = createContext<CollapsibleContextType | undefined>(undefined);
// Hook
const useCollapsible = () => {
const context = useContext(CollapsibleContext);
if (!context) {
throw new Error("Collapsible compound components cannot be rendered outside the Collapsible component");
}
return context;
};
// Components
function Root({ children, className, isOpen: controlledIsOpen, onToggle, defaultOpen }: RootProps) {
const [localIsOpen, setLocalIsOpen] = useState<boolean>(controlledIsOpen || defaultOpen || false);
useEffect(() => {
if (controlledIsOpen !== undefined) {
setLocalIsOpen(controlledIsOpen);
}
}, [controlledIsOpen]);
const handleToggle = useCallback(() => {
if (controlledIsOpen !== undefined) {
onToggle?.();
} else {
setLocalIsOpen((prev) => !prev);
}
}, [controlledIsOpen, onToggle]);
return (
<CollapsibleContext.Provider value={{ isOpen: localIsOpen, onToggle: handleToggle }}>
<BaseCollapsible.Root
className={clsx(className)}
defaultOpen={defaultOpen}
open={localIsOpen}
onOpenChange={handleToggle}
>
{children}
</BaseCollapsible.Root>
</CollapsibleContext.Provider>
);
}
function Trigger({ children, className, buttonRef }: TriggerProps) {
const { isOpen } = useCollapsible();
return (
<BaseCollapsible.Trigger data-panel-open={isOpen} ref={buttonRef} className={className}>
{children}
</BaseCollapsible.Trigger>
);
}
function Content({ children, className }: ContentProps) {
return (
<BaseCollapsible.Panel
className={clsx(
"flex h-[var(--collapsible-panel-height)] flex-col overflow-hidden text-13 transition-all ease-out data-[ending-style]:h-0 data-[starting-style]:h-0",
className
)}
>
{children}
</BaseCollapsible.Panel>
);
}
// Compound Component
export const Collapsible = {
CollapsibleRoot: Root,
CollapsibleTrigger: Trigger,
CollapsibleContent: Content,
};
@@ -0,0 +1,7 @@
/**
* Copyright (c) 2023-present Plane Software, Inc. and contributors
* SPDX-License-Identifier: AGPL-3.0-only
* See the LICENSE file for details.
*/
export * from "./collapsible";