forked from cerc-io/snowballtools-base
⚡️ feat: create button component
This commit is contained in:
parent
082353e9bc
commit
e850435c2d
137
packages/frontend/src/components/shared/Button/Button.theme.ts
Normal file
137
packages/frontend/src/components/shared/Button/Button.theme.ts
Normal file
@ -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<typeof buttonTheme>;
|
150
packages/frontend/src/components/shared/Button/Button.tsx
Normal file
150
packages/frontend/src/components/shared/Button/Button.tsx
Normal file
@ -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<ComponentPropsWithoutRef<'a'>, '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<ComponentPropsWithoutRef<'button'>, '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 <NextLink>, <a> or <button> depending on props
|
||||||
|
// useCallback to prevent unnecessary re-rendering
|
||||||
|
const Component = useCallback(
|
||||||
|
({ children: _children, ..._props }: ButtonOrLinkProps) => {
|
||||||
|
if (_props.as === 'a') {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const { external, href, as, ...baseLinkProps } = _props;
|
||||||
|
|
||||||
|
// External link
|
||||||
|
if (external) {
|
||||||
|
const externalLinkProps = {
|
||||||
|
target: '_blank',
|
||||||
|
rel: 'noopener',
|
||||||
|
href,
|
||||||
|
...baseLinkProps,
|
||||||
|
};
|
||||||
|
return <a {...externalLinkProps}>{_children}</a>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal link
|
||||||
|
return (
|
||||||
|
<Link {...baseLinkProps} to={href}>
|
||||||
|
{_children}
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
const { ...buttonProps } = _props;
|
||||||
|
// @ts-expect-error - as prop is not a valid prop for button elements
|
||||||
|
return <button {...buttonProps}>{_children}</button>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 (
|
||||||
|
<Component
|
||||||
|
{...props}
|
||||||
|
className={buttonTheme({ ...styleProps, class: className })}
|
||||||
|
>
|
||||||
|
{leftIcon}
|
||||||
|
{children}
|
||||||
|
{rightIcon}
|
||||||
|
</Component>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
|
export { Button };
|
2
packages/frontend/src/components/shared/Button/index.ts
Normal file
2
packages/frontend/src/components/shared/Button/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export * from './Button';
|
||||||
|
export * from './Button.theme';
|
Loading…
Reference in New Issue
Block a user