diff --git a/packages/frontend/src/components/shared/Tags/Tags.theme.ts b/packages/frontend/src/components/shared/Tags/Tags.theme.ts new file mode 100644 index 0000000..299f31e --- /dev/null +++ b/packages/frontend/src/components/shared/Tags/Tags.theme.ts @@ -0,0 +1,93 @@ +import { tv } from 'tailwind-variants'; +import type { VariantProps } from 'tailwind-variants'; + +export const tagsTheme = tv( + { + slots: { + wrapper: ['flex', 'gap-1.5', 'rounded-lg', 'border'], + icon: ['h-4', 'w-4'], + label: ['font-inter', 'text-xs'], + }, + variants: { + type: { + attention: { + icon: ['text-elements-warning'], + }, + negative: { + icon: ['text-elements-danger'], + }, + positive: { + icon: ['text-elements-success'], + }, + emphasized: { + icon: ['text-elements-on-secondary'], + }, + neutral: { + icon: ['text-elements-mid-em'], + }, + }, + style: { + default: {}, + minimal: { + wrapper: ['border-border-interactive', 'bg-controls-tertiary'], + label: ['text-elements-high-em'], + }, + }, + size: { + sm: { + wrapper: ['px-2', 'py-2'], + }, + xs: { + wrapper: ['px-2', 'py-1.5'], + }, + }, + }, + compoundVariants: [ + { + type: 'attention', + style: 'default', + class: { + wrapper: ['border-orange-200', 'bg-orange-50'], + }, + }, + { + type: 'negative', + style: 'default', + class: { + wrapper: ['border-rose-200', 'bg-rose-50'], + }, + }, + { + type: 'positive', + style: 'default', + class: { + wrapper: ['border-emerald-200', 'bg-emerald-50'], + }, + }, + { + type: 'emphasized', + style: 'default', + class: { + wrapper: ['border-snowball-200', 'bg-snowball-50'], + }, + }, + { + type: 'neutral', + style: 'default', + class: { + wrapper: ['border-gray-200', 'bg-gray-50'], + }, + }, + ], + defaultVariants: { + type: 'attention', + style: 'default', + size: 'sm', + }, + }, + { + responsiveVariants: true, + }, +); + +export type TagsTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Tags/Tags.tsx b/packages/frontend/src/components/shared/Tags/Tags.tsx new file mode 100644 index 0000000..9cdb123 --- /dev/null +++ b/packages/frontend/src/components/shared/Tags/Tags.tsx @@ -0,0 +1,75 @@ +import React, { + type ReactNode, + type ComponentPropsWithoutRef, + useMemo, +} from 'react'; +import { TagsTheme, tagsTheme } from './Tags.theme'; +import { cloneIcon } from 'utils/cloneIcon'; + +type TagsProps = ComponentPropsWithoutRef<'div'> & + TagsTheme & { + /** + * The optional left icon element for a component. + * @type {ReactNode} + */ + leftIcon?: ReactNode; + /** + * The optional right icon element to display. + * @type {ReactNode} + */ + rightIcon?: ReactNode; + /** + * The optional type of the tags component. + * @type {TagsTheme['type']} + **/ + type?: TagsTheme['type']; + /** + * The optional style of the tags component. + * @type {TagsTheme['style']} + */ + style?: TagsTheme['style']; + /** + * The optional size of the tags component. + * @type {TagsTheme['size']} + */ + size?: TagsTheme['size']; + }; + +export const Tags = ({ + children, + leftIcon, + rightIcon, + type = 'attention', + style = 'default', + size = 'sm', +}: TagsProps) => { + const { + wrapper: wrapperCls, + icon: iconCls, + label: labelCls, + } = tagsTheme({ + type, + style, + size, + }); + + const renderLeftIcon = useMemo(() => { + if (!leftIcon) return null; + return
{cloneIcon(leftIcon, { size: 16 })}
; + }, [iconCls, leftIcon]); + + const renderRightIcon = useMemo(() => { + if (!rightIcon) return null; + return ( +
{cloneIcon(rightIcon, { size: 16 })}
+ ); + }, [iconCls, rightIcon]); + + return ( +
+ {renderLeftIcon} +

{children}

+ {renderRightIcon} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Tags/index.ts b/packages/frontend/src/components/shared/Tags/index.ts new file mode 100644 index 0000000..de1af19 --- /dev/null +++ b/packages/frontend/src/components/shared/Tags/index.ts @@ -0,0 +1,2 @@ +export * from './Tags'; +export * from './Tags.theme';