diff --git a/packages/frontend/package.json b/packages/frontend/package.json index f31fa586..53252773 100644 --- a/packages/frontend/package.json +++ b/packages/frontend/package.json @@ -7,7 +7,12 @@ "@material-tailwind/react": "^2.1.7", "@radix-ui/react-avatar": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-radio-group": "^1.1.3", "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-toast": "^1.1.5", + "@radix-ui/react-tooltip": "^1.0.7", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/packages/frontend/src/components/shared/Button/Button.theme.ts b/packages/frontend/src/components/shared/Button/Button.theme.ts index 6c9614c6..194c6550 100644 --- a/packages/frontend/src/components/shared/Button/Button.theme.ts +++ b/packages/frontend/src/components/shared/Button/Button.theme.ts @@ -62,7 +62,7 @@ export const buttonTheme = tv( 'text-elements-on-tertiary', 'border', 'border-border-interactive/10', - 'bg-transparent', + 'bg-controls-tertiary', 'hover:bg-controls-tertiary-hovered', 'hover:border-border-interactive-hovered', 'hover:border-border-interactive-hovered/[0.14]', diff --git a/packages/frontend/src/components/shared/Button/Button.tsx b/packages/frontend/src/components/shared/Button/Button.tsx index af845e0c..90d9085c 100644 --- a/packages/frontend/src/components/shared/Button/Button.tsx +++ b/packages/frontend/src/components/shared/Button/Button.tsx @@ -9,7 +9,7 @@ import { cloneIcon } from 'utils/cloneIcon'; /** * Represents the properties of a base button component. */ -interface ButtonBaseProps { +export interface ButtonBaseProps { /** * The optional left icon element for a component. * @type {ReactNode} @@ -98,19 +98,29 @@ const Button = forwardRef< href, ...baseLinkProps, }; - return {_children}; + return ( + // @ts-expect-error - ref + + {_children} + + ); } // Internal link return ( - + // @ts-expect-error - ref + {_children} ); } else { const { ...buttonProps } = _props; - // @ts-expect-error - as prop is not a valid prop for button elements - return ; + return ( + // @ts-expect-error - as prop is not a valid prop for button elements + + ); } }, [], @@ -161,8 +171,6 @@ const Button = forwardRef< return ( {cloneIcon(leftIcon, { ...iconSize })} diff --git a/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx new file mode 100644 index 00000000..6a51210a --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CalendarIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CalendarIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx new file mode 100644 index 00000000..145e68e1 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/CheckRoundFilledIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const CheckRoundFilledIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/InfoRoundFilledIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/InfoRoundFilledIcon.tsx new file mode 100644 index 00000000..d7eb24c4 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/InfoRoundFilledIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const InfoRoundFilledIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/LoadingIcon.tsx b/packages/frontend/src/components/shared/CustomIcon/LoadingIcon.tsx new file mode 100644 index 00000000..012fa986 --- /dev/null +++ b/packages/frontend/src/components/shared/CustomIcon/LoadingIcon.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { CustomIcon, CustomIconProps } from './CustomIcon'; + +export const LoadingIcon = (props: CustomIconProps) => { + return ( + + + + ); +}; diff --git a/packages/frontend/src/components/shared/CustomIcon/index.ts b/packages/frontend/src/components/shared/CustomIcon/index.ts index 18dbf249..46fd03ad 100644 --- a/packages/frontend/src/components/shared/CustomIcon/index.ts +++ b/packages/frontend/src/components/shared/CustomIcon/index.ts @@ -9,3 +9,7 @@ export * from './WarningIcon'; export * from './SearchIcon'; export * from './CrossIcon'; export * from './GlobeIcon'; +export * from './CalendarIcon'; +export * from './CheckRoundFilledIcon'; +export * from './InfoRoundFilledIcon'; +export * from './LoadingIcon'; diff --git a/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts b/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts new file mode 100644 index 00000000..522bd348 --- /dev/null +++ b/packages/frontend/src/components/shared/DatePicker/DatePicker.theme.ts @@ -0,0 +1,9 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const datePickerTheme = tv({ + slots: { + input: [], + }, +}); + +export type DatePickerTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx b/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx new file mode 100644 index 00000000..69b44e84 --- /dev/null +++ b/packages/frontend/src/components/shared/DatePicker/DatePicker.tsx @@ -0,0 +1,100 @@ +import React, { useCallback, useState } from 'react'; +import { Input, InputProps } from 'components/shared/Input'; +import * as Popover from '@radix-ui/react-popover'; +import { datePickerTheme } from './DatePicker.theme'; +import { Calendar, CalendarProps } from 'components/shared/Calendar'; +import { CalendarIcon } from 'components/shared/CustomIcon'; +import { Value } from 'react-calendar/dist/cjs/shared/types'; +import { format } from 'date-fns'; + +export interface DatePickerProps + extends Omit { + /** + * The props for the calendar component. + */ + calendarProps?: CalendarProps; + /** + * Optional callback function that is called when the value of the input changes. + * @param {string} value - The new value of the input. + * @returns None + */ + onChange?: (value: Value) => void; + /** + * The value of the input. + */ + value?: Value; + /** + * Whether to allow the selection of a date range. + */ + selectRange?: boolean; +} + +/** + * A date picker component that allows users to select a date from a calendar. + * @param {DatePickerProps} props - The props for the date picker component. + * @returns The rendered date picker component. + */ +export const DatePicker = ({ + className, + calendarProps, + value, + onChange, + selectRange = false, + ...props +}: DatePickerProps) => { + const { input } = datePickerTheme(); + + const [open, setOpen] = useState(false); + + /** + * Renders the value of the date based on the current state of `props.value`. + * @returns {string | undefined} - The formatted date value or `undefined` if `props.value` is falsy. + */ + const renderValue = useCallback(() => { + if (!value) return undefined; + if (Array.isArray(value)) { + return value + .map((date) => format(date as Date, 'dd/MM/yyyy')) + .join(' - '); + } + return format(value, 'dd/MM/yyyy'); + }, [value]); + + /** + * Handles the selection of a date from the calendar. + */ + const handleSelect = useCallback( + (date: Value) => { + setOpen(false); + onChange?.(date); + }, + [setOpen, onChange], + ); + + return ( + + + setOpen(true)} />} + readOnly + placeholder="Select a date..." + value={renderValue()} + className={input({ className })} + onClick={() => setOpen(true)} + /> + + + setOpen(false)}> + setOpen(false)} + onSelect={handleSelect} + /> + + + + ); +}; diff --git a/packages/frontend/src/components/shared/DatePicker/index.ts b/packages/frontend/src/components/shared/DatePicker/index.ts new file mode 100644 index 00000000..a48b62e4 --- /dev/null +++ b/packages/frontend/src/components/shared/DatePicker/index.ts @@ -0,0 +1 @@ +export * from './DatePicker'; diff --git a/packages/frontend/src/components/shared/Input/Input.theme.ts b/packages/frontend/src/components/shared/Input/Input.theme.ts index 8def6a0d..bf5ce915 100644 --- a/packages/frontend/src/components/shared/Input/Input.theme.ts +++ b/packages/frontend/src/components/shared/Input/Input.theme.ts @@ -50,6 +50,7 @@ export const inputTheme = tv( 'outline-offset-0', 'outline-border-danger', 'shadow-none', + 'focus:outline-border-danger', ], helperText: 'text-elements-danger', }, diff --git a/packages/frontend/src/components/shared/Input/Input.tsx b/packages/frontend/src/components/shared/Input/Input.tsx index f5bbdca5..d2d5459a 100644 --- a/packages/frontend/src/components/shared/Input/Input.tsx +++ b/packages/frontend/src/components/shared/Input/Input.tsx @@ -55,7 +55,7 @@ export const Input = ({ const renderLeftIcon = useMemo(() => { return (
- {cloneIcon(leftIcon, { className: iconCls(), ariaHidden: true })} + {cloneIcon(leftIcon, { className: iconCls(), 'aria-hidden': true })}
); }, [cloneIcon, iconCls, iconContainerCls, leftIcon]); @@ -63,7 +63,7 @@ export const Input = ({ const renderRightIcon = useMemo(() => { return (
- {cloneIcon(rightIcon, { className: iconCls(), ariaHidden: true })} + {cloneIcon(rightIcon, { className: iconCls(), 'aria-hidden': true })}
); }, [cloneIcon, iconCls, iconContainerCls, rightIcon]); @@ -73,7 +73,7 @@ export const Input = ({
{state && cloneIcon(, { - ariaHidden: true, + 'aria-hidden': true, })}

{helperText}

diff --git a/packages/frontend/src/components/shared/Radio/Radio.theme.ts b/packages/frontend/src/components/shared/Radio/Radio.theme.ts new file mode 100644 index 00000000..0b84601e --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/Radio.theme.ts @@ -0,0 +1,54 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const radioTheme = tv({ + slots: { + root: ['flex', 'gap-3', 'flex-wrap'], + wrapper: ['flex', 'items-center', 'gap-2', 'group'], + label: ['text-sm', 'tracking-[-0.006em]', 'text-elements-high-em'], + radio: [ + 'w-5', + 'h-5', + 'rounded-full', + 'border', + 'group', + 'border-border-interactive/10', + 'shadow-button', + 'group-hover:border-border-interactive/[0.14]', + 'focus-ring', + // Checked + 'data-[state=checked]:bg-controls-primary', + 'data-[state=checked]:group-hover:bg-controls-primary-hovered', + ], + indicator: [ + 'flex', + 'items-center', + 'justify-center', + 'relative', + 'w-full', + 'h-full', + 'after:content-[""]', + 'after:block', + 'after:w-2.5', + 'after:h-2.5', + 'after:rounded-full', + 'after:bg-transparent', + 'after:group-hover:bg-controls-disabled', + 'after:group-focus-visible:bg-controls-disabled', + // Checked + 'after:data-[state=checked]:bg-elements-on-primary', + 'after:data-[state=checked]:group-hover:bg-elements-on-primary', + 'after:data-[state=checked]:group-focus-visible:bg-elements-on-primary', + ], + }, + variants: { + orientation: { + vertical: { root: ['flex-col'] }, + horizontal: { root: ['flex-row'] }, + }, + }, + defaultVariants: { + orientation: 'vertical', + }, +}); + +export type RadioTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Radio/Radio.tsx b/packages/frontend/src/components/shared/Radio/Radio.tsx new file mode 100644 index 00000000..96542493 --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/Radio.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { + Root as RadixRoot, + RadioGroupProps, +} from '@radix-ui/react-radio-group'; +import { RadioTheme, radioTheme } from './Radio.theme'; +import { RadioItem, RadioItemProps } from './RadioItem'; + +export interface RadioOption extends RadioItemProps { + /** + * The label of the radio option. + */ + label: string; + /** + * The value of the radio option. + */ + value: string; +} + +export interface RadioProps extends RadioGroupProps, RadioTheme { + /** + * The options of the radio. + * @default [] + * @example + * ```tsx + * const options = [ + * { + * label: 'Label 1', + * value: '1', + * }, + * { + * label: 'Label 2', + * value: '2', + * }, + * { + * label: 'Label 3', + * value: '3', + * }, + * ]; + * ``` + */ + options: RadioOption[]; +} + +/** + * The Radio component is used to select one option from a list of options. + */ +export const Radio = ({ + className, + options, + orientation, + ...props +}: RadioProps) => { + const { root } = radioTheme({ orientation }); + + return ( + + {options.map((option) => ( + + ))} + + ); +}; diff --git a/packages/frontend/src/components/shared/Radio/RadioItem.tsx b/packages/frontend/src/components/shared/Radio/RadioItem.tsx new file mode 100644 index 00000000..177af9db --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/RadioItem.tsx @@ -0,0 +1,74 @@ +import React, { ComponentPropsWithoutRef } from 'react'; +import { + Item as RadixRadio, + Indicator as RadixIndicator, + RadioGroupItemProps, + RadioGroupIndicatorProps, +} from '@radix-ui/react-radio-group'; +import { radioTheme } from './Radio.theme'; + +export interface RadioItemProps extends RadioGroupItemProps { + /** + * The wrapper props of the radio item. + * You can use this prop to customize the wrapper props. + */ + wrapperProps?: ComponentPropsWithoutRef<'div'>; + /** + * The label props of the radio item. + * You can use this prop to customize the label props. + */ + labelProps?: ComponentPropsWithoutRef<'label'>; + /** + * The indicator props of the radio item. + * You can use this prop to customize the indicator props. + */ + indicatorProps?: RadioGroupIndicatorProps; + /** + * The id of the radio item. + */ + id?: string; + /** + * The label of the radio item. + */ + label?: string; +} + +/** + * The RadioItem component is used to render a radio item. + */ +export const RadioItem = ({ + className, + wrapperProps, + labelProps, + indicatorProps, + label, + id, + ...props +}: RadioItemProps) => { + const { wrapper, label: labelClass, radio, indicator } = radioTheme(); + + // Generate a unique id for the radio item from the label if the id is not provided + const kebabCaseLabel = label?.toLowerCase().replace(/\s+/g, '-'); + const componentId = id ?? kebabCaseLabel; + + return ( +
+ + + + {label && ( + + )} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Radio/index.ts b/packages/frontend/src/components/shared/Radio/index.ts new file mode 100644 index 00000000..6d49f1a8 --- /dev/null +++ b/packages/frontend/src/components/shared/Radio/index.ts @@ -0,0 +1,2 @@ +export * from './Radio'; +export * from './RadioItem'; diff --git a/packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx new file mode 100644 index 00000000..3fe5bda6 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControlItem.tsx @@ -0,0 +1,77 @@ +import React, { + forwardRef, + type ComponentPropsWithoutRef, + type ReactNode, +} from 'react'; + +import { + segmentedControlsTheme, + type SegmentedControlsVariants, +} from './SegmentedControls.theme'; +import { cloneIcon } from 'utils/cloneIcon'; + +/** + * Interface for the props of a segmented control item component. + */ +export interface SegmentedControlItemProps + extends Omit, 'type' | 'children'>, + SegmentedControlsVariants { + /** + * The optional left icon element for a component. + */ + leftIcon?: ReactNode; + /** + * The optional right icon element to display. + */ + rightIcon?: ReactNode; + /** + * Indicates whether the item is active or not. + */ + active?: boolean; + /** + * Optional prop that represents the children of a React component. + */ + children?: ReactNode; +} + +/** + * A functional component that represents an item in a segmented control. + * @returns The rendered segmented control item. + */ +const SegmentedControlItem = forwardRef< + HTMLButtonElement, + SegmentedControlItemProps +>( + ( + { + className, + children, + size, + type, + leftIcon, + rightIcon, + active = false, + ...props + }, + ref, + ) => { + const { item, icon } = segmentedControlsTheme({ size, type }); + + return ( + + ); + }, +); + +SegmentedControlItem.displayName = 'SegmentedControlItem'; + +export { SegmentedControlItem }; diff --git a/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts new file mode 100644 index 00000000..ed54a072 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.theme.ts @@ -0,0 +1,76 @@ +import { tv, type VariantProps } from 'tailwind-variants'; + +/** + * Defines the theme for a segmented controls. + */ +export const segmentedControlsTheme = tv({ + slots: { + parent: [ + 'flex', + 'items-center', + 'bg-base-bg-emphasized', + 'gap-0.5', + 'rounded-lg', + ], + item: [ + 'flex', + 'items-center', + 'justify-center', + 'gap-2', + 'text-elements-mid-em', + 'bg-transparent', + 'border', + 'border-transparent', + 'cursor-default', + 'whitespace-nowrap', + 'rounded-lg', + 'focus-ring', + 'hover:bg-controls-tertiary-hovered', + 'focus-visible:z-20', + 'focus-visible:bg-controls-tertiary-hovered', + 'disabled:text-controls-disabled', + 'disabled:bg-transparent', + 'disabled:cursor-not-allowed', + 'disabled:border-transparent', + 'data-[active=true]:bg-controls-tertiary', + 'data-[active=true]:text-elements-high-em', + 'data-[active=true]:border-border-interactive/10', + 'data-[active=true]:shadow-field', + 'data-[active=true]:hover:bg-controls-tertiary-hovered', + ], + icon: [], + }, + variants: { + size: { + sm: { + item: ['px-3', 'py-2', 'text-xs'], + icon: ['h-4', 'w-4'], + }, + md: { + item: ['px-4', 'py-3', 'text-sm', 'tracking-[-0.006em]'], + icon: ['h-5', 'w-5'], + }, + }, + type: { + 'fixed-width': { + parent: ['w-fit'], + item: ['w-fit'], + }, + 'full-width': { + parent: ['w-full'], + item: ['w-full'], + }, + }, + }, + defaultVariants: { + size: 'md', + type: 'fixed-width', + }, +}); + +/** + * Defines the type for the variants of a segmented controls. + */ +export type SegmentedControlsVariants = VariantProps< + typeof segmentedControlsTheme +>; diff --git a/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx new file mode 100644 index 00000000..f96fe8c2 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/SegmentedControls.tsx @@ -0,0 +1,93 @@ +import React, { + useCallback, + type ComponentPropsWithoutRef, + type ReactNode, +} from 'react'; + +import { + SegmentedControlItem, + type SegmentedControlItemProps, +} from './SegmentedControlItem'; +import { + segmentedControlsTheme, + type SegmentedControlsVariants, +} from './SegmentedControls.theme'; + +/** + * Represents an option for a segmented control. + */ +export interface SegmentedControlsOption + extends Omit { + /** + * The label of the item. + */ + label: ReactNode; + /** + * The value of the item. + * + */ + value: string; +} + +/** + * Represents the props for the SegmentedControls component. + */ +export interface SegmentedControlsProps + extends Omit, 'onChange'>, + SegmentedControlsVariants { + /** + * An array of options for a segmented control component. + */ + options: SegmentedControlsOption[]; + /** + * An optional string value. + */ + value?: T; + /** + * Optional callback function to handle changes in state. + */ + onChange?: (v: T) => void; +} + +/** + * A component that renders segmented controls with customizable options. + */ +export function SegmentedControls({ + className, + options, + value, + type, + size, + onChange, + ...props +}: SegmentedControlsProps) { + const { parent } = segmentedControlsTheme({ size, type }); + + /** + * Handles the change event for a given option. + */ + const handleChange = useCallback( + (option: T) => { + if (!option) return; + onChange?.(option); + }, + [onChange], + ); + + return ( +
+ {options.map((option, index) => ( + handleChange(option.value as T)} + {...option} + > + {option.label} + + ))} +
+ ); +} diff --git a/packages/frontend/src/components/shared/SegmentedControls/index.ts b/packages/frontend/src/components/shared/SegmentedControls/index.ts new file mode 100644 index 00000000..5d569509 --- /dev/null +++ b/packages/frontend/src/components/shared/SegmentedControls/index.ts @@ -0,0 +1,2 @@ +export * from './SegmentedControlItem'; +export * from './SegmentedControls'; diff --git a/packages/frontend/src/components/shared/Switch/Switch.theme.ts b/packages/frontend/src/components/shared/Switch/Switch.theme.ts new file mode 100644 index 00000000..87fbd092 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/Switch.theme.ts @@ -0,0 +1,84 @@ +import { tv, type VariantProps } from 'tailwind-variants'; + +export const switchTheme = tv({ + slots: { + wrapper: ['flex', 'items-start', 'gap-4', 'w-[375px]'], + switch: [ + 'h-6', + 'w-12', + 'rounded-full', + 'transition-all', + 'duration-500', + 'relative', + 'cursor-default', + 'shadow-inset', + 'focus-ring', + 'outline-none', + ], + thumb: [ + 'block', + 'h-4', + 'w-4', + 'translate-x-1', + 'transition-transform', + 'duration-100', + 'will-change-transform', + 'rounded-full', + 'shadow-button', + 'data-[state=checked]:translate-x-7', + 'bg-controls-elevated', + ], + label: [ + 'flex', + 'flex-1', + 'flex-col', + 'px-1', + 'gap-1', + 'text-sm', + 'text-elements-high-em', + 'tracking-[-0.006em]', + ], + description: ['text-xs', 'text-elements-low-em'], + }, + variants: { + checked: { + true: { + switch: [ + 'bg-controls-primary', + 'hover:bg-controls-primary-hovered', + 'focus-visible:bg-controls-primary-hovered', + ], + }, + false: { + switch: [ + 'bg-controls-inset', + 'hover:bg-controls-inset-hovered', + 'focus-visible:bg-controls-inset-hovered', + ], + }, + }, + disabled: { + true: { + switch: ['bg-controls-disabled', 'cursor-not-allowed'], + thumb: ['bg-elements-on-disabled'], + }, + }, + fullWidth: { + true: { + wrapper: ['w-full', 'justify-between'], + }, + }, + }, + compoundVariants: [ + { + checked: true, + disabled: true, + class: { + switch: ['bg-controls-disabled-active'], + thumb: ['bg-snowball-900'], + }, + }, + ], +}); + +export type SwitchVariants = VariantProps; diff --git a/packages/frontend/src/components/shared/Switch/Switch.tsx b/packages/frontend/src/components/shared/Switch/Switch.tsx new file mode 100644 index 00000000..32d35957 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/Switch.tsx @@ -0,0 +1,85 @@ +import React, { type ComponentPropsWithoutRef } from 'react'; +import { type SwitchProps as SwitchRadixProps } from '@radix-ui/react-switch'; +import * as SwitchRadix from '@radix-ui/react-switch'; + +import { switchTheme, type SwitchVariants } from './Switch.theme'; + +interface SwitchProps + extends Omit, + SwitchVariants { + /** + * The label of the switch. + */ + label?: string; + /** + * The description of the switch. + */ + description?: string; + /** + * Custom wrapper props for the switch. + */ + wrapperProps?: ComponentPropsWithoutRef<'div'>; + /** + * Function that is called when the checked state of the switch changes. + * @param checked The new checked state of the switch. + */ + onCheckedChange?(checked: boolean): void; +} + +/** + * A switch is a component used for toggling between two states. + */ +export const Switch = ({ + className, + checked, + label, + description, + disabled, + name, + wrapperProps, + fullWidth, + ...props +}: SwitchProps) => { + const { + wrapper, + switch: switchClass, + thumb, + label: labelClass, + description: descriptionClass, + } = switchTheme({ + checked, + disabled, + fullWidth, + }); + + const switchComponent = ( + + + + ); + + // If a label is provided, wrap the switch in a label element. + if (label) { + return ( +
+ + {switchComponent} +
+ ); + } + + return switchComponent; +}; diff --git a/packages/frontend/src/components/shared/Switch/index.ts b/packages/frontend/src/components/shared/Switch/index.ts new file mode 100644 index 00000000..1b19c1d3 --- /dev/null +++ b/packages/frontend/src/components/shared/Switch/index.ts @@ -0,0 +1 @@ +export * from './Switch'; diff --git a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts index 4667fe83..7c39b077 100644 --- a/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts +++ b/packages/frontend/src/components/shared/Tabs/Tabs.theme.ts @@ -9,6 +9,8 @@ export const tabsTheme = tv({ // Horizontal – default 'px-1', 'pb-5', + 'cursor-default', + 'select-none', 'text-elements-low-em', 'border-b-2', 'border-transparent', diff --git a/packages/frontend/src/components/shared/Toast/SimpleToast.theme.ts b/packages/frontend/src/components/shared/Toast/SimpleToast.theme.ts new file mode 100644 index 00000000..bac805ce --- /dev/null +++ b/packages/frontend/src/components/shared/Toast/SimpleToast.theme.ts @@ -0,0 +1,58 @@ +import { VariantProps, tv } from 'tailwind-variants'; + +export const simpleToastTheme = tv( + { + slots: { + wrapper: [ + 'flex', + 'items-center', + 'py-2', + 'pl-2', + 'pr-1.5', + 'gap-2', + 'rounded-full', + 'mx-auto', + 'mt-3', + 'w-fit', + 'overflow-hidden', + 'bg-surface-high-contrast', + 'shadow-sm', + ], + icon: ['flex', 'items-center', 'justify-center', 'w-5', 'h-5'], + closeIcon: [ + 'cursor-pointer', + 'flex', + 'items-center', + 'justify-center', + 'w-6', + 'h-6', + 'text-elements-on-high-contrast', + ], + title: ['text-sm', 'text-elements-on-high-contrast'], + }, + variants: { + variant: { + success: { + icon: ['text-elements-success'], + }, + error: { + icon: ['text-elements-danger'], + }, + warning: { + icon: ['text-elements-warning'], + }, + info: { + icon: ['text-elements-info'], + }, + loading: { + icon: ['text-elements-info'], + }, + }, + }, + }, + { + responsiveVariants: true, + }, +); + +export type SimpleToastTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Toast/SimpleToast.tsx b/packages/frontend/src/components/shared/Toast/SimpleToast.tsx new file mode 100644 index 00000000..7c10f855 --- /dev/null +++ b/packages/frontend/src/components/shared/Toast/SimpleToast.tsx @@ -0,0 +1,94 @@ +import React, { useMemo } from 'react'; +import * as ToastPrimitive from '@radix-ui/react-toast'; +import { ToastProps } from '@radix-ui/react-toast'; +import { motion } from 'framer-motion'; +import { simpleToastTheme, type SimpleToastTheme } from './SimpleToast.theme'; +import { + LoadingIcon, + CheckRoundFilledIcon, + CrossIcon, + InfoRoundFilledIcon, + WarningIcon, +} from 'components/shared/CustomIcon'; +import { Button, type ButtonOrLinkProps } from 'components/shared/Button'; +import { cloneIcon } from 'utils/cloneIcon'; + +type CtaProps = ButtonOrLinkProps & { + buttonLabel: string; +}; +export interface SimpleToastProps extends ToastProps { + id: string; + title: string; + variant?: SimpleToastTheme['variant']; + cta?: CtaProps[]; + onDismiss: (toastId: string) => void; +} + +export const SimpleToast = ({ + id, + className, + title, + variant = 'success', + cta = [], + onDismiss, + ...props +}: SimpleToastProps) => { + const hasCta = cta.length > 0; + const { + wrapper: wrapperCls, + icon: iconCls, + closeIcon: closeIconCls, + title: titleCls, + } = simpleToastTheme({ variant }); + + const Icon = useMemo(() => { + if (variant === 'success') return ; + if (variant === 'error') return ; + if (variant === 'warning') return ; + if (variant === 'info') return ; + return ; // variant === 'loading' + }, [variant]); + + const renderCta = useMemo(() => { + if (!hasCta) return null; + return ( +
+ {cta.map(({ buttonLabel, ...props }, index) => ( + + ))} +
+ ); + }, [cta]); + + const renderCloseButton = useMemo( + () => ( +
onDismiss(id)} className={closeIconCls()}> + +
+ ), + [id], + ); + + return ( + + + {cloneIcon(Icon, { className: iconCls() })} + +

{title}

+
+ {renderCta} + {renderCloseButton} +
+
+ ); +}; diff --git a/packages/frontend/src/components/shared/Toast/ToastProvider.tsx b/packages/frontend/src/components/shared/Toast/ToastProvider.tsx new file mode 100644 index 00000000..d45d6e89 --- /dev/null +++ b/packages/frontend/src/components/shared/Toast/ToastProvider.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { + Provider, + Viewport, + type ToastProviderProps, +} from '@radix-ui/react-toast'; + +export const ToastProvider = ({ children, ...props }: ToastProviderProps) => { + return ( + + {children} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Toast/Toaster.tsx b/packages/frontend/src/components/shared/Toast/Toaster.tsx new file mode 100644 index 00000000..66556956 --- /dev/null +++ b/packages/frontend/src/components/shared/Toast/Toaster.tsx @@ -0,0 +1,25 @@ +import React, { ComponentPropsWithoutRef, useMemo } from 'react'; +import { Provider, Viewport } from '@radix-ui/react-toast'; +import { SimpleToast, SimpleToastProps } from './SimpleToast'; +import { useToast } from './useToast'; + +interface ToasterProps extends ComponentPropsWithoutRef<'div'> {} + +export const Toaster = ({}: ToasterProps) => { + const { toasts } = useToast(); + + const renderToasts = useMemo( + () => + toasts.map(({ id, ...props }) => ( + + )), + [toasts], + ); + + return ( + + {renderToasts} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Toast/index.ts b/packages/frontend/src/components/shared/Toast/index.ts new file mode 100644 index 00000000..6404e342 --- /dev/null +++ b/packages/frontend/src/components/shared/Toast/index.ts @@ -0,0 +1,2 @@ +export * from './Toaster'; +export * from './useToast'; diff --git a/packages/frontend/src/components/shared/Toast/useToast.tsx b/packages/frontend/src/components/shared/Toast/useToast.tsx new file mode 100644 index 00000000..bd0be17e --- /dev/null +++ b/packages/frontend/src/components/shared/Toast/useToast.tsx @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import React from 'react'; +import { type ToastProps } from '@radix-ui/react-toast'; +import { SimpleToastProps } from './SimpleToast'; + +const TOAST_LIMIT = 3; +const TOAST_REMOVE_DELAY_DEFAULT = 7000; + +type ToasterToast = ToastProps & + SimpleToastProps & { + id: string; + }; + +const actionTypes = { + ADD_TOAST: 'ADD_TOAST', + UPDATE_TOAST: 'UPDATE_TOAST', + DISMISS_TOAST: 'DISMISS_TOAST', + REMOVE_TOAST: 'REMOVE_TOAST', +} as const; + +let count = 0; + +const genId = () => { + count = (count + 1) % Number.MAX_VALUE; + return count.toString(); +}; + +type ActionType = typeof actionTypes; + +type Action = + | { + type: ActionType['ADD_TOAST']; + toast: ToasterToast; + } + | { + type: ActionType['UPDATE_TOAST']; + toast: Partial; + } + | { + type: ActionType['DISMISS_TOAST']; + toastId?: ToasterToast['id']; + duration?: ToasterToast['duration']; + } + | { + type: ActionType['REMOVE_TOAST']; + toastId?: ToasterToast['id']; + }; + +interface State { + toasts: ToasterToast[]; +} + +const toastTimeouts = new Map>(); + +const addToRemoveQueue = (toastId: string, duration: number) => { + if (toastTimeouts.has(toastId)) { + return; + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId); + dispatch({ + type: 'REMOVE_TOAST', + toastId: toastId, + }); + }, duration ?? TOAST_REMOVE_DELAY_DEFAULT); + + toastTimeouts.set(toastId, timeout); +}; + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case 'ADD_TOAST': + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + }; + + case 'UPDATE_TOAST': + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id + ? ({ ...t, ...action.toast } as ToasterToast) + : t, + ), + }; + + case 'DISMISS_TOAST': { + const { toastId, duration = TOAST_REMOVE_DELAY_DEFAULT } = action; + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId, duration); + } else { + state.toasts.forEach((_toast) => { + addToRemoveQueue(_toast.id, duration); + }); + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t, + ), + }; + } + case 'REMOVE_TOAST': + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + }; + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + }; + } +}; + +const listeners: Array<(_state: State) => void> = []; + +let memoryState: State = { toasts: [] }; + +const dispatch = (action: Action) => { + memoryState = reducer(memoryState, action); + listeners.forEach((listener) => { + listener(memoryState); + }); +}; + +const toast = (props: ToasterToast) => { + if (!props.duration) { + props.duration = 2000; + } + const id = genId(); + + const update = (_props: ToasterToast) => + dispatch({ + type: 'UPDATE_TOAST', + toast: { ..._props, id }, + }); + const dismiss = () => + dispatch({ type: 'DISMISS_TOAST', toastId: id, duration: props.duration }); + + dispatch({ + type: 'ADD_TOAST', + toast: { + ...props, + id, + open: true, + onOpenChange: (open: boolean) => { + if (!open) dismiss(); + }, + }, + }); + + return { + id: id, + dismiss, + update, + }; +}; + +const useToast = () => { + const [state, setState] = React.useState(memoryState); + + React.useEffect(() => { + listeners.push(setState); + return () => { + const index = listeners.indexOf(setState); + if (index > -1) { + listeners.splice(index, 1); + } + }; + }, [state]); + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: 'DISMISS_TOAST', toastId }), + }; +}; + +export { toast, useToast }; diff --git a/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts b/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts new file mode 100644 index 00000000..c4c1c5e0 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/Tooltip.theme.ts @@ -0,0 +1,14 @@ +import { tv } from 'tailwind-variants'; + +export const tooltipTheme = tv({ + slots: { + content: [ + 'z-tooltip', + 'rounded-md', + 'bg-surface-high-contrast', + 'p-2', + 'text-elements-on-high-contrast', + ], + arrow: ['fill-surface-high-contrast'], + }, +}); diff --git a/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx b/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx new file mode 100644 index 00000000..75d73c78 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/Tooltip.tsx @@ -0,0 +1,47 @@ +import React from 'react'; +import type { + TooltipContentProps, + TooltipTriggerProps, +} from '@radix-ui/react-tooltip'; +import { ReactNode, useState } from 'react'; + +import { TooltipBase, type TooltipBaseProps } from './TooltipBase'; + +export interface TooltipProps extends TooltipBaseProps { + triggerProps?: TooltipTriggerProps; + contentProps?: TooltipContentProps; + content?: ReactNode; +} + +// https://github.com/radix-ui/primitives/issues/955#issuecomment-1798201143 +// Wrap on top of Tooltip base to make tooltip open on mobile via click +export const Tooltip = ({ + children, + triggerProps, + contentProps, + content, + ...props +}: TooltipProps) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + return ( + + setIsTooltipVisible(false)} + onClick={() => setIsTooltipVisible((prevOpen) => !prevOpen)} + onFocus={() => setTimeout(() => setIsTooltipVisible(true), 0)} + {...triggerProps} + > + {triggerProps?.children ?? children} + + + {content ?? contentProps?.children ?? 'Coming soon'} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx new file mode 100644 index 00000000..273a9cbe --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipBase.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import { + Provider, + TooltipProps as RadixTooltipProps, + Root, + type TooltipProviderProps, +} from '@radix-ui/react-tooltip'; +import { useState, type PropsWithChildren } from 'react'; + +import { TooltipContent } from './TooltipContent'; +import { TooltipTrigger } from './TooltipTrigger'; + +export interface TooltipBaseProps extends RadixTooltipProps { + providerProps?: TooltipProviderProps; +} + +export const TooltipBase = ({ + children, + providerProps, + ...props +}: PropsWithChildren) => { + const [isTooltipVisible, setIsTooltipVisible] = useState(false); + + return ( + + + {children} + + + ); +}; + +TooltipBase.Trigger = TooltipTrigger; +TooltipBase.Content = TooltipContent; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx new file mode 100644 index 00000000..57678206 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipContent/TooltipContent.tsx @@ -0,0 +1,46 @@ +import React from 'react'; +import { + Arrow, + Content, + Portal, + type TooltipArrowProps, + type TooltipContentProps, +} from '@radix-ui/react-tooltip'; + +import { tooltipTheme } from '../Tooltip.theme'; + +export interface ContentProps extends TooltipContentProps { + hasArrow?: boolean; + arrowProps?: TooltipArrowProps; +} + +export const TooltipContent = ({ + children, + arrowProps, + className, + hasArrow = true, + ...props +}: ContentProps) => { + const { content, arrow } = tooltipTheme(); + return ( + + + {hasArrow && ( + + )} + {children} + + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts b/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts new file mode 100644 index 00000000..1675248c --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipContent/index.ts @@ -0,0 +1 @@ +export * from './TooltipContent'; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx new file mode 100644 index 00000000..c27993ed --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/TooltipTrigger.tsx @@ -0,0 +1,12 @@ +import React from 'react'; +import { Trigger, type TooltipTriggerProps } from '@radix-ui/react-tooltip'; + +export type TriggerProps = TooltipTriggerProps; + +export const TooltipTrigger = ({ children, ...props }: TriggerProps) => { + return ( + + {children} + + ); +}; diff --git a/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts new file mode 100644 index 00000000..e73d66c6 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/TooltipTrigger/index.ts @@ -0,0 +1 @@ +export * from './TooltipTrigger'; diff --git a/packages/frontend/src/components/shared/Tooltip/index.ts b/packages/frontend/src/components/shared/Tooltip/index.ts new file mode 100644 index 00000000..a51f2a31 --- /dev/null +++ b/packages/frontend/src/components/shared/Tooltip/index.ts @@ -0,0 +1,2 @@ +export * from './Tooltip'; +export * from './TooltipBase'; diff --git a/packages/frontend/src/index.tsx b/packages/frontend/src/index.tsx index da29f575..d0c0d9b7 100644 --- a/packages/frontend/src/index.tsx +++ b/packages/frontend/src/index.tsx @@ -1,7 +1,6 @@ import React from 'react'; import ReactDOM from 'react-dom/client'; import assert from 'assert'; -import { Toaster } from 'react-hot-toast'; import { GQLClient } from 'gql-client'; import { ThemeProvider } from '@material-tailwind/react'; @@ -11,6 +10,7 @@ import '@fontsource/inter'; import App from './App'; import reportWebVitals from './reportWebVitals'; import { GQLClientProvider } from './context/GQLClientContext'; +import { Toaster } from 'components/shared/Toast'; const root = ReactDOM.createRoot( document.getElementById('root') as HTMLElement, @@ -26,7 +26,7 @@ root.render( - + , diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 5c1708eb..9714b58d 100644 --- a/packages/frontend/src/pages/components/index.tsx +++ b/packages/frontend/src/pages/components/index.tsx @@ -1,10 +1,10 @@ import React, { useState } from 'react'; import { Calendar } from 'components/shared/Calendar'; +import { DatePicker } from 'components/shared/DatePicker'; +import { Radio } from 'components/shared/Radio'; +import { SegmentedControls } from 'components/shared/SegmentedControls'; +import { Switch } from 'components/shared/Switch'; import { Value } from 'react-calendar/dist/cjs/shared/types'; -import { - renderCheckbox, - renderCheckboxWithDescription, -} from './renders/checkbox'; import { avatars, avatarsFallback } from './renders/avatar'; import { renderBadges } from './renders/badge'; import { @@ -14,20 +14,32 @@ import { renderLinks, } from './renders/button'; import { - renderTabWithBadges, - renderTabs, - renderVerticalTabs, -} from './renders/tabs'; + renderCheckbox, + renderCheckboxWithDescription, +} from './renders/checkbox'; import { renderInlineNotificationWithDescriptions, renderInlineNotifications, } from './renders/inlineNotifications'; import { renderInputs } from './renders/input'; +import { RADIO_OPTIONS } from './renders/radio'; +import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls'; +import { + renderTabWithBadges, + renderTabs, + renderVerticalTabs, +} from './renders/tabs'; import { renderDefaultTag, renderMinimalTag } from './renders/tag'; +import { renderToast, renderToastsWithCta } from './renders/toast'; +import { renderTooltips } from './renders/tooltip'; const Page = () => { const [singleDate, setSingleDate] = useState(); const [dateRange, setDateRange] = useState(); + const [selectedSegmentedControl, setSelectedSegmentedControl] = + useState('Test 1'); + const [switchValue, setSwitchValue] = useState(false); + const [selectedRadio, setSelectedRadio] = useState(''); return (
@@ -55,13 +67,31 @@ const Page = () => {
- {/* Button */} + {/* Toast */}
+

Toasts

+ {renderToastsWithCta()} + {renderToast()} +
+ +
+ + {/* Tooltip */} +
+

Tooltip

+
+ {renderTooltips()} +
+ +
+ + {/* Input */}

Input

{renderInputs()}
+ {/* Button */}

Button

{renderButtons()} @@ -137,6 +167,16 @@ const Page = () => { />
+ +

Date Picker

+
+ + +
@@ -165,6 +205,69 @@ const Page = () => {
+ {/* Segmented Controls */} +
+

Segmented Controls

+
+ + +
+
+ +
+ + {/* Switch */} +
+

Switch

+
+ + + + +
+
+ +
+ + {/* Radio */} +
+

Radio

+
+ + +
+
+ +
+ {/* Inline notification */}

Inline Notification

diff --git a/packages/frontend/src/pages/components/renders/radio.ts b/packages/frontend/src/pages/components/renders/radio.ts new file mode 100644 index 00000000..ff262f66 --- /dev/null +++ b/packages/frontend/src/pages/components/renders/radio.ts @@ -0,0 +1,16 @@ +import { RadioOption } from 'components/shared/Radio'; + +export const RADIO_OPTIONS: RadioOption[] = [ + { + label: 'Label 1', + value: '1', + }, + { + label: 'Label 2', + value: '2', + }, + { + label: 'Label 3', + value: '3', + }, +]; diff --git a/packages/frontend/src/pages/components/renders/segmentedControls.tsx b/packages/frontend/src/pages/components/renders/segmentedControls.tsx new file mode 100644 index 00000000..8f4a019f --- /dev/null +++ b/packages/frontend/src/pages/components/renders/segmentedControls.tsx @@ -0,0 +1,44 @@ +import React from 'react'; +import { Badge } from 'components/shared/Badge'; +import { SegmentedControlsOption } from 'components/shared/SegmentedControls'; + +export const SEGMENTED_CONTROLS_OPTIONS: SegmentedControlsOption[] = [ + { label: 'Test 1', value: 'Test 1' }, + { + label: 'Test 2', + value: 'Test 2', + leftIcon: ( + + 1 + + ), + }, + { + label: 'Test 3', + value: 'Test 3', + rightIcon: ( + + 1 + + ), + }, + { + label: 'Test 4', + value: 'Test 4', + leftIcon: ( + + 1 + + ), + rightIcon: ( + + 1 + + ), + }, + { + label: 'Test 5', + value: 'Test 5', + disabled: true, + }, +]; diff --git a/packages/frontend/src/pages/components/renders/toast.tsx b/packages/frontend/src/pages/components/renders/toast.tsx new file mode 100644 index 00000000..fb19e33a --- /dev/null +++ b/packages/frontend/src/pages/components/renders/toast.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import { Button } from 'components/shared/Button'; +import { useToast } from 'components/shared/Toast'; + +export const renderToastsWithCta = () => { + const { toast, dismiss } = useToast(); + + return ( +
+ {(['success', 'error', 'warning', 'info', 'loading'] as const).map( + (variant, index) => ( + + ), + )} +
+ ); +}; + +export const renderToast = () => { + const { toast, dismiss } = useToast(); + + return ( +
+ {(['success', 'error', 'warning', 'info', 'loading'] as const).map( + (variant, index) => ( + + ), + )} +
+ ); +}; diff --git a/packages/frontend/src/pages/components/renders/tooltip.tsx b/packages/frontend/src/pages/components/renders/tooltip.tsx new file mode 100644 index 00000000..7114f4db --- /dev/null +++ b/packages/frontend/src/pages/components/renders/tooltip.tsx @@ -0,0 +1,30 @@ +import React from 'react'; +import { Button } from 'components/shared/Button'; +import { Tooltip } from 'components/shared/Tooltip'; +import { ContentProps } from 'components/shared/Tooltip/TooltipContent'; + +const alignments: ContentProps['align'][] = ['start', 'center', 'end']; +const sides: ContentProps['side'][] = ['left', 'top', 'bottom', 'right']; + +export const renderTooltips = () => { + const tooltips = sides.map((side) => { + return alignments.map((align) => { + return ( + + + + ); + }); + }); + return tooltips; +}; diff --git a/packages/frontend/tailwind.config.js b/packages/frontend/tailwind.config.js index 4bcb0b01..ffd17636 100644 --- a/packages/frontend/tailwind.config.js +++ b/packages/frontend/tailwind.config.js @@ -8,10 +8,13 @@ export default withMT({ '../../node_modules/@material-tailwind/react/theme/components/**/*.{js,ts,jsx,tsx}', ], theme: { - fontFamily: { - sans: ['Inter', 'sans-serif'], - }, extend: { + zIndex: { + tooltip: '52', + }, + fontFamily: { + sans: ['Inter', 'sans-serif'], + }, fontSize: { '2xs': '0.625rem', '3xs': '0.5rem', @@ -151,12 +154,16 @@ export default withMT({ calendar: '0px 3px 20px rgba(8, 47, 86, 0.1), 0px 0px 4px rgba(8, 47, 86, 0.14)', field: '0px 1px 2px rgba(0, 0, 0, 0.04)', + inset: 'inset 0px 1px 0px rgba(8, 47, 86, 0.06)', }, spacing: { 2.5: '0.625rem', 3.25: '0.8125rem', 3.5: '0.875rem', }, + zIndex: { + toast: '9999', + }, }, }, plugins: [], diff --git a/yarn.lock b/yarn.lock index c8881430..ce8ae5dd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2032,6 +2032,13 @@ link-module-alias "^1.2.0" shx "^0.3.4" +"@floating-ui/core@^1.0.0": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.6.0.tgz#fa41b87812a16bf123122bf945946bae3fdf7fc1" + integrity sha512-PcF++MykgmTj3CIyOQbKA/hDzOAiqI3mhuoN44WRCopIs1sgoDoU4oty4Jtqaj/y3oDU6fnVSm4QG0a3t5i0+g== + dependencies: + "@floating-ui/utils" "^0.2.1" + "@floating-ui/core@^1.4.2": version "1.5.2" resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.5.2.tgz#53a0f7a98c550e63134d504f26804f6b83dbc071" @@ -2047,6 +2054,14 @@ "@floating-ui/core" "^1.4.2" "@floating-ui/utils" "^0.1.3" +"@floating-ui/dom@^1.6.1": + version "1.6.3" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.6.3.tgz#954e46c1dd3ad48e49db9ada7218b0985cee75ef" + integrity sha512-RnDthu3mzPlQ31Ss/BTwQ1zjzIhr3lk1gZB1OC56h/1vEtaXkESrOqL5fQVMfXpwGtRwX+YsZBdyHtJMQnkArw== + dependencies: + "@floating-ui/core" "^1.0.0" + "@floating-ui/utils" "^0.2.0" + "@floating-ui/react-dom@^1.2.2": version "1.3.0" resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-1.3.0.tgz#4d35d416eb19811c2b0e9271100a6aa18c1579b3" @@ -2054,6 +2069,13 @@ dependencies: "@floating-ui/dom" "^1.2.1" +"@floating-ui/react-dom@^2.0.0": + version "2.0.8" + resolved "https://registry.yarnpkg.com/@floating-ui/react-dom/-/react-dom-2.0.8.tgz#afc24f9756d1b433e1fe0d047c24bd4d9cefaa5d" + integrity sha512-HOdqOt3R3OGeTKidaLvJKcgg75S6tibQ3Tif4eyd91QnIJWr0NLvoXFpJA/j8HqkFSL68GDca9AuyWEHlhyClw== + dependencies: + "@floating-ui/dom" "^1.6.1" + "@floating-ui/react@0.19.0": version "0.19.0" resolved "https://registry.yarnpkg.com/@floating-ui/react/-/react-0.19.0.tgz#d8e19a3fcfaa0684d5ec3f335232b4e0ac0c87e1" @@ -2068,6 +2090,11 @@ resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.1.6.tgz#22958c042e10b67463997bd6ea7115fe28cbcaf9" integrity sha512-OfX7E2oUDYxtBvsuS4e/jSn4Q9Qb6DzgeYtsAdkPZ47znpoNsMgZw0+tVijiv3uGNR6dgNlty6r9rzIzHjtd/A== +"@floating-ui/utils@^0.2.0", "@floating-ui/utils@^0.2.1": + version "0.2.1" + resolved "https://registry.yarnpkg.com/@floating-ui/utils/-/utils-0.2.1.tgz#16308cea045f0fc777b6ff20a9f25474dd8293d2" + integrity sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q== + "@fontsource/inter@^5.0.16": version "5.0.16" resolved "https://registry.yarnpkg.com/@fontsource/inter/-/inter-5.0.16.tgz#b858508cdb56dcbbf3166903122851e2fbd16b50" @@ -3284,6 +3311,14 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-arrow@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-arrow/-/react-arrow-1.0.3.tgz#c24f7968996ed934d57fe6cde5d6ec7266e1d25d" + integrity sha512-wSP+pHsB/jQRaL6voubsQ/ZlrGBHHrOjmBnr19hxYgtS0WvAFwZhK2WP/YY5yF9uKECCEEDGxuLxq1NBK51wFA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-avatar@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-avatar/-/react-avatar-1.0.4.tgz#de9a5349d9e3de7bbe990334c4d2011acbbb9623" @@ -3342,6 +3377,35 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-dismissable-layer@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.5.tgz#3f98425b82b9068dfbab5db5fff3df6ebf48b9d4" + integrity sha512-aJeDjQhywg9LBu2t/At58hCvr7pEm0o2Ke1x33B+MhjNmmZ17sy4KImo0KPLgsnc/zN7GPdce8Cnn0SWvwZO7g== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-escape-keydown" "1.0.3" + +"@radix-ui/react-focus-guards@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.0.1.tgz#1ea7e32092216b946397866199d892f71f7f98ad" + integrity sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA== + dependencies: + "@babel/runtime" "^7.13.10" + +"@radix-ui/react-focus-scope@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.0.4.tgz#2ac45fce8c5bb33eb18419cdc1905ef4f1906525" + integrity sha512-sL04Mgvf+FmyvZeYfNu1EPAaaxD+aw7cYeIB9L9Fvq8+urhltTRaEo5ysKOpHuKPclsZcSUMKlN05x4u+CINpA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-id@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.0.1.tgz#73cdc181f650e4df24f0b6a5b7aa426b912c88c0" @@ -3350,6 +3414,53 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-popover@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c" + integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-focus-guards" "1.0.1" + "@radix-ui/react-focus-scope" "1.0.4" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + aria-hidden "^1.1.1" + react-remove-scroll "2.5.5" + +"@radix-ui/react-popper@1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.3.tgz#24c03f527e7ac348fabf18c89795d85d21b00b42" + integrity sha512-cKpopj/5RHZWjrbF2846jBNacjQVwkP068DfmgrNJXpvVWrOvlAmE9xSiy5OqeE+Gi8D9fP+oDhUnPqNMY8/5w== + dependencies: + "@babel/runtime" "^7.13.10" + "@floating-ui/react-dom" "^2.0.0" + "@radix-ui/react-arrow" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" + "@radix-ui/react-use-rect" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/rect" "1.0.1" + +"@radix-ui/react-portal@1.0.4": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.0.4.tgz#df4bfd353db3b1e84e639e9c63a5f2565fb00e15" + integrity sha512-Qki+C/EuGUVCQTOTD5vzJzJuMUlewbzuKyUy+/iHM2uwGiru9gZeBJtHAPKAEkB5KWGi9mP/CHKcY0wt1aW45Q== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-presence@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.0.1.tgz#491990ba913b8e2a5db1b06b203cb24b5cdef9ba" @@ -3367,6 +3478,23 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-slot" "1.0.2" +"@radix-ui/react-radio-group@^1.1.3": + version "1.1.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-radio-group/-/react-radio-group-1.1.3.tgz#3197f5dcce143bcbf961471bf89320735c0212d3" + integrity sha512-x+yELayyefNeKeTx4fjK6j99Fs6c4qKm3aY38G3swQVTN6xMpsrbigC0uHs2L//g8q4qR7qOcww8430jJmi2ag== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-direction" "1.0.1" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-roving-focus" "1.0.4" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-roving-focus@1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.0.4.tgz#e90c4a6a5f6ac09d3b8c1f5b5e81aab2f0db1974" @@ -3391,6 +3519,20 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-compose-refs" "1.0.1" +"@radix-ui/react-switch@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-switch/-/react-switch-1.0.3.tgz#6119f16656a9eafb4424c600fdb36efa5ec5837e" + integrity sha512-mxm87F88HyHztsI7N+ZUmEoARGkC22YVW5CaC+Byc+HRpuvCrOBPTAnXgf+tZ/7i0Sg/eOePGdMhUKhPaQEqow== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-previous" "1.0.1" + "@radix-ui/react-use-size" "1.0.1" + "@radix-ui/react-tabs@^1.0.4": version "1.0.4" resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz#993608eec55a5d1deddd446fa9978d2bc1053da2" @@ -3406,6 +3548,42 @@ "@radix-ui/react-roving-focus" "1.0.4" "@radix-ui/react-use-controllable-state" "1.0.1" +"@radix-ui/react-toast@^1.1.5": + version "1.1.5" + resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6" + integrity sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-collection" "1.0.3" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-tooltip@^1.0.7": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@radix-ui/react-tooltip/-/react-tooltip-1.0.7.tgz#8f55070f852e7e7450cc1d9210b793d2e5a7686e" + integrity sha512-lPh5iKNFVQ/jav/j6ZrWq3blfDJ0OH9R6FlNUHPMqdLuQ9vwDgFsRxvl8b7Asuy5c8xmoojHUxKHQSOAvMHxyw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/primitive" "1.0.1" + "@radix-ui/react-compose-refs" "1.0.1" + "@radix-ui/react-context" "1.0.1" + "@radix-ui/react-dismissable-layer" "1.0.5" + "@radix-ui/react-id" "1.0.1" + "@radix-ui/react-popper" "1.1.3" + "@radix-ui/react-portal" "1.0.4" + "@radix-ui/react-presence" "1.0.1" + "@radix-ui/react-primitive" "1.0.3" + "@radix-ui/react-slot" "1.0.2" + "@radix-ui/react-use-controllable-state" "1.0.1" + "@radix-ui/react-visually-hidden" "1.0.3" + "@radix-ui/react-use-callback-ref@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.0.1.tgz#f4bb1f27f2023c984e6534317ebc411fc181107a" @@ -3421,6 +3599,14 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-callback-ref" "1.0.1" +"@radix-ui/react-use-escape-keydown@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.0.3.tgz#217b840c250541609c66f67ed7bab2b733620755" + integrity sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-use-callback-ref" "1.0.1" + "@radix-ui/react-use-layout-effect@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz#be8c7bc809b0c8934acf6657b577daf948a75399" @@ -3435,6 +3621,14 @@ dependencies: "@babel/runtime" "^7.13.10" +"@radix-ui/react-use-rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/react-use-rect/-/react-use-rect-1.0.1.tgz#fde50b3bb9fd08f4a1cd204572e5943c244fcec2" + integrity sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/rect" "1.0.1" + "@radix-ui/react-use-size@1.0.1": version "1.0.1" resolved "https://registry.yarnpkg.com/@radix-ui/react-use-size/-/react-use-size-1.0.1.tgz#1c5f5fea940a7d7ade77694bb98116fb49f870b2" @@ -3443,6 +3637,21 @@ "@babel/runtime" "^7.13.10" "@radix-ui/react-use-layout-effect" "1.0.1" +"@radix-ui/react-visually-hidden@1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.0.3.tgz#51aed9dd0fe5abcad7dee2a234ad36106a6984ac" + integrity sha512-D4w41yN5YRKtu464TLnByKzMDG/JlMPHtfZgQAu9v6mNakUqGUI9vUrfQKz8NK41VMm/xbZbh76NUTVtIYqOMA== + dependencies: + "@babel/runtime" "^7.13.10" + "@radix-ui/react-primitive" "1.0.3" + +"@radix-ui/rect@1.0.1": + version "1.0.1" + resolved "https://registry.yarnpkg.com/@radix-ui/rect/-/rect-1.0.1.tgz#bf8e7d947671996da2e30f4904ece343bc4a883f" + integrity sha512-fyrgCaedtvMg9NK3en0pnOYJdtfwxUcNolezkNPUsoX57X8oQk+NkqcvzHXD2uKNij6GXmWU9NDru2IWjrO4BQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@remix-run/router@1.13.1": version "1.13.1" resolved "https://registry.yarnpkg.com/@remix-run/router/-/router-1.13.1.tgz#07e2a8006f23a3bc898b3f317e0a58cc8076b86e" @@ -5282,7 +5491,7 @@ argparse@^2.0.1: resolved "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz" integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== -aria-hidden@^1.1.3: +aria-hidden@^1.1.1, aria-hidden@^1.1.3: version "1.2.3" resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.3.tgz#14aeb7fb692bbb72d69bebfa47279c1fd725e954" integrity sha512-xcLxITLe2HYa1cnYnwCjkOO1PqUHQpozB8x9AR0OgWN2woOBi5kSDVxKfd0b7sb1hw5qFeJhXm9H1nu3xSfLeQ== @@ -7268,6 +7477,11 @@ detect-newline@^3.0.0: resolved "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz" integrity sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA== +detect-node-es@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/detect-node-es/-/detect-node-es-1.1.0.tgz#163acdf643330caa0b4cd7c21e7ee7755d6fa493" + integrity sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ== + detect-node@^2.0.4: version "2.1.0" resolved "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz" @@ -8858,6 +9072,11 @@ get-intrinsic@^1.0.2, get-intrinsic@^1.1.1, get-intrinsic@^1.1.3, get-intrinsic@ has-symbols "^1.0.3" hasown "^2.0.0" +get-nonce@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-nonce/-/get-nonce-1.0.1.tgz#fdf3f0278073820d2ce9426c18f07481b1e0cdf3" + integrity sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q== + get-own-enumerable-property-symbols@^3.0.0: version "3.0.2" resolved "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz" @@ -9687,6 +9906,13 @@ interpret@^1.0.0: resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.4.0.tgz#665ab8bc4da27a774a40584e812e3e0fa45b1a1e" integrity sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA== +invariant@^2.2.4: + version "2.2.4" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" + integrity sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== + dependencies: + loose-envify "^1.0.0" + ip@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/ip/-/ip-2.0.0.tgz#4cf4ab182fee2314c75ede1276f8c80b479936da" @@ -13749,6 +13975,25 @@ react-refresh@^0.11.0: resolved "https://registry.npmjs.org/react-refresh/-/react-refresh-0.11.0.tgz" integrity sha512-F27qZr8uUqwhWZboondsPx8tnC3Ct3SxZA3V5WyEvujRyyNv0VYPhoBg1gZ8/MV5tubQp76Trw8lTv9hzRBa+A== +react-remove-scroll-bar@^2.3.3: + version "2.3.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.5.tgz#cd2543b3ed7716c7c5b446342d21b0e0b303f47c" + integrity sha512-3cqjOqg6s0XbOjWvmasmqHch+RLxIEk2r/70rzGXuz3iIGQsQheEQyqYCBb5EECoD01Vo2SIbDqW4paLeLTASw== + dependencies: + react-style-singleton "^2.2.1" + tslib "^2.0.0" + +react-remove-scroll@2.5.5: + version "2.5.5" + resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.5.5.tgz#1e31a1260df08887a8a0e46d09271b52b3a37e77" + integrity sha512-ImKhrzJJsyXJfBZ4bzu8Bwpka14c/fQt0k+cyFp/PBhTfyDnU5hjOtM4AG/0AMyy8oKzOTR0lDgJIM7pYXI0kw== + dependencies: + react-remove-scroll-bar "^2.3.3" + react-style-singleton "^2.2.1" + tslib "^2.1.0" + use-callback-ref "^1.3.0" + use-sidecar "^1.1.2" + react-router-dom@^6.20.1: version "6.20.1" resolved "https://registry.yarnpkg.com/react-router-dom/-/react-router-dom-6.20.1.tgz#e34f8075b9304221420de3609e072bb349824984" @@ -13819,6 +14064,15 @@ react-scripts@5.0.1: optionalDependencies: fsevents "^2.3.2" +react-style-singleton@^2.2.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/react-style-singleton/-/react-style-singleton-2.2.1.tgz#f99e420492b2d8f34d38308ff660b60d0b1205b4" + integrity sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g== + dependencies: + get-nonce "^1.0.0" + invariant "^2.2.4" + tslib "^2.0.0" + react-syntax-highlighter@^15.5.0: version "15.5.0" resolved "https://registry.yarnpkg.com/react-syntax-highlighter/-/react-syntax-highlighter-15.5.0.tgz#4b3eccc2325fa2ec8eff1e2d6c18fa4a9e07ab20" @@ -15980,6 +16234,21 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +use-callback-ref@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/use-callback-ref/-/use-callback-ref-1.3.1.tgz#9be64c3902cbd72b07fe55e56408ae3a26036fd0" + integrity sha512-Lg4Vx1XZQauB42Hw3kK7JM6yjVjgFmFC5/Ab797s79aARomD2nEErc4mCgM8EZrARLmmbWpi5DGCadmK50DcAQ== + dependencies: + tslib "^2.0.0" + +use-sidecar@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-sidecar/-/use-sidecar-1.1.2.tgz#2f43126ba2d7d7e117aa5855e5d8f0276dfe73c2" + integrity sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw== + dependencies: + detect-node-es "^1.1.0" + tslib "^2.0.0" + usehooks-ts@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/usehooks-ts/-/usehooks-ts-2.10.0.tgz#ccd0d63168e4db95b061e26e585b0068d3fad0ed"