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