diff --git a/packages/frontend/.vscode/settings.json b/packages/frontend/.vscode/settings.json new file mode 100644 index 00000000..3566c26b --- /dev/null +++ b/packages/frontend/.vscode/settings.json @@ -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\\((([^()]*|\\([^()]*\\))*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"] + ] +} diff --git a/packages/frontend/package.json b/packages/frontend/package.json index 98f62537..a2a77ba1 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -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", diff --git a/packages/frontend/src/components/projects/project/settings/DisplayEnvironmentVariables.tsx b/packages/frontend/src/components/projects/project/settings/DisplayEnvironmentVariables.tsx index 75ee3dad..49e14c5e 100644 --- a/packages/frontend/src/components/projects/project/settings/DisplayEnvironmentVariables.tsx +++ b/packages/frontend/src/components/projects/project/settings/DisplayEnvironmentVariables.tsx @@ -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; 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..897fdfbc --- /dev/null +++ b/packages/frontend/src/components/shared/Button/Button.theme.ts @@ -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; 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..af845e0c --- /dev/null +++ b/packages/frontend/src/components/shared/Button/Button.tsx @@ -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, '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 = forwardRef< + HTMLButtonElement | HTMLAnchorElement, + ButtonOrLinkProps +>( + ( + { + children, + className, + leftIcon, + rightIcon, + fullWidth, + iconOnly, + shape, + variant, + ...props + }, + ref, + ) => { + // 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, + 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 ( + + {cloneIcon(leftIcon, { ...iconSize })} + {children} + {cloneIcon(rightIcon, { ...iconSize })} + + ); + }, +); + +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'; diff --git a/packages/frontend/src/components/shared/CustomIcon/CustomIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CustomIcon.tsx new file mode 100644 index 00000000..6b32bb73 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CustomIcon.tsx @@ -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 ( + + {children} + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/PlusIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/PlusIcon.tsx new file mode 100644 index 00000000..da4b1ee0 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/PlusIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const PlusIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/README.md b/packages/frontend/src/components/shared/CustomIcon/README.md new file mode 100644 index 00000000..d7e8b3d1 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/README.md @@ -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 `` has only 1 child, remove the `` parent entirely so you only have the path left + B. If your component has more than 1 paths, rename `` tag with the `` tag. Then, remove all attributes of this `` tag so that it's just `` +5. Usually, icons are single colored. If that's the case, replace all fill/stroke color with `currentColor`. E.g. . 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 `` component for accessibility sake +9. Done! + +**2.3 Use your newly imported icon** + +1. You can change simply use `` 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. `` diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts new file mode 100644 index 00000000..d50d942c --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -0,0 +1,2 @@ +export * from './PlusIcon'; +export * from './CustomIcon'; diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx index 6317b830..da29f575 100644 --- a/packages/frontend/src/index.tsx +++ b/packages/frontend/src/index.tsx @@ -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'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 9ba75753..8c23f341 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -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 */}
-

Component A

- -
-
-
-
-
-
- -
-
-
-
-
+

Button

+
+ {['primary', 'secondary', 'tertiary', 'danger'].map( + (variant, index) => ( +
+ {['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => ( + + ))} +
+ ), + )} + {[ + 'primary', + 'secondary', + 'tertiary', + 'ghost', + 'danger', + 'danger-ghost', + ].map((variant, index) => ( +
+ {['lg', 'md', 'sm', 'xs', 'disabled'].map((size) => ( + + ))} +
+ ))}
diff --git a/packages/frontend/src/utils/cloneIcon.tsx b/packages/frontend/src/utils/cloneIcon.tsx new file mode 100644 index 00000000..a9dc2745 --- /dev/null +++ b/packages/frontend/src/utils/cloneIcon.tsx @@ -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

( + icon: ReactNode, + props?: Attributes & P, +): ReactNode { + return Children.map(icon, (child) => + isValidElement(child) ? cloneElement(child as ReactElement, props) : child, + ); +} diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 6477b651..92ee77d0 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -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: [], diff --git a/packages/frontend/tsconfig.json b/packages/frontend/tsconfig.json index 9d379a3c..5452a6dc 100644 --- a/packages/frontend/tsconfig.json +++ b/packages/frontend/tsconfig.json @@ -14,7 +14,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx" + "jsx": "react-jsx", + "baseUrl": "src" }, "include": ["src"] } diff --git a/yarn.lock b/yarn.lock index 79c848fa..e4713492 100644 --- a/yarn.lock +++ b/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"