diff --git a/packages/frontend/src/components/shared/Tag/Tag.theme.ts b/packages/frontend/src/components/shared/Tag/Tag.theme.ts new file mode 100644 index 00000000..086b59b3 --- /dev/null +++ b/packages/frontend/src/components/shared/Tag/Tag.theme.ts @@ -0,0 +1,93 @@ +import { tv } from 'tailwind-variants'; +import type { VariantProps } from 'tailwind-variants'; + +export const tagTheme = 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 TagTheme = VariantProps; diff --git a/packages/frontend/src/components/shared/Tag/Tag.tsx b/packages/frontend/src/components/shared/Tag/Tag.tsx new file mode 100644 index 00000000..086d3515 --- /dev/null +++ b/packages/frontend/src/components/shared/Tag/Tag.tsx @@ -0,0 +1,60 @@ +import React, { + type ReactNode, + type ComponentPropsWithoutRef, + useMemo, +} from 'react'; +import { tagTheme, type TagTheme } from './Tag.theme'; +import { cloneIcon } from 'utils/cloneIcon'; + +type TagProps = ComponentPropsWithoutRef<'div'> & + TagTheme & { + /** + * The optional left icon element for a component. + * @type {ReactNode} + */ + leftIcon?: ReactNode; + /** + * The optional right icon element to display. + * @type {ReactNode} + */ + rightIcon?: ReactNode; + }; + +export const Tag = ({ + children, + leftIcon, + rightIcon, + type = 'attention', + style = 'default', + size = 'sm', +}: TagProps) => { + const { + wrapper: wrapperCls, + icon: iconCls, + label: labelCls, + } = tagTheme({ + type, + style, + size, + }); + + const renderLeftIcon = useMemo(() => { + if (!leftIcon) return null; + return
{cloneIcon(leftIcon, { size: 16 })}
; + }, [cloneIcon, iconCls, leftIcon]); + + const renderRightIcon = useMemo(() => { + if (!rightIcon) return null; + return ( +
{cloneIcon(rightIcon, { size: 16 })}
+ ); + }, [cloneIcon, iconCls, rightIcon]); + + return ( +
+ {renderLeftIcon} +

{children}

+ {renderRightIcon} +
+ ); +}; diff --git a/packages/frontend/src/components/shared/Tag/index.ts b/packages/frontend/src/components/shared/Tag/index.ts new file mode 100644 index 00000000..e889934c --- /dev/null +++ b/packages/frontend/src/components/shared/Tag/index.ts @@ -0,0 +1,2 @@ +export * from './Tag'; +export * from './Tag.theme'; diff --git a/packages/frontend/src/pages/components/index.tsx b/packages/frontend/src/pages/components/index.tsx index 9c1d2569..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,21 +14,22 @@ import { renderLinks, } from './renders/button'; import { - renderTabWithBadges, - renderTabs, - renderVerticalTabs, -} from './renders/tabs'; -import { SegmentedControls } from 'components/shared/SegmentedControls'; -import { SEGMENTED_CONTROLS_OPTIONS } from './renders/segmentedControls'; -import { Switch } from 'components/shared/Switch'; -import { RADIO_OPTIONS } from './renders/radio'; -import { Radio } from 'components/shared/Radio'; + renderCheckbox, + renderCheckboxWithDescription, +} from './renders/checkbox'; import { renderInlineNotificationWithDescriptions, renderInlineNotifications, } from './renders/inlineNotifications'; import { renderInputs } from './renders/input'; -import { DatePicker } from 'components/shared/DatePicker'; +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'; @@ -53,6 +54,19 @@ const Page = () => {
+ {/* Tag */} +
+
+

Tag

+
+ {renderDefaultTag()} + {renderMinimalTag()} +
+
+
+ +
+ {/* Toast */}

Toasts

diff --git a/packages/frontend/src/pages/components/renders/tag.tsx b/packages/frontend/src/pages/components/renders/tag.tsx new file mode 100644 index 00000000..f01c526b --- /dev/null +++ b/packages/frontend/src/pages/components/renders/tag.tsx @@ -0,0 +1,63 @@ +import React from 'react'; +import { Tag } from 'components/shared/Tag'; +import { PlusIcon } from 'components/shared/CustomIcon'; + +export const renderDefaultTag = () => + (['default'] as const).map((style) => ( +
+ {( + ['attention', 'negative', 'positive', 'emphasized', 'neutral'] as const + ).map((type) => ( +
+ } + rightIcon={} + style={style} + type={type} + size="sm" + > + Label + + } + rightIcon={} + size="xs" + style={style} + type={type} + > + Label + +
+ ))} +
+ )); + +export const renderMinimalTag = () => + (['minimal'] as const).map((style) => ( +
+ {( + ['attention', 'negative', 'positive', 'emphasized', 'neutral'] as const + ).map((type) => ( +
+ } + rightIcon={} + style={style} + type={type} + size="sm" + > + Label + + } + rightIcon={} + size="xs" + style={style} + type={type} + > + Label + +
+ ))} +
+ ));