feat: Adding an auto-completion component

feat-adding-an-auto-completion-component
liangfung 2023-11-05 19:32:19 +08:00
parent 00e0c4fddc
commit 10a2ea2afd
9 changed files with 511 additions and 47 deletions

View File

@ -1,17 +1,26 @@
import { UseChatHelpers } from 'ai/react'
import * as React from 'react'
import Textarea from 'react-textarea-autosize'
import { Button, buttonVariants } from '@/components/ui/button'
import { IconArrowElbow, IconEdit, IconPlus } from '@/components/ui/icons'
import { IconArrowElbow, IconEdit } from '@/components/ui/icons'
import {
Tooltip,
TooltipContent,
TooltipTrigger
} from '@/components/ui/tooltip'
import {
Combobox,
ComboboxAnchor,
ComboboxContent,
ComboboxOption,
ComboboxTextarea
} from '@/components/ui/combobox'
import { Popover, PopoverAnchor, PopoverContent } from '@/components/ui/popover'
import { useEnterSubmit } from '@/lib/hooks/use-enter-submit'
import { cn } from '@/lib/utils'
import { useRouter } from 'next/navigation'
import { cn, getSearchCompletionQueryName } from '@/lib/utils'
import useSWR from 'swr'
import fetcher from '@/lib/tabby-fetcher'
import { debounce, has } from 'lodash'
import { ISearchHit, SearchReponse } from '@/lib/types'
export interface PromptProps
extends Pick<UseChatHelpers, 'input' | 'setInput'> {
@ -26,15 +35,56 @@ export function PromptForm({
isLoading
}: PromptProps) {
const { formRef, onKeyDown } = useEnterSubmit()
const inputRef = React.useRef<HTMLTextAreaElement>(null)
const router = useRouter()
const [queryCompletionUrl, setQueryCompletionUrl] = React.useState<
string | null
>(null)
const latestFetchKey = React.useRef('')
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.focus()
useSWR<SearchReponse>(queryCompletionUrl, fetcher, {
revalidateOnFocus: false,
dedupingInterval: 500,
onSuccess: (data, key) => {
if (key !== latestFetchKey.current) return
setOptions(data?.hits ?? [])
}
})
const [options, setOptions] = React.useState<SearchReponse['hits']>([])
const onSearch = React.useMemo(() => {
return debounce((e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value = e.target?.value ?? ''
const end = e.target?.selectionEnd ?? 0
const queryname = getSearchCompletionQueryName(value, end)
if (queryname) {
let url = `/v1beta/search?q=name:${queryname}`
latestFetchKey.current = url
setQueryCompletionUrl(`/v1beta/search?q=name:${queryname}`)
} else {
setOptions([])
}
}, 200)
}, [])
const onSelectCompletion = (
inputRef: React.MutableRefObject<
HTMLTextAreaElement | HTMLInputElement | null
>,
item: ISearchHit
) => {
const replaceString = '`@' + item?.doc?.name + '` '
const selectionEnd = inputRef.current?.selectionEnd ?? 0
const queryname = getSearchCompletionQueryName(input, selectionEnd)
const prevInput = input
.substring(0, selectionEnd)
.replace(new RegExp(`@${queryname}$`), '')
if (queryname) {
setInput(prevInput + replaceString + input.substring(selectionEnd))
}
setOptions([])
}
return (
<form
onSubmit={async e => {
@ -47,42 +97,108 @@ export function PromptForm({
}}
ref={formRef}
>
<div className="relative flex max-h-60 w-full grow flex-col overflow-hidden bg-background px-8 sm:rounded-md sm:border sm:px-12">
<span
className={cn(
buttonVariants({ size: 'sm', variant: 'ghost' }),
'absolute left-0 top-4 h-8 w-8 rounded-full bg-background p-0 hover:bg-background sm:left-4'
)}
>
<IconEdit />
</span>
<Textarea
ref={inputRef}
tabIndex={0}
onKeyDown={onKeyDown}
rows={1}
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Ask a question."
spellCheck={false}
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
/>
<div className="absolute right-0 top-4 sm:right-4">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
size="icon"
disabled={isLoading || input === ''}
<Combobox options={options} onSelect={onSelectCompletion}>
{({ open, inputRef, highlightedIndex }) => {
return (
<>
<ComboboxAnchor>
<div className="bg-background relative flex max-h-60 w-full grow flex-col overflow-hidden px-8 sm:rounded-md sm:border sm:px-12">
<span
className={cn(
buttonVariants({ size: 'sm', variant: 'ghost' }),
'bg-background hover:bg-background absolute left-0 top-4 h-8 w-8 rounded-full p-0 sm:left-4'
)}
>
<IconEdit />
</span>
<ComboboxTextarea
tabIndex={0}
rows={1}
placeholder="Ask a question."
spellCheck={false}
className="min-h-[60px] w-full resize-none bg-transparent px-4 py-[1.3rem] focus-within:outline-none sm:text-sm"
value={input}
ref={
inputRef as React.MutableRefObject<HTMLTextAreaElement>
}
onChange={e => {
if (has(e, 'target.value')) {
let event = e as React.ChangeEvent<HTMLTextAreaElement>
setInput(event.target.value)
onSearch?.(event)
}
}}
onKeyDown={onKeyDown}
/>
<div className="absolute right-0 top-4 sm:right-4">
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
size="icon"
disabled={isLoading || input === ''}
>
<IconArrowElbow />
<span className="sr-only">Send message</span>
</Button>
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
</div>
</div>
</ComboboxAnchor>
<ComboboxContent
align="start"
onOpenAutoFocus={e => e.preventDefault()}
// popupMatchAnchorWidth
className="w-1/2 max-w-xl"
>
<IconArrowElbow />
<span className="sr-only">Send message</span>
</Button>
</TooltipTrigger>
<TooltipContent>Send message</TooltipContent>
</Tooltip>
</div>
</div>
<Popover open={!!options?.[highlightedIndex]}>
<PopoverAnchor>
<div className="max-h-[300px] overflow-y-scroll">
{open &&
!!options?.length &&
options.map((item, index) => (
<ComboboxOption
item={item}
index={index}
key={item?.id}
>
<div className="flex flex-col overflow-x-hidden">
<div className="truncate">{item?.doc?.name}</div>
<div className="text-muted-foreground truncate text-sm">
{item?.doc?.body}
</div>
</div>
</ComboboxOption>
))}
</div>
</PopoverAnchor>
<PopoverContent
asChild
align="end"
side="right"
alignOffset={-4}
onOpenAutoFocus={e => e.preventDefault()}
onFocus={e => e.preventDefault()}
onClick={e => e.preventDefault()}
className="max-w-xl rounded-none"
>
<div className="flex flex-col px-2">
<div className="mb-2">
{options?.[highlightedIndex]?.doc?.name}
</div>
<div className="text-muted-foreground flex-1 overflow-auto whitespace-pre">
{options?.[highlightedIndex]?.doc?.body}
</div>
</div>
</PopoverContent>
</Popover>
</ComboboxContent>
</>
)
}}
</Combobox>
</form>
)
}

View File

@ -0,0 +1,216 @@
import * as React from 'react'
import {
Popover,
PopoverAnchor,
PopoverPortal,
PopoverContent,
PopoverClose
} from '@/components/ui/popover'
import { cn } from '@/lib/utils'
import { useCombobox } from 'downshift'
import type { UseComboboxReturnValue } from 'downshift'
import Textarea from 'react-textarea-autosize'
import { isNil, omitBy } from 'lodash'
interface ComboboxContextValue<T = any> extends UseComboboxReturnValue<T> {
open: boolean
inputRef: React.MutableRefObject<
HTMLInputElement | HTMLTextAreaElement | null
>
anchorRef: React.MutableRefObject<HTMLElement | null>
}
export const ComboboxContext = React.createContext({} as ComboboxContextValue)
export const ComboboxClose = PopoverClose
export const ComboboxAnchor = React.forwardRef<
React.ElementRef<typeof PopoverAnchor>,
React.ComponentPropsWithoutRef<typeof PopoverAnchor>
>((props, forwardRef) => {
return <PopoverAnchor {...props} ref={forwardRef} />
})
ComboboxAnchor.displayName = 'ComboboxAnchor'
export const ComboboxTextarea = React.forwardRef<
React.ElementRef<typeof Textarea>,
React.ComponentPropsWithoutRef<typeof Textarea>
>((props, forwardRef) => {
const { getInputProps, open } = React.useContext(ComboboxContext)
const { onKeyDown, onChange, onInput, onBlur, onClick, ...rest } = props
return (
<Textarea
{...getInputProps(
omitBy(
{
onKeyDown: (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && open) {
e.preventDefault()
} else if (!open) {
onKeyDown?.(e)
}
},
onChange,
onInput,
onBlur,
onClick,
ref: forwardRef
},
isNil
)
)}
{...rest}
/>
)
})
ComboboxTextarea.displayName = 'ComboboxTextarea'
export const ComboboxContent = React.forwardRef<
React.ElementRef<typeof PopoverContent>,
React.ComponentPropsWithoutRef<typeof PopoverContent> & {
popupMatchAnchorWidth?: boolean
}
>(({ children, style, popupMatchAnchorWidth, ...rest }, forwardRef) => {
const { getMenuProps, anchorRef } = React.useContext(ComboboxContext)
const popupWidth = React.useRef<number | undefined>(undefined)
React.useLayoutEffect(() => {
if (popupMatchAnchorWidth) {
const anchor = anchorRef.current
if (anchor) {
const rect = anchor.getBoundingClientRect()
popupWidth.current = rect.width
}
}
}, [])
return (
<PopoverPortal>
<PopoverContent
align="start"
onOpenAutoFocus={e => e.preventDefault()}
style={{
width: popupWidth.current,
...style
}}
{...getMenuProps({ ref: forwardRef }, { suppressRefError: true })}
{...rest}
>
{children}
</PopoverContent>
</PopoverPortal>
)
})
ComboboxContent.displayName = 'ComboboxContent'
interface ComboboxOptionProps<T = any> {
item: T
index: number
className?: string
disabled?: boolean
children?:
| React.ReactNode
| React.ReactNode[]
| ((p: { selected: boolean; highlighted: boolean }) => React.ReactNode)
}
export const ComboboxOption = React.forwardRef<
React.RefObject<HTMLDivElement>,
ComboboxOptionProps
>(({ item, index, className, children, disabled, ...rest }, forwardRef) => {
const { highlightedIndex, selectedItem, getItemProps } =
React.useContext(ComboboxContext)
const highlighted = highlightedIndex === index
const selected = selectedItem === item
return (
<ComboboxClose key={item.id} asChild>
<div
className={cn(
'relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none',
highlighted && 'text-accent-foreground bg-accent',
selected && 'font-bold',
disabled && 'pointer-events-none opacity-50',
className
)}
{...getItemProps({ item, index })}
{...rest}
>
{typeof children === 'function'
? children({ highlighted, selected })
: children}
</div>
</ComboboxClose>
)
})
ComboboxOption.displayName = 'ComboboxOption'
interface ComboboxProps<T> {
options: T[] | undefined
onSelect?: (
ref: React.MutableRefObject<HTMLTextAreaElement | HTMLInputElement | null>,
data: T
) => void
children?:
| React.ReactNode
| React.ReactNode[]
| ((p: ComboboxContextValue) => React.ReactNode)
}
export function Combobox<T extends { id: number }>({
options,
onSelect,
children
}: ComboboxProps<T>) {
const [manualOpen, setManualOpen] = React.useState(false)
const inputRef = React.useRef<HTMLTextAreaElement | HTMLInputElement | null>(
null
)
const anchorRef = React.useRef<HTMLElement | null>(null)
const comboboxValue = useCombobox({
items: options ?? [],
isOpen: manualOpen,
onSelectedItemChange({ selectedItem }) {
if (selectedItem) {
onSelect?.(inputRef, selectedItem)
setManualOpen(false)
}
},
onIsOpenChange: ({ isOpen }) => {
if (!isOpen) {
setManualOpen(false)
}
}
})
const { setHighlightedIndex, highlightedIndex, isOpen } = comboboxValue
const open = isOpen && !!options?.length
React.useEffect(() => {
if (open && !!options.length && highlightedIndex === -1) {
setHighlightedIndex(0)
}
if (open && !options.length) {
setManualOpen(false)
}
}, [open])
React.useEffect(() => {
if (options?.length) {
setManualOpen(true)
} else {
setManualOpen(false)
}
}, [options])
const contextValue = React.useMemo(() => {
return { ...comboboxValue, open, inputRef, anchorRef }
}, [comboboxValue, open, inputRef, anchorRef])
return (
<ComboboxContext.Provider value={contextValue}>
<Popover open={open}>
{typeof children === 'function' ? children(contextValue) : children}
</Popover>
</ComboboxContext.Provider>
)
}

View File

@ -0,0 +1,41 @@
'use client'
import * as React from 'react'
import * as PopoverPrimitive from '@radix-ui/react-popover'
import { cn } from '@/lib/utils'
const Popover = PopoverPrimitive.Root
const PopoverTrigger = PopoverPrimitive.Trigger
const PopoverClose = PopoverPrimitive.Close
const PopoverPortal = PopoverPrimitive.Portal
const PopoverAnchor = PopoverPrimitive.Anchor
const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<PopoverPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
'z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow animate-in 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',
className
)}
{...props}
/>
))
PopoverContent.displayName = PopoverPrimitive.Content.displayName
export {
Popover,
PopoverTrigger,
PopoverContent,
PopoverClose,
PopoverPortal,
PopoverAnchor
}

View File

@ -0,0 +1,2 @@
export * from './common'
export * from './search'

View File

@ -0,0 +1,15 @@
export type ISearchHit = {
id: number
doc?: {
body?: string
name?: string
filepath?: string
git_url?: string
kind?: string
language?: string
}
}
export type SearchReponse = {
hits?: Array<ISearchHit>
num_hits?: number
}

View File

@ -41,3 +41,18 @@ export function formatDate(input: string | number | Date): string {
year: 'numeric'
})
}
/**
* Retrieves the name of the completion query from a given string@.
* @param {string} val - The input string to search for the completion query name.
* @param {number | undefined} selectionEnd - The index at which the selection ends in the input string.
* @return {string | undefined} - The name of the completion query if found, otherwise undefined.
*/
export function getSearchCompletionQueryName(
val: string,
selectionEnd: number | undefined
): string | undefined {
const queryString = val.substring(0, selectionEnd)
const matches = /@(\w+)$/.exec(queryString)
return matches?.[1]
}

View File

@ -18,6 +18,7 @@
"@radix-ui/react-dialog": "1.0.4",
"@radix-ui/react-dropdown-menu": "^2.0.5",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-select": "^1.2.2",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
@ -30,7 +31,9 @@
"class-variance-authority": "^0.4.0",
"clsx": "^1.2.1",
"compare-versions": "^6.1.0",
"downshift": "^8.2.2",
"focus-trap-react": "^10.1.1",
"lodash": "^4.17.21",
"nanoid": "^4.0.2",
"next": "^13.4.7",
"next-auth": "0.0.0-manual.83c4ebd1",
@ -49,6 +52,7 @@
},
"devDependencies": {
"@tailwindcss/typography": "^0.5.9",
"@types/lodash": "^4.14.200",
"@types/node": "^17.0.12",
"@types/react": "^18.0.22",
"@types/react-dom": "^18.0.7",

View File

@ -31,6 +31,13 @@
dependencies:
regenerator-runtime "^0.14.0"
"@babel/runtime@^7.22.15":
version "7.23.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.23.2.tgz#062b0ac103261d68a966c4c7baf2ae3e62ec3885"
integrity sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==
dependencies:
regenerator-runtime "^0.14.0"
"@eslint-community/eslint-utils@^4.2.0":
version "4.4.0"
resolved "https://registry.yarnpkg.com/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz#a23514e8fb9af1269d5f7788aa556798d61c6b59"
@ -418,6 +425,28 @@
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popover@^1.0.7":
version "1.0.7"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popover/-/react-popover-1.0.7.tgz#23eb7e3327330cb75ec7b4092d685398c1654e3c"
integrity sha512-shtvVnlsxT6faMnK/a7n0wptwBD23xc1Z5mdrtKLwVEfsEMXodS0r5s0/g5P0hX//EKYZS2sxUjqfzlg52ZSnQ==
dependencies:
"@babel/runtime" "^7.13.10"
"@radix-ui/primitive" "1.0.1"
"@radix-ui/react-compose-refs" "1.0.1"
"@radix-ui/react-context" "1.0.1"
"@radix-ui/react-dismissable-layer" "1.0.5"
"@radix-ui/react-focus-guards" "1.0.1"
"@radix-ui/react-focus-scope" "1.0.4"
"@radix-ui/react-id" "1.0.1"
"@radix-ui/react-popper" "1.1.3"
"@radix-ui/react-portal" "1.0.4"
"@radix-ui/react-presence" "1.0.1"
"@radix-ui/react-primitive" "1.0.3"
"@radix-ui/react-slot" "1.0.2"
"@radix-ui/react-use-controllable-state" "1.0.1"
aria-hidden "^1.1.1"
react-remove-scroll "2.5.5"
"@radix-ui/react-popper@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-popper/-/react-popper-1.1.2.tgz#4c0b96fcd188dc1f334e02dba2d538973ad842e9"
@ -705,6 +734,11 @@
resolved "https://registry.yarnpkg.com/@types/katex/-/katex-0.16.3.tgz#a341c89705145b7dd8e2a133b282a133eabe6076"
integrity sha512-CeVMX9EhVUW8MWnei05eIRks4D5Wscw/W9Byz1s3PA+yJvcdvq9SaDjiUKvRvEgjpdTyJMjQA43ae4KTwsvOPg==
"@types/lodash@^4.14.200":
version "4.14.200"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.200.tgz#435b6035c7eba9cdf1e039af8212c9e9281e7149"
integrity sha512-YI/M/4HRImtNf3pJgbF+W6FrXovqj+T+/HpENLTooK9PnkacBsDpeP3IpHab40CClUfhNmdM2WTNP2sa2dni5Q==
"@types/mdast@^3.0.0":
version "3.0.13"
resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.13.tgz#b7ba6e52d0faeb9c493e32c205f3831022be4e1b"
@ -1269,6 +1303,11 @@ compare-versions@^6.1.0:
resolved "https://registry.yarnpkg.com/compare-versions/-/compare-versions-6.1.0.tgz#3f2131e3ae93577df111dba133e6db876ffe127a"
integrity sha512-LNZQXhqUvqUTotpZ00qLSaify3b4VFD588aRr8MKFw4CMUr98ytzCW5wDH5qx/DEY5kCDXcbcRuCqL0szEf2tg==
compute-scroll-into-view@^3.0.3:
version "3.1.0"
resolved "https://registry.yarnpkg.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz#753f11d972596558d8fe7c6bcbc8497690ab4c87"
integrity sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==
concat-map@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b"
@ -1435,6 +1474,17 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
downshift@^8.2.2:
version "8.2.2"
resolved "https://registry.yarnpkg.com/downshift/-/downshift-8.2.2.tgz#bc4bf0024ad9b70bf2c493b58cca9c652fb1d555"
integrity sha512-UmJHlNTzmFN3i427Hh9f1OXMnkhgSB/J+urC9ywabvwuftm0nB0/Utsb89OtDq+2UqyScQV4Ro7EM2PEV80N5w==
dependencies:
"@babel/runtime" "^7.22.15"
compute-scroll-into-view "^3.0.3"
prop-types "^15.8.1"
react-is "^18.2.0"
tslib "^2.6.2"
electron-to-chromium@^1.4.535:
version "1.4.537"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.537.tgz#aac4101db53066be1e49baedd000a26bc754adc9"
@ -2601,6 +2651,11 @@ lodash.merge@^4.6.2:
resolved "https://registry.yarnpkg.com/lodash.merge/-/lodash.merge-4.6.2.tgz#558aa53b43b661e1925a0afdfa36a9a1085fe57a"
integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
lodash@^4.17.21:
version "4.17.21"
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
longest-streak@^3.0.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4"
@ -3586,7 +3641,7 @@ react-is@^16.13.1:
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react-is@^18.0.0:
react-is@^18.0.0, react-is@^18.2.0:
version "18.2.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b"
integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==
@ -4179,7 +4234,7 @@ tslib@^1.8.1:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0:
tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0, tslib@^2.6.2:
version "2.6.2"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.2.tgz#703ac29425e7b37cd6fd456e92404d46d1f3e4ae"
integrity sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==