feat: mobile filters panel

Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
Chloe 2024-05-07 14:40:18 +07:00
parent 145eb3eaed
commit 98d1f5c821
No known key found for this signature in database
GPG Key ID: CFD53CE570D42DF5
13 changed files with 264 additions and 255 deletions

View File

@ -1,4 +1,4 @@
import { getCollection, getCollectionProducts } from 'lib/shopify'; import { getCollection, getCollectionProducts, getMenu } from 'lib/shopify';
import { Metadata } from 'next'; import { Metadata } from 'next';
import { notFound } from 'next/navigation'; import { notFound } from 'next/navigation';
@ -6,7 +6,9 @@ import Breadcrumb from 'components/breadcrumb';
import BreadcrumbHome from 'components/breadcrumb/breadcrumb-home'; import BreadcrumbHome from 'components/breadcrumb/breadcrumb-home';
import Grid from 'components/grid'; import Grid from 'components/grid';
import ProductGridItems from 'components/layout/product-grid-items'; import ProductGridItems from 'components/layout/product-grid-items';
import Filters from 'components/layout/search/filters'; import FiltersList from 'components/layout/search/filters/filters-list';
import MobileFilters from 'components/layout/search/filters/mobile-filters';
import SubMenu from 'components/layout/search/filters/sub-menu';
import SortingMenu from 'components/layout/search/sorting-menu'; import SortingMenu from 'components/layout/search/sorting-menu';
import { import {
AVAILABILITY_FILTER_ID, AVAILABILITY_FILTER_ID,
@ -94,10 +96,14 @@ export default async function CategoryPage({
reverse, reverse,
...(filtersInput.length ? { filters: filtersInput } : {}) ...(filtersInput.length ? { filters: filtersInput } : {})
}); });
const collectionData = getCollection(params.collection); const collectionData = getCollection(params.collection);
const menuData = getMenu('main-menu');
const [{ products, filters }, collection] = await Promise.all([productsData, collectionData]); const [{ products, filters }, collection, menu] = await Promise.all([
productsData,
collectionData,
menuData
]);
return ( return (
<> <>
@ -107,12 +113,13 @@ export default async function CategoryPage({
</Suspense> </Suspense>
</div> </div>
{collection ? ( {collection ? (
<div className="mb-1 mt-3 max-w-5xl"> <div className="mb-3 mt-3 max-w-5xl lg:mb-1">
<h1 className="text-4xl font-bold tracking-tight text-gray-900">{collection.title}</h1> <h1 className="text-4xl font-bold tracking-tight text-gray-900">{collection.title}</h1>
<p className="mt-2 text-base text-gray-500">{collection.description}</p> <p className="mt-2 text-base text-gray-500">{collection.description}</p>
</div> </div>
) : null} ) : null}
<div className="flex w-full justify-end"> <div className="flex w-full flex-wrap items-center justify-between lg:justify-end">
<MobileFilters collection={params.collection} filters={filters} menu={menu} />
<SortingMenu /> <SortingMenu />
</div> </div>
<section> <section>
@ -121,7 +128,9 @@ export default async function CategoryPage({
) : ( ) : (
<Grid className="pt-5 lg:grid-cols-3 lg:gap-x-8 xl:grid-cols-4"> <Grid className="pt-5 lg:grid-cols-3 lg:gap-x-8 xl:grid-cols-4">
<aside className="hidden lg:block"> <aside className="hidden lg:block">
<Filters collection={params.collection} filters={filters} /> <SubMenu menu={menu} collection={params.collection} />
<h3 className="sr-only">Filters</h3>
<FiltersList filters={filters} />
</aside> </aside>
<div className="lg:col-span-2 xl:col-span-3"> <div className="lg:col-span-2 xl:col-span-3">
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"> <Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Popover, Transition } from '@headlessui/react'; import { Popover, PopoverGroup, PopoverPanel, Transition } from '@headlessui/react';
import clsx from 'clsx'; import clsx from 'clsx';
import { Menu } from 'lib/shopify/types'; import { Menu } from 'lib/shopify/types';
import Link from 'next/link'; import Link from 'next/link';
@ -13,7 +13,7 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => {
return menu.length ? ( return menu.length ? (
<div className="mt-2 hidden h-11 w-full border-b text-sm font-medium md:flex"> <div className="mt-2 hidden h-11 w-full border-b text-sm font-medium md:flex">
<Popover.Group as={Fragment}> <PopoverGroup as={Fragment}>
<div className="z-10 flex h-full w-full items-center justify-center gap-8 px-4 lg:gap-16"> <div className="z-10 flex h-full w-full items-center justify-center gap-8 px-4 lg:gap-16">
{menu.map((item: Menu) => { {menu.map((item: Menu) => {
const isActiveItem = const isActiveItem =
@ -62,7 +62,7 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => {
leaveTo="opacity-0" leaveTo="opacity-0"
show={isOpen} show={isOpen}
> >
<Popover.Panel <PopoverPanel
static static
className="absolute inset-x-0 left-1/2 top-full z-10 mt-1 min-w-32 max-w-sm -translate-x-1/2 transform text-sm" className="absolute inset-x-0 left-1/2 top-full z-10 mt-1 min-w-32 max-w-sm -translate-x-1/2 transform text-sm"
> >
@ -80,14 +80,14 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => {
))} ))}
</ul> </ul>
</div> </div>
</Popover.Panel> </PopoverPanel>
</Transition> </Transition>
</div> </div>
</Popover> </Popover>
); );
})} })}
</div> </div>
</Popover.Group> </PopoverGroup>
</div> </div>
) : null; ) : null;
}; };

View File

@ -1,64 +0,0 @@
'use client';
import { usePathname, useSearchParams } from 'next/navigation';
import { useEffect, useRef, useState } from 'react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import type { ListItem } from '.';
import { FilterItem } from './item';
export default function FilterItemDropdown({ list }: { list: ListItem[] }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const [active, setActive] = useState('');
const [openSelect, setOpenSelect] = useState(false);
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target as Node)) {
setOpenSelect(false);
}
};
window.addEventListener('click', handleClickOutside);
return () => window.removeEventListener('click', handleClickOutside);
}, []);
useEffect(() => {
list.forEach((listItem: ListItem) => {
if (
('path' in listItem && pathname === listItem.path) ||
('slug' in listItem && searchParams.get('sort') === listItem.slug)
) {
setActive(listItem.title);
}
});
}, [pathname, list, searchParams]);
return (
<div className="relative" ref={ref}>
<div
onClick={() => {
setOpenSelect(!openSelect);
}}
className="flex w-full items-center justify-between rounded border border-black/30 px-4 py-2 text-sm dark:border-white/30"
>
<div>{active}</div>
<ChevronDownIcon className="h-4" />
</div>
{openSelect && (
<div
onClick={() => {
setOpenSelect(false);
}}
className="absolute z-40 w-full rounded-b-md bg-white p-4 shadow-md dark:bg-black"
>
{list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} />
))}
</div>
)}
</div>
);
}

View File

@ -1,41 +0,0 @@
import { SortFilterItem } from 'lib/constants';
import { Suspense } from 'react';
import FilterItemDropdown from './dropdown';
import { FilterItem } from './item';
export type ListItem = SortFilterItem | PathFilterItem;
export type PathFilterItem = { title: string; path: string };
function FilterItemList({ list }: { list: ListItem[] }) {
return (
<>
{list.map((item: ListItem, i) => (
<FilterItem key={i} item={item} />
))}
</>
);
}
export default function FilterList({ list, title }: { list: ListItem[]; title?: string }) {
return (
<>
<nav>
{title ? (
<h3 className="hidden text-xs text-neutral-500 md:block dark:text-neutral-400">
{title}
</h3>
) : null}
<ul className="hidden md:block">
<Suspense fallback={null}>
<FilterItemList list={list} />
</Suspense>{' '}
</ul>
<ul className="md:hidden">
<Suspense fallback={null}>
<FilterItemDropdown list={list} />
</Suspense>{' '}
</ul>
</nav>
</>
);
}

View File

@ -1,67 +0,0 @@
'use client';
import clsx from 'clsx';
import type { SortFilterItem } from 'lib/constants';
import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
import type { ListItem, PathFilterItem } from '.';
function PathFilterItem({ item }: { item: PathFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = pathname === item.path;
const newParams = new URLSearchParams(searchParams.toString());
const DynamicTag = active ? 'p' : Link;
newParams.delete('q');
return (
<li className="mt-2 flex text-black dark:text-white" key={item.title}>
<DynamicTag
href={createUrl(item.path, newParams)}
className={clsx(
'w-full text-sm underline-offset-4 hover:underline dark:hover:text-neutral-100',
{
'underline underline-offset-4': active
}
)}
>
{item.title}
</DynamicTag>
</li>
);
}
function SortFilterItem({ item }: { item: SortFilterItem }) {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = searchParams.get('sort') === item.slug;
const q = searchParams.get('q');
const href = createUrl(
pathname,
new URLSearchParams({
...(q && { q }),
...(item.slug && item.slug.length && { sort: item.slug })
})
);
const DynamicTag = active ? 'p' : Link;
return (
<li className="mt-2 flex text-sm text-black dark:text-white" key={item.title}>
<DynamicTag
prefetch={!active ? false : undefined}
href={href}
className={clsx('w-full hover:underline hover:underline-offset-4', {
'underline underline-offset-4': active
})}
>
{item.title}
</DynamicTag>
</li>
);
}
export function FilterItem({ item }: { item: ListItem }) {
return 'path' in item ? <PathFilterItem item={item} /> : <SortFilterItem item={item} />;
}

View File

@ -1,10 +1,12 @@
'use client'; 'use client';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx'; import clsx from 'clsx';
import { Filter } from 'lib/shopify/types'; import { Filter } from 'lib/shopify/types';
import { createUrl } from 'lib/utils'; import { createUrl } from 'lib/utils';
import { usePathname, useRouter, useSearchParams } from 'next/navigation'; import { usePathname, useRouter, useSearchParams } from 'next/navigation';
const Filters = ({ filters }: { filters: Filter[] }) => { const Filters = ({ filters, defaultOpen = true }: { filters: Filter[]; defaultOpen?: boolean }) => {
const router = useRouter(); const router = useRouter();
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
@ -31,9 +33,17 @@ const Filters = ({ filters }: { filters: Filter[] }) => {
return ( return (
<form onChange={handleChange} className="space-y-5 divide-y divide-gray-200"> <form onChange={handleChange} className="space-y-5 divide-y divide-gray-200">
{filters.map(({ label, id, values }) => ( {filters.map(({ label, id, values }) => (
<div key={id} className="flex h-auto max-h-[550px] flex-col gap-y-3 overflow-hidden pt-5"> <Disclosure
<div className="block text-sm font-medium text-gray-900">{label}</div> key={id}
<div className="flex-grow space-y-3 overflow-auto pb-1 pl-1 pt-2"> as="div"
className="flex h-auto max-h-[550px] flex-col gap-y-3 overflow-hidden pt-5"
defaultOpen={defaultOpen}
>
<DisclosureButton className="group flex items-center justify-between">
<div className="text-sm font-medium text-gray-900">{label}</div>
<ChevronDownIcon className="size-4 group-data-[open]:rotate-180" />
</DisclosureButton>
<DisclosurePanel className="flex-grow space-y-3 overflow-auto pb-1 pl-1 pt-2">
{values.map(({ id: valueId, label, count, value }) => ( {values.map(({ id: valueId, label, count, value }) => (
<label <label
key={valueId} key={valueId}
@ -54,8 +64,8 @@ const Filters = ({ filters }: { filters: Filter[] }) => {
<span>{`${label} (${count})`}</span> <span>{`${label} (${count})`}</span>
</label> </label>
))} ))}
</div> </DisclosurePanel>
</div> </Disclosure>
))} ))}
</form> </form>
); );

View File

@ -1,34 +0,0 @@
import { getMenu } from 'lib/shopify';
import { Filter } from 'lib/shopify/types';
import Link from 'next/link';
import FiltersList from './filters-list';
const Filters = async ({ collection, filters }: { collection: string; filters: Filter[] }) => {
const menu = await getMenu('main-menu');
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
return (
<div>
{subMenu.length ? (
<>
<h3 className="sr-only">Categories</h3>
<ul
role="list"
className="space-y-4 border-b border-gray-200 pb-6 text-sm font-medium text-gray-900"
>
{subMenu.map((subMenuItem) => (
<li key={subMenuItem.title}>
<Link href={subMenuItem.path} className="hover:underline">
{subMenuItem.title}
</Link>
</li>
))}
</ul>
</>
) : null}
<h3 className="sr-only">Filters</h3>
<FiltersList filters={filters} />
</div>
);
};
export default Filters;

View File

@ -0,0 +1,79 @@
'use client';
import { Dialog, DialogPanel, Transition, TransitionChild } from '@headlessui/react';
import { FunnelIcon } from '@heroicons/react/24/outline';
import { XMarkIcon } from '@heroicons/react/24/solid';
import { Filter, Menu } from 'lib/shopify/types';
import { Fragment, useState } from 'react';
import Filters from './filters-list';
import SubMenu from './sub-menu';
const MobileFilters = ({
collection,
filters,
menu
}: {
collection: string;
filters: Filter[];
menu: Menu[];
}) => {
const [openDialog, setOpenDialog] = useState(false);
return (
<div className="lg:hidden">
<button
className="flex items-center gap-2 rounded border border-gray-300 px-3 py-1 text-sm text-gray-700"
onClick={() => setOpenDialog(true)}
>
Filters
<FunnelIcon className="size-4" />
</button>
<Transition show={openDialog}>
<Dialog as="div" className="relative z-40" onClose={setOpenDialog}>
<TransitionChild
as={Fragment}
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<div className="fixed inset-0 bg-black bg-opacity-25" />
</TransitionChild>
<div className="fixed inset-0 z-40 flex">
<TransitionChild
as={Fragment}
enter="transition ease-in-out duration-300 transform"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-in-out duration-300 transform"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
>
<DialogPanel className="relative ml-auto flex h-full w-full max-w-xs flex-col overflow-y-auto bg-white py-4 pb-6 shadow-xl">
<div className="flex items-center justify-between px-4">
<h2 className="text-lg font-medium text-gray-900">Filters</h2>
<button
type="button"
className="-mr-2 flex h-10 w-10 items-center justify-center p-2 text-gray-400 hover:text-gray-500"
onClick={() => setOpenDialog(false)}
>
<span className="sr-only">Close menu</span>
<XMarkIcon className="size-6" aria-hidden="true" />
</button>
</div>
<div className="mt-4 border-t border-gray-200 px-4 pt-4">
<SubMenu collection={collection} menu={menu} />
<Filters filters={filters} defaultOpen={false} />
</div>
</DialogPanel>
</TransitionChild>
</div>
</Dialog>
</Transition>
</div>
);
};
export default MobileFilters;

View File

@ -0,0 +1,26 @@
import { Menu } from 'lib/shopify/types';
import Link from 'next/link';
const SubMenu = ({ menu, collection }: { menu: Menu[]; collection: string }) => {
const subMenu = menu.find((item) => item.path === `/search/${collection}`)?.items || [];
return subMenu.length ? (
<>
<h3 className="sr-only">Categories</h3>
<ul
role="list"
className="space-y-4 border-b border-gray-200 pb-6 text-sm font-medium text-gray-900"
>
{subMenu.map((subMenuItem) => (
<li key={subMenuItem.title}>
<Link href={subMenuItem.path} className="hover:underline">
{subMenuItem.title}
</Link>
</li>
))}
</ul>
</>
) : null;
};
export default SubMenu;

View File

@ -1,6 +1,6 @@
'use client'; 'use client';
import { Menu, Transition } from '@headlessui/react'; import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid'; import { ChevronDownIcon } from '@heroicons/react/20/solid';
import { defaultSort, sorting } from 'lib/constants'; import { defaultSort, sorting } from 'lib/constants';
import { useSearchParams } from 'next/navigation'; import { useSearchParams } from 'next/navigation';
@ -14,7 +14,7 @@ const SortingMenu = () => {
return ( return (
<Menu as="div" className="relative inline-block text-left"> <Menu as="div" className="relative inline-block text-left">
<div> <div>
<Menu.Button className="group inline-flex justify-center rounded border border-gray-300 px-3 py-1 text-sm text-gray-700 hover:bg-gray-100"> <MenuButton className="group inline-flex justify-center rounded border border-gray-300 px-3 py-1 text-sm text-gray-700 hover:bg-gray-100">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
Sort by:{' '} Sort by:{' '}
<span> <span>
@ -25,7 +25,7 @@ const SortingMenu = () => {
className="-mr-1 ml-1.5 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500" className="-mr-1 ml-1.5 h-5 w-5 flex-shrink-0 text-gray-400 group-hover:text-gray-500"
aria-hidden="true" aria-hidden="true"
/> />
</Menu.Button> </MenuButton>
</div> </div>
<Transition <Transition
@ -37,19 +37,17 @@ const SortingMenu = () => {
leaveFrom="transform opacity-100 scale-100" leaveFrom="transform opacity-100 scale-100"
leaveTo="transform opacity-0 scale-95" leaveTo="transform opacity-0 scale-95"
> >
<Menu.Items className="absolute right-0 z-10 mt-2 w-full origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"> <MenuItems className="absolute right-0 z-10 mt-2 w-full origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none">
<div className="py-1"> <div className="py-1">
{sorting.map((option) => ( {sorting.map((option) => (
<Menu.Item key={option.title}> <MenuItem key={option.title}>
{({ active }) => ( <div className="data-[focus]:bg-gray-100">
<div> <SortingItem item={option} />
<SortingItem item={option} hover={active} /> </div>
</div> </MenuItem>
)}
</Menu.Item>
))} ))}
</div> </div>
</Menu.Items> </MenuItems>
</Transition> </Transition>
</Menu> </Menu>
); );

View File

@ -4,7 +4,7 @@ import { createUrl } from 'lib/utils';
import Link from 'next/link'; import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation'; import { usePathname, useSearchParams } from 'next/navigation';
const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean }) => { const SortingItem = ({ item }: { item: SortFilterItem }) => {
const pathname = usePathname(); const pathname = usePathname();
const searchParams = useSearchParams(); const searchParams = useSearchParams();
const active = searchParams.get('sort') === item.slug; const active = searchParams.get('sort') === item.slug;
@ -23,11 +23,9 @@ const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean })
<DynamicTag <DynamicTag
prefetch={!active ? false : undefined} prefetch={!active ? false : undefined}
href={href} href={href}
className={clsx('block px-4 py-2 text-sm', { className={clsx('block bg-transparent px-4 py-2 text-sm', {
'font-medium text-gray-900': active, 'font-medium text-gray-900': active,
'text-gray-500': !active, 'text-gray-500': !active
'bg-gray-100': hover,
'bg-transparent': !hover
})} })}
> >
{item.title} {item.title}

View File

@ -22,7 +22,7 @@
"*": "prettier --write --ignore-unknown" "*": "prettier --write --ignore-unknown"
}, },
"dependencies": { "dependencies": {
"@headlessui/react": "^1.7.18", "@headlessui/react": "^2.0.1",
"@heroicons/react": "^2.1.3", "@heroicons/react": "^2.1.3",
"@radix-ui/react-checkbox": "^1.0.4", "@radix-ui/react-checkbox": "^1.0.4",
"clsx": "^2.1.0", "clsx": "^2.1.0",

121
pnpm-lock.yaml generated
View File

@ -6,8 +6,8 @@ settings:
dependencies: dependencies:
'@headlessui/react': '@headlessui/react':
specifier: ^1.7.18 specifier: ^2.0.1
version: 1.7.18(react-dom@18.2.0)(react@18.2.0) version: 2.0.1(react-dom@18.2.0)(react@18.2.0)
'@heroicons/react': '@heroicons/react':
specifier: ^2.1.3 specifier: ^2.1.3
version: 2.1.3(react@18.2.0) version: 2.1.3(react@18.2.0)
@ -186,19 +186,45 @@ packages:
'@floating-ui/utils': 0.2.1 '@floating-ui/utils': 0.2.1
dev: false dev: false
/@floating-ui/react-dom@2.0.9(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-q0umO0+LQK4+p6aGyvzASqKbKOJcAHJ7ycE9CuUvfx3s9zTHWmGJTPOIlM/hmSBfUfg/XfY5YhLBLR/LHwShQQ==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/dom': 1.6.3
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@floating-ui/react@0.26.13(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-kBa9wntpugzrZ8t/4yWelvSmEKZdeTXTJzrxqyrLmcU/n1SM4nvse8yQh2e1b37rJGvtu0EplV9+IkBrCJ1vkw==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
dependencies:
'@floating-ui/react-dom': 2.0.9(react-dom@18.2.0)(react@18.2.0)
'@floating-ui/utils': 0.2.1
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
tabbable: 6.2.0
dev: false
/@floating-ui/utils@0.2.1: /@floating-ui/utils@0.2.1:
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==} resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
dev: false dev: false
/@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0): /@headlessui/react@2.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==} resolution: {integrity: sha512-GxFvHHk27AYELf0WIMa0LgSeVqJ0SOvIwg7USTptMFbtLz31jNGQoolHiQPnvsI/IMmEeJ4ybzQlqV69/uvQ8A==}
engines: {node: '>=10'} engines: {node: '>=10'}
peerDependencies: peerDependencies:
react: ^16 || ^17 || ^18 react: ^18
react-dom: ^16 || ^17 || ^18 react-dom: ^18
dependencies: dependencies:
'@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0) '@floating-ui/react': 0.26.13(react-dom@18.2.0)(react@18.2.0)
client-only: 0.0.1 '@react-aria/focus': 3.17.0(react@18.2.0)
'@react-aria/interactions': 3.21.2(react@18.2.0)
'@tanstack/react-virtual': 3.5.0(react-dom@18.2.0)(react@18.2.0)
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
@ -584,6 +610,71 @@ packages:
react: 18.2.0 react: 18.2.0
dev: false dev: false
/@react-aria/focus@3.17.0(react@18.2.0):
resolution: {integrity: sha512-aRzBw1WTUkcIV3xFrqPA6aB8ZVt3XyGpTaSHAypU0Pgoy2wRq9YeJYpbunsKj9CJmskuffvTqXwAjTcaQish1Q==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/interactions': 3.21.2(react@18.2.0)
'@react-aria/utils': 3.24.0(react@18.2.0)
'@react-types/shared': 3.23.0(react@18.2.0)
'@swc/helpers': 0.5.2
clsx: 2.1.0
react: 18.2.0
dev: false
/@react-aria/interactions@3.21.2(react@18.2.0):
resolution: {integrity: sha512-Ju706DtoEmI/2vsfu9DCEIjDqsRBVLm/wmt2fr0xKbBca7PtmK8daajxFWz+eTq+EJakvYfLr7gWgLau9HyWXg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/ssr': 3.9.3(react@18.2.0)
'@react-aria/utils': 3.24.0(react@18.2.0)
'@react-types/shared': 3.23.0(react@18.2.0)
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-aria/ssr@3.9.3(react@18.2.0):
resolution: {integrity: sha512-5bUZ93dmvHFcmfUcEN7qzYe8yQQ8JY+nHN6m9/iSDCQ/QmCiE0kWXYwhurjw5ch6I8WokQzx66xKIMHBAa4NNA==}
engines: {node: '>= 12'}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-aria/utils@3.24.0(react@18.2.0):
resolution: {integrity: sha512-JAxkPhK5fCvFVNY2YG3TW3m1nTzwRcbz7iyTSkUzLFat4N4LZ7Kzh7NMHsgeE/oMOxd8zLY+XsUxMu/E/2GujA==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@react-aria/ssr': 3.9.3(react@18.2.0)
'@react-stately/utils': 3.10.0(react@18.2.0)
'@react-types/shared': 3.23.0(react@18.2.0)
'@swc/helpers': 0.5.2
clsx: 2.1.0
react: 18.2.0
dev: false
/@react-stately/utils@3.10.0(react@18.2.0):
resolution: {integrity: sha512-nji2i9fTYg65ZWx/3r11zR1F2tGya+mBubRCbMTwHyRnsSLFZaeq/W6lmrOyIy1uMJKBNKLJpqfmpT4x7rw6pg==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
'@swc/helpers': 0.5.2
react: 18.2.0
dev: false
/@react-types/shared@3.23.0(react@18.2.0):
resolution: {integrity: sha512-GQm/iPiii3ikcaMNR4WdVkJ4w0mKtV3mLqeSfSqzdqbPr6vONkqXbh3RhPlPmAJs1b4QHnexd/wZQP3U9DHOwQ==}
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
dependencies:
react: 18.2.0
dev: false
/@rushstack/eslint-patch@1.8.0: /@rushstack/eslint-patch@1.8.0:
resolution: {integrity: sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ==} resolution: {integrity: sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ==}
dev: true dev: true
@ -623,19 +714,19 @@ packages:
tailwindcss: 3.4.1 tailwindcss: 3.4.1
dev: true dev: true
/@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0): /@tanstack/react-virtual@3.5.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==} resolution: {integrity: sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies: dependencies:
'@tanstack/virtual-core': 3.2.0 '@tanstack/virtual-core': 3.5.0
react: 18.2.0 react: 18.2.0
react-dom: 18.2.0(react@18.2.0) react-dom: 18.2.0(react@18.2.0)
dev: false dev: false
/@tanstack/virtual-core@3.2.0: /@tanstack/virtual-core@3.5.0:
resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==} resolution: {integrity: sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==}
dev: false dev: false
/@types/json5@0.0.29: /@types/json5@0.0.29:
@ -3568,6 +3659,10 @@ packages:
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
dev: true dev: true
/tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
dev: false
/tailwind-merge@2.2.2: /tailwind-merge@2.2.2:
resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==} resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==}
dependencies: dependencies: