refactor: auto completion component

Signed-off-by: Innei <i@innei.in>
This commit is contained in:
Innei 2024-09-20 20:41:28 +08:00
parent e93831e650
commit 75cfd554ca
No known key found for this signature in database
GPG Key ID: 0F62D33977F021F7
6 changed files with 251 additions and 292 deletions

View File

@ -15,6 +15,7 @@
"@follow/electron-main": "workspace:*",
"@follow/shared": "workspace:*",
"@fontsource/sn-pro": "5.1.0",
"@headlessui/react": "2.1.8",
"@hono/auth-js": "1.0.10",
"@hookform/resolvers": "3.9.0",
"@iconify/tools": "4.0.6",

View File

@ -1,24 +1,11 @@
import clsx from "clsx"
import { AnimatePresence } from "framer-motion"
import { Combobox, ComboboxInput, ComboboxOption, ComboboxOptions } from "@headlessui/react"
import { AnimatePresence, m } from "framer-motion"
import Fuse from "fuse.js"
import { merge, throttle } from "lodash-es"
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react"
import { useEventCallback } from "usehooks-ts"
import { forwardRef, Fragment, useCallback, useEffect, useState } from "react"
import { useRefValue } from "~/hooks/common"
import { nextFrame, stopPropagation } from "~/lib/dom"
import { cn } from "~/lib/utils"
import { Input } from "../input"
import { RootPortal } from "../portal"
export type Suggestion = {
name: string
@ -28,277 +15,110 @@ export interface AutocompleteProps extends React.InputHTMLAttributes<HTMLInputEl
suggestions: Suggestion[]
renderSuggestion?: (suggestion: Suggestion) => any
onSuggestionSelected: (suggestion: Suggestion) => void
onConfirm?: (value: string) => void
onEndReached?: () => void
onSuggestionSelected: (suggestion: NoInfer<Suggestion> | null) => void
portal?: boolean
// classnames
wrapperClassName?: string
maxHeight?: number
}
const defaultRenderSuggestion = (suggestion: any) => suggestion.name
const defaultRenderSuggestion = (suggestion: any) => suggestion?.name
export const Autocomplete = forwardRef<HTMLInputElement, AutocompleteProps>(
(
{
suggestions,
renderSuggestion = defaultRenderSuggestion,
onSuggestionSelected,
onConfirm,
onEndReached,
onChange,
portal,
wrapperClassName,
maxHeight,
portal,
value,
defaultValue,
...inputProps
},
forwardedRef,
) => {
const [filterableSuggestions, setFilterableSuggestions] = useState(suggestions)
const [inputValue, setInputValue] = useState(inputProps.value || inputProps.defaultValue || "")
const [selectedOptions, setSelectedOptions] = useState<NoInfer<Suggestion> | null>(
() => suggestions.find((suggestion) => suggestion.value === value) || null,
)
const doFilter = useEventCallback(() => {
const [filterableSuggestions, setFilterableSuggestions] = useState(suggestions)
const doFilter = useCallback(() => {
const fuse = new Fuse(suggestions, {
keys: ["name", "value"],
})
const trimInputValue = (inputValue as string).trim()
const trimInputValue = (value as string)?.trim()
if (!trimInputValue) return setFilterableSuggestions(suggestions)
const results = fuse.search(trimInputValue)
setFilterableSuggestions(results.map((result) => result.item))
})
}, [suggestions, value])
useEffect(() => {
doFilter()
}, [inputValue, suggestions])
useEffect(() => {
const $input = inputRef.current
if (!$input) return
if (document.activeElement !== $input) {
return
}
setIsOpen(filterableSuggestions.length > 0)
}, [filterableSuggestions])
const [isOpen, setIsOpen] = useState(false)
const [listRef, setListRef] = useState<HTMLElement | null>(null)
const onBlur = useEventCallback((e: any) => {
inputProps.onBlur?.(e)
if (listRef?.contains(e.relatedTarget)) {
return
}
setIsOpen(false)
})
const handleInputKeyDown: React.KeyboardEventHandler<HTMLInputElement> = useEventCallback(
(e) => {
if (e.key === "Enter") {
e.preventDefault()
onConfirm?.((e.target as HTMLInputElement).value)
setIsOpen(false)
}
inputProps.onKeyDown?.(e)
},
)
const inputRef = useRef<HTMLInputElement>(null)
useImperativeHandle(forwardedRef, () => inputRef.current!)
const [currentActiveIndex, setCurrentActiveIndex] = useState(0)
const currentActiveIndexRef = useRefValue(currentActiveIndex)
const filterableSuggestionsRef = useRefValue(filterableSuggestions)
// Bind hotkey
useEffect(() => {
const $input = inputRef.current
if (!$input) return
const handleKeyDown = (e: KeyboardEvent) => {
const currentActiveIndex = currentActiveIndexRef.current
const filterableSuggestionsLength = filterableSuggestionsRef.current.length
const filterableSuggestions = filterableSuggestionsRef.current
if (e.key === "ArrowDown" || e.key === "ArrowUp") {
e.preventDefault()
const nextIndex =
currentActiveIndex + (e.key === "ArrowDown" ? 1 : e.key === "ArrowUp" ? -1 : 0)
setCurrentActiveIndex(Math.max(0, Math.min(nextIndex, filterableSuggestionsLength - 1)))
} else if (
(e.key === "Enter" || e.key === "Tab") &&
currentActiveIndex >= 0 &&
currentActiveIndex < filterableSuggestionsLength
) {
e.stopPropagation()
e.preventDefault()
onSuggestionSelected(filterableSuggestions[currentActiveIndex])
setInputValue(filterableSuggestions[currentActiveIndex].name)
nextFrame(() => {
setIsOpen(false)
})
}
}
$input.addEventListener("keydown", handleKeyDown)
return () => {
$input.removeEventListener("keydown", handleKeyDown)
}
}, [currentActiveIndexRef, filterableSuggestionsRef, onSuggestionSelected])
const [inputRect, setInputRect] = useState<DOMRect | null>(null)
const [dropdownPos, setDropdownPos] = useState("down")
const [dropdownHeight, setDropdownHeight] = useState(maxHeight || 200)
useLayoutEffect(() => {
const $input = inputRef.current
if (!$input) return
const handler = () => {
const rect = $input.getBoundingClientRect()
setInputRect(rect)
const { top, height } = rect
if (top + height + dropdownHeight > window.innerHeight) {
setDropdownPos("up")
} else {
setDropdownPos("down")
}
}
handler()
const resizeObserver = new ResizeObserver(handler)
resizeObserver.observe($input)
return () => {
resizeObserver.disconnect()
}
}, [dropdownHeight])
const handleScroll = useEventCallback(
throttle(() => {
if (!listRef) return
const { scrollHeight, scrollTop, clientHeight } = listRef!
// gap 50px
if (scrollHeight - scrollTop - clientHeight < 50) {
onEndReached?.()
}
}, 30),
)
const handleChange = useEventCallback((e: any) => {
setInputValue(e.target.value)
onChange?.(e)
})
const ListElement = (
<div
className={clsx(
"pointer-events-auto z-[999] mt-1 overflow-hidden",
portal ? "absolute flex flex-col" : "absolute w-full",
)}
ref={useCallback((el) => {
setListRef(el)
const height = el?.getBoundingClientRect().height
if (height) {
setDropdownHeight(height)
}
}, [])}
style={merge(
{},
portal && inputRect
? {
width: `${inputRect.width}px`,
left: `${inputRect.x}px`,
top: `${inputRect.y + inputRect.height}px`,
bottom: dropdownPos === "up" ? 0 : undefined,
transform: dropdownPos === "up" ? `translateY(-${dropdownHeight}px)` : undefined,
maxHeight,
}
: {},
)}
>
<ul
data-state={isOpen ? "open" : "closed"}
className={clsx(
"pointer-events-auto max-h-48 grow",
"overflow-auto rounded-md border border-border bg-popover text-popover-foreground",
"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",
)}
// FIXME: https://github.com/radix-ui/primitives/issues/2125
onWheel={stopPropagation}
onScroll={handleScroll}
>
{filterableSuggestions.map((suggestion, index) => {
const handleClick = () => {
onSuggestionSelected(suggestion)
setIsOpen(false)
setInputValue(suggestion.name)
}
return (
<li
className={cn(
"cursor-default px-4 py-1.5 text-sm",
currentActiveIndex === index && "bg-theme-item-hover dark:bg-neutral-800",
)}
key={suggestion.value}
onMouseDown={handleClick}
onMouseEnter={() => setCurrentActiveIndex(index)}
>
{renderSuggestion(suggestion)}
</li>
)
})}
</ul>
</div>
)
const handleFocus: React.FocusEventHandler<HTMLInputElement> = useEventCallback((e) => {
setIsOpen(true)
inputProps.onFocus?.(e)
})
}, [doFilter])
return (
<div className={cn("pointer-events-auto relative", wrapperClassName)}>
<div className="relative">
<Input
value={inputValue}
ref={inputRef}
className="pr-8"
{...inputProps}
onBlur={onBlur}
onKeyDown={handleInputKeyDown}
onChange={handleChange}
onFocus={handleFocus}
/>
{!!inputValue && (
<button
onClick={() => {
setInputValue("")
onChange?.({ target: { value: "" } } as any)
}}
type="button"
className="center absolute inset-y-0 right-0 flex px-2 opacity-80 duration-200 hover:opacity-100"
>
<i className="i-mingcute-close-circle-fill" />
</button>
)}
</div>
<AnimatePresence>
{isOpen &&
filterableSuggestions.length > 0 &&
(portal ? <RootPortal>{ListElement}</RootPortal> : ListElement)}
</AnimatePresence>
</div>
<Combobox
immediate
value={selectedOptions}
onChange={(suggestion) => {
setSelectedOptions(suggestion)
onSuggestionSelected(suggestion)
}}
>
{({ open }) => {
return (
<Fragment>
<ComboboxInput
ref={forwardedRef}
as={Input}
aria-label="Select Category"
displayValue={renderSuggestion}
value={value}
{...inputProps}
/>
<AnimatePresence>
{open && (
<ComboboxOptions
portal={portal}
static
as={m.div}
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.98 }}
anchor="bottom"
className={cn(
"pointer-events-auto z-[99] max-h-48 grow",
"shadow-perfect overflow-auto rounded-md border border-border bg-popover text-popover-foreground",
"w-[var(--input-width)] empty:invisible",
)}
>
<div style={{ maxHeight }}>
{filterableSuggestions.map((suggestion) => (
<ComboboxOption
key={suggestion.value}
value={suggestion}
className={cn(
"data-[focus]:bg-theme-item-hover dark:data-[focus]:bg-neutral-800",
"px-4 py-1.5 text-sm",
)}
>
{suggestion.name}
</ComboboxOption>
))}
</div>
</ComboboxOptions>
)}
</AnimatePresence>
</Fragment>
)
}}
</Combobox>
)
},
)

View File

@ -49,9 +49,10 @@ export const useInputComposition = <E = HTMLInputElement>(
onKeyDown?.(e)
if (e.key === "Escape") {
e.currentTarget.blur()
e.preventDefault()
e.stopPropagation()
e.currentTarget.blur()
}
},
[onKeyDown],

View File

@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod"
import { useMutation } from "@tanstack/react-query"
import { useEffect, useRef } from "react"
import { useEffect, useMemo, useRef } from "react"
import { useForm } from "react-hook-form"
import { useTranslation } from "react-i18next"
import { toast } from "sonner"
@ -25,8 +25,7 @@ import { LoadingCircle } from "~/components/ui/loading"
import { useCurrentModal } from "~/components/ui/modal"
import { Switch } from "~/components/ui/switch"
import { views } from "~/constants"
import { useDeleteSubscription } from "~/hooks/biz/useSubscriptionActions"
import { useAuthQuery } from "~/hooks/common"
import { useAuthQuery, useI18n } from "~/hooks/common"
import { apiClient } from "~/lib/api-fetch"
import { tipcClient } from "~/lib/client"
import { FeedViewType } from "~/lib/enum"
@ -58,6 +57,7 @@ export const FeedForm: Component<{
onSuccess?: () => void
}> = ({ id: _id, defaultValues = defaultValue, url, asWidget, onSuccess }) => {
const queryParams = { id: _id, url }
const feedQuery = useFeed(queryParams)
const id = feedQuery.data?.feed.id || _id
@ -171,7 +171,7 @@ const FeedInnerForm = ({
defaultValues,
})
const { setClickOutSideToDismiss } = useCurrentModal()
const { setClickOutSideToDismiss, dismiss } = useCurrentModal()
useEffect(() => {
setClickOutSideToDismiss(!form.formState.isDirty)
@ -233,27 +233,22 @@ const FeedInnerForm = ({
},
})
const deleteSubscription = useDeleteSubscription({
onSuccess: () => {
if (!asWidget && !isSubscribed) {
window.close()
}
onSuccess?.()
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
followMutation.mutate(values)
}
const { t } = useTranslation()
const t = useI18n()
const categories = useAuthQuery(subscriptionQuery.categories())
// useEffect(() => {
// if (feed.isSuccess) nextFrame(() => buttonRef.current?.focus());
// }, [feed.isSuccess]);
const suggestions = useMemo(
() =>
categories.data?.map((i) => ({
name: i,
value: i,
})) || [],
[categories.data],
)
return (
<div className="flex flex-1 flex-col gap-y-4">
@ -333,15 +328,12 @@ const FeedInnerForm = ({
<Autocomplete
maxHeight={window.innerHeight < 600 ? 120 : 240}
portal
suggestions={
categories.data?.map((i) => ({
name: i,
value: i,
})) || []
}
suggestions={suggestions}
{...(field as any)}
onSuggestionSelected={(suggestion) => {
field.onChange(suggestion.value)
if (suggestion) {
field.onChange(suggestion.value)
}
}}
/>
</div>
@ -378,16 +370,11 @@ const FeedInnerForm = ({
type="button"
ref={buttonRef}
variant="text"
isLoading={deleteSubscription.isPending}
className="text-red-500"
onClick={(e) => {
e.preventDefault()
if (subscription) {
deleteSubscription.mutate(subscription)
}
onClick={() => {
dismiss()
}}
>
{t("feed_form.unfollow")}
{t.common("cancel")}
</Button>
)}
<Button ref={buttonRef} type="submit" isLoading={followMutation.isPending}>

View File

@ -3,11 +3,11 @@
"1001": "创建会话失败",
"1002": "参数无效",
"1003": "邀请码无效",
"2000": "只有管理员可以刷新动态",
"2000": "只有管理员可以刷新 Feed",
"2001": "未找到动态",
"2002": "需要 feedId 或 url",
"2003": "动态获取错误",
"2004": "动态解析失败",
"2003": "Feed 获取错误",
"2004": "Feed 解析失败",
"2010": "所有权验证失败",
"2011": "订阅限制已超出",
"3000": "未找到条目",
@ -15,7 +15,7 @@
"4000": "已领取",
"4001": "用户钱包错误",
"4002": "余额不足",
"4003": "动态可提余额不足",
"4003": "可提余额不足",
"4004": "目标用户钱包错误",
"5000": "邀请限制已超出。请几天后再试。",
"5001": "邀请已存在。",

View File

@ -381,6 +381,9 @@ importers:
'@fontsource/sn-pro':
specifier: 5.1.0
version: 5.1.0
'@headlessui/react':
specifier: 2.1.8
version: 2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@hono/auth-js':
specifier: 1.0.10
version: 1.0.10(@auth/core@0.34.2)(hono@4.6.2(patch_hash=iej2g43ojhll5opwbnvyorjq4e))(react@18.3.1)
@ -2428,15 +2431,37 @@ packages:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/react-dom@2.1.2':
resolution: {integrity: sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/react@0.26.24':
resolution: {integrity: sha512-2ly0pCkZIGEQUq5H8bBK0XJmc1xIK/RM3tvVzY3GBER7IOD1UgmC2Y2tjj4AuS+TC+vTE1KJv2053290jua0Sw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@floating-ui/utils@0.2.7':
resolution: {integrity: sha512-X8R8Oj771YRl/w+c1HqAC1szL8zWQRwFvgDwT129k9ACdBoud/+/rX9V0qiMl6LWUdP9voC2nDVZYPMQQsb6eA==}
'@floating-ui/utils@0.2.8':
resolution: {integrity: sha512-kym7SodPp8/wloecOpcmSnWJsK7M0E5Wg8UcFA+uO4B9s5d0ywXOEro/8HM9x0rW+TljRzul/14UYz3TleT3ig==}
'@fontsource/sn-pro@5.1.0':
resolution: {integrity: sha512-k7cdU1hftD/pyrnrmQg+egKAssbuPORbtgQM/9JG5b76EchEKZdl/juO8xBjN3O/H5BQ16iu8/9vNPgzyExZeg==}
'@gar/promisify@1.1.3':
resolution: {integrity: sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==}
'@headlessui/react@2.1.8':
resolution: {integrity: sha512-uajqVkAcVG/wHwG9Fh5PFMcFpf2VxM4vNRNKxRjuK009kePVur8LkuuygHfIE+2uZ7z7GnlTtYsyUe6glPpTLg==}
engines: {node: '>=10'}
peerDependencies:
react: ^18
react-dom: ^18
'@hono/auth-js@1.0.10':
resolution: {integrity: sha512-hnkZzPMHveX2GzX/zS53uP6OqzB6SCtJCaimXdjewCWWiNi4FFtv1jcsca3PV7haiaLKict5JPZWzfMq+t3Bzw==}
engines: {node: '>=18.4.0'}
@ -3501,6 +3526,37 @@ packages:
'@radix-ui/rect@1.1.0':
resolution: {integrity: sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==}
'@react-aria/focus@3.18.2':
resolution: {integrity: sha512-Jc/IY+StjA3uqN73o6txKQ527RFU7gnG5crEl5Xy3V+gbYp2O5L3ezAo/E0Ipi2cyMbG6T5Iit1IDs7hcGu8aw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/interactions@3.22.2':
resolution: {integrity: sha512-xE/77fRVSlqHp2sfkrMeNLrqf2amF/RyuAS6T5oDJemRSgYM3UoxTbWjucPhfnoW7r32pFPHHgz4lbdX8xqD/g==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/ssr@3.9.5':
resolution: {integrity: sha512-xEwGKoysu+oXulibNUSkXf8itW0npHHTa6c4AyYeZIJyRoegeteYuFpZUBPtIDE8RfHdNsSmE1ssOkxRnwbkuQ==}
engines: {node: '>= 12'}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-aria/utils@3.25.2':
resolution: {integrity: sha512-GdIvG8GBJJZygB4L2QJP1Gabyn2mjFsha73I2wSe+o4DYeGWoJiMZRM06PyTIxLH4S7Sn7eVDtsSBfkc2VY/NA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-stately/utils@3.10.3':
resolution: {integrity: sha512-moClv7MlVSHpbYtQIkm0Cx+on8Pgt1XqtPx6fy9rQFb2DNc9u1G3AUVnqA17buOkH1vLxAtX4MedlxMWyRCYYA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@react-types/shared@3.24.1':
resolution: {integrity: sha512-AUQeGYEm/zDTN6zLzdXolDxz3Jk5dDL7f506F07U8tBwxNNI3WRdhU84G0/AaFikOZzDXhOZDr3MhQMzyE7Ydw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0 || ^19.0.0
'@remix-run/router@1.19.2':
resolution: {integrity: sha512-baiMx18+IMuD1yyvOGaHM9QrVUPGGG0jC+z+IPHnRJWUAUvaKuWKyE8gjDj2rzv3sz9zOGoRSPgeBVHRhZnBlA==}
engines: {node: '>=14.0.0'}
@ -3767,6 +3823,9 @@ packages:
peerDependencies:
eslint: '>=8.40.0'
'@swc/helpers@0.5.13':
resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==}
'@szmarczak/http-timer@4.0.6':
resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==}
engines: {node: '>=10'}
@ -3819,6 +3878,15 @@ packages:
peerDependencies:
react: ^18 || ^19
'@tanstack/react-virtual@3.10.8':
resolution: {integrity: sha512-VbzbVGSsZlQktyLrP5nxE+vE1ZR+U0NFAWPbJLoG2+DKPwd2D7dVICTVIIaYlJqX1ZCEnYDbaOpmMwbsyhBoIA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
'@tanstack/virtual-core@3.10.8':
resolution: {integrity: sha512-PBu00mtt95jbKFi6Llk9aik8bnR3tR/oQP1o3TSi+iG//+Q2RTIzCEgKkHG8BB86kxMNW6O8wku+Lmi+QFR6jA==}
'@tootallnate/once@2.0.0':
resolution: {integrity: sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==}
engines: {node: '>= 10'}
@ -8465,6 +8533,9 @@ packages:
systemjs@6.15.1:
resolution: {integrity: sha512-Nk8c4lXvMB98MtbmjX7JwJRgJOL8fluecYCfCeYBznwmpOs8Bf15hLM6z4z71EDAhQVrQrI+wt1aLWSXZq+hXA==}
tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
tailwind-merge@2.5.2:
resolution: {integrity: sha512-kjEBm+pvD+6eAwzJL2Bi+02/9LFLal1Gs61+QB7HvTfQQ0aXwC5LGT8PEt1gS0CWKktKe6ysPTAy3cBC5MeiIg==}
@ -10747,12 +10818,37 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/react-dom@2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/dom': 1.6.10
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@floating-ui/react@0.26.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react-dom': 2.1.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@floating-ui/utils': 0.2.8
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tabbable: 6.2.0
'@floating-ui/utils@0.2.7': {}
'@floating-ui/utils@0.2.8': {}
'@fontsource/sn-pro@5.1.0': {}
'@gar/promisify@1.1.3': {}
'@headlessui/react@2.1.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@floating-ui/react': 0.26.24(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-aria/focus': 3.18.2(react@18.3.1)
'@react-aria/interactions': 3.22.2(react@18.3.1)
'@tanstack/react-virtual': 3.10.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@hono/auth-js@1.0.10(@auth/core@0.34.2)(hono@4.6.2(patch_hash=iej2g43ojhll5opwbnvyorjq4e))(react@18.3.1)':
dependencies:
'@auth/core': 0.34.2
@ -11976,6 +12072,46 @@ snapshots:
'@radix-ui/rect@1.1.0': {}
'@react-aria/focus@3.18.2(react@18.3.1)':
dependencies:
'@react-aria/interactions': 3.22.2(react@18.3.1)
'@react-aria/utils': 3.25.2(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.13
clsx: 2.1.1
react: 18.3.1
'@react-aria/interactions@3.22.2(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-aria/utils': 3.25.2(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.13
react: 18.3.1
'@react-aria/ssr@3.9.5(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.13
react: 18.3.1
'@react-aria/utils@3.25.2(react@18.3.1)':
dependencies:
'@react-aria/ssr': 3.9.5(react@18.3.1)
'@react-stately/utils': 3.10.3(react@18.3.1)
'@react-types/shared': 3.24.1(react@18.3.1)
'@swc/helpers': 0.5.13
clsx: 2.1.1
react: 18.3.1
'@react-stately/utils@3.10.3(react@18.3.1)':
dependencies:
'@swc/helpers': 0.5.13
react: 18.3.1
'@react-types/shared@3.24.1(react@18.3.1)':
dependencies:
react: 18.3.1
'@remix-run/router@1.19.2': {}
'@rollup/rollup-android-arm-eabi@4.21.2':
@ -12309,6 +12445,10 @@ snapshots:
- supports-color
- typescript
'@swc/helpers@0.5.13':
dependencies:
tslib: 2.7.0
'@szmarczak/http-timer@4.0.6':
dependencies:
defer-to-connect: 2.0.1
@ -12361,6 +12501,14 @@ snapshots:
'@tanstack/query-core': 5.56.2
react: 18.3.1
'@tanstack/react-virtual@3.10.8(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@tanstack/virtual-core': 3.10.8
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
'@tanstack/virtual-core@3.10.8': {}
'@tootallnate/once@2.0.0': {}
'@trysound/sax@0.2.0': {}
@ -17953,6 +18101,8 @@ snapshots:
systemjs@6.15.1: {}
tabbable@6.2.0: {}
tailwind-merge@2.5.2: {}
tailwindcss-animate@1.0.7(tailwindcss@3.4.11):