mirror of
https://github.com/twentyhq/twenty.git
synced 2026-09-16 16:06:17 -04:00
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.
117 lines
3.1 KiB
TypeScript
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, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"');
|
|
|
|
const escapeProseText = (text: string): string =>
|
|
text
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/\{/g, '{')
|
|
.replace(/\}/g, '}');
|
|
|
|
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, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/\{/g, '{')
|
|
.replace(/\}/g, '}')
|
|
.replace(/\|/g, '|')
|
|
.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'
|
|
);
|
|
};
|