Generated docs cleanup (#3935)

Our generated docs are pretty verbose and space inefficient. This diff
has a few design tweaks to try and make sure that the information that's
emphasised is the stuff that's most important, and makes the typical
docs item use a bit less space in the process.


![image](https://github.com/tldraw/tldraw/assets/1489520/df433ae0-1400-4f5b-951e-e25869621a40)



### Change Type

- [x] `docs` — Changes to the documentation, examples, or templates.
- [x] `improvement` — Improving existing features
This commit is contained in:
alex 2024-06-13 17:04:12 +01:00 committed by GitHub
parent 012e54959d
commit fba82ed924
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 219 additions and 82 deletions

View file

@ -5,6 +5,7 @@ import { Breadcrumb } from './Breadcrumb'
import { Header } from './Header' import { Header } from './Header'
import { Mdx } from './Mdx' import { Mdx } from './Mdx'
import { Sidebar } from './Sidebar' import { Sidebar } from './Sidebar'
import { TitleWithSourceLink } from './mdx-components/api-docs'
import { Image } from './mdx-components/generic' import { Image } from './mdx-components/generic'
export async function ArticleReferenceDocsPage({ article }: { article: Article }) { export async function ArticleReferenceDocsPage({ article }: { article: Article }) {
@ -25,7 +26,9 @@ export async function ArticleReferenceDocsPage({ article }: { article: Article }
<main className="main-content article article__api-docs"> <main className="main-content article article__api-docs">
<div className="page-header"> <div className="page-header">
<Breadcrumb section={section} category={category} /> <Breadcrumb section={section} category={category} />
<h1>{article.title}</h1> <TitleWithSourceLink source={article.sourceUrl} large tags={article.apiTags?.split(',')}>
<h1>{article.title}</h1>
</TitleWithSourceLink>
</div> </div>
{article.hero && <Image alt="hero" title={article.title} src={`images/${article.hero}`} />} {article.hero && <Image alt="hero" title={article.title} src={`images/${article.hero}`} />}
{article.content && <Mdx content={article.content} />} {article.content && <Mdx content={article.content} />}

View file

@ -1,4 +1,6 @@
import classNames from 'classnames'
import { ReactNode } from 'react' import { ReactNode } from 'react'
import { Icon } from '../Icon'
export function ParametersTable({ children }: { children: ReactNode }) { export function ParametersTable({ children }: { children: ReactNode }) {
return ( return (
@ -27,3 +29,38 @@ export function ParametersTableName({ children }: { children: ReactNode }) {
export function ParametersTableDescription({ children }: { children: ReactNode }) { export function ParametersTableDescription({ children }: { children: ReactNode }) {
return <td className="article__parameters-table__description">{children}</td> return <td className="article__parameters-table__description">{children}</td>
} }
export function TitleWithSourceLink({
children,
source,
large,
tags,
}: {
children: ReactNode
source?: string | null
large?: boolean
tags?: string[]
}) {
return (
<div
className={classNames(
'article__title-with-source-link',
large && 'article__title-with-source-link--large'
)}
>
{children}
<div className="article__title-with-source-link__meta">
{tags?.map((tag) => <Tag key={tag}>{tag}</Tag>)}
{source && (
<a href={source} target="_blank" rel="noopener noreferrer" title="Source code">
<Icon icon="code" />
</a>
)}
</div>
</div>
)
}
export function Tag({ children }: { children: string }) {
return <span className={classNames(`article__tag`, `article__tag--${children}`)}>{children}</span>
}

View file

@ -0,0 +1,5 @@
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.9285 5.37141C20.1336 4.85863 19.8842 4.27666 19.3714 4.07155C18.8586 3.86643 18.2766 4.11585 18.0715 4.62863L10.0715 24.6286C9.86641 25.1414 10.1158 25.7234 10.6286 25.9285C11.1414 26.1336 11.7234 25.8842 11.9285 25.3714L19.9285 5.37141Z" fill="black"/>
<path d="M7.70711 10.2929C8.09763 10.6834 8.09763 11.3166 7.70711 11.7071L4.41421 15L7.70711 18.2929C8.09763 18.6834 8.09763 19.3166 7.70711 19.7071C7.31658 20.0977 6.68342 20.0977 6.29289 19.7071L2.29289 15.7071C1.90237 15.3166 1.90237 14.6834 2.29289 14.2929L6.29289 10.2929C6.68342 9.90239 7.31658 9.90239 7.70711 10.2929Z" fill="black"/>
<path d="M22.2929 10.2929C22.6834 9.90239 23.3166 9.90239 23.7071 10.2929L27.7071 14.2929C28.0976 14.6834 28.0976 15.3166 27.7071 15.7071L23.7071 19.7071C23.3166 20.0977 22.6834 20.0977 22.2929 19.7071C21.9024 19.3166 21.9024 18.6834 22.2929 18.2929L25.5858 15L22.2929 11.7071C21.9024 11.3166 21.9024 10.6834 22.2929 10.2929Z" fill="black"/>
</svg>

After

Width:  |  Height:  |  Size: 1 KiB

View file

@ -72,6 +72,7 @@ export async function connect(opts: { reset?: boolean; mode: 'readonly' | 'readw
componentCode TEXT, componentCode TEXT,
componentCodeFiles TEXT, componentCodeFiles TEXT,
keywords TEXT, keywords TEXT,
apiTags TEXT,
content TEXT NOT NULL, content TEXT NOT NULL,
path TEXT, path TEXT,
FOREIGN KEY (authorId) REFERENCES authors(id), FOREIGN KEY (authorId) REFERENCES authors(id),

View file

@ -118,6 +118,7 @@ export function generateSection(section: InputSection, articles: Articles, index
: isUncategorized : isUncategorized
? `/${section.id}/${articleId}` ? `/${section.id}/${articleId}`
: `/${section.id}/${categoryId}/${articleId}`, : `/${section.id}/${categoryId}/${articleId}`,
apiTags: parsed.data.apiTags ?? null,
} }
if (isExamples) { if (isExamples) {

View file

@ -14,15 +14,14 @@ import {
ApiMethod, ApiMethod,
ApiMethodSignature, ApiMethodSignature,
ApiNamespace, ApiNamespace,
ApiOptionalMixin,
ApiProperty, ApiProperty,
ApiPropertySignature, ApiPropertySignature,
ApiReadonlyMixin, ApiReadonlyMixin,
ApiReleaseTagMixin,
ApiStaticMixin, ApiStaticMixin,
ApiTypeAlias, ApiTypeAlias,
ApiVariable, ApiVariable,
Excerpt, Excerpt,
ReleaseTag,
} from '@microsoft/api-extractor-model' } from '@microsoft/api-extractor-model'
import { MarkdownWriter, formatWithPrettier, getPath, getSlug } from '../utils' import { MarkdownWriter, formatWithPrettier, getPath, getSlug } from '../utils'
@ -31,8 +30,6 @@ interface Result {
keywords: string[] keywords: string[]
} }
const REPO_URL = 'https://github.com/tldraw/tldraw/blob/main/'
const date = new Intl.DateTimeFormat('en-US', { const date = new Intl.DateTimeFormat('en-US', {
year: 'numeric', year: 'numeric',
month: '2-digit', month: '2-digit',
@ -101,8 +98,7 @@ export async function getApiMarkdown(
addMarkdown(membersResult, constructorResult.markdown) addMarkdown(membersResult, constructorResult.markdown)
} }
if (properties.length || componentProps) { if (properties.length || isComponent) {
addMarkdown(propertiesResult, `## Properties\n\n`)
if (componentProps) addExtends(propertiesResult, componentProps) if (componentProps) addExtends(propertiesResult, componentProps)
for (const member of properties) { for (const member of properties) {
const slug = getSlug(member) const slug = getSlug(member)
@ -125,9 +121,14 @@ export async function getApiMarkdown(
} }
) )
} }
if (propertiesResult.markdown.trim()) { if (propertiesResult.markdown.trim()) {
addMarkdown(toc, `- [Properties](#properties)\n`) addMarkdown(toc, `- [Properties](#properties)\n`)
addMarkdown(membersResult, `## Properties\n\n`)
addMarkdown(membersResult, propertiesResult.markdown) addMarkdown(membersResult, propertiesResult.markdown)
} else if (isComponent && !componentProps) {
addMarkdown(membersResult, `## Properties\n\n`)
addMarkdown(membersResult, `This component does not take any props.\n\n`)
} }
} }
@ -152,12 +153,8 @@ export async function getApiMarkdown(
result.markdown += `</details>\n\n` result.markdown += `</details>\n\n`
} }
addTags(model, result, item)
await addDocComment(model, result, item) await addDocComment(model, result, item)
addLinkToSource(result, item)
if (membersResult.markdown.length) { if (membersResult.markdown.length) {
addHorizontalRule(result) addHorizontalRule(result)
addMarkdown(result, membersResult.markdown) addMarkdown(result, membersResult.markdown)
@ -179,9 +176,8 @@ async function addMarkdownForMember(
{ isComponentProp = false } = {} { isComponentProp = false } = {}
) { ) {
if (member.displayName.startsWith('_')) return if (member.displayName.startsWith('_')) return
addMemberName(result, member) addMemberNameAndMeta(result, model, member, { isComponentProp })
addTags(model, result, member, { isComponentProp }) await addDocComment(model, result, member)
await addDocComment(model, result, member, { isComponentProp })
} }
async function addFrontmatter( async function addFrontmatter(
@ -211,40 +207,71 @@ async function addFrontmatter(
} }
} }
result.markdown += `--- const frontmatter: Record<string, string> = {
title: ${member.displayName} title: member.displayName,
status: published status: 'published',
description: ${description} description,
category: ${categoryName} category: categoryName,
group: ${model.isComponent(member) ? APIGroup.Component : member.kind} group: model.isComponent(member) ? APIGroup.Component : member.kind,
author: api date,
date: ${date} order: order.toString(),
order: ${order} apiTags: getTags(model, member).join(','),
sourceUrl: ${'_fileUrlPath' in member ? member._fileUrlPath : ''}${kw} }
---
` if (member instanceof ApiDeclaredItem && member.sourceLocation.fileUrl) {
frontmatter.sourceUrl = member.sourceLocation.fileUrl
}
result.markdown += [
'---',
...Object.entries(frontmatter).map(([key, value]) => `${key}: ${value}`),
kw,
'---',
'',
].join('\n')
} }
function addHorizontalRule(result: Result) { function addHorizontalRule(result: Result) {
result.markdown += `---\n\n` result.markdown += `---\n\n`
} }
function addMemberName(result: Result, member: ApiItem) { function getItemTitle(item: ApiItem) {
if (member.kind === 'Constructor') { if (item.kind === ApiItemKind.Constructor) {
result.markdown += `### Constructor\n\n` return 'Constructor'
return
} }
if (!member.displayName) return const name = item.displayName
result.markdown += `### \`${member.displayName}${member.kind === 'Method' ? '()' : ''}\`\n\n` if (item.kind === ApiItemKind.Method || item.kind === ApiItemKind.Function) {
return `${name}()`
}
return name
}
function addMemberNameAndMeta(
result: Result,
model: TldrawApiModel,
item: ApiItem,
{ level = 3, isComponentProp = false } = {}
) {
const heading = `${'#'.repeat(level)} ${getItemTitle(item)}`
if (item instanceof ApiDeclaredItem && item.sourceLocation.fileUrl) {
const source = item.sourceLocation.fileUrl
const tags = getTags(model, item, { isComponentProp, includeKind: false })
result.markdown += [
`<TitleWithSourceLink source={${JSON.stringify(source)}} tags={${JSON.stringify(tags)}}>`,
'',
heading,
'',
'</TitleWithSourceLink>',
'',
].join('\n')
} else {
result.markdown += `${heading}\n\n`
}
} }
async function addDocComment( async function addDocComment(model: TldrawApiModel, result: Result, member: ApiItem) {
model: TldrawApiModel,
result: Result,
member: ApiItem,
{ isComponentProp = false } = {}
) {
if (!(member instanceof ApiDocumentedItem)) { if (!(member instanceof ApiDocumentedItem)) {
return return
} }
@ -256,7 +283,28 @@ async function addDocComment(
member, member,
member.tsdocComment.summarySection member.tsdocComment.summarySection
) )
}
if (
!isComponent &&
(member instanceof ApiVariable ||
member instanceof ApiTypeAlias ||
member instanceof ApiProperty ||
member instanceof ApiPropertySignature ||
member instanceof ApiClass ||
member instanceof ApiFunction ||
member instanceof ApiInterface ||
member instanceof ApiEnum ||
member instanceof ApiNamespace ||
member instanceof ApiMethod)
) {
result.markdown += await excerptToMarkdown(member, member.excerpt, {
kind: member.kind,
})
result.markdown += `\n\n`
}
if (member.tsdocComment) {
const exampleBlocks = member.tsdocComment.customBlocks.filter( const exampleBlocks = member.tsdocComment.customBlocks.filter(
(block) => block.blockTag.tagNameWithUpperCase === '@EXAMPLE' (block) => block.blockTag.tagNameWithUpperCase === '@EXAMPLE'
) )
@ -270,25 +318,6 @@ async function addDocComment(
} }
} }
if (
member instanceof ApiVariable ||
member instanceof ApiTypeAlias ||
member instanceof ApiProperty ||
member instanceof ApiPropertySignature ||
member instanceof ApiClass ||
member instanceof ApiFunction ||
member instanceof ApiInterface ||
member instanceof ApiEnum ||
member instanceof ApiNamespace ||
member instanceof ApiMethod
) {
if (!isComponentProp) result.markdown += `<ApiHeading>Signature</ApiHeading>\n\n`
result.markdown += await excerptToMarkdown(member, member.excerpt, {
kind: member.kind,
})
result.markdown += `\n\n`
}
if (isComponent) return if (isComponent) return
if ( if (
@ -452,32 +481,37 @@ async function excerptToMarkdown(
].join('\n') ].join('\n')
} }
function addTags( function getTags(
model: TldrawApiModel, model: TldrawApiModel,
result: Result,
member: ApiItem, member: ApiItem,
{ isComponentProp = false } = {} { isComponentProp = false, includeKind = true } = {}
) { ) {
const tags = [] const tags = []
if (!isComponentProp) {
if (ApiReleaseTagMixin.isBaseClassOf(member)) { if (ApiStaticMixin.isBaseClassOf(member) && member.isStatic) {
tags.push(ReleaseTag[member.releaseTag]) tags.push('static')
} }
if (ApiStaticMixin.isBaseClassOf(member) && member.isStatic) { let kind = member.kind.toLowerCase()
tags.push('static') if (ApiReadonlyMixin.isBaseClassOf(member) && member.isReadonly) {
} if (member.kind === ApiItemKind.Variable) {
if (ApiReadonlyMixin.isBaseClassOf(member) && member.isReadonly) { kind = 'constant'
} else if (!isComponentProp) {
tags.push('readonly') tags.push('readonly')
} }
} }
if (member instanceof ApiPropertySignature && member.isOptional) { if (model.isComponent(member)) {
kind = 'component'
}
if (ApiOptionalMixin.isBaseClassOf(member) && member.isOptional) {
tags.push('optional') tags.push('optional')
} }
if (!isComponentProp) {
const kind = model.isComponent(member) ? 'component' : member.kind.toLowerCase() if (includeKind) {
tags.push(kind) tags.push(kind)
} }
result.markdown += `<Small>${tags.filter((t) => t.toLowerCase() !== 'none').join(' ')}</Small>\n\n`
return tags
} }
function addExtends(result: Result, item: ApiItem) { function addExtends(result: Result, item: ApiItem) {
@ -515,10 +549,3 @@ function addExtends(result: Result, item: ApiItem) {
'', '',
].join('\n') ].join('\n')
} }
function addLinkToSource(result: Result, member: ApiItem) {
if ('_fileUrlPath' in member && member._fileUrlPath) {
result.markdown += `<ApiHeading>Source</ApiHeading>\n\n`
result.markdown += `[${member._fileUrlPath}](${REPO_URL}${member._fileUrlPath})\n\n`
}
}

View file

@ -296,6 +296,62 @@ body {
text-align: right; text-align: right;
} }
.article__title-with-source-link {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-start;
}
.article__title-with-source-link__meta {
display: flex;
align-items: center;
justify-content: center;
margin-left: auto;
padding-left: 16px;
}
.article__title-with-source-link__meta > a {
display: block;
width: 32px;
height: 32px;
border-radius: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.article__title-with-source-link .article__title-with-source-link__meta > a {
color: var(--color-text);
}
.article__title-with-source-link > a:hover {
background-color: var(--color-tint-1);
}
.article__title-with-source-link .icon {
display: block;
width: 20px;
height: 20px;
}
.article__title-with-source-link--large .article__title-with-source-link__meta > a {
width: 42px;
height: 42px;
}
.article__title-with-source-link--large .icon {
width: 24px;
height: 24px;
}
.article__tag {
display: inline-block;
padding: 4px 6px;
border-radius: var(--border-radius-menu);
background-color: var(--color-tint-0);
color: var(--color-tint-5);
font-size: 12px;
margin-right: 8px;
line-height: 1;
}
.article__title-with-source-link--large .article__tag {
font-size: 14px;
}
/* Prev / Next Links */ /* Prev / Next Links */
.article__links { .article__links {
@ -472,6 +528,9 @@ body {
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
.article a.anchor {
color: inherit;
}
@media (hover: hover) { @media (hover: hover) {
.article a:hover { .article a:hover {

View file

@ -114,6 +114,8 @@ export interface Article extends ContentPage {
componentCode: string | null componentCode: string | null
/** The article's code example files, JSON stringified (optional). */ /** The article's code example files, JSON stringified (optional). */
componentCodeFiles: string | null componentCodeFiles: string | null
/** Tags for this item if it's a reference page */
apiTags: string | null
} }
export enum ArticleStatus { export enum ArticleStatus {

View file

@ -38,9 +38,10 @@ export async function addContentToDb(
componentCode, componentCode,
componentCodeFiles, componentCodeFiles,
keywords, keywords,
apiTags,
content, content,
path path
) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)` ) VALUES ( ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
) )
for (let i = 0; i < content.sections.length; i++) { for (let i = 0; i < content.sections.length; i++) {
@ -98,6 +99,7 @@ export async function addContentToDb(
article.componentCode, article.componentCode,
article.componentCodeFiles, article.componentCodeFiles,
article.keywords.join(', '), article.keywords.join(', '),
article.apiTags,
article.content, article.content,
article.path article.path
) )

View file

@ -191,7 +191,7 @@
* SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName> * SUPPORTED TOKENS: <projectFolder>, <packageName>, <unscopedPackageName>
* DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json" * DEFAULT VALUE: "<projectFolder>/temp/<unscopedPackageName>.api.json"
*/ */
"apiJsonFilePath": "<projectFolder>/packages/<unscopedPackageName>/api/api.json" "apiJsonFilePath": "<projectFolder>/packages/<unscopedPackageName>/api/api.json",
/** /**
* Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations * Whether "forgotten exports" should be included in the doc model file. Forgotten exports are declarations
* flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to * flagged with `ae-forgotten-export` warnings. See https://api-extractor.com/pages/messages/ae-forgotten-export/ to
@ -214,7 +214,7 @@
* SUPPORTED TOKENS: none * SUPPORTED TOKENS: none
* DEFAULT VALUE: "" * DEFAULT VALUE: ""
*/ */
// "projectFolderUrl": "http://github.com/path/to/your/projectFolder" "projectFolderUrl": "https://github.com/tldraw/tldraw/blob/main"
}, },
/** /**
* Configures how the .d.ts rollup file will be generated. * Configures how the .d.ts rollup file will be generated.