diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 0e22ca7b..7caa5ab7 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -17,6 +17,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/InfoSquareIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/InfoSquareIcon.tsx
new file mode 100644
index 00000000..273131d6
--- /dev/null
+++ b/packages/frontend/src/components/shared/CustomIcon/InfoSquareIcon.tsx
@@ -0,0 +1,21 @@
+import React from 'react';
+import { CustomIcon, CustomIconProps } from './CustomIcon';
+
+export const InfoSquareIcon = (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 5e246238..7b84840e 100644
--- a/packages/frontend/src/components/shared/CustomIcon/index.ts
+++ b/packages/frontend/src/components/shared/CustomIcon/index.ts
@@ -4,5 +4,9 @@ export * from './CheckIcon';
export * from './ChevronGrabberHorizontal';
export * from './ChevronLeft';
export * from './ChevronRight';
+export * from './InfoSquareIcon';
+export * from './WarningIcon';
+export * from './SearchIcon';
+export * from './CrossIcon';
export * from './GlobeIcon';
export * from './CheckRoundFilledIcon';
diff --git a/packages/frontend/src/components/shared/InlineNotification/InlineNotification.theme.ts b/packages/frontend/src/components/shared/InlineNotification/InlineNotification.theme.ts
new file mode 100644
index 00000000..43536b55
--- /dev/null
+++ b/packages/frontend/src/components/shared/InlineNotification/InlineNotification.theme.ts
@@ -0,0 +1,78 @@
+import { VariantProps, tv } from 'tailwind-variants';
+
+export const inlineNotificationTheme = tv({
+ slots: {
+ wrapper: ['rounded-xl', 'flex', 'gap-2', 'items-start', 'w-full', 'border'],
+ content: ['flex', 'flex-col', 'gap-1'],
+ title: [],
+ description: [],
+ icon: ['flex', 'items-start'],
+ },
+ variants: {
+ variant: {
+ info: {
+ wrapper: ['border-border-info-light', 'bg-base-bg-emphasized-info'],
+ title: ['text-elements-on-emphasized-info'],
+ description: ['text-elements-on-emphasized-info'],
+ icon: ['text-elements-info'],
+ },
+ danger: {
+ wrapper: ['border-border-danger-light', 'bg-base-bg-emphasized-danger'],
+ title: ['text-elements-on-emphasized-danger'],
+ description: ['text-elements-on-emphasized-danger'],
+ icon: ['text-elements-danger'],
+ },
+ warning: {
+ wrapper: [
+ 'border-border-warning-light',
+ 'bg-base-bg-emphasized-warning',
+ ],
+ title: ['text-elements-on-emphasized-warning'],
+ description: ['text-elements-on-emphasized-warning'],
+ icon: ['text-elements-warning'],
+ },
+ success: {
+ wrapper: [
+ 'border-border-success-light',
+ 'bg-base-bg-emphasized-success',
+ ],
+ title: ['text-elements-on-emphasized-success'],
+ description: ['text-elements-on-emphasized-success'],
+ icon: ['text-elements-success'],
+ },
+ generic: {
+ wrapper: ['border-border-separator', 'bg-base-bg-emphasized'],
+ title: ['text-elements-high-em'],
+ description: ['text-elements-on-emphasized-info'],
+ icon: ['text-elements-high-em'],
+ },
+ },
+ size: {
+ sm: {
+ wrapper: ['px-2', 'py-2'],
+ title: ['leading-4', 'text-xs'],
+ description: ['leading-4', 'text-xs'],
+ icon: ['h-4', 'w-4'],
+ },
+ md: {
+ wrapper: ['px-3', 'py-3'],
+ title: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
+ description: ['leading-5', 'tracking-[-0.006em]', 'text-sm'],
+ icon: ['h-5', 'w-5'],
+ },
+ },
+ hasDescription: {
+ true: {
+ title: ['font-medium'],
+ },
+ },
+ },
+ defaultVariants: {
+ variant: 'generic',
+ size: 'md',
+ },
+});
+
+export type InlineNotificationTheme = VariantProps<
+ typeof inlineNotificationTheme
+>;
diff --git a/packages/frontend/src/components/shared/InlineNotification/InlineNotification.tsx b/packages/frontend/src/components/shared/InlineNotification/InlineNotification.tsx
new file mode 100644
index 00000000..b3b2845c
--- /dev/null
+++ b/packages/frontend/src/components/shared/InlineNotification/InlineNotification.tsx
@@ -0,0 +1,68 @@
+import React, { ReactNode, useCallback } from 'react';
+import { ComponentPropsWithoutRef } from 'react';
+import {
+ InlineNotificationTheme,
+ inlineNotificationTheme,
+} from './InlineNotification.theme';
+import { InfoSquareIcon } from 'components/shared/CustomIcon';
+import { cloneIcon } from 'utils/cloneIcon';
+
+export interface InlineNotificationProps
+ extends ComponentPropsWithoutRef<'div'>,
+ InlineNotificationTheme {
+ /**
+ * The title of the notification
+ */
+ title: string;
+ /**
+ * The description of the notification
+ */
+ description?: string;
+ /**
+ * The icon to display in the notification
+ * @default
+ */
+ icon?: ReactNode;
+}
+
+/**
+ * A notification that is displayed inline with the content
+ *
+ * @example
+ * ```tsx
+ *
+ * ```
+ */
+export const InlineNotification = ({
+ className,
+ title,
+ description,
+ size,
+ variant,
+ icon,
+ ...props
+}: InlineNotificationProps) => {
+ const {
+ wrapper,
+ content,
+ title: titleClass,
+ description: descriptionClass,
+ icon: iconClass,
+ } = inlineNotificationTheme({ size, variant, hasDescription: !!description });
+
+ // Render custom icon or default icon
+ const renderIcon = useCallback(() => {
+ if (!icon) return ;
+ return cloneIcon(icon, { className: iconClass() });
+ }, [icon]);
+
+ return (
+
+ {renderIcon()}
+
+
{title}
+ {description &&
{description}
}
+
+
+ );
+};
diff --git a/packages/frontend/src/components/shared/InlineNotification/index.ts b/packages/frontend/src/components/shared/InlineNotification/index.ts
new file mode 100644
index 00000000..fbe7bbd4
--- /dev/null
+++ b/packages/frontend/src/components/shared/InlineNotification/index.ts
@@ -0,0 +1 @@
+export * from './InlineNotification';
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 82fa69f3..5866cdde 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,
@@ -15,6 +19,11 @@ import {
} from './renders/tabs';
import { useToast } from 'components/shared/Toast/useToast';
import { Button } from 'components/shared/Button';
+import {
+ renderInlineNotificationWithDescriptions,
+ renderInlineNotifications,
+} from './renders/inlineNotifications';
+import { renderInputs } from './renders/input';
const Page = () => {
const { toast } = useToast();
@@ -24,7 +33,7 @@ const Page = () => {
return (
-
+
Manual Storybook
Get started by editing{' '}
@@ -44,6 +53,11 @@ const Page = () => {
{/* Button */}
+
Input
+
{renderInputs()}
+
+
+
Button
{renderButtons()}
@@ -123,6 +137,29 @@ const Page = () => {
Vertical Tabs
{renderVerticalTabs()}
+
+
+
+ {/* Inline notification */}
+
+
Inline Notification
+
+ {renderInlineNotifications()}
+
+
+ {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) => (
+ }
+ rightIcon={}
+ disabled={variant === 'disabled'}
+ >
+ Link
+
+ ));
+};
diff --git a/packages/frontend/src/pages/components/renders/inlineNotifications.tsx b/packages/frontend/src/pages/components/renders/inlineNotifications.tsx
new file mode 100644
index 00000000..fc36b914
--- /dev/null
+++ b/packages/frontend/src/pages/components/renders/inlineNotifications.tsx
@@ -0,0 +1,43 @@
+import { InlineNotification } from 'components/shared/InlineNotification';
+import { InlineNotificationTheme } from 'components/shared/InlineNotification/InlineNotification.theme';
+import React from 'react';
+
+const inlineNotificationVariants = [
+ 'info',
+ 'danger',
+ 'warning',
+ 'success',
+ 'generic',
+];
+const inlineNotificationSizes = ['md', 'sm'];
+
+export const renderInlineNotifications = () => {
+ return inlineNotificationVariants.map((variant) => (
+
+ {inlineNotificationSizes.map((size) => (
+
+ ))}
+
+ ));
+};
+
+export const renderInlineNotificationWithDescriptions = () => {
+ return inlineNotificationVariants.map((variant) => (
+
+ {inlineNotificationSizes.map((size) => (
+
+ ))}
+
+ ));
+};
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 98e99a0a..aa52ebdc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -6382,7 +6382,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==