refactor: docs (#10126)

This commit is contained in:
Noel
2024-02-29 04:37:52 +01:00
committed by GitHub
parent 0f9017ef95
commit 18cce83d80
192 changed files with 8116 additions and 6321 deletions

View File

@@ -1,9 +0,0 @@
import { FiLink } from '@react-icons/all-files/fi/FiLink';
export function Anchor({ href }: { readonly href: string }) {
return (
<a className="mr-1 inline-block rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple" href={href}>
<FiLink size={20} />
</a>
);
}

View File

@@ -1,42 +1,38 @@
import type { ApiDocumentedItem } from '@discordjs/api-extractor-model';
import { ApiAbstractMixin, ApiProtectedMixin, ApiReadonlyMixin, ApiStaticMixin } from '@discordjs/api-extractor-model';
import { AlertTriangle } from 'lucide-react';
import type { PropsWithChildren } from 'react';
export enum BadgeColor {
Danger = 'bg-red-500',
Primary = 'bg-blurple',
Warning = 'bg-yellow-500',
}
export function Badge({
children,
color = BadgeColor.Primary,
}: PropsWithChildren<{ readonly color?: BadgeColor | undefined }>) {
export function Badge({ children, className = '' }: PropsWithChildren<{ readonly className?: string }>) {
return (
<span
className={`h-5 flex flex-row place-content-center place-items-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white ${color}`}
className={`inline-flex place-items-center gap-1 rounded-full px-2 py-1 font-sans text-sm font-normal leading-none ${className}`}
>
{children}
</span>
);
}
export function Badges({ item }: { readonly item: ApiDocumentedItem }) {
const isStatic = ApiStaticMixin.isBaseClassOf(item) && item.isStatic;
const isProtected = ApiProtectedMixin.isBaseClassOf(item) && item.isProtected;
const isReadonly = ApiReadonlyMixin.isBaseClassOf(item) && item.isReadonly;
const isAbstract = ApiAbstractMixin.isBaseClassOf(item) && item.isAbstract;
const isDeprecated = Boolean(item.tsdocComment?.deprecatedBlock);
export async function Badges({ node }: { readonly node: any }) {
const isDeprecated = Boolean(node.summary?.deprecatedBlock?.length);
const isProtected = node.isProtected;
const isStatic = node.isStatic;
const isAbstract = node.isAbstract;
const isReadonly = node.isReadonly;
const isOptional = node.isOptional;
const isAny = isStatic || isProtected || isReadonly || isAbstract || isDeprecated;
const isAny = isDeprecated || isProtected || isStatic || isAbstract || isReadonly || isOptional;
return isAny ? (
<div className="flex flex-row gap-1 md:ml-7">
{isDeprecated ? <Badge color={BadgeColor.Danger}>Deprecated</Badge> : null}
{isProtected ? <Badge>Protected</Badge> : null}
{isStatic ? <Badge>Static</Badge> : null}
{isAbstract ? <Badge>Abstract</Badge> : null}
{isReadonly ? <Badge>Readonly</Badge> : null}
<div className="mb-1 flex gap-3">
{isDeprecated ? (
<Badge className="bg-red-500/20 text-red-500">
<AlertTriangle aria-hidden size={14} /> deprecated
</Badge>
) : null}
{isProtected ? <Badge className="bg-purple-500/20 text-purple-500">protected</Badge> : null}
{isStatic ? <Badge className="bg-purple-500/20 text-purple-500">static</Badge> : null}
{isAbstract ? <Badge className="bg-cyan-500/20 text-cyan-500">abstract</Badge> : null}
{isReadonly ? <Badge className="bg-purple-500/20 text-purple-500">readonly</Badge> : null}
{isOptional ? <Badge className="bg-cyan-500/20 text-cyan-500">optional</Badge> : null}
</div>
) : null;
}

View File

@@ -1,139 +0,0 @@
'use client';
import type { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscArrowRight } from '@react-icons/all-files/vsc/VscArrowRight';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import { Dialog } from 'ariakit/dialog';
import { Command } from 'cmdk';
import { usePathname, useRouter } from 'next/navigation';
import { useEffect, useMemo, useState } from 'react';
import { useKey } from 'react-use';
import { useCmdK } from '~/contexts/cmdK';
import { client } from '~/util/search';
function resolveIcon(item: keyof typeof ApiItemKind) {
switch (item) {
case 'Class':
return <VscSymbolClass className="shrink-0" size={25} />;
case 'Enum':
return <VscSymbolEnum className="shrink-0" size={25} />;
case 'Interface':
return <VscSymbolInterface className="shrink-0" size={25} />;
case 'Property':
return <VscSymbolProperty className="shrink-0" size={25} />;
case 'TypeAlias':
return <VscSymbolVariable className="shrink-0" size={25} />;
case 'Variable':
return <VscSymbolVariable className="shrink-0" size={25} />;
case 'Event':
return <VscSymbolEvent className="shrink-0" size={25} />;
default:
return <VscSymbolMethod className="shrink-0" size={25} />;
}
}
export function CmdKDialog() {
const pathname = usePathname();
const router = useRouter();
const dialog = useCmdK();
const [search, setSearch] = useState('');
const [searchResults, setSearchResults] = useState<any[]>([]);
const packageName = pathname?.split('/').slice(3, 4)[0];
const branchName = pathname?.split('/').slice(4, 5)[0];
const searchResultItems = useMemo(
() =>
searchResults?.map((item, idx) => (
<Command.Item
className="my-1 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-content-center rounded bg-transparent px-4 py-2 text-base text-black font-semibold leading-none outline-none active:translate-y-px dark:border-dark-100 active:bg-neutral-200 hover:bg-neutral-100 dark:text-white [&[aria-selected]]:ring [&[aria-selected]]:ring-width-2 [&[aria-selected]]:ring-blurple dark:active:bg-dark-200 dark:hover:bg-dark-300"
key={`${item.id}-${idx}`}
onSelect={() => {
router.push(item.path);
dialog!.setOpen(false);
}}
value={`${item.id}`}
>
<div className="flex grow flex-row place-content-between place-items-center gap-4">
<div className="flex flex-row place-items-center gap-4">
{resolveIcon(item.kind)}
<div className="w-50 flex flex-col sm:w-100">
<h2 className="font-semibold">{item.name}</h2>
<div className="line-clamp-1 text-sm font-normal">{item.summary}</div>
<div className="line-clamp-1 hidden text-xs font-light opacity-75 sm:block dark:opacity-50">
{item.path}
</div>
</div>
</div>
<VscArrowRight className="shrink-0" size={20} />
</div>
</Command.Item>
)) ?? [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[searchResults],
);
useKey(
(event) => {
if (event.key === 'k' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
return true;
}
return false;
},
dialog!.toggle,
{ event: 'keydown', options: {} },
[],
);
useEffect(() => {
if (!dialog!.open) {
setSearch('');
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dialog!.open]);
useEffect(() => {
const searchDoc = async (searchString: string, version: string) => {
const res = await client
.index(`${packageName?.replaceAll('.', '-')}-${version}`)
.search(searchString, { limit: 5 });
setSearchResults(res.hits);
};
if (search && packageName) {
void searchDoc(search, branchName?.replaceAll('.', '-') ?? 'main');
} else {
setSearchResults([]);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [search]);
return (
<Dialog className="fixed left-1/2 top-1/4 z-50 -translate-x-1/2" state={dialog!}>
<Command
className="max-w-xs min-w-xs border border-light-900 rounded bg-white/50 shadow backdrop-blur-md sm:max-w-lg sm:min-w-lg dark:border-dark-100 dark:bg-dark/50"
label="Command Menu"
shouldFilter={false}
>
<Command.Input
className="w-full border-0 border-b border-light-900 rounded rounded-b-0 bg-white/50 p-4 text-lg caret-blurple outline-none dark:border-dark-100 dark:bg-dark/50 placeholder:text-dark-300/75 dark:placeholder:text-white/75"
onValueChange={setSearch}
placeholder="Quick search..."
value={search}
/>
<Command.List className="pt-0">
<Command.Empty className="p-4 text-center">No results found</Command.Empty>
{search ? searchResultItems : null}
</Command.List>
</Command>
</Dialog>
);
}

View File

@@ -1,40 +0,0 @@
import type { ReactNode } from 'react';
import { Anchor } from './Anchor';
import { SourceLink } from './documentation/SourceLink';
export interface CodeListingProps {
/**
* The value of this heading.
*/
readonly children: ReactNode;
/**
* Additional class names to apply to the root element.
*/
readonly className?: string | undefined;
/**
* The href of this heading.
*/
readonly href?: string | undefined;
/**
* The line in the source code where this part is declared
*/
readonly sourceLine?: number | undefined;
/**
* The URL of the source code of this code part
*/
readonly sourceURL?: string | undefined;
}
export function CodeHeading({ href, className, children, sourceURL, sourceLine }: CodeListingProps) {
return (
<div className="flex flex-row place-items-center justify-between gap-1">
<div
className={`flex flex-row flex-wrap place-items-center gap-1 break-all font-mono text-lg font-bold ${className}`}
>
{href ? <Anchor href={href} /> : null}
{children}
</div>
{sourceURL ? <SourceLink className="text-2xl" sourceLine={sourceLine} sourceURL={sourceURL} /> : null}
</div>
);
}

View File

@@ -0,0 +1,48 @@
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { ParameterNode } from './ParameterNode';
import { SummaryNode } from './SummaryNode';
export async function ConstructorNode({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} />
Constructors
</h2>
<div className="flex place-content-between place-items-center">
<h3 id="constructor" className="group scroll-mt-8 break-words font-mono font-semibold">
{/* constructor({parsedContent.constructor.parametersString}) */}
<Link href="#constructor" className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block">
<LinkIcon aria-hidden size={16} />
</Link>
constructor({node.parameters?.length ? <ParameterNode node={node.parameters} version={version} /> : null})
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={node.sourceLine ? `${node.sourceURL}#L${node.sourceLine}` : node.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{node.summary?.summarySection.length ? (
<SummaryNode padding node={node.summary.summarySection} version={version} />
) : null}
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
);
}

View File

@@ -0,0 +1,18 @@
import { DocNode } from './DocNode';
import { Alert } from './ui/Alert';
export async function DeprecatedNode({
deprecatedBlock,
version,
}: {
readonly deprecatedBlock: any;
readonly version: string;
}) {
return (
<Alert title="Deprecated" type="danger">
<p className="break-words">
<DocNode node={deprecatedBlock} version={version} />
</p>
</Alert>
);
}

View File

@@ -0,0 +1,134 @@
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { ConstructorNode } from './ConstructorNode';
import { DeprecatedNode } from './DeprecatedNode';
import { EnumMemberNode } from './EnumMemberNode';
import { EventNode } from './EventNode';
import { InformationNode } from './InformationNode';
import { MethodNode } from './MethodNode';
import { Outline } from './Outline';
import { OverlayScrollbarsComponent } from './OverlayScrollbars';
import { ParameterNode } from './ParameterNode';
import { PropertyNode } from './PropertyNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { SyntaxHighlighter } from './SyntaxHighlighter';
import { TypeParameterNode } from './TypeParameterNode';
import { UnionMember } from './UnionMember';
import { Tab, TabList, TabPanel, Tabs } from './ui/Tabs';
async function OverloadNode({ node, packageName, version }: { node: any; packageName: string; version: string }) {
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2">
{node.overloads.map((overload: any) => {
return (
<Tab
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
>
<span>Overload {overload.overloadIndex}</span>
</Tab>
);
})}
</TabList>
{node.overloads.map((overload: any) => {
return (
<TabPanel
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
className="flex flex-col gap-8"
>
<DocItem node={overload} packageName={packageName} version={version} />
</TabPanel>
);
})}
</Tabs>
);
}
export function DocItem({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
if (node.overloads?.length) {
return <OverloadNode node={node} packageName={packageName} version={version} />;
}
return (
<>
<InformationNode node={node} version={version} />
<OverlayScrollbarsComponent
defer
options={{
overflow: { y: 'hidden' },
scrollbars: { autoHide: 'scroll', autoHideDelay: 500, autoHideSuspend: true, clickScroll: true },
}}
className="rounded-md border border-neutral-300 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900"
>
<SyntaxHighlighter className="py-4 text-sm" lang="typescript" code={node.sourceExcerpt} />
</OverlayScrollbarsComponent>
{node.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={node.summary.deprecatedBlock} version={version} />
) : null}
{node.summary?.summarySection ? <SummaryNode node={node.summary.summarySection} version={version} /> : null}
{node.summary?.returnsBlock.length ? <ReturnNode node={node.summary.returnsBlock} version={version} /> : null}
{node.summary?.seeBlocks.length ? <SeeNode node={node.summary.seeBlocks} version={version} /> : null}
<Outline node={node} />
{node.constructor?.parametersString ? <ConstructorNode node={node.constructor} version={version} /> : null}
{node.typeParameters?.length ? (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} />
Type Parameters
</h2>
<TypeParameterNode description node={node.typeParameters} version={version} />
</div>
) : null}
{node.parameters?.length ? (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolParameter aria-hidden className="flex-shrink-0" size={24} />
Parameters
</h2>
<ParameterNode description node={node.parameters} version={version} />
</div>
) : null}
{node.members?.properties?.length ? (
<PropertyNode node={node.members.properties} packageName={packageName} version={version} />
) : null}
{node.members?.events?.length ? (
<div>
<EventNode node={node.members.events} packageName={packageName} version={version} />
</div>
) : null}
{node.members?.methods?.length ? (
<div>
<MethodNode node={node.members.methods} packageName={packageName} version={version} />
</div>
) : null}
{node.members?.length ? <EnumMemberNode node={node.members} packageName={packageName} version={version} /> : null}
{node.unionMembers?.length ? <UnionMember node={node.unionMembers} version={version} /> : null}
</>
);
}

View File

@@ -0,0 +1,44 @@
export function resolveNodeKind(kind: string) {
switch (kind) {
case 'Class':
return {
text: 'text-green-500',
background: 'bg-green-500/20',
};
case 'Interface':
return {
text: 'text-amber-500',
background: 'bg-amber-500/20',
};
case 'Function':
return {
text: 'text-blue-500',
background: 'bg-blue-500/20',
};
case 'Enum':
return {
text: 'text-rose-500',
background: 'bg-rose-500/20',
};
case 'TypeAlias':
return {
text: 'text-pink-500',
background: 'bg-pink-500/20',
};
case 'Variable':
return {
text: 'text-purple-500',
background: 'bg-purple-500/20',
};
default:
return {
text: 'text-gray-500',
background: 'bg-gray-500/20',
};
}
}
export async function DocKind({ background = false, node }: { readonly background?: boolean; readonly node: any }) {
const kind = resolveNodeKind(node.kind);
return <span className={background ? `${kind.background} ${kind.text}` : kind.text}>{node.kind.toLowerCase()}</span>;
}

View File

@@ -0,0 +1,72 @@
import Link from 'next/link';
import { OverlayScrollbarsComponent } from './OverlayScrollbars';
import { SyntaxHighlighter } from './SyntaxHighlighter';
export async function DocNode({ node, version }: { readonly node?: any; readonly version: string }) {
const createNode = (node: any, idx: number) => {
switch (node.kind) {
case 'PlainText':
return <span key={`${node.text}-${idx}`}>{node.text}</span>;
case 'LinkTag': {
if (node.resolvedPackage) {
return (
<Link
key={`${node.text}-${idx}`}
className="font-mono text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={`/docs/packages/${node.resolvedPackage.packageName}/${version}/${node.uri}`}
>
{node.text}
</Link>
);
}
if (node.uri) {
return (
<a
key={`${node.text}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={node.uri}
rel="external noreferrer noopener"
target="_blank"
>
{node.text}
</a>
);
}
return <span key={`${node.text}-${idx}`}>{node.text}</span>;
}
case 'CodeSpan':
return (
<code key={`${node.text}-${idx}`} className="font-mono text-sm">
{node.text}
</code>
);
case 'FencedCode': {
const { language, text } = node;
return (
<OverlayScrollbarsComponent
defer
options={{
overflow: { y: 'hidden' },
scrollbars: { autoHide: 'scroll', autoHideDelay: 500, autoHideSuspend: true, clickScroll: true },
}}
className="my-4 rounded-md border border-neutral-300 bg-neutral-100 dark:border-neutral-700 dark:bg-neutral-900"
>
<SyntaxHighlighter className="py-4 text-sm " lang={language} code={text} />
</OverlayScrollbarsComponent>
);
}
case 'SoftBreak':
return null;
default:
return null;
}
};
return node?.map(createNode) ?? null;
}

View File

@@ -1,9 +0,0 @@
import type { PropsWithChildren } from 'react';
export function DocumentationLink({ children, href }: PropsWithChildren<{ readonly href: string }>) {
return (
<a className="text-blurple" href={href} rel="external noreferrer noopener" target="_blank">
{children}
</a>
);
}

View File

@@ -0,0 +1,109 @@
import { VscSymbolEnumMember } from '@react-icons/all-files/vsc/VscSymbolEnumMember';
import { Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode';
import { ExcerptNode } from './ExcerptNode';
import { InheritedFromNode } from './InheritedFromNode';
import { ParameterNode } from './ParameterNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
export async function EnumMemberNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">
<VscSymbolEnumMember aria-hidden className="flex-shrink-0" size={24} />
Members
</h2>
<div className="flex flex-col gap-8">
{node.map((enumMember: any, idx: number) => {
return (
<Fragment key={`${enumMember.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3 id={enumMember.displayName} className="group scroll-mt-8 break-words font-mono font-semibold">
<Badges node={enumMember} />
<span>
<Link
href={`#${enumMember.displayName}`}
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block"
>
<LinkIcon aria-hidden size={16} />
</Link>
{enumMember.displayName}
{enumMember.parameters?.length ? (
<ParameterNode node={enumMember.parameters} version={version} />
) : null}
{enumMember.initializerExcerpt ? (
<>
{' = '}
<ExcerptNode node={enumMember.initializerExcerpt} version={version} />
</>
) : null}
</span>
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={
enumMember.sourceLine ? `${enumMember.sourceURL}#L${enumMember.sourceLine}` : enumMember.sourceURL
}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{enumMember.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={enumMember.summary.deprecatedBlock} version={version} />
) : null}
{enumMember.summary?.summarySection.length ? (
<SummaryNode padding node={enumMember.summary.summarySection} version={version} />
) : null}
{enumMember.summary?.exampleBlocks.length ? (
<ExampleNode node={enumMember.summary.exampleBlocks} version={version} />
) : null}
{enumMember.summary?.returnsBlock.length ? (
<ReturnNode padding node={enumMember.summary.returnsBlock} version={version} />
) : null}
{enumMember.inheritedFrom ? (
<InheritedFromNode node={enumMember.inheritedFrom} packageName={packageName} version={version} />
) : null}
{enumMember.summary?.seeBlocks.length ? (
<SeeNode padding node={enumMember.summary.seeBlocks} version={version} />
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</Fragment>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,170 @@
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { ChevronDown, ChevronUp, Code2 } from 'lucide-react';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode';
import { InheritedFromNode } from './InheritedFromNode';
import { ParameterNode } from './ParameterNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { TypeParameterNode } from './TypeParameterNode';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { Tab, TabList, TabPanel, Tabs } from './ui/Tabs';
async function EventBodyNode({
event,
packageName,
version,
overload = false,
}: {
readonly event: any;
readonly overload?: boolean;
readonly packageName: string;
readonly version: string;
}) {
return (
<>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3
id={event.displayName}
className={`${overload ? 'scroll-mt-16' : 'scroll-mt-8'} break-words font-mono font-semibold`}
>
<Badges node={event} /> {event.displayName}
{event.typeParameters?.length ? (
<>
{'<'}
<TypeParameterNode node={event.typeParameters} version={version} />
{'>'}
</>
) : null}
({event.parameters?.length ? <ParameterNode node={event.parameters} version={version} /> : null})
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={event.sourceLine ? `${event.sourceURL}#L${event.sourceLine}` : event.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{event.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={event.summary.deprecatedBlock} version={version} />
) : null}
{event.summary?.summarySection.length ? (
<SummaryNode padding node={event.summary.summarySection} version={version} />
) : null}
{event.summary?.exampleBlocks.length ? (
<ExampleNode node={event.summary.exampleBlocks} version={version} />
) : null}
{event.summary?.returnsBlock.length ? (
<ReturnNode padding node={event.summary.returnsBlock} version={version} />
) : null}
{event.inheritedFrom ? (
<InheritedFromNode node={event.inheritedFrom} packageName={packageName} version={version} />
) : null}
{event.summary?.seeBlocks.length ? <SeeNode padding node={event.summary.seeBlocks} version={version} /> : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</>
);
}
async function OverloadNode({
event,
packageName,
version,
}: {
readonly event: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2">
{event.overloads.map((overload: any) => {
return (
<Tab
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
>
<span>Overload {overload.overloadIndex}</span>
</Tab>
);
})}
</TabList>
{event.overloads.map((overload: any) => {
return (
<TabPanel
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
className="flex flex-col gap-8"
>
<EventBodyNode overload event={overload} packageName={packageName} version={version} />
</TabPanel>
);
})}
</Tabs>
);
}
export async function EventNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} /> Events
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-8">
{node.map((event: any) => {
return event.overloads?.length ? (
<OverloadNode
key={`${event.displayName}-${event.overloadIndex}`}
event={event}
packageName={packageName}
version={version}
/>
) : (
<EventBodyNode
key={`${event.displayName}-${event.overloadIndex}`}
event={event}
packageName={packageName}
version={version}
/>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -0,0 +1,10 @@
import { DocNode } from './DocNode';
export async function ExampleNode({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="break-words pl-4">
<span className="font-semibold">Examples:</span>
<DocNode node={node} version={version} />
</div>
);
}

View File

@@ -0,0 +1,66 @@
import Link from 'next/link';
import { Fragment } from 'react';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
export async function ExcerptNode({ node, version }: { readonly node?: any; readonly version: string }) {
const createExcerpt = (excerpts: any) => {
const excerpt = Array.isArray(excerpts) ? excerpts : excerpts.excerpts ?? [excerpts];
return (
<span
className={
excerpts?.type === 'Extends' || excerpts?.type === 'Implements'
? 'after:content-[",_"] last-of-type:after:content-none'
: ''
}
>
{excerpt.map((excerpt: any, idx: number) => {
if (excerpt.resolvedItem) {
return (
<Link
key={`${excerpt.resolvedItem.displayName}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={`/docs/packages/${excerpt.resolvedItem.packageName}/${version}/${excerpt.resolvedItem.uri}`}
>
{excerpt.text}
</Link>
);
}
if (excerpt.href) {
return (
<a
key={`${excerpt.text}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={excerpt.href}
rel="external noreferrer noopener"
target="_blank"
>
{excerpt.text}
</a>
);
}
if (excerpt.text in BuiltinDocumentationLinks) {
const href = BuiltinDocumentationLinks[excerpt.text as keyof typeof BuiltinDocumentationLinks];
return (
<a
key={`${excerpt.text}-${idx}`}
className="text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={href}
rel="external noreferrer noopener"
target="_blank"
>
{excerpt.text}
</a>
);
}
return <Fragment key={`${excerpt.text}-${idx}`}>{excerpt.text}</Fragment>;
})}
</span>
);
};
return node?.map(createExcerpt) ?? null;
}

View File

@@ -1,86 +0,0 @@
import type { ApiPackage, Excerpt } from '@discordjs/api-extractor-model';
import { ExcerptTokenKind } from '@discordjs/api-extractor-model';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
import { DISCORD_API_TYPES_DOCS_URL } from '~/util/constants';
import { DocumentationLink } from './DocumentationLink';
import { ItemLink } from './ItemLink';
import { resolveCanonicalReference, resolveItemURI } from './documentation/util';
export interface ExcerptTextProps {
/**
* The package this excerpt is referenced from.
*/
readonly apiPackage: ApiPackage;
/**
* The tokens to render.
*/
readonly excerpt: Excerpt;
}
/**
* A component that renders excerpt tokens from an api item.
*/
export function ExcerptText({ excerpt, apiPackage }: ExcerptTextProps) {
return (
<span>
{excerpt.spannedTokens.map((token, idx) => {
if (token.kind === ExcerptTokenKind.Reference) {
if (token.text in BuiltinDocumentationLinks) {
const href = BuiltinDocumentationLinks[token.text as keyof typeof BuiltinDocumentationLinks];
return (
<DocumentationLink key={`${token.text}-${idx}`} href={href}>
{token.text}
</DocumentationLink>
);
}
const source = token.canonicalReference?.source;
const symbol = token.canonicalReference?.symbol;
if (source && 'packageName' in source && source.packageName === 'discord-api-types' && symbol) {
const { meaning, componentPath: path } = symbol;
let href = DISCORD_API_TYPES_DOCS_URL;
// dapi-types doesn't have routes for class members
// so we can assume this member is for an enum
if (meaning === 'member' && path && 'parent' in path) {
href += `/enum/${path.parent}#${path.component}`;
} else if (meaning === 'type' || meaning === 'var') {
href += `#${token.text}`;
} else {
href += `/${meaning}/${token.text}`;
}
return (
<DocumentationLink key={`${token.text}-${idx}`} href={href}>
{token.text}
</DocumentationLink>
);
}
const resolved = token.canonicalReference
? resolveCanonicalReference(token.canonicalReference, apiPackage)
: null;
if (!resolved) {
return token.text;
}
return (
<ItemLink
className="text-blurple"
itemURI={resolveItemURI(resolved.item)}
key={`${resolved.item.displayName}-${resolved.item.containerKey}-${idx}`}
packageName={resolved.package}
version={resolved.version}
>
{token.text}
</ItemLink>
);
}
return token.text.replace(/import\("discord-api-types(?:\/v\d+)?"\)\./, '');
})}
</span>
);
}

View File

@@ -1,116 +0,0 @@
'use client';
import { FiCommand } from '@react-icons/all-files/fi/FiCommand';
import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
import { VscMenu } from '@react-icons/all-files/vsc/VscMenu';
import { VscSearch } from '@react-icons/all-files/vsc/VscSearch';
import { Button } from 'ariakit/button';
import type { Route } from 'next';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { Fragment, useMemo } from 'react';
import { useCmdK } from '~/contexts/cmdK';
import { useNav } from '~/contexts/nav';
const ThemeSwitcher = dynamic(async () => import('./ThemeSwitcher'));
export default function Header() {
const pathname = usePathname();
const { setOpened } = useNav();
const dialog = useCmdK();
const pathElements = useMemo(
() =>
pathname
.split('/')
.slice(1)
.map((path, idx, original) => (
<Link
className="rounded outline-none hover:underline focus:ring focus:ring-width-2 focus:ring-blurple"
href={`/${original.slice(0, idx + 1).join('/')}` as Route}
key={`${path}-${idx}`}
>
{path}
</Link>
)),
[pathname],
);
const breadcrumbs = useMemo(
() =>
pathElements.flatMap((el, idx, array) => {
if (idx === 0) {
return (
<Fragment key={`${el.key}-${idx}`}>
<div className="mx-2">/</div>
<div>{el}</div>
<div className="mx-2">/</div>
</Fragment>
);
}
if (idx !== array.length - 1) {
return (
<Fragment key={`${el.key}-${idx}`}>
<div>{el}</div>
<div className="mx-2">/</div>
</Fragment>
);
}
return <div key={`${el.key}-${idx}`}>{el}</div>;
}),
[pathElements],
);
return (
<header className="sticky top-4 z-20 border border-light-900 rounded-md bg-white/75 shadow backdrop-blur-md dark:border-dark-100 dark:bg-dark-600/75">
<div className="block h-16 px-6">
<div className="h-full flex flex-row place-content-between place-items-center gap-8">
<Button
aria-label="Menu"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none lg:hidden active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => setOpened((open) => !open)}
>
<VscMenu size={24} />
</Button>
<div className="hidden lg:flex lg:grow lg:flex-row lg:overflow-hidden">{breadcrumbs}</div>
<Button
as="div"
className="hidden w-56 grow rounded bg-white px-4 py-2.5 outline-none md:block sm:grow-0 dark:bg-dark-800 focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => dialog?.toggle()}
>
<div className="flex flex-row place-items-center gap-4 md:justify-between">
<VscSearch size={18} />
<span className="opacity-65">Search...</span>
<div className="hidden md:flex md:flex-row md:place-items-center md:gap-2 md:opacity-65">
<FiCommand size={18} /> K
</div>
</div>
</Button>
<div className="flex flex-row place-items-center gap-4">
<Button
as="div"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none md:hidden active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => dialog?.toggle()}
>
<VscSearch size={24} />
</Button>
<Button
aria-label="GitHub"
as="a"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded rounded-full bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer"
target="_blank"
>
<VscGithubInverted size={24} />
</Button>
<ThemeSwitcher />
</div>
</div>
</div>
</header>
);
}

View File

@@ -0,0 +1,33 @@
import { FileCode2 } from 'lucide-react';
import { Badges } from './Badges';
import { DocKind } from './DocKind';
import { InheritanceNode } from './InheritanceNode';
export async function InformationNode({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="flex place-content-between place-items-center">
<div className="flex flex-col gap-1">
<h1 className="text-xl">
<DocKind node={node} /> <span className="break-words font-bold">{node.displayName}</span>
</h1>
{node.implements ? <InheritanceNode text="implements" node={node.implements} version={version} /> : null}
{node.extends ? <InheritanceNode text="extends" node={node.extends} version={version} /> : null}
<Badges node={node} />
</div>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={node.sourceLine ? `${node.sourceURL}#L${node.sourceLine}` : node.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<FileCode2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
);
}

View File

@@ -0,0 +1,20 @@
import { ExcerptNode } from './ExcerptNode';
export async function InheritanceNode({
text,
node,
version,
}: {
readonly node: any;
readonly text: string;
readonly version: string;
}) {
return (
<div>
<h2 className="inline-block min-w-min text-sm italic text-neutral-500 dark:text-neutral-400">{text}</h2>{' '}
<span className="break-words font-mono text-sm">
<ExcerptNode node={node} version={version} />
</span>
</div>
);
}

View File

@@ -1,17 +0,0 @@
import type { ApiDeclaredItem } from '@discordjs/api-extractor-model';
import { ItemLink } from './ItemLink';
import { resolveItemURI } from './documentation/util';
export function InheritanceText({ parent }: { readonly parent: ApiDeclaredItem }) {
return (
<span className="font-semibold">
Inherited from{' '}
<ItemLink
className="rounded text-blurple font-mono outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
itemURI={resolveItemURI(parent)}
>
{parent.displayName}
</ItemLink>
</span>
);
}

View File

@@ -0,0 +1,23 @@
import Link from 'next/link';
export async function InheritedFromNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<p className="break-words pl-4">
<span className="font-semibold">Inherited from:</span>{' '}
<Link
className="font-mono text-blurple hover:text-blurple-500 dark:hover:text-blurple-300"
href={`/docs/packages/${packageName}/${version}/${node}`}
>
{node.slice(0, node.indexOf(':'))}
</Link>
</p>
);
}

View File

@@ -1,38 +0,0 @@
'use client';
import { FiCheck } from '@react-icons/all-files/fi/FiCheck';
import { FiCopy } from '@react-icons/all-files/fi/FiCopy';
import { useEffect, useState } from 'react';
import { useCopyToClipboard } from 'react-use';
import { buttonVariants } from '~/styles/Button';
export function InstallButton() {
const [interacted, setInteracted] = useState(false);
const [state, copyToClipboard] = useCopyToClipboard();
useEffect(() => {
const timer = setTimeout(() => setInteracted(false), 2_000);
return () => clearTimeout(timer);
}, [interacted]);
return (
<button
className={buttonVariants({
variant: 'secondary',
className: 'cursor-copy font-mono',
})}
onClick={() => {
setInteracted(true);
copyToClipboard('npm install discord.js');
}}
type="button"
>
<span className="text-blurple font-semibold">{'>'}</span> npm install discord.js{' '}
{state.value && interacted ? (
<FiCheck className="ml-1 inline-block text-green-500" />
) : (
<FiCopy className="ml-1 inline-block" />
)}
</button>
);
}

View File

@@ -1,48 +0,0 @@
'use client';
import type { LinkProps } from 'next/link';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { useCurrentPathMeta } from '~/hooks/useCurrentPathMeta';
export interface ItemLinkProps<Route extends string> extends Omit<LinkProps<Route>, 'href'> {
readonly className?: string;
/**
* The URI of the api item to link to. (e.g. `/RestManager`)
*/
readonly itemURI: string;
/**
* The name of the package the item belongs to.
*/
readonly packageName?: string | undefined;
// TODO: This needs to be properly typed above but monkey-patching it for now.
readonly title?: string | undefined;
/**
* The version of the package the item belongs to.
*/
readonly version?: string | undefined;
}
/**
* A component that renders a link to an api item.
*
* @remarks
* This component only needs the relative path to the item, and will automatically
* generate the full path to the item client-side.
*/
export function ItemLink<Route extends string>(props: PropsWithChildren<ItemLinkProps<Route>>) {
const pathname = usePathname();
const { packageName, version } = useCurrentPathMeta();
if (!pathname) {
throw new Error('ItemLink must be used inside a Next.js page. (e.g. /docs/packages/foo/main)');
}
const { itemURI, packageName: pkgName, version: pkgVersion, ...linkProps } = props;
return <Link {...linkProps} href={`/docs/packages/${pkgName ?? packageName}/${pkgVersion ?? version}/${itemURI}`} />;
}

View File

@@ -0,0 +1,180 @@
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExampleNode } from './ExampleNode';
import { ExcerptNode } from './ExcerptNode';
import { InheritedFromNode } from './InheritedFromNode';
import { ParameterNode } from './ParameterNode';
import { ReturnNode } from './ReturnNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { TypeParameterNode } from './TypeParameterNode';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { Tab, TabList, TabPanel, Tabs } from './ui/Tabs';
async function MethodBodyNode({
method,
packageName,
version,
overload = false,
}: {
readonly method: any;
readonly overload?: boolean;
readonly packageName: string;
readonly version: string;
}) {
return (
<>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3
id={method.displayName}
className={`${overload ? 'scroll-mt-16' : 'scroll-mt-8'} group break-words font-mono font-semibold`}
>
<Badges node={method} /> {method.displayName}
<span>
<Link href={`#${method.displayName}`} className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block">
<LinkIcon aria-hidden size={16} />
</Link>
{method.typeParameters?.length ? (
<>
{'<'}
<TypeParameterNode node={method.typeParameters} version={version} />
{'>'}
</>
) : null}
({method.parameters?.length ? <ParameterNode node={method.parameters} version={version} /> : null}
) : <ExcerptNode node={method.returnTypeExcerpt} version={version} />
</span>
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={method.sourceLine ? `${method.sourceURL}#L${method.sourceLine}` : method.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{method.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={method.summary.deprecatedBlock} version={version} />
) : null}
{method.summary?.summarySection.length ? (
<SummaryNode padding node={method.summary.summarySection} version={version} />
) : null}
{method.summary?.exampleBlocks.length ? (
<ExampleNode node={method.summary.exampleBlocks} version={version} />
) : null}
{method.summary?.returnsBlock.length ? (
<ReturnNode padding node={method.summary.returnsBlock} version={version} />
) : null}
{method.inheritedFrom ? (
<InheritedFromNode node={method.inheritedFrom} packageName={packageName} version={version} />
) : null}
{method.summary?.seeBlocks.length ? (
<SeeNode padding node={method.summary.seeBlocks} version={version} />
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</>
);
}
async function OverloadNode({
method,
packageName,
version,
}: {
readonly method: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Tabs className="flex flex-col gap-4">
<TabList className="flex gap-2">
{method.overloads.map((overload: any) => {
return (
<Tab
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-${overload.displayName}-${overload.overloadIndex}`}
className="cursor-pointer rounded-full bg-neutral-800/10 px-2 py-1 font-sans text-sm font-normal leading-none text-neutral-800 hover:bg-neutral-800/20 data-[selected]:bg-neutral-500 data-[selected]:text-neutral-100 dark:bg-neutral-200/10 dark:text-neutral-200 dark:hover:bg-neutral-200/20 dark:data-[selected]:bg-neutral-500/70"
>
<span>Overload {overload.overloadIndex}</span>
</Tab>
);
})}
</TabList>
{method.overloads.map((overload: any) => {
return (
<TabPanel
id={`overload-${overload.displayName}-${overload.overloadIndex}`}
key={`overload-tab-panel-${overload.displayName}-${overload.overloadIndex}`}
className="flex flex-col gap-8"
>
<MethodBodyNode overload method={overload} packageName={packageName} version={version} />
</TabPanel>
);
})}
</Tabs>
);
}
export async function MethodNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} /> Methods
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-8">
{node.map((method: any) => {
return method.overloads?.length ? (
<OverloadNode
key={`${method.displayName}-${method.overloadIndex}`}
method={method}
packageName={packageName}
version={version}
/>
) : (
<MethodBodyNode
key={`${method.displayName}-${method.overloadIndex}`}
method={method}
packageName={packageName}
version={version}
/>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -1,47 +0,0 @@
'use client';
import dynamic from 'next/dynamic';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { useNav } from '~/contexts/nav';
import { Sidebar } from './Sidebar';
import type { SidebarSectionItemData } from './Sidebar';
const PackageSelect = dynamic(async () => import('./PackageSelect'));
const VersionSelect = dynamic(async () => import('./VersionSelect'));
export function Nav({
members,
versions,
}: {
readonly members: SidebarSectionItemData[];
readonly versions: string[];
}) {
const { opened } = useNav();
return (
<nav
className={`dark:bg-dark-600/75 dark:border-dark-100 border-light-900 top-22 fixed bottom-4 left-4 right-4 z-20 mx-auto max-w-5xl rounded-md border bg-white/75 shadow backdrop-blur-md ${
opened ? 'block' : 'hidden'
} lg:min-w-xs lg:sticky lg:block lg:h-full lg:w-full lg:max-w-xs`}
>
<Scrollbars
autoHide
className="[&>div]:overscroll-none"
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="z-30 rounded bg-light-900 dark:bg-dark-100" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute bottom-0.5 right-0.5 top-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<div className="flex flex-col gap-4 p-3">
<div className="flex flex-col gap-4">
<PackageSelect />
<VersionSelect versions={versions} />
</div>
<Sidebar members={members} />
</div>
</Scrollbars>
</nav>
);
}

View File

@@ -0,0 +1,227 @@
import { VscGithubInverted } from '@react-icons/all-files/vsc/VscGithubInverted';
import { ChevronDown, ChevronUp } from 'lucide-react';
import dynamic from 'next/dynamic';
import Link from 'next/link';
import { fetchVersions } from '~/util/fetchVersions';
import { resolveNodeKind } from './DocKind';
import { NavigationItem } from './NavigationItem';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { PackageSelect } from './ui/PackageSelect';
import { SearchButton } from './ui/SearchButton';
import { VersionSelect } from './ui/VersionSelect';
// eslint-disable-next-line promise/prefer-await-to-then
const ThemeSwitch = dynamic(async () => import('~/components/ui/ThemeSwitch').then((mod) => mod.ThemeSwitch), {
ssr: false,
});
export async function Navigation({
className = '',
packageName,
version,
drawer = false,
}: {
readonly className?: string;
readonly drawer?: boolean;
readonly packageName: string;
readonly version: string;
}) {
const isMainVersion = version === 'main';
const fileContent = await fetch(
`${process.env.BLOB_STORAGE_URL}/rewrite/${packageName}/${version}.sitemap.api.json`,
{ next: isMainVersion ? { revalidate: 0 } : { revalidate: 604_800 } },
);
const node = await fileContent.json();
const versions = await fetchVersions(packageName);
const groupedNodes = node.reduce((acc: any, node: any) => {
(acc[node.kind.toLowerCase()] ||= []).push(node);
return acc;
}, {});
return (
<aside className={`flex min-w-52 max-w-52 flex-col gap-2 lg:min-w-72 lg:max-w-72 ${className}`}>
<div
className={`sticky top-0 flex flex-col gap-4 pb-4 ${drawer ? 'bg-neutral-100 dark:bg-neutral-900' : 'bg-white dark:bg-[#121212]'}`}
>
<div className="flex flex-col gap-2 pt-px">
<div className="flex place-content-between place-items-center p-1">
<Link href={`/docs/packages/${packageName}/${version}`} className="text-xl font-bold">
{packageName}
</Link>
<div className="flex gap-2">
<Link
aria-label="GitHub"
className="rounded-full"
href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer"
target="_blank"
>
<VscGithubInverted aria-hidden size={24} />
</Link>
<ThemeSwitch />
</div>
</div>
<PackageSelect packageName={packageName} />
{/* <h3 className="p-1 text-lg font-semibold">{version}</h3> */}
<VersionSelect packageName={packageName} version={version} versions={versions} />
</div>
<SearchButton />
</div>
<nav className="flex flex-col gap-4">
{groupedNodes.class?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Classes</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.class.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.function?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Functions</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.function.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.enum?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Enums</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.enum.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.interface?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Interfaces</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.interface.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.typealias?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Types</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.typealias.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{groupedNodes.variable?.length ? (
<Collapsible className="flex flex-col gap-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h4 className="font-semibold">Variables</h4>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-1.5">
{groupedNodes.variable.map((node: any, idx: number) => {
const kind = resolveNodeKind(node.kind);
return (
<NavigationItem key={`${node.name}-${idx}`} node={node} packageName={packageName} version={version}>
<div className={`inline-block h-6 w-6 rounded-full text-center ${kind.background} ${kind.text}`}>
{node.kind[0]}
</div>{' '}
<span className="font-sans">{node.name}</span>
</NavigationItem>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
</nav>
</aside>
);
}

View File

@@ -0,0 +1,34 @@
'use client';
import { useSetAtom } from 'jotai';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import type { PropsWithChildren } from 'react';
import { isDrawerOpenAtom } from '~/stores/drawer';
export function NavigationItem({
node,
packageName,
version,
children,
}: PropsWithChildren<{
readonly node: any;
readonly packageName: string;
readonly version: string;
}>) {
const pathname = usePathname();
const setDrawerOpen = useSetAtom(isDrawerOpenAtom);
const href = `/docs/packages/${packageName}/${version}/${node.href}`;
return (
<Link
className={`truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1 ${pathname === href ? 'bg-neutral-200 font-medium text-blurple dark:bg-neutral-800' : ''}`}
href={href}
title={node.name}
onClick={() => setDrawerOpen(false)}
>
{children}
</Link>
);
}

View File

@@ -1,32 +1,135 @@
'use client';
import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { ChevronDown, ChevronUp } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
import { useOutline } from '~/contexts/outline';
import { Scrollbars } from './Scrollbars';
import { TableOfContentItems } from './TableOfContentItems';
export async function Outline({ node }: { readonly node: any }) {
const hasAny = node.members?.properties?.length || node.members?.events?.length || node.members?.methods?.length;
export function Outline() {
const { members } = useOutline();
return hasAny ? (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscListSelection aria-hidden className="flex-shrink-0" size={24} /> Table of contents
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
if (!members) {
return null;
}
<CollapsibleContent>
<div className="flex flex-col gap-8">
<div className="grid gap-2 sm:grid-cols-2">
{node.members?.properties?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} />
Properties
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
return (
<div className="lg:sticky lg:top-23 lg:h-[calc(100vh_-_105px)]">
<aside className="fixed bottom-4 left-4 right-4 top-22 z-20 mx-auto hidden max-w-5xl border border-light-900 rounded-md bg-white/75 shadow backdrop-blur-md lg:sticky lg:block lg:h-full lg:max-w-xs lg:min-w-xs lg:w-full dark:border-dark-100 dark:bg-dark-600/75">
<Scrollbars
autoHide
className="[&>div]:overscroll-none"
hideTracksWhenNotNeeded
renderThumbVertical={(props) => <div {...props} className="z-30 rounded bg-light-900 dark:bg-dark-100" />}
renderTrackVertical={(props) => (
<div {...props} className="absolute bottom-0.5 right-0.5 top-0.5 z-30 w-1.5 rounded" />
)}
universal
>
<TableOfContentItems serializedMembers={members} />
</Scrollbars>
</aside>
</div>
);
<CollapsibleContent>
<div className="flex flex-col gap-2 px-4">
{node.members.properties.map((property: any, idx: number) => {
return (
<Fragment key={`${property.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<Link
href={`#${property.displayName}`}
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1"
>
{property.displayName}
</Link>
</div>
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{node.members?.events?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolEvent aria-hidden className="flex-shrink-0" size={24} />
Events
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-2 px-4">
{node.members.events.map((event: any, idx: number) => {
return (
<Fragment key={`${event.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<Link
href={`#${event.displayName}`}
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1"
>
{event.displayName}
</Link>
</div>
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
{node.members?.methods?.length ? (
<Collapsible className="flex flex-col gap-4 px-4" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolMethod aria-hidden className="flex-shrink-0" size={24} />
Methods
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-2 px-4">
{node.members.methods.map((method: any, idx: number) => {
return (
<Fragment key={`${method.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<Link
href={`#${method.displayName}`}
className="grow truncate rounded-md p-2 font-mono transition-colors hover:bg-neutral-200 dark:hover:bg-neutral-800 md:px-1 md:py-1"
>
{method.displayName}
</Link>
</div>
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</div>
</CollapsibleContent>
</Collapsible>
) : null;
}

View File

@@ -0,0 +1,7 @@
'use client';
import { OverlayScrollbars, ClickScrollPlugin } from 'overlayscrollbars';
OverlayScrollbars.plugin(ClickScrollPlugin);
export { OverlayScrollbarsComponent } from 'overlayscrollbars-react';

View File

@@ -1,98 +0,0 @@
'use client';
import { VscChevronDown } from '@react-icons/all-files/vsc/VscChevronDown';
import { VscVersions } from '@react-icons/all-files/vsc/VscVersions';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import type { PropsWithChildren, ReactNode } from 'react';
import { useCallback, useMemo, useState, useEffect } from 'react';
export interface OverloadSwitcherProps {
methodName: string;
overloads: ReactNode[];
}
export default function OverloadSwitcher({
methodName,
overloads,
children,
}: PropsWithChildren<{
readonly methodName: string;
readonly overloads: ReactNode[];
}>) {
const [hash, setHash] = useState(() => (typeof window === 'undefined' ? '' : window.location.hash));
const hashChangeHandler = useCallback(() => {
setHash(window.location.hash);
}, []);
const [overloadIndex, setOverloadIndex] = useState(1);
const overloadedNode = overloads[overloadIndex - 1]!;
const menu = useMenuState({ gutter: 8, sameWidth: true, fitViewport: true });
useEffect(() => {
window.addEventListener('hashchange', hashChangeHandler);
return () => {
window.removeEventListener('hashchange', hashChangeHandler);
};
});
useEffect(() => {
if (hash) {
const elementId = hash.replace('#', '');
const [name, idx] = elementId.split(':');
if (name && methodName === name) {
if (idx) {
const hashOverload = Number.parseInt(idx, 10);
const resolvedOverload = Math.max(Math.min(hashOverload, overloads.length), 1);
setOverloadIndex(Number.isNaN(resolvedOverload) ? 1 : resolvedOverload);
}
const element = document.querySelector(`[id^='${name}']`);
if (element) {
element.scrollIntoView({ behavior: 'smooth' });
}
}
}
}, [hash, methodName, overloads.length]);
const menuItems = useMemo(
() =>
overloads.map((_, idx) => (
<MenuItem
className="my-0.5 cursor-pointer rounded bg-white p-3 text-sm outline-none active:bg-light-800 dark:bg-dark-600 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-500"
key={idx}
onClick={() => setOverloadIndex(idx + 1)}
>
{`Overload ${idx + 1}`}
</MenuItem>
)),
[overloads],
);
return (
<div className="flex flex-col place-items-start gap-2">
<MenuButton
className="mb-2 rounded bg-white p-3 outline-none active:bg-light-900 dark:bg-dark-400 hover:bg-light-800 focus:ring focus:ring-width-2 focus:ring-blurple md:-ml-2 dark:active:bg-dark-200 dark:hover:bg-dark-300"
state={menu}
>
<div className="flex flex-row place-content-between place-items-center gap-2">
<VscVersions size={20} />
<div>
<span className="font-semibold">{`Overload ${overloadIndex}`}</span>
{` of ${overloads.length}`}
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${menu.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="z-20 flex flex-col border border-light-800 rounded bg-white p-1 outline-none dark:border-dark-100 dark:bg-dark-600 focus:ring focus:ring-width-2 focus:ring-blurple"
state={menu}
>
{menuItems}
</Menu>
{children}
{overloadedNode}
</div>
);
}

View File

@@ -1,63 +0,0 @@
'use client';
import { VscChevronDown } from '@react-icons/all-files/vsc/VscChevronDown';
import { VscPackage } from '@react-icons/all-files/vsc/VscPackage';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useMemo } from 'react';
import { PACKAGES } from '~/util/constants';
export default function PackageSelect() {
const pathname = usePathname();
const packageName = pathname?.split('/').slice(3, 4)[0];
const packageMenu = useMenuState({
gutter: 8,
sameWidth: true,
fitViewport: true,
});
const packageMenuItems = useMemo(
() =>
PACKAGES.map((pkg, idx) => (
<Link href={`/docs/packages/${pkg}/main`} key={`${pkg}-${idx}`}>
<MenuItem
className="my-0.5 rounded bg-white p-3 text-sm outline-none active:bg-light-800 dark:bg-dark-600 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-500"
id={pkg}
onClick={() => packageMenu.setOpen(false)}
state={packageMenu}
>
{pkg}
</MenuItem>
</Link>
)),
[packageMenu],
);
return (
<>
<MenuButton
className="rounded bg-light-600 p-3 outline-none active:bg-light-800 dark:bg-dark-400 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-300"
state={packageMenu}
>
<div className="flex flex-row place-content-between place-items-center">
<div className="flex flex-row place-items-center gap-3">
<VscPackage size={20} />
<span className="font-semibold">{packageName}</span>
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${packageMenu.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="z-20 flex flex-col border border-light-800 rounded bg-white p-1 outline-none dark:border-dark-100 dark:bg-dark-600 focus:ring focus:ring-width-2 focus:ring-blurple"
state={packageMenu}
>
{packageMenuItems}
</Menu>
</>
);
}

View File

@@ -1,10 +0,0 @@
import type { PropsWithChildren } from 'react';
export function Panel({ children }: PropsWithChildren) {
return (
<>
{children}
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</>
);
}

View File

@@ -0,0 +1,49 @@
import { LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DocNode } from './DocNode';
import { ExcerptNode } from './ExcerptNode';
export async function ParameterNode({
description = false,
node,
version,
}: {
readonly description?: boolean;
readonly node: any;
readonly version: string;
}) {
return (
<div className={`${description ? 'flex flex-col gap-8' : 'inline'}`}>
{node.map((parameter: any, idx: number) => {
return (
<Fragment key={`${parameter.name}-${idx}`}>
<div className={description ? 'group' : 'inline after:content-[",_"] last-of-type:after:content-none'}>
<span className="font-mono font-semibold">
{description ? (
<Link href={`#${parameter.name}`} className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block">
<LinkIcon aria-hidden size={16} />
</Link>
) : null}
{description ? <Badges node={parameter} /> : null}
{parameter.name}
{parameter.isOptional ? '?' : ''}: <ExcerptNode node={parameter.typeExcerpt} version={version} />
</span>
{description && parameter.description?.length ? (
<div className="mt-4 pl-4">
<DocNode node={parameter.description} version={version} />
</div>
) : null}
</div>
</Fragment>
);
})}
{description ? (
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
) : null}
</div>
);
}

View File

@@ -1,32 +0,0 @@
import type { ApiDocumentedItem, ApiParameterListMixin } from '@discordjs/api-extractor-model';
import { useMemo } from 'react';
import { resolveParameters } from '~/util/model';
import { ExcerptText } from './ExcerptText';
import { Table } from './Table';
import { TSDoc } from './documentation/tsdoc/TSDoc';
const columnStyles = {
Name: 'font-mono whitespace-nowrap',
Type: 'font-mono whitespace-pre-wrap break-normal',
};
export function ParameterTable({ item }: { readonly item: ApiDocumentedItem & ApiParameterListMixin }) {
const params = resolveParameters(item);
const rows = useMemo(
() =>
params.map((param) => ({
Name: param.isRest ? `...${param.name}` : param.name,
Type: <ExcerptText excerpt={param.parameterTypeExcerpt} apiPackage={item.getAssociatedPackage()!} />,
Optional: param.isOptional ? 'Yes' : 'No',
Description: param.description ? <TSDoc item={item} tsdoc={param.description} /> : 'None',
})),
[item, params],
);
return (
<div className="overflow-x-auto">
<Table columnStyles={columnStyles} columns={['Name', 'Type', 'Optional', 'Description']} rows={rows} />
</div>
);
}

View File

@@ -1,49 +0,0 @@
import type {
ApiDeclaredItem,
ApiItemContainerMixin,
ApiProperty,
ApiPropertySignature,
} from '@discordjs/api-extractor-model';
import type { PropsWithChildren } from 'react';
import { Badges } from './Badges';
import { CodeHeading } from './CodeHeading';
import { ExcerptText } from './ExcerptText';
import { InheritanceText } from './InheritanceText';
import { TSDoc } from './documentation/tsdoc/TSDoc';
export function Property({
item,
children,
inheritedFrom,
}: PropsWithChildren<{
readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined;
readonly item: ApiProperty | ApiPropertySignature;
}>) {
const hasSummary = Boolean(item.tsdocComment?.summarySection);
return (
<div className="flex flex-col scroll-mt-30 gap-4" id={item.displayName}>
<div className="flex flex-col gap-2 md:-ml-9">
<Badges item={item} />
<CodeHeading
href={`#${item.displayName}`}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
>
{`${item.displayName}${item.isOptional ? '?' : ''}`}
<span>:</span>
{item.propertyTypeExcerpt.text ? (
<ExcerptText excerpt={item.propertyTypeExcerpt} apiPackage={item.getAssociatedPackage()!} />
) : null}
</CodeHeading>
</div>
{hasSummary || inheritedFrom ? (
<div className="mb-4 w-full flex flex-col gap-4">
{item.tsdocComment ? <TSDoc item={item} tsdoc={item.tsdocComment} /> : null}
{inheritedFrom ? <InheritanceText parent={inheritedFrom} /> : null}
{children}
</div>
) : null}
</div>
);
}

View File

@@ -1,37 +0,0 @@
import type {
ApiDeclaredItem,
ApiItem,
ApiItemContainerMixin,
ApiProperty,
ApiPropertySignature,
} from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { Fragment, useMemo } from 'react';
import { resolveMembers } from '~/util/members';
import { Property } from './Property';
export function isPropertyLike(item: ApiItem): item is ApiProperty | ApiPropertySignature {
return item.kind === ApiItemKind.Property || item.kind === ApiItemKind.PropertySignature;
}
export function PropertyList({ item }: { readonly item: ApiItemContainerMixin }) {
const members = resolveMembers(item, isPropertyLike);
const propertyItems = useMemo(
() =>
members.map((prop, idx) => {
return (
<Fragment key={`${prop.item.displayName}-${idx}`}>
<Property
inheritedFrom={prop.inherited as ApiDeclaredItem & ApiItemContainerMixin}
item={prop.item as ApiProperty}
/>
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</Fragment>
);
}),
[members],
);
return <div className="flex flex-col gap-4">{propertyItems}</div>;
}

View File

@@ -0,0 +1,98 @@
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { ChevronDown, ChevronUp, Code2, LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DeprecatedNode } from './DeprecatedNode';
import { ExcerptNode } from './ExcerptNode';
import { InheritedFromNode } from './InheritedFromNode';
import { SeeNode } from './SeeNode';
import { SummaryNode } from './SummaryNode';
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from './ui/Collapsible';
export async function PropertyNode({
node,
packageName,
version,
}: {
readonly node: any;
readonly packageName: string;
readonly version: string;
}) {
return (
<Collapsible className="flex flex-col gap-8" defaultOpen>
<CollapsibleTrigger className="group flex place-content-between place-items-center rounded-md p-2 hover:bg-neutral-200 dark:hover:bg-neutral-800">
<h2 className="flex place-items-center gap-2 text-xl font-bold">
<VscSymbolProperty aria-hidden className="flex-shrink-0" size={24} />
Properties
</h2>
<ChevronDown className='group-data-[state="open"]:hidden' aria-hidden size={24} />
<ChevronUp className='group-data-[state="closed"]:hidden' aria-hidden size={24} />
</CollapsibleTrigger>
<CollapsibleContent>
<div className="flex flex-col gap-8">
{node.map((property: any, idx: number) => {
return (
<Fragment key={`${property.displayName}-${idx}`}>
<div className="flex flex-col gap-4">
<div className="flex place-content-between place-items-center">
<h3
id={property.displayName}
className="group flex scroll-mt-8 flex-col gap-2 break-words font-mono font-semibold"
>
<Badges node={property} />
<span>
<Link
href={`#${property.displayName}`}
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block"
>
<LinkIcon aria-hidden size={16} />
</Link>
{property.displayName}
{property.isOptional ? '?' : ''} : <ExcerptNode node={property.typeExcerpt} version={version} />
</span>
</h3>
<a
aria-label="Open source file in new tab"
className="min-w-min"
href={property.sourceLine ? `${property.sourceURL}#L${property.sourceLine}` : property.sourceURL}
rel="external noreferrer noopener"
target="_blank"
>
<Code2
aria-hidden
size={20}
className="text-neutral-500 hover:text-neutral-600 dark:text-neutral-400 dark:hover:text-neutral-300"
/>
</a>
</div>
{property.summary?.deprecatedBlock.length ? (
<DeprecatedNode deprecatedBlock={property.summary.deprecatedBlock} version={version} />
) : null}
{property.summary?.summarySection.length ? (
<SummaryNode padding node={property.summary.summarySection} version={version} />
) : null}
{property.inheritedFrom ? (
<InheritedFromNode node={property.inheritedFrom} packageName={packageName} version={version} />
) : null}
{property.summary?.seeBlocks.length ? (
<SeeNode padding node={property.summary.seeBlocks} version={version} />
) : null}
</div>
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
</Fragment>
);
})}
</div>
</CollapsibleContent>
</Collapsible>
);
}

View File

@@ -0,0 +1,17 @@
import { DocNode } from './DocNode';
export async function ReturnNode({
padding = false,
node,
version,
}: {
readonly node: any;
readonly padding?: boolean;
readonly version: string;
}) {
return (
<p className={`break-words ${padding ? 'pl-4' : ''}`}>
<span className="font-semibold">Returns:</span> <DocNode node={node} version={version} />
</p>
);
}

View File

@@ -1,8 +0,0 @@
'use client';
import type { ScrollbarProps } from 'react-custom-scrollbars-2';
import { Scrollbars as ReactScrollbars2 } from 'react-custom-scrollbars-2';
export function Scrollbars(props: ScrollbarProps) {
return <ReactScrollbars2 {...props} />;
}

View File

@@ -1,8 +0,0 @@
'use client';
import { Section as DJSSection, type SectionOptions } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
export function Section(options: PropsWithChildren<SectionOptions>) {
return <DJSSection {...options} />;
}

View File

@@ -0,0 +1,17 @@
import { DocNode } from './DocNode';
export async function SeeNode({
padding = false,
node,
version,
}: {
readonly node: any;
readonly padding?: boolean;
readonly version: string;
}) {
return (
<p className={`break-words ${padding ? 'pl-4' : ''}`}>
<span className="font-semibold">See also:</span> <DocNode node={node} version={version} />
</p>
);
}

View File

@@ -1,124 +0,0 @@
'use client';
import type { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import { useSelectedLayoutSegment } from 'next/navigation';
import { useMemo } from 'react';
import { useNav } from '~/contexts/nav';
import { ItemLink } from './ItemLink';
import { Section } from './Section';
export interface SidebarSectionItemData {
href: string;
kind: ApiItemKind;
name: string;
overloadIndex?: number | undefined;
}
interface GroupedMembers {
Classes: SidebarSectionItemData[];
Enums: SidebarSectionItemData[];
Functions: SidebarSectionItemData[];
Interfaces: SidebarSectionItemData[];
Types: SidebarSectionItemData[];
Variables: SidebarSectionItemData[];
}
function groupMembers(members: readonly SidebarSectionItemData[]): GroupedMembers {
const Classes: SidebarSectionItemData[] = [];
const Enums: SidebarSectionItemData[] = [];
const Interfaces: SidebarSectionItemData[] = [];
const Types: SidebarSectionItemData[] = [];
const Variables: SidebarSectionItemData[] = [];
const Functions: SidebarSectionItemData[] = [];
for (const member of members) {
switch (member.kind) {
case 'Class':
Classes.push(member);
break;
case 'Enum':
Enums.push(member);
break;
case 'Interface':
Interfaces.push(member);
break;
case 'TypeAlias':
Types.push(member);
break;
case 'Variable':
Variables.push(member);
break;
case 'Function':
Functions.push(member);
break;
default:
break;
}
}
return { Classes, Functions, Enums, Interfaces, Types, Variables };
}
function resolveIcon(item: string) {
switch (item) {
case 'Classes':
return <VscSymbolClass size={20} />;
case 'Enums':
return <VscSymbolEnum size={20} />;
case 'Interfaces':
return <VscSymbolInterface size={20} />;
case 'Types':
case 'Variables':
return <VscSymbolVariable size={20} />;
default:
return <VscSymbolMethod size={20} />;
}
}
export function Sidebar({ members }: { readonly members: SidebarSectionItemData[] }) {
const segment = useSelectedLayoutSegment();
const { setOpened } = useNav();
const groupItems = useMemo(() => groupMembers(members), [members]);
return (
<div className="flex flex-col gap-4">
{(Object.keys(groupItems) as (keyof GroupedMembers)[])
.filter((group) => groupItems[group].length)
.map((group, idx) => (
<Section
buttonClassName="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-400 dark:hover:bg-dark-300 dark:active:bg-dark-400 focus:ring-width-2 focus:ring-blurple rounded p-3 outline-none focus:ring z-10"
icon={resolveIcon(group)}
key={`${group}-${idx}`}
title={group}
>
{groupItems[group].map((member, index) => (
<ItemLink
className={`dark:border-dark-100 border-light-800 focus:ring-width-2 focus:ring-blurple ml-5 flex flex-col border-l first:mt-1 p-[5px] pl-6 outline-none focus:rounded focus:border-0 focus:ring ${
decodeURIComponent(segment ?? '') === member.href
? 'bg-blurple text-white'
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'
}`}
itemURI={member.href}
key={`${member.name}-${index}`}
onClick={() => setOpened(false)}
title={member.name}
>
<div className="flex flex-row place-items-center gap-2 lg:text-sm">
<span className="truncate">{member.name}</span>
{member.overloadIndex && member.overloadIndex > 1 ? (
<span className="text-xs">{member.overloadIndex}</span>
) : null}
</div>
</ItemLink>
))}
</Section>
))}
</div>
);
}

View File

@@ -1,10 +0,0 @@
import type { ApiPackage, Excerpt } from '@discordjs/api-extractor-model';
import { ExcerptText } from './ExcerptText';
export function SignatureText({ excerpt, apiPackage }: { readonly apiPackage: ApiPackage; readonly excerpt: Excerpt }) {
return (
<h4 className="break-all text-lg font-bold font-mono">
<ExcerptText excerpt={excerpt} apiPackage={apiPackage} />
</h4>
);
}

View File

@@ -0,0 +1,17 @@
import { DocNode } from './DocNode';
export async function SummaryNode({
padding = false,
node,
version,
}: {
readonly node: any;
readonly padding?: boolean;
readonly version: string;
}) {
return (
<p className={`break-words ${padding ? 'pl-4' : ''}`}>
<DocNode node={node} version={version} />
</p>
);
}

View File

@@ -1,14 +1,33 @@
import { Code } from 'bright';
import { getHighlighterCore } from 'shiki/core';
import getWasm from 'shiki/wasm';
const highlighter = await getHighlighterCore({
themes: [import('shiki/themes/github-light.mjs'), import('shiki/themes/github-dark-dimmed.mjs')],
langs: [import('shiki/langs/typescript.mjs'), import('shiki/langs/javascript.mjs')],
loadWasm: getWasm,
});
export async function SyntaxHighlighter({
lang,
code,
className = '',
}: {
readonly className?: string;
readonly code: string;
readonly lang: string;
}) {
const codeHTML = highlighter.codeToHtml(code.trim(), {
lang,
themes: {
light: 'github-light',
dark: 'github-dark-dimmed',
},
});
export async function SyntaxHighlighter(props: typeof Code) {
return (
<>
<div data-theme="dark">
<Code codeClassName="font-mono" lang={props.lang ?? 'typescript'} {...props} theme="github-dark-dimmed" />
</div>
<div className="[&_pre]:border [&_pre]:border-gray-300 [&_pre]:rounded-md" data-theme="light">
<Code codeClassName="font-mono" lang={props.lang ?? 'typescript'} {...props} theme="min-light" />
</div>
{/* eslint-disable-next-line react/no-danger */}
<div className={className} dangerouslySetInnerHTML={{ __html: codeHTML }} />
</>
);
}

View File

@@ -1,54 +0,0 @@
'use client';
import { useMemo, type ReactNode } from 'react';
export function Table({
rows,
columns,
columnStyles,
}: {
readonly columnStyles?: Record<string, string>;
readonly columns: string[];
readonly rows: Record<string, ReactNode>[];
}) {
const cols = useMemo(
() =>
columns.map((column, idx) => (
<th
className="break-normal border-b border-light-900 px-3 py-2 text-left text-sm dark:border-dark-100"
key={`${column}-${idx}`}
>
{column}
</th>
)),
[columns],
);
const data = useMemo(
() =>
rows.map((row, idx) => (
<tr className="[&>td]:last-of-type:border-0" key={idx}>
{Object.entries(row).map(([colName, val], index) => (
<td
className={`border-light-900 dark:border-dark-100 border-b px-3 py-2 text-left text-sm ${
columnStyles?.[colName] ?? ''
}`}
key={`${colName}-${index}`}
>
{val}
</td>
))}
</tr>
)),
[columnStyles, rows],
);
return (
<table className="w-full border-collapse">
<thead>
<tr>{cols}</tr>
</thead>
<tbody>{data}</tbody>
</table>
);
}

View File

@@ -1,161 +0,0 @@
'use client';
import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { useMemo } from 'react';
export interface TableOfContentsSerializedMethod {
kind: 'Method' | 'MethodSignature';
name: string;
overloadIndex?: number;
}
export interface TableOfContentsSerializedProperty {
kind: 'Property' | 'PropertySignature';
name: string;
}
export interface TableOfContentsSerializedEvent {
kind: 'Event';
name: string;
}
export type TableOfContentsSerialized =
| TableOfContentsSerializedEvent
| TableOfContentsSerializedMethod
| TableOfContentsSerializedProperty;
export interface TableOfContentsItemProps {
readonly serializedMembers: TableOfContentsSerialized[];
}
export function TableOfContentsPropertyItem({ property }: { readonly property: TableOfContentsSerializedProperty }) {
return (
<a
className="ml-[10px] border-l border-light-800 p-[5px] pl-6.5 text-sm outline-none focus:border-0 dark:border-dark-100 focus:rounded active:bg-light-800 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-100 dark:hover:bg-dark-200"
href={`#${property.name}`}
key={`${property.name}-${property.kind}`}
title={property.name}
>
<span className="line-clamp-1">{property.name}</span>
</a>
);
}
export function TableOfContentsMethodItem({ method }: { readonly method: TableOfContentsSerializedMethod }) {
if (method.overloadIndex && method.overloadIndex > 1) {
return null;
}
const key = `${method.name}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`;
return (
<a
className="ml-[10px] flex flex-row place-items-center gap-2 border-l border-light-800 p-[5px] pl-6.5 text-sm outline-none focus:border-0 dark:border-dark-100 focus:rounded active:bg-light-800 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-100 dark:hover:bg-dark-200"
href={`#${key}`}
key={key}
title={method.name}
>
<span className="line-clamp-1">{method.name}</span>
{method.overloadIndex && method.overloadIndex > 1 ? (
<span className="text-xs">{method.overloadIndex}</span>
) : null}
</a>
);
}
export function TableOfContentsEventItem({ event }: { readonly event: TableOfContentsSerializedEvent }) {
return (
<a
className="ml-[10px] border-l border-light-800 p-[5px] pl-6.5 text-sm outline-none focus:border-0 dark:border-dark-100 focus:rounded active:bg-light-800 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-100 dark:hover:bg-dark-200"
href={`#${event.name}`}
key={`${event.name}-${event.kind}`}
title={event.name}
>
<span className="line-clamp-1">{event.name}</span>
</a>
);
}
export function TableOfContentItems({ serializedMembers }: TableOfContentsItemProps) {
const propertyItems = useMemo(
() =>
serializedMembers
.filter(
(member): member is TableOfContentsSerializedProperty =>
member.kind === 'Property' || member.kind === 'PropertySignature',
)
.map((prop, idx) => <TableOfContentsPropertyItem key={`${prop.name}-${prop.kind}-${idx}`} property={prop} />),
[serializedMembers],
);
const methodItems = useMemo(
() =>
serializedMembers
.filter(
(member): member is TableOfContentsSerializedMethod =>
member.kind === 'Method' || member.kind === 'MethodSignature',
)
.map((member, idx) => (
<TableOfContentsMethodItem
key={`${member.name}${member.overloadIndex ? `:${member.overloadIndex}` : ''}-${idx}`}
method={member}
/>
)),
[serializedMembers],
);
const eventItems = useMemo(
() =>
serializedMembers
.filter((member): member is TableOfContentsSerializedEvent => member.kind === 'Event')
.map((event, idx) => <TableOfContentsEventItem key={`${event.name}-${event.kind}-${idx}`} event={event} />),
[serializedMembers],
);
return (
<div className="flex flex-col break-all p-3 pb-8">
<div className="ml-2 mt-4 flex flex-row gap-2">
<VscListSelection size={25} />
<span className="font-semibold">Contents</span>
</div>
<div className="ml-2 mt-5.5 flex flex-col gap-2">
{eventItems.length ? (
<div className="flex flex-col">
<div className="flex flex-row place-items-center gap-4">
<VscSymbolEvent size={20} />
<div className="p-3 pl-0">
<span className="font-semibold">Events</span>
</div>
</div>
{eventItems}
</div>
) : null}
{propertyItems.length ? (
<div className="flex flex-col">
<div className="flex flex-row place-items-center gap-4">
<VscSymbolProperty size={20} />
<div className="p-3 pl-0">
<span className="font-semibold">Properties</span>
</div>
</div>
{propertyItems}
</div>
) : null}
{methodItems.length ? (
<div className="flex flex-col">
<div className="flex flex-row place-items-center gap-4">
<VscSymbolMethod size={20} />
<div className="p-3 pl-0">
<span className="font-semibold">Methods</span>
</div>
</div>
{methodItems}
</div>
) : null}
</div>
</div>
);
}

View File

@@ -1,20 +0,0 @@
'use client';
import { VscColorMode } from '@react-icons/all-files/vsc/VscColorMode';
import { Button } from 'ariakit/button';
import { useTheme } from 'next-themes';
export default function ThemeSwitcher() {
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
return (
<Button
aria-label="Toggle theme"
className="h-6 w-6 flex flex-row transform-gpu cursor-pointer select-none appearance-none place-items-center border-0 rounded rounded-full bg-transparent p-0 text-sm font-semibold leading-none no-underline outline-none active:translate-y-px focus:ring focus:ring-width-2 focus:ring-blurple"
onClick={() => toggleTheme()}
>
<VscColorMode size={24} />
</Button>
);
}

View File

@@ -1,39 +0,0 @@
import type { ApiTypeParameterListMixin } from '@discordjs/api-extractor-model';
import { useMemo } from 'react';
import { ExcerptText } from './ExcerptText';
import { Table } from './Table';
import { TSDoc } from './documentation/tsdoc/TSDoc';
const rowElements = {
Name: 'font-mono whitespace-nowrap',
Constraints: 'font-mono whitespace-pre break-normal',
Default: 'font-mono whitespace-pre break-normal',
};
export function TypeParamTable({ item }: { readonly item: ApiTypeParameterListMixin }) {
const rows = useMemo(
() =>
item.typeParameters.map((typeParam) => ({
Name: typeParam.name,
Constraints: <ExcerptText excerpt={typeParam.constraintExcerpt} apiPackage={item.getAssociatedPackage()!} />,
Optional: typeParam.isOptional ? 'Yes' : 'No',
Default: <ExcerptText excerpt={typeParam.defaultTypeExcerpt} apiPackage={item.getAssociatedPackage()!} />,
Description: typeParam.tsdocTypeParamBlock ? (
<TSDoc item={item} tsdoc={typeParam.tsdocTypeParamBlock.content} />
) : (
'None'
),
})),
[item],
);
return (
<div className="overflow-x-auto">
<Table
columnStyles={rowElements}
columns={['Name', 'Constraints', 'Optional', 'Default', 'Description']}
rows={rows}
/>
</div>
);
}

View File

@@ -0,0 +1,67 @@
import { LinkIcon } from 'lucide-react';
import Link from 'next/link';
import { Fragment } from 'react';
import { Badges } from './Badges';
import { DocNode } from './DocNode';
import { ExcerptNode } from './ExcerptNode';
export async function TypeParameterNode({
description = false,
node,
version,
}: {
readonly description?: boolean;
readonly node: any;
readonly version: string;
}) {
return (
<div className={`${description ? 'flex flex-col gap-8' : 'inline-block'}`}>
{node.map((typeParameter: any, idx: number) => {
return (
<Fragment key={`${typeParameter.name}-${idx}`}>
<div className={description ? '' : 'inline after:content-[",_"] last-of-type:after:content-none'}>
<h3 id={typeParameter.name} className="group inline scroll-mt-8 break-words font-mono font-semibold">
{description ? <Badges node={typeParameter} /> : null}
<span>
{description ? (
<Link
href={`#${typeParameter.name}`}
className="float-left -ml-6 hidden pb-2 pr-2 group-hover:block"
>
<LinkIcon aria-hidden size={16} />
</Link>
) : null}
{typeParameter.name}
{typeParameter.isOptional ? '?' : ''}
{typeParameter.constraintsExcerpt.length ? (
<>
{' extends '}
<ExcerptNode node={typeParameter.constraintsExcerpt} version={version} />
</>
) : null}
{typeParameter.defaultExcerpt.length ? (
<>
{' = '}
<ExcerptNode node={typeParameter.defaultExcerpt} version={version} />
</>
) : null}
</span>
</h3>
{description && typeParameter.description?.length ? (
<div className="pl-4">
<DocNode node={typeParameter.description} version={version} />
</div>
) : null}
</div>
</Fragment>
);
})}
{description ? (
<div aria-hidden className="px-4">
<div role="separator" className="h-[2px] bg-neutral-300 dark:bg-neutral-700" />
</div>
) : null}
</div>
);
}

View File

@@ -0,0 +1,13 @@
import { ExcerptNode } from './ExcerptNode';
export async function UnionMember({ node, version }: { readonly node: any; readonly version: string }) {
return (
<div className="flex flex-col gap-8">
<h2 className="flex place-items-center gap-2 p-2 text-xl font-bold">Union Members</h2>
<span className="flex flex-col gap-4 break-words font-mono text-sm">
<ExcerptNode node={node} version={version} />
</span>
</div>
);
}

View File

@@ -1,69 +0,0 @@
'use client';
import { VscChevronDown } from '@react-icons/all-files/vsc/VscChevronDown';
import { VscVersions } from '@react-icons/all-files/vsc/VscVersions';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import Link from 'next/link';
import { usePathname } from 'next/navigation';
import { useMemo } from 'react';
import useSWR from 'swr';
const isDev = process.env.NEXT_PUBLIC_LOCAL_DEV === 'true' ?? process.env.NEXT_PUBLIC_VERCEL_ENV === 'preview';
export default function VersionSelect({ versions }: { readonly versions: string[] }) {
const pathname = usePathname();
const packageName = pathname?.split('/').slice(3, 4)[0];
const branchName = pathname?.split('/').slice(4, 5)[0];
const { data } = useSWR<string[]>(packageName ? `/api/${packageName}/versions` : null, {
fallbackData: versions,
});
const versionMenu = useMenuState({
gutter: 8,
sameWidth: true,
fitViewport: true,
});
const versionMenuItems = useMemo(
() =>
data?.map((item, idx) => (
<Link href={`/docs/packages/${packageName}/${isDev ? 'main' : item}`} key={`${item}-${idx}`}>
<MenuItem
className="my-0.5 rounded bg-white p-3 text-sm outline-none active:bg-light-800 dark:bg-dark-600 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-500"
onClick={() => versionMenu.setOpen(false)}
state={versionMenu}
>
{item}
</MenuItem>
</Link>
)) ?? [],
[data, packageName, versionMenu],
);
return (
<>
<MenuButton
className="rounded bg-light-600 p-3 outline-none active:bg-light-800 dark:bg-dark-400 hover:bg-light-700 focus:ring focus:ring-width-2 focus:ring-blurple dark:active:bg-dark-400 dark:hover:bg-dark-300"
state={versionMenu}
>
<div className="flex flex-row place-content-between place-items-center">
<div className="flex flex-row place-items-center gap-3">
<VscVersions size={20} />
<span className="font-semibold">{branchName}</span>
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${versionMenu.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</MenuButton>
<Menu
className="z-20 flex flex-col border border-light-800 rounded bg-white p-1 outline-none dark:border-dark-100 dark:bg-dark-600 focus:ring focus:ring-width-2 focus:ring-blurple"
state={versionMenu}
>
{versionMenuItems}
</Menu>
</>
);
}

View File

@@ -1,8 +0,0 @@
import type { PropsWithChildren } from 'react';
/**
* Layout parent of documentation pages.
*/
export function Documentation({ children }: PropsWithChildren) {
return <div className="w-full flex flex-col gap-4">{children}</div>;
}

View File

@@ -1,51 +0,0 @@
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscSymbolClass } from '@react-icons/all-files/vsc/VscSymbolClass';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { VscSymbolInterface } from '@react-icons/all-files/vsc/VscSymbolInterface';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { VscSymbolVariable } from '@react-icons/all-files/vsc/VscSymbolVariable';
import type { PropsWithChildren } from 'react';
import { SourceLink } from './SourceLink';
function generateIcon(kind: ApiItemKind) {
switch (kind) {
case ApiItemKind.Class:
return <VscSymbolClass />;
case ApiItemKind.Function:
case ApiItemKind.Method:
return <VscSymbolMethod />;
case ApiItemKind.Enum:
return <VscSymbolEnum />;
case ApiItemKind.Interface:
return <VscSymbolInterface />;
case ApiItemKind.TypeAlias:
case ApiItemKind.Variable:
return <VscSymbolVariable />;
default:
return <VscSymbolMethod />;
}
}
export function Header({
kind,
name,
sourceURL,
sourceLine,
}: PropsWithChildren<{
readonly kind: ApiItemKind;
readonly name: string;
readonly sourceLine?: number | undefined;
readonly sourceURL?: string | undefined;
}>) {
return (
<div className="flex flex-col">
<h2 className="flex flex-row place-items-center justify-between gap-2 break-all text-2xl font-bold">
<span className="row flex flex place-items-center gap-2">
<span>{generateIcon(kind)}</span>
{name}
</span>
{sourceURL ? <SourceLink sourceLine={sourceLine} sourceURL={sourceURL} /> : null}
</h2>
</div>
);
}

View File

@@ -1,57 +0,0 @@
import type { ApiClass, ApiInterface, Excerpt } from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { ExcerptText } from '../ExcerptText';
export function HierarchyText({
item,
type,
}: {
readonly item: ApiClass | ApiInterface;
readonly type: 'Extends' | 'Implements';
}) {
if (
(item.kind === ApiItemKind.Class &&
(item as ApiClass).extendsType === undefined &&
(item as ApiClass).implementsTypes.length === 0) ||
(item.kind === ApiItemKind.Interface && !(item as ApiInterface).extendsTypes)
) {
return null;
}
let excerpts: Excerpt[];
if (item.kind === ApiItemKind.Class) {
if (type === 'Implements') {
if ((item as ApiClass).implementsTypes.length === 0) {
return null;
}
excerpts = (item as ApiClass).implementsTypes.map((typeExcerpt) => typeExcerpt.excerpt);
} else {
if (!(item as ApiClass).extendsType) {
return null;
}
excerpts = [(item as ApiClass).extendsType!.excerpt];
}
} else {
if ((item as ApiInterface).extendsTypes.length === 0) {
return null;
}
excerpts = (item as ApiInterface).extendsTypes.map((typeExcerpt) => typeExcerpt.excerpt);
}
return (
<div className="flex flex-col gap-4">
{excerpts.map((excerpt, idx) => (
<div className="flex flex-row place-items-center gap-4" key={`${type}-${idx}`}>
<h3 className="text-xl font-bold">{type}</h3>
<span className="break-all font-mono space-y-2">
<ExcerptText excerpt={excerpt} apiPackage={item.getAssociatedPackage()!} />
</span>
</div>
))}
</div>
);
}

View File

@@ -1,15 +0,0 @@
import type { ApiDeclaredItem, ApiItemContainerMixin } from '@discordjs/api-extractor-model';
import { EventsSection } from './section/EventsSection';
import { MethodsSection } from './section/MethodsSection';
import { PropertiesSection } from './section/PropertiesSection';
import { hasEvents, hasProperties, hasMethods } from './util';
export function Members({ item }: { readonly item: ApiDeclaredItem & ApiItemContainerMixin }) {
return (
<>
{hasEvents(item) ? <EventsSection item={item} /> : null}
{hasProperties(item) ? <PropertiesSection item={item} /> : null}
{hasMethods(item) ? <MethodsSection item={item} /> : null}
</>
);
}

View File

@@ -1,24 +0,0 @@
import type { ApiDeclaredItem } from '@discordjs/api-extractor-model';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { Header } from './Header';
import { SummarySection } from './section/SummarySection';
export interface ObjectHeaderProps {
readonly item: ApiDeclaredItem;
}
export function ObjectHeader({ item }: ObjectHeaderProps) {
return (
<>
<Header
kind={item.kind}
name={item.displayName}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
/>
{/* @ts-expect-error async component */}
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
</>
);
}

View File

@@ -1,22 +0,0 @@
import { VscFileCode } from '@react-icons/all-files/vsc/VscFileCode';
export function SourceLink({
className,
sourceURL,
sourceLine,
}: {
readonly className?: string | undefined;
readonly sourceLine?: number | undefined;
readonly sourceURL?: string | undefined;
}) {
return (
<a
className={` text-blurple ${className}`}
href={sourceLine ? `${sourceURL}#L${sourceLine}` : sourceURL}
rel="external noopener noreferrer"
target="_blank"
>
<VscFileCode />
</a>
);
}

View File

@@ -1,22 +0,0 @@
import type { ApiConstructor } from '@discordjs/api-extractor-model';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { CodeHeading } from '~/components/CodeHeading';
import { ParameterTable } from '../../ParameterTable';
import { TSDoc } from '../tsdoc/TSDoc';
import { parametersString } from '../util';
import { DocumentationSection } from './DocumentationSection';
export function ConstructorSection({ item }: { readonly item: ApiConstructor }) {
return (
<DocumentationSection icon={<VscSymbolMethod size={20} />} padded title="Constructor">
<div className="flex flex-col gap-2">
<CodeHeading
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
>{`constructor(${parametersString(item)})`}</CodeHeading>
{item.tsdocComment ? <TSDoc item={item} tsdoc={item.tsdocComment} /> : null}
<ParameterTable item={item} />
</div>
</DocumentationSection>
);
}

View File

@@ -1,14 +0,0 @@
import type { SectionOptions } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
import { Section } from '../../Section';
export function DocumentationSection(opts: PropsWithChildren<SectionOptions & { separator?: boolean }>) {
const { children, separator, ...props } = opts;
return (
<Section {...props}>
{children}
{separator ? <div className="mt-6 border-t-2 border-light-900 dark:border-dark-100" /> : null}
</Section>
);
}

View File

@@ -1,42 +0,0 @@
import {
ApiItemKind,
type ApiEvent,
type ApiItem,
type ApiItemContainerMixin,
type ApiDeclaredItem,
} from '@discordjs/api-extractor-model';
import { VscSymbolEvent } from '@react-icons/all-files/vsc/VscSymbolEvent';
import { Fragment, useMemo } from 'react';
import { Event } from '~/components/model/Event';
import { resolveMembers } from '~/util/members';
import { DocumentationSection } from './DocumentationSection';
function isEventLike(item: ApiItem): item is ApiEvent {
return item.kind === ApiItemKind.Event;
}
export function EventsSection({ item }: { readonly item: ApiItemContainerMixin }) {
const members = resolveMembers(item, isEventLike);
const eventItems = useMemo(
() =>
members.map((event, idx) => {
return (
<Fragment key={`${event.item.displayName}-${idx}`}>
<Event
inheritedFrom={event.inherited as ApiDeclaredItem & ApiItemContainerMixin}
item={event.item as ApiEvent}
/>
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</Fragment>
);
}),
[members],
);
return (
<DocumentationSection icon={<VscSymbolEvent size={20} />} padded title="Events">
<div className="flex flex-col gap-4">{eventItems}</div>
</DocumentationSection>
);
}

View File

@@ -1,45 +0,0 @@
import type {
ApiDeclaredItem,
ApiItem,
ApiItemContainerMixin,
ApiMethod,
ApiMethodSignature,
} from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { VscSymbolMethod } from '@react-icons/all-files/vsc/VscSymbolMethod';
import { useMemo, Fragment } from 'react';
import { resolveMembers } from '~/util/members';
import { Method } from '../../model/method/Method';
import { DocumentationSection } from './DocumentationSection';
function isMethodLike(item: ApiItem): item is ApiMethod | ApiMethodSignature {
return (
item.kind === ApiItemKind.Method ||
(item.kind === ApiItemKind.MethodSignature && (item as ApiMethod).overloadIndex <= 1)
);
}
export function MethodsSection({ item }: { readonly item: ApiItemContainerMixin }) {
const members = resolveMembers(item, isMethodLike);
const methodItems = useMemo(
() =>
members.map(({ item: method, inherited }) => (
<Fragment
key={`${method.displayName}${
method.overloadIndex && method.overloadIndex > 1 ? `:${(method as ApiMethod).overloadIndex}` : ''
}`}
>
<Method inheritedFrom={inherited as ApiDeclaredItem & ApiItemContainerMixin} method={method} />
<div className="border-t-2 border-light-900 dark:border-dark-100" />
</Fragment>
)),
[members],
);
return (
<DocumentationSection icon={<VscSymbolMethod size={20} />} padded title="Methods">
<div className="flex flex-col gap-4">{methodItems}</div>
</DocumentationSection>
);
}

View File

@@ -1,12 +0,0 @@
import type { ApiDocumentedItem, ApiParameterListMixin } from '@discordjs/api-extractor-model';
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { ParameterTable } from '../../ParameterTable';
import { DocumentationSection } from './DocumentationSection';
export function ParameterSection({ item }: { readonly item: ApiDocumentedItem & ApiParameterListMixin }) {
return (
<DocumentationSection icon={<VscSymbolParameter size={20} />} padded title="Parameters">
<ParameterTable item={item} />
</DocumentationSection>
);
}

View File

@@ -1,12 +0,0 @@
import type { ApiItemContainerMixin } from '@discordjs/api-extractor-model';
import { VscSymbolProperty } from '@react-icons/all-files/vsc/VscSymbolProperty';
import { PropertyList } from '../../PropertyList';
import { DocumentationSection } from './DocumentationSection';
export function PropertiesSection({ item }: { readonly item: ApiItemContainerMixin }) {
return (
<DocumentationSection icon={<VscSymbolProperty size={20} />} padded title="Properties">
<PropertyList item={item} />
</DocumentationSection>
);
}

View File

@@ -1,16 +0,0 @@
import type { ApiDeclaredItem } from '@discordjs/api-extractor-model';
import { VscListSelection } from '@react-icons/all-files/vsc/VscListSelection';
import { TSDoc } from '../tsdoc/TSDoc';
import { DocumentationSection } from './DocumentationSection';
export function SummarySection({ item }: { readonly item: ApiDeclaredItem }) {
return (
<DocumentationSection icon={<VscListSelection size={20} />} padded separator title="Summary">
{item.tsdocComment?.summarySection ? (
<TSDoc item={item} tsdoc={item.tsdocComment} />
) : (
<p>No summary provided.</p>
)}
</DocumentationSection>
);
}

View File

@@ -1,12 +0,0 @@
import type { ApiTypeParameterListMixin } from '@discordjs/api-extractor-model';
import { VscSymbolParameter } from '@react-icons/all-files/vsc/VscSymbolParameter';
import { TypeParamTable } from '../../TypeParamTable';
import { DocumentationSection } from './DocumentationSection';
export function TypeParameterSection({ item }: { readonly item: ApiTypeParameterListMixin }) {
return (
<DocumentationSection icon={<VscSymbolParameter size={20} />} padded title="Type Parameters">
<TypeParamTable item={item} />
</DocumentationSection>
);
}

View File

@@ -1,36 +0,0 @@
import { Excerpt, type ApiTypeAlias, type ExcerptToken } from '@discordjs/api-extractor-model';
import { VscSymbolArray } from '@react-icons/all-files/vsc/VscSymbolArray';
import { useMemo } from 'react';
import { ExcerptText } from '~/components/ExcerptText';
import { DocumentationSection } from './DocumentationSection';
export type UnionMember = readonly ExcerptToken[];
export function UnionMembersSection({
item,
members,
}: {
readonly item: ApiTypeAlias;
readonly members: UnionMember[];
}) {
const unionMembers = useMemo(
() =>
members.map((member, idx) => (
<div className="flex flex-row place-items-center gap-4" key={`union-${idx}`}>
<span className="break-all font-mono space-y-2">
<ExcerptText
excerpt={new Excerpt(member, { startIndex: 0, endIndex: member.length })}
apiPackage={item.getAssociatedPackage()!}
/>
</span>
</div>
)),
[item, members],
);
return (
<DocumentationSection icon={<VscSymbolArray size={20} />} padded title="Union Members">
<div className="flex flex-col gap-4">{unionMembers}</div>
</DocumentationSection>
);
}

View File

@@ -1,44 +0,0 @@
import { Alert } from '@discordjs/ui';
import type { PropsWithChildren } from 'react';
export function Block({ children, title }: PropsWithChildren<{ readonly title: string }>) {
return (
<div className="flex flex-col gap-2">
<h5 className="font-bold">{title}</h5>
{children}
</div>
);
}
export function ExampleBlock({
children,
exampleIndex,
}: PropsWithChildren<{
readonly exampleIndex?: number | undefined;
}>): JSX.Element {
return <Block title={`Example ${exampleIndex ? exampleIndex : ''}`}>{children}</Block>;
}
export function DefaultValueBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Default value">{children}</Block>;
}
export function RemarksBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Remarks">{children}</Block>;
}
export function DeprecatedBlock({ children }: PropsWithChildren): JSX.Element {
return (
<Alert title="Deprecated" type="danger">
{children}
</Alert>
);
}
export function SeeBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="See Also">{children}</Block>;
}
export function ReturnsBlock({ children }: PropsWithChildren): JSX.Element {
return <Block title="Returns">{children}</Block>;
}

View File

@@ -1,178 +0,0 @@
import type { ApiItem } from '@discordjs/api-extractor-model';
import type { DocComment, DocFencedCode, DocLinkTag, DocNode, DocNodeContainer, DocPlainText } from '@microsoft/tsdoc';
import { DocNodeKind, StandardTags } from '@microsoft/tsdoc';
import type { Route } from 'next';
import Link from 'next/link';
import { Fragment, useCallback, type ReactNode } from 'react';
import { DocumentationLink } from '~/components/DocumentationLink';
import { BuiltinDocumentationLinks } from '~/util/builtinDocumentationLinks';
import { DISCORD_API_TYPES_DOCS_URL } from '~/util/constants';
import { ItemLink } from '../../ItemLink';
import { SyntaxHighlighter } from '../../SyntaxHighlighter';
import { resolveCanonicalReference, resolveItemURI } from '../util';
import { DefaultValueBlock, DeprecatedBlock, ExampleBlock, RemarksBlock, ReturnsBlock, SeeBlock } from './BlockComment';
export function TSDoc({ item, tsdoc }: { readonly item: ApiItem; readonly tsdoc: DocNode }): JSX.Element {
const createNode = useCallback(
(tsdoc: DocNode, idx?: number): ReactNode => {
switch (tsdoc.kind) {
case DocNodeKind.PlainText:
return (
<span className="break-words" key={idx}>
{(tsdoc as DocPlainText).text}
</span>
);
case DocNodeKind.Section:
case DocNodeKind.Paragraph:
return (
<div className="break-words leading-relaxed" key={idx}>
{(tsdoc as DocNodeContainer).nodes.map((node, idx) => createNode(node, idx))}
</div>
);
case DocNodeKind.SoftBreak:
return <Fragment key={idx} />;
case DocNodeKind.LinkTag: {
const { codeDestination, urlDestination, linkText } = tsdoc as DocLinkTag;
if (codeDestination) {
if (
!codeDestination.importPath &&
!codeDestination.packageName &&
codeDestination.memberReferences.length === 1 &&
codeDestination.memberReferences[0]!.memberIdentifier &&
codeDestination.memberReferences[0]!.memberIdentifier.identifier in BuiltinDocumentationLinks
) {
const typeName = codeDestination.memberReferences[0]!.memberIdentifier.identifier;
const href = BuiltinDocumentationLinks[typeName as keyof typeof BuiltinDocumentationLinks];
return (
<DocumentationLink key={`${typeName}-${idx}`} href={href}>
{typeName}
</DocumentationLink>
);
}
const declarationReference = item.getAssociatedModel()?.resolveDeclarationReference(codeDestination, item);
const foundItem = declarationReference?.resolvedApiItem;
const resolved = resolveCanonicalReference(codeDestination, item.getAssociatedPackage());
if (!foundItem && !resolved) return null;
if (resolved && resolved.package === 'discord-api-types') {
const { displayName, kind, members, containerKey } = resolved.item;
let href = DISCORD_API_TYPES_DOCS_URL;
// dapi-types doesn't have routes for class members
// so we can assume this member is for an enum
if (kind === 'enum' && members?.[0]) {
href += `/enum/${displayName}#${members[0].displayName}`;
} else if (kind === 'type' || kind === 'var') {
href += `#${displayName}`;
} else {
href += `/${kind}/${displayName}`;
}
return (
<DocumentationLink key={`${containerKey}-${idx}`} href={href}>
{displayName}
{members?.map((member) => `.${member.displayName}`).join('') ?? ''}
</DocumentationLink>
);
}
return (
<ItemLink
className="rounded text-blurple font-mono outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
itemURI={resolveItemURI(foundItem ?? resolved!.item)}
key={idx}
packageName={resolved?.package ?? item.getAssociatedPackage()?.displayName.replace('@discordjs/', '')}
version={
resolved?.package
? // eslint-disable-next-line unicorn/better-regex
item.getAssociatedPackage()?.dependencies?.[resolved.package]?.replace(/[~^]/, '')
: undefined
}
>
{linkText ?? foundItem?.displayName ?? resolved!.item.displayName}
</ItemLink>
);
}
if (urlDestination) {
return (
<Link
className="rounded text-blurple font-mono outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
href={urlDestination as Route}
key={idx}
>
{linkText ?? urlDestination}
</Link>
);
}
return null;
}
case DocNodeKind.CodeSpan: {
const { code } = tsdoc as DocFencedCode;
return (
<code className="text-sm font-mono" key={idx}>
{code}
</code>
);
}
case DocNodeKind.FencedCode: {
const { language, code } = tsdoc as DocFencedCode;
// @ts-expect-error async component
return <SyntaxHighlighter code={code.trim()} key={idx} lang={language ?? 'typescript'} />;
}
case DocNodeKind.Comment: {
const comment = tsdoc as DocComment;
const exampleBlocks = comment.customBlocks.filter(
(block) => block.blockTag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase,
);
const defaultValueBlock = comment.customBlocks.find(
(block) => block.blockTag.tagName.toUpperCase() === StandardTags.defaultValue.tagNameWithUpperCase,
);
return (
<div className="flex flex-col gap-2">
{comment.deprecatedBlock ? (
<DeprecatedBlock>{createNode(comment.deprecatedBlock.content)}</DeprecatedBlock>
) : null}
{comment.summarySection ? createNode(comment.summarySection) : null}
{comment.remarksBlock ? <RemarksBlock>{createNode(comment.remarksBlock.content)}</RemarksBlock> : null}
{defaultValueBlock ? (
<DefaultValueBlock>{createNode(defaultValueBlock.content)}</DefaultValueBlock>
) : null}
{comment.returnsBlock ? <ReturnsBlock>{createNode(comment.returnsBlock.content)}</ReturnsBlock> : null}
{exampleBlocks.length
? exampleBlocks.map((block, idx) => <ExampleBlock key={idx}>{createNode(block.content)}</ExampleBlock>)
: null}
{comment.seeBlocks.length ? (
<SeeBlock>{comment.seeBlocks.map((seeBlock, idx) => createNode(seeBlock.content, idx))}</SeeBlock>
) : null}
</div>
);
}
default:
// console.log(`Captured unknown node kind: ${node.kind}`);
return null;
}
},
[item],
);
return (
<>
{tsdoc.kind === 'Paragraph' || tsdoc.kind === 'Section' ? (
<>{(tsdoc as DocNodeContainer).nodes.map((node, idx) => createNode(node, idx))}</>
) : (
createNode(tsdoc)
)}
</>
);
}

View File

@@ -1,173 +0,0 @@
import { ApiItemKind, Meaning } from '@discordjs/api-extractor-model';
import type {
ApiItem,
ApiItemContainerMixin,
ApiMethod,
ApiMethodSignature,
ApiProperty,
ApiPropertySignature,
ApiDocumentedItem,
ApiParameterListMixin,
ApiEvent,
ApiPackage,
} from '@discordjs/api-extractor-model';
import type { DocDeclarationReference } from '@microsoft/tsdoc';
import { SelectorKind } from '@microsoft/tsdoc';
import type { DeclarationReference } from '@microsoft/tsdoc/lib-commonjs/beta/DeclarationReference';
import { METHOD_SEPARATOR, OVERLOAD_SEPARATOR } from '~/util/constants';
import { resolveMembers } from '~/util/members';
import { resolveParameters } from '~/util/model';
import type { TableOfContentsSerialized } from '../TableOfContentItems';
export interface ApiItemLike {
containerKey?: string;
displayName: string;
kind: string;
members?: readonly ApiItemLike[];
parent?: ApiItemLike | undefined;
}
interface ResolvedCanonicalReference {
item: ApiItemLike;
package: string | undefined;
version: string | undefined;
}
const kindToMeaning = new Map([
[ApiItemKind.CallSignature, Meaning.CallSignature],
[ApiItemKind.Class, Meaning.Class],
[ApiItemKind.ConstructSignature, Meaning.ConstructSignature],
[ApiItemKind.Constructor, Meaning.Constructor],
[ApiItemKind.Enum, Meaning.Enum],
[ApiItemKind.Event, Meaning.Event],
[ApiItemKind.Function, Meaning.Function],
[ApiItemKind.IndexSignature, Meaning.IndexSignature],
[ApiItemKind.Interface, Meaning.Interface],
[ApiItemKind.Property, Meaning.Member],
[ApiItemKind.Namespace, Meaning.Namespace],
[ApiItemKind.None, Meaning.ComplexType],
[ApiItemKind.TypeAlias, Meaning.TypeAlias],
[ApiItemKind.Variable, Meaning.Variable],
]);
export function hasProperties(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(
({ item: member }) => member.kind === ApiItemKind.Property || member.kind === ApiItemKind.PropertySignature,
);
}
export function hasMethods(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(
({ item: member }) => member.kind === ApiItemKind.Method || member.kind === ApiItemKind.MethodSignature,
);
}
export function hasEvents(item: ApiItemContainerMixin) {
return resolveMembers(item, memberPredicate).some(({ item: member }) => member.kind === ApiItemKind.Event);
}
export function resolveItemURI(item: ApiItemLike): string {
return !item.parent || item.parent.kind === ApiItemKind.EntryPoint
? `${item.displayName}${OVERLOAD_SEPARATOR}${item.kind}`
: `${item.parent.displayName}${OVERLOAD_SEPARATOR}${item.parent.kind}${METHOD_SEPARATOR}${item.displayName}`;
}
export function resolveCanonicalReference(
canonicalReference: DeclarationReference | DocDeclarationReference,
apiPackage: ApiPackage | undefined,
): ResolvedCanonicalReference | null {
if (
'source' in canonicalReference &&
canonicalReference.source &&
'packageName' in canonicalReference.source &&
canonicalReference.symbol?.componentPath &&
canonicalReference.symbol.meaning
)
return {
package: canonicalReference.source.unscopedPackageName,
item: {
kind: mapMeaningToKind(canonicalReference.symbol.meaning as unknown as Meaning),
displayName: canonicalReference.symbol.componentPath.component.toString(),
containerKey: `|${
canonicalReference.symbol.meaning
}|${canonicalReference.symbol.componentPath.component.toString()}`,
},
// eslint-disable-next-line unicorn/better-regex
version: apiPackage?.dependencies?.[canonicalReference.source.packageName]?.replace(/[~^]/, ''),
};
else if (
'memberReferences' in canonicalReference &&
canonicalReference.memberReferences.length &&
canonicalReference.memberReferences[0]?.memberIdentifier &&
canonicalReference.memberReferences[0]?.selector?.selectorKind === SelectorKind.System
) {
const member = canonicalReference.memberReferences[0]!;
return {
package: canonicalReference.packageName?.replace('@discordjs/', ''),
item: {
kind: member.selector!.selector,
displayName: member.memberIdentifier!.identifier,
containerKey: `|${member.selector!.selector}|${member.memberIdentifier!.identifier}`,
members: canonicalReference.memberReferences
.slice(1)
.map((member) => ({ kind: member.kind, displayName: member.memberIdentifier!.identifier! })),
},
// eslint-disable-next-line unicorn/better-regex
version: apiPackage?.dependencies?.[canonicalReference.packageName ?? '']?.replace(/[~^]/, ''),
};
}
return null;
}
export function mapMeaningToKind(meaning: Meaning): ApiItemKind {
return [...kindToMeaning.entries()].find((mapping) => mapping[1] === meaning)?.[0] ?? ApiItemKind.None;
}
export function mapKindToMeaning(kind: ApiItemKind): Meaning {
return kindToMeaning.get(kind) ?? Meaning.Variable;
}
export function memberPredicate(
item: ApiItem,
): item is ApiEvent | ApiMethod | ApiMethodSignature | ApiProperty | ApiPropertySignature {
return (
item.kind === ApiItemKind.Property ||
item.kind === ApiItemKind.PropertySignature ||
item.kind === ApiItemKind.Method ||
item.kind === ApiItemKind.MethodSignature ||
item.kind === ApiItemKind.Event
);
}
export function serializeMembers(clazz: ApiItemContainerMixin): TableOfContentsSerialized[] {
return resolveMembers(clazz, memberPredicate).map(({ item: member }) => {
if (member.kind === 'Method' || member.kind === 'MethodSignature') {
return {
kind: member.kind as 'Method' | 'MethodSignature',
name: member.displayName,
overloadIndex: (member as ApiMethod | ApiMethodSignature).overloadIndex,
};
} else if (member.kind === 'Event') {
return {
kind: member.kind as 'Event',
name: member.displayName,
};
} else {
return {
kind: member.kind as 'Property' | 'PropertySignature',
name: member.displayName,
};
}
});
}
export function parametersString(item: ApiDocumentedItem & ApiParameterListMixin) {
return resolveParameters(item).reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isRest ? '...' : ''}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isRest ? '...' : ''}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '');
}

View File

@@ -1,32 +0,0 @@
import type { ApiClass, ApiConstructor } from '@discordjs/api-extractor-model';
import { ApiItemKind } from '@discordjs/api-extractor-model';
import { Badges } from '../Badges';
import { Documentation } from '../documentation/Documentation';
import { HierarchyText } from '../documentation/HierarchyText';
import { Members } from '../documentation/Members';
import { ObjectHeader } from '../documentation/ObjectHeader';
import { ConstructorSection } from '../documentation/section/ConstructorSection';
import { TypeParameterSection } from '../documentation/section/TypeParametersSection';
import { serializeMembers } from '../documentation/util';
import { OutlineSetter } from './OutlineSetter';
export function Class({ clazz }: { readonly clazz: ApiClass }) {
const constructor = clazz.members.find((member) => member.kind === ApiItemKind.Constructor) as
| ApiConstructor
| undefined;
const outlineMembers = serializeMembers(clazz);
return (
<Documentation>
<Badges item={clazz} />
<ObjectHeader item={clazz} />
<HierarchyText item={clazz} type="Extends" />
<HierarchyText item={clazz} type="Implements" />
{clazz.typeParameters.length ? <TypeParameterSection item={clazz} /> : null}
{constructor ? <ConstructorSection item={constructor} /> : null}
<Members item={clazz} />
<OutlineSetter members={outlineMembers} />
</Documentation>
);
}

View File

@@ -1,38 +0,0 @@
import type { ApiDeclaredItem, ApiItemContainerMixin, ApiEvent } from '@discordjs/api-extractor-model';
import { Badges } from '../Badges';
import { CodeHeading } from '../CodeHeading';
import { InheritanceText } from '../InheritanceText';
import { ParameterTable } from '../ParameterTable';
import { TSDoc } from '../documentation/tsdoc/TSDoc';
export function Event({
item,
inheritedFrom,
}: {
readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined;
readonly item: ApiEvent;
}) {
const hasSummary = Boolean(item.tsdocComment?.summarySection);
return (
<div className="flex flex-col scroll-mt-30 gap-4" id={item.displayName}>
<div className="flex flex-col gap-2 md:-ml-9">
<Badges item={item} />
<CodeHeading
href={`#${item.displayName}`}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
>
{item.name}
</CodeHeading>
</div>
{hasSummary || inheritedFrom ? (
<div className="mb-4 w-full flex flex-col gap-4">
{item.tsdocComment ? <TSDoc item={item} tsdoc={item.tsdocComment} /> : null}
{item.parameters.length ? <ParameterTable item={item} /> : null}
{inheritedFrom ? <InheritanceText parent={inheritedFrom} /> : null}
</div>
) : null}
</div>
);
}

View File

@@ -1,22 +0,0 @@
import type { ApiInterface } from '@discordjs/api-extractor-model';
import { Documentation } from '../documentation/Documentation';
import { HierarchyText } from '../documentation/HierarchyText';
import { Members } from '../documentation/Members';
import { ObjectHeader } from '../documentation/ObjectHeader';
import { TypeParameterSection } from '../documentation/section/TypeParametersSection';
import { serializeMembers } from '../documentation/util';
import { OutlineSetter } from './OutlineSetter';
export function Interface({ item }: { readonly item: ApiInterface }) {
const outlineMembers = serializeMembers(item);
return (
<Documentation>
<ObjectHeader item={item} />
<HierarchyText item={item} type="Extends" />
{item.typeParameters.length ? <TypeParameterSection item={item} /> : null}
<Members item={item} />
<OutlineSetter members={outlineMembers} />
</Documentation>
);
}

View File

@@ -1,19 +0,0 @@
'use client';
import { useEffect, type PropsWithChildren } from 'react';
import { useOutline } from '~/contexts/outline';
import type { TableOfContentsSerialized } from '../TableOfContentItems';
export function OutlineSetter({ members }: PropsWithChildren<{ readonly members: TableOfContentsSerialized[] }>) {
const { setMembers } = useOutline();
useEffect(() => {
setMembers(members);
return () => {
setMembers(null);
};
}, [members, setMembers]);
return null;
}

View File

@@ -1,65 +0,0 @@
import { ExcerptTokenKind, type ApiTypeAlias, ExcerptToken } from '@discordjs/api-extractor-model';
import { useMemo } from 'react';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { Documentation } from '../documentation/Documentation';
import { Header } from '../documentation/Header';
import { SummarySection } from '../documentation/section/SummarySection';
import { UnionMembersSection } from '../documentation/section/UnionMembersSection';
export function TypeAlias({ item }: { readonly item: ApiTypeAlias }) {
const union = useMemo(() => {
const union: ExcerptToken[][] = [];
let currentUnionMember: ExcerptToken[] = [];
let depth = 0;
for (const token of item.typeExcerpt.spannedTokens) {
if (token.text.includes('?')) {
return [item.typeExcerpt.spannedTokens];
}
depth += token.text.split('<').length - token.text.split('>').length;
if (token.text.trim() === '|' && depth === 0) {
if (currentUnionMember.length) {
union.push(currentUnionMember);
currentUnionMember = [];
}
} else if (depth === 0 && token.kind === ExcerptTokenKind.Content && token.text.includes('|')) {
for (const [idx, tokenpart] of token.text.split('|').entries()) {
if (currentUnionMember.length && depth === 0 && idx === 0) {
currentUnionMember.push(new ExcerptToken(ExcerptTokenKind.Content, tokenpart));
union.push(currentUnionMember);
currentUnionMember = [];
} else if (currentUnionMember.length && depth === 0) {
union.push(currentUnionMember);
currentUnionMember = [new ExcerptToken(ExcerptTokenKind.Content, tokenpart)];
} else if (tokenpart.length) {
currentUnionMember.push(new ExcerptToken(ExcerptTokenKind.Content, tokenpart));
}
}
} else {
currentUnionMember.push(token);
}
}
if (currentUnionMember.length) {
union.push(currentUnionMember);
}
return union;
}, [item]);
return (
<Documentation>
<Header
kind={item.kind}
name={item.displayName}
sourceURL={item.sourceLocation.fileUrl}
sourceLine={item.sourceLocation.fileLine}
/>
{/* @ts-expect-error async component */}
<SyntaxHighlighter code={item.excerpt.text} />
<SummarySection item={item} />
{union.length ? <UnionMembersSection item={item} members={union} /> : null}
</Documentation>
);
}

View File

@@ -1,11 +0,0 @@
import type { ApiVariable } from '@discordjs/api-extractor-model';
import { Documentation } from '../documentation/Documentation';
import { ObjectHeader } from '../documentation/ObjectHeader';
export function Variable({ item }: { readonly item: ApiVariable }) {
return (
<Documentation>
<ObjectHeader item={item} />
</Documentation>
);
}

View File

@@ -1,24 +0,0 @@
import type { ApiEnum } from '@discordjs/api-extractor-model';
import { VscSymbolEnum } from '@react-icons/all-files/vsc/VscSymbolEnum';
import { Panel } from '../../Panel';
import { Documentation } from '../../documentation/Documentation';
import { ObjectHeader } from '../../documentation/ObjectHeader';
import { DocumentationSection } from '../../documentation/section/DocumentationSection';
import { EnumMember } from './EnumMember';
export function Enum({ item }: { readonly item: ApiEnum }) {
return (
<Documentation>
<ObjectHeader item={item} />
<DocumentationSection icon={<VscSymbolEnum size={20} />} padded title="Members">
<div className="flex flex-col gap-4">
{item.members.map((member, idx) => (
<Panel key={`${member.displayName}-${idx}`}>
<EnumMember member={member} />
</Panel>
))}
</div>
</DocumentationSection>
</Documentation>
);
}

View File

@@ -1,24 +0,0 @@
import type { ApiEnumMember } from '@discordjs/api-extractor-model';
import { CodeHeading } from '~/components/CodeHeading';
import { SignatureText } from '../../SignatureText';
import { TSDoc } from '../../documentation/tsdoc/TSDoc';
export function EnumMember({ member }: { readonly member: ApiEnumMember }) {
return (
<div className="flex flex-col scroll-mt-30" id={member.displayName}>
<CodeHeading
className="md:-ml-8.5"
href={`#${member.displayName}`}
sourceURL={member.sourceLocation.fileUrl}
sourceLine={member.sourceLocation.fileLine}
>
{member.name}
<span>=</span>
{member.initializerExcerpt ? (
<SignatureText excerpt={member.initializerExcerpt} apiPackage={member.getAssociatedPackage()!} />
) : null}
</CodeHeading>
{member.tsdocComment ? <TSDoc item={member} tsdoc={member.tsdocComment.summarySection} /> : null}
</div>
);
}

View File

@@ -1,27 +0,0 @@
import type { ApiFunction } from '@discordjs/api-extractor-model';
import dynamic from 'next/dynamic';
import { Documentation } from '~/components/documentation/Documentation';
import { ObjectHeader } from '~/components/documentation/ObjectHeader';
import { FunctionBody } from './FunctionBody';
const OverloadSwitcher = dynamic(async () => import('../../OverloadSwitcher'));
export function Function({ item }: { readonly item: ApiFunction }) {
if (item.getMergedSiblings().length > 1) {
const overloads = item.getMergedSiblings().map((sibling, idx) => (
<Documentation key={`${sibling.displayName}-${idx}`}>
<ObjectHeader item={sibling as ApiFunction} />
<FunctionBody item={sibling as ApiFunction} />
</Documentation>
));
return <OverloadSwitcher methodName={item.displayName} overloads={overloads} />;
}
return (
<Documentation>
<ObjectHeader item={item} />
<FunctionBody item={item} />
</Documentation>
);
}

View File

@@ -1,17 +0,0 @@
import type { ApiFunction } from '@discordjs/api-extractor-model';
import { ParameterSection } from '../../documentation/section/ParametersSection';
import { TypeParameterSection } from '../../documentation/section/TypeParametersSection';
export interface FunctionBodyProps {
mergedSiblingCount: number;
overloadDocumentation: React.ReactNode[];
}
export function FunctionBody({ item }: { readonly item: ApiFunction }) {
return (
<>
{item.typeParameters.length ? <TypeParameterSection item={item} /> : null}
{item.parameters.length ? <ParameterSection item={item} /> : null}
</>
);
}

View File

@@ -1,50 +0,0 @@
import {
ApiItemKind,
type ApiDeclaredItem,
type ApiItemContainerMixin,
type ApiMethod,
type ApiMethodSignature,
} from '@discordjs/api-extractor-model';
import dynamic from 'next/dynamic';
import { Fragment } from 'react';
import { MethodDocumentation } from './MethodDocumentation';
import { MethodHeader } from './MethodHeader';
const OverloadSwitcher = dynamic(async () => import('../../OverloadSwitcher'));
export function Method({
method,
inheritedFrom,
}: {
readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined;
readonly method: ApiMethod | ApiMethodSignature;
}) {
if (
method
.getMergedSiblings()
.filter((sibling) => sibling.kind === ApiItemKind.Method || sibling.kind === ApiItemKind.MethodSignature).length >
1
) {
// We have overloads, use the overload switcher, but render
// each overload node on the server.
const overloads = method
.getMergedSiblings()
.filter((sibling) => sibling.kind === ApiItemKind.Method || sibling.kind === ApiItemKind.MethodSignature)
.map((sibling, idx) => (
<Fragment key={`${sibling.displayName}-${idx}`}>
<MethodHeader method={sibling as ApiMethod | ApiMethodSignature} />
<MethodDocumentation method={sibling as ApiMethod | ApiMethodSignature} />
</Fragment>
));
return <OverloadSwitcher methodName={method.displayName} overloads={overloads} />;
}
// We have just a single method, render it on the server.
return (
<>
<MethodHeader method={method} />
<MethodDocumentation inheritedFrom={inheritedFrom} method={method} />
</>
);
}

View File

@@ -1,40 +0,0 @@
import {
ApiItemKind,
type ApiDeclaredItem,
type ApiItemContainerMixin,
type ApiMethod,
type ApiMethodSignature,
} from '@discordjs/api-extractor-model';
import { ParameterSection } from '~/components/documentation/section/ParametersSection';
import { TypeParameterSection } from '~/components/documentation/section/TypeParametersSection';
import { InheritanceText } from '../../InheritanceText';
import { TSDoc } from '../../documentation/tsdoc/TSDoc';
export interface MethodDocumentationProps {
readonly inheritedFrom?: (ApiDeclaredItem & ApiItemContainerMixin) | undefined;
readonly method: ApiMethod | ApiMethodSignature;
}
export function MethodDocumentation({ method, inheritedFrom }: MethodDocumentationProps) {
const parent = method.parent as ApiDeclaredItem;
const firstOverload = method
.getMergedSiblings()
.find(
(meth): meth is ApiMethod => meth.kind === ApiItemKind.Method && (meth as ApiMethod).overloadIndex === 1,
)?.tsdocComment;
if (!(method.tsdocComment?.summarySection || firstOverload?.summarySection || method.parameters.length > 0)) {
return null;
}
return (
<div className="mb-4 w-full flex flex-col gap-4">
{method.tsdocComment || firstOverload ? (
<TSDoc item={method} tsdoc={method.tsdocComment ?? firstOverload!} />
) : null}
{method.typeParameters.length ? <TypeParameterSection item={method} /> : null}
{method.parameters.length ? <ParameterSection item={method} /> : null}
{inheritedFrom && parent ? <InheritanceText parent={inheritedFrom} /> : null}
</div>
);
}

View File

@@ -1,30 +0,0 @@
import type { ApiMethod, ApiMethodSignature } from '@discordjs/api-extractor-model';
import { useMemo } from 'react';
import { Badges } from '~/components/Badges';
import { CodeHeading } from '~/components/CodeHeading';
import { ExcerptText } from '~/components/ExcerptText';
import { parametersString } from '~/components/documentation/util';
export function MethodHeader({ method }: { readonly method: ApiMethod | ApiMethodSignature }) {
const key = useMemo(
() => `${method.displayName}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`,
[method.displayName, method.overloadIndex],
);
return (
<div className="w-full flex flex-col scroll-mt-30" id={key}>
<div className="flex flex-col gap-2 md:-ml-9">
<Badges item={method} />
<CodeHeading
href={`#${key}`}
sourceLine={method.sourceLocation.fileLine}
sourceURL={method.sourceLocation.fileUrl}
>
{`${method.name}(${parametersString(method)})`}
<span>:</span>
<ExcerptText excerpt={method.returnTypeExcerpt} apiPackage={method.getAssociatedPackage()!} />
</CodeHeading>
</div>
</div>
);
}

View 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>
);
}

View File

@@ -0,0 +1,3 @@
'use client';
export { Button } from 'react-aria-components';

View 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>
);
}

View File

@@ -0,0 +1,3 @@
'use client';
export { Collapsible, CollapsibleTrigger, CollapsibleContent } from '@radix-ui/react-collapsible';

View 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>
);
}

View File

@@ -2,13 +2,13 @@ import Image from 'next/image';
import vercelLogo from '~/assets/powered-by-vercel.svg';
import workersLogo from '~/assets/powered-by-workers.png';
export default function Footer() {
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 outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
className="rounded"
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
rel="external noopener noreferrer"
target="_blank"
@@ -24,7 +24,7 @@ export default function Footer() {
/>
</a>
<a
className="rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
className="rounded"
href="https://www.cloudflare.com"
rel="external noopener noreferrer"
target="_blank"
@@ -40,20 +40,15 @@ export default function Footer() {
/>
</a>
</div>
<div className="flex flex-col place-self-center gap-6 sm:flex-row md:gap-12">
<div className="max-w-max flex flex-col gap-2">
<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 outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
href="https://discord.gg/djs"
rel="external noopener noreferrer"
target="_blank"
>
<a className="rounded" href="https://discord.gg/djs" rel="external noopener noreferrer" target="_blank">
Discord
</a>
<a
className="rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
className="rounded"
href="https://github.com/discordjs/discord.js/discussions"
rel="external noopener noreferrer"
target="_blank"
@@ -62,27 +57,22 @@ export default function Footer() {
</a>
</div>
</div>
<div className="max-w-max flex flex-col gap-2">
<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 outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
className="rounded"
href="https://github.com/discordjs/discord.js"
rel="external noopener noreferrer"
target="_blank"
>
discord.js
</a>
<a
className="rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
href="https://discord.js.org/docs"
rel="noopener noreferrer"
target="_blank"
>
<a className="rounded" href="https://discord.js.org/docs" rel="noopener noreferrer" target="_blank">
discord.js documentation
</a>
<a
className="rounded outline-none focus:ring focus:ring-width-2 focus:ring-blurple"
className="rounded"
href="https://discord-api-types.dev"
rel="external noopener noreferrer"
target="_blank"

View 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>
);
}

View File

@@ -0,0 +1,3 @@
'use client';
export { ListBox, ListBoxItem } from 'react-aria-components';

View 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>
</>
);
}

View File

@@ -0,0 +1,3 @@
'use client';
export { Popover } from 'react-aria-components';

View 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>
);
}

View File

@@ -0,0 +1,3 @@
'use client';
export { Select, SelectValue } from 'react-aria-components';

View File

@@ -0,0 +1,3 @@
'use client';
export { Tabs, TabList, Tab, TabPanel } from 'react-aria-components';

View 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>
);
}

View 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>
</>
);
}