mirror of
https://github.com/vercel/commerce.git
synced 2025-05-12 12:47:50 +00:00
feat: mobile filters panel
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
145eb3eaed
commit
98d1f5c821
@ -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">
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
@ -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} />;
|
||||
}
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
79
components/layout/search/filters/mobile-filters.tsx
Normal file
79
components/layout/search/filters/mobile-filters.tsx
Normal 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;
|
26
components/layout/search/filters/sub-menu.tsx
Normal file
26
components/layout/search/filters/sub-menu.tsx
Normal 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;
|
@ -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>
|
||||
);
|
||||
|
@ -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}
|
||||
|
@ -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
121
pnpm-lock.yaml
generated
@ -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:
|
||||
|
Loading…
x
Reference in New Issue
Block a user