emblor-maintained
Version:
A full featured, unstyled tag input component for React
1 lines • 86.3 kB
Source Map (JSON)
{"version":3,"sources":["../src/tag/tag-input.tsx","../src/ui/input.tsx","../src/utils.ts","../src/ui/button.tsx","../src/tag/tag-popover.tsx","../src/ui/popover.tsx","../src/tag/tag-list.tsx","../src/tag/tag.tsx","../src/tag/autocomplete.tsx"],"sourcesContent":["'use client';\n\nimport React, { useMemo } from 'react';\nimport { Input } from '../ui/input';\nimport { Button } from '../ui/button';\nimport { type VariantProps } from 'class-variance-authority';\n// import { CommandInput } from '../ui/command';\nimport { TagPopover } from './tag-popover';\nimport { TagList } from './tag-list';\nimport { tagVariants } from './tag';\nimport { Autocomplete } from './autocomplete';\nimport { cn, uuid } from '../utils';\n\nexport enum Delimiter {\n Comma = ',',\n Enter = 'Enter',\n}\n\ntype OmittedInputProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, 'size' | 'value'>;\n\nexport type Tag = {\n id: string;\n text: string;\n};\n\nexport interface TagInputStyleClassesProps {\n inlineTagsContainer?: string;\n tagPopover?: {\n popoverTrigger?: string;\n popoverContent?: string;\n };\n tagList?: {\n container?: string;\n sortableList?: string;\n };\n autoComplete?: {\n command?: string;\n popoverTrigger?: string;\n popoverContent?: string;\n commandList?: string;\n commandGroup?: string;\n commandItem?: string;\n };\n tag?: {\n body?: string;\n closeButton?: string;\n };\n input?: string;\n clearAllButton?: string;\n}\n\nexport interface TagInputProps extends OmittedInputProps, VariantProps<typeof tagVariants> {\n placeholder?: string;\n tags: Tag[];\n setTags: React.Dispatch<React.SetStateAction<Tag[]>>;\n enableAutocomplete?: boolean;\n autocompleteOptions?: Tag[];\n maxTags?: number;\n minTags?: number;\n readOnly?: boolean;\n disabled?: boolean;\n onTagAdd?: (tag: string) => void;\n onTagRemove?: (tag: string) => void;\n allowDuplicates?: boolean;\n validateTag?: (tag: string) => boolean;\n delimiter?: Delimiter;\n showCount?: boolean;\n placeholderWhenFull?: string;\n sortTags?: boolean;\n delimiterList?: string[];\n truncate?: number;\n minLength?: number;\n maxLength?: number;\n usePopoverForTags?: boolean;\n value?: string | number | readonly string[] | { id: string; text: string }[];\n autocompleteFilter?: (option: string) => boolean;\n direction?: 'row' | 'column';\n onInputChange?: (value: string) => void;\n customTagRenderer?: (tag: Tag, isActiveTag: boolean) => React.ReactNode;\n onFocus?: React.FocusEventHandler<HTMLInputElement>;\n onBlur?: React.FocusEventHandler<HTMLInputElement>;\n onTagClick?: (tag: Tag) => void;\n draggable?: boolean;\n inputFieldPosition?: 'bottom' | 'top';\n clearAll?: boolean;\n onClearAll?: () => void;\n inputProps?: React.InputHTMLAttributes<HTMLInputElement>;\n restrictTagsToAutocompleteOptions?: boolean;\n inlineTags?: boolean;\n activeTagIndex: number | null;\n setActiveTagIndex: React.Dispatch<React.SetStateAction<number | null>>;\n styleClasses?: TagInputStyleClassesProps;\n usePortal?: boolean;\n addOnPaste?: boolean;\n addTagsOnBlur?: boolean;\n generateTagId?: () => string;\n}\n\nconst TagInput = React.forwardRef<HTMLInputElement, TagInputProps>((props, ref) => {\n const {\n id,\n placeholder,\n tags,\n setTags,\n variant,\n size,\n shape,\n enableAutocomplete,\n autocompleteOptions,\n maxTags,\n delimiter = Delimiter.Comma,\n onTagAdd,\n onTagRemove,\n allowDuplicates,\n showCount,\n validateTag,\n placeholderWhenFull = 'Max tags reached',\n sortTags,\n delimiterList,\n truncate,\n autocompleteFilter,\n borderStyle,\n textCase,\n interaction,\n animation,\n textStyle,\n minLength,\n maxLength,\n direction = 'row',\n onInputChange,\n customTagRenderer,\n onFocus,\n onBlur,\n onTagClick,\n draggable = false,\n inputFieldPosition = 'bottom',\n clearAll = false,\n onClearAll,\n usePopoverForTags = false,\n inputProps = {},\n restrictTagsToAutocompleteOptions,\n inlineTags = true,\n addTagsOnBlur = false,\n activeTagIndex,\n setActiveTagIndex,\n styleClasses = {},\n disabled = false,\n usePortal = false,\n addOnPaste = false,\n generateTagId = uuid,\n } = props;\n\n const [inputValue, setInputValue] = React.useState('');\n const [tagCount, setTagCount] = React.useState(Math.max(0, tags.length));\n const inputRef = React.useRef<HTMLInputElement>(null);\n\n if ((maxTags !== undefined && maxTags < 0) || (props.minTags !== undefined && props.minTags < 0)) {\n console.warn('maxTags and minTags cannot be less than 0');\n // error\n return null;\n }\n\n const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {\n const newValue = e.target.value;\n if (addOnPaste && newValue.includes(delimiter)) {\n const splitValues = newValue\n .split(delimiter)\n .map((v) => v.trim())\n .filter((v) => v);\n splitValues.forEach((value) => {\n if (!value) return; // Skip empty strings from split\n\n const newTagText = value.trim();\n\n // Check if the tag is in the autocomplete options if restrictTagsToAutocomplete is true\n if (restrictTagsToAutocompleteOptions && !autocompleteOptions?.some((option) => option.text === newTagText)) {\n console.warn('Tag not allowed as per autocomplete options');\n return;\n }\n\n if (validateTag && !validateTag(newTagText)) {\n console.warn('Invalid tag as per validateTag');\n return;\n }\n\n if (minLength && newTagText.length < minLength) {\n console.warn(`Tag \"${newTagText}\" is too short`);\n return;\n }\n\n if (maxLength && newTagText.length > maxLength) {\n console.warn(`Tag \"${newTagText}\" is too long`);\n return;\n }\n\n const newTagId = generateTagId();\n\n // Add tag if duplicates are allowed or tag does not already exist\n if (allowDuplicates || !tags.some((tag) => tag.text === newTagText)) {\n if (maxTags === undefined || tags.length < maxTags) {\n // Check for maxTags limit\n const newTag = { id: newTagId, text: newTagText };\n setTags((prevTags) => [...prevTags, newTag]);\n onTagAdd?.(newTagText);\n } else {\n console.warn('Reached the maximum number of tags allowed');\n }\n } else {\n console.warn(`Duplicate tag \"${newTagText}\" not added`);\n }\n });\n setInputValue('');\n } else {\n setInputValue(newValue);\n }\n onInputChange?.(newValue);\n };\n\n const handleInputFocus = (event: React.FocusEvent<HTMLInputElement>) => {\n setActiveTagIndex(null); // Reset active tag index when the input field gains focus\n onFocus?.(event);\n };\n\n const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {\n if (addTagsOnBlur && inputValue.trim()) {\n const newTagText = inputValue.trim();\n\n if (validateTag && !validateTag(newTagText)) {\n return;\n }\n\n if (minLength && newTagText.length < minLength) {\n console.warn('Tag is too short');\n return;\n }\n\n if (maxLength && newTagText.length > maxLength) {\n console.warn('Tag is too long');\n return;\n }\n\n if (\n (allowDuplicates || !tags.some((tag) => tag.text === newTagText)) &&\n (maxTags === undefined || tags.length < maxTags)\n ) {\n const newTagId = generateTagId();\n setTags([...tags, { id: newTagId, text: newTagText }]);\n onTagAdd?.(newTagText);\n setTagCount((prevTagCount) => prevTagCount + 1);\n setInputValue('');\n }\n }\n\n onBlur?.(event);\n };\n\n const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {\n if (delimiterList ? delimiterList.includes(e.key) : e.key === delimiter || e.key === Delimiter.Enter) {\n e.preventDefault();\n const newTagText = inputValue.trim();\n\n // Check if the tag is in the autocomplete options if restrictTagsToAutocomplete is true\n if (restrictTagsToAutocompleteOptions && !autocompleteOptions?.some((option) => option.text === newTagText)) {\n // error\n return;\n }\n\n if (validateTag && !validateTag(newTagText)) {\n return;\n }\n\n if (minLength && newTagText.length < minLength) {\n console.warn('Tag is too short');\n // error\n return;\n }\n\n // Validate maxLength\n if (maxLength && newTagText.length > maxLength) {\n // error\n console.warn('Tag is too long');\n return;\n }\n\n const newTagId = generateTagId();\n\n if (\n newTagText &&\n (allowDuplicates || !tags.some((tag) => tag.text === newTagText)) &&\n (maxTags === undefined || tags.length < maxTags)\n ) {\n setTags([...tags, { id: newTagId, text: newTagText }]);\n onTagAdd?.(newTagText);\n setTagCount((prevTagCount) => prevTagCount + 1);\n }\n setInputValue('');\n } else {\n switch (e.key) {\n case 'Delete':\n if (activeTagIndex !== null) {\n e.preventDefault();\n const newTags = [...tags];\n newTags.splice(activeTagIndex, 1);\n setTags(newTags);\n setActiveTagIndex((prev) =>\n newTags.length === 0 ? null : prev! >= newTags.length ? newTags.length - 1 : prev,\n );\n setTagCount((prevTagCount) => prevTagCount - 1);\n onTagRemove?.(tags[activeTagIndex].text);\n }\n break;\n case 'Backspace':\n if (activeTagIndex !== null) {\n e.preventDefault();\n const newTags = [...tags];\n newTags.splice(activeTagIndex, 1);\n setTags(newTags);\n setActiveTagIndex((prev) => (prev! === 0 ? null : prev! - 1));\n setTagCount((prevTagCount) => prevTagCount - 1);\n onTagRemove?.(tags[activeTagIndex].text);\n }\n break;\n case 'ArrowRight':\n e.preventDefault();\n if (activeTagIndex === null) {\n setActiveTagIndex(0);\n } else {\n setActiveTagIndex((prev) => (prev! + 1 >= tags.length ? 0 : prev! + 1));\n }\n break;\n case 'ArrowLeft':\n e.preventDefault();\n if (activeTagIndex === null) {\n setActiveTagIndex(tags.length - 1);\n } else {\n setActiveTagIndex((prev) => (prev! === 0 ? tags.length - 1 : prev! - 1));\n }\n break;\n case 'Home':\n e.preventDefault();\n setActiveTagIndex(0);\n break;\n case 'End':\n e.preventDefault();\n setActiveTagIndex(tags.length - 1);\n break;\n }\n }\n };\n\n const removeTag = (idToRemove: string) => {\n setTags(tags.filter((tag) => tag.id !== idToRemove));\n onTagRemove?.(tags.find((tag) => tag.id === idToRemove)?.text || '');\n setTagCount((prevTagCount) => prevTagCount - 1);\n };\n\n const onSortEnd = (oldIndex: number, newIndex: number) => {\n setTags((currentTags) => {\n const newTags = [...currentTags];\n const [removedTag] = newTags.splice(oldIndex, 1);\n newTags.splice(newIndex, 0, removedTag);\n\n return newTags;\n });\n };\n\n const handleClearAll = () => {\n if (!onClearAll) {\n setActiveTagIndex(-1);\n setTags([]);\n return;\n }\n onClearAll?.();\n };\n\n // const filteredAutocompleteOptions = autocompleteFilter\n // ? autocompleteOptions?.filter((option) => autocompleteFilter(option.text))\n // : autocompleteOptions;\n const filteredAutocompleteOptions = useMemo(() => {\n return (autocompleteOptions || []).filter((option) =>\n option.text.toLowerCase().includes(inputValue ? inputValue.toLowerCase() : ''),\n );\n }, [inputValue, autocompleteOptions]);\n\n const displayedTags = sortTags ? [...tags].sort() : tags;\n\n const truncatedTags = truncate\n ? tags.map((tag) => ({\n id: tag.id,\n text: tag.text?.length > truncate ? `${tag.text.substring(0, truncate)}...` : tag.text,\n }))\n : displayedTags;\n\n return (\n <div\n className={`w-full flex ${!inlineTags && tags.length > 0 ? 'gap-3' : ''} ${\n inputFieldPosition === 'bottom' ? 'flex-col' : inputFieldPosition === 'top' ? 'flex-col-reverse' : 'flex-row'\n }`}\n >\n {!usePopoverForTags &&\n (!inlineTags ? (\n <TagList\n tags={truncatedTags}\n customTagRenderer={customTagRenderer}\n variant={variant}\n size={size}\n shape={shape}\n borderStyle={borderStyle}\n textCase={textCase}\n interaction={interaction}\n animation={animation}\n textStyle={textStyle}\n onTagClick={onTagClick}\n draggable={draggable}\n onSortEnd={onSortEnd}\n onRemoveTag={removeTag}\n direction={direction}\n inlineTags={inlineTags}\n activeTagIndex={activeTagIndex}\n setActiveTagIndex={setActiveTagIndex}\n classStyleProps={{\n tagListClasses: styleClasses?.tagList,\n tagClasses: styleClasses?.tag,\n }}\n disabled={disabled}\n />\n ) : (\n !enableAutocomplete && (\n <div className=\"w-full\">\n <div\n className={cn(\n `flex flex-row flex-wrap items-center gap-2 p-2 w-full rounded-md border border-input bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`,\n styleClasses?.inlineTagsContainer,\n )}\n >\n <TagList\n tags={truncatedTags}\n customTagRenderer={customTagRenderer}\n variant={variant}\n size={size}\n shape={shape}\n borderStyle={borderStyle}\n textCase={textCase}\n interaction={interaction}\n animation={animation}\n textStyle={textStyle}\n onTagClick={onTagClick}\n draggable={draggable}\n onSortEnd={onSortEnd}\n onRemoveTag={removeTag}\n direction={direction}\n inlineTags={inlineTags}\n activeTagIndex={activeTagIndex}\n setActiveTagIndex={setActiveTagIndex}\n classStyleProps={{\n tagListClasses: styleClasses?.tagList,\n tagClasses: styleClasses?.tag,\n }}\n disabled={disabled}\n />\n <Input\n ref={inputRef}\n id={id}\n type=\"text\"\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n value={inputValue}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n {...inputProps}\n className={cn(\n 'border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit',\n // className,\n styleClasses?.input,\n )}\n autoComplete={enableAutocomplete ? 'on' : 'off'}\n list={enableAutocomplete ? 'autocomplete-options' : undefined}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n />\n </div>\n </div>\n )\n ))}\n {enableAutocomplete ? (\n <div className=\"w-full\">\n <Autocomplete\n tags={tags}\n setTags={setTags}\n setInputValue={setInputValue}\n autocompleteOptions={filteredAutocompleteOptions as Tag[]}\n setTagCount={setTagCount}\n maxTags={maxTags}\n onTagAdd={onTagAdd}\n onTagRemove={onTagRemove}\n allowDuplicates={allowDuplicates ?? false}\n inlineTags={inlineTags}\n usePortal={usePortal}\n classStyleProps={{\n command: styleClasses?.autoComplete?.command,\n popoverTrigger: styleClasses?.autoComplete?.popoverTrigger,\n popoverContent: styleClasses?.autoComplete?.popoverContent,\n commandList: styleClasses?.autoComplete?.commandList,\n commandGroup: styleClasses?.autoComplete?.commandGroup,\n commandItem: styleClasses?.autoComplete?.commandItem,\n }}\n >\n {!usePopoverForTags ? (\n !inlineTags ? (\n // <CommandInput\n // placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n // ref={inputRef}\n // value={inputValue}\n // disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n // onChangeCapture={handleInputChange}\n // onKeyDown={handleKeyDown}\n // onFocus={handleInputFocus}\n // onBlur={handleInputBlur}\n // className={cn(\n // 'w-full',\n // // className,\n // styleClasses?.input,\n // )}\n // />\n <Input\n ref={inputRef}\n id={id}\n type=\"text\"\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n value={inputValue}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n {...inputProps}\n className={cn(\n 'border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit',\n // className,\n styleClasses?.input,\n )}\n autoComplete={enableAutocomplete ? 'on' : 'off'}\n list={enableAutocomplete ? 'autocomplete-options' : undefined}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n />\n ) : (\n <div\n className={cn(\n `flex flex-row flex-wrap items-center p-2 gap-2 h-fit w-full bg-background text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50`,\n styleClasses?.inlineTagsContainer,\n )}\n >\n <TagList\n tags={truncatedTags}\n customTagRenderer={customTagRenderer}\n variant={variant}\n size={size}\n shape={shape}\n borderStyle={borderStyle}\n textCase={textCase}\n interaction={interaction}\n animation={animation}\n textStyle={textStyle}\n onTagClick={onTagClick}\n draggable={draggable}\n onSortEnd={onSortEnd}\n onRemoveTag={removeTag}\n direction={direction}\n inlineTags={inlineTags}\n activeTagIndex={activeTagIndex}\n setActiveTagIndex={setActiveTagIndex}\n classStyleProps={{\n tagListClasses: styleClasses?.tagList,\n tagClasses: styleClasses?.tag,\n }}\n disabled={disabled}\n />\n {/* <CommandInput\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n ref={inputRef}\n value={inputValue}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n onChangeCapture={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n inlineTags={inlineTags}\n className={cn(\n 'border-0 flex-1 w-fit h-5',\n // className,\n styleClasses?.input,\n )}\n /> */}\n <Input\n ref={inputRef}\n id={id}\n type=\"text\"\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n value={inputValue}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n {...inputProps}\n className={cn(\n 'border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit',\n // className,\n styleClasses?.input,\n )}\n autoComplete={enableAutocomplete ? 'on' : 'off'}\n list={enableAutocomplete ? 'autocomplete-options' : undefined}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n />\n </div>\n )\n ) : (\n <TagPopover\n tags={truncatedTags}\n customTagRenderer={customTagRenderer}\n variant={variant}\n size={size}\n shape={shape}\n borderStyle={borderStyle}\n textCase={textCase}\n interaction={interaction}\n animation={animation}\n textStyle={textStyle}\n onTagClick={onTagClick}\n draggable={draggable}\n onSortEnd={onSortEnd}\n onRemoveTag={removeTag}\n direction={direction}\n activeTagIndex={activeTagIndex}\n setActiveTagIndex={setActiveTagIndex}\n classStyleProps={{\n popoverClasses: styleClasses?.tagPopover,\n tagListClasses: styleClasses?.tagList,\n tagClasses: styleClasses?.tag,\n }}\n disabled={disabled}\n >\n {/* <CommandInput\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n ref={inputRef}\n value={inputValue}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n onChangeCapture={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n className={cn(\n 'w-full',\n // className,\n styleClasses?.input,\n )}\n /> */}\n <Input\n ref={inputRef}\n id={id}\n type=\"text\"\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n value={inputValue}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n {...inputProps}\n className={cn(\n 'border-0 h-5 bg-transparent focus-visible:ring-0 focus-visible:ring-transparent focus-visible:ring-offset-0 flex-1 w-fit',\n // className,\n styleClasses?.input,\n )}\n autoComplete={enableAutocomplete ? 'on' : 'off'}\n list={enableAutocomplete ? 'autocomplete-options' : undefined}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n />\n </TagPopover>\n )}\n </Autocomplete>\n </div>\n ) : (\n <div className=\"w-full\">\n {!usePopoverForTags ? (\n !inlineTags ? (\n <Input\n ref={inputRef}\n id={id}\n type=\"text\"\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n value={inputValue}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n {...inputProps}\n className={cn(\n styleClasses?.input,\n // className\n )}\n autoComplete={enableAutocomplete ? 'on' : 'off'}\n list={enableAutocomplete ? 'autocomplete-options' : undefined}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n />\n ) : null\n ) : (\n <TagPopover\n tags={truncatedTags}\n customTagRenderer={customTagRenderer}\n variant={variant}\n size={size}\n shape={shape}\n borderStyle={borderStyle}\n textCase={textCase}\n interaction={interaction}\n animation={animation}\n textStyle={textStyle}\n onTagClick={onTagClick}\n draggable={draggable}\n onSortEnd={onSortEnd}\n onRemoveTag={removeTag}\n direction={direction}\n activeTagIndex={activeTagIndex}\n setActiveTagIndex={setActiveTagIndex}\n classStyleProps={{\n popoverClasses: styleClasses?.tagPopover,\n tagListClasses: styleClasses?.tagList,\n tagClasses: styleClasses?.tag,\n }}\n disabled={disabled}\n >\n <Input\n ref={inputRef}\n id={id}\n type=\"text\"\n placeholder={maxTags !== undefined && tags.length >= maxTags ? placeholderWhenFull : placeholder}\n value={inputValue}\n onChange={handleInputChange}\n onKeyDown={handleKeyDown}\n onFocus={handleInputFocus}\n onBlur={handleInputBlur}\n {...inputProps}\n autoComplete={enableAutocomplete ? 'on' : 'off'}\n list={enableAutocomplete ? 'autocomplete-options' : undefined}\n disabled={disabled || (maxTags !== undefined && tags.length >= maxTags)}\n className={cn(\n 'border-0 w-full',\n styleClasses?.input,\n // className\n )}\n />\n </TagPopover>\n )}\n </div>\n )}\n\n {showCount && maxTags && (\n <div className=\"flex\">\n <span className=\"text-muted-foreground text-sm mt-1 ml-auto\">\n {`${tagCount}`}/{`${maxTags}`}\n </span>\n </div>\n )}\n {clearAll && (\n <Button type=\"button\" onClick={handleClearAll} className={cn('mt-2', styleClasses?.clearAllButton)}>\n Clear All\n </Button>\n )}\n </div>\n );\n});\n\nTagInput.displayName = 'TagInput';\n\nexport { TagInput };\n","import React, { forwardRef } from 'react';\nimport { cn } from '../utils';\n\nexport interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}\n\nconst Input = forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {\n return (\n <input\n type={type}\n className={cn(\n 'flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50',\n className,\n )}\n ref={ref}\n {...props}\n />\n );\n});\nInput.displayName = 'Input';\n\nexport { Input };\n","import { type ClassValue, clsx } from 'clsx';\nimport { twMerge } from 'tailwind-merge';\n\nexport function cn(...inputs: ClassValue[]) {\n return twMerge(clsx(inputs));\n}\n\nexport function uuid() {\n return crypto.getRandomValues(new Uint32Array(1))[0].toString();\n}\n","import * as React from 'react';\nimport { Slot } from '@radix-ui/react-slot';\nimport { cva, type VariantProps } from 'class-variance-authority';\nimport { cn } from '../utils';\n\nconst buttonVariants = cva(\n 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50',\n {\n variants: {\n variant: {\n default: 'bg-primary text-primary-foreground shadow hover:bg-primary/90',\n destructive: 'bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90',\n outline: 'border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground',\n secondary: 'bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80',\n ghost: 'hover:bg-accent hover:text-accent-foreground',\n link: 'text-primary underline-offset-4 hover:underline',\n },\n size: {\n default: 'h-9 px-4 py-2',\n sm: 'h-8 rounded-md px-3 text-xs',\n lg: 'h-10 rounded-md px-8',\n icon: 'h-9 w-9',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'default',\n },\n },\n);\n\nexport interface ButtonProps\n extends React.ButtonHTMLAttributes<HTMLButtonElement>,\n VariantProps<typeof buttonVariants> {\n asChild?: boolean;\n}\n\nconst Button = React.forwardRef<HTMLButtonElement, ButtonProps>(\n ({ className, variant, size, asChild = false, ...props }, ref) => {\n const Comp = asChild ? Slot : 'button';\n return <Comp className={cn(buttonVariants({ variant, size, className }))} ref={ref} {...props} />;\n },\n);\nButton.displayName = 'Button';\n\nexport { Button, buttonVariants };\n","import React, { useCallback, useEffect, useRef, useState } from 'react';\nimport { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';\nimport { TagInputStyleClassesProps, type Tag as TagType } from './tag-input';\nimport { TagList, TagListProps } from './tag-list';\nimport { Button } from '../ui/button';\nimport { cn } from '../utils';\n\ntype TagPopoverProps = {\n children: React.ReactNode;\n tags: TagType[];\n customTagRenderer?: (tag: TagType, isActiveTag: boolean) => React.ReactNode;\n activeTagIndex?: number | null;\n setActiveTagIndex?: (index: number | null) => void;\n classStyleProps: {\n popoverClasses: TagInputStyleClassesProps['tagPopover'];\n tagListClasses: TagInputStyleClassesProps['tagList'];\n tagClasses: TagInputStyleClassesProps['tag'];\n };\n disabled?: boolean;\n usePortal?: boolean;\n} & TagListProps;\n\nexport const TagPopover: React.FC<TagPopoverProps> = ({\n children,\n tags,\n customTagRenderer,\n activeTagIndex,\n setActiveTagIndex,\n classStyleProps,\n disabled,\n usePortal,\n ...tagProps\n}) => {\n const triggerContainerRef = useRef<HTMLDivElement | null>(null);\n const triggerRef = useRef<HTMLButtonElement | null>(null);\n const popoverContentRef = useRef<HTMLDivElement | null>(null);\n const inputRef = useRef<HTMLInputElement | null>(null);\n\n const [popoverWidth, setPopoverWidth] = useState<number>(0);\n const [isPopoverOpen, setIsPopoverOpen] = useState(false);\n const [inputFocused, setInputFocused] = useState(false);\n const [sideOffset, setSideOffset] = useState<number>(0);\n\n useEffect(() => {\n const handleResize = () => {\n if (triggerContainerRef.current && triggerRef.current) {\n setPopoverWidth(triggerContainerRef.current.offsetWidth);\n setSideOffset(triggerContainerRef.current.offsetWidth - triggerRef?.current?.offsetWidth);\n }\n };\n\n handleResize(); // Call on mount and layout changes\n\n window.addEventListener('resize', handleResize); // Adjust on window resize\n return () => window.removeEventListener('resize', handleResize);\n }, [triggerContainerRef, triggerRef]);\n\n // Close the popover when clicking outside of it\n useEffect(() => {\n const handleOutsideClick = (event: MouseEvent | TouchEvent | React.MouseEvent | React.TouchEvent) => {\n if (\n isPopoverOpen &&\n triggerContainerRef.current &&\n popoverContentRef.current &&\n !triggerContainerRef.current.contains(event.target as Node) &&\n !popoverContentRef.current.contains(event.target as Node)\n ) {\n setIsPopoverOpen(false);\n }\n };\n\n document.addEventListener('mousedown', handleOutsideClick);\n\n return () => {\n document.removeEventListener('mousedown', handleOutsideClick);\n };\n }, [isPopoverOpen]);\n\n const handleOpenChange = useCallback(\n (open: boolean) => {\n if (open && triggerContainerRef.current) {\n setPopoverWidth(triggerContainerRef.current.offsetWidth);\n }\n\n if (open) {\n inputRef.current?.focus();\n setIsPopoverOpen(open);\n }\n },\n [inputFocused],\n );\n\n const handleInputFocus = (event: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLTextAreaElement>) => {\n // Only set inputFocused to true if the popover is already open.\n // This will prevent the popover from opening due to an input focus if it was initially closed.\n if (isPopoverOpen) {\n setInputFocused(true);\n }\n\n const userOnFocus = (children as React.ReactElement<any>).props.onFocus;\n if (userOnFocus) userOnFocus(event);\n };\n\n const handleInputBlur = (event: React.FocusEvent<HTMLInputElement> | React.FocusEvent<HTMLTextAreaElement>) => {\n setInputFocused(false);\n\n // Allow the popover to close if no other interactions keep it open\n if (!isPopoverOpen) {\n setIsPopoverOpen(false);\n }\n\n const userOnBlur = (children as React.ReactElement<any>).props.onBlur;\n if (userOnBlur) userOnBlur(event);\n };\n\n return (\n <Popover open={isPopoverOpen} onOpenChange={handleOpenChange} modal={usePortal}>\n <div\n className=\"relative flex items-center rounded-md border border-input bg-transparent pr-3\"\n ref={triggerContainerRef}\n >\n {React.cloneElement(children as React.ReactElement<any>, {\n onFocus: handleInputFocus,\n onBlur: handleInputBlur,\n ref: inputRef,\n })}\n <PopoverTrigger asChild>\n <Button\n ref={triggerRef}\n variant=\"ghost\"\n size=\"icon\"\n role=\"combobox\"\n className={cn(`hover:bg-transparent`, classStyleProps?.popoverClasses?.popoverTrigger)}\n onClick={() => setIsPopoverOpen(!isPopoverOpen)}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"24\"\n height=\"24\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className={`lucide lucide-chevron-down h-4 w-4 shrink-0 opacity-50 ${isPopoverOpen ? 'rotate-180' : 'rotate-0'}`}\n >\n <path d=\"m6 9 6 6 6-6\"></path>\n </svg>\n </Button>\n </PopoverTrigger>\n </div>\n <PopoverContent\n ref={popoverContentRef}\n className={cn(`w-full space-y-3`, classStyleProps?.popoverClasses?.popoverContent)}\n style={{\n marginLeft: `-${sideOffset}px`,\n width: `${popoverWidth}px`,\n }}\n >\n <div className=\"space-y-1\">\n <h4 className=\"text-sm font-medium leading-none\">Entered Tags</h4>\n <p className=\"text-sm text-muted-foregrounsd text-left\">These are the tags you've entered.</p>\n </div>\n <TagList\n tags={tags}\n customTagRenderer={customTagRenderer}\n activeTagIndex={activeTagIndex}\n setActiveTagIndex={setActiveTagIndex}\n classStyleProps={{\n tagListClasses: classStyleProps?.tagListClasses,\n tagClasses: classStyleProps?.tagClasses,\n }}\n {...tagProps}\n disabled={disabled}\n />\n </PopoverContent>\n </Popover>\n );\n};\n","'use client';\n\nimport * as React from 'react';\nimport * as PopoverPrimitive from '@radix-ui/react-popover';\nimport { cn } from '../utils';\n\nconst Popover = PopoverPrimitive.Root;\n\nconst PopoverTrigger = PopoverPrimitive.Trigger;\n\nconst PopoverAnchor = PopoverPrimitive.Anchor;\n\nconst PopoverContent = React.forwardRef<\n React.ElementRef<typeof PopoverPrimitive.Content>,\n React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>\n>(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (\n <PopoverPrimitive.Portal>\n <PopoverPrimitive.Content\n ref={ref}\n align={align}\n sideOffset={sideOffset}\n className={cn(\n 'z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',\n className,\n )}\n {...props}\n />\n </PopoverPrimitive.Portal>\n));\nPopoverContent.displayName = PopoverPrimitive.Content.displayName;\n\nexport { Popover, PopoverTrigger, PopoverContent, PopoverAnchor };\n","import React from 'react';\nimport { TagInputStyleClassesProps, type Tag as TagType } from './tag-input';\nimport { Tag, TagProps } from './tag';\nimport SortableList, { SortableItem } from 'react-easy-sort';\nimport { cn } from '../utils';\n\nexport type TagListProps = {\n tags: TagType[];\n customTagRenderer?: (tag: TagType, isActiveTag: boolean) => React.ReactNode;\n direction?: TagProps['direction'];\n onSortEnd: (oldIndex: number, newIndex: number) => void;\n className?: string;\n inlineTags?: boolean;\n activeTagIndex?: number | null;\n setActiveTagIndex?: (index: number | null) => void;\n classStyleProps: {\n tagListClasses: TagInputStyleClassesProps['tagList'];\n tagClasses: TagInputStyleClassesProps['tag'];\n };\n disabled?: boolean;\n} & Omit<TagProps, 'tagObj'>;\n\nconst DropTarget: React.FC = () => {\n return <div className={cn('h-full rounded-md bg-secondary/50')} />;\n};\n\nexport const TagList: React.FC<TagListProps> = ({\n tags,\n customTagRenderer,\n direction,\n draggable,\n onSortEnd,\n className,\n inlineTags,\n activeTagIndex,\n setActiveTagIndex,\n classStyleProps,\n disabled,\n ...tagListProps\n}) => {\n const [draggedTagId, setDraggedTagId] = React.useState<string | null>(null);\n\n const handleMouseDown = (id: string) => {\n setDraggedTagId(id);\n };\n\n const handleMouseUp = () => {\n setDraggedTagId(null);\n };\n\n return (\n <>\n {!inlineTags ? (\n <div\n className={cn(\n 'rounded-md w-full',\n // className,\n {\n 'flex flex-wrap gap-2': direction === 'row',\n 'flex flex-col gap-2': direction === 'column',\n },\n classStyleProps?.tagListClasses?.container,\n )}\n >\n {draggable ? (\n <SortableList\n onSortEnd={onSortEnd}\n // className=\"flex flex-wrap gap-2 list\"\n className={`flex flex-wrap gap-2 list ${classStyleProps?.tagListClasses?.sortableList}`}\n dropTarget={<DropTarget />}\n >\n {tags.map((tagObj, index) => (\n <SortableItem key={tagObj.id}>\n <div\n onMouseDown={() => handleMouseDown(tagObj.id)}\n onMouseLeave={handleMouseUp}\n className={cn(\n {\n 'border border-solid border-primary rounded-md': draggedTagId === tagObj.id,\n },\n 'transition-all duration-200 ease-in-out',\n )}\n >\n {customTagRenderer ? (\n customTagRenderer(tagObj, index === activeTagIndex)\n ) : (\n <Tag\n tagObj={tagObj}\n isActiveTag={index === activeTagIndex}\n direction={direction}\n draggable={draggable}\n tagClasses={classStyleProps?.tagClasses}\n {...tagListProps}\n disabled={disabled}\n />\n )}\n </div>\n </SortableItem>\n ))}\n </SortableList>\n ) : (\n tags.map((tagObj, index) =>\n customTagRenderer ? (\n customTagRenderer(tagObj, index === activeTagIndex)\n ) : (\n <Tag\n key={tagObj.id}\n tagObj={tagObj}\n isActiveTag={index === activeTagIndex}\n direction={direction}\n draggable={draggable}\n tagClasses={classStyleProps?.tagClasses}\n {...tagListProps}\n disabled={disabled}\n />\n ),\n )\n )}\n </div>\n ) : (\n <>\n {draggable ? (\n <SortableList onSortEnd={onSortEnd} className=\"flex flex-wrap gap-2 list\" dropTarget={<DropTarget />}>\n {tags.map((tagObj, index) => (\n <SortableItem key={tagObj.id}>\n <div\n onMouseDown={() => handleMouseDown(tagObj.id)}\n onMouseLeave={handleMouseUp}\n className={cn(\n {\n 'border border-solid border-primary rounded-md': draggedTagId === tagObj.id,\n },\n 'transition-all duration-200 ease-in-out',\n )}\n >\n {customTagRenderer ? (\n customTagRenderer(tagObj, index === activeTagIndex)\n ) : (\n <Tag\n tagObj={tagObj}\n isActiveTag={index === activeTagIndex}\n direction={direction}\n draggable={draggable}\n tagClasses={classStyleProps?.tagClasses}\n {...tagListProps}\n disabled={disabled}\n />\n )}\n </div>\n </SortableItem>\n ))}\n </SortableList>\n ) : (\n tags.map((tagObj, index) =>\n customTagRenderer ? (\n customTagRenderer(tagObj, index === activeTagIndex)\n ) : (\n <Tag\n key={tagObj.id}\n tagObj={tagObj}\n isActiveTag={index === activeTagIndex}\n direction={direction}\n draggable={draggable}\n tagClasses={classStyleProps?.tagClasses}\n {...tagListProps}\n disabled={disabled}\n />\n ),\n )\n )}\n </>\n )}\n </>\n );\n};\n","import React from 'react';\nimport { Button } from '../ui/button';\nimport { cn } from '../utils';\nimport { TagInputProps, TagInputStyleClassesProps, type Tag as TagType } from './tag-input';\n\nimport { cva } from 'class-variance-authority';\n\nexport const tagVariants = cva('transition-all border inline-flex items-center text-sm pl-2 rounded-md', {\n variants: {\n variant: {\n default:\n 'bg-secondary text-secondary-foreground hover:bg-secondary/80 disabled:cursor-not-allowed disabled:opacity-50',\n primary:\n 'bg-primary border-primary text-primary-foreground hover:bg-primary/90 disabled:cursor-not-allowed disabled:opacity-50',\n destructive:\n 'bg-destructive border-destructive text-destructive-foreground hover:bg-destructive/90 disabled:cursor-not-allowed disabled:opacity-50',\n },\n size: {\n sm: 'text-xs h-7',\n md: 'text-sm h-8',\n lg: 'text-base h-9',\n xl: 'text-lg h-10',\n },\n shape: {\n default: 'rounded-sm',\n rounded: 'rounded-lg',\n square: 'rounded-none',\n pill: 'rounded-full',\n },\n borderStyle: {\n default: 'border-solid',\n none: 'border-none',\n dashed: 'border-dashed',\n dotted: 'border-dotted',\n double: 'border-double',\n },\n textCase: {\n uppercase: 'uppercase',\n lowercase: 'lowercase',\n capitalize: 'capitalize',\n },\n interaction: {\n clickable: 'cursor-pointer hover:shadow-md',\n nonClickable: 'cursor-default',\n },\n animation: {\n none: '',\n fadeIn: 'animate-fadeIn',\n slideIn: 'animate-slideIn',\n bounce: 'animate-bounce',\n },\n textStyle: {\n normal: 'font-normal',\n bold: 'font-bold',\n italic: 'italic',\n underline: 'underline',\n lineThrough: 'line-through',\n },\n },\n defaultVariants: {\n variant: 'default',\n size: 'md',\n shape: 'default',\n borderStyle: 'default',\n interaction: 'nonClickable',\n animation: 'fadeIn',\n textStyle: 'normal',\n },\n});\n\nexport type TagProps = {\n tagObj: TagType;\n variant: TagInputProps['variant'];\n size: TagInputProps['size'];\n shape: TagInputProps['shape'];\n borderStyle: TagInputProps['borderStyle'];\n textCase: TagInputProps['textCase'];\n interaction: TagInputProps['interaction'];\n animation: TagInputProps['animation'];\n textStyle: TagInputProps['textStyle'];\n onRemoveTag: (id: string) => void;\n isActiveTag?: boolean;\n tagClasses?: TagInputStyleClassesProps['tag'];\n disabled?: boolean;\n} & Pick<TagInputProps, 'direction' | 'onTagClick' | 'draggable'>;\n\nexport const Tag: React.FC<TagProps> = ({\n tagObj,\n direction,\n draggable,\n onTagClick,\n onRemoveTag,\n variant,\n size,\n shape,\n borderStyle,\n textCase,\n interaction,\n animation,\n textStyle,\n isActiveTag,\n tagClasses,\n disabled,\n}) => {\n return (\n <span\n key={tagObj.id}\n draggable={draggable}\n className={cn(\n tagVariants({\n variant,\n size,\n shape,\n borderStyle,\n textCase,\n interaction,\n animation,\n textStyle,\n }),\n {\n 'justify-between w-full': direction === 'column',\n 'cursor-pointer': draggable,\n 'ring-ring ring-offset-2 ring-2 ring-offset-background': isActiveTag,\n },\n tagClasses?.body,\n )}\n onClick={() => onTagClick?.(tagObj)}\n >\n {tagObj.text}\n <Button\n type=\"button\"\n variant=\"ghost\"\n onClick={(e) => {\n e.stopPropagation(); // Prevent event from bubbling up to the tag span\n onRemoveTag(tagObj.id);\n }}\n disabled={disabled}\n className={cn(`py-1 px-3 h-full hover:bg-transparent`, tagClasses?.closeButton)}\n >\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n width=\"14\"\n height=\"14\"\n viewBox=\"0 0 24 24\"\n fill=\"none\"\n stroke=\"currentColor\"\n strokeWidth=\"2\"\n strokeLinecap=\"round\"\n strokeLinejoin=\"round\"\n className=\"lucide lucide-x\"\n >\n <path d=\"M18 6 6 18\"></path>\n <path d=\"m6 6 12 12\"></path>\n </svg>\n </Button>\n </spa