diff --git a/packages/frontend/package.json b/packages/frontend/package.json index a9674426..f31fa586 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -16,6 +16,7 @@ "@types/react": "^18.2.42", "@types/react-dom": "^18.2.17", "assert": "^2.1.0", + "clsx": "^2.1.0", "date-fns": "^3.3.1", "downshift": "^8.2.3", "eslint-config-react-app": "^7.0.1", diff --git a/packages/frontend/src/components/shared/Button/Button.theme.ts b/packages/frontend/src/components/shared/Button/Button.theme.ts index 897fdfbc..6c9614c6 100644 --- a/packages/frontend/src/components/shared/Button/Button.theme.ts +++ b/packages/frontend/src/components/shared/Button/Button.theme.ts @@ -118,6 +118,29 @@ export const buttonTheme = tv( 'disabled:border-transparent', 'disabled:shadow-none', ], + link: [ + 'p-0', + 'gap-1.5', + 'text-elements-link', + 'rounded', + 'focus-ring', + 'hover:underline', + 'hover:text-elements-link-hovered', + 'disabled:text-controls-disabled', + 'disabled:hover:no-underline', + ], + 'link-emphasized': [ + 'p-0', + 'gap-1.5', + 'text-elements-high-em', + 'rounded', + 'underline', + 'focus-ring', + 'hover:text-elements-link-hovered', + 'disabled:text-controls-disabled', + 'disabled:hover:no-underline', + 'dark:text-elements-on-high-contrast', + ], unstyled: [], }, }, diff --git a/packages/frontend/src/components/shared/CustomIcon/CrossIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CrossIcon.tsx new file mode 100644 index 00000000..8d649f0c --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CrossIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CrossIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/SearchIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/SearchIcon.tsx new file mode 100644 index 00000000..7688adcd --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/SearchIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const SearchIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/WarningIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/WarningIcon.tsx new file mode 100644 index 00000000..1fd27fb4 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/WarningIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const WarningIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index 96dd8eb4..18dbf249 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -5,4 +5,7 @@ export * from './ChevronGrabberHorizontal'; export * from './ChevronLeft'; export * from './ChevronRight'; export * from './InfoSquareIcon'; +export * from './WarningIcon'; +export * from './SearchIcon'; +export * from './CrossIcon'; export * from './GlobeIcon'; diff --git a/packages/frontend/src/components/shared/Input/Input.theme.ts b/packages/frontend/src/components/shared/Input/Input.theme.ts new file mode 100644 index 00000000..8def6a0d --- /dev/null +++ b/packages/frontend/src/components/shared/Input/Input.theme.ts @@ -0,0 +1,84 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const inputTheme = tv( + { + slots: { + container: [ + 'flex', + 'items-center', + 'rounded-lg', + 'relative', + 'placeholder:text-elements-disabled', + 'disabled:cursor-not-allowed', + 'disabled:bg-controls-disabled', + ], + label: ['text-sm', 'text-elements-high-em'], + description: ['text-xs', 'text-elements-low-em'], + input: [ + 'focus-ring', + 'block', + 'w-full', + 'h-full', + 'rounded-lg', + 'text-elements-mid-em', + 'shadow-sm', + 'border', + 'border-border-interactive', + 'disabled:shadow-none', + 'disabled:border-none', + ], + icon: ['text-elements-mid-em'], + iconContainer: [ + 'absolute', + 'inset-y-0', + 'flex', + 'items-center', + 'z-10', + 'cursor-pointer', + ], + helperIcon: [], + helperText: ['flex', 'gap-2', 'items-center', 'text-elements-danger'], + }, + variants: { + state: { + default: { + input: '', + }, + error: { + input: [ + 'outline', + 'outline-offset-0', + 'outline-border-danger', + 'shadow-none', + ], + helperText: 'text-elements-danger', + }, + }, + size: { + md: { + container: 'h-11', + input: ['text-sm pl-4 pr-4'], + icon: ['h-[18px] w-[18px]'], + helperText: 'text-sm', + helperIcon: ['h-5 w-5'], + }, + sm: { + container: 'h-8', + input: ['text-xs pl-3 pr-3'], + icon: ['h-4 w-4'], + helperText: 'text-xs', + helperIcon: ['h-4 w-4'], + }, + }, + }, + defaultVariants: { + size: 'md', + state: 'default', + }, + }, + { + responsiveVariants: true, + }, +); + +export type InputTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Input/Input.tsx b/packages/frontend/src/components/shared/Input/Input.tsx new file mode 100644 index 00000000..f5bbdca5 --- /dev/null +++ b/packages/frontend/src/components/shared/Input/Input.tsx @@ -0,0 +1,100 @@ +import React, { ReactNode, useMemo } from 'react'; +import { ComponentPropsWithoutRef } from 'react'; +import { InputTheme, inputTheme } from './Input.theme'; +import { WarningIcon } from 'components/shared/CustomIcon'; +import { cloneIcon } from 'utils/cloneIcon'; +import { cn } from 'utils/classnames'; + +export interface InputProps + extends InputTheme, + Omit, 'size'> { + label?: string; + description?: string; + leftIcon?: ReactNode; + rightIcon?: ReactNode; + helperText?: string; +} + +export const Input = ({ + className, + label, + description, + leftIcon, + rightIcon, + helperText, + size, + state, + ...props +}: InputProps) => { + const styleProps = (({ size = 'md', state }) => ({ + size, + state, + }))({ size, state }); + + const { + container: containerCls, + label: labelCls, + description: descriptionCls, + input: inputCls, + icon: iconCls, + iconContainer: iconContainerCls, + helperText: helperTextCls, + helperIcon: helperIconCls, + } = inputTheme({ ...styleProps }); + + const renderLabels = useMemo( + () => ( +
+

{label}

+

{description}

+
+ ), + [labelCls, descriptionCls, label, description], + ); + + const renderLeftIcon = useMemo(() => { + return ( +
+ {cloneIcon(leftIcon, { className: iconCls(), ariaHidden: true })} +
+ ); + }, [cloneIcon, iconCls, iconContainerCls, leftIcon]); + + const renderRightIcon = useMemo(() => { + return ( +
+ {cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })} +
+ ); + }, [cloneIcon, iconCls, iconContainerCls, rightIcon]); + + const renderHelperText = useMemo( + () => ( +
+ {state && + cloneIcon(, { + ariaHidden: true, + })} +

{helperText}

+
+ ), + [cloneIcon, state, helperIconCls, helperText, helperTextCls], + ); + + return ( +
+ {renderLabels} +
+ {leftIcon && renderLeftIcon} + + {rightIcon && renderRightIcon} +
+ {renderHelperText} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Input/index.ts b/packages/frontend/src/components/shared/Input/index.ts new file mode 100644 index 00000000..9ffcc229 --- /dev/null +++ b/packages/frontend/src/components/shared/Input/index.ts @@ -0,0 +1,2 @@ +export * from './Input'; +export * from './Input.theme'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 07ffedb7..b2c81d31 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -7,7 +7,11 @@ import { } from './renders/checkbox'; import { avatars, avatarsFallback } from './renders/avatar'; import { renderBadges } from './renders/badge'; -import { renderButtonIcons, renderButtons } from './renders/button'; +import { + renderButtonIcons, + renderButtons, + renderLinks, +} from './renders/button'; import { renderTabWithBadges, renderTabs, @@ -17,6 +21,7 @@ import { renderInlineNotificationWithDescriptions, renderInlineNotifications, } from './renders/inlineNotifications'; +import { renderInputs } from './renders/input'; const Page = () => { const [singleDate, setSingleDate] = useState(); @@ -37,6 +42,11 @@ const Page = () => { {/* Button */}
+

Input

+
{renderInputs()}
+ +
+

Button

{renderButtons()} @@ -129,6 +139,16 @@ const Page = () => { {renderInlineNotificationWithDescriptions()}
+ +
+ + {/* Link */} +
+

Link

+
+ {renderLinks()} +
+
diff --git a/packages/frontend/src/pages/components/renders/button.tsx b/packages/frontend/src/pages/components/renders/button.tsx index c1c69c48..f5272d3e 100644 --- a/packages/frontend/src/pages/components/renders/button.tsx +++ b/packages/frontend/src/pages/components/renders/button.tsx @@ -47,3 +47,19 @@ export const renderButtonIcons = () => { )); }; + +export const renderLinks = () => { + return ['link', 'link-emphasized', 'disabled'].map((variant) => ( + + )); +}; diff --git a/packages/frontend/src/pages/components/renders/input.tsx b/packages/frontend/src/pages/components/renders/input.tsx new file mode 100644 index 00000000..bc0b79ff --- /dev/null +++ b/packages/frontend/src/pages/components/renders/input.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { Input } from 'components/shared/Input'; +import { SearchIcon, CrossIcon } from 'components/shared/CustomIcon'; + +export const renderInputs = () => { + return ( + <> +
+ } + rightIcon={} + placeholder="Placeholder text" + /> + + +
+
+ } + rightIcon={} + description="Additional information or context" + placeholder="Placeholder text" + size="sm" + /> + + +
+ + ); +}; diff --git a/packages/frontend/src/utils/classnames.ts b/packages/frontend/src/utils/classnames.ts new file mode 100644 index 00000000..7c5139f9 --- /dev/null +++ b/packages/frontend/src/utils/classnames.ts @@ -0,0 +1,13 @@ +import { clsx } from 'clsx'; +import type { ClassValue } from 'clsx'; +import { twMerge } from 'tailwind-merge'; + +/** + * Returns a merged class name string by merging and processing multiple class names and Tailwind CSS styles. + * + * @param {...string[]} args - One or more class names and/or Tailwind CSS styles to be merged. + * @returns {string} - The merged class name string. + */ +export function cn(...args: ClassValue[]): string { + return twMerge(clsx(args)); +} diff --git a/yarn.lock b/yarn.lock index d24c5a8a..c8881430 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6327,7 +6327,7 @@ clone@^1.0.2: resolved "https://registry.yarnpkg.com/clone/-/clone-1.0.4.tgz#da309cc263df15994c688ca902179ca3c7cd7c7e" integrity sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg== -clsx@^2.0.0: +clsx@^2.0.0, clsx@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.0.tgz#e851283bcb5c80ee7608db18487433f7b23f77cb" integrity sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==