From 153d718d265242325ff50374dfdace4f620a7762 Mon Sep 17 00:00:00 2001 From: DGiannaris Date: Tue, 17 Dec 2024 16:53:57 +0200 Subject: [PATCH] feat(ui): add SplitButton component Introduces a new SplitButton component, combining a primary action button with a dropdown menu for additional options. This component enhances user interface flexibility by providing quick access to a main action while offering related secondary actions in a compact design. The implementation includes various customization options such as different variants, tones, sizes, and icon support, along with Storybook documentation for easy integration and testing. --- .../src/stories/SplitButton.stories.tsx | 164 ++++++++++++++++++ .../ui/src/components/SplitButton/index.tsx | 80 +++++++++ .../ui/src/components/SplitButton/styles.ts | 39 +++++ .../ui/src/components/SplitButton/types.ts | 17 ++ packages/ui/src/index.ts | 4 + 5 files changed, 304 insertions(+) create mode 100644 examples/storybook/src/stories/SplitButton.stories.tsx create mode 100644 packages/ui/src/components/SplitButton/index.tsx create mode 100644 packages/ui/src/components/SplitButton/styles.ts create mode 100644 packages/ui/src/components/SplitButton/types.ts diff --git a/examples/storybook/src/stories/SplitButton.stories.tsx b/examples/storybook/src/stories/SplitButton.stories.tsx new file mode 100644 index 0000000..72ced60 --- /dev/null +++ b/examples/storybook/src/stories/SplitButton.stories.tsx @@ -0,0 +1,164 @@ +import type { Meta, StoryObj } from "@storybook/react"; +import { SplitButton, type SplitButtonProps } from "@synopsisapp/symbiosis-ui"; + +const meta: Meta = { + title: "Components/SplitButton", + component: SplitButton, + tags: ["autodocs"], + argTypes: { + variant: { + control: { type: "select" }, + options: ["primary", "outline", "ghost"], + description: "Visual style variant of the button", + table: { + type: { summary: "string" }, + defaultValue: { summary: "primary" }, + }, + }, + tone: { + control: { type: "select" }, + options: ["default", "destructive", "monochrome-light", "monochrome-dark"], + description: "Color tone of the button", + table: { + type: { summary: "string" }, + defaultValue: { summary: "default" }, + }, + }, + size: { + control: { type: "select" }, + options: ["small-200", "small-100", "base"], + description: "Size of the button", + table: { + type: { summary: "string" }, + defaultValue: { summary: "base" }, + }, + }, + leftIcon: { + control: { type: "text" }, + description: "Icon to display on the left side of the button", + }, + rightIcon: { + control: { type: "text" }, + description: "Icon to display on the right side of the button", + }, + isDisabled: { + control: { type: "boolean" }, + description: "Whether the button is disabled", + }, + isLoading: { + control: { type: "boolean" }, + description: "Whether the button is in a loading state", + }, + }, +}; + +export default meta; + +type Story = StoryObj; + +const defaultItems = [ + { + text: "Option 1", + icon: "edit", + onSelect: () => console.log("Option 1 selected"), + }, + { + text: "Option 2", + icon: "close", + onSelect: () => console.log("Option 2 selected"), + }, + { + text: "Section Title", + isSectionTitle: true, + isSeparated: true, + }, + { + text: "Option 3", + icon: "symbiosis-chevron-down", + onSelect: () => console.log("Option 3 selected"), + }, +]; + +export const Default: Story = { + args: { + label: "Split Button", + variant: "primary", + tone: "default", + size: "base", + isDisabled: false, + isLoading: false, + leftIcon: "edit", + items: defaultItems as SplitButtonProps["items"], + }, +}; + +export const DifferentVariants: Story = { + render: (args) => ( +
+ + + +
+ ), + args: { + ...Default.args, + }, +}; + +export const DifferentTones: Story = { + render: (args) => ( +
+ + +
+ (custom background container to showcase) + +
+ + + + +
+ ), + args: { + ...Default.args, + }, +}; + +export const DifferentSizes: Story = { + render: (args) => ( +
+ + + +
+ ), + args: { + ...Default.args, + }, +}; + +export const WithIcons: Story = { + render: (args) => ( +
+ + + +
+ ), + args: { + ...Default.args, + }, +}; + +export const States: Story = { + render: (args) => ( +
+ + +
+ ), + args: { + ...Default.args, + }, +}; diff --git a/packages/ui/src/components/SplitButton/index.tsx b/packages/ui/src/components/SplitButton/index.tsx new file mode 100644 index 0000000..b835708 --- /dev/null +++ b/packages/ui/src/components/SplitButton/index.tsx @@ -0,0 +1,80 @@ + +import { Button } from "../Button"; +import { cn } from "../../utils/cn"; +import { Dropdown, DropdownMenuLabel, DropdownSimpleItem } from "../Dropdown"; +import { IconButton } from "../IconButton"; +import { iconButtonLeftBorderIconVariant } from "./styles"; +import type { SplitButtonProps } from "./types"; + +export const SplitButton = ({ + label, + variant = "primary", + size = "base", + leftIcon, + rightIcon, + layout = "normal", + type = "button", + className, + tone = "default", + items, + ...restProps +}: SplitButtonProps) => { + return ( +
+
+ ); +}; diff --git a/packages/ui/src/components/SplitButton/styles.ts b/packages/ui/src/components/SplitButton/styles.ts new file mode 100644 index 0000000..74f9872 --- /dev/null +++ b/packages/ui/src/components/SplitButton/styles.ts @@ -0,0 +1,39 @@ +import { cva } from "class-variance-authority"; + +export const iconButtonLeftBorderIconVariant = cva([], { + variants: { + variant: { + primary: [], + outline: ["border-l-transparent"], + ghost: ["border-l-transparent"], + }, + tone: { + default: [], + destructive: [], + "monochrome-light": [], + "monochrome-dark": [], + }, + }, + compoundVariants: [ + { + variant: "primary", + tone: "default", + className: ["border-l-mainColors-light-400 hover:border-l-mainColors-light-400"], + }, + { + variant: "primary", + tone: "destructive", + className: ["border-l-white hover:border-l-white"], + }, + { + variant: "primary", + tone: "monochrome-light", + className: ["border-l-slate-600 hover:border-l-slate-600"], + }, + { + variant: "primary", + tone: "monochrome-dark", + className: ["border-l-white hover:border-l-white"], + }, + ], +}); diff --git a/packages/ui/src/components/SplitButton/types.ts b/packages/ui/src/components/SplitButton/types.ts new file mode 100644 index 0000000..3dbb1e6 --- /dev/null +++ b/packages/ui/src/components/SplitButton/types.ts @@ -0,0 +1,17 @@ +import type { ButtonVariant, BaseProps } from "../Button/types"; +import type { IconProps } from "../Icon/types"; + +export type SplitButtonItemProps = { + text: string; + content?: React.ReactNode; + icon?: IconProps["name"]; + onSelect?: () => void; + wrapperClassName?: string; + isSeparated?: boolean; + isSectionTitle?: boolean; +}; + +export type SplitButtonProps = Omit & { + items: SplitButtonItemProps[]; + variant: Exclude; +}; diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 062ca56..f098cbf 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -93,3 +93,7 @@ export type { TextAreaFieldProps } from "./components/TextAreaField/types"; export * from "./hooks/useToast"; export { Toaster } from "./components/Toaster"; export * from "./components/Toaster/types"; + +export { SplitButton } from "./components/SplitButton"; +export * from "./components/SplitButton/types"; +export type { SplitButtonProps } from "./components/SplitButton/types";