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 { notFound } from 'next/navigation';
@ -6,7 +6,9 @@ import Breadcrumb from 'components/breadcrumb';
import BreadcrumbHome from 'components/breadcrumb/breadcrumb-home';
import Grid from 'components/grid';
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 {
AVAILABILITY_FILTER_ID,
@ -94,10 +96,14 @@ export default async function CategoryPage({
reverse,
...(filtersInput.length ? { filters: filtersInput } : {})
});
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 (
<>
@ -107,12 +113,13 @@ export default async function CategoryPage({
</Suspense>
</div>
{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>
<p className="mt-2 text-base text-gray-500">{collection.description}</p>
</div>
) : 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 />
</div>
<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">
<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>
<div className="lg:col-span-2 xl:col-span-3">
<Grid className="grid-cols-1 sm:grid-cols-2 lg:grid-cols-3">

View File

@ -1,6 +1,6 @@
'use client';
import { Popover, Transition } from '@headlessui/react';
import { Popover, PopoverGroup, PopoverPanel, Transition } from '@headlessui/react';
import clsx from 'clsx';
import { Menu } from 'lib/shopify/types';
import Link from 'next/link';
@ -13,7 +13,7 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => {
return menu.length ? (
<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">
{menu.map((item: Menu) => {
const isActiveItem =
@ -62,7 +62,7 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => {
leaveTo="opacity-0"
show={isOpen}
>
<Popover.Panel
<PopoverPanel
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"
>
@ -80,14 +80,14 @@ const MainMenu = ({ menu }: { menu: Menu[] }) => {
))}
</ul>
</div>
</Popover.Panel>
</PopoverPanel>
</Transition>
</div>
</Popover>
);
})}
</div>
</Popover.Group>
</PopoverGroup>
</div>
) : 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';
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/24/outline';
import clsx from 'clsx';
import { Filter } from 'lib/shopify/types';
import { createUrl } from 'lib/utils';
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 pathname = usePathname();
const searchParams = useSearchParams();
@ -31,9 +33,17 @@ const Filters = ({ filters }: { filters: Filter[] }) => {
return (
<form onChange={handleChange} className="space-y-5 divide-y divide-gray-200">
{filters.map(({ label, id, values }) => (
<div key={id} className="flex h-auto max-h-[550px] flex-col gap-y-3 overflow-hidden pt-5">
<div className="block text-sm font-medium text-gray-900">{label}</div>
<div className="flex-grow space-y-3 overflow-auto pb-1 pl-1 pt-2">
<Disclosure
key={id}
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 }) => (
<label
key={valueId}
@ -54,8 +64,8 @@ const Filters = ({ filters }: { filters: Filter[] }) => {
<span>{`${label} (${count})`}</span>
</label>
))}
</div>
</div>
</DisclosurePanel>
</Disclosure>
))}
</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';
import { Menu, Transition } from '@headlessui/react';
import { Menu, MenuButton, MenuItem, MenuItems, Transition } from '@headlessui/react';
import { ChevronDownIcon } from '@heroicons/react/20/solid';
import { defaultSort, sorting } from 'lib/constants';
import { useSearchParams } from 'next/navigation';
@ -14,7 +14,7 @@ const SortingMenu = () => {
return (
<Menu as="div" className="relative inline-block text-left">
<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">
Sort by:{' '}
<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"
aria-hidden="true"
/>
</Menu.Button>
</MenuButton>
</div>
<Transition
@ -37,19 +37,17 @@ const SortingMenu = () => {
leaveFrom="transform opacity-100 scale-100"
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">
{sorting.map((option) => (
<Menu.Item key={option.title}>
{({ active }) => (
<div>
<SortingItem item={option} hover={active} />
</div>
)}
</Menu.Item>
<MenuItem key={option.title}>
<div className="data-[focus]:bg-gray-100">
<SortingItem item={option} />
</div>
</MenuItem>
))}
</div>
</Menu.Items>
</MenuItems>
</Transition>
</Menu>
);

View File

@ -4,7 +4,7 @@ import { createUrl } from 'lib/utils';
import Link from 'next/link';
import { usePathname, useSearchParams } from 'next/navigation';
const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean }) => {
const SortingItem = ({ item }: { item: SortFilterItem }) => {
const pathname = usePathname();
const searchParams = useSearchParams();
const active = searchParams.get('sort') === item.slug;
@ -23,11 +23,9 @@ const SortingItem = ({ item, hover }: { item: SortFilterItem; hover: boolean })
<DynamicTag
prefetch={!active ? false : undefined}
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,
'text-gray-500': !active,
'bg-gray-100': hover,
'bg-transparent': !hover
'text-gray-500': !active
})}
>
{item.title}

View File

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

121
pnpm-lock.yaml generated
View File

@ -6,8 +6,8 @@ settings:
dependencies:
'@headlessui/react':
specifier: ^1.7.18
version: 1.7.18(react-dom@18.2.0)(react@18.2.0)
specifier: ^2.0.1
version: 2.0.1(react-dom@18.2.0)(react@18.2.0)
'@heroicons/react':
specifier: ^2.1.3
version: 2.1.3(react@18.2.0)
@ -186,19 +186,45 @@ packages:
'@floating-ui/utils': 0.2.1
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:
resolution: {integrity: sha512-9TANp6GPoMtYzQdt54kfAyMmz1+osLlXdg2ENroU7zzrtflTLrrC/lgrIfaSe+Wu0b89GKccT7vxXA0MoAIO+Q==}
dev: false
/@headlessui/react@1.7.18(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-4i5DOrzwN4qSgNsL4Si61VMkUcWbcSKueUV7sFhpHzQcSShdlHENE5+QBntMSRvHt8NyoFO2AGG8si9lq+w4zQ==}
/@headlessui/react@2.0.1(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-GxFvHHk27AYELf0WIMa0LgSeVqJ0SOvIwg7USTptMFbtLz31jNGQoolHiQPnvsI/IMmEeJ4ybzQlqV69/uvQ8A==}
engines: {node: '>=10'}
peerDependencies:
react: ^16 || ^17 || ^18
react-dom: ^16 || ^17 || ^18
react: ^18
react-dom: ^18
dependencies:
'@tanstack/react-virtual': 3.2.0(react-dom@18.2.0)(react@18.2.0)
client-only: 0.0.1
'@floating-ui/react': 0.26.13(react-dom@18.2.0)(react@18.2.0)
'@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-dom: 18.2.0(react@18.2.0)
dev: false
@ -584,6 +610,71 @@ packages:
react: 18.2.0
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:
resolution: {integrity: sha512-0HejFckBN2W+ucM6cUOlwsByTKt9/+0tWhqUffNIcHqCXkthY/mZ7AuYPK/2IIaGWhdl0h+tICDO0ssLMd6XMQ==}
dev: true
@ -623,19 +714,19 @@ packages:
tailwindcss: 3.4.1
dev: true
/@tanstack/react-virtual@3.2.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-OEdMByf2hEfDa6XDbGlZN8qO6bTjlNKqjM3im9JG+u3mCL8jALy0T/67oDI001raUUPh1Bdmfn4ZvPOV5knpcg==}
/@tanstack/react-virtual@3.5.0(react-dom@18.2.0)(react@18.2.0):
resolution: {integrity: sha512-rtvo7KwuIvqK9zb0VZ5IL7fiJAEnG+0EiFZz8FUOs+2mhGqdGmjKIaT1XU7Zq0eFqL0jonLlhbayJI/J2SA/Bw==}
peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
dependencies:
'@tanstack/virtual-core': 3.2.0
'@tanstack/virtual-core': 3.5.0
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
dev: false
/@tanstack/virtual-core@3.2.0:
resolution: {integrity: sha512-P5XgYoAw/vfW65byBbJQCw+cagdXDT/qH6wmABiLt4v4YBT2q2vqCOhihe+D1Nt325F/S/0Tkv6C5z0Lv+VBQQ==}
/@tanstack/virtual-core@3.5.0:
resolution: {integrity: sha512-KnPRCkQTyqhanNC0K63GBG3wA8I+D1fQuVnAvcBF8f13akOKeQp1gSbu6f77zCxhEk727iV5oQnbHLYzHrECLg==}
dev: false
/@types/json5@0.0.29:
@ -3568,6 +3659,10 @@ packages:
engines: {node: '>= 0.4'}
dev: true
/tabbable@6.2.0:
resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==}
dev: false
/tailwind-merge@2.2.2:
resolution: {integrity: sha512-tWANXsnmJzgw6mQ07nE3aCDkCK4QdT3ThPMCzawoYA2Pws7vSTCvz3Vrjg61jVUGfFZPJzxEP+NimbcW+EdaDw==}
dependencies: