diff --git a/packages/frontend/src/components/shared/Button/Button.theme.ts b/packages/frontend/src/components/shared/Button/Button.theme.ts new file mode 100644 index 00000000..74613090 --- /dev/null +++ b/packages/frontend/src/components/shared/Button/Button.theme.ts @@ -0,0 +1,137 @@ +import { tv } from 'tailwind-variants'; +import type { VariantProps } from 'tailwind-variants'; + +/** + * Defines the theme for a button component. + */ +export const buttonTheme = tv( + { + base: [ + 'inline-flex', + 'items-center', + 'justify-center', + 'font-medium', + 'whitespace-nowrap', + 'focus-ring', + 'disabled:cursor-not-allowed', + ], + variants: { + size: { + lg: ['gap-3', 'py-4', 'px-6', 'rounded-lg', 'text-lg'], + md: ['gap-2', 'py-3', 'px-4', 'rounded-lg', 'text-base'], + sm: ['gap-1', 'py-2', 'px-2', 'rounded-md', 'text-xs'], + xs: ['gap-1', 'py-1', 'px-2', 'rounded-md', 'text-xs'], + }, + fullWidth: { + true: 'w-full', + }, + shape: { + default: '', + rounded: 'rounded-full', + }, + variant: { + primary: [ + 'text-elements-on-primary', + 'border', + 'border-primary-700', + 'bg-controls-primary', + 'shadow-button', + 'hover:bg-controls-primary-hovered', + 'focus-visible:bg-controls-primary-hovered', + 'disabled:text-elements-on-disabled', + 'disabled:bg-controls-disabled', + 'disabled:border-transparent', + 'disabled:shadow-none', + ], + secondary: [ + 'text-components-buttons-secondary-foreground', + 'border', + 'border-primary-500', + 'bg-components-buttons-secondary-background', + 'hover:text-components-buttons-secondary-foreground-hover', + 'hover:bg-components-buttons-secondary-background-hover', + 'focus-visible:text-components-buttons-secondary-foreground-focus', + 'focus-visible:bg-components-buttons-secondary-background-focus', + 'disabled:text-components-buttons-secondary-foreground-disabled', + 'disabled:bg-components-buttons-secondary-background-disabled ', + 'disabled:border-transparent', + ], + tertiary: [ + 'text-components-buttons-tertiary-background', + 'border', + 'border-components-buttons-tertiary-background', + 'bg-transparent', + 'hover:text-components-buttons-tertiary-hover', + 'hover:border-components-buttons-tertiary-hover', + 'focus-visible:text-components-buttons-tertiary-focus', + 'focus-visible:border-components-buttons-tertiary-focus', + 'disabled:text-components-buttons-tertiary-disabled', + 'disabled:border-components-buttons-tertiary-disabled', + ], + 'text-only': [ + 'text-components-buttons-text-only-background', + 'border', + 'border-transparent', + 'bg-transparent', + 'hover:text-components-buttons-text-only-foreground-hover', + 'hover:bg-components-buttons-text-only-background-hover', + 'focus-visible:text-components-buttons-text-only-foreground-focus', + 'focus-visible:bg-components-buttons-text-only-background-focus', + 'disabled:text-components-buttons-tertiary-disabled', + 'disabled:bg-transparent', + ], + danger: [ + 'text-components-button-icon-alert-foreground', + 'border', + 'border-transparent', + 'bg-components-buttons-alert-background', + 'hover:text-components-buttons-alert-foreground-hover', + 'hover:bg-components-buttons-alert-background-hover', + 'focus-visible:text-components-buttons-alert-foreground-focus', + 'focus-visible:bg-components-buttons-alert-background-focus', + 'disabled:text-components-button-icon-alert-foreground-disabled', + 'disabled:bg-components-button-icon-alert-background-disabled', + ], + 'icon-only': [ + 'p-0 flex items-center justify-center', + 'text-components-button-icon-text-only-foreground', + 'border', + 'border-transparent', + 'bg-transparent', + 'hover:text-components-button-icon-text-only-foreground-hover', + 'hover:bg-components-button-icon-text-only-background-hover', + 'focus-visible:text-components-button-icon-low-emphasis-foreground-focus', + 'focus-visible:bg-components-button-icon-low-emphasis-background-focus', + 'disabled:text-components-button-icon-low-emphasis-outlined-foreground-disabled', + 'disabled:bg-transparent', + ], + unstyled: [], + }, + }, + compoundVariants: [ + { + size: 'md', + variant: 'icon-only', + class: ['h-11', 'w-11', 'rounded-lg'], + }, + { + size: 'sm', + variant: 'icon-only', + class: ['h-8', 'w-8', 'rounded-md'], + }, + ], + defaultVariants: { + size: 'md', + variant: 'primary', + fullWidth: false, + }, + }, + { + responsiveVariants: true, + }, +); + +/** + * Represents the type of a button theme, which is derived from the `buttonTheme` variant props. + */ +export type ButtonTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Button/Button.tsx b/packages/frontend/src/components/shared/Button/Button.tsx new file mode 100644 index 00000000..917642a8 --- /dev/null +++ b/packages/frontend/src/components/shared/Button/Button.tsx @@ -0,0 +1,150 @@ +import React, { useCallback } from 'react'; +import type { ComponentPropsWithoutRef, ReactNode } from 'react'; + +import { buttonTheme } from './Button.theme'; +import type { ButtonTheme } from './Button.theme'; +import { Link } from 'react-router-dom'; + +/** + * Represents the properties of a base button component. + */ +interface ButtonBaseProps { + /** + * The optional left icon element for a component. + * @type {ReactNode} + */ + leftIcon?: ReactNode; + /** + * The optional right icon element to display. + * @type {ReactNode} + */ + rightIcon?: ReactNode; +} + +/** + * Interface for the props of a button link component. + */ +export interface ButtonLinkProps + extends Omit, 'color'> { + /** + * Specifies the optional property `as` with a value of `'a'`. + * @type {'a'} + */ + as?: 'a'; + /** + * Indicates whether the item is external or not. + * @type {boolean} + */ + external?: boolean; + /** + * The URL of a web page or resource. + * @type {string} + */ + href: string; +} + +export interface ButtonProps + extends Omit, 'color'> { + /** + * Specifies the optional property `as` with a value of `'button'`. + * @type {'button'} + */ + as?: 'button'; +} + +/** + * Interface representing the props for a button component. + * Extends the ComponentPropsWithoutRef<'button'> and ButtonTheme interfaces. + */ +export type ButtonOrLinkProps = (ButtonLinkProps | ButtonProps) & + ButtonBaseProps & + ButtonTheme; + +/** + * A custom button component that can be used in React applications. + */ +const Button = ({ + children, + className, + leftIcon, + rightIcon, + fullWidth, + shape, + variant, + ...props +}: ButtonOrLinkProps) => { + // Conditionally render between , or ; + } + }, + [], + ); + + /** + * Extracts specific style properties from the given props object and returns them as a new object. + */ + const styleProps = (({ + variant = 'primary', + size = 'md', + fullWidth = false, + shape = 'rounded', + as, + }) => ({ + variant, + size, + fullWidth, + shape, + as, + }))({ ...props, fullWidth, shape, variant }); + + /** + * Validates that a button component has either children or an aria-label prop. + */ + if (typeof children === 'undefined' && !props['aria-label']) { + throw new Error( + 'Button components must have either children or an aria-label prop', + ); + } + + return ( + + {leftIcon} + {children} + {rightIcon} + + ); +}; + +Button.displayName = 'Button'; + +export { Button }; diff --git a/packages/frontend/src/components/shared/Button/index.ts b/packages/frontend/src/components/shared/Button/index.ts new file mode 100644 index 00000000..1331278b --- /dev/null +++ b/packages/frontend/src/components/shared/Button/index.ts @@ -0,0 +1,2 @@ +export * from './Button'; +export * from './Button.theme';