mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-25 04:20:02 +00:00
TypeScript & UI: Support displaying parameters & return types in Type Table
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'fumadocs-typescript': patch
|
||||
'fumadocs-ui': patch
|
||||
---
|
||||
|
||||
Type Table: Support displaying parameters & return types
|
||||
@@ -11,7 +11,14 @@ description: Auto-generated type table
|
||||
*
|
||||
* See https://fumadocs.vercel.app/docs/ui/components/type-table
|
||||
*/
|
||||
name: string;
|
||||
name: boolean | null;
|
||||
|
||||
/**
|
||||
* @param name - user name.
|
||||
* @param allowNull - is null value allowed.
|
||||
* @returns user ID
|
||||
**/
|
||||
fn: (name: string, allowNull?: boolean) => string
|
||||
|
||||
/**
|
||||
* We love Shiki.
|
||||
@@ -21,7 +28,7 @@ description: Auto-generated type table
|
||||
* \`\`\`
|
||||
* @default { a: "test" }
|
||||
*/
|
||||
options: Partial<{ a: unknown }>;
|
||||
options?: Partial<{ a: unknown }>;
|
||||
|
||||
}`} />
|
||||
|
||||
|
||||
@@ -28,11 +28,16 @@ export interface DocEntry {
|
||||
type: string;
|
||||
simplifiedType: string;
|
||||
|
||||
tags: Record<string, string>;
|
||||
tags: RawTag[];
|
||||
required: boolean;
|
||||
deprecated: boolean;
|
||||
}
|
||||
|
||||
export interface RawTag {
|
||||
name: string;
|
||||
text: string;
|
||||
}
|
||||
|
||||
interface EntryContext {
|
||||
program: Project;
|
||||
transform?: Transformer;
|
||||
@@ -201,16 +206,21 @@ function getDocEntry(
|
||||
.getTypeChecker()
|
||||
.getTypeOfSymbolAtLocation(prop, context.declaration);
|
||||
const isOptional = prop.isOptional();
|
||||
const tags = Object.fromEntries(
|
||||
prop
|
||||
.getJsDocTags()
|
||||
.map((tag) => [tag.getName(), ts.displayPartsToString(tag.getText())]),
|
||||
const tags = prop.getJsDocTags().map(
|
||||
(tag) =>
|
||||
({
|
||||
name: tag.getName(),
|
||||
text: ts.displayPartsToString(tag.getText()),
|
||||
}) satisfies RawTag,
|
||||
);
|
||||
|
||||
let type = getFullType(subType, isOptional);
|
||||
let type = getFullType(subType);
|
||||
|
||||
if ('remarks' in tags) {
|
||||
type = /^`(?<name>.+)`/.exec(tags.remarks)?.[1] ?? type;
|
||||
for (const tag of tags) {
|
||||
if (tag.name !== 'remarks') continue;
|
||||
|
||||
// replace type with @remarks
|
||||
type = /^`(?<name>.+)`/.exec(tag.text)?.[1] ?? type;
|
||||
}
|
||||
|
||||
const entry: DocEntry = {
|
||||
@@ -222,11 +232,13 @@ function getDocEntry(
|
||||
),
|
||||
tags,
|
||||
type,
|
||||
simplifiedType: getSimpleForm(subType, program.getTypeChecker()),
|
||||
simplifiedType: getSimpleForm(
|
||||
subType,
|
||||
program.getTypeChecker(),
|
||||
isOptional,
|
||||
),
|
||||
required: !isOptional,
|
||||
deprecated: prop
|
||||
.getJsDocTags()
|
||||
.some((tag) => tag.getName() === 'deprecated'),
|
||||
deprecated: tags.some((tag) => tag.name === 'deprecated'),
|
||||
};
|
||||
|
||||
transform?.call(context, entry, subType, prop);
|
||||
@@ -234,25 +246,13 @@ function getDocEntry(
|
||||
return entry;
|
||||
}
|
||||
|
||||
function getFullType(type: Type, isOptional: boolean): string {
|
||||
if (type.isUnion() && isOptional) {
|
||||
const t = type.compilerType as ts.UnionType;
|
||||
const originalTypes = t.types;
|
||||
|
||||
t.types = t.types.filter((v) => v.flags !== ts.TypeFlags.Undefined);
|
||||
const result = getFullType(type, false);
|
||||
t.types = originalTypes;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
let typeName = type
|
||||
.getNonNullableType()
|
||||
.getText(
|
||||
undefined,
|
||||
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope &
|
||||
ts.TypeFormatFlags.NoTruncation,
|
||||
);
|
||||
function getFullType(type: Type): string {
|
||||
let typeName = type.getText(
|
||||
undefined,
|
||||
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
|
||||
ts.TypeFormatFlags.NoTruncation |
|
||||
ts.TypeFormatFlags.InTypeAlias,
|
||||
);
|
||||
|
||||
if (type.getAliasSymbol() && type.getAliasTypeArguments().length === 0) {
|
||||
typeName = type.getAliasSymbol()?.getEscapedName() ?? typeName;
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import * as ts from 'ts-morph';
|
||||
|
||||
function simplifyType(type: ts.Type, checker: ts.TypeChecker): string {
|
||||
// Handle union types
|
||||
export function getSimpleForm(
|
||||
type: ts.Type,
|
||||
checker: ts.TypeChecker,
|
||||
noUndefined = false,
|
||||
): string {
|
||||
if (type.isUndefined() && noUndefined) return '';
|
||||
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias && type.getAliasTypeArguments().length === 0) {
|
||||
return alias.getEscapedName();
|
||||
}
|
||||
|
||||
if (type.isUnion()) {
|
||||
const types: string[] = [];
|
||||
for (const t of type.getUnionTypes()) {
|
||||
const str = simplifyType(t, checker);
|
||||
if (str !== 'never') types.unshift(str);
|
||||
const str = getSimpleForm(t, checker, noUndefined);
|
||||
if (str.length > 0 && str !== 'never') types.unshift(str);
|
||||
}
|
||||
|
||||
return types.length > 0 ? types.join(' | ') : 'never';
|
||||
return types.length > 0
|
||||
? // boolean | null will become true | false | null, need to ensure it's still returned as boolean
|
||||
types.join(' | ').replace('true | false', 'boolean')
|
||||
: 'never';
|
||||
}
|
||||
|
||||
// Handle intersection types
|
||||
if (type.isIntersection()) {
|
||||
const types: string[] = [];
|
||||
for (const t of type.getIntersectionTypes()) {
|
||||
types.unshift(simplifyType(t, checker));
|
||||
const str = getSimpleForm(t, checker, noUndefined);
|
||||
if (str.length > 0 && str !== 'never') types.unshift(str);
|
||||
}
|
||||
|
||||
return types.join(' & ');
|
||||
@@ -25,17 +38,12 @@ function simplifyType(type: ts.Type, checker: ts.TypeChecker): string {
|
||||
if (type.isTuple()) {
|
||||
const elements = type
|
||||
.getTupleElements()
|
||||
.map((t) => simplifyType(t, checker))
|
||||
.map((t) => getSimpleForm(t, checker))
|
||||
.join(', ');
|
||||
|
||||
return `[${elements}]`;
|
||||
}
|
||||
|
||||
const alias = type.getAliasSymbol();
|
||||
if (alias && type.getAliasTypeArguments().length === 0) {
|
||||
return alias.getEscapedName();
|
||||
}
|
||||
|
||||
if (type.isArray() || type.isReadonlyArray()) {
|
||||
return 'array';
|
||||
}
|
||||
@@ -48,9 +56,9 @@ function simplifyType(type: ts.Type, checker: ts.TypeChecker): string {
|
||||
return 'object';
|
||||
}
|
||||
|
||||
return type.getText();
|
||||
}
|
||||
|
||||
export function getSimpleForm(type: ts.Type, checker: ts.TypeChecker): string {
|
||||
return simplifyType(type, checker);
|
||||
return type.getText(
|
||||
undefined,
|
||||
ts.TypeFormatFlags.UseAliasDefinedOutsideCurrentScope |
|
||||
ts.TypeFormatFlags.InTypeAlias,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import * as path from 'node:path';
|
||||
import {
|
||||
type DocEntry,
|
||||
type GeneratedDoc,
|
||||
type Generator,
|
||||
type GenerateOptions,
|
||||
type Generator,
|
||||
} from './base';
|
||||
|
||||
interface Templates {
|
||||
@@ -39,8 +39,8 @@ ${doc.description}
|
||||
|
||||
${c.description || 'No Description'}
|
||||
|
||||
${Object.entries(c.tags)
|
||||
.map(([tag, value]) => `- ${tag}:\n${replaceJsDocLinks(value)}`)
|
||||
${c.tags
|
||||
.map((tag) => `- ${tag.name}:\n${replaceJsDocLinks(tag.text)}`)
|
||||
.join('\n')}
|
||||
|
||||
</div>`,
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
import { RawTag } from '@/lib/base';
|
||||
|
||||
export interface ParameterTag {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface TypedTags {
|
||||
default?: string;
|
||||
params?: ParameterTag[];
|
||||
returns?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse tags, only returns recognized fields.
|
||||
*/
|
||||
export function parseTags(tags: RawTag[]): TypedTags {
|
||||
const typed: TypedTags = {};
|
||||
|
||||
for (const { name: key, text } of tags) {
|
||||
if (key === 'default' || key === 'defaultValue') {
|
||||
typed.default = text;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'param') {
|
||||
const [param, description] = text.split('-', 2);
|
||||
|
||||
typed.params ??= [];
|
||||
typed.params.push({
|
||||
name: param.trim(),
|
||||
description: description.trim(),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (key === 'returns') {
|
||||
typed.returns = text;
|
||||
}
|
||||
}
|
||||
|
||||
return typed;
|
||||
}
|
||||
+88
-49
@@ -2,10 +2,10 @@ import type { Root } from 'mdast';
|
||||
import type { Nodes } from 'hast';
|
||||
import type { Transformer } from 'unified';
|
||||
import type {
|
||||
Expression,
|
||||
ExpressionStatement,
|
||||
ObjectExpression,
|
||||
Program,
|
||||
Property,
|
||||
} from 'estree';
|
||||
import { createGenerator, type DocEntry, type Generator } from '@/lib/base';
|
||||
import { renderMarkdownToHast, renderTypeToHast } from '@/markdown';
|
||||
@@ -17,58 +17,104 @@ import {
|
||||
} from '@/lib/type-table';
|
||||
import { toEstree } from 'hast-util-to-estree';
|
||||
import { dirname } from 'node:path';
|
||||
import { type ParameterTag, parseTags } from '@/lib/parse-tags';
|
||||
|
||||
async function mapProperty(
|
||||
entry: DocEntry,
|
||||
function objectBuilder() {
|
||||
const out: ObjectExpression = {
|
||||
type: 'ObjectExpression',
|
||||
properties: [],
|
||||
};
|
||||
|
||||
return {
|
||||
addExpressionNode(key: string, expression: Expression) {
|
||||
out.properties.push({
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: key,
|
||||
},
|
||||
kind: 'init',
|
||||
value: expression,
|
||||
});
|
||||
},
|
||||
addJsxProperty(key: string, hast: Nodes) {
|
||||
const estree = toEstree(hast, {
|
||||
elementAttributeNameCase: 'react',
|
||||
}).body[0] as ExpressionStatement;
|
||||
|
||||
this.addExpressionNode(key, estree.expression);
|
||||
},
|
||||
build() {
|
||||
return out;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async function buildTypeProp(
|
||||
entries: DocEntry[],
|
||||
{
|
||||
renderMarkdown = renderMarkdownToHast,
|
||||
renderType = renderTypeToHast,
|
||||
}: RemarkAutoTypeTableOptions,
|
||||
): Promise<Property> {
|
||||
const value = valueToEstree({
|
||||
required: entry.required,
|
||||
}) as ObjectExpression;
|
||||
): Promise<ObjectExpression> {
|
||||
async function onItem(entry: DocEntry) {
|
||||
const node = objectBuilder();
|
||||
node.addJsxProperty('type', await renderType(entry.simplifiedType));
|
||||
node.addJsxProperty('typeDescription', await renderType(entry.type));
|
||||
node.addExpressionNode('required', valueToEstree(entry.required));
|
||||
|
||||
function addJsxProperty(key: string, hast: Nodes) {
|
||||
const estree = toEstree(hast, {
|
||||
elementAttributeNameCase: 'react',
|
||||
}).body[0] as ExpressionStatement;
|
||||
const tags = parseTags(entry.tags);
|
||||
if (tags.default)
|
||||
node.addJsxProperty('default', await renderType(tags.default));
|
||||
|
||||
value.properties.push({
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Identifier',
|
||||
name: key,
|
||||
},
|
||||
kind: 'init',
|
||||
value: estree.expression,
|
||||
});
|
||||
if (tags.returns)
|
||||
node.addJsxProperty('returns', await renderMarkdown(tags.returns));
|
||||
|
||||
if (tags.params) {
|
||||
node.addExpressionNode('parameters', {
|
||||
type: 'ArrayExpression',
|
||||
elements: await Promise.all(tags.params.map(onParam)),
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.description) {
|
||||
node.addJsxProperty(
|
||||
'description',
|
||||
await renderMarkdown(entry.description),
|
||||
);
|
||||
}
|
||||
|
||||
return node.build();
|
||||
}
|
||||
|
||||
addJsxProperty('type', await renderType(entry.simplifiedType));
|
||||
addJsxProperty('typeDescription', await renderType(entry.type));
|
||||
const defaultValue = entry.tags.default ?? entry.tags.defaultValue;
|
||||
if (defaultValue) addJsxProperty('default', await renderType(defaultValue));
|
||||
async function onParam(param: ParameterTag) {
|
||||
const node = objectBuilder();
|
||||
node.addExpressionNode('name', valueToEstree(param.name));
|
||||
if (param.description)
|
||||
node.addJsxProperty(
|
||||
'description',
|
||||
await renderMarkdown(param.description),
|
||||
);
|
||||
|
||||
if (entry.description) {
|
||||
addJsxProperty('description', await renderMarkdown(entry.description));
|
||||
return node.build();
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'Property',
|
||||
method: false,
|
||||
shorthand: false,
|
||||
computed: false,
|
||||
key: {
|
||||
type: 'Literal',
|
||||
value: entry.name,
|
||||
},
|
||||
kind: 'init',
|
||||
value,
|
||||
};
|
||||
const prop = objectBuilder();
|
||||
const output = await Promise.all(
|
||||
entries.map(async (entry) => ({
|
||||
name: entry.name,
|
||||
node: await onItem(entry),
|
||||
})),
|
||||
);
|
||||
|
||||
for (const node of output) {
|
||||
prop.addExpressionNode(node.name, node.node);
|
||||
}
|
||||
|
||||
return prop.build();
|
||||
}
|
||||
|
||||
export interface RemarkAutoTypeTableOptions {
|
||||
@@ -141,10 +187,6 @@ export function remarkAutoTypeTable(
|
||||
);
|
||||
|
||||
const rendered = output.map(async (doc) => {
|
||||
const properties = await Promise.all(
|
||||
doc.entries.map((entry) => mapProperty(entry, config)),
|
||||
);
|
||||
|
||||
return {
|
||||
type: 'mdxJsxFlowElement',
|
||||
name: outputName,
|
||||
@@ -162,10 +204,7 @@ export function remarkAutoTypeTable(
|
||||
body: [
|
||||
{
|
||||
type: 'ExpressionStatement',
|
||||
expression: {
|
||||
type: 'ObjectExpression',
|
||||
properties,
|
||||
},
|
||||
expression: await buildTypeProp(doc.entries, config),
|
||||
},
|
||||
],
|
||||
} satisfies Program,
|
||||
@@ -1,4 +1,8 @@
|
||||
import { type TypeNode, TypeTable } from 'fumadocs-ui/components/type-table';
|
||||
import {
|
||||
type ParameterNode,
|
||||
type TypeNode,
|
||||
TypeTable,
|
||||
} from 'fumadocs-ui/components/type-table';
|
||||
import { type Jsx, toJsxRuntime } from 'hast-util-to-jsx-runtime';
|
||||
import * as runtime from 'react/jsx-runtime';
|
||||
import defaultMdxComponents from 'fumadocs-ui/mdx';
|
||||
@@ -11,6 +15,7 @@ import {
|
||||
} from '@/lib/type-table';
|
||||
import { type Generator } from '@/lib/base';
|
||||
import type { Nodes } from 'hast';
|
||||
import { parseTags } from '@/lib/parse-tags';
|
||||
|
||||
export type AutoTypeTableProps = BaseTypeTableProps;
|
||||
|
||||
@@ -31,7 +36,17 @@ export async function AutoTypeTable({
|
||||
|
||||
return output.map(async (item) => {
|
||||
const entries = item.entries.map(async (entry) => {
|
||||
const defaultValue = entry.tags.default ?? entry.tags.defaultValue;
|
||||
const tags = parseTags(entry.tags);
|
||||
const paramNodes: ParameterNode[] = [];
|
||||
|
||||
for (const param of tags.params ?? []) {
|
||||
paramNodes.push({
|
||||
name: param.name,
|
||||
description: param.description
|
||||
? await renderMarkdown(param.description)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
return [
|
||||
entry.name,
|
||||
@@ -39,9 +54,13 @@ export async function AutoTypeTable({
|
||||
type: await renderType(entry.simplifiedType),
|
||||
typeDescription: await renderType(entry.type),
|
||||
description: await renderMarkdown(entry.description),
|
||||
default: defaultValue ? await renderType(defaultValue) : undefined,
|
||||
default: tags.default ? await renderType(tags.default) : undefined,
|
||||
parameters: paramNodes,
|
||||
required: entry.required,
|
||||
deprecated: entry.deprecated,
|
||||
returns: tags.returns
|
||||
? await renderMarkdown(tags.returns)
|
||||
: undefined,
|
||||
} as TypeNode,
|
||||
];
|
||||
});
|
||||
|
||||
+44
-7
@@ -11,9 +11,7 @@ function _createMdxContent(props) {
|
||||
children: _jsx(_Fragment, {
|
||||
children: _jsx(TypeTable, {
|
||||
type: {
|
||||
"name": {
|
||||
"default": "Henry",
|
||||
"required": true,
|
||||
name: {
|
||||
type: _jsx(_components.span, {
|
||||
className: "shiki",
|
||||
children: _jsx(_components.code, {
|
||||
@@ -38,15 +36,26 @@ function _createMdxContent(props) {
|
||||
})
|
||||
})
|
||||
}),
|
||||
required: true,
|
||||
default: _jsx(_components.span, {
|
||||
className: "shiki",
|
||||
children: _jsx(_components.code, {
|
||||
children: _jsx(_components.span, {
|
||||
style: {
|
||||
"--shiki-light": "#24292E",
|
||||
"--shiki-dark": "#E1E4E8"
|
||||
},
|
||||
children: "Henry"
|
||||
})
|
||||
})
|
||||
}),
|
||||
description: _jsx(_Fragment, {
|
||||
children: _jsx(_components.p, {
|
||||
children: "The name of player"
|
||||
})
|
||||
})
|
||||
},
|
||||
"age": {
|
||||
"default": undefined,
|
||||
"required": true,
|
||||
age: {
|
||||
type: _jsx(_components.span, {
|
||||
className: "shiki",
|
||||
children: _jsx(_components.code, {
|
||||
@@ -70,7 +79,35 @@ function _createMdxContent(props) {
|
||||
children: "timestamp"
|
||||
})
|
||||
})
|
||||
})
|
||||
}),
|
||||
required: true
|
||||
},
|
||||
privateValue: {
|
||||
type: _jsx(_components.span, {
|
||||
className: "shiki",
|
||||
children: _jsx(_components.code, {
|
||||
children: _jsx(_components.span, {
|
||||
style: {
|
||||
"--shiki-light": "#24292E",
|
||||
"--shiki-dark": "#E1E4E8"
|
||||
},
|
||||
children: "string"
|
||||
})
|
||||
})
|
||||
}),
|
||||
typeDescription: _jsx(_components.span, {
|
||||
className: "shiki",
|
||||
children: _jsx(_components.code, {
|
||||
children: _jsx(_components.span, {
|
||||
style: {
|
||||
"--shiki-light": "#24292E",
|
||||
"--shiki-dark": "#E1E4E8"
|
||||
},
|
||||
children: "string"
|
||||
})
|
||||
})
|
||||
}),
|
||||
required: true
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
+53
-21
@@ -6,7 +6,7 @@
|
||||
{
|
||||
"name": "name",
|
||||
"description": "",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "string",
|
||||
"simplifiedType": "string",
|
||||
"required": true,
|
||||
@@ -15,11 +15,14 @@
|
||||
{
|
||||
"name": "age",
|
||||
"description": "",
|
||||
"tags": {
|
||||
"defaultValue": "4"
|
||||
},
|
||||
"type": "number",
|
||||
"simplifiedType": "number | undefined",
|
||||
"tags": [
|
||||
{
|
||||
"name": "defaultValue",
|
||||
"text": "4"
|
||||
}
|
||||
],
|
||||
"type": "number | undefined",
|
||||
"simplifiedType": "number",
|
||||
"required": false,
|
||||
"deprecated": false
|
||||
}
|
||||
@@ -32,7 +35,7 @@
|
||||
{
|
||||
"name": "name",
|
||||
"description": "",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "string",
|
||||
"simplifiedType": "string",
|
||||
"required": true,
|
||||
@@ -41,18 +44,21 @@
|
||||
{
|
||||
"name": "age",
|
||||
"description": "",
|
||||
"tags": {
|
||||
"defaultValue": "4"
|
||||
},
|
||||
"type": "number",
|
||||
"simplifiedType": "number | undefined",
|
||||
"tags": [
|
||||
{
|
||||
"name": "defaultValue",
|
||||
"text": "4"
|
||||
}
|
||||
],
|
||||
"type": "number | undefined",
|
||||
"simplifiedType": "number",
|
||||
"required": false,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "generic",
|
||||
"description": "",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "GenericType<string, string, string>",
|
||||
"simplifiedType": "object",
|
||||
"required": true,
|
||||
@@ -67,10 +73,16 @@
|
||||
{
|
||||
"name": "name",
|
||||
"description": "The name of player",
|
||||
"tags": {
|
||||
"see": "https://fumadocs.vercel.app",
|
||||
"defaultValue": "Henry"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "see",
|
||||
"text": "https://fumadocs.vercel.app"
|
||||
},
|
||||
{
|
||||
"name": "defaultValue",
|
||||
"text": "Henry"
|
||||
}
|
||||
],
|
||||
"type": "string",
|
||||
"simplifiedType": "string",
|
||||
"required": true,
|
||||
@@ -79,14 +91,34 @@
|
||||
{
|
||||
"name": "age",
|
||||
"description": "",
|
||||
"tags": {
|
||||
"example": "```js\nconsole.log(\"Hello World\")\n```",
|
||||
"remarks": "`timestamp`\nReturned by API"
|
||||
},
|
||||
"tags": [
|
||||
{
|
||||
"name": "example",
|
||||
"text": "```js\nconsole.log(\"Hello World\")\n```"
|
||||
},
|
||||
{
|
||||
"name": "remarks",
|
||||
"text": "`timestamp`\nReturned by API"
|
||||
}
|
||||
],
|
||||
"type": "timestamp",
|
||||
"simplifiedType": "number",
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "privateValue",
|
||||
"description": "",
|
||||
"tags": [
|
||||
{
|
||||
"name": "internal",
|
||||
"text": ""
|
||||
}
|
||||
],
|
||||
"type": "string",
|
||||
"simplifiedType": "string",
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ test('class members', () => {
|
||||
"name": "test",
|
||||
"required": true,
|
||||
"simplifiedType": "string",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
@@ -52,7 +52,7 @@ test('class members', () => {
|
||||
"name": "age",
|
||||
"required": true,
|
||||
"simplifiedType": "number",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "number",
|
||||
},
|
||||
],
|
||||
@@ -87,7 +87,7 @@ test('interface members', () => {
|
||||
"name": "#name",
|
||||
"required": true,
|
||||
"simplifiedType": "string",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "string",
|
||||
},
|
||||
{
|
||||
@@ -96,7 +96,7 @@ test('interface members', () => {
|
||||
"name": "age",
|
||||
"required": true,
|
||||
"simplifiedType": "number",
|
||||
"tags": {},
|
||||
"tags": [],
|
||||
"type": "number",
|
||||
},
|
||||
],
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
--color-fd-popover-foreground: hsl(0, 0%, 15.1%);
|
||||
--color-fd-card: hsl(0, 0%, 94.7%);
|
||||
--color-fd-card-foreground: hsl(0, 0%, 3.9%);
|
||||
--color-fd-border: hsla(0, 0%, 62%, 20%);
|
||||
--color-fd-border: hsla(0, 0%, 80%, 50%);
|
||||
--color-fd-primary: hsl(0, 0%, 9%);
|
||||
--color-fd-primary-foreground: hsl(0, 0%, 98%);
|
||||
--color-fd-secondary: hsl(0, 0%, 93.1%);
|
||||
|
||||
@@ -100,7 +100,7 @@ export function CodeBlock({
|
||||
inTab ? 'bg-fd-secondary' : 'my-4 bg-fd-card',
|
||||
keepBackground && 'bg-(--shiki-light-bg) dark:bg-(--shiki-dark-bg)',
|
||||
|
||||
'shiki relative border shadow-md rounded-xl outline-none not-prose overflow-hidden text-sm',
|
||||
'shiki relative border shadow-sm rounded-xl outline-none not-prose overflow-hidden text-sm',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
@@ -201,7 +201,7 @@ export function CodeBlockTabs({ ref, ...props }: ComponentProps<typeof Tabs>) {
|
||||
{...props}
|
||||
className={cn(
|
||||
'bg-fd-card rounded-xl border',
|
||||
nested ? 'shadow-sm' : 'my-4',
|
||||
!nested && 'my-4',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -68,7 +68,7 @@ interface InternalContext {
|
||||
}
|
||||
|
||||
const itemVariants = cva(
|
||||
'relative flex flex-row items-center gap-2 rounded-xl p-2 ps-(--sidebar-item-offset) text-start text-fd-muted-foreground [overflow-wrap:anywhere] [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
'relative flex flex-row items-center gap-2 rounded-lg p-2 ps-(--sidebar-item-offset) text-start text-fd-muted-foreground [overflow-wrap:anywhere] [&_svg]:size-4 [&_svg]:shrink-0',
|
||||
{
|
||||
variants: {
|
||||
active: {
|
||||
@@ -125,7 +125,7 @@ export function SidebarContent(props: ComponentProps<'aside'>) {
|
||||
{...props}
|
||||
data-collapsed={collapsed}
|
||||
className={cn(
|
||||
'fixed left-0 rtl:left-auto rtl:right-(--removed-body-scroll-bar-size,0) flex flex-col items-end top-(--fd-sidebar-top) bottom-(--fd-sidebar-margin) z-20 bg-fd-card text-sm border-e transition-[top,opacity,translate,width] max-md:hidden *:w-(--fd-sidebar-width)',
|
||||
'fixed left-0 rtl:left-auto rtl:right-(--removed-body-scroll-bar-size,0) flex flex-col items-end top-(--fd-sidebar-top) bottom-(--fd-sidebar-margin) z-20 bg-fd-card text-sm border-e transition-[top,opacity,translate,width] duration-200 max-md:hidden *:w-(--fd-sidebar-width)',
|
||||
collapsed && [
|
||||
'rounded-xl border translate-x-(--fd-sidebar-offset) rtl:-translate-x-(--fd-sidebar-offset)',
|
||||
hover ? 'z-50 shadow-lg' : 'opacity-0',
|
||||
|
||||
@@ -4,13 +4,18 @@ import { ChevronDown } from 'lucide-react';
|
||||
import Link from 'fumadocs-core/link';
|
||||
import { cva } from 'class-variance-authority';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { type ReactNode } from 'react';
|
||||
import { type ReactNode, useState } from 'react';
|
||||
import {
|
||||
Collapsible,
|
||||
CollapsibleContent,
|
||||
CollapsibleTrigger,
|
||||
} from '@/components/ui/collapsible';
|
||||
|
||||
export interface ParameterNode {
|
||||
name: string;
|
||||
description: ReactNode;
|
||||
}
|
||||
|
||||
export interface TypeNode {
|
||||
/**
|
||||
* Additional description of the field
|
||||
@@ -36,6 +41,10 @@ export interface TypeNode {
|
||||
|
||||
required?: boolean;
|
||||
deprecated?: boolean;
|
||||
|
||||
parameters?: ParameterNode[];
|
||||
|
||||
returns?: ReactNode;
|
||||
}
|
||||
|
||||
const keyVariants = cva('text-fd-primary', {
|
||||
@@ -46,10 +55,12 @@ const keyVariants = cva('text-fd-primary', {
|
||||
},
|
||||
});
|
||||
|
||||
const fieldVariants = cva('text-fd-muted-foreground not-prose');
|
||||
|
||||
export function TypeTable({ type }: { type: Record<string, TypeNode> }) {
|
||||
return (
|
||||
<div className="@container flex flex-col p-1 bg-fd-card text-fd-card-foreground rounded-2xl border my-6 text-sm overflow-hidden">
|
||||
<div className="flex flex-row font-medium items-center px-4 py-1 not-prose text-fd-muted-foreground">
|
||||
<div className="flex font-medium items-center px-3 py-1 not-prose text-fd-muted-foreground">
|
||||
<p className="w-[25%]">Prop</p>
|
||||
<p className="@max-xl:hidden">Type</p>
|
||||
</div>
|
||||
@@ -60,58 +71,99 @@ export function TypeTable({ type }: { type: Record<string, TypeNode> }) {
|
||||
);
|
||||
}
|
||||
|
||||
function Item({ name, item }: { name: string; item: TypeNode }) {
|
||||
function Item({
|
||||
name,
|
||||
item: {
|
||||
parameters = [],
|
||||
description,
|
||||
required = false,
|
||||
deprecated,
|
||||
typeDescription,
|
||||
default: defaultValue,
|
||||
type,
|
||||
typeDescriptionLink,
|
||||
returns,
|
||||
},
|
||||
}: {
|
||||
name: string;
|
||||
item: TypeNode;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<Collapsible className="rounded-xl border shadow-sm overflow-hidden transition-all data-[state=open]:bg-fd-muted data-[state=closed]:border-transparent not-last:data-[state=open]:mb-4">
|
||||
<Collapsible
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
className={cn(
|
||||
'rounded-xl border overflow-hidden transition-all',
|
||||
open
|
||||
? 'shadow-sm bg-fd-background not-last:mb-2'
|
||||
: 'border-transparent',
|
||||
)}
|
||||
>
|
||||
<CollapsibleTrigger className="relative flex flex-row items-center w-full group text-start px-3 py-2 not-prose hover:bg-fd-accent">
|
||||
<span className="pe-2 min-w-fit font-medium w-[25%]">
|
||||
<code
|
||||
className={cn(
|
||||
keyVariants({
|
||||
deprecated: item.deprecated,
|
||||
deprecated,
|
||||
}),
|
||||
)}
|
||||
>
|
||||
{name}
|
||||
{!item.required && '?'}
|
||||
{!required && '?'}
|
||||
</code>
|
||||
</span>
|
||||
{item.typeDescriptionLink ? (
|
||||
<Link
|
||||
href={item.typeDescriptionLink}
|
||||
className="underline @max-xl:hidden"
|
||||
>
|
||||
{item.type}
|
||||
{typeDescriptionLink ? (
|
||||
<Link href={typeDescriptionLink} className="underline @max-xl:hidden">
|
||||
{type}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="@max-xl:hidden">{item.type}</span>
|
||||
<span className="@max-xl:hidden">{type}</span>
|
||||
)}
|
||||
<ChevronDown className="absolute end-2 size-4 text-fd-muted-foreground transition-transform group-data-[state=open]:rotate-180" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<div className="grid grid-cols-1 gap-y-4 text-sm p-3 overflow-auto fd-scroll-container prose-no-margin @xl:grid-cols-[1fr_3fr]">
|
||||
{item.description && (
|
||||
<div className="grid grid-cols-[1fr_3fr] gap-y-4 text-sm p-3 overflow-auto fd-scroll-container border-t">
|
||||
<div className="text-sm prose col-span-full prose-no-margin empty:hidden">
|
||||
{description}
|
||||
</div>
|
||||
{typeDescription && (
|
||||
<>
|
||||
<p className="text-fd-muted-foreground font-medium not-prose">
|
||||
Description
|
||||
</p>
|
||||
<div className="text-sm my-auto prose">{item.description}</div>
|
||||
<p className={cn(fieldVariants())}>Type</p>
|
||||
<p className="my-auto not-prose">{typeDescription}</p>
|
||||
</>
|
||||
)}
|
||||
{item.typeDescription && (
|
||||
{defaultValue && (
|
||||
<>
|
||||
<p className="text-fd-muted-foreground font-medium not-prose">
|
||||
Type
|
||||
</p>
|
||||
<p className="my-auto not-prose">{item.typeDescription}</p>
|
||||
<p className={cn(fieldVariants())}>Default</p>
|
||||
<p className="my-auto not-prose">{defaultValue}</p>
|
||||
</>
|
||||
)}
|
||||
{item.default && (
|
||||
{parameters.length > 0 && (
|
||||
<>
|
||||
<p className="text-fd-muted-foreground font-medium not-prose">
|
||||
Default
|
||||
</p>
|
||||
<p className="my-auto not-prose">{item.default}</p>
|
||||
<p className={cn(fieldVariants())}>Parameters</p>
|
||||
<div className="flex flex-col gap-2">
|
||||
{parameters.map((param) => (
|
||||
<div
|
||||
key={param.name}
|
||||
className="inline-flex items-center gap-1"
|
||||
>
|
||||
<p className="font-medium not-prose">{param.name} -</p>
|
||||
<div className="text-sm prose prose-no-margin">
|
||||
{param.description}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{returns && (
|
||||
<>
|
||||
<p className={cn(fieldVariants())}>Returns</p>
|
||||
<div className="my-auto text-sm prose prose-no-margin">
|
||||
{returns}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user