diff --git a/packages/utah-design-system/src/components/TagGroup.stories.tsx b/packages/utah-design-system/src/components/TagGroup.stories.tsx new file mode 100644 index 00000000..77a41f4d --- /dev/null +++ b/packages/utah-design-system/src/components/TagGroup.stories.tsx @@ -0,0 +1,26 @@ +import type { Meta } from '@storybook/react'; +import { Tag, TagGroup } from './TagGroup'; + +const meta: Meta = { + component: TagGroup, + parameters: { + layout: 'centered', + }, +}; + +export default meta; + +export const Example = (args: any) => ( + + Chocolate + Mint + Strawberry + Vanilla + +); + +Example.args = { + label: 'Ice cream flavor', + selectionMode: 'single', + onRemove: (key: string) => console.log('Removed:', key), +}; diff --git a/packages/utah-design-system/src/components/TagGroup.tsx b/packages/utah-design-system/src/components/TagGroup.tsx new file mode 100644 index 00000000..0a6ccc9a --- /dev/null +++ b/packages/utah-design-system/src/components/TagGroup.tsx @@ -0,0 +1,134 @@ +import { XIcon } from 'lucide-react'; +import { createContext, useContext } from 'react'; +import { + Tag as AriaTag, + TagGroup as AriaTagGroup, + TagGroupProps as AriaTagGroupProps, + TagProps as AriaTagProps, + Button, + TagList, + TagListProps, + Text, + composeRenderProps, +} from 'react-aria-components'; +import { twMerge } from 'tailwind-merge'; +import { tv } from 'tailwind-variants'; +import { Description, Label } from './Field'; +import { focusRing } from './utils'; + +const colors = { + gray: 'bg-gray-100 text-gray-600 border-gray-200 hover:border-gray-300 dark:bg-zinc-700 dark:text-zinc-300 dark:border-zinc-600 dark:hover:border-zinc-500', + primary: + 'bg-primary-100 text-primary-700 border-primary-200 hover:border-primary-300 dark:bg-primary-300/20 dark:text-primary-400 dark:border-primary-300/10 dark:hover:border-primary-300/20', + secondary: + 'bg-secondary-100 text-secondary-700 border-secondary-200 hover:border-secondary-300 dark:bg-secondary-300/20 dark:text-secondary-400 dark:border-secondary-300/10 dark:hover:border-secondary-300/20', + accent: + 'bg-accent-100 text-accent-700 border-accent-200 hover:border-accent-300 dark:bg-accent-400/20 dark:text-accent-300 dark:border-accent-400/10 dark:hover:border-accent-400/20', +}; + +type Color = keyof typeof colors; +const ColorContext = createContext('gray'); + +const tagStyles = tv({ + extend: focusRing, + base: 'flex max-w-fit cursor-default items-center gap-1 rounded-md border px-3 py-0.5 text-xs transition', + variants: { + color: { + gray: '', + primary: '', + secondary: '', + accent: '', + }, + allowsRemoving: { + true: 'pr-1', + }, + isSelected: { + true: 'border-transparent bg-primary-800 text-white forced-color-adjust-none forced-colors:bg-[Highlight] forced-colors:text-[HighlightText]', + }, + isDisabled: { + true: 'bg-gray-100 text-gray-300 forced-colors:text-[GrayText]', + }, + }, + compoundVariants: (Object.keys(colors) as Color[]).map((color) => ({ + isSelected: false, + color, + class: colors[color], + })), +}); + +export interface TagGroupProps + extends Omit, + Pick, 'items' | 'children' | 'renderEmptyState'> { + color?: Color; + label?: string; + description?: string; + errorMessage?: string; +} + +export interface TagProps extends AriaTagProps { + color?: Color; +} + +export function TagGroup({ + label, + description, + errorMessage, + items, + children, + renderEmptyState, + ...props +}: TagGroupProps) { + return ( + + + + + {children} + + + {description && {description}} + {errorMessage && ( + + {errorMessage} + + )} + + ); +} + +const removeButtonStyles = tv({ + extend: focusRing, + base: 'flex cursor-default items-center justify-center rounded-full p-0.5 transition-[background-color] hover:bg-black/10 pressed:bg-black/20 dark:hover:bg-white/10 dark:pressed:bg-white/20', +}); + +export function Tag({ children, color, ...props }: TagProps) { + let textValue = typeof children === 'string' ? children : undefined; + let groupColor = useContext(ColorContext); + return ( + + tagStyles({ ...renderProps, className, color: color || groupColor }), + )} + > + {({ allowsRemoving }) => ( + <> + {children} + {allowsRemoving && ( + + )} + + )} + + ); +}