Files
twenty/packages/twenty-docs/scripts/ui/render-ui-reference.ts
Raphaël Bosi a44c8bdf92 Add Twenty UI documentation and generated API references (#25791)
Add a UI Library documentation section so developers can get started
with Twenty UI, customize its theme, and use its components.

The section has guides for setup, theming, design tokens, dark mode,
server rendering, and accessibility, plus a page per component (17 so
far) with usage examples and a generated props reference.

How the references are generated and kept in sync:
- `nx generate:ui twenty-docs` extracts prop types and JSDoc from the
twenty-ui components with react-docgen-typescript and the design tokens
from the token pipeline, writes them to `packages/twenty-ui/generated`,
and renders MDX snippets under
`packages/twenty-docs/snippets/ui/generated`.
- `nx check:ui twenty-docs` runs in CI Docs and fails when the data or
snippets are stale, when a page and the generated snippets disagree, or
when a guide example does not compile against the public entry points
and peer dependencies.
- Twenty UI's own props now carry JSDoc descriptions, and generation
fails on an undocumented prop.

Old `/twenty-ui/*` URLs redirect to the new pages. The pages use the
existing Crowdin translation workflow; the generated snippets are shared
across locales.
2026-09-14 08:03:21 +00:00

117 lines
3.1 KiB
TypeScript

import {
type ComponentDocumentation,
type TokenDocumentation,
} from '../../../twenty-ui/docs/types';
const CODE_SEGMENT_PATTERN = /(`{3,})[\s\S]*?\1|`[^`\n]+`/g;
const escapeAttributeValue = (value: string): string =>
value
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
const escapeProseText = (text: string): string =>
text
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/\{/g, '&#123;')
.replace(/\}/g, '&#125;');
const escapeMarkdown = (markdown: string): string => {
let escaped = '';
let proseStart = 0;
for (const codeSegment of markdown.matchAll(CODE_SEGMENT_PATTERN)) {
escaped +=
escapeProseText(markdown.slice(proseStart, codeSegment.index)) +
codeSegment[0];
proseStart = codeSegment.index + codeSegment[0].length;
}
return escaped + escapeProseText(markdown.slice(proseStart));
};
const indent = (text: string): string =>
text
.split('\n')
.map((line) => (line.length > 0 ? ` ${line}` : line))
.join('\n');
const renderProps = ({
props,
partName,
}: {
props: ComponentDocumentation['props'];
partName?: string;
}): string =>
props
.map((prop) => {
const attributes = [
`body="${escapeAttributeValue(partName ? `${partName}.${prop.name}` : prop.name)}"`,
`type="${escapeAttributeValue(prop.type)}"`,
...(prop.required ? ['required'] : []),
...(prop.defaultValue === null
? []
: [`default="${escapeAttributeValue(prop.defaultValue)}"`]),
];
return `<ParamField ${attributes.join(' ')}>\n${indent(escapeMarkdown(prop.description))}\n</ParamField>`;
})
.join('\n\n') + '\n';
export const renderComponentReference = (
component: ComponentDocumentation,
): string =>
component.parts
? component.parts
.map(
(part) =>
`### ${component.name}.${part.name}\n\n${renderProps({ props: part.props, partName: part.name })}`,
)
.join('\n')
: renderProps({ props: component.props });
const escapeTableCell = (value: string): string =>
value
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/\{/g, '&#123;')
.replace(/\}/g, '&#125;')
.replace(/\|/g, '&#124;')
.replace(/\r?\n/g, ' ');
export const renderTokenReference = (tokens: TokenDocumentation[]): string => {
const groups = new Map<string, TokenDocumentation[]>();
for (const token of tokens) {
const group = token.path.split('.')[0];
const entries = groups.get(group);
if (entries) {
entries.push(token);
} else {
groups.set(group, [token]);
}
}
return (
[...groups.entries()]
.map(([group, entries]) =>
[
`## ${group}`,
'',
'| Token path | CSS variable | Light | Dark | Numeric |',
'| --- | --- | --- | --- | --- |',
...entries.map(
(token) =>
`| ${[token.path, token.cssVariable, token.light, token.dark, token.isNumber ? 'Yes' : ''].map(escapeTableCell).join(' | ')} |`,
),
].join('\n'),
)
.join('\n\n') + '\n'
);
};