Merge pull request #77 from snowball-tools/ayungavis/T-4834-button
[T-4834: feat] Button component
This commit is contained in:
commit
69198307fb
39
packages/frontend/.vscode/settings.json
vendored
Normal file
39
packages/frontend/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
// eslint extension options
|
||||
"eslint.enable": true,
|
||||
"eslint.validate": [
|
||||
"javascript",
|
||||
"javascriptreact",
|
||||
"typescript",
|
||||
"typescriptreact"
|
||||
],
|
||||
"css.customData": [".vscode/tailwind.json"],
|
||||
// prettier extension setting
|
||||
"editor.formatOnSave": true,
|
||||
"[javascript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[javascriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"[typescriptreact]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode"
|
||||
},
|
||||
"editor.rulers": [80],
|
||||
"editor.codeActionsOnSave": [
|
||||
"source.addMissingImports",
|
||||
"source.fixAll",
|
||||
"source.organizeImports"
|
||||
],
|
||||
// Show in vscode "Problems" tab when there are errors
|
||||
"typescript.tsserver.experimental.enableProjectDiagnostics": true,
|
||||
// Use absolute import for typescript files
|
||||
"typescript.preferences.importModuleSpecifier": "non-relative",
|
||||
// IntelliSense for taiwind variants
|
||||
"tailwindCSS.experimental.classRegex": [
|
||||
["tv\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"]
|
||||
]
|
||||
}
|
@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@fontsource/inter": "^5.0.16",
|
||||
"@material-tailwind/react": "^2.1.7",
|
||||
"@testing-library/jest-dom": "^5.17.0",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
@ -29,6 +30,7 @@
|
||||
"react-router-dom": "^6.20.1",
|
||||
"react-scripts": "5.0.1",
|
||||
"react-timer-hook": "^3.0.7",
|
||||
"tailwind-variants": "^0.2.0",
|
||||
"typescript": "^4.9.5",
|
||||
"usehooks-ts": "^2.10.0",
|
||||
"vertical-stepper-nav": "^1.0.2",
|
||||
|
@ -1,9 +1,8 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Card, Collapse, Typography } from '@material-tailwind/react';
|
||||
|
||||
import { Environment, EnvironmentVariable } from 'gql-client/dist/src/types';
|
||||
|
||||
import EditEnvironmentVariableRow from './EditEnvironmentVariableRow';
|
||||
import { Environment, EnvironmentVariable } from 'gql-client';
|
||||
|
||||
interface DisplayEnvironmentVariablesProps {
|
||||
environment: Environment;
|
||||
|
162
packages/frontend/src/components/shared/Button/Button.theme.ts
Normal file
162
packages/frontend/src/components/shared/Button/Button.theme.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { tv } from 'tailwind-variants';
|
||||
import type { VariantProps } from 'tailwind-variants';
|
||||
|
||||
/**
|
||||
* Defines the theme for a button component.
|
||||
*/
|
||||
export const buttonTheme = tv(
|
||||
{
|
||||
base: [
|
||||
'h-fit',
|
||||
'inline-flex',
|
||||
'items-center',
|
||||
'justify-center',
|
||||
'whitespace-nowrap',
|
||||
'focus-ring',
|
||||
'disabled:cursor-not-allowed',
|
||||
],
|
||||
variants: {
|
||||
size: {
|
||||
lg: ['gap-2', 'py-3.5', 'px-5', 'text-base', 'tracking-[-0.011em]'],
|
||||
md: ['gap-2', 'py-3.25', 'px-5', 'text-sm', 'tracking-[-0.006em]'],
|
||||
sm: ['gap-1', 'py-2', 'px-3', 'text-xs'],
|
||||
xs: ['gap-1', 'py-1', 'px-2', 'text-xs'],
|
||||
},
|
||||
fullWidth: {
|
||||
true: 'w-full',
|
||||
},
|
||||
shape: {
|
||||
default: '',
|
||||
rounded: 'rounded-full',
|
||||
},
|
||||
iconOnly: {
|
||||
true: '',
|
||||
},
|
||||
variant: {
|
||||
primary: [
|
||||
'text-elements-on-primary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'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-elements-on-secondary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-controls-secondary',
|
||||
'hover:bg-controls-secondary-hovered',
|
||||
'focus-visible:bg-controls-secondary-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
tertiary: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-border-interactive/10',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
ghost: [
|
||||
'text-elements-on-tertiary',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
danger: [
|
||||
'text-elements-on-danger',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-border-danger',
|
||||
'hover:bg-controls-danger-hovered',
|
||||
'focus-visible:bg-controls-danger-hovered',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
'danger-ghost': [
|
||||
'text-elements-danger',
|
||||
'border',
|
||||
'border-transparent',
|
||||
'bg-transparent',
|
||||
'hover:bg-controls-tertiary-hovered',
|
||||
'hover:border-border-interactive-hovered',
|
||||
'hover:border-border-interactive-hovered/[0.14]',
|
||||
'focus-visible:bg-controls-tertiary-hovered',
|
||||
'focus-visible:border-border-interactive-hovered',
|
||||
'focus-visible:border-border-interactive-hovered/[0.14]',
|
||||
'disabled:text-elements-on-disabled',
|
||||
'disabled:bg-controls-disabled',
|
||||
'disabled:border-transparent',
|
||||
'disabled:shadow-none',
|
||||
],
|
||||
unstyled: [],
|
||||
},
|
||||
},
|
||||
compoundVariants: [
|
||||
{
|
||||
size: 'lg',
|
||||
iconOnly: true,
|
||||
class: ['py-3.5', 'px-3.5'],
|
||||
},
|
||||
{
|
||||
size: 'md',
|
||||
iconOnly: true,
|
||||
class: ['py-3.25', 'px-3.25'],
|
||||
},
|
||||
{
|
||||
size: 'sm',
|
||||
iconOnly: true,
|
||||
class: ['py-2', 'px-2'],
|
||||
},
|
||||
{
|
||||
size: 'xs',
|
||||
iconOnly: true,
|
||||
class: ['py-1', 'px-1'],
|
||||
},
|
||||
],
|
||||
defaultVariants: {
|
||||
size: 'md',
|
||||
variant: 'primary',
|
||||
fullWidth: false,
|
||||
iconOnly: false,
|
||||
shape: 'rounded',
|
||||
},
|
||||
},
|
||||
{
|
||||
responsiveVariants: true,
|
||||
},
|
||||
);
|
||||
|
||||
/**
|
||||
* Represents the type of a button theme, which is derived from the `buttonTheme` variant props.
|
||||
*/
|
||||
export type ButtonTheme = VariantProps<typeof buttonTheme>;
|
178
packages/frontend/src/components/shared/Button/Button.tsx
Normal file
178
packages/frontend/src/components/shared/Button/Button.tsx
Normal file
@ -0,0 +1,178 @@
|
||||
import React, { forwardRef, 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';
|
||||
import { cloneIcon } from 'utils/cloneIcon';
|
||||
|
||||
/**
|
||||
* 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 = forwardRef<
|
||||
HTMLButtonElement | HTMLAnchorElement,
|
||||
ButtonOrLinkProps
|
||||
>(
|
||||
(
|
||||
{
|
||||
children,
|
||||
className,
|
||||
leftIcon,
|
||||
rightIcon,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
variant,
|
||||
...props
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
// 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,
|
||||
iconOnly = false,
|
||||
shape = 'rounded',
|
||||
as,
|
||||
}) => ({
|
||||
variant,
|
||||
size,
|
||||
fullWidth,
|
||||
iconOnly,
|
||||
shape,
|
||||
as,
|
||||
}))({ ...props, fullWidth, iconOnly, 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',
|
||||
);
|
||||
}
|
||||
|
||||
const iconSize = useCallback(() => {
|
||||
switch (styleProps.size) {
|
||||
case 'lg':
|
||||
return { width: 20, height: 20 };
|
||||
case 'sm':
|
||||
case 'xs':
|
||||
return { width: 16, height: 16 };
|
||||
case 'md':
|
||||
default: {
|
||||
return { width: 18, height: 18 };
|
||||
}
|
||||
}
|
||||
}, [styleProps.size])();
|
||||
|
||||
return (
|
||||
<Component
|
||||
{...props}
|
||||
// @ts-expect-error - ref is not a valid prop for button elements
|
||||
ref={ref}
|
||||
className={buttonTheme({ ...styleProps, class: className })}
|
||||
>
|
||||
{cloneIcon(leftIcon, { ...iconSize })}
|
||||
{children}
|
||||
{cloneIcon(rightIcon, { ...iconSize })}
|
||||
</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';
|
@ -0,0 +1,30 @@
|
||||
import React, { ComponentPropsWithoutRef } from 'react';
|
||||
|
||||
export interface CustomIconProps extends ComponentPropsWithoutRef<'svg'> {
|
||||
size?: number | string; // width and height will both be set as the same value
|
||||
name?: string;
|
||||
}
|
||||
|
||||
export const CustomIcon = ({
|
||||
children,
|
||||
width = 24,
|
||||
height = 24,
|
||||
size,
|
||||
viewBox = '0 0 24 24',
|
||||
name,
|
||||
...rest
|
||||
}: CustomIconProps) => {
|
||||
return (
|
||||
<svg
|
||||
aria-labelledby={name}
|
||||
height={size || height}
|
||||
role="presentation"
|
||||
viewBox={viewBox}
|
||||
width={size || width}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
);
|
||||
};
|
@ -0,0 +1,21 @@
|
||||
import React from 'react';
|
||||
import { CustomIcon, CustomIconProps } from './CustomIcon';
|
||||
|
||||
export const PlusIcon = (props: CustomIconProps) => {
|
||||
return (
|
||||
<CustomIcon
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
fill="none"
|
||||
{...props}
|
||||
>
|
||||
<path
|
||||
fillRule="evenodd"
|
||||
clipRule="evenodd"
|
||||
d="M1.66666 9.99999C1.66666 5.39762 5.39762 1.66666 9.99999 1.66666C14.6024 1.66666 18.3333 5.39762 18.3333 9.99999C18.3333 14.6024 14.6024 18.3333 9.99999 18.3333C5.39762 18.3333 1.66666 14.6024 1.66666 9.99999ZM10.625 6.46483C10.625 6.11966 10.3452 5.83983 9.99999 5.83983C9.65481 5.83983 9.37499 6.11966 9.37499 6.46483V9.37537H6.46446C6.11928 9.37537 5.83946 9.65519 5.83946 10.0004C5.83946 10.3455 6.11928 10.6254 6.46446 10.6254H9.37499V13.5359C9.37499 13.8811 9.65481 14.1609 9.99999 14.1609C10.3452 14.1609 10.625 13.8811 10.625 13.5359V10.6254H13.5355C13.8807 10.6254 14.1605 10.3455 14.1605 10.0004C14.1605 9.65519 13.8807 9.37537 13.5355 9.37537H10.625V6.46483Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</CustomIcon>
|
||||
);
|
||||
};
|
24
packages/frontend/src/components/shared/CustomIcon/README.md
Normal file
24
packages/frontend/src/components/shared/CustomIcon/README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# 1. What icons are compatible with this component?
|
||||
|
||||
- Viewbox "0 0 24 24": From where you're exporting from, please make sure the icon is using viewBox="0 0 24 24" before downloading/exporting. Not doing so will result in incorrect icon scaling
|
||||
|
||||
# 2. How to add a new icon?
|
||||
|
||||
**2.1 Sanitising the icon**
|
||||
|
||||
1. Duplicate a current icon e.g. CrossIcon and rename it accordingly.
|
||||
2. Rename the function inside the new file you duplicated too
|
||||
3. Replace the markup with your SVG markup (make sure it complies with the above section's rule)
|
||||
4. Depending on the svg you pasted...
|
||||
A. If the `<svg>` has only 1 child, remove the `<svg>` parent entirely so you only have the path left
|
||||
B. If your component has more than 1 paths, rename `<svg>` tag with the `<g>` tag. Then, remove all attributes of this `<g>` tag so that it's just `<g>`
|
||||
5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. <path d="..." fill="currentColor">. Leave the other attributes without removing them.
|
||||
6. If your icon has more than one colour, then it's up to you to decide whether we want to use tailwind to help set the fill and stroke colors
|
||||
7. Lastly, export your icon in `index.ts` by following what was done for CrossIcon
|
||||
8. Make sure to provide a name to the `<CustomIcon>` component for accessibility sake
|
||||
9. Done!
|
||||
|
||||
**2.3 Use your newly imported icon**
|
||||
|
||||
1. You can change simply use `<BellIcon size="32" />` to quickly change both width and height with the same value (square). For custom viewBox, width and height, simply provide all three props.
|
||||
2. Coloring the icon: Simply add a className with text color. E.g. `<BellIcon className="text-gray-500" />`
|
@ -0,0 +1,2 @@
|
||||
export * from './PlusIcon';
|
||||
export * from './CustomIcon';
|
@ -7,6 +7,7 @@ import { GQLClient } from 'gql-client';
|
||||
import { ThemeProvider } from '@material-tailwind/react';
|
||||
|
||||
import './index.css';
|
||||
import '@fontsource/inter';
|
||||
import App from './App';
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import { GQLClientProvider } from './context/GQLClientContext';
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Button, ButtonOrLinkProps } from 'components/shared/Button';
|
||||
import { PlusIcon } from 'components/shared/CustomIcon';
|
||||
import React from 'react';
|
||||
|
||||
const Page = () => {
|
||||
@ -15,32 +17,56 @@ const Page = () => {
|
||||
|
||||
{/* Insert Components here */}
|
||||
<div className="flex flex-col gap-10 items-center justify-between">
|
||||
<h1 className="text-2xl font-bold">Component A</h1>
|
||||
|
||||
<div className="flex flex-row gap-10 items-center justify-center">
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="h-20 w-40 bg-red-400 rounded-md focus-ring"
|
||||
/>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="h-20 w-40 bg-red-400 rounded-md focus-ring"
|
||||
/>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="h-20 w-40 bg-red-400 rounded-md focus-ring"
|
||||
/>
|
||||
<div
|
||||
tabIndex={0}
|
||||
className="h-20 w-40 bg-red-400 rounded-md focus-ring"
|
||||
/>
|
||||
<h1 className="text-2xl font-bold">Button</h1>
|
||||
<div className="flex flex-col gap-10">
|
||||
{['primary', 'secondary', 'tertiary', 'danger'].map(
|
||||
(variant, index) => (
|
||||
<div className="flex gap-5 flex-wrap" key={index}>
|
||||
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
|
||||
<Button
|
||||
leftIcon={<PlusIcon />}
|
||||
rightIcon={<PlusIcon />}
|
||||
variant={variant as ButtonOrLinkProps['variant']}
|
||||
size={
|
||||
size !== 'disabled'
|
||||
? (size as ButtonOrLinkProps['size'])
|
||||
: 'md'
|
||||
}
|
||||
key={`${variant}-${size}`}
|
||||
disabled={size === 'disabled'}
|
||||
>
|
||||
Button
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-row gap-10 items-center justify-center">
|
||||
<div className="h-20 w-40 bg-red-400 rounded-md" />
|
||||
<div className="h-20 w-40 bg-red-400 rounded-md" />
|
||||
<div className="h-20 w-40 bg-red-400 rounded-md" />
|
||||
<div className="h-20 w-40 bg-red-400 rounded-md" />
|
||||
),
|
||||
)}
|
||||
{[
|
||||
'primary',
|
||||
'secondary',
|
||||
'tertiary',
|
||||
'ghost',
|
||||
'danger',
|
||||
'danger-ghost',
|
||||
].map((variant, index) => (
|
||||
<div className="flex gap-5 flex-wrap" key={index}>
|
||||
{['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => (
|
||||
<Button
|
||||
iconOnly
|
||||
variant={variant as ButtonOrLinkProps['variant']}
|
||||
size={
|
||||
size !== 'disabled'
|
||||
? (size as ButtonOrLinkProps['size'])
|
||||
: 'md'
|
||||
}
|
||||
key={`${variant}-${size}`}
|
||||
disabled={size === 'disabled'}
|
||||
>
|
||||
<PlusIcon />
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
17
packages/frontend/src/utils/cloneIcon.tsx
Normal file
17
packages/frontend/src/utils/cloneIcon.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { Children, cloneElement, isValidElement } from 'react';
|
||||
import type { Attributes, ReactElement, ReactNode } from 'react';
|
||||
|
||||
/**
|
||||
* Clones an icon element with optional additional props.
|
||||
* @param {ReactNode} icon - The icon element to clone.
|
||||
* @param {Attributes & P} [props] - Additional props to apply to the cloned icon.
|
||||
* @returns {ReactNode} - The cloned icon element with the applied props.
|
||||
*/
|
||||
export function cloneIcon<P extends object>(
|
||||
icon: ReactNode,
|
||||
props?: Attributes & P,
|
||||
): ReactNode {
|
||||
return Children.map(icon, (child) =>
|
||||
isValidElement(child) ? cloneElement(child as ReactElement, props) : child,
|
||||
);
|
||||
}
|
@ -8,6 +8,9 @@ export default withMT({
|
||||
'../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}',
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'sans-serif'],
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
emerald: {
|
||||
@ -138,6 +141,15 @@ export default withMT({
|
||||
'high-contrast': '#0b1d2e',
|
||||
},
|
||||
},
|
||||
boxShadow: {
|
||||
button:
|
||||
'inset 0px 0px 4px rgba(255, 255, 255, 0.25), inset 0px -2px 0px rgba(0, 0, 0, 0.04)',
|
||||
},
|
||||
spacing: {
|
||||
2.5: '0.625rem',
|
||||
3.25: '0.8125rem',
|
||||
3.5: '0.875rem',
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
@ -14,7 +14,8 @@
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx"
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "src"
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
|
21
yarn.lock
21
yarn.lock
@ -1280,7 +1280,7 @@
|
||||
resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz"
|
||||
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
|
||||
|
||||
"@babel/runtime@^7.10.4", "@babel/runtime@^7.3.1":
|
||||
"@babel/runtime@^7.10.4", "@babel/runtime@^7.23.7", "@babel/runtime@^7.3.1":
|
||||
version "7.23.9"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.9.tgz#47791a15e4603bb5f905bc0753801cf21d6345f7"
|
||||
integrity sha512-0CX6F+BI2s9dkUqr08KFrAIZgNFj75rdBU/DjCyYLIaV/quFjkk6T+EJ2LkZHyZTbEV4L5p97mNkUsHl2wLFAw==
|
||||
@ -2068,6 +2068,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9"
|
||||
integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A==
|
||||
|
||||
"@fontsource/inter@^5.0.16":
|
||||
version "5.0.16"
|
||||
resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50"
|
||||
integrity sha512-qF0aH5UiZvCmneX5orJbVRoc2VTyLTV3X/7laMp03Qt28L+B9tFlZODOGUL64wDWc69YVdi1LeJB0cIgd51lvw==
|
||||
|
||||
"@gar/promisify@^1.1.3":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@gar/promisify/-/promisify-1.1.3.tgz#555193ab2e3bb3b6adc3d551c9c030d9e860daf6"
|
||||
@ -15026,6 +15031,20 @@ tailwind-merge@1.8.1:
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-1.8.1.tgz#0e56c8afbab2491f72e06381043ffec8b720ba04"
|
||||
integrity sha512-+fflfPxvHFr81hTJpQ3MIwtqgvefHZFUHFiIHpVIRXvG/nX9+gu2P7JNlFu2bfDMJ+uHhi/pUgzaYacMoXv+Ww==
|
||||
|
||||
tailwind-merge@^2.2.0:
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-merge/-/tailwind-merge-2.2.1.tgz#3f10f296a2dba1d88769de8244fafd95c3324aeb"
|
||||
integrity sha512-o+2GTLkthfa5YUt4JxPfzMIpQzZ3adD1vLVkvKE1Twl9UAhGsEbIZhHHZVRttyW177S8PDJI3bTQNaebyofK3Q==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.7"
|
||||
|
||||
tailwind-variants@^0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/tailwind-variants/-/tailwind-variants-0.2.0.tgz#5c3a24afb6e8bb23f33aa3aa3550add8848de8a3"
|
||||
integrity sha512-EuW5Sic7c0tzp+p5rJwAgb7398Jb0hi4zkyCstOoZPW0DWwr+EWkNtnZYEo5CjgE1tazHUzyt4oIhss64UXRVA==
|
||||
dependencies:
|
||||
tailwind-merge "^2.2.0"
|
||||
|
||||
tailwindcss@^3.0.2, tailwindcss@^3.3.6:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.3.6.tgz"
|
||||
|
Loading…
Reference in New Issue
Block a user