refactor: website components (#8600)

This commit is contained in:
Noel
2022-09-06 19:48:33 +02:00
committed by GitHub
parent f3ce4a75d0
commit c3341570d9
55 changed files with 1910 additions and 2673 deletions

View File

@@ -1,6 +1,4 @@
import type { TokenDocumentation, ApiItemJSON, AnyDocNodeJSON, InheritanceData } from '@discordjs/api-extractor-utils';
import { ActionIcon, Badge, Box, createStyles, Group, MediaQuery, Stack, Title } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import type { PropsWithChildren } from 'react';
import { FiLink } from 'react-icons/fi';
import { HyperlinkedText } from './HyperlinkedText';
@@ -12,19 +10,6 @@ export enum CodeListingSeparatorType {
Value = '=',
}
const useStyles = createStyles((theme) => ({
outer: {
display: 'flex',
alignItems: 'center',
gap: 16,
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
alignItems: 'unset',
},
},
}));
export function CodeListing({
name,
separator = CodeListingSeparatorType.Type,
@@ -47,48 +32,51 @@ export function CodeListing({
summary?: ApiItemJSON['summary'];
typeTokens: TokenDocumentation[];
}>) {
const { classes } = useStyles();
const matches = useMediaQuery('(max-width: 768px)');
return (
<Stack id={name} className="scroll-mt-30" spacing="xs">
<Box className={classes.outer} ml={matches ? 0 : -45}>
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
<ActionIcon component="a" href={`#${name}`} variant="transparent">
<FiLink size={20} />
</ActionIcon>
</MediaQuery>
<div id={name} className="scroll-mt-30 flex flex-col gap-4">
<div className={`md:-ml-8.5 flex flex-col gap-0.5 md:flex-row md:place-items-center md:gap-2`}>
<a className="hidden md:inline-block" href={`#${name}`}>
<FiLink size={20} />
</a>
{deprecation || readonly || optional ? (
<Group spacing={10} noWrap>
<div className="flex flex-row flex-wrap gap-1">
{deprecation ? (
<Badge variant="filled" color="red">
<div className="h-5 place-content-center rounded-full bg-red-500 px-3 text-center text-xs font-semibold uppercase text-white">
Deprecated
</Badge>
</div>
) : null}
{readonly ? <Badge variant="filled">Readonly</Badge> : null}
{optional ? <Badge variant="filled">Optional</Badge> : null}
</Group>
{readonly ? (
<div className="bg-blurple h-5 place-content-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Readonly
</div>
) : null}
{optional ? (
<div className="bg-blurple h-5 place-content-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Optional
</div>
) : null}
</div>
) : null}
<Group spacing={10}>
<Title order={4} className="font-mono">
<div className="flex flex-row flex-wrap place-items-center gap-1">
<h4 className="break-all font-mono text-lg font-bold">
{name}
{optional ? '?' : ''}
</Title>
<Title order={4}>{separator}</Title>
<Title sx={{ wordBreak: 'break-all' }} order={4} className="font-mono">
</h4>
<h4 className="font-mono text-lg font-bold">{separator}</h4>
<h4 className="break-all font-mono text-lg font-bold">
<HyperlinkedText tokens={typeTokens} />
</Title>
</Group>
</Box>
<Group>
<Stack>
</h4>
</div>
</div>
{summary || inheritanceData ? (
<div className="flex flex-col gap-4">
{deprecation ? <TSDoc node={deprecation} /> : null}
{summary && <TSDoc node={summary} />}
{comment && <TSDoc node={comment} />}
{summary ? <TSDoc node={summary} /> : null}
{comment ? <TSDoc node={comment} /> : null}
{inheritanceData ? <InheritanceText data={inheritanceData} /> : null}
{children}
</Stack>
</Group>
</Stack>
</div>
) : null}
</div>
);
}

View File

@@ -5,22 +5,8 @@ import type {
ApiClassJSON,
ApiInterfaceJSON,
} from '@discordjs/api-extractor-utils';
import {
Group,
Stack,
Title,
Text,
Box,
MediaQuery,
Aside,
ScrollArea,
Skeleton,
Divider,
useMantineColorScheme,
} from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useRouter } from 'next/router';
import { Fragment, type PropsWithChildren } from 'react';
import { Scrollbars } from 'react-custom-scrollbars-2';
import {
VscSymbolClass,
VscSymbolMethod,
@@ -30,10 +16,10 @@ import {
VscListSelection,
VscSymbolParameter,
} from 'react-icons/vsc';
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus, ghcolors } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { useMedia } from 'react-use';
import { HyperlinkedText } from './HyperlinkedText';
import { Section } from './Section';
import { SyntaxHighlighter } from './SyntaxHighlighter';
import { TableOfContentItems } from './TableOfContentItems';
import { TypeParamTable } from './TypeParamTable';
import { TSDoc } from './tsdoc/TSDoc';
@@ -75,95 +61,76 @@ export function DocContainer({
methods,
properties,
}: DocContainerProps) {
const router = useRouter();
const matches = useMediaQuery('(max-width: 768px)');
const { colorScheme } = useMantineColorScheme();
const matches = useMedia('(max-width: 768px)', true);
return (
<Group>
<Stack sx={{ flexGrow: 1, maxWidth: '100%' }}>
<Skeleton visible={router.isFallback} radius="sm">
<Title sx={{ wordBreak: 'break-all' }} order={2} ml="xs">
<Group>
{generateIcon(kind)}
{name}
</Group>
</Title>
</Skeleton>
<>
<div className="flex flex-col gap-4">
<h2 className="flex flex-row place-items-center gap-2 break-all text-2xl font-bold">
<span>{generateIcon(kind)}</span>
{name}
</h2>
<Skeleton visible={router.isFallback} radius="sm">
<Section title="Summary" icon={<VscListSelection size={20} />} padded dense={matches}>
{summary ? <TSDoc node={summary} /> : <Text>No summary provided.</Text>}
<Divider size="md" mt={20} />
</Section>
</Skeleton>
<Section title="Summary" icon={<VscListSelection size={20} />} padded dense={matches}>
{summary ? <TSDoc node={summary} /> : <span>No summary provided.</span>}
<div className="border-light-900 -mx-8 mt-6 border-t-2" />
</Section>
<Skeleton visible={router.isFallback} radius="sm">
<Box pb="xs">
<SyntaxHighlighter
wrapLongLines
language="typescript"
style={colorScheme === 'dark' ? vscDarkPlus : ghcolors}
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
>
{excerpt}
</SyntaxHighlighter>
</Box>
</Skeleton>
<SyntaxHighlighter code={excerpt} />
{extendsTokens?.length ? (
<Group pb="xs" noWrap>
<Title order={3} ml="xs">
Extends
</Title>
<Text sx={{ wordBreak: 'break-all' }} className="font-mono">
<div className="flex flex-row place-items-center gap-4">
<h3 className="text-xl font-bold">Extends</h3>
<span className="break-all font-mono">
<HyperlinkedText tokens={extendsTokens} />
</Text>
</Group>
</span>
</div>
) : null}
{implementsTokens?.length ? (
<Group pb="xs" noWrap>
<Title order={3} ml="xs">
Implements
</Title>
<Text sx={{ wordBreak: 'break-all' }} className="font-mono">
<div className="flex flex-row place-items-center gap-4">
<h3 className="text-xl font-bold">Implements</h3>
<span className="break-all font-mono">
{implementsTokens.map((token, idx) => (
<Fragment key={idx}>
<HyperlinkedText tokens={token} />
{idx < implementsTokens.length - 1 ? ', ' : ''}
</Fragment>
))}
</Text>
</Group>
</span>
</div>
) : null}
<Skeleton visible={router.isFallback} radius="sm">
<Stack>
{typeParams?.length ? (
<Section
title="Type Parameters"
icon={<VscSymbolParameter size={20} />}
padded
dense={matches}
defaultClosed
>
<TypeParamTable data={typeParams} />
</Section>
) : null}
<Stack>{children}</Stack>
</Stack>
</Skeleton>
</Stack>
<div className="flex flex-col gap-4">
{typeParams?.length ? (
<Section
title="Type Parameters"
icon={<VscSymbolParameter size={20} />}
padded
dense={matches}
defaultClosed
>
<TypeParamTable data={typeParams} />
</Section>
) : null}
{children}
</div>
</div>
{(kind === 'Class' || kind === 'Interface') && (methods?.length || properties?.length) ? (
<MediaQuery smallerThan="lg" styles={{ display: 'none' }}>
<Aside hiddenBreakpoint="lg" width={{ lg: 250 }} withBorder>
<ScrollArea p="sm" offsetScrollbars>
<TableOfContentItems properties={properties ?? []} methods={methods ?? []} />
</ScrollArea>
</Aside>
</MediaQuery>
<aside className="h-[calc(100vh - 72px)] dark:bg-dark-600 dark:border-dark-100 border-light-800 fixed top-[72px] right-0 bottom-0 z-20 hidden w-64 border-l bg-white pr-2 xl:block">
<Scrollbars
universal
autoHide
hideTracksWhenNotNeeded
renderTrackVertical={(props) => (
<div {...props} className="absolute top-0.5 right-0.5 bottom-0.5 z-30 w-1.5 rounded" />
)}
renderThumbVertical={(props) => <div {...props} className="dark:bg-dark-100 bg-light-900 z-30 rounded" />}
>
<TableOfContentItems properties={properties ?? []} methods={methods ?? []} />
</Scrollbars>
</aside>
) : null}
</Group>
</>
);
}

View File

@@ -1,5 +1,4 @@
import type { TokenDocumentation } from '@discordjs/api-extractor-utils';
import { Anchor, Text } from '@mantine/core';
import Link from 'next/link';
export function HyperlinkedText({ tokens }: { tokens: TokenDocumentation[] }) {
@@ -8,19 +7,13 @@ export function HyperlinkedText({ tokens }: { tokens: TokenDocumentation[] }) {
{tokens.map((token, idx) => {
if (token.path) {
return (
<Link key={idx} href={token.path} passHref prefetch={false}>
<Anchor component="a" inherit>
{token.text}
</Anchor>
<Link key={idx} href={token.path} prefetch={false}>
<a className="text-blurple">{token.text}</a>
</Link>
);
}
return (
<Text key={idx} span unstyled>
{token.text}
</Text>
);
return <span key={idx}>{token.text}</span>;
})}
</>
);

View File

@@ -1,16 +1,13 @@
import type { InheritanceData } from '@discordjs/api-extractor-utils';
import { Anchor, Text } from '@mantine/core';
import Link from 'next/link';
export function InheritanceText({ data }: { data: InheritanceData }) {
return (
<Text weight={600}>
<span className="font-semibold">
{'Inherited from '}
<Link href={data.path} passHref prefetch={false}>
<Anchor component="a" className="font-mono">
{data.parentName}
</Anchor>
<Link href={data.path} prefetch={false}>
<a className="text-blurple font-mono">{data.parentName}</a>
</Link>
</Text>
</span>
);
}

View File

@@ -1,86 +1,77 @@
import type { ApiMethodJSON, ApiMethodSignatureJSON } from '@discordjs/api-extractor-utils';
import { ActionIcon, Badge, Box, createStyles, Group, MediaQuery, Stack, Title } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useCallback, useMemo } from 'react';
import { FiLink } from 'react-icons/fi';
import { HyperlinkedText } from './HyperlinkedText';
import { InheritanceText } from './InheritanceText';
import { ParameterTable } from './ParameterTable';
import { TSDoc } from './tsdoc/TSDoc';
const useStyles = createStyles((theme) => ({
outer: {
display: 'flex',
alignItems: 'center',
gap: 16,
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
alignItems: 'unset',
},
},
}));
function getShorthandName(data: ApiMethodJSON | ApiMethodSignatureJSON) {
return `${data.name}${data.optional ? '?' : ''}(${data.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`;
}
export function MethodItem({ data }: { data: ApiMethodJSON | ApiMethodSignatureJSON }) {
const { classes } = useStyles();
const matches = useMediaQuery('(max-width: 768px)');
const method = data as ApiMethodJSON;
const key = `${data.name}${data.overloadIndex && data.overloadIndex > 1 ? `:${data.overloadIndex}` : ''}`;
const key = useMemo(
() => `${data.name}${data.overloadIndex && data.overloadIndex > 1 ? `:${data.overloadIndex}` : ''}`,
[data.name, data.overloadIndex],
);
const getShorthandName = useCallback(
(data: ApiMethodJSON | ApiMethodSignatureJSON) =>
`${data.name}${data.optional ? '?' : ''}(${data.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`,
[],
);
return (
<Stack id={key} className="scroll-mt-30" spacing="xs">
<Group>
<Stack>
<Box className={classes.outer} ml={matches ? 0 : -45}>
<MediaQuery smallerThan="sm" styles={{ display: 'none' }}>
<ActionIcon component="a" href={`#${key}`} variant="transparent" color="dark">
<FiLink size={20} />
</ActionIcon>
</MediaQuery>
{data.deprecated ||
(data.kind === 'Method' && method.protected) ||
(data.kind === 'Method' && method.static) ? (
<Group spacing={10} noWrap>
{data.deprecated ? (
<Badge variant="filled" color="red">
Deprecated
</Badge>
) : null}
{data.kind === 'Method' && method.protected ? <Badge variant="filled">Protected</Badge> : null}
{data.kind === 'Method' && method.static ? <Badge variant="filled">Static</Badge> : null}
</Group>
) : null}
<Group spacing={10}>
<Title sx={{ wordBreak: 'break-all' }} order={4} className="font-mono">{`${getShorthandName(
data,
)}`}</Title>
<Title order={4}>:</Title>
<Title sx={{ wordBreak: 'break-all' }} order={4} className="font-mono">
<HyperlinkedText tokens={data.returnTypeTokens} />
</Title>
</Group>
</Box>
</Stack>
</Group>
<Group sx={{ display: data.summary || data.parameters.length ? 'block' : 'none' }} mb="lg">
<Stack>
<div id={key} className="scroll-mt-30 flex flex-col gap-4">
<div className="flex flex-col">
<div className="flex flex-col gap-2 md:-ml-9 md:flex-row md:place-items-center">
<a className="hidden md:inline-block" href={`#${key}`}>
<FiLink size={20} />
</a>
{data.deprecated ||
(data.kind === 'Method' && method.protected) ||
(data.kind === 'Method' && method.static) ? (
<div className="flex flex-row flex-wrap gap-1">
{data.deprecated ? (
<div className="h-5 place-content-center rounded-full bg-red-500 px-3 text-center text-xs font-semibold uppercase text-white">
Deprecated
</div>
) : null}
{data.kind === 'Method' && method.protected ? (
<div className="bg-blurple h-5 place-content-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Protected
</div>
) : null}
{data.kind === 'Method' && method.static ? (
<div className="bg-blurple h-5 place-content-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Static
</div>
) : null}
</div>
) : null}
<div className="flex flex-row flex-wrap gap-1">
<h4 className="break-all font-mono text-lg font-bold">{`${getShorthandName(data)}`}</h4>
<h4 className="font-mono text-lg font-bold">:</h4>
<h4 className="break-all font-mono text-lg font-bold">
<HyperlinkedText tokens={data.returnTypeTokens} />
</h4>
</div>
</div>
</div>
{data.summary || data.parameters.length ? (
<div className="mb-4 flex flex-col gap-4">
{data.deprecated ? <TSDoc node={data.deprecated} /> : null}
{data.summary ? <TSDoc node={data.summary} /> : null}
{data.remarks ? <TSDoc node={data.remarks} /> : null}
{data.comment ? <TSDoc node={data.comment} /> : null}
{data.parameters.length ? <ParameterTable data={data.parameters} /> : null}
{data.inheritanceData ? <InheritanceText data={data.inheritanceData} /> : null}
</Stack>
</Group>
</Stack>
</div>
) : null}
</div>
);
}

View File

@@ -1,19 +1,20 @@
import type { ApiMethodJSON, ApiMethodSignatureJSON } from '@discordjs/api-extractor-utils';
import { Divider, Stack } from '@mantine/core';
import { Fragment } from 'react';
import { Fragment, useMemo } from 'react';
import { MethodItem } from './MethodItem';
export function MethodList({ data }: { data: (ApiMethodJSON | ApiMethodSignatureJSON)[] }) {
return (
<Stack>
{data.map((method) => (
const methodItems = useMemo(
() =>
data.map((method) => (
<Fragment
key={`${method.name}${method.overloadIndex && method.overloadIndex > 1 ? `:${method.overloadIndex}` : ''}`}
>
<MethodItem data={method} />
<Divider size="md" />
<div className="border-light-900 -mx-8 border-t-2" />
</Fragment>
))}
</Stack>
)),
[data],
);
return <div className="flex flex-col gap-4">{methodItems}</div>;
}

View File

@@ -1,5 +1,5 @@
import type { ParameterDocumentation } from '@discordjs/api-extractor-utils';
import { Box, ScrollArea } from '@mantine/core';
import { useMemo } from 'react';
import { HyperlinkedText } from './HyperlinkedText';
import { Table } from './Table';
import { TSDoc } from './tsdoc/TSDoc';
@@ -10,18 +10,20 @@ const columnStyles = {
};
export function ParameterTable({ data }: { data: ParameterDocumentation[] }) {
const rows = data.map((param) => ({
Name: param.name,
Type: <HyperlinkedText tokens={param.tokens} />,
Optional: param.isOptional ? 'Yes' : 'No',
Description: param.paramCommentBlock ? <TSDoc node={param.paramCommentBlock} /> : 'None',
}));
const rows = useMemo(
() =>
data.map((param) => ({
Name: param.name,
Type: <HyperlinkedText tokens={param.tokens} />,
Optional: param.isOptional ? 'Yes' : 'No',
Description: param.paramCommentBlock ? <TSDoc node={param.paramCommentBlock} /> : 'None',
})),
[data],
);
return (
<Box>
<ScrollArea pb="xs" offsetScrollbars>
<Table columns={['Name', 'Type', 'Optional', 'Description']} rows={rows} columnStyles={columnStyles} />
</ScrollArea>
</Box>
<div className="overflow-x-auto">
<Table columns={['Name', 'Type', 'Optional', 'Description']} rows={rows} columnStyles={columnStyles} />
</div>
);
}

View File

@@ -1,12 +1,11 @@
import type { ApiPropertyItemJSON } from '@discordjs/api-extractor-utils';
import { Divider, Stack } from '@mantine/core';
import { Fragment } from 'react';
import { Fragment, useMemo } from 'react';
import { CodeListing } from './CodeListing';
export function PropertyList({ data }: { data: ApiPropertyItemJSON[] }) {
return (
<Stack>
{data.map((prop) => (
const propertyItems = useMemo(
() =>
data.map((prop) => (
<Fragment key={prop.name}>
<CodeListing
name={prop.name}
@@ -18,9 +17,11 @@ export function PropertyList({ data }: { data: ApiPropertyItemJSON[] }) {
deprecation={prop.deprecated}
inheritanceData={prop.inheritanceData}
/>
<Divider size="md" />
<div className="border-light-900 -mx-8 border-t-2" />
</Fragment>
))}
</Stack>
)),
[data],
);
return <div className="flex flex-col gap-4">{propertyItems}</div>;
}

View File

@@ -1,25 +0,0 @@
import { startNavigationProgress, resetNavigationProgress, NavigationProgress } from '@mantine/nprogress';
import { useRouter } from 'next/router';
import { useEffect } from 'react';
export function RouterTransition() {
const router = useRouter();
useEffect(() => {
const handleStart = (url: string) => url !== router.asPath && startNavigationProgress();
const handleComplete = () => resetNavigationProgress();
router.events.on('routeChangeStart', handleStart);
router.events.on('routeChangeComplete', handleComplete);
router.events.on('routeChangeError', handleComplete);
return () => {
router.events.off('routeChangeStart', handleStart);
router.events.off('routeChangeComplete', handleComplete);
router.events.off('routeChangeError', handleComplete);
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [router.asPath]);
return <NavigationProgress color="blurple" />;
}

View File

@@ -1,37 +1,7 @@
import {
createStyles,
UnstyledButton,
Group,
ThemeIcon,
Collapse,
Box,
Text,
useMantineColorScheme,
} from '@mantine/core';
import { useState, useEffect, type PropsWithChildren } from 'react';
import { Disclosure, DisclosureContent, useDisclosureState } from 'ariakit/disclosure';
import type { PropsWithChildren } from 'react';
import { VscChevronDown } from 'react-icons/vsc';
const useStyles = createStyles((theme, { opened }: { opened: boolean }) => ({
control: {
display: 'block',
width: '100%',
padding: theme.spacing.xs,
color: theme.colorScheme === 'dark' ? theme.colors.dark![0] : theme.black,
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark![7] : 'transparent',
borderRadius: theme.radius.xs,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark![5] : theme.colors.gray![2],
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
},
},
icon: {
transition: 'transform 150ms ease',
transform: opened ? 'rotate(180deg)' : 'rotate(0deg)',
},
}));
export function Section({
title,
icon,
@@ -46,41 +16,28 @@ export function Section({
padded?: boolean;
title: string;
}>) {
const [opened, setOpened] = useState(!defaultClosed);
const { colorScheme } = useMantineColorScheme();
const { classes } = useStyles({ opened });
useEffect(() => {
setOpened(!defaultClosed);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const disclosure = useDisclosureState({ defaultOpen: !defaultClosed });
return (
<Box sx={{ wordBreak: 'break-all' }}>
<UnstyledButton className={classes.control} onClick={() => setOpened((isOpen) => !isOpen)}>
<Group position="apart">
<Group>
{icon ? (
<ThemeIcon variant={colorScheme === 'dark' ? 'filled' : 'outline'} radius="sm" size={30}>
{icon}
</ThemeIcon>
) : null}
<Text weight={600} size="md">
{title}
</Text>
</Group>
<VscChevronDown size={20} className={classes.icon} />
</Group>
</UnstyledButton>
<Collapse in={opened}>
{padded ? (
<Box py={20} px={dense ? 0 : 31} mx={dense ? 10 : 25}>
{children}
</Box>
) : (
children
)}
</Collapse>
</Box>
<div>
<Disclosure
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 rounded p-3"
state={disclosure}
>
<div className="flex flex-row place-content-between place-items-center">
<div className="flex flex-row place-items-center gap-3">
{icon ?? null}
<span className="font-semibold">{title}</span>
</div>
<VscChevronDown
className={`transform transition duration-150 ease-in-out ${disclosure.open ? 'rotate-180' : 'rotate-0'}`}
size={20}
/>
</div>
</Disclosure>
<DisclosureContent state={disclosure}>
{padded ? <div className={`py-5 ${dense ? 'mx-2 px-0' : 'px-4.5 mx-6.5'}`}>{children}</div> : children}
</DisclosureContent>
</div>
);
}

View File

@@ -4,9 +4,9 @@ import type {
ParameterDocumentation,
ApiConstructorJSON,
} from '@discordjs/api-extractor-utils';
import { Stack, Group, Badge, Title } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useMemo } from 'react';
import { VscSymbolConstant, VscSymbolMethod, VscSymbolProperty } from 'react-icons/vsc';
import { useMedia } from 'react-use';
import { MethodList } from './MethodList';
import { ParameterTable } from './ParameterTable';
import { PropertyList } from './PropertyList';
@@ -14,7 +14,7 @@ import { Section } from './Section';
import { TSDoc } from './tsdoc/TSDoc';
export function PropertiesSection({ data }: { data: ApiClassJSON['properties'] | ApiInterfaceJSON['properties'] }) {
const matches = useMediaQuery('(max-width: 768px)');
const matches = useMedia('(max-width: 768px)', true);
return data.length ? (
<Section title="Properties" icon={<VscSymbolProperty size={20} />} padded dense={matches}>
@@ -24,7 +24,7 @@ export function PropertiesSection({ data }: { data: ApiClassJSON['properties'] |
}
export function MethodsSection({ data }: { data: ApiClassJSON['methods'] | ApiInterfaceJSON['methods'] }) {
const matches = useMediaQuery('(max-width: 768px)');
const matches = useMedia('(max-width: 768px)', true);
return data.length ? (
<Section title="Methods" icon={<VscSymbolMethod size={20} />} padded dense={matches}>
@@ -34,7 +34,7 @@ export function MethodsSection({ data }: { data: ApiClassJSON['methods'] | ApiIn
}
export function ParametersSection({ data }: { data: ParameterDocumentation[] }) {
const matches = useMediaQuery('(max-width: 768px)');
const matches = useMedia('(max-width: 768px)', true);
return data.length ? (
<Section title="Parameters" icon={<VscSymbolConstant size={20} />} padded dense={matches}>
@@ -44,45 +44,52 @@ export function ParametersSection({ data }: { data: ParameterDocumentation[] })
}
export function ConstructorSection({ data }: { data: ApiConstructorJSON }) {
const matches = useMediaQuery('(max-width: 768px)');
const matches = useMedia('(max-width: 768px)', true);
const getShorthandName = () =>
`constructor(${data.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
const getShorthandName = useMemo(
() =>
`constructor(${data.parameters.reduce((prev, cur, index) => {
if (index === 0) {
return `${prev}${cur.isOptional ? `${cur.name}?` : cur.name}`;
}
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`;
return `${prev}, ${cur.isOptional ? `${cur.name}?` : cur.name}`;
}, '')})`,
[data.parameters],
);
return data.parameters.length ? (
<Section title="Constructor" icon={<VscSymbolMethod size={20} />} padded dense={matches}>
<Stack id={data.name} className="scroll-mt-30" spacing="xs">
<Group>
<Stack>
<Group>
{data.deprecated ? (
<Badge variant="filled" color="red">
Deprecated
</Badge>
) : null}
{data.protected ? <Badge variant="filled">Protected</Badge> : null}
<Title sx={{ wordBreak: 'break-all' }} order={4} className="font-mono">
{getShorthandName()}
</Title>
</Group>
</Stack>
</Group>
<Group sx={{ display: data.summary || data.parameters.length ? 'block' : 'none' }} mb="lg">
<Stack>
<div id={data.name} className="scroll-mt-30 flex flex-col gap-4">
<div className="flex flex-col">
<div className="flex flex-col gap-2 md:flex-row md:place-items-center">
{data.deprecated || data.protected ? (
<div className="flex flex-row flex-wrap gap-1">
{data.deprecated ? (
<div className="h-5 place-content-center rounded-full bg-red-500 px-3 text-center text-xs font-semibold uppercase text-white">
Deprecated
</div>
) : null}
{data.protected ? (
<div className="bg-blurple h-5 place-content-center rounded-full px-3 text-center text-xs font-semibold uppercase text-white">
Protected
</div>
) : null}
</div>
) : null}
<h4 className="break-all font-mono text-lg font-bold">{getShorthandName}</h4>
</div>
</div>
{data.summary || data.parameters.length ? (
<div className="mb-4 flex flex-col gap-4">
{data.deprecated ? <TSDoc node={data.deprecated} /> : null}
{data.summary ? <TSDoc node={data.summary} /> : null}
{data.remarks ? <TSDoc node={data.remarks} /> : null}
{data.comment ? <TSDoc node={data.comment} /> : null}
{data.parameters.length ? <ParameterTable data={data.parameters} /> : null}
</Stack>
</Group>
</Stack>
</div>
) : null}
</div>
</Section>
) : null;
}

View File

@@ -1,4 +1,3 @@
import { createStyles, Group, Text, NavLink, Box } from '@mantine/core';
import Link from 'next/link';
import { useRouter } from 'next/router';
import { type Dispatch, type SetStateAction, useEffect, useState, useMemo } from 'react';
@@ -66,32 +65,6 @@ function resolveIcon(item: keyof GroupedMembers) {
}
}
const useStyles = createStyles((theme) => ({
link: {
...theme.fn.focusStyles(),
fontWeight: 500,
display: 'block',
width: 'unset',
padding: 5,
paddingLeft: 31,
marginLeft: 25,
fontSize: theme.fontSizes.sm,
color: theme.colorScheme === 'dark' ? theme.colors.dark![0] : theme.colors.gray![7],
borderLeft: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark![4] : theme.colors.gray![3]}`,
'&[data-active]': {
'&:hover': {
color: theme.white,
},
},
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark![6] : theme.colors.gray![0],
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
},
},
}));
export function SidebarItems({
members,
setOpened,
@@ -101,7 +74,6 @@ export function SidebarItems({
}) {
const router = useRouter();
const [asPathWithoutQueryAndAnchor, setAsPathWithoutQueryAndAnchor] = useState('');
const { classes } = useStyles();
const groupItems = useMemo(() => groupMembers(members), [members]);
useEffect(() => {
@@ -109,36 +81,33 @@ export function SidebarItems({
}, [router.asPath]);
return (
<Box sx={(theme) => ({ paddingBottom: 48, [theme.fn.smallerThan('md')]: { paddingBottom: 128 } })}>
<div className="flex flex-col gap-3 p-3 pb-32 lg:pb-12">
{(Object.keys(groupItems) as (keyof GroupedMembers)[])
.filter((group) => groupItems[group].length)
.map((group, idx) => (
<Section key={idx} title={group} icon={resolveIcon(group)}>
{groupItems[group].map((member, index) => (
<Link key={index} href={member.path} passHref prefetch={false}>
<NavLink
className={classes.link}
component="a"
onClick={() => setOpened((isOpened) => !isOpened)}
label={
<Group>
<Text sx={{ textOverflow: 'ellipsis', overflow: 'hidden' }} className="line-clamp-1">
{member.name}
</Text>
{member.overloadIndex && member.overloadIndex > 1 ? (
<Text size="xs" color="dimmed">
{member.overloadIndex}
</Text>
) : null}
</Group>
}
active={asPathWithoutQueryAndAnchor === member.path}
variant="filled"
/>
<Link key={index} href={member.path} prefetch={false}>
<a
className={`dark:border-dark-100 border-light-800 ml-5 border-l p-[5px] pl-6 ${
asPathWithoutQueryAndAnchor === member.path
? 'bg-blurple text-white'
: 'dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800'
}`}
title={member.name}
onClick={() => setOpened(false)}
>
<div className="flex flex-row place-items-center gap-2 text-sm">
<span className="truncate">{member.name}</span>
{member.overloadIndex && member.overloadIndex > 1 ? (
<span className="text-xs">{member.overloadIndex}</span>
) : null}
</div>
</a>
</Link>
))}
</Section>
))}
</Box>
</div>
);
}

View File

@@ -1,41 +1,20 @@
import type { getMembers, ApiItemJSON } from '@discordjs/api-extractor-utils';
import {
useMantineTheme,
AppShell,
Navbar,
MediaQuery,
Header,
Burger,
Anchor,
Breadcrumbs,
ScrollArea,
Group,
Text,
ThemeIcon,
Box,
UnstyledButton,
createStyles,
Menu,
ActionIcon,
useMantineColorScheme,
Stack,
Skeleton,
LoadingOverlay,
Container,
Title,
} from '@mantine/core';
import { NextLink } from '@mantine/next';
import { Button } from 'ariakit/button';
import { Menu, MenuButton, MenuItem, useMenuState } from 'ariakit/menu';
import Image from 'next/future/image';
import Link from 'next/link';
import { useRouter } from 'next/router';
import type { MDXRemoteSerializeResult } from 'next-mdx-remote';
import { type PropsWithChildren, useState, useEffect, useMemo } from 'react';
import { VscChevronDown, VscGithubInverted, VscPackage, VscVersions } from 'react-icons/vsc';
import { WiDaySunny, WiNightClear } from 'react-icons/wi';
import { useTheme } from 'next-themes';
import { type PropsWithChildren, useState, useEffect, useMemo, Fragment } from 'react';
import { Scrollbars } from 'react-custom-scrollbars-2';
import { VscChevronDown, VscColorMode, VscGithubInverted, VscMenu, VscPackage, VscVersions } from 'react-icons/vsc';
import { useMedia /* useLockBodyScroll */ } from 'react-use';
import useSWR from 'swr';
import vercelLogo from '../assets/powered-by-vercel.svg';
import { SidebarItems } from './SidebarItems';
import { PACKAGES } from '~/util/constants';
import type { findMember } from '~/util/model.server';
import { PACKAGES } from '~/util/packages';
const fetcher = async (url: string) => {
const res = await fetch(url);
@@ -66,89 +45,6 @@ export interface GroupedMembers {
Variables: Members;
}
const useStyles = createStyles(
(theme, { openedLib, openedVersion }: { openedLib: boolean; openedVersion: boolean }) => ({
control: {
display: 'block',
width: '100%',
padding: theme.spacing.xs,
color: theme.colorScheme === 'dark' ? theme.colors.dark![0] : theme.black,
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark![6] : theme.colors.gray![1],
borderRadius: theme.radius.xs,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark![5] : theme.colors.gray![2],
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
},
},
iconLib: {
transition: 'transform 150ms ease',
transform: openedLib ? 'rotate(180deg)' : 'rotate(0deg)',
},
iconVersion: {
transition: 'transform 150ms ease',
transform: openedVersion ? 'rotate(180deg)' : 'rotate(0deg)',
},
content: {
position: 'relative',
minHeight: 'calc(100vh - 50px)',
zIndex: 1,
background: theme.colorScheme === 'dark' ? theme.colors.dark![8] : theme.colors.gray![0],
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
},
footer: {
position: 'fixed',
bottom: 0,
left: 0,
right: 0,
height: 200,
background: theme.colorScheme === 'dark' ? theme.colors.dark![7] : theme.colors.gray![0],
paddingLeft: 324,
[theme.fn.smallerThan('lg')]: {
paddingRight: 54,
},
[theme.fn.smallerThan('md')]: {
paddingLeft: 24,
},
[theme.fn.smallerThan('sm')]: {
paddingRight: 24,
height: 300,
},
},
links: {
display: 'flex',
justifyContent: 'space-between',
[theme.fn.smallerThan('sm')]: {
flexDirection: 'column',
alignItems: 'center',
gap: 50,
},
},
link: { color: theme.colorScheme === 'dark' ? theme.white : theme.black },
}),
);
const packageMenuItems = PACKAGES.map((pkg) => (
<Menu.Item
key={pkg}
component={NextLink}
href={`/docs/packages/${pkg}/main`}
sx={(theme) => ({ color: theme.colorScheme === 'dark' ? theme.white : theme.black })}
>
{pkg}
</Menu.Item>
));
export function SidebarLayout({
packageName,
branchName,
@@ -157,261 +53,244 @@ export function SidebarLayout({
}: PropsWithChildren<Partial<SidebarLayoutProps>>) {
const router = useRouter();
const [asPathWithoutQueryAndAnchor, setAsPathWithoutQueryAndAnchor] = useState('');
const { data: versions } = useSWR<string[]>(
`https://docs.discordjs.dev/api/info?package=${packageName ?? 'builders'}`,
fetcher,
);
const theme = useMantineTheme();
// eslint-disable-next-line @typescript-eslint/unbound-method
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const { data: versions } = useSWR<string[]>(`https://docs.discordjs.dev/api/info?package=${packageName}`, fetcher);
const { resolvedTheme, setTheme } = useTheme();
const toggleTheme = () => setTheme(resolvedTheme === 'light' ? 'dark' : 'light');
const matches = useMedia('(min-width: 992px)', false);
const [opened, setOpened] = useState(false);
const [openedLibPicker, setOpenedLibPicker] = useState(false);
const [openedVersionPicker, setOpenedVersionPicker] = useState(false);
const packageMenu = useMenuState({ gutter: 8, sameWidth: true });
const versionMenu = useMenuState({ gutter: 8, sameWidth: true });
// useLockBodyScroll(opened);
useEffect(() => {
setOpened(false);
setOpenedLibPicker(false);
setOpenedVersionPicker(false);
}, []);
if (matches) {
setOpened(false);
}
}, [matches]);
useEffect(() => {
setAsPathWithoutQueryAndAnchor(router.asPath.split('?')[0]?.split('#')[0]?.split(':')[0] ?? '');
}, [router.asPath]);
const { classes } = useStyles({ openedLib: openedLibPicker, openedVersion: openedVersionPicker });
const packageMenuItems = PACKAGES.map((pkg) => (
<Link key={pkg} href={`/docs/packages/${pkg}/main`} passHref prefetch={false}>
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 rounded bg-white p-3 text-sm"
as="a"
state={packageMenu}
>
{pkg}
</MenuItem>
</Link>
));
const versionMenuItems = useMemo(
() =>
versions?.map((item) => (
<Menu.Item
key={item}
component={NextLink}
href={`/docs/packages/${packageName ?? 'builders'}/${item}`}
sx={(theme) => ({ color: theme.colorScheme === 'dark' ? theme.white : theme.black })}
>
{item}
</Menu.Item>
)) ?? [],
versions
?.map((item) => (
<Link key={item} href={`/docs/packages/${packageName}/${item}`} passHref prefetch={false}>
<MenuItem
className="hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 rounded bg-white p-3 text-sm"
as="a"
state={versionMenu}
>
{item}
</MenuItem>
</Link>
))
.reverse() ?? [],
// eslint-disable-next-line react-hooks/exhaustive-deps
[versions, packageName],
);
const breadcrumbs = useMemo(
const pathElements = useMemo(
() =>
asPathWithoutQueryAndAnchor.split('/').map((path, idx, original) => (
<Link key={idx} href={original.slice(0, idx + 1).join('/')} passHref prefetch={false}>
<Anchor component="a" sx={(theme) => ({ color: theme.colorScheme === 'dark' ? theme.white : theme.black })}>
{path}
</Anchor>
<Link key={idx} href={original.slice(0, idx + 1).join('/')} prefetch={false}>
<a className="hover:underline">{path}</a>
</Link>
)),
[asPathWithoutQueryAndAnchor],
);
const breadcrumbs = useMemo(
() =>
pathElements.flatMap((el, idx, array) => {
if (idx !== array.length - 1) {
return (
<Fragment key={idx}>
{el}
<div className="mx-2">/</div>
</Fragment>
);
}
return <Fragment key={idx}>{el}</Fragment>;
}),
[pathElements],
);
return (
<AppShell
sx={(theme) => ({
main: {
background: theme.colorScheme === 'dark' ? theme.colors.dark![8] : theme.colors.gray![0],
overflowX: 'auto',
},
})}
padding={0}
navbarOffsetBreakpoint="md"
asideOffsetBreakpoint="md"
navbar={
<>
<MediaQuery smallerThan="md" styles={{ display: 'none' }}>
<LoadingOverlay
sx={{ position: 'fixed', top: 70, width: 300 }}
visible={router.isFallback}
overlayBlur={2}
/>
</MediaQuery>
<Navbar hiddenBreakpoint="md" hidden={!opened} width={{ md: 300 }}>
{packageName && data ? (
<>
<Navbar.Section p="xs">
<Stack spacing="xs">
<Menu
onOpen={() => setOpenedLibPicker(true)}
onClose={() => setOpenedLibPicker(false)}
radius="sm"
width="target"
>
<Menu.Target>
<UnstyledButton className={classes.control}>
<Group position="apart">
<Group>
<ThemeIcon variant={colorScheme === 'dark' ? 'filled' : 'outline'} radius="sm" size={30}>
<VscPackage size={20} />
</ThemeIcon>
<Text weight="600" size="md">
{packageName}
</Text>
</Group>
<VscChevronDown className={classes.iconLib} size={20} />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>{packageMenuItems}</Menu.Dropdown>
</Menu>
<Menu
onOpen={() => setOpenedVersionPicker(true)}
onClose={() => setOpenedVersionPicker(false)}
radius="sm"
width="target"
>
<Menu.Target>
<UnstyledButton className={classes.control}>
<Group position="apart">
<Group>
<ThemeIcon variant={colorScheme === 'dark' ? 'filled' : 'outline'} radius="sm" size={30}>
<VscVersions size={20} />
</ThemeIcon>
<Text weight="600" size="md">
{branchName}
</Text>
</Group>
<VscChevronDown className={classes.iconVersion} size={20} />
</Group>
</UnstyledButton>
</Menu.Target>
<Menu.Dropdown>{versionMenuItems}</Menu.Dropdown>
</Menu>
</Stack>
</Navbar.Section>
<Navbar.Section px="xs" pb="xs" grow component={ScrollArea}>
<SidebarItems members={data.members} setOpened={setOpened} />
</Navbar.Section>
</>
) : null}
</Navbar>
</>
}
header={
<Header
sx={(theme) => ({
boxShadow:
theme.colorScheme === 'dark'
? '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)'
: 'unset',
})}
height={70}
p="md"
<>
<header className="dark:bg-dark-600 dark:border-dark-100 bg-light-600 border-light-800 fixed top-0 left-0 z-20 w-full border-b">
<div className="h-18 block px-6">
<div className="flex h-full flex-row place-content-between place-items-center">
<Button
className="flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline active:translate-y-px lg:hidden"
onClick={() => setOpened((open) => !open)}
>
<VscMenu size={24} />
</Button>
<div className="hidden md:flex md:flex-row">{breadcrumbs}</div>
<div className="flex flex-row gap-4">
<Button
as="a"
className="flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline active:translate-y-px"
href="https://github.com/discordjs/discord.js"
target="_blank"
rel="noopener noreferrer"
>
<VscGithubInverted size={24} />
</Button>
<Button
className="flex h-6 w-6 transform-gpu cursor-pointer select-none appearance-none place-items-center rounded border-0 bg-transparent p-0 text-sm font-semibold leading-none no-underline active:translate-y-px"
role="button"
onClick={() => toggleTheme()}
>
<VscColorMode size={24} />
</Button>
</div>
</div>
</div>
</header>
<nav
className={`h-[calc(100vh - 73px)] dark:bg-dark-600 dark:border-dark-100 border-light-800 fixed top-[73px] left-0 bottom-0 z-20 w-full border-r bg-white ${
opened ? 'block' : 'hidden'
} lg:w-76 lg:max-w-76 lg:block`}
>
<Scrollbars
universal
autoHide
hideTracksWhenNotNeeded
renderTrackVertical={(props) => (
<div {...props} className="absolute top-0.5 right-0.5 bottom-0.5 z-30 w-1.5 rounded" />
)}
renderThumbVertical={(props) => <div {...props} className="dark:bg-dark-100 bg-light-900 z-30 rounded" />}
>
<Box sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', height: '100%' }}>
<Box>
<MediaQuery largerThan="md" styles={{ display: 'none' }}>
<Burger
opened={opened}
onClick={() => (router.isFallback ? null : setOpened((isOpened) => !isOpened))}
size="sm"
color={theme.colors.gray![6]}
mr="xl"
<div className="flex flex-col gap-3 px-3 pt-3">
<MenuButton
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 rounded p-3"
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}
/>
</MediaQuery>
</div>
</MenuButton>
<Menu
className="dark:bg-dark-600 border-light-800 dark:border-dark-100 z-20 rounded border bg-white p-1"
state={packageMenu}
>
{packageMenuItems}
</Menu>
<MediaQuery smallerThan="md" styles={{ display: 'none' }}>
<Skeleton visible={router.isFallback} radius="sm">
<Breadcrumbs>{breadcrumbs}</Breadcrumbs>
</Skeleton>
</MediaQuery>
</Box>
<Group>
<Link href="https://github.com/discordjs/discord.js" passHref prefetch={false}>
<ActionIcon
component="a"
<MenuButton
className="bg-light-600 hover:bg-light-700 active:bg-light-800 dark:bg-dark-600 dark:hover:bg-dark-500 dark:active:bg-dark-400 rounded p-3"
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="dark:bg-dark-600 border-light-800 dark:border-dark-100 z-20 rounded border bg-white p-1"
state={versionMenu}
>
{versionMenuItems}
</Menu>
</div>
<SidebarItems members={data?.members ?? []} setOpened={setOpened} />
</Scrollbars>
</nav>
<main
className={`pt-18 lg:pl-76 ${
data?.member?.kind === 'Class' || data?.member?.kind === 'Interface' ? 'xl:pr-64' : ''
}`}
>
<article className="dark:bg-dark-600 bg-light-600">
<div className="min-h-[calc(100vh - 50px)] dark:bg-dark-800 relative z-10 bg-white p-6 pb-20 shadow">
{children}
</div>
<div className="h-76 md:h-52" />
<footer
className={`dark:bg-dark-600 h-76 lg:pl-84 bg-light-600 fixed bottom-0 left-0 right-0 md:h-52 md:pl-4 md:pr-16 ${
data?.member?.kind === 'Class' || data?.member?.kind === 'Interface' ? 'xl:pr-76' : 'xl:pr-16'
}`}
>
<div className="mx-auto flex max-w-6xl flex-col place-items-center gap-12 pt-12 lg:place-content-center">
<div className="flex w-full flex-col place-content-between place-items-center gap-12 md:flex-row md:gap-0">
<a
href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"
target="_blank"
rel="noopener noreferrer"
variant="transparent"
color="dark"
title="GitHub repository"
title="Vercel"
>
<VscGithubInverted size={20} />
</ActionIcon>
</Link>
<ActionIcon
variant="subtle"
color={colorScheme === 'dark' ? 'yellow' : 'blue'}
onClick={() => toggleColorScheme()}
title="Toggle color scheme"
radius="sm"
>
{colorScheme === 'dark' ? <WiDaySunny size={30} /> : <WiNightClear size={30} />}
</ActionIcon>
</Group>
</Box>
</Header>
}
>
<article>
<Box className={classes.content} p="lg" pb={80}>
{children}
</Box>
<Box sx={(theme) => ({ height: 200, [theme.fn.smallerThan('sm')]: { height: 300 } })} />
<Box
component="footer"
sx={{ paddingRight: data?.member?.kind !== 'Class' && data?.member?.kind !== 'Interface' ? 54 : 324 }}
className={classes.footer}
pt={50}
>
<Container>
<Box className={classes.links}>
<Link href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss" prefetch={false}>
<a title="Vercel">
<Image
src="/powered-by-vercel.svg"
alt="Vercel"
width={0}
height={0}
style={{ height: '100%', width: '100%', maxWidth: 200 }}
/>
<Image src={vercelLogo} alt="Vercel" />
</a>
</Link>
<Group sx={(theme) => ({ gap: 50, [theme.fn.smallerThan('sm')]: { gap: 25 } })} align="flex-start">
<Stack spacing={8}>
<Title order={4}>Community</Title>
<Stack spacing={0}>
<Link href="https://discord.gg/djs" passHref prefetch={false}>
<Anchor component="a" target="_blank" rel="noopener noreferrer" className={classes.link}>
<div className="flex flex-row gap-6 md:gap-12">
<div className="flex flex-col gap-2">
<h4 className="text-lg font-semibold">Community</h4>
<div className="flex flex-col gap-1">
<a href="https://discord.gg/djs" target="_blank" rel="noopener noreferrer">
Discord
</Anchor>
</Link>
<Link href="https://github.com/discordjs/discord.js/discussions" passHref prefetch={false}>
<Anchor component="a" target="_blank" rel="noopener noreferrer" className={classes.link}>
</a>
<a
href="https://github.com/discordjs/discord.js/discussions"
target="_blank"
rel="noopener noreferrer"
>
GitHub discussions
</Anchor>
</Link>
</Stack>
</Stack>
<Stack spacing={8}>
<Title order={4}>Project</Title>
<Stack spacing={0}>
<Link href="https://github.com/discordjs/discord.js" passHref prefetch={false}>
<Anchor component="a" target="_blank" rel="noopener noreferrer" className={classes.link}>
</a>
</div>
</div>
<div className="flex flex-col gap-2">
<h4 className="text-lg font-semibold">Project</h4>
<div className="flex flex-col gap-1">
<a href="https://github.com/discordjs/discord.js" target="_blank" rel="noopener noreferrer">
discord.js
</Anchor>
</Link>
<Link href="https://discordjs.guide" passHref prefetch={false}>
<Anchor component="a" target="_blank" rel="noopener noreferrer" className={classes.link}>
</a>
<a href="https://discordjs.guide" target="_blank" rel="noopener noreferrer">
discord.js guide
</Anchor>
</Link>
<Link href="https://discord-api-types.dev" passHref prefetch={false}>
<Anchor component="a" target="_blank" rel="noopener noreferrer" className={classes.link}>
</a>
<a href="https://discord-api-types.dev" target="_blank" rel="noopener noreferrer">
discord-api-types
</Anchor>
</Link>
</Stack>
</Stack>
</Group>
</Box>
</Container>
</Box>
</article>
</AppShell>
</a>
</div>
</div>
</div>
</div>
</div>
</footer>
</article>
</main>
</>
);
}

View File

@@ -0,0 +1,31 @@
import { PrismAsyncLight } from 'react-syntax-highlighter';
import { vscDarkPlus, prism } from 'react-syntax-highlighter/dist/cjs/styles/prism';
export function SyntaxHighlighter({ language = 'typescript', code }: { code: string; language?: string }) {
return (
<>
<div data-theme="dark">
<PrismAsyncLight
wrapLines
wrapLongLines
language={language}
style={vscDarkPlus}
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
>
{code}
</PrismAsyncLight>
</div>
<div data-theme="light">
<PrismAsyncLight
wrapLines
wrapLongLines
language={language}
style={prism}
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
>
{code}
</PrismAsyncLight>
</div>
</>
);
}

View File

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

View File

@@ -1,31 +1,7 @@
import type { ApiClassJSON, ApiInterfaceJSON } from '@discordjs/api-extractor-utils';
import { createStyles, Group, Text, Box, Stack, ThemeIcon, useMantineColorScheme } from '@mantine/core';
import { useMemo } from 'react';
import { VscListSelection, VscSymbolMethod, VscSymbolProperty } from 'react-icons/vsc';
const useStyles = createStyles((theme) => ({
link: {
...theme.fn.focusStyles(),
fontWeight: 500,
display: 'block',
textDecoration: 'none',
color: theme.colorScheme === 'dark' ? theme.colors.dark![0] : theme.colors.gray![7],
lineHeight: 1.2,
fontSize: theme.fontSizes.sm,
padding: 5,
paddingLeft: theme.spacing.md,
marginLeft: 14,
borderTopRightRadius: theme.radius.sm,
borderBottomRightRadius: theme.radius.sm,
borderLeft: `1px solid ${theme.colorScheme === 'dark' ? theme.colors.dark![4] : theme.colors.gray![3]}`,
'&:hover': {
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark![6] : theme.colors.gray![0],
color: theme.colorScheme === 'dark' ? theme.white : theme.black,
},
},
}));
export function TableOfContentItems({
methods,
properties,
@@ -33,22 +9,19 @@ export function TableOfContentItems({
methods: ApiClassJSON['methods'] | ApiInterfaceJSON['methods'];
properties: ApiClassJSON['properties'] | ApiInterfaceJSON['properties'];
}) {
const { colorScheme } = useMantineColorScheme();
const { classes } = useStyles();
const propertyItems = useMemo(
() =>
properties.map((prop) => (
<Box<'a'> key={prop.name} href={`#${prop.name}`} component="a" className={classes.link}>
<Group>
<Text sx={{ textOverflow: 'ellipsis', overflow: 'hidden' }} className="line-clamp-1">
{prop.name}
</Text>
</Group>
</Box>
<a
key={prop.name}
href={`#${prop.name}`}
title={prop.name}
className="dark:border-dark-100 border-light-800 dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800 pl-6.5 ml-[10px] border-l p-[5px] text-sm"
>
<span className="line-clamp-1">{prop.name}</span>
</a>
)),
// eslint-disable-next-line react-hooks/exhaustive-deps
[properties, colorScheme],
[properties],
);
const methodItems = useMemo(
@@ -59,61 +32,52 @@ export function TableOfContentItems({
}`;
return (
<Box<'a'> key={key} component="a" href={`#${key}`} className={classes.link}>
<Group>
<Text sx={{ textOverflow: 'ellipsis', overflow: 'hidden' }} className="line-clamp-1">
{member.name}
</Text>
{member.overloadIndex && member.overloadIndex > 1 ? (
<Text size="xs" color="dimmed">
{member.overloadIndex}
</Text>
) : null}
</Group>
</Box>
<a
key={key}
href={`#${key}`}
title={member.name}
className="dark:border-dark-100 border-light-800 dark:hover:bg-dark-200 dark:active:bg-dark-100 hover:bg-light-700 active:bg-light-800 pl-6.5 ml-[10px] flex flex-row place-items-center gap-2 border-l p-[5px] text-sm"
>
<span className="line-clamp-1">{member.name}</span>
{member.overloadIndex && member.overloadIndex > 1 ? (
<span className="text-xs">{member.overloadIndex}</span>
) : null}
</a>
);
}),
[methods, classes.link],
[methods],
);
return (
<Box sx={{ wordBreak: 'break-all' }} pb="xl">
<Group spacing="xs" mt={6} mb="sm" ml={6}>
<div className="flex flex-col break-all p-3 pb-8">
<div className="mt-4 ml-2 flex flex-row gap-2">
<VscListSelection size={25} />
<Text weight={600}>Table of contents</Text>
</Group>
<Stack spacing={0} mt={26} ml={4}>
<span className="font-semibold">Contents</span>
</div>
<div className="mt-5.5 ml-2 flex flex-col gap-2">
{propertyItems.length ? (
<Box>
<Group spacing="xs">
<ThemeIcon variant={colorScheme === 'dark' ? 'filled' : 'outline'} radius="sm" size={30}>
<VscSymbolProperty size={20} />
</ThemeIcon>
<Box p="sm" pl={0}>
<Text weight={600} size="md">
Properties
</Text>
</Box>
</Group>
<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}
</Box>
</div>
) : null}
{methodItems.length ? (
<Box>
<Group spacing="xs">
<ThemeIcon variant={colorScheme === 'dark' ? 'filled' : 'outline'} radius="sm" size={30}>
<VscSymbolMethod size={20} />
</ThemeIcon>
<Box p="sm" pl={0}>
<Text weight={600} size="md">
Methods
</Text>
</Box>
</Group>
<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}
</Box>
</div>
) : null}
</Stack>
</Box>
</div>
</div>
);
}

View File

@@ -1,5 +1,5 @@
import type { TypeParameterData } from '@discordjs/api-extractor-utils';
import { ScrollArea } from '@mantine/core';
import { useMemo } from 'react';
import { HyperlinkedText } from './HyperlinkedText';
import { Table } from './Table';
import { TSDoc } from './tsdoc/TSDoc';
@@ -11,21 +11,25 @@ const rowElements = {
};
export function TypeParamTable({ data }: { data: TypeParameterData[] }) {
const rows = data.map((typeParam) => ({
Name: typeParam.name,
Constraints: <HyperlinkedText tokens={typeParam.constraintTokens} />,
Optional: typeParam.optional ? 'Yes' : 'No',
Default: <HyperlinkedText tokens={typeParam.defaultTokens} />,
Description: typeParam.commentBlock ? <TSDoc node={typeParam.commentBlock} /> : 'None',
}));
const rows = useMemo(
() =>
data.map((typeParam) => ({
Name: typeParam.name,
Constraints: <HyperlinkedText tokens={typeParam.constraintTokens} />,
Optional: typeParam.optional ? 'Yes' : 'No',
Default: <HyperlinkedText tokens={typeParam.defaultTokens} />,
Description: typeParam.commentBlock ? <TSDoc node={typeParam.commentBlock} /> : 'None',
})),
[data],
);
return (
<ScrollArea pb="xs" offsetScrollbars>
<div className="overflow-x-auto">
<Table
columns={['Name', 'Constraints', 'Optional', 'Default', 'Description']}
rows={rows}
columnStyles={rowElements}
/>
</ScrollArea>
</div>
);
}

View File

@@ -1,12 +1,8 @@
import type { ApiClassJSON } from '@discordjs/api-extractor-utils';
import { Skeleton } from '@mantine/core';
import { useRouter } from 'next/router';
import { DocContainer } from '../DocContainer';
import { ConstructorSection, MethodsSection, PropertiesSection } from '../Sections';
export function Class({ data }: { data: ApiClassJSON }) {
const router = useRouter();
return (
<DocContainer
name={data.name}
@@ -20,12 +16,8 @@ export function Class({ data }: { data: ApiClassJSON }) {
properties={data.properties}
>
{data.constructor ? <ConstructorSection data={data.constructor} /> : null}
<Skeleton visible={router.isFallback} radius="sm">
<PropertiesSection data={data.properties} />
</Skeleton>
<Skeleton visible={router.isFallback} radius="sm">
<MethodsSection data={data.methods} />
</Skeleton>
<PropertiesSection data={data.properties} />
<MethodsSection data={data.methods} />
</DocContainer>
);
}

View File

@@ -1,33 +1,28 @@
import type { ApiEnumJSON } from '@discordjs/api-extractor-utils';
import { Skeleton, Stack } from '@mantine/core';
import { useMediaQuery } from '@mantine/hooks';
import { useRouter } from 'next/router';
import { VscSymbolEnumMember } from 'react-icons/vsc';
import { useMedia } from 'react-use';
import { CodeListing, CodeListingSeparatorType } from '../CodeListing';
import { DocContainer } from '../DocContainer';
import { Section } from '../Section';
export function Enum({ data }: { data: ApiEnumJSON }) {
const router = useRouter();
const matches = useMediaQuery('(max-width: 768px)');
const matches = useMedia('(max-width: 768px)', true);
return (
<DocContainer name={data.name} kind={data.kind} excerpt={data.excerpt} summary={data.summary}>
<Skeleton visible={router.isFallback} radius="sm">
<Section title="Members" icon={<VscSymbolEnumMember size={20} />} padded dense={matches}>
<Stack>
{data.members.map((member) => (
<CodeListing
key={member.name}
name={member.name}
separator={CodeListingSeparatorType.Value}
typeTokens={member.initializerTokens}
summary={member.summary}
/>
))}
</Stack>
</Section>
</Skeleton>
<Section title="Members" icon={<VscSymbolEnumMember size={20} />} padded dense={matches}>
<div className="flex flex-col gap-4">
{data.members.map((member) => (
<CodeListing
key={member.name}
name={member.name}
separator={CodeListingSeparatorType.Value}
typeTokens={member.initializerTokens}
summary={member.summary}
/>
))}
</div>
</Section>
</DocContainer>
);
}

View File

@@ -1,12 +1,8 @@
import type { ApiFunctionJSON } from '@discordjs/api-extractor-utils';
import { Skeleton } from '@mantine/core';
import { useRouter } from 'next/router';
import { DocContainer } from '../DocContainer';
import { ParametersSection } from '../Sections';
export function Function({ data }: { data: ApiFunctionJSON }) {
const router = useRouter();
return (
<DocContainer
name={`${data.name}${data.overloadIndex && data.overloadIndex > 1 ? ` (${data.overloadIndex})` : ''}`}
@@ -15,9 +11,7 @@ export function Function({ data }: { data: ApiFunctionJSON }) {
summary={data.summary}
typeParams={data.typeParameters}
>
<Skeleton visible={router.isFallback} radius="sm">
<ParametersSection data={data.parameters} />
</Skeleton>
<ParametersSection data={data.parameters} />
</DocContainer>
);
}

View File

@@ -1,12 +1,8 @@
import type { ApiInterfaceJSON } from '@discordjs/api-extractor-utils';
import { Skeleton } from '@mantine/core';
import { useRouter } from 'next/router';
import { DocContainer } from '../DocContainer';
import { MethodsSection, PropertiesSection } from '../Sections';
export function Interface({ data }: { data: ApiInterfaceJSON }) {
const router = useRouter();
return (
<DocContainer
name={data.name}
@@ -17,12 +13,8 @@ export function Interface({ data }: { data: ApiInterfaceJSON }) {
methods={data.methods}
properties={data.properties}
>
<Skeleton visible={router.isFallback} radius="sm">
<PropertiesSection data={data.properties} />
</Skeleton>
<Skeleton visible={router.isFallback} radius="sm">
<MethodsSection data={data.methods} />
</Skeleton>
<PropertiesSection data={data.properties} />
<MethodsSection data={data.methods} />
</DocContainer>
);
}

View File

@@ -1,14 +1,13 @@
import { Alert, Box, Title, Text } from '@mantine/core';
import { StandardTags } from '@microsoft/tsdoc';
import type { PropsWithChildren } from 'react';
import { VscWarning } from 'react-icons/vsc';
export function Block({ children, title }: PropsWithChildren<{ title: string }>) {
return (
<Box>
<Title order={5}>{title}</Title>
<div className="flex flex-col gap-2">
<h5 className="font-bold">{title}</h5>
{children}
</Box>
</div>
);
}
@@ -21,9 +20,17 @@ export function ExampleBlock({
export function DeprecatedBlock({ children }: PropsWithChildren): JSX.Element {
return (
<Alert icon={<VscWarning />} title="Deprecated" variant="outline" color="red" radius="sm">
{children}
</Alert>
<div className="rounded border border-red-500 p-4">
<div className="flex flex-row place-items-center gap-4">
<span className="text-red-500">
<VscWarning size={20} />
</span>
<div className="flex flex-col gap-2 text-sm">
<span className="font-semibold text-red-500">Deprecated</span>
{children}
</div>
</div>
</div>
);
}
@@ -54,7 +61,7 @@ export function BlockComment({
return <DefaultValueBlock>{children}</DefaultValueBlock>;
case StandardTags.typeParam.tagNameWithUpperCase:
case StandardTags.param.tagNameWithUpperCase:
return <Text>{children}</Text>;
return <span>{children}</span>;
default: // TODO: Support more blocks in the future.
return <>{children}</>;
}

View File

@@ -7,132 +7,113 @@ import type {
DocBlockJSON,
DocCommentJSON,
} from '@discordjs/api-extractor-utils';
import { Anchor, Box, Code, Text, useMantineColorScheme } from '@mantine/core';
import { DocNodeKind, StandardTags } from '@microsoft/tsdoc';
import Link from 'next/link';
import { Fragment, useCallback, type ReactNode } from 'react';
import { PrismAsyncLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus, ghcolors } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { SyntaxHighlighter } from '../SyntaxHighlighter';
import { BlockComment } from './BlockComment';
export function TSDoc({ node }: { node: AnyDocNodeJSON }): JSX.Element {
const { colorScheme } = useMantineColorScheme();
const createNode = useCallback((node: AnyDocNodeJSON, idx?: number): ReactNode => {
let numberOfExamples = 0;
let exampleIndex = 0;
const createNode = useCallback(
(node: AnyDocNodeJSON, idx?: number): ReactNode => {
let numberOfExamples = 0;
let exampleIndex = 0;
switch (node.kind) {
case DocNodeKind.PlainText:
return (
<span key={idx} className="break-words">
{(node as DocPlainTextJSON).text}
</span>
);
case DocNodeKind.Paragraph:
return (
<span key={idx} className="break-words leading-relaxed">
{(node as DocNodeContainerJSON).nodes.map((node, idx) => createNode(node, idx))}
</span>
);
case DocNodeKind.SoftBreak:
return <Fragment key={idx} />;
case DocNodeKind.LinkTag: {
const { codeDestination, urlDestination, text } = node as DocLinkTagJSON;
switch (node.kind) {
case DocNodeKind.PlainText:
if (codeDestination) {
return (
<Text key={idx} span style={{ wordBreak: 'break-word' }}>
{(node as DocPlainTextJSON).text}
</Text>
<Link key={idx} href={codeDestination.path} prefetch={false}>
<a className="text-blurple font-mono">{text ?? codeDestination.name}</a>
</Link>
);
case DocNodeKind.Paragraph:
}
if (urlDestination) {
return (
<Text key={idx} inline style={{ wordBreak: 'break-word' }}>
{(node as DocNodeContainerJSON).nodes.map((node, idx) => createNode(node, idx))}
</Text>
<Link key={idx} href={urlDestination} prefetch={false}>
<a className="text-blurple font-mono">{text ?? urlDestination}</a>
</Link>
);
case DocNodeKind.SoftBreak:
return <Fragment key={idx} />;
case DocNodeKind.LinkTag: {
const { codeDestination, urlDestination, text } = node as DocLinkTagJSON;
}
if (codeDestination) {
return (
<Link key={idx} href={codeDestination.path} passHref prefetch={false}>
<Anchor component="a" className="font-mono">
{text ?? codeDestination.name}
</Anchor>
</Link>
);
}
return null;
}
if (urlDestination) {
return (
<Link key={idx} href={urlDestination} passHref prefetch={false}>
<Anchor component="a" className="font-mono">
{text ?? urlDestination}
</Anchor>
</Link>
);
}
case DocNodeKind.CodeSpan: {
const { code } = node as DocFencedCodeJSON;
return (
<code key={idx} className="font-mono text-sm">
{code}
</code>
);
}
case DocNodeKind.FencedCode: {
const { language, code } = node as DocFencedCodeJSON;
return <SyntaxHighlighter key={idx} language={language} code={code} />;
}
case DocNodeKind.ParamBlock:
case DocNodeKind.Block: {
const { tag } = node as DocBlockJSON;
if (tag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase) {
exampleIndex++;
}
const index = numberOfExamples > 1 ? exampleIndex : undefined;
return (
<BlockComment tagName={tag.tagName} key={idx} index={index}>
{(node as DocBlockJSON).content.map((node, idx) => createNode(node, idx))}
</BlockComment>
);
}
case DocNodeKind.Comment: {
const comment = node as DocCommentJSON;
if (!comment.customBlocks.length) {
return null;
}
case DocNodeKind.CodeSpan: {
const { code } = node as DocFencedCodeJSON;
return (
<Code key={idx} sx={{ display: 'inline' }} className="text-sm font-mono">
{code}
</Code>
);
}
// Cheat a bit by finding out how many comments we have beforehand...
numberOfExamples = comment.customBlocks.filter(
(block) => block.tag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase,
).length;
case DocNodeKind.FencedCode: {
const { language, code } = node as DocFencedCodeJSON;
return (
<SyntaxHighlighter
key={idx}
wrapLines
wrapLongLines
language={language}
style={colorScheme === 'dark' ? vscDarkPlus : ghcolors}
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
>
{code}
</SyntaxHighlighter>
);
}
case DocNodeKind.ParamBlock:
case DocNodeKind.Block: {
const { tag } = node as DocBlockJSON;
if (tag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase) {
exampleIndex++;
}
const index = numberOfExamples > 1 ? exampleIndex : undefined;
return (
<BlockComment tagName={tag.tagName} key={idx} index={index}>
{(node as DocBlockJSON).content.map((node, idx) => createNode(node, idx))}
</BlockComment>
);
}
case DocNodeKind.Comment: {
const comment = node as DocCommentJSON;
// Cheat a bit by finding out how many comments we have beforehand...
numberOfExamples = comment.customBlocks.filter(
(block) => block.tag.tagName.toUpperCase() === StandardTags.example.tagNameWithUpperCase,
).length;
return <Box key={idx}>{comment.customBlocks.map((node, idx) => createNode(node, idx))}</Box>;
}
default:
console.log(`Captured unknown node kind: ${node.kind}`);
break;
return <div key={idx}>{comment.customBlocks.map((node, idx) => createNode(node, idx))}</div>;
}
return null;
},
[colorScheme],
);
default:
// console.log(`Captured unknown node kind: ${node.kind}`);
return null;
}
}, []);
return (
<Box>
<>
{node.kind === 'Paragraph' || node.kind === 'Section' ? (
<>{(node as DocNodeContainerJSON).nodes.map((node, idx) => createNode(node, idx))}</>
) : (
createNode(node)
)}
</Box>
</>
);
}