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';