diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c8799e..753923a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,37 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [0.0.313](https://github.com/getpingback/ui/compare/v0.0.312...v0.0.313) (2025-01-07) + + +### Bug Fixes + +* changes requested ([65f9c2d](https://github.com/getpingback/ui/commits/65f9c2d25bb8e39dc57c581a8893af148244d337)) +* remove props ([ffb5e79](https://github.com/getpingback/ui/commits/ffb5e791a33540d37baade5e9fcb9bf5d4b0436c)) +* types ([6c072ff](https://github.com/getpingback/ui/commits/6c072ff427ccdf0d42081621005a3010d9e0fb0e)) + +### [0.0.311](https://github.com/getpingback/ui/compare/v0.0.310...v0.0.311) (2025-01-06) + + +### Bug Fixes + +* splitbutton font style ([5052395](https://github.com/getpingback/ui/commits/505239574bf2c634e648531a3a409e710674cd06)) + +### [0.0.309](https://github.com/getpingback/ui/compare/v0.0.308...v0.0.309) (2025-01-06) + + +### Bug Fixes + +* splitbutton styles ([75d1224](https://github.com/getpingback/ui/commits/75d1224157950f57c9420945bf4c5925c7d03d54)) + +### [0.0.307](https://github.com/getpingback/ui/compare/v0.0.305...v0.0.307) (2025-01-06) + + +### Features + +* create a splitbutton and tests ([ed2e45c](https://github.com/getpingback/ui/commits/ed2e45c04755bedf8e0d657e69d3a3901f63bea6)) +* export splitbutton ([e3cdfe3](https://github.com/getpingback/ui/commits/e3cdfe3f5075f3fffe2dd57cbc75744f03ada978)) + ### [0.0.305](https://github.com/getpingback/ui/compare/v0.0.303...v0.0.305) (2025-01-02) ### [0.0.303](https://github.com/getpingback/ui/compare/v0.0.301...v0.0.303) (2025-01-02) diff --git a/package.json b/package.json index cae4308..f5e6339 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@getpingback/ui", "author": "Pingback Team", - "version": "0.0.306", + "version": "0.0.314", "license": "MIT", "main": "dist/cjs/index.js", "module": "dist/esm/index.js", diff --git a/src/components/index.ts b/src/components/index.ts index 86ec4c4..8c14791 100644 --- a/src/components/index.ts +++ b/src/components/index.ts @@ -24,3 +24,4 @@ export * from './pagination'; export * from './variable-input'; export * from './radio-group'; export * from './switch'; +export * from './split-button'; diff --git a/src/components/split-button/index.ts b/src/components/split-button/index.ts new file mode 100644 index 0000000..0c1a468 --- /dev/null +++ b/src/components/split-button/index.ts @@ -0,0 +1 @@ +export * from './split-button'; diff --git a/src/components/split-button/split-button.stories.tsx b/src/components/split-button/split-button.stories.tsx new file mode 100644 index 0000000..3ada3f7 --- /dev/null +++ b/src/components/split-button/split-button.stories.tsx @@ -0,0 +1,54 @@ +import * as React from 'react'; +import type { Meta, StoryObj } from '@storybook/react'; +import { SplitButton } from './split-button'; +import { TrashCanIcon, PlusIcon } from '@stash-ui/light-icons'; +import { HeartIcon } from '@stash-ui/regular-icons'; + +const meta = { + title: 'Components/SplitButton', + component: SplitButton, + + tags: ['autodocs'], + + argTypes: {} +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Solid: Story = { + args: { + prefixIcon: , + variant: 'solid', + label: 'Button label', + onPrefixClick: () => {}, + menuItems: [ + { + key: 'add', + icon: , + text: 'Add', + onClick: () => console.log('add') + }, + { + key: 'delete', + icon: , + text: 'Delete', + onClick: () => console.log('delete') + } + ] + } +}; + +export const Outlined: Story = { + args: { + ...Solid.args, + variant: 'outlined' + } +}; + +export const Ghost: Story = { + args: { + ...Solid.args, + variant: 'ghost' + } +}; diff --git a/src/components/split-button/split-button.test.tsx b/src/components/split-button/split-button.test.tsx new file mode 100644 index 0000000..475e3fd --- /dev/null +++ b/src/components/split-button/split-button.test.tsx @@ -0,0 +1,104 @@ +import React from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { composeStories } from '@storybook/testing-react'; + +import * as stories from './split-button.stories'; + +const { Solid, Outlined, Ghost } = composeStories(stories); + +describe('SplitButton Component', () => { + it('should render the SplitButton component', () => { + render(); + const buttonContainer = screen.getByTestId('split-button'); + const primaryButton = screen.getByTestId('split-button-primary'); + const menuTrigger = screen.getByTestId('split-button-menu-trigger'); + expect(buttonContainer).toBeInTheDocument(); + expect(primaryButton).toBeInTheDocument(); + expect(menuTrigger).toBeInTheDocument(); + + expect(primaryButton).toHaveStyle({ backgroundColor: 'var(--buttons-solid_default)' }); + expect(menuTrigger).toHaveStyle({ backgroundColor: 'var(--buttons-solid_default)' }); + }); + + it('should render the SplitButton menu items', async () => { + render(); + const menuTrigger = screen.getByTestId('split-button-menu-trigger'); + + const user = userEvent.setup(); + await user.click(menuTrigger); + const menuContent = screen.getByTestId('split-button-menu-content'); + const menuItems = screen.getAllByTestId(/split-button-menu-item/i); + expect(menuContent).toBeInTheDocument(); + expect(menuItems[0]).toHaveTextContent('Add'); + expect(menuItems[1]).toHaveTextContent('Delete'); + }); + + it('should call the onClick function when a menu item is clicked', async () => { + const onClickMock = jest.fn(); + render( + onClickMock('add') + }, + { + key: 'delete', + text: 'Delete', + onClick: () => onClickMock('delete') + } + ] + }} + /> + ); + + const user = userEvent.setup(); + const menuTrigger = screen.getByTestId('split-button-menu-trigger'); + await user.click(menuTrigger); + const menuItems = screen.getAllByTestId(/split-button-menu-item/i); + await user.click(menuItems[0]); + + expect(onClickMock).toHaveBeenCalledWith('add'); + }); + + it('should call the onClick function when a primary button is clicked', async () => { + const onClickMock = jest.fn(); + render( onClickMock('primary action') }} />); + const user = userEvent.setup(); + const primaryButton = screen.getByTestId('split-button-primary'); + await user.click(primaryButton); + + expect(onClickMock).toHaveBeenCalledWith('primary action'); + }); + + it('should render the outlined SplitButton component', () => { + render(); + const buttonContainer = screen.getByTestId('split-button'); + const primaryButton = screen.getByTestId('split-button-primary'); + const menuTrigger = screen.getByTestId('split-button-menu-trigger'); + + expect(buttonContainer).toBeInTheDocument(); + expect(primaryButton).toBeInTheDocument(); + expect(menuTrigger).toBeInTheDocument(); + + expect(primaryButton).toHaveStyle({ backgroundColor: 'var(--buttons-outlined_default)' }); + expect(menuTrigger).toHaveStyle({ backgroundColor: 'var(--buttons-outlined_default)' }); + }); + + it('should render the ghost SplitButton component', () => { + render(); + const buttonContainer = screen.getByTestId('split-button'); + const primaryButton = screen.getByTestId('split-button-primary'); + const menuTrigger = screen.getByTestId('split-button-menu-trigger'); + + expect(buttonContainer).toBeInTheDocument(); + expect(primaryButton).toBeInTheDocument(); + expect(menuTrigger).toBeInTheDocument(); + + expect(primaryButton).toHaveStyle({ backgroundColor: 'var(--buttons-ghost_bg-color)' }); + expect(menuTrigger).toHaveStyle({ backgroundColor: 'var(--buttons-ghost_bg-color)' }); + }); +}); diff --git a/src/components/split-button/split-button.tsx b/src/components/split-button/split-button.tsx new file mode 100644 index 0000000..97e6dfa --- /dev/null +++ b/src/components/split-button/split-button.tsx @@ -0,0 +1,104 @@ +import React, { useState } from 'react'; +import * as DropdownMenu from '@radix-ui/react-dropdown-menu'; +import { CaretDownIcon } from '@stash-ui/light-icons'; +import { cva } from 'class-variance-authority'; +import { DropdownItem } from '../dropdown'; + +const containerVariants = cva('flex items-center font-primary max-w-fit h-[32px] text-sm rounded-lg ', { + variants: { + type: { + solid: 'bg-button-solid text-button-solid-foreground hover:shadow-solid transition-all duration-200 ease-in-out', + outlined: + 'bg-button-outlined text-secondary-foreground border border-button-outlined-border hover:shadow-outlined transition-all duration-200 ease-in-out', + ghost: + 'bg-button-ghost-gray text-button-ghost-foreground text-secondary-foreground hover:shadow-ghost transition-all duration-200 ease-in-out' + } + }, + defaultVariants: { + type: 'solid' + } +}); + +const leftButtonVariants = cva( + 'h-full w-full flex items-center gap-1 px-3 rounded-l-lg rounded-r-none transition-all duration-200 ease-in-out font-semibold', + { + variants: { + type: { + solid: 'hover:bg-button-solid-hover', + outlined: 'hover:bg-button-outlined-hover opacity-85 hover:opacity-100', + ghost: 'hover:bg-button-ghost-hover opacity-85 hover:opacity-100' + } + } + } +); + +const menuTriggerVariants = cva( + 'h-full w-full min-w-[32px] max-w-[32px] border-l flex items-center justify-center rounded-r-lg rounded-l-none border-solid transition-all duration-200 ease-in-out', + { + variants: { + type: { + solid: 'hover:bg-button-solid-hover border-[#282C2F1F]', + outlined: 'hover:bg-button-outlined-hover opacity-85 hover:opacity-100 border-[#282C2F1F]', + ghost: 'hover:bg-button-ghost-hover border-[#282C2F1F]' + } + } + } +); + +interface SplitButtonProps { + prefixIcon: React.ReactNode; + label: string; + variant?: 'solid' | 'outlined' | 'ghost'; + onPrefixClick: () => void; + sufixIcon?: React.ReactNode; + className?: string; + menuItems: { + key: string; + icon: JSX.Element | undefined; + text: string; + onClick: () => void; + }[]; +} + +function SplitButton({ prefixIcon, label, variant, onPrefixClick, menuItems, sufixIcon, className }: SplitButtonProps) { + const [isMenuActionsOpen, setIsMenuActionsOpen] = useState(false); + + return ( + !open && setIsMenuActionsOpen(false)}> + +
+ + +
+
+ + + setIsMenuActionsOpen(false)} + side="bottom" + className={`rounded-lg w-[252px] flex-col z-50 min-w-fit overflow-hidden bg-background-accent shadow-modal py-2 ${className}`} + sideOffset={4} + align="center" + > + {menuItems.map((item, index) => ( + + {item.text} + + ))} + + +
+ ); +} + +export { SplitButton }; diff --git a/src/styles/globals.css b/src/styles/globals.css index 279ca4f..333bad8 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -22,10 +22,15 @@ --buttons-solid_default: #9061f9; --button-solid-bg: #9061f9; + --button-solid-hover: #7e3af2; --buttons-label_color: #7e3af2; --button-solid-text: #fafafa; --button-ghost-bg: #9061f91f; --button-ghost-text: #52525b; + --button-ghost-gray: #52525B14; + --button-ghost-gray-hover: #282C2F14; + --button-outlined-border: #D4D4D8; + --button-outlined-hover: #52525B14; --buttons-label_inverse: #fafafa; --button-hover-solid-color: #9061f940; --buttons-ghost_bg-color: #9061f91f; @@ -45,6 +50,8 @@ --switch-shadow: 0px 0px 1px 0px rgba(0, 0, 0, 0.161) inset; --button-solid-shadow: 0px 0px 0px 3px #9061f940; + --button-outlined-shadow: 0px 0px 0px 3px #9CA3AF1F; + --button-ghost-shadow: 0px 0px 0px 3px #9CA3AF14; --list-hightlight--hover: 0px 0px 0px 3px rgba(14, 159, 110, 0.12); --list-actived: #9061f914; @@ -104,6 +111,7 @@ --buttons-solid_default: #9061f9; --button-solid-bg: #9061f9; --button-solid-text: #fafafa; + --button-solid-hover: #7e3af2; --buttons-label: #52525b; --button-ghost-bg: #ac94fa3d; --button-ghost-text: #fafafa; diff --git a/tailwind.config.js b/tailwind.config.js index 96b6b81..00913ec 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -141,6 +141,8 @@ const config = { drawer: 'var(--drawer-shadow)', custom: '0px 0px 0px 1px rgba(0,0,0,0.15)', solid: 'var(--button-solid-shadow)', + outlined: 'var(--button-outlined-shadow)', + ghost: 'var(--button-ghost-shadow)', dropdown: 'var(--dropdown-shadow)', switch: 'var(--switch-shadow)' }, @@ -298,11 +300,18 @@ const config = { button: { solid: { DEFAULT: 'var(--button-solid-bg)', - foreground: 'var(--button-solid-text)' + foreground: 'var(--button-solid-text)', + hover: 'var(--button-solid-hover)' }, ghost: { DEFAULT: 'var(--button-ghost-bg)', - foreground: 'var(--button-ghost-text)' + foreground: 'var(--button-ghost-text)', + gray: 'var(--button-ghost-gray)', + hover: 'var(--button-ghost-gray-hover)' + }, + outlined: { + border: 'var(--button-outlined-border)', + hover: 'var(--button-outlined-hover)' }, done: { DEFAULT: 'var(--badge-done-bg)',