mirror of
https://github.com/discordjs/discord.js.git
synced 2026-03-13 01:53:30 +01:00
refactor: docs (#10126)
This commit is contained in:
67
apps/website/src/components/ui/Alert.tsx
Normal file
67
apps/website/src/components/ui/Alert.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import { VscFlame } from '@react-icons/all-files/vsc/VscFlame';
|
||||
import { VscInfo } from '@react-icons/all-files/vsc/VscInfo';
|
||||
import { VscWarning } from '@react-icons/all-files/vsc/VscWarning';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
interface IAlert {
|
||||
readonly title?: string | undefined;
|
||||
readonly type: 'danger' | 'info' | 'success' | 'warning';
|
||||
}
|
||||
|
||||
function resolveType(type: IAlert['type']) {
|
||||
switch (type) {
|
||||
case 'danger': {
|
||||
return {
|
||||
text: 'text-red-500',
|
||||
border: 'border-red-500',
|
||||
icon: <VscWarning aria-hidden size={20} />,
|
||||
};
|
||||
}
|
||||
|
||||
case 'info': {
|
||||
return {
|
||||
text: 'text-blue-500',
|
||||
border: 'border-blue-500',
|
||||
icon: <VscInfo aria-hidden size={20} />,
|
||||
};
|
||||
}
|
||||
|
||||
case 'success': {
|
||||
return {
|
||||
text: 'text-green-500',
|
||||
border: 'border-green-500',
|
||||
icon: <VscFlame aria-hidden size={20} />,
|
||||
};
|
||||
}
|
||||
|
||||
case 'warning': {
|
||||
return {
|
||||
text: 'text-yellow-500',
|
||||
border: 'border-yellow-500',
|
||||
icon: <VscWarning aria-hidden size={20} />,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function Alert({ title, type, children }: PropsWithChildren<IAlert>) {
|
||||
const { text, border, icon } = resolveType(type);
|
||||
|
||||
return (
|
||||
<div className="mb-4 mt-6" role="alert">
|
||||
<div className="relative flex">
|
||||
<div className="p-4">{children}</div>
|
||||
<div className="pointer-events-none absolute flex h-full w-full">
|
||||
<div className={`w-4 shrink-0 rounded-bl-md rounded-tl-md border-b-2 border-l-2 border-t-2 ${border}`} />
|
||||
<div className={`relative border-b-2 ${border}`}>
|
||||
<div className={`pointer-events-auto flex -translate-y-1/2 place-items-center gap-2 px-2 ${text}`}>
|
||||
{icon}
|
||||
{title ? <span className={`font-semibold ${text}`}>{title}</span> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className={`flex-1 rounded-br-md rounded-tr-md border-b-2 border-r-2 border-t-2 ${border}`} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
3
apps/website/src/components/ui/Button.tsx
Normal file
3
apps/website/src/components/ui/Button.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { Button } from 'react-aria-components';
|
||||
148
apps/website/src/components/ui/CmdK.tsx
Normal file
148
apps/website/src/components/ui/CmdK.tsx
Normal file
@@ -0,0 +1,148 @@
|
||||
'use client';
|
||||
|
||||
import { Command } from 'cmdk';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { ArrowRight } from 'lucide-react';
|
||||
import MeiliSearch from 'meilisearch';
|
||||
import { usePathname, useRouter } from 'next/navigation';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useDebounceValue } from 'usehooks-ts';
|
||||
import { isCmdKOpenAtom } from '~/stores/cmdk';
|
||||
import { isDrawerOpenAtom } from '~/stores/drawer';
|
||||
import { resolveKind } from '~/util/resolveNodeKind';
|
||||
import { OverlayScrollbarsComponent } from '../OverlayScrollbars';
|
||||
|
||||
const client = new MeiliSearch({
|
||||
host: 'https://search.discordjs.dev',
|
||||
apiKey: 'b51923c6abb574b1e97be9a03dc6414b6c69fb0c5696d0ef01a82b0f77d223db',
|
||||
});
|
||||
|
||||
export function CmdK({ dependencies }: { readonly dependencies: string[] }) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const [open, setOpen] = useAtom(isCmdKOpenAtom);
|
||||
const setDrawerOpen = useSetAtom(isDrawerOpenAtom);
|
||||
const [search, setSearch] = useDebounceValue('', 250);
|
||||
const [searchResults, setSearchResults] = useState<any[]>([]);
|
||||
|
||||
const packageName = useMemo(() => pathname?.split('/').slice(3, 4)[0], [pathname]);
|
||||
const branchName = useMemo(() => pathname?.split('/').slice(4, 5)[0], [pathname]);
|
||||
|
||||
const searchResultItems = useMemo(
|
||||
() =>
|
||||
searchResults?.map((item, idx) => (
|
||||
<Command.Item
|
||||
key={`${item.id}-${idx}`}
|
||||
className="flex cursor-pointer place-items-center gap-2 rounded-md p-2 data-[selected]:bg-neutral-200 dark:data-[selected]:bg-neutral-800"
|
||||
onSelect={() => {
|
||||
router.push(item.path);
|
||||
setOpen(false);
|
||||
}}
|
||||
value={item.id}
|
||||
>
|
||||
{resolveKind(item.kind)}
|
||||
<div className="flex flex-grow flex-col">
|
||||
<span className="font-semibold">{item.name}</span>
|
||||
<span className="line-clamp-1 text-sm">{item.summary}</span>
|
||||
<span className="truncate text-xs">{item.path}</span>
|
||||
</div>
|
||||
<ArrowRight aria-hidden className="flex-shrink-0" />
|
||||
</Command.Item>
|
||||
)) ?? [],
|
||||
[router, searchResults, setOpen],
|
||||
);
|
||||
|
||||
// Toggle the menu when ⌘K is pressed
|
||||
useEffect(() => {
|
||||
const down = (event: KeyboardEvent) => {
|
||||
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
|
||||
event.preventDefault();
|
||||
setOpen((open) => !open);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', down);
|
||||
return () => {
|
||||
document.removeEventListener('keydown', down);
|
||||
};
|
||||
}, [setOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
setDrawerOpen(false);
|
||||
setSearch('');
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.body.style.pointerEvents = 'auto';
|
||||
};
|
||||
}, [open, setDrawerOpen, setSearch]);
|
||||
|
||||
useEffect(() => {
|
||||
// const searchDoc = async (searchString: string, version: string) => {
|
||||
// console.log(dependencies);
|
||||
// const res = await client
|
||||
// .index(`${packageName?.replaceAll('.', '-')}-${version}`)
|
||||
// .search(searchString, { limit: 25 });
|
||||
// setSearchResults(res.hits);
|
||||
// };
|
||||
|
||||
const searchDoc = async (searchString: string, version: string) => {
|
||||
const result = await client.multiSearch({
|
||||
queries: [`${packageName?.replaceAll('.', '-')}-${version}`, ...dependencies].map((dep) => ({
|
||||
indexUid: dep,
|
||||
// eslint-disable-next-line id-length
|
||||
q: searchString,
|
||||
limit: 25,
|
||||
attributesToSearchOn: ['name'],
|
||||
})),
|
||||
});
|
||||
setSearchResults(result.results.flatMap((res) => res.hits));
|
||||
};
|
||||
|
||||
if (search && packageName) {
|
||||
void searchDoc(search, branchName?.replaceAll('.', '-') ?? 'main');
|
||||
} else {
|
||||
setSearchResults([]);
|
||||
}
|
||||
}, [branchName, dependencies, packageName, search]);
|
||||
|
||||
return (
|
||||
<Command.Dialog
|
||||
className="w-full rounded-md border border-neutral-300 bg-neutral-100 p-2 shadow-md dark:border-neutral-700 dark:bg-neutral-900"
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
label="Command Menu"
|
||||
shouldFilter={false}
|
||||
>
|
||||
<Command.Input
|
||||
className="mb-4 w-full border-b border-neutral-300 bg-transparent px-2 pb-4 pt-2 outline-none dark:border-neutral-700"
|
||||
onValueChange={setSearch}
|
||||
placeholder="Quick search..."
|
||||
/>
|
||||
<OverlayScrollbarsComponent
|
||||
className="max-h-96 pr-3"
|
||||
defer
|
||||
options={{
|
||||
overflow: { x: 'hidden' },
|
||||
scrollbars: {
|
||||
autoHide: 'scroll',
|
||||
autoHideDelay: 500,
|
||||
autoHideSuspend: true,
|
||||
clickScroll: true,
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Command.List>
|
||||
{search && searchResultItems.length ? (
|
||||
searchResultItems
|
||||
) : (
|
||||
<div role="presentation" className="flex h-12 place-content-center place-items-center text-sm">
|
||||
No results found.
|
||||
</div>
|
||||
)}
|
||||
</Command.List>
|
||||
</OverlayScrollbarsComponent>
|
||||
</Command.Dialog>
|
||||
);
|
||||
}
|
||||
3
apps/website/src/components/ui/Collapsible.tsx
Normal file
3
apps/website/src/components/ui/Collapsible.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@radix-ui/react-collapsible';
|
||||
37
apps/website/src/components/ui/Drawer.tsx
Normal file
37
apps/website/src/components/ui/Drawer.tsx
Normal file
@@ -0,0 +1,37 @@
|
||||
'use client';
|
||||
|
||||
import { useAtom } from 'jotai';
|
||||
import { ChevronUp } from 'lucide-react';
|
||||
import { useEffect, type PropsWithChildren } from 'react';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
import { Drawer as Vaul } from 'vaul';
|
||||
import { isDrawerOpenAtom } from '~/stores/drawer';
|
||||
|
||||
export function Drawer({ children }: PropsWithChildren) {
|
||||
const [open, setOpen] = useAtom(isDrawerOpenAtom);
|
||||
const isMedium = useMediaQuery('(min-width: 768px)');
|
||||
|
||||
useEffect(() => {
|
||||
if (isMedium) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [isMedium, setOpen]);
|
||||
|
||||
return (
|
||||
<Vaul.Root open={open} onOpenChange={setOpen}>
|
||||
<Vaul.Trigger
|
||||
aria-label="Open navigation"
|
||||
className="flex h-12 w-full place-content-center place-items-center rounded-t-lg border-t border-neutral-300 bg-neutral-100 p-2 dark:border-neutral-700 dark:bg-neutral-900"
|
||||
>
|
||||
<ChevronUp aria-hidden size={28} />
|
||||
</Vaul.Trigger>
|
||||
<Vaul.Portal>
|
||||
<Vaul.Overlay className="fixed inset-0 bg-black/40" />
|
||||
<Vaul.Content className="fixed bottom-0 left-0 right-0 flex max-h-[86%] flex-col rounded-t-lg bg-neutral-100 p-4 dark:bg-neutral-900">
|
||||
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-neutral-400" />
|
||||
{children}
|
||||
</Vaul.Content>
|
||||
</Vaul.Portal>
|
||||
</Vaul.Root>
|
||||
);
|
||||
}
|
||||
88
apps/website/src/components/ui/Footer.tsx
Normal file
88
apps/website/src/components/ui/Footer.tsx
Normal file
@@ -0,0 +1,88 @@
|
||||
import Image from 'next/image';
|
||||
import vercelLogo from '~/assets/powered-by-vercel.svg';
|
||||
import workersLogo from '~/assets/powered-by-workers.png';
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="md:pl-12 md:pr-12">
|
||||
<div className="flex flex-col flex-wrap place-content-center gap-6 pt-12 sm:flex-row md:gap-12">
|
||||
<div className="flex flex-wrap place-content-center place-items-center gap-4">
|
||||
<a
|
||||
className="rounded"
|
||||
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
title="Vercel"
|
||||
>
|
||||
<Image
|
||||
alt="Vercel"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAABLCAQAAAA1k5H2AAAAi0lEQVR42u3SMQEAAAgDoC251a3gL2SgmfBYBRAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARCAgwWEOSWBnYbKggAAAABJRU5ErkJggg=="
|
||||
height={44}
|
||||
placeholder="blur"
|
||||
src={vercelLogo}
|
||||
width={212}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
className="rounded"
|
||||
href="https://www.cloudflare.com"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
title="Cloudflare Workers"
|
||||
>
|
||||
<Image
|
||||
alt="Cloudflare"
|
||||
blurDataURL="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAQAAAABLCAQAAAA1k5H2AAAAi0lEQVR42u3SMQEAAAgDoC251a3gL2SgmfBYBRAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARAAARCAgwWEOSWBnYbKggAAAABJRU5ErkJggg=="
|
||||
height={44}
|
||||
placeholder="blur"
|
||||
priority
|
||||
src={workersLogo}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-col gap-6 place-self-center sm:flex-row md:gap-12">
|
||||
<div className="flex max-w-max flex-col gap-2">
|
||||
<div className="text-lg font-semibold">Community</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<a className="rounded" href="https://discord.gg/djs" rel="external noopener noreferrer" target="_blank">
|
||||
Discord
|
||||
</a>
|
||||
<a
|
||||
className="rounded"
|
||||
href="https://github.com/discordjs/discord.js/discussions"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
GitHub discussions
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex max-w-max flex-col gap-2">
|
||||
<div className="text-lg font-semibold">Project</div>
|
||||
<div className="flex flex-col gap-1">
|
||||
<a
|
||||
className="rounded"
|
||||
href="https://github.com/discordjs/discord.js"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
discord.js
|
||||
</a>
|
||||
<a className="rounded" href="https://discord.js.org/docs" rel="noopener noreferrer" target="_blank">
|
||||
discord.js documentation
|
||||
</a>
|
||||
<a
|
||||
className="rounded"
|
||||
href="https://discord-api-types.dev"
|
||||
rel="external noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
discord-api-types
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
);
|
||||
}
|
||||
33
apps/website/src/components/ui/InstallButton.tsx
Normal file
33
apps/website/src/components/ui/InstallButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
'use client';
|
||||
|
||||
import { Copy, CopyCheck } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCopyToClipboard } from 'usehooks-ts';
|
||||
|
||||
export function InstallButton({ className = '' }: { readonly className?: string }) {
|
||||
const [interacted, setInteracted] = useState(false);
|
||||
const [copiedText, copyToClipboard] = useCopyToClipboard();
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setInteracted(false), 2_000);
|
||||
return () => clearTimeout(timer);
|
||||
}, [interacted]);
|
||||
|
||||
return (
|
||||
<button
|
||||
className={`cursor-copy rounded-md border border-neutral-300 bg-white px-4 py-2 font-mono hover:bg-neutral-200 dark:border-neutral-700 dark:bg-transparent dark:hover:bg-neutral-800 ${className}`}
|
||||
onClick={async () => {
|
||||
setInteracted(true);
|
||||
await copyToClipboard('npm install discord.js');
|
||||
}}
|
||||
type="button"
|
||||
>
|
||||
<span className="font-semibold text-blurple">{'>'}</span> npm install discord.js{' '}
|
||||
{copiedText && interacted ? (
|
||||
<CopyCheck aria-hidden size={20} className="ml-1 inline-block text-green-500" />
|
||||
) : (
|
||||
<Copy aria-hidden size={20} className="ml-1 inline-block" />
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
3
apps/website/src/components/ui/ListBox.tsx
Normal file
3
apps/website/src/components/ui/ListBox.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { ListBox, ListBoxItem } from 'react-aria-components';
|
||||
95
apps/website/src/components/ui/PackageSelect.tsx
Normal file
95
apps/website/src/components/ui/PackageSelect.tsx
Normal file
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronsUpDown } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Key } from 'react-aria-components';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
import { Drawer as Vaul } from 'vaul';
|
||||
import { PACKAGES } from '~/util/constants';
|
||||
import { Button } from './Button';
|
||||
import { ListBox, ListBoxItem } from './ListBox';
|
||||
import { Popover } from './Popover';
|
||||
import { Select, SelectValue } from './Select';
|
||||
|
||||
export function PackageSelect({ packageName }: { readonly packageName: string }) {
|
||||
const [selectedPackage, setSelectedPackage] = useState<Key>(packageName);
|
||||
const [open, setOpen] = useState(false);
|
||||
const isMedium = useMediaQuery('(min-width: 768px)');
|
||||
|
||||
useEffect(() => {
|
||||
if (isMedium) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [isMedium, setOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
aria-label="Select a package"
|
||||
className="hidden md:block"
|
||||
selectedKey={selectedPackage}
|
||||
onSelectionChange={(selected) => {
|
||||
setSelectedPackage(selected);
|
||||
}}
|
||||
>
|
||||
<Button className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800">
|
||||
<SelectValue className="font-medium" />
|
||||
<ChevronsUpDown aria-hidden size={20} />
|
||||
</Button>
|
||||
<Popover className="max-h-60 w-[--trigger-width] overflow-auto rounded-md border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<ListBox shouldFocusWrap items={PACKAGES}>
|
||||
{(item) => (
|
||||
<ListBoxItem
|
||||
id={item.name}
|
||||
textValue={item.name}
|
||||
href={`/docs/packages/${item.name}/main`}
|
||||
className="flex p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
|
||||
>
|
||||
{item.name}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
|
||||
<Vaul.NestedRoot open={open} onOpenChange={setOpen} dismissible={false}>
|
||||
<Vaul.Trigger
|
||||
aria-label="Open package select"
|
||||
className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800 md:hidden"
|
||||
>
|
||||
<span className="font-medium">{selectedPackage}</span>
|
||||
<ChevronsUpDown aria-hidden size={20} />
|
||||
</Vaul.Trigger>
|
||||
<Vaul.Portal>
|
||||
<Vaul.Overlay className="fixed inset-0 bg-black/40" />
|
||||
<Vaul.Content className="fixed bottom-0 left-0 right-0 flex max-h-[80%] flex-col rounded-t-lg bg-neutral-100 p-4 dark:bg-neutral-900">
|
||||
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-neutral-400" />
|
||||
<ListBox
|
||||
aria-label="Select a package"
|
||||
className="flex flex-col gap-2 overflow-auto"
|
||||
shouldFocusWrap
|
||||
items={PACKAGES}
|
||||
selectionMode="single"
|
||||
selectedKeys={[selectedPackage]}
|
||||
onSelectionChange={(selected) => {
|
||||
const [val] = selected;
|
||||
setSelectedPackage(val as Key);
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<ListBoxItem
|
||||
id={item.name}
|
||||
textValue={item.name}
|
||||
href={`/docs/packages/${item.name}/main`}
|
||||
className="rounded-md p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
|
||||
>
|
||||
{item.name}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</ListBox>
|
||||
</Vaul.Content>
|
||||
</Vaul.Portal>
|
||||
</Vaul.NestedRoot>
|
||||
</>
|
||||
);
|
||||
}
|
||||
3
apps/website/src/components/ui/Popover.tsx
Normal file
3
apps/website/src/components/ui/Popover.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { Popover } from 'react-aria-components';
|
||||
26
apps/website/src/components/ui/SearchButton.tsx
Normal file
26
apps/website/src/components/ui/SearchButton.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
'use client';
|
||||
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { Command, Search } from 'lucide-react';
|
||||
import { isCmdKOpenAtom } from '~/stores/cmdk';
|
||||
|
||||
export function SearchButton() {
|
||||
const setIsOpen = useSetAtom(isCmdKOpenAtom);
|
||||
|
||||
return (
|
||||
<button
|
||||
aria-label="Open search"
|
||||
className="flex place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800"
|
||||
type="button"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<span className="flex place-items-center gap-2">
|
||||
<Search aria-hidden size={20} />
|
||||
Search...
|
||||
</span>
|
||||
<span className="hidden place-items-center gap-1 md:flex">
|
||||
<Command aria-hidden size={20} /> K
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
3
apps/website/src/components/ui/Select.tsx
Normal file
3
apps/website/src/components/ui/Select.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { Select, SelectValue } from 'react-aria-components';
|
||||
3
apps/website/src/components/ui/Tabs.tsx
Normal file
3
apps/website/src/components/ui/Tabs.tsx
Normal file
@@ -0,0 +1,3 @@
|
||||
'use client';
|
||||
|
||||
export { Tabs, TabList, Tab, TabPanel } from 'react-aria-components';
|
||||
16
apps/website/src/components/ui/ThemeSwitch.tsx
Normal file
16
apps/website/src/components/ui/ThemeSwitch.tsx
Normal file
@@ -0,0 +1,16 @@
|
||||
'use client';
|
||||
|
||||
import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { Button } from './Button';
|
||||
|
||||
export function ThemeSwitch() {
|
||||
const { resolvedTheme, setTheme } = useTheme();
|
||||
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
|
||||
|
||||
return (
|
||||
<Button aria-label="Toggle theme" className="rounded-full" onPress={() => toggleTheme()}>
|
||||
<VscColorMode aria-hidden size={24} />
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
102
apps/website/src/components/ui/VersionSelect.tsx
Normal file
102
apps/website/src/components/ui/VersionSelect.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
'use client';
|
||||
|
||||
import { ChevronsUpDown } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import type { Key } from 'react-aria-components';
|
||||
import { useMediaQuery } from 'usehooks-ts';
|
||||
import { Drawer as Vaul } from 'vaul';
|
||||
import { Button } from './Button';
|
||||
import { ListBox, ListBoxItem } from './ListBox';
|
||||
import { Popover } from './Popover';
|
||||
import { Select, SelectValue } from './Select';
|
||||
|
||||
export function VersionSelect({
|
||||
packageName,
|
||||
version,
|
||||
versions,
|
||||
}: {
|
||||
readonly packageName: string;
|
||||
readonly version: string;
|
||||
readonly versions: { readonly version: string }[];
|
||||
}) {
|
||||
const [selectedVersion, setSelectedVersion] = useState<Key>(version);
|
||||
const [open, setOpen] = useState(false);
|
||||
const isMedium = useMediaQuery('(min-width: 768px)');
|
||||
|
||||
useEffect(() => {
|
||||
if (isMedium) {
|
||||
setOpen(false);
|
||||
}
|
||||
}, [isMedium, setOpen]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Select
|
||||
aria-label="Select a version"
|
||||
className="hidden md:block"
|
||||
selectedKey={selectedVersion}
|
||||
onSelectionChange={(selected) => {
|
||||
setSelectedVersion(selected);
|
||||
}}
|
||||
>
|
||||
<Button className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800">
|
||||
<SelectValue className="font-medium" />
|
||||
<ChevronsUpDown aria-hidden size={20} />
|
||||
</Button>
|
||||
<Popover className="max-h-60 w-[--trigger-width] overflow-auto rounded-md border border-neutral-300 bg-neutral-200 dark:border-neutral-700 dark:bg-neutral-800">
|
||||
<ListBox shouldFocusWrap items={versions}>
|
||||
{(item) => (
|
||||
<ListBoxItem
|
||||
id={item.version}
|
||||
textValue={item.version}
|
||||
href={`/docs/packages/${packageName}/${item.version}`}
|
||||
className="flex p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
|
||||
>
|
||||
{item.version}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</ListBox>
|
||||
</Popover>
|
||||
</Select>
|
||||
|
||||
<Vaul.NestedRoot open={open} onOpenChange={setOpen} dismissible={false}>
|
||||
<Vaul.Trigger
|
||||
aria-label="Open version select"
|
||||
className="flex w-full place-content-between place-items-center rounded-md bg-neutral-200 p-2 dark:bg-neutral-800 md:hidden"
|
||||
>
|
||||
<span className="font-medium">{selectedVersion}</span>
|
||||
<ChevronsUpDown aria-hidden size={20} />
|
||||
</Vaul.Trigger>
|
||||
<Vaul.Portal>
|
||||
<Vaul.Overlay className="fixed inset-0 bg-black/40" />
|
||||
<Vaul.Content className="fixed bottom-0 left-0 right-0 flex max-h-[80%] flex-col rounded-t-lg bg-neutral-100 p-4 dark:bg-neutral-900">
|
||||
<div className="mx-auto mb-8 h-1.5 w-12 flex-shrink-0 rounded-full bg-neutral-400" />
|
||||
<ListBox
|
||||
aria-label="Select a version"
|
||||
className="flex flex-col gap-2 overflow-auto"
|
||||
shouldFocusWrap
|
||||
items={versions}
|
||||
selectionMode="single"
|
||||
selectedKeys={[selectedVersion]}
|
||||
onSelectionChange={(selected) => {
|
||||
const [val] = selected;
|
||||
setSelectedVersion(val as Key);
|
||||
}}
|
||||
>
|
||||
{(item) => (
|
||||
<ListBoxItem
|
||||
id={item.version}
|
||||
textValue={item.version}
|
||||
href={`/docs/packages/${packageName}/${item.version}`}
|
||||
className="rounded-md p-2 outline-none data-[focus-visible]:bg-neutral-300 data-[hovered]:bg-neutral-300 data-[selected]:bg-blurple data-[selected]:data-[focus-visible]:bg-blurple-500 data-[selected]:data-[hovered]:bg-blurple-500 data-[selected]:text-white dark:data-[focus-visible]:bg-neutral-700 dark:data-[hovered]:bg-neutral-700 dark:data-[selected]:data-[focus-visible]:bg-blurple-500 dark:data-[selected]:data-[hovered]:bg-blurple-500"
|
||||
>
|
||||
{item.version}
|
||||
</ListBoxItem>
|
||||
)}
|
||||
</ListBox>
|
||||
</Vaul.Content>
|
||||
</Vaul.Portal>
|
||||
</Vaul.NestedRoot>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user