feat(website): show package members in a sidebar (#8245)

* feat(website): show package members in a sidebar

* fix: put response instead of loader

* Apply suggestions from code review

Co-authored-by: Noel <buechler.noel@outlook.com>

* chore: make requested changes

* refactor: make only package list scrollable

* feat: make sidebar mobile responsive

* fix: breakpoints for sidebar

Co-authored-by: Noel <buechler.noel@outlook.com>
This commit is contained in:
Suneet Tipirneni
2022-07-07 16:09:19 -04:00
committed by GitHub
parent 43f62bb667
commit e78c9c9ee9
18 changed files with 156 additions and 106 deletions

View File

@@ -8,14 +8,14 @@ import {
import { DocItem } from './DocItem';
import { DocMethod } from './DocMethod';
import { DocProperty } from './DocProperty';
import { type TokenDocumentation, genToken, TypeParameterData, generateTypeParamData } from '~/util/parse.server';
import { TypeParameterMixin } from './TypeParameterMixin';
import { type TokenDocumentation, genToken } from '~/util/parse.server';
export class DocClass extends DocItem<ApiClass> {
export class DocClass extends TypeParameterMixin(DocItem<ApiClass>) {
public readonly extendsTokens: TokenDocumentation[] | null;
public readonly implementsTokens: TokenDocumentation[][];
public readonly methods: DocMethod[] = [];
public readonly properties: DocProperty[] = [];
public readonly typeParameters: TypeParameterData[] = [];
public constructor(model: ApiModel, item: ApiClass) {
super(model, item);
@@ -29,8 +29,6 @@ export class DocClass extends DocItem<ApiClass> {
excerpt.excerpt.spannedTokens.map((token) => genToken(this.model, token)),
);
this.typeParameters = item.typeParameters.map((typeParam) => generateTypeParamData(model, typeParam));
for (const member of item.members) {
switch (member.kind) {
case ApiItemKind.Method:
@@ -52,7 +50,6 @@ export class DocClass extends DocItem<ApiClass> {
implementsTokens: this.implementsTokens,
methods: this.methods.map((method) => method.toJSON()),
properties: this.properties.map((prop) => prop.toJSON()),
typeParameters: this.typeParameters,
};
}
}

View File

@@ -1,26 +1,18 @@
import type { ApiFunction, ApiModel } from '@microsoft/api-extractor-model';
import type { ApiFunction, ApiModel, ApiParameterListMixin } from '@microsoft/api-extractor-model';
import { DocItem } from './DocItem';
import {
type ParameterDocumentation,
type TokenDocumentation,
genParameter,
genToken,
type TypeParameterData,
generateTypeParamData,
} from '~/util/parse.server';
import { TypeParameterMixin } from './TypeParameterMixin';
import { type TokenDocumentation, genToken, genParameter, ParameterDocumentation } from '~/util/parse.server';
export class DocFunction extends DocItem<ApiFunction> {
public readonly parameters: ParameterDocumentation[];
export class DocFunction extends TypeParameterMixin(DocItem<ApiFunction>) {
public readonly returnTypeTokens: TokenDocumentation[];
public readonly overloadIndex: number;
public readonly typeParameters: TypeParameterData[] = [];
public readonly parameters: ParameterDocumentation[];
public constructor(model: ApiModel, item: ApiFunction) {
super(model, item);
this.parameters = item.parameters.map((param) => genParameter(this.model, param));
this.returnTypeTokens = item.returnTypeExcerpt.spannedTokens.map((token) => genToken(this.model, token));
this.overloadIndex = item.overloadIndex;
this.typeParameters = item.typeParameters.map((typeParam) => generateTypeParamData(model, typeParam));
this.parameters = (item as ApiParameterListMixin).parameters.map((param) => genParameter(this.model, param));
}
public override toJSON() {
@@ -29,7 +21,6 @@ export class DocFunction extends DocItem<ApiFunction> {
parameters: this.parameters,
returnTypeTokens: this.returnTypeTokens,
overloadIndex: this.overloadIndex,
typeParameters: this.typeParameters,
};
}
}

View File

@@ -1,14 +1,14 @@
import { DocItem } from './DocItem';
import { DocMethodSignature } from './DocMethodSignature';
import { DocProperty } from './DocProperty';
import { TypeParameterMixin } from './TypeParameterMixin';
import { ApiInterface, ApiItemKind, ApiMethodSignature, ApiModel, ApiPropertySignature } from '~/api-extractor.server';
import { type TokenDocumentation, genToken, type TypeParameterData, generateTypeParamData } from '~/util/parse.server';
import { type TokenDocumentation, genToken } from '~/util/parse.server';
export class DocInterface extends DocItem<ApiInterface> {
export class DocInterface extends TypeParameterMixin(DocItem<ApiInterface>) {
public readonly extendsTokens: TokenDocumentation[][] | null;
public readonly methods: DocMethodSignature[] = [];
public readonly properties: DocProperty[] = [];
public readonly typeParameters: TypeParameterData[] = [];
public constructor(model: ApiModel, item: ApiInterface) {
super(model, item);
@@ -17,8 +17,6 @@ export class DocInterface extends DocItem<ApiInterface> {
excerpt.excerpt.spannedTokens.map((token) => genToken(this.model, token)),
);
this.typeParameters = item.typeParameters.map((typeParam) => generateTypeParamData(this.model, typeParam));
for (const member of item.members) {
switch (member.kind) {
case ApiItemKind.MethodSignature:
@@ -39,7 +37,6 @@ export class DocInterface extends DocItem<ApiInterface> {
extendsTokens: this.extendsTokens,
methods: this.methods.map((method) => method.toJSON()),
properties: this.properties.map((prop) => prop.toJSON()),
typeParameters: this.typeParameters,
};
}
}

View File

@@ -2,6 +2,8 @@ import type { ApiModel, ApiDeclaredItem } from '@microsoft/api-extractor-model';
import type { ReferenceData } from '~/model.server';
import { resolveName, genReference, resolveDocComment, TokenDocumentation, genToken } from '~/util/parse.server';
export type DocItemConstructor<T = DocItem> = new (...args: any[]) => T;
export class DocItem<T extends ApiDeclaredItem = ApiDeclaredItem> {
public readonly item: T;
public readonly name: string;

View File

@@ -1,33 +1,17 @@
import type { ApiMethod, ApiModel } from '@microsoft/api-extractor-model';
import { DocItem } from './DocItem';
import { DocFunction } from './DocFunction';
import { Visibility } from './Visibility';
import {
type ParameterDocumentation,
type TokenDocumentation,
genParameter,
genToken,
generateTypeParamData,
TypeParameterData,
} from '~/util/parse.server';
export class DocMethod extends DocItem<ApiMethod> {
public readonly parameters: ParameterDocumentation[];
export class DocMethod extends DocFunction {
public readonly static: boolean;
public readonly optional: boolean;
public readonly visibility: Visibility;
public readonly returnTypeTokens: TokenDocumentation[];
public readonly overloadIndex: number;
public readonly typeParameters: TypeParameterData[] = [];
public constructor(model: ApiModel, item: ApiMethod) {
super(model, item);
this.parameters = item.parameters.map((param) => genParameter(this.model, param));
this.static = item.isStatic;
this.optional = item.isOptional;
this.visibility = item.isProtected ? Visibility.Protected : Visibility.Public;
this.returnTypeTokens = item.returnTypeExcerpt.spannedTokens.map((token) => genToken(this.model, token));
this.overloadIndex = item.overloadIndex;
this.typeParameters = item.typeParameters.map((typeParam) => generateTypeParamData(this.model, typeParam));
}
public override toJSON() {
@@ -36,9 +20,6 @@ export class DocMethod extends DocItem<ApiMethod> {
static: this.static,
optional: this.optional,
visibility: this.visibility,
parameters: this.parameters,
returnTypeTokens: this.returnTypeTokens,
overloadIndex: this.overloadIndex,
};
}
}

View File

@@ -1,37 +1,18 @@
import type { ApiMethodSignature, ApiModel } from '@microsoft/api-extractor-model';
import { DocItem } from './DocItem';
import {
type ParameterDocumentation,
type TokenDocumentation,
genParameter,
genToken,
generateTypeParamData,
type TypeParameterData,
} from '~/util/parse.server';
import { DocFunction } from './DocFunction';
export class DocMethodSignature extends DocItem<ApiMethodSignature> {
public readonly parameters: ParameterDocumentation[];
export class DocMethodSignature extends DocFunction {
public readonly optional: boolean;
public readonly returnTypeTokens: TokenDocumentation[];
public readonly overloadIndex: number;
public readonly typeParameters: TypeParameterData[] = [];
public constructor(model: ApiModel, item: ApiMethodSignature) {
super(model, item);
this.parameters = item.parameters.map((param) => genParameter(this.model, param));
this.optional = item.isOptional;
this.returnTypeTokens = item.returnTypeExcerpt.spannedTokens.map((token) => genToken(this.model, token));
this.overloadIndex = item.overloadIndex;
this.typeParameters = item.typeParameters.map((typeParam) => generateTypeParamData(this.model, typeParam));
}
public override toJSON() {
return {
...super.toJSON(),
optional: this.optional,
parameters: this.parameters,
returnTypeTokens: this.returnTypeTokens,
overloadIndex: this.overloadIndex,
};
}
}

View File

@@ -1,22 +1,20 @@
import type { ApiModel, ApiTypeAlias } from '@microsoft/api-extractor-model';
import { DocItem } from './DocItem';
import { type TokenDocumentation, genToken, generateTypeParamData, type TypeParameterData } from '~/util/parse.server';
import { TypeParameterMixin } from './TypeParameterMixin';
import { type TokenDocumentation, genToken } from '~/util/parse.server';
export class DocTypeAlias extends DocItem<ApiTypeAlias> {
export class DocTypeAlias extends TypeParameterMixin(DocItem<ApiTypeAlias>) {
public readonly typeTokens: TokenDocumentation[];
public readonly typeParameters: TypeParameterData[] = [];
public constructor(model: ApiModel, item: ApiTypeAlias) {
super(model, item);
this.typeTokens = item.typeExcerpt.spannedTokens.map((token) => genToken(model, token));
this.typeParameters = item.typeParameters.map((typeParam) => generateTypeParamData(this.model, typeParam));
}
public override toJSON() {
return {
...super.toJSON(),
typeTokens: this.typeTokens,
typeParameters: this.typeParameters,
};
}
}

View File

@@ -0,0 +1,24 @@
import type { ApiItem, ApiModel, ApiTypeParameterListMixin } from '@microsoft/api-extractor-model';
import type { DocItemConstructor } from './DocItem';
import { generateTypeParamData, TypeParameterData } from '~/util/parse.server';
export function TypeParameterMixin<TBase extends DocItemConstructor>(Base: TBase) {
return class Mixed extends Base {
public readonly typeParameters: TypeParameterData[] = [];
public constructor(...args: any[]);
public constructor(model: ApiModel, item: ApiItem) {
super(model, item);
this.typeParameters = (item as ApiTypeParameterListMixin).typeParameters.map((typeParam) =>
generateTypeParamData(this.model, typeParam),
);
}
public override toJSON() {
return {
...super.toJSON(),
typeParameterData: this.typeParameters,
};
}
};
}

View File

@@ -1,7 +1,7 @@
import { VscSymbolClass, VscSymbolMethod, VscSymbolEnum, VscSymbolInterface, VscSymbolVariable } from 'react-icons/vsc';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vs } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { TypeParamTable } from './TypeParamTable';
import { generateIcon } from '~/util/icon';
import type { TypeParameterData } from '~/util/parse.server';
export interface DocContainerProps {
@@ -13,27 +13,25 @@ export interface DocContainerProps {
typeParams?: TypeParameterData[];
}
const symbolClass = 'mr-2';
const icons = {
Class: <VscSymbolClass color="blue" className={symbolClass} />,
Method: <VscSymbolMethod className={symbolClass} />,
Function: <VscSymbolMethod color="purple" className={symbolClass} />,
Enum: <VscSymbolEnum className={symbolClass} />,
Interface: <VscSymbolInterface color="blue" className={symbolClass} />,
TypeAlias: <VscSymbolVariable color="blue" className={symbolClass} />,
};
export function DocContainer({ name, kind, excerpt, summary, typeParams, children }: DocContainerProps) {
return (
<div className="px-10">
<h1 style={{ fontFamily: 'JetBrains Mono' }} className="flex items-csenter content-center">
{icons[kind as keyof typeof icons]}
<h1 className="font-mono flex items-center content-center break-all">
{generateIcon(kind, 'mr-2')}
{name}
</h1>
<h3>Code declaration:</h3>
<SyntaxHighlighter language="typescript" style={vs} codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}>
{excerpt}
</SyntaxHighlighter>
<div>
<SyntaxHighlighter
wrapLines
wrapLongLines
language="typescript"
style={vs}
codeTagProps={{ style: { fontFamily: 'JetBrains Mono' } }}
>
{excerpt}
</SyntaxHighlighter>
</div>
{typeParams?.length ? (
<>
<h3>Type Parameters</h3>

View File

@@ -0,0 +1,45 @@
import { AiOutlineMenu } from 'react-icons/ai';
import { VscPackage } from 'react-icons/vsc';
import { generateIcon } from '~/util/icon';
import type { getMembers } from '~/util/parse.server';
export interface ItemListProps {
packageName: string;
data: {
members: ReturnType<typeof getMembers>;
};
}
function onMenuClick() {
console.log('menu clicked');
// Todo show/hide list
}
export function ItemSidebar({ packageName, data }: ItemListProps) {
return (
<div className="flex flex-col max-h-full min-w-[270px] border-r-solid border-b-solid border-gray border-width-0.5">
<div className="flex justify-between content-center items-center border-b-solid border-gray border-width-0.5">
<h1 className="px-2 font-mono flex items-center content-center">
<VscPackage className="px-1" />
{`${packageName}`}
</h1>
<button className="lg:hidden mr-2 bg-transparent border-none" onClick={onMenuClick}>
<AiOutlineMenu size={32} />
</button>
</div>
<div className="hidden lg:block overflow-y-scroll overflow-x-clip p-7">
{data.members.map((member, i) => (
<div key={i} className="mb-1">
<a
className="flex content-center items-center align-center font-mono no-underline break-all color-blue-500"
href={member.path}
>
{generateIcon(member.kind, 'px-1')}
{member.name}
</a>
</div>
))}
</div>
</div>
);
}

View File

@@ -14,7 +14,7 @@ export function Class({ data }: ClassProps) {
kind={data.kind}
excerpt={data.excerpt}
summary={data.summary}
typeParams={data.typeParameters}
typeParams={data.typeParameterData}
>
<>
{data.properties.length ? <PropertyList data={data.properties} /> : null}

View File

@@ -13,7 +13,7 @@ export function Function({ data }: FunctionProps) {
kind={data.kind}
excerpt={data.excerpt}
summary={data.summary}
typeParams={data.typeParameters}
typeParams={data.typeParameterData}
>
<ParameterTable data={data.parameters} />
</DocContainer>

View File

@@ -14,7 +14,7 @@ export function Interface({ data }: InterfaceProps) {
kind={data.kind}
excerpt={data.excerpt}
summary={data.summary}
typeParams={data.typeParameters}
typeParams={data.typeParameterData}
>
<>
{data.properties.length ? <PropertyList data={data.properties} /> : null}

View File

@@ -12,7 +12,7 @@ export function TypeAlias({ data }: TypeAliasProps) {
kind={data.kind}
excerpt={data.excerpt}
summary={data.summary}
typeParams={data.typeParameters}
typeParams={data.typeParameterData}
>
<div>WIP</div>
</DocContainer>

View File

@@ -1,19 +1,32 @@
/* eslint-disable @typescript-eslint/no-throw-literal */
import { json } from '@remix-run/node';
import { Params, useLoaderData } from '@remix-run/react';
import { Outlet, Params, useLoaderData, useParams } from '@remix-run/react';
import { ItemSidebar } from '~/components/ItemSidebar';
import { createApiModel } from '~/util/api-model.server';
import { findPackage, getMembers } from '~/util/parse.server';
export async function loader({ params }: { params: Params }) {
const UnknownResponse = new Response('Not Found', {
status: 404,
});
const res = await fetch(
`https://raw.githubusercontent.com/discordjs/docs/main/${params.packageName!}/${params.branchName!}.api.json`,
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const data = await res.json();
const model = createApiModel(data);
const data = await res.json().catch(() => {
throw UnknownResponse;
});
const model = createApiModel(data);
const pkg = findPackage(model, params.packageName!);
if (!pkg) {
throw UnknownResponse;
}
return json({
members: getMembers(pkg!),
members: getMembers(pkg),
});
}
@@ -23,14 +36,16 @@ interface LoaderData {
export default function Package() {
const data = useLoaderData<LoaderData>();
const { packageName } = useParams();
return (
<ul>
{data.members.map((member, i) => (
<li key={i}>
<a href={member.path}>{member.name}</a>
</li>
))}
</ul>
<div className="flex flex-col lg:flex-row overflow-none max-w-full h-full">
<div className="w-full lg:min-w-1/4 lg:max-w-1/4">
<ItemSidebar packageName={packageName!} data={data} />
</div>
<div>
<Outlet />
</div>
</div>
);
}

View File

@@ -0,0 +1,14 @@
import { VscSymbolClass, VscSymbolMethod, VscSymbolEnum, VscSymbolInterface, VscSymbolVariable } from 'react-icons/vsc';
export function generateIcon(kind: string, className?: string) {
const icons = {
Class: <VscSymbolClass color="blue" className={className} />,
Method: <VscSymbolMethod className={className} />,
Function: <VscSymbolMethod color="purple" className={className} />,
Enum: <VscSymbolEnum className={className} />,
Interface: <VscSymbolInterface color="blue" className={className} />,
TypeAlias: <VscSymbolVariable color="blue" className={className} />,
};
return icons[kind as keyof typeof icons];
}

View File

@@ -165,6 +165,7 @@ export function genParameter(model: ApiModel, param: Parameter): ParameterDocume
export function getMembers(pkg: ApiPackage) {
return pkg.members[0]!.members.map((member) => ({
name: member.displayName,
kind: member.kind,
path: generatePath(member.getHierarchy()),
}));
}

View File

@@ -1,3 +1,9 @@
import { defineConfig } from 'unocss';
export default defineConfig({});
export default defineConfig({
theme: {
fontFamily: {
mono: ['JetBrains Mono'],
},
},
});