[FRONT COMPONENTS] Twenty UI elements generation (#17866)

## PR description

This PR:
- Creates a TypeScript-based extractor that discovers all exported
twenty-ui components by scanning barrel files, extracting
props/slots/events via ts-morph type analysis, and generating the remote
DOM bindings automatically.

- Adds a new ESLint rule which enforces all *Props types in twenty-ui
components to be exported, which is required for the extractor to
discover component prop types. Existing twenty-ui components are updated
to comply with this rule.

- Extends the remote DOM generation to support slots, per-component
events, forwardRef wrappers, and richer property types (array, object,
function)

## Edge cases to fix in another PR

- Icons cannot be rendered inside buttons
- IconButtons throw an error when mounted
- MenuItems are not displayed correctly
- MenuItemNavigate throws on click

## Video Demo


https://github.com/user-attachments/assets/c2ed67cf-6a15-4896-9fec-e83fac0e862b
This commit is contained in:
Raphaël Bosi
2026-02-12 12:48:16 +01:00
committed by GitHub
parent c48ccbca99
commit cb7d0b83a8
133 changed files with 10556 additions and 984 deletions
+5
View File
@@ -6,6 +6,10 @@ import {
rule as effectComponents,
RULE_NAME as effectComponentsName,
} from './rules/effect-components';
import {
rule as exportComponentProps,
RULE_NAME as exportComponentPropsName,
} from './rules/export-component-props';
import {
rule as explicitBooleanPredicatesInIf,
RULE_NAME as explicitBooleanPredicatesInIfName,
@@ -95,6 +99,7 @@ module.exports = {
rules: {
[componentPropsNamingName]: componentPropsNaming,
[effectComponentsName]: effectComponents,
[exportComponentPropsName]: exportComponentProps,
[matchingStateVariableName]: matchingStateVariable,
[noHardcodedColorsName]: noHardcodedColors,
[noStateUserefName]: noStateUseref,
@@ -0,0 +1,119 @@
/**
* @jest-environment node
*/
import { RuleTester } from 'eslint';
import { rule, RULE_NAME } from './export-component-props';
const typescriptParser = require('@typescript-eslint/parser');
const ruleTester = new RuleTester({
languageOptions: {
parser: typescriptParser,
parserOptions: {
ecmaFeatures: {
jsx: true,
},
},
},
});
ruleTester.run(RULE_NAME, rule as any, {
valid: [
{
name: 'Props type is already exported',
code: `
export type MyComponentProps = { label: string };
`,
},
{
name: 'Props interface is already exported',
code: `
export interface MyComponentProps { label: string }
`,
},
{
name: 'Type that doesn\'t end with Props is ignored',
code: `
type MyComponentOptions = { flag: boolean };
`,
},
{
name: 'Interface that doesn\'t end with Props is ignored',
code: `
interface MyComponentConfig { flag: boolean }
`,
},
{
name: 'Props re-exported via export { FooProps } is treated as exported',
code: `
type MyComponentProps = { label: string };
export { MyComponentProps };
`,
},
{
name: 'Non-Props type is not required to be exported',
code: `
type InternalState = { count: number };
`,
},
],
invalid: [
{
name: 'Unexported Props type alias',
code: `
type MyComponentProps = { label: string };
`,
errors: [{ messageId: 'mustExportProps' }],
output: `
export type MyComponentProps = { label: string };
`,
},
{
name: 'Unexported Props interface',
code: `
interface MyComponentProps { label: string }
`,
errors: [{ messageId: 'mustExportProps' }],
output: `
export interface MyComponentProps { label: string }
`,
},
{
name: 'Unexported Props type used in a component',
code: `
type MyComponentProps = { label: string };
export const MyComponent = ({ label }: MyComponentProps) => <div>{label}</div>;
`,
errors: [{ messageId: 'mustExportProps' }],
output: `
export type MyComponentProps = { label: string };
export const MyComponent = ({ label }: MyComponentProps) => <div>{label}</div>;
`,
},
{
name: 'Unexported Props type with non-exported component',
code: `
type MyComponentProps = { label: string };
const MyComponent = ({ label }: MyComponentProps) => <div>{label}</div>;
`,
errors: [{ messageId: 'mustExportProps' }],
output: `
export type MyComponentProps = { label: string };
const MyComponent = ({ label }: MyComponentProps) => <div>{label}</div>;
`,
},
{
name: 'Unexported Props type defined after the component',
code: `
export const MyComponent = ({ label }: MyComponentProps) => <div>{label}</div>;
type MyComponentProps = { label: string };
`,
errors: [{ messageId: 'mustExportProps' }],
output: `
export const MyComponent = ({ label }: MyComponentProps) => <div>{label}</div>;
export type MyComponentProps = { label: string };
`,
},
],
});
@@ -0,0 +1,99 @@
import { ESLintUtils, TSESTree } from '@typescript-eslint/utils';
import { isIdentifier } from '@typescript-eslint/utils/ast-utils';
export const RULE_NAME = 'export-component-props';
// NOTE: The rule will be available in ESLint configs as "@nx/workspace-export-component-props"
export const rule = ESLintUtils.RuleCreator(() => __filename)({
name: RULE_NAME,
meta: {
type: 'problem',
docs: {
description:
'Ensure types/interfaces ending with "Props" are exported',
},
fixable: 'code',
schema: [],
messages: {
mustExportProps:
"Props type '{{ typeName }}' must be exported.",
},
},
defaultOptions: [],
create: (context) => {
const reExportedNames = new Set<string>();
const unexportedPropsNodes = new Map<
string,
| TSESTree.TSTypeAliasDeclaration
| TSESTree.TSInterfaceDeclaration
>();
const collectUnexportedProps = (
node:
| TSESTree.TSTypeAliasDeclaration
| TSESTree.TSInterfaceDeclaration,
) => {
const typeName = node.id.name;
if (!typeName.endsWith('Props')) {
return;
}
const isExported =
node.parent?.type ===
TSESTree.AST_NODE_TYPES.ExportNamedDeclaration;
if (!isExported) {
unexportedPropsNodes.set(typeName, node);
}
};
return {
TSTypeAliasDeclaration: collectUnexportedProps,
TSInterfaceDeclaration: collectUnexportedProps,
ExportNamedDeclaration: (
node: TSESTree.ExportNamedDeclaration,
) => {
for (const specifier of node.specifiers) {
if (
specifier.type ===
TSESTree.AST_NODE_TYPES.ExportSpecifier &&
isIdentifier(specifier.local)
) {
const name = specifier.local.name;
if (name.endsWith('Props')) {
reExportedNames.add(name);
}
}
}
},
'Program:exit': () => {
for (const [typeName, node] of unexportedPropsNodes) {
if (reExportedNames.has(typeName)) {
continue;
}
context.report({
node: node.id,
messageId: 'mustExportProps',
data: { typeName },
fix: (fixer) => {
const sourceCode = context.sourceCode;
const firstToken = sourceCode.getFirstToken(node);
const target = firstToken ?? node;
if (firstToken?.value === 'export') {
return null;
}
return fixer.insertTextBefore(target, 'export ');
},
});
}
},
};
},
});
+14 -5
View File
@@ -6,7 +6,7 @@
"tags": ["scope:sdk", "scope:shared"],
"targets": {
"build": {
"dependsOn": ["^build"],
"dependsOn": ["^build", "generate-remote-dom-elements"],
"outputs": ["{projectRoot}/dist"]
},
"dev": {
@@ -85,25 +85,34 @@
}
]
},
"generateRemoteDomElements": {
"generate-remote-dom-elements": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["^build"],
"inputs": [
"{projectRoot}/scripts/remote-dom/**/*",
"{projectRoot}/src/front-component-constants/**/*"
"{projectRoot}/src/sdk/front-component-common/**/*",
"{workspaceRoot}/packages/twenty-ui/src/**/index.ts",
"{workspaceRoot}/packages/twenty-ui/src/**/*.tsx"
],
"outputs": [
"{projectRoot}/src/front-component/host/generated/*",
"{projectRoot}/src/front-component/remote/generated/*"
],
"options": {
"command": "tsx {projectRoot}/scripts/remote-dom/generateRemoteDomElements.ts"
"cwd": "packages/twenty-sdk",
"command": "tsx -r tsconfig-paths/register scripts/remote-dom/generate-remote-dom-elements.ts"
},
"configurations": {
"verbose": {
"command": "tsx -r tsconfig-paths/register scripts/remote-dom/generate-remote-dom-elements.ts --verbose"
}
}
},
"storybook:prebuild": {
"executor": "nx:run-commands",
"cache": true,
"dependsOn": ["generateRemoteDomElements"],
"dependsOn": ["generate-remote-dom-elements"],
"inputs": [
"{projectRoot}/src/front-component/__stories__/mocks/**/*",
"{projectRoot}/src/front-component/__stories__/utils/**/*",
@@ -1,10 +1,10 @@
import * as prettier from '@prettier/sync';
import * as fs from 'fs';
import * as path from 'path';
import { fileURLToPath } from 'url';
import { IndentationText, Project, QuoteKind } from 'ts-morph';
import { ALLOWED_HTML_ELEMENTS } from '../../src/sdk/front-component-common/AllowedHtmlElements';
import { ALLOWED_UI_COMPONENTS } from '../../src/sdk/front-component-common/AllowedUiComponents';
import { COMMON_HTML_EVENTS } from '../../src/sdk/front-component-common/CommonHtmlEvents';
import { EVENT_TO_REACT } from '../../src/sdk/front-component-common/EventToReact';
import { HTML_COMMON_PROPERTIES } from '../../src/sdk/front-component-common/HtmlCommonProperties';
@@ -17,10 +17,27 @@ import {
generateRemoteElements,
HtmlElementConfigArrayZ,
OUTPUT_FILES,
UiComponentConfigArrayZ,
} from './generators';
import { extractAllComponentsFromTwentyUi } from './twenty-ui-extractor';
import {
logCount,
logDetail,
logEmpty,
logError,
logFileWritten,
logGroupLabel,
logSectionHeader,
logSeparator,
logSuccess,
logTitle,
setVerbose,
} from './utils/logger';
const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname);
const parseVerboseFlag = (): boolean => {
return process.argv.includes('--verbose');
};
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const PACKAGE_PATH = path.resolve(SCRIPT_DIR, '../..');
const FRONT_COMPONENT_PATH = path.join(PACKAGE_PATH, 'src/front-component');
const HOST_GENERATED_DIR = path.join(FRONT_COMPONENT_PATH, 'host/generated');
@@ -61,24 +78,21 @@ const getHtmlElementSchemas = (): ComponentSchema[] => {
};
const getUiComponentSchemas = (): ComponentSchema[] => {
const result = UiComponentConfigArrayZ.safeParse(ALLOWED_UI_COMPONENTS);
const discoveredComponents = extractAllComponentsFromTwentyUi();
if (!result.success) {
throw new Error(
`Invalid UI component configuration:\n${formatZodError(result.error)}`,
);
}
return result.data.map((component) => ({
return discoveredComponents.map((component) => ({
name: component.name,
tagName: component.name,
customElementName: component.tag,
properties: component.properties,
events: COMMON_HTML_EVENTS,
slots: component.slots,
events: component.events,
isHtmlElement: false,
htmlTag: undefined,
componentImport: component.componentImport,
componentPath: component.componentPath,
propsTypeName: component.propsTypeName,
supportsRefForwarding: component.supportsRefForwarding,
}));
};
@@ -106,7 +120,7 @@ const writeGeneratedFile = (
endOfLine: 'lf',
});
fs.writeFileSync(filePath, formattedContent, 'utf-8');
console.log(`✓ Generated ${filePath}`);
logFileWritten(filePath);
};
const ensureDirectoriesExist = (): void => {
@@ -119,7 +133,10 @@ const ensureDirectoriesExist = (): void => {
};
const main = (): void => {
console.log('📖 Generating remote DOM elements...\n');
const verbose = parseVerboseFlag();
setVerbose(verbose);
logTitle('Remote DOM Elements Generator');
let htmlElements: ComponentSchema[];
let uiComponents: ComponentSchema[];
@@ -128,23 +145,24 @@ const main = (): void => {
htmlElements = getHtmlElementSchemas();
uiComponents = getUiComponentSchemas();
} catch (error) {
console.error('Validation failed:', error);
logError('Validation failed:', error);
process.exit(1);
}
console.log(`HTML Elements: ${htmlElements.length} elements`);
console.log(
` Tags: ${htmlElements.map((element) => element.htmlTag).join(', ')}`,
);
console.log(
` Events: ${COMMON_HTML_EVENTS.length} common events per element`,
);
logSeparator();
logSectionHeader('Summary');
console.log(`\nUI Components: ${uiComponents.length} components`);
console.log(
` Tags: ${uiComponents.map((component) => component.customElementName).join(', ')}`,
logCount('HTML Elements', htmlElements.length, 'element', 'elements');
logDetail(
`Tags: ${htmlElements.map((element) => element.htmlTag).join(', ')}`,
);
logDetail(`Events: ${COMMON_HTML_EVENTS.length} common events per element`);
logEmpty();
logCount('UI Components', uiComponents.length, 'component', 'components');
logDetail(
`Tags: ${uiComponents.map((component) => component.customElementName).join(', ')}`,
);
console.log('');
const allComponents = [...htmlElements, ...uiComponents];
@@ -152,7 +170,11 @@ const main = (): void => {
const project = createProject();
console.log('Host files:');
logSeparator();
logSectionHeader('Writing Files');
logGroupLabel('Host');
const hostRegistry = generateHostRegistry(
project,
allComponents,
@@ -164,7 +186,9 @@ const main = (): void => {
hostRegistry.getFullText(),
);
console.log('\nRemote files:');
logEmpty();
logGroupLabel('Remote');
const remoteElements = generateRemoteElements(
project,
allComponents,
@@ -184,9 +208,11 @@ const main = (): void => {
remoteComponents.getFullText(),
);
console.log('\n✅ All generated files created');
console.log(` Host: ${HOST_GENERATED_DIR}`);
console.log(` Remote: ${REMOTE_GENERATED_DIR}`);
logSeparator();
logSuccess('Done!', 'All generated files created.');
logDetail(`Host: ${HOST_GENERATED_DIR}`);
logDetail(`Remote: ${REMOTE_GENERATED_DIR}`);
logEmpty();
};
main();
@@ -1,6 +1,7 @@
import type { Project, SourceFile } from 'ts-morph';
import { isDefined } from 'twenty-shared/utils';
import { EVENT_TO_REACT } from '@/sdk/front-component-common/EventToReact';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { CUSTOM_ELEMENT_NAMES } from './constants';
import { type ComponentSchema } from './schemas';
import { addFileHeader, addStatement } from './utils';
@@ -98,7 +99,7 @@ const wrapEventHandler = (handler: (detail: SerializedEventData) => void) => {
};
};
const filterProps = (props: Record<string, unknown>) => {
const filterHtmlProps = <T extends object>(props: T): T => {
const filtered: Record<string, unknown> = {};
for (const [key, value] of Object.entries(props)) {
if (INTERNAL_PROPS.has(key) || value === undefined) continue;
@@ -114,7 +115,23 @@ const filterProps = (props: Record<string, unknown>) => {
}
}
}
return filtered;
return filtered as T;
};
const filterUiProps = <T extends object>(props: T, eventPropNames?: Set<string>): T => {
const filtered: Record<string, unknown> = {};
for (const [key, value] of Object.entries(props)) {
if (INTERNAL_PROPS.has(key) || value === undefined) continue;
if (key === 'style') {
filtered.style = parseStyle(value as string | undefined);
} else if (eventPropNames?.has(key) && typeof value === 'function') {
filtered[key] = wrapEventHandler(value as (detail: SerializedEventData) => void);
} else {
filtered[key] = value;
}
}
return filtered as T;
};`;
};
@@ -140,19 +157,35 @@ const generateHtmlWrapperComponent = (component: ComponentSchema): string => {
const isVoidElement = VOID_ELEMENTS.has(component.htmlTag ?? '');
if (isVoidElement) {
return `const ${component.name}Wrapper = ({ children: _children, ...props }: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('${component.htmlTag}', filterProps(props));
};`;
return `const ${component.name}Wrapper = React.forwardRef<HTMLElement, { children?: React.ReactNode } & Record<string, unknown>>(({ children: _children, ...props }, ref) => {
return React.createElement('${component.htmlTag}', { ...filterHtmlProps(props), ref });
});`;
}
return `const ${component.name}Wrapper = ({ children, ...props }: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement('${component.htmlTag}', filterProps(props), children);
};`;
return `const ${component.name}Wrapper = React.forwardRef<HTMLElement, { children?: React.ReactNode } & Record<string, unknown>>(({ children, ...props }, ref) => {
return React.createElement('${component.htmlTag}', { ...filterHtmlProps(props), ref }, children);
});`;
};
const generateUiWrapperComponent = (component: ComponentSchema): string => {
return `const ${component.name}Wrapper = ({ children, ...props }: { children?: React.ReactNode } & Record<string, unknown>) => {
return React.createElement(${component.componentImport}, filterProps(props), children);
const propsType = isDefined(component.propsTypeName)
? `${component.propsTypeName} & { children?: React.ReactNode }`
: '{ children?: React.ReactNode } & Record<string, unknown>';
const hasEvents = isNonEmptyArray(component.events);
const filterCall = hasEvents
? `filterUiProps(props, new Set([${component.events.map((event) => `'${EVENT_TO_REACT[event]}'`).join(', ')}]))`
: 'filterUiProps(props)';
if (component.supportsRefForwarding) {
return `const ${component.name}Wrapper = React.forwardRef<unknown, ${propsType}>((props, ref) => {
return React.createElement(${component.componentImport} as React.ElementType, { ...${filterCall}, ref });
});`;
}
return `const ${component.name}Wrapper = (props: ${propsType}) => {
return React.createElement(${component.componentImport}, ${filterCall});
};`;
};
@@ -181,10 +214,15 @@ ${entries}
]);`;
};
type ImportGroup = {
namedImports: string[];
typeImports: string[];
};
const groupImportsByPath = (
components: ComponentSchema[],
): Map<string, string[]> => {
const importsByPath = new Map<string, string[]>();
): Map<string, ImportGroup> => {
const importsByPath = new Map<string, ImportGroup>();
for (const component of components) {
if (
@@ -192,10 +230,22 @@ const groupImportsByPath = (
isDefined(component.componentPath) &&
isDefined(component.componentImport)
) {
const existing = importsByPath.get(component.componentPath) ?? [];
if (!existing.includes(component.componentImport)) {
existing.push(component.componentImport);
const existing = importsByPath.get(component.componentPath) ?? {
namedImports: [],
typeImports: [],
};
if (!existing.namedImports.includes(component.componentImport)) {
existing.namedImports.push(component.componentImport);
}
if (
isDefined(component.propsTypeName) &&
!existing.typeImports.includes(component.propsTypeName)
) {
existing.typeImports.push(component.propsTypeName);
}
importsByPath.set(component.componentPath, existing);
}
}
@@ -231,10 +281,18 @@ export const generateHostRegistry = (
const uiImports = groupImportsByPath(components);
for (const [modulePath, namedImports] of uiImports) {
for (const [modulePath, importGroup] of uiImports) {
const allImports = [
...importGroup.namedImports,
...importGroup.typeImports.map((typeName) => ({
name: typeName,
isTypeOnly: true,
})),
];
sourceFile.addImportDeclaration({
moduleSpecifier: modulePath,
namedImports,
namedImports: allImports,
});
}
@@ -1,7 +1,8 @@
import type { Project, SourceFile } from 'ts-morph';
import { EVENT_TO_REACT } from '@/sdk/front-component-common/EventToReact';
import { type ComponentSchema } from './schemas';
import { addExportedConst, addFileHeader, eventToReactProp } from './utils';
import { addExportedConst, addFileHeader } from './utils';
const generateComponentDefinition = (
sourceFile: SourceFile,
@@ -15,7 +16,7 @@ const generateComponentDefinition = (
if (hasEvents) {
const eventProps = component.events
.map((event) => {
const propName = eventToReactProp(event);
const propName = EVENT_TO_REACT[event];
return ` ${propName}: { event: '${event}' },`;
})
.join('\n');
@@ -1,8 +1,8 @@
import {
type CodeBlockWriter,
type Project,
type SourceFile,
VariableDeclarationKind,
type WriterFunction,
} from 'ts-morph';
import {
@@ -13,7 +13,6 @@ import {
import { type ComponentSchema, type PropertySchema } from './schemas';
import {
addFileHeader,
generatePropertiesConfig,
schemaTypeToConstructor,
schemaTypeToTs,
} from './utils';
@@ -147,7 +146,9 @@ const generateElementPropertyType = (
isExported: true,
name: `${component.name}Properties`,
type: (writer) => {
writer.write(`${TYPE_NAMES.COMMON_PROPERTIES} & `);
if (component.isHtmlElement) {
writer.write(`${TYPE_NAMES.COMMON_PROPERTIES} & `);
}
writer.block(() => {
for (const entry of entries) {
writer.writeLine(`${entry};`);
@@ -158,12 +159,14 @@ const generateElementPropertyType = (
}
};
const createPropertiesConfigWriter = (
const writePropertyEntries = (
writer: CodeBlockWriter,
properties: Record<string, PropertySchema>,
): WriterFunction => {
return (writer) => {
writer.write(generatePropertiesConfig(properties));
};
): void => {
for (const [name, schema] of Object.entries(properties)) {
const constructorType = schemaTypeToConstructor(schema.type);
writer.writeLine(`'${name}': { type: ${constructorType} },`);
}
};
const generateElementDefinition = (
@@ -172,17 +175,23 @@ const generateElementDefinition = (
specificProperties: Record<string, PropertySchema>,
options: ElementGenerationOptions,
): void => {
const { useSharedEvents, useSharedPropertiesConfig } = options;
const useSharedEvents = options.useSharedEvents && component.isHtmlElement;
const { useSharedPropertiesConfig } = options;
const hasEvents = component.events.length > 0;
const hasSpecificProps = Object.keys(specificProperties).length > 0;
const hasProps = Object.keys(component.properties).length > 0;
const hasSlots = (component.slots ?? []).length > 0;
const propsType = hasSpecificProps
? `${component.name}Properties`
: hasProps
: hasProps && component.isHtmlElement
? TYPE_NAMES.COMMON_PROPERTIES
: TYPE_NAMES.EMPTY_RECORD;
const slotsType = hasSlots
? `{ ${(component.slots ?? []).map((slot) => `'${slot}': true`).join('; ')} }`
: TYPE_NAMES.EMPTY_RECORD;
const eventsType = hasEvents
? useSharedEvents
? TYPE_NAMES.COMMON_EVENTS
@@ -201,13 +210,13 @@ const generateElementDefinition = (
writer.indent(() => {
writer.writeLine(`${propsType},`);
writer.writeLine('Record<string, never>,');
writer.writeLine('Record<string, never>,');
writer.writeLine(`${slotsType},`);
writer.write(eventsType);
});
writer.newLine();
writer.write('>');
const hasConfig = hasProps || hasEvents;
const hasConfig = hasProps || hasEvents || hasSlots;
if (!hasConfig) {
writer.write('({})');
return;
@@ -215,34 +224,33 @@ const generateElementDefinition = (
writer.write('(');
writer.block(() => {
if (hasSlots) {
writer.write(
`slots: [${(component.slots ?? []).map((slot) => `'${slot}'`).join(', ')}],`,
);
writer.newLine();
}
if (hasProps) {
if (hasSpecificProps) {
if (hasSpecificProps && component.isHtmlElement) {
writer.write('properties: ');
writer.block(() => {
writer.writeLine(
`...${TYPE_NAMES.COMMON_PROPERTIES_CONFIG},`,
);
for (const [name, schema] of Object.entries(
specificProperties,
)) {
const constructorType = schemaTypeToConstructor(
schema.type,
);
writer.writeLine(
`'${name}': { type: ${constructorType} },`,
);
}
writePropertyEntries(writer, specificProperties);
});
writer.write(',');
writer.newLine();
} else if (useSharedPropertiesConfig) {
} else if (useSharedPropertiesConfig && component.isHtmlElement) {
writer.write(
`properties: ${TYPE_NAMES.COMMON_PROPERTIES_CONFIG},`,
);
writer.newLine();
} else {
writer.write('properties: ');
createPropertiesConfigWriter(component.properties)(writer);
writer.block(() => {
writePropertyEntries(writer, specificProperties);
});
writer.write(',');
writer.newLine();
}
@@ -313,10 +321,9 @@ const prepareComponentsWithSpecificProps = (
): ComponentWithSpecificProps[] => {
return components.map((component) => ({
component,
specificProperties: getElementSpecificProperties(
component,
commonPropertyNames,
),
specificProperties: component.isHtmlElement
? getElementSpecificProperties(component, commonPropertyNames)
: component.properties,
}));
};
@@ -381,12 +388,9 @@ export const generateRemoteElements = (
generateCustomElementRegistrations(sourceFile, components);
sourceFile.addExportDeclaration({
namedExports: [
INTERNAL_ELEMENT_CLASSES.ROOT,
INTERNAL_ELEMENT_CLASSES.FRAGMENT,
],
});
sourceFile.addStatements(
`export { ${INTERNAL_ELEMENT_CLASSES.ROOT}, ${INTERNAL_ELEMENT_CLASSES.FRAGMENT} };`,
);
generateTagNameMapDeclaration(sourceFile, components);
@@ -1,7 +1,7 @@
import { z } from 'zod';
export const PropertySchemaZ = z.object({
type: z.enum(['string', 'number', 'boolean']),
type: z.enum(['string', 'number', 'boolean', 'array', 'object', 'function']),
optional: z.boolean(),
});
@@ -15,33 +15,21 @@ export const HtmlElementConfigZ = z.object({
export const HtmlElementConfigArrayZ = z.array(HtmlElementConfigZ);
export const UiComponentConfigZ = z.object({
tag: z
.string()
.regex(/^twenty-ui-[a-z0-9-]+$/, 'Tag must start with "twenty-ui-"'),
name: z
.string()
.regex(/^TwentyUi[A-Z]/, 'Name must be PascalCase starting with TwentyUi'),
properties: z.record(z.string(), PropertySchemaZ),
componentImport: z.string().min(1),
componentPath: z.string().min(1),
});
export const UiComponentConfigArrayZ = z.array(UiComponentConfigZ);
export const ComponentSchemaZ = z.object({
name: z.string().min(1),
tagName: z.string().min(1),
customElementName: z.string().min(1),
properties: z.record(z.string(), PropertySchemaZ),
slots: z.array(z.string()).optional(),
events: z.array(z.string()).readonly(),
isHtmlElement: z.boolean(),
htmlTag: z.string().optional(),
componentImport: z.string().optional(),
componentPath: z.string().optional(),
propsTypeName: z.string().optional(),
supportsRefForwarding: z.boolean().optional(),
});
export type PropertySchema = z.infer<typeof PropertySchemaZ>;
export type HtmlElementConfig = z.infer<typeof HtmlElementConfigZ>;
export type UiComponentConfig = z.infer<typeof UiComponentConfigZ>;
export type ComponentSchema = z.infer<typeof ComponentSchemaZ>;
@@ -1,3 +0,0 @@
export const eventToReactProp = (eventName: string): string => {
return `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
};
@@ -2,10 +2,9 @@ export { addExportedConst } from './add-exported-const';
export { addExportedType } from './add-exported-type';
export { addFileHeader } from './add-file-header';
export { addStatement } from './add-statement';
export { eventToReactProp } from './event-to-react-prop';
export { extractHtmlTag } from './extract-html-tag';
export { GENERATED_FILE_HEADER } from './generated-file-header';
export { generatePropertiesConfig } from './generate-properties-config';
export { generatePropertiesType } from './generate-properties-type';
export { GENERATED_FILE_HEADER } from './generated-file-header';
export { schemaTypeToConstructor } from './schema-type-to-constructor';
export { schemaTypeToTs } from './schema-type-to-ts';
@@ -4,6 +4,9 @@ const SCHEMA_TYPE_TO_CONSTRUCTOR: Record<PropertySchema['type'], string> = {
boolean: 'Boolean',
number: 'Number',
string: 'String',
array: 'Array',
object: 'Object',
function: 'Function',
};
export const schemaTypeToConstructor = (type: PropertySchema['type']): string =>
@@ -4,6 +4,9 @@ const SCHEMA_TYPE_TO_TS: Record<PropertySchema['type'], string> = {
boolean: 'boolean',
number: 'number',
string: 'string',
array: 'unknown[]',
object: 'Record<string, unknown>',
function: '(...args: unknown[]) => unknown',
};
export const schemaTypeToTs = (type: PropertySchema['type']): string =>
@@ -0,0 +1,9 @@
import { EVENT_TO_REACT } from '@/sdk/front-component-common/EventToReact';
export const REACT_PROP_TO_DOM_EVENT: Record<string, string> =
Object.fromEntries(
Object.entries(EVENT_TO_REACT).map(([domEvent, reactProp]) => [
reactProp,
domEvent,
]),
);
@@ -0,0 +1,9 @@
export const TWENTY_UI_COMPONENT_CATEGORIES_TO_SCAN = [
'input',
'components',
'display',
'feedback',
'layout',
'navigation',
'accessibility',
];
@@ -0,0 +1,8 @@
import * as path from 'path';
import { fileURLToPath } from 'url';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const WORKSPACE_ROOT = path.resolve(SCRIPT_DIR, '../../../../../../');
const TWENTY_UI_ROOT = path.join(WORKSPACE_ROOT, 'packages/twenty-ui');
export const TWENTY_UI_ROOT_PATH = TWENTY_UI_ROOT;
@@ -0,0 +1,3 @@
export { REACT_PROP_TO_DOM_EVENT } from './ReactPropToDomEvent';
export { TWENTY_UI_COMPONENT_CATEGORIES_TO_SCAN } from './TwentyUiComponentCategoriesToScan';
export { TWENTY_UI_ROOT_PATH } from './TwentyUiRootPath';
@@ -0,0 +1,153 @@
import * as path from 'path';
import { type ExportSpecifier, Project } from 'ts-morph';
import { isDefined, pascalToKebab } from 'twenty-shared/utils';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import {
logCategory,
logCountInline,
logDimText,
logEmpty,
logLine,
logWarning,
} from '../utils/logger';
import {
TWENTY_UI_COMPONENT_CATEGORIES_TO_SCAN,
TWENTY_UI_ROOT_PATH,
} from './constants';
import { classifyComponentPropsForRemoteDomGeneration } from './utils/classify-component-props-for-remote-dom-generation';
import { doesComponentSupportRefForwarding } from './utils/does-component-support-ref-forwarding';
import { getTwentyUiComponentCategoryIndexPath } from './utils/get-twenty-ui-component-category-index-path';
import { isReactComponentExport } from './utils/is-react-component-export';
import { logDiscoveredComponents } from './utils/log-discovered-components';
import { shouldSkipExport } from './utils/should-skip-export';
export type DiscoveredComponent = {
tag: string;
name: string;
properties: Record<string, PropertySchema>;
events: string[];
slots: string[];
supportsRefForwarding: boolean;
componentImport: string;
componentPath: string;
propsTypeName: string;
};
const extractComponentsFromCategory = (
project: Project,
category: string,
): DiscoveredComponent[] => {
const indexPath = getTwentyUiComponentCategoryIndexPath(category);
const sourceFile = project.getSourceFile(indexPath);
if (!isDefined(sourceFile)) {
logWarning(`Could not find barrel file at ${indexPath}`);
return [];
}
const allNamedExports = sourceFile
.getExportDeclarations()
.flatMap((declaration) => declaration.getNamedExports());
const propsTypeExportsByName = new Map<string, ExportSpecifier>();
const componentExports: ExportSpecifier[] = [];
for (const namedExport of allNamedExports) {
if (namedExport.getName().endsWith('Props')) {
propsTypeExportsByName.set(namedExport.getName(), namedExport);
continue;
}
if (!isReactComponentExport(namedExport)) {
continue;
}
componentExports.push(namedExport);
}
const discoveredComponents: DiscoveredComponent[] = [];
for (const namedExport of componentExports) {
const exportName = namedExport.getName();
if (shouldSkipExport(exportName)) {
continue;
}
const expectedPropsTypeName = `${exportName}Props`;
const propsTypeExport = propsTypeExportsByName.get(expectedPropsTypeName);
if (!isDefined(propsTypeExport)) {
continue;
}
const propsType = propsTypeExport.getNameNode().getType();
const { properties, events, slots } =
classifyComponentPropsForRemoteDomGeneration(propsType);
const supportsRefForwarding =
doesComponentSupportRefForwarding(namedExport);
const kebabName = pascalToKebab(exportName);
discoveredComponents.push({
tag: `twenty-ui-${kebabName}`,
name: `TwentyUi${exportName}`,
properties,
events,
slots,
supportsRefForwarding,
componentImport: exportName,
componentPath: `twenty-ui/${category}`,
propsTypeName: expectedPropsTypeName,
});
}
return discoveredComponents;
};
export const extractAllComponentsFromTwentyUi = (): DiscoveredComponent[] => {
logDimText(' Loading twenty-ui TypeScript project...');
const project = new Project({
tsConfigFilePath: path.join(TWENTY_UI_ROOT_PATH, 'tsconfig.json'),
skipAddingFilesFromTsConfig: false,
});
logLine(
' ' +
logCountInline(
project.getSourceFiles().length,
'source file loaded from twenty-ui',
'source files loaded from twenty-ui',
),
);
logEmpty();
const allDiscoveredComponents: DiscoveredComponent[] = [];
for (const [
index,
category,
] of TWENTY_UI_COMPONENT_CATEGORIES_TO_SCAN.entries()) {
if (index > 0) {
logEmpty();
}
logCategory(category);
const discoveredComponents = extractComponentsFromCategory(
project,
category,
);
logDiscoveredComponents(discoveredComponents);
allDiscoveredComponents.push(...discoveredComponents);
}
return allDiscoveredComponents;
};
@@ -0,0 +1,4 @@
export {
extractAllComponentsFromTwentyUi,
type DiscoveredComponent,
} from './extract-all-components-from-twenty-ui';
@@ -0,0 +1,65 @@
import { type Type } from 'ts-morph';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
import { REACT_PROP_TO_DOM_EVENT } from '../constants/ReactPropToDomEvent';
import { mapTypeToPropertySchema } from './map-type-to-property-schema';
import { isDomEventHandler } from './is-dom-event-handler';
import { isReactElementType } from './is-react-element-type';
export type ClassifiedComponentProps = {
properties: Record<string, PropertySchema>;
events: string[];
slots: string[];
};
export const classifyComponentPropsForRemoteDomGeneration = (
propsType: Type,
): ClassifiedComponentProps => {
const properties: Record<string, PropertySchema> = {};
const events: string[] = [];
const slots: string[] = [];
const propsTypeProperties = propsType.getProperties();
for (const propertySymbol of propsTypeProperties) {
const propertyName = propertySymbol.getName();
const propertyDeclarations = propertySymbol.getDeclarations();
if (!isNonEmptyArray(propertyDeclarations)) {
continue;
}
const firstDeclaration = propertyDeclarations[0];
const propertyType = firstDeclaration.getType();
const correspondingDomEvent = REACT_PROP_TO_DOM_EVENT[propertyName];
if (isDefined(correspondingDomEvent) && isDomEventHandler(propertyType)) {
events.push(correspondingDomEvent);
continue;
}
const isOptional =
propertySymbol.isOptional() ||
propertyType.isNullable() ||
propertyType.isUndefined();
if (isReactElementType(propertyType)) {
slots.push(propertyName);
continue;
}
const classifiedType = mapTypeToPropertySchema(propertyType);
if (isDefined(classifiedType)) {
properties[propertyName] = {
type: classifiedType,
optional: isOptional,
};
}
}
return { properties, events, slots };
};
@@ -0,0 +1,16 @@
import { type ExportSpecifier } from 'ts-morph';
const REF_FORWARDING_TYPE_MARKERS = [
'StyledComponent',
'ForwardRefExoticComponent',
];
export const doesComponentSupportRefForwarding = (
namedExport: ExportSpecifier,
): boolean => {
const typeText = namedExport.getNameNode().getType().getText();
return REF_FORWARDING_TYPE_MARKERS.some((marker) =>
typeText.includes(marker),
);
};
@@ -0,0 +1,9 @@
import * as path from 'path';
import { TWENTY_UI_ROOT_PATH } from '../constants/TwentyUiRootPath';
const TWENTY_UI_SRC_PATH = path.join(TWENTY_UI_ROOT_PATH, 'src');
export const getTwentyUiComponentCategoryIndexPath = (
category: string,
): string => path.join(TWENTY_UI_SRC_PATH, `${category}/index.ts`);
@@ -0,0 +1,6 @@
export * from './does-component-support-ref-forwarding';
export * from './map-type-to-property-schema';
export * from './classify-component-props-for-remote-dom-generation';
export * from './is-react-element-type';
export * from './log-discovered-components';
export * from './should-skip-export';
@@ -0,0 +1,57 @@
import { type Type } from 'ts-morph';
import { isDefined, isNonEmptyArray } from 'twenty-shared/utils';
const DOM_EVENT_TYPE_NAMES = new Set([
'Event',
'UIEvent',
'MouseEvent',
'KeyboardEvent',
'FocusEvent',
'ChangeEvent',
'FormEvent',
'DragEvent',
'WheelEvent',
'ClipboardEvent',
'TouchEvent',
'PointerEvent',
'AnimationEvent',
'TransitionEvent',
'SyntheticEvent',
'BaseSyntheticEvent',
]);
const isEventType = (type: Type): boolean => {
const symbol = type.getSymbol() ?? type.getAliasSymbol();
if (isDefined(symbol) && DOM_EVENT_TYPE_NAMES.has(symbol.getName())) {
return true;
}
return type.getBaseTypes().some(isEventType);
};
export const isDomEventHandler = (propertyType: Type): boolean => {
const nonNullableType = propertyType.getNonNullableType();
const callSignatures = nonNullableType.getCallSignatures();
if (!isNonEmptyArray(callSignatures)) {
return false;
}
return callSignatures.some((signature) => {
const firstParam = signature.getParameters()[0];
if (!isDefined(firstParam)) {
return false;
}
const paramType = firstParam.getValueDeclaration()?.getType();
if (!isDefined(paramType)) {
return false;
}
return isEventType(paramType);
});
};
@@ -0,0 +1,28 @@
import { type ExportSpecifier } from 'ts-morph';
import { isNonEmptyArray } from 'twenty-shared/utils';
import { typeSymbolMatchesAnyName } from './type-symbol-matches-any-name';
const REACT_RETURN_TYPE_NAMES = new Set([
'ReactNode',
'ReactElement',
'Element',
]);
export const isReactComponentExport = (
namedExport: ExportSpecifier,
): boolean => {
const exportType = namedExport.getNameNode().getType();
const callSignatures = exportType.getCallSignatures();
if (!isNonEmptyArray(callSignatures)) {
return false;
}
return callSignatures.some((signature) =>
typeSymbolMatchesAnyName(
signature.getReturnType(),
REACT_RETURN_TYPE_NAMES,
),
);
};
@@ -0,0 +1,24 @@
import { type Type } from 'ts-morph';
import { typeSymbolMatchesAnyName } from './type-symbol-matches-any-name';
const REACT_ELEMENT_TYPE_NAMES = new Set([
'ReactNode',
'ReactElement',
'Element',
]);
const REACT_COMPONENT_TYPE_NAMES = new Set([
'FunctionComponent',
'ComponentType',
'IconComponent',
]);
const ALL_REACT_TYPE_NAMES = new Set([
...REACT_ELEMENT_TYPE_NAMES,
...REACT_COMPONENT_TYPE_NAMES,
]);
export const isReactElementType = (propertyType: Type): boolean => {
return typeSymbolMatchesAnyName(propertyType, ALL_REACT_TYPE_NAMES);
};
@@ -0,0 +1,63 @@
import chalk from 'chalk';
import {
formatEvents,
formatProps,
formatSlots,
logCountInline,
logDimText,
logLine,
} from '../../utils/logger';
type ComponentSummary = {
tag: string;
componentImport: string;
properties: Record<string, unknown>;
events: string[];
slots: string[];
};
export const logDiscoveredComponents = (
discoveredComponents: ComponentSummary[],
): void => {
if (discoveredComponents.length === 0) {
logDimText(' No components found');
return;
}
logLine(
' ' +
logCountInline(
discoveredComponents.length,
'component found',
'components found',
),
);
for (const discoveredComponent of discoveredComponents) {
const propertyCount = Object.keys(discoveredComponent.properties).length;
const eventCount = discoveredComponent.events.length;
const slotCount = discoveredComponent.slots.length;
const parts: string[] = [formatProps(propertyCount)];
if (eventCount > 0) {
parts.push(formatEvents(eventCount, discoveredComponent.events));
}
if (slotCount > 0) {
parts.push(formatSlots(slotCount, discoveredComponent.slots));
}
logLine(
chalk.green(' · ') +
chalk.green(discoveredComponent.componentImport) +
chalk.gray(' -> ') +
chalk.white(discoveredComponent.tag) +
chalk.gray(' (') +
parts.join(chalk.green(', ')) +
chalk.gray(')'),
);
}
};
@@ -0,0 +1,75 @@
import { type Type } from 'ts-morph';
import { type PropertySchema } from '@/front-component/types/PropertySchema';
import { isNonEmptyArray } from 'twenty-shared/utils';
type PropertyType = PropertySchema['type'];
export const mapTypeToPropertySchema = (
propertyType: Type,
): PropertyType | null => {
if (propertyType.isString() || propertyType.isStringLiteral()) {
return 'string';
}
if (propertyType.isNumber() || propertyType.isNumberLiteral()) {
return 'number';
}
if (propertyType.isBoolean() || propertyType.isBooleanLiteral()) {
return 'boolean';
}
if (propertyType.isArray()) {
return 'array';
}
if (propertyType.isTuple()) {
return 'array';
}
const callSignatures = propertyType.getCallSignatures();
if (isNonEmptyArray(callSignatures)) {
return 'function';
}
if (propertyType.isUnion()) {
const unionMemberTypes = propertyType.getUnionTypes();
const nonNullableTypes = unionMemberTypes.filter(
(memberType) => !memberType.isUndefined() && !memberType.isNull(),
);
if (!isNonEmptyArray(nonNullableTypes)) {
return null;
}
const classifiedTypeSet = new Set(
nonNullableTypes.map((memberType) => mapTypeToPropertySchema(memberType)),
);
if (classifiedTypeSet.size === 1) {
return classifiedTypeSet.values().next().value ?? null;
}
const primitiveTypes = new Set(['string', 'number', 'boolean']);
const allPrimitive = [...classifiedTypeSet].every(
(type) => type !== null && primitiveTypes.has(type),
);
if (allPrimitive && classifiedTypeSet.size > 0) {
return 'string';
}
}
if (propertyType.isEnum() || propertyType.isEnumLiteral()) {
return 'string';
}
if (propertyType.isObject() && !propertyType.isArray()) {
return 'object';
}
return null;
};
@@ -0,0 +1,31 @@
const SCREAMING_SNAKE_CASE_REGEX = /^[A-Z][A-Z_0-9]+$/;
const SKIP_PREFIXES = [
'Styled',
'use',
'Icon',
'Illustration',
'base',
'BASE',
'get',
] as const;
const SKIP_SUFFIXES = ['Provider', 'State', 'state'] as const;
const NEVER_SKIP = new Set(['Icon']);
export const shouldSkipExport = (exportName: string): boolean => {
if (NEVER_SKIP.has(exportName)) {
return false;
}
const hasSkipPrefix = SKIP_PREFIXES.some((prefix) =>
exportName.startsWith(prefix),
);
const hasSkipSuffix = SKIP_SUFFIXES.some((suffix) =>
exportName.endsWith(suffix),
);
const isScreamingSnakeCase = SCREAMING_SNAKE_CASE_REGEX.test(exportName);
return hasSkipPrefix || hasSkipSuffix || isScreamingSnakeCase;
};
@@ -0,0 +1,27 @@
import { type Type } from 'ts-morph';
import { isDefined } from 'twenty-shared/utils';
const getTypeSymbolName = (type: Type): string | undefined => {
return (type.getSymbol() ?? type.getAliasSymbol())?.getName();
};
export const typeSymbolMatchesAnyName = (
type: Type,
names: Set<string>,
): boolean => {
const symbolName = getTypeSymbolName(type);
if (isDefined(symbolName) && names.has(symbolName)) {
return true;
}
if (type.isUnion()) {
return type
.getUnionTypes()
.filter((memberType) => !memberType.isUndefined() && !memberType.isNull())
.some((memberType) => typeSymbolMatchesAnyName(memberType, names));
}
return false;
};
@@ -0,0 +1,135 @@
import chalk from 'chalk';
const SEPARATOR_WIDTH = 60;
let _verbose = false;
export const setVerbose = (value: boolean): void => {
_verbose = value;
};
export const isVerbose = (): boolean => _verbose;
export const logSeparator = (): void => {
if (!_verbose) return;
console.log('');
console.log(chalk.gray('─'.repeat(SEPARATOR_WIDTH)));
console.log('');
};
export const logTitle = (text: string): void => {
if (!_verbose) return;
console.log('');
console.log(chalk.bold.white(` ${text}`));
console.log('');
console.log(chalk.gray('─'.repeat(SEPARATOR_WIDTH)));
console.log('');
};
export const logSectionHeader = (text: string): void => {
if (!_verbose) return;
console.log(chalk.bold.white(` ${text}`));
console.log('');
};
export const logCategory = (name: string): void => {
if (!_verbose) return;
console.log(chalk.green(' ▸ ') + chalk.green.bold(name));
};
export const logSubItem = (label: string, value: string): void => {
if (!_verbose) return;
console.log(
chalk.gray(' ') +
chalk.green(label) +
chalk.gray(' -> ') +
chalk.white(value),
);
};
const pluralize = (count: number, singular: string, plural: string): string =>
count === 1 ? singular : plural;
export const logCount = (
label: string,
count: number,
singularUnit: string,
pluralUnit?: string,
): void => {
if (!_verbose) return;
const unit = pluralize(count, singularUnit, pluralUnit ?? singularUnit + 's');
console.log(
chalk.green(` ${label} `) +
chalk.white.bold(`${count}`) +
chalk.gray(` ${unit}`),
);
};
export const logDetail = (text: string): void => {
if (!_verbose) return;
console.log(chalk.gray(` ${text}`));
};
export const logDimText = (text: string): void => {
if (!_verbose) return;
console.log(chalk.gray(text));
};
export const logFileWritten = (filePath: string): void => {
if (!_verbose) return;
console.log(chalk.green(' ✓ ') + chalk.gray(filePath));
};
export const logGroupLabel = (text: string): void => {
if (!_verbose) return;
console.log(chalk.green(` ${text}`));
};
export const logSuccess = (message: string, detail?: string): void => {
if (!_verbose) return;
const detailSuffix = detail ? chalk.gray(` ${detail}`) : '';
console.log(chalk.green(``) + chalk.green.bold(message) + detailSuffix);
};
export const logError = (message: string, error?: unknown): void => {
console.error(chalk.red.bold(`${message}`), error ?? '');
};
export const logWarning = (message: string): void => {
if (!_verbose) return;
console.warn(chalk.yellow(` ${message}`));
};
export const logEmpty = (): void => {
if (!_verbose) return;
console.log('');
};
export const logLine = (text: string): void => {
if (!_verbose) return;
console.log(text);
};
export const logCountInline = (
count: number,
singularUnit: string,
pluralUnit?: string,
prefix?: string,
): string => {
const unit = pluralize(count, singularUnit, pluralUnit ?? singularUnit + 's');
const prefixText = prefix ? chalk.gray(`${prefix} `) : '';
return prefixText + chalk.white.bold(`${count}`) + chalk.gray(` ${unit}`);
};
export const formatProps = (count: number): string =>
chalk.green(`${count} ${pluralize(count, 'prop', 'props')}`);
export const formatEvents = (count: number, names: string[]): string =>
chalk.yellow(`${count} ${pluralize(count, 'event', 'events')}`) +
chalk.gray(` [${names.join(', ')}]`);
export const formatSlots = (count: number, names: string[]): string =>
chalk.magenta(`${count} ${pluralize(count, 'slot', 'slots')}`) +
chalk.gray(` [${names.join(', ')}]`);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -1,4 +1,4 @@
export type PropertySchema = {
type: 'string' | 'number' | 'boolean';
type: 'string' | 'number' | 'boolean' | 'array' | 'object' | 'function';
optional: boolean;
};
@@ -1,26 +0,0 @@
import { type PropertySchema } from '@/front-component/types/PropertySchema';
export type AllowedUiComponent = {
tag: string;
name: string;
properties: Record<string, PropertySchema>;
componentImport: string;
componentPath: string;
};
export const ALLOWED_UI_COMPONENTS: AllowedUiComponent[] = [
{
tag: 'twenty-ui-button',
name: 'TwentyUiButton',
properties: {
title: { type: 'string', optional: true },
variant: { type: 'string', optional: true },
accent: { type: 'string', optional: true },
size: { type: 'string', optional: true },
disabled: { type: 'boolean', optional: true },
fullWidth: { type: 'boolean', optional: true },
},
componentImport: 'Button',
componentPath: 'twenty-ui/input',
},
];
@@ -9,8 +9,6 @@
export type { AllowedHtmlElement } from './AllowedHtmlElements';
export { ALLOWED_HTML_ELEMENTS } from './AllowedHtmlElements';
export type { AllowedUiComponent } from './AllowedUiComponents';
export { ALLOWED_UI_COMPONENTS } from './AllowedUiComponents';
export { COMMON_HTML_EVENTS } from './CommonHtmlEvents';
export { EVENT_TO_REACT } from './EventToReact';
export { HTML_COMMON_PROPERTIES } from './HtmlCommonProperties';
-2
View File
@@ -59,8 +59,6 @@ export type { FrontComponentExecutionContext } from './front-component-api';
// Front Component Common exports
export type { AllowedHtmlElement } from './front-component-common';
export { ALLOWED_HTML_ELEMENTS } from './front-component-common';
export type { AllowedUiComponent } from './front-component-common';
export { ALLOWED_UI_COMPONENTS } from './front-component-common';
export { COMMON_HTML_EVENTS } from './front-component-common';
export { EVENT_TO_REACT } from './front-component-common';
export { HTML_COMMON_PROPERTIES } from './front-component-common';
+1
View File
@@ -20,6 +20,7 @@
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.d.ts",
"scripts/**/*.ts",
".storybook/*.ts",
".storybook/*.tsx",
"**/__mocks__/**/*",
+20 -13
View File
@@ -77,19 +77,26 @@ export default defineConfig(() => {
}
warn(warning);
},
external: [
...Object.keys((packageJson as any).dependencies || {}),
'path',
'fs',
'fs/promises',
'url',
'crypto',
'stream',
'util',
'os',
'module',
/^node:/,
],
external: (id: string) => {
if (/^node:/.test(id)) return true;
const builtins = [
'path',
'fs',
'fs/promises',
'url',
'crypto',
'stream',
'util',
'os',
'module',
];
if (builtins.includes(id)) return true;
const deps = Object.keys((packageJson as any).dependencies || {});
return deps.some((dep) => id === dep || id.startsWith(dep + '/'));
},
output: [
{
format: 'es',
@@ -2,6 +2,8 @@ import * as fs from 'fs';
import * as path from 'path';
import * as process from 'process';
import { pascalToKebab } from 'twenty-shared/utils';
import { INTROSPECTION_QUERY } from './introspection-query';
import {
type Field,
@@ -33,10 +35,6 @@ const fetchGraphQLSchema = async (): Promise<IntrospectionResponse> => {
return response.json();
};
const toKebabCase = (name: string): string => {
return name.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
};
const unwrapType = (typeInfo: TypeRef): any => {
while (typeInfo.ofType) {
typeInfo = typeInfo.ofType;
@@ -124,7 +122,7 @@ const writeTestFile = (
): string => {
if (!content) return 'skipped';
const fileName = `${toKebabCase(queryName)}.integration-spec.ts`;
const fileName = `${pascalToKebab(queryName)}.integration-spec.ts`;
const filePath = path.join(TEST_OUTPUT_DIR, fileName);
if (fs.existsSync(filePath) && !force) {
@@ -148,6 +148,7 @@ export { appendCopySuffix } from './strings/appendCopySuffix';
export { camelToSnakeCase } from './strings/camelToSnakeCase';
export { capitalize } from './strings/capitalize';
export { pascalCase } from './strings/pascalCase';
export { pascalToKebab } from './strings/pascalToKebab';
export { stringifySafely } from './strings/stringifySafely';
export { uncapitalize } from './strings/uncapitalize';
export type {
@@ -0,0 +1,27 @@
import { pascalToKebab } from '../pascalToKebab';
describe('pascalToKebab', () => {
it('should convert PascalCase to kebab-case', () => {
expect(pascalToKebab('UserProfile')).toBe('user-profile');
});
it('should convert single word', () => {
expect(pascalToKebab('Button')).toBe('button');
});
it('should handle consecutive uppercase letters', () => {
expect(pascalToKebab('HTMLElement')).toBe('html-element');
});
it('should handle numbers in the name', () => {
expect(pascalToKebab('H1Title')).toBe('h1-title');
});
it('should handle multi-word PascalCase', () => {
expect(pascalToKebab('MyComponentName')).toBe('my-component-name');
});
it('should handle already lowercase', () => {
expect(pascalToKebab('button')).toBe('button');
});
});
@@ -1,3 +1,4 @@
export * from './appendCopySuffix';
export * from './capitalize';
export * from './pascalCase';
export * from './pascalToKebab';
@@ -0,0 +1,6 @@
export const pascalToKebab = (name: string): string => {
return name
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
.toLowerCase();
};
+22
View File
@@ -69,4 +69,26 @@ export default [
'lingui/text-restrictions': 'off',
},
},
{
files: [
'**/src/input/**/*.tsx',
'**/src/components/**/*.tsx',
'**/src/display/**/*.tsx',
'**/src/feedback/**/*.tsx',
'**/src/layout/**/*.tsx',
'**/src/navigation/**/*.tsx',
'**/src/accessibility/**/*.tsx',
],
ignores: [
'**/*.stories.tsx',
'**/__stories__/**/*.tsx',
'**/*.test.tsx',
'**/__tests__/**/*.tsx',
'**/testing/**/*.tsx',
],
rules: {
'twenty/export-component-props': 'error',
},
},
];
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { type IconComponent } from '@ui/display';
type PillProps = {
export type PillProps = {
className?: string;
label?: string;
Icon?: IconComponent;
+2 -1
View File
@@ -16,6 +16,7 @@ export { ChipSize, ChipAccent, ChipVariant, Chip } from './chip/Chip';
export { LINK_CHIP_CLICK_OUTSIDE_ID } from './chip/constants/LinkChipClickOutsideId';
export type { LinkChipProps } from './chip/LinkChip';
export { LinkChip } from './chip/LinkChip';
export type { PillProps } from './Pill/Pill';
export { Pill } from './Pill/Pill';
export type { TagColor } from './tag/Tag';
export type { TagColor, TagProps } from './tag/Tag';
export { Tag } from './tag/Tag';
@@ -86,7 +86,7 @@ type TagWeight = 'regular' | 'medium';
type TagVariant = 'solid' | 'outline' | 'border';
export type TagColor = ThemeColor | 'transparent';
type TagProps = {
export type TagProps = {
className?: string;
color: TagColor;
text: string;
@@ -22,7 +22,7 @@ const StyledBanner = styled.div<{ variant?: BannerVariant }>`
export type BannerVariant = 'danger' | 'default';
type BannerProps = {
export type BannerProps = {
variant?: BannerVariant;
className?: string;
children: React.ReactNode;
@@ -29,7 +29,7 @@ const StyledLineSpan = styled.span`
color: ${({ theme }) => theme.code.text.green};
`;
type CommandBlockProps = {
export type CommandBlockProps = {
commands: string[];
button?: ReactElement;
};
@@ -2,7 +2,7 @@ import IconAddressBookRaw from '@assets/icons/address-book.svg?react';
import { useTheme } from '@emotion/react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconAddressBookProps = Pick<
export type IconAddressBookProps = Pick<
IconComponentProps,
'size' | 'stroke' | 'color'
>;
@@ -9,7 +9,7 @@ const StyledRotatedIconWrapper = styled.div`
transform: rotate(90deg);
`;
type IconChartBarHorizontalProps = Pick<
export type IconChartBarHorizontalProps = Pick<
IconComponentProps,
'size' | 'stroke' | 'color'
>;
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconGmailRaw from '@assets/icons/gmail.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconGmailProps = Pick<IconComponentProps, 'size'>;
export type IconGmailProps = Pick<IconComponentProps, 'size'>;
export const IconGmail = (props: IconGmailProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconGoogleRaw from '@assets/icons/google.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconGoogleProps = Pick<IconComponentProps, 'size'>;
export type IconGoogleProps = Pick<IconComponentProps, 'size'>;
export const IconGoogle = (props: IconGoogleProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconGoogleCalendarRaw from '@assets/icons/google-calendar.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconGoogleCalendarProps = Pick<IconComponentProps, 'size'>;
export type IconGoogleCalendarProps = Pick<IconComponentProps, 'size'>;
export const IconGoogleCalendar = (props: IconGoogleCalendarProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconLockRaw from '@assets/icons/lock.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconLockCustomProps = Pick<IconComponentProps, 'size'>;
export type IconLockCustomProps = Pick<IconComponentProps, 'size'>;
export const IconLockCustom = (props: IconLockCustomProps) => {
const theme = useTheme();
@@ -2,7 +2,7 @@ import { useTheme } from '@emotion/react';
import IconMicrosoftRaw from '@assets/icons/microsoft.svg?react';
interface IconMicrosoftProps {
export interface IconMicrosoftProps {
size?: number | string;
}
@@ -2,7 +2,7 @@ import { useTheme } from '@emotion/react';
import IconMicrosoftCalendarRaw from '@assets/icons/microsoft-calendar.svg?react';
interface IconMicrosoftCalendarProps {
export interface IconMicrosoftCalendarProps {
size?: number | string;
}
@@ -2,7 +2,7 @@ import { useTheme } from '@emotion/react';
import IconMicrosoftOutlookRaw from '@assets/icons/microsoft-outlook.svg?react';
interface IconMicrosoftOutlookProps {
export interface IconMicrosoftOutlookProps {
size?: number | string;
}
@@ -3,7 +3,10 @@ import { useTheme } from '@emotion/react';
import IconRelationManyToOneRaw from '@assets/icons/many-to-one.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconRelationManyToOneProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export type IconRelationManyToOneProps = Pick<
IconComponentProps,
'size' | 'stroke'
>;
export const IconRelationManyToOne = (props: IconRelationManyToOneProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconTrashXOffRaw from '@assets/icons/trash-x-off.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconTrashXOffProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export type IconTrashXOffProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export const IconTrashXOff = (props: IconTrashXOffProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import IconTwentyStarRaw from '@assets/icons/twenty-star.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IconTwentyStarProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export type IconTwentyStarProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export const IconTwentyStar = (props: IconTwentyStarProps) => {
const theme = useTheme();
@@ -2,7 +2,10 @@ import IconTwentyStarFilledRaw from '@assets/icons/twenty-star-filled.svg?react'
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
import { THEME_COMMON } from '@ui/theme';
type IconTwentyStarFilledProps = Pick<IconComponentProps, 'size' | 'stroke'>;
export type IconTwentyStarFilledProps = Pick<
IconComponentProps,
'size' | 'stroke'
>;
const iconStrokeMd = THEME_COMMON.icon.stroke.md;
@@ -2,7 +2,7 @@ import IllustrationIconArrayRaw from '@assets/icons/illustration-array.svg?react
import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconArrayProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconArrayProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconArray = (props: IllustrationIconArrayProps) => {
const theme = useTheme();
@@ -2,7 +2,10 @@ import IllustrationIconCalendarEventRaw from '@assets/icons/illustration-calenda
import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconCalendarEventProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconCalendarEventProps = Pick<
IconComponentProps,
'size'
>;
export const IllustrationIconCalendarEvent = (
props: IllustrationIconCalendarEventProps,
@@ -3,7 +3,10 @@ import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconCalendarTimeProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconCalendarTimeProps = Pick<
IconComponentProps,
'size'
>;
export const IllustrationIconCalendarTime = (
props: IllustrationIconCalendarTimeProps,
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconCurrencyRaw from '@assets/icons/illustration-currency.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconCurrencyProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconCurrencyProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconCurrency = (
props: IllustrationIconCurrencyProps,
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconFileRaw from '@assets/icons/illustration-file.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconFileProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconFileProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconFile = (props: IllustrationIconFileProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconJsonRaw from '@assets/icons/illustration-json.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconJsonProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconJsonProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconJson = (props: IllustrationIconJsonProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconLinkRaw from '@assets/icons/illustration-link.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconLinkProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconLinkProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconLink = (props: IllustrationIconLinkProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconMailProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconMailProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconMail = (props: IllustrationIconMailProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconManyToManyProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconManyToManyProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconManyToMany = (
props: IllustrationIconManyToManyProps,
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconMapProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconMapProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconMap = (props: IllustrationIconMapProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useTheme } from '@emotion/react';
import { IllustrationIconWrapper } from '@ui/display/icon/components/IllustrationIconWrapper';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconNumbersProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconNumbersProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconNumbers = (
props: IllustrationIconNumbersProps,
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconOneToManyRaw from '@assets/icons/illustration-one-to-many.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconOneToManyProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconOneToManyProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconOneToMany = (
props: IllustrationIconOneToManyProps,
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconOneToOneRaw from '@assets/icons/illustration-one-to-one.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconOneToOneProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconOneToOneProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconOneToOne = (
props: IllustrationIconOneToOneProps,
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconPhoneRaw from '@assets/icons/illustration-phone.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconPhoneProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconPhoneProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconPhone = (props: IllustrationIconPhoneProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconSettingRaw from '@assets/icons/illustration-setting.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconSettingProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconSettingProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconSetting = (
props: IllustrationIconSettingProps,
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconStarRaw from '@assets/icons/illustration-star.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconStarProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconStarProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconStar = (props: IllustrationIconStarProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconTagRaw from '@assets/icons/illustration-tag.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconTagProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconTagProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconTag = (props: IllustrationIconTagProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconTagsRaw from '@assets/icons/illustration-tags.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconTagsProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconTagsProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconTags = (props: IllustrationIconTagsProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconTextRaw from '@assets/icons/illustration-text.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconTextProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconTextProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconText = (props: IllustrationIconTextProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconToggleRaw from '@assets/icons/illustration-toggle.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconToggleProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconToggleProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconToggle = (props: IllustrationIconToggleProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconUidRaw from '@assets/icons/illustration-uid.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconUidProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconUidProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconUid = (props: IllustrationIconUidProps) => {
const theme = useTheme();
@@ -4,7 +4,7 @@ import { IllustrationIconWrapper } from '@ui/display/icon/components/Illustratio
import IllustrationIconUserRaw from '@assets/icons/illustration-user.svg?react';
import { type IconComponentProps } from '@ui/display/icon/types/IconComponent';
type IllustrationIconUserProps = Pick<IconComponentProps, 'size'>;
export type IllustrationIconUserProps = Pick<IconComponentProps, 'size'>;
export const IllustrationIconUser = (props: IllustrationIconUserProps) => {
const theme = useTheme();
@@ -3,7 +3,7 @@ import { useSetRecoilState } from 'recoil';
import { iconsState } from '@ui/display/icon/states/iconsState';
type IconsProviderProps = {
export type IconsProviderProps = {
children: JSX.Element;
};
+45 -1
View File
@@ -15,7 +15,7 @@ export { invalidAvatarUrlsState } from './avatar/components/states/isInvalidAvat
export { AVATAR_PROPERTIES_BY_SIZE } from './avatar/constants/AvatarPropertiesBySize';
export type { AvatarSize } from './avatar/types/AvatarSize';
export type { AvatarType } from './avatar/types/AvatarType';
export type { BannerVariant } from './banner/components/Banner';
export type { BannerVariant, BannerProps } from './banner/components/Banner';
export { Banner } from './banner/components/Banner';
export type { SidePanelInformationBannerProps } from './banner/components/SidePanelInformationBanner';
export { SidePanelInformationBanner } from './banner/components/SidePanelInformationBanner';
@@ -30,43 +30,79 @@ export type {
ColorSampleProps,
} from './color/components/ColorSample';
export { ColorSample } from './color/components/ColorSample';
export type { CommandBlockProps } from './command-block/components/CommandBlock';
export { CommandBlock } from './command-block/components/CommandBlock';
export type { IconProps } from './icon/components/Icon';
export { Icon } from './icon/components/Icon';
export type { IconAddressBookProps } from './icon/components/IconAddressBook';
export { IconAddressBook } from './icon/components/IconAddressBook';
export type { IconChartBarHorizontalProps } from './icon/components/IconChartBarHorizontal';
export { IconChartBarHorizontal } from './icon/components/IconChartBarHorizontal';
export type { IconGmailProps } from './icon/components/IconGmail';
export { IconGmail } from './icon/components/IconGmail';
export type { IconGoogleProps } from './icon/components/IconGoogle';
export { IconGoogle } from './icon/components/IconGoogle';
export type { IconGoogleCalendarProps } from './icon/components/IconGoogleCalendar';
export { IconGoogleCalendar } from './icon/components/IconGoogleCalendar';
export type { IconLockCustomProps } from './icon/components/IconLock';
export { IconLockCustom } from './icon/components/IconLock';
export type { IconMicrosoftProps } from './icon/components/IconMicrosoft';
export { IconMicrosoft } from './icon/components/IconMicrosoft';
export type { IconMicrosoftCalendarProps } from './icon/components/IconMicrosoftCalendar';
export { IconMicrosoftCalendar } from './icon/components/IconMicrosoftCalendar';
export type { IconMicrosoftOutlookProps } from './icon/components/IconMicrosoftOutlook';
export { IconMicrosoftOutlook } from './icon/components/IconMicrosoftOutlook';
export type { IconRelationManyToOneProps } from './icon/components/IconRelationManyToOne';
export { IconRelationManyToOne } from './icon/components/IconRelationManyToOne';
export type { IconTrashXOffProps } from './icon/components/IconTrashXOff';
export { IconTrashXOff } from './icon/components/IconTrashXOff';
export type { IconTwentyStarProps } from './icon/components/IconTwentyStar';
export { IconTwentyStar } from './icon/components/IconTwentyStar';
export type { IconTwentyStarFilledProps } from './icon/components/IconTwentyStarFilled';
export { IconTwentyStarFilled } from './icon/components/IconTwentyStarFilled';
export type { IllustrationIconArrayProps } from './icon/components/IllustrationIconArray';
export { IllustrationIconArray } from './icon/components/IllustrationIconArray';
export type { IllustrationIconCalendarEventProps } from './icon/components/IllustrationIconCalendarEvent';
export { IllustrationIconCalendarEvent } from './icon/components/IllustrationIconCalendarEvent';
export type { IllustrationIconCalendarTimeProps } from './icon/components/IllustrationIconCalendarTime';
export { IllustrationIconCalendarTime } from './icon/components/IllustrationIconCalendarTime';
export type { IllustrationIconCurrencyProps } from './icon/components/IllustrationIconCurrency';
export { IllustrationIconCurrency } from './icon/components/IllustrationIconCurrency';
export type { IllustrationIconFileProps } from './icon/components/IllustrationIconFile';
export { IllustrationIconFile } from './icon/components/IllustrationIconFile';
export type { IllustrationIconJsonProps } from './icon/components/IllustrationIconJson';
export { IllustrationIconJson } from './icon/components/IllustrationIconJson';
export type { IllustrationIconLinkProps } from './icon/components/IllustrationIconLink';
export { IllustrationIconLink } from './icon/components/IllustrationIconLink';
export type { IllustrationIconMailProps } from './icon/components/IllustrationIconMail';
export { IllustrationIconMail } from './icon/components/IllustrationIconMail';
export type { IllustrationIconManyToManyProps } from './icon/components/IllustrationIconManyToMany';
export { IllustrationIconManyToMany } from './icon/components/IllustrationIconManyToMany';
export type { IllustrationIconMapProps } from './icon/components/IllustrationIconMap';
export { IllustrationIconMap } from './icon/components/IllustrationIconMap';
export type { IllustrationIconNumbersProps } from './icon/components/IllustrationIconNumbers';
export { IllustrationIconNumbers } from './icon/components/IllustrationIconNumbers';
export type { IllustrationIconOneToManyProps } from './icon/components/IllustrationIconOneToMany';
export { IllustrationIconOneToMany } from './icon/components/IllustrationIconOneToMany';
export type { IllustrationIconOneToOneProps } from './icon/components/IllustrationIconOneToOne';
export { IllustrationIconOneToOne } from './icon/components/IllustrationIconOneToOne';
export type { IllustrationIconPhoneProps } from './icon/components/IllustrationIconPhone';
export { IllustrationIconPhone } from './icon/components/IllustrationIconPhone';
export type { IllustrationIconSettingProps } from './icon/components/IllustrationIconSetting';
export { IllustrationIconSetting } from './icon/components/IllustrationIconSetting';
export type { IllustrationIconStarProps } from './icon/components/IllustrationIconStar';
export { IllustrationIconStar } from './icon/components/IllustrationIconStar';
export type { IllustrationIconTagProps } from './icon/components/IllustrationIconTag';
export { IllustrationIconTag } from './icon/components/IllustrationIconTag';
export type { IllustrationIconTagsProps } from './icon/components/IllustrationIconTags';
export { IllustrationIconTags } from './icon/components/IllustrationIconTags';
export type { IllustrationIconTextProps } from './icon/components/IllustrationIconText';
export { IllustrationIconText } from './icon/components/IllustrationIconText';
export type { IllustrationIconToggleProps } from './icon/components/IllustrationIconToggle';
export { IllustrationIconToggle } from './icon/components/IllustrationIconToggle';
export type { IllustrationIconUidProps } from './icon/components/IllustrationIconUid';
export { IllustrationIconUid } from './icon/components/IllustrationIconUid';
export type { IllustrationIconUserProps } from './icon/components/IllustrationIconUser';
export { IllustrationIconUser } from './icon/components/IllustrationIconUser';
export { IllustrationIconWrapper } from './icon/components/IllustrationIconWrapper';
export type { TablerIconsProps } from './icon/components/TablerIcons';
@@ -471,6 +507,7 @@ export {
IconX,
} from './icon/components/TablerIcons';
export { useIcons } from './icon/hooks/useIcons';
export type { IconsProviderProps } from './icon/providers/IconsProvider';
export { IconsProvider } from './icon/providers/IconsProvider';
export { iconsState } from './icon/states/iconsState';
export type {
@@ -479,7 +516,9 @@ export type {
} from './icon/types/IconComponent';
export type { InfoAccent, InfoProps } from './info/components/Info';
export { Info } from './info/components/Info';
export type { StatusProps } from './status/components/Status';
export { Status } from './status/components/Status';
export type { HorizontalSeparatorProps } from './text/components/HorizontalSeparator';
export { HorizontalSeparator } from './text/components/HorizontalSeparator';
export { SeparatorLineText } from './text/components/SeparatorLineText';
export type { AppTooltipProps } from './tooltip/AppTooltip';
@@ -488,12 +527,17 @@ export {
TooltipDelay,
AppTooltip,
} from './tooltip/AppTooltip';
export type { OverflowingTextWithTooltipProps } from './tooltip/OverflowingTextWithTooltip';
export { OverflowingTextWithTooltip } from './tooltip/OverflowingTextWithTooltip';
export type { H1TitleProps } from './typography/components/H1Title';
export { H1TitleFontColor, H1Title } from './typography/components/H1Title';
export type { H2TitleProps } from './typography/components/H2Title';
export { H2Title } from './typography/components/H2Title';
export type { H3TitleProps } from './typography/components/H3Title';
export { H3Title } from './typography/components/H3Title';
export type { LabelVariant } from './typography/components/Label';
export { Label } from './typography/components/Label';
export type { StyledTextProps } from './typography/components/StyledText';
export {
StyledTextContent,
StyledTextWrapper,
@@ -43,7 +43,7 @@ const StyledContent = styled.span`
white-space: nowrap;
`;
type StatusProps = {
export type StatusProps = {
className?: string;
color: ThemeColor;
isLoaderVisible?: boolean;
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { type JSX } from 'react';
import { Label } from '@ui/display';
type HorizontalSeparatorProps = {
export type HorizontalSeparatorProps = {
visible?: boolean;
text?: string;
noMargin?: boolean;
@@ -59,7 +59,7 @@ const StyledPre = styled.pre`
white-space: pre-wrap;
`;
type OverflowingTextWithTooltipProps = {
export type OverflowingTextWithTooltipProps = {
size?: 'large' | 'small';
isTooltipMultiline?: boolean;
displayedMaxRows?: number;
@@ -1,7 +1,7 @@
import { type ReactNode } from 'react';
import styled from '@emotion/styled';
type H1TitleProps = {
export type H1TitleProps = {
title: ReactNode;
fontColor?: H1TitleFontColor;
className?: string;
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
type H2TitleProps = {
export type H2TitleProps = {
title: string;
description?: string;
adornment?: React.ReactNode;
@@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { OverflowingTextWithTooltip } from '@ui/display/tooltip/OverflowingTextWithTooltip';
import { type ReactNode } from 'react';
type H3TitleProps = {
export type H3TitleProps = {
title: ReactNode;
description?: string;
className?: string;
@@ -1,7 +1,7 @@
import { type ReactElement, type ReactNode } from 'react';
import styled from '@emotion/styled';
type StyledTextProps = {
export type StyledTextProps = {
PrefixComponent?: ReactElement;
text: ReactNode;
color?: string;
+2
View File
@@ -7,7 +7,9 @@
* |___/
*/
export type { LoaderProps } from './loader/components/Loader';
export { Loader } from './loader/components/Loader';
export type { CircularProgressBarProps } from './progress-bar/components/CircularProgressBar';
export { CircularProgressBar } from './progress-bar/components/CircularProgressBar';
export type {
ProgressBarProps,
@@ -33,7 +33,7 @@ const StyledLoader = styled(motion.div)<{
width: 8px;
`;
type LoaderProps = {
export type LoaderProps = {
color?: ThemeColor;
};
@@ -1,7 +1,7 @@
import { motion, useAnimation } from 'framer-motion';
import { useEffect, useMemo } from 'react';
interface CircularProgressBarProps {
export interface CircularProgressBarProps {
size?: number;
barWidth?: number;
barColor?: string;
@@ -5,7 +5,7 @@ const StyledSoonPill = styled(Pill)`
margin-left: auto;
`;
type ButtonSoonProps = {
export type ButtonSoonProps = {
label?: string;
};
@@ -6,7 +6,7 @@ import {
type LightIconButtonProps,
} from '@ui/input/button/components/LightIconButton';
type ColorPickerButtonProps = Pick<ColorSampleProps, 'colorName'> &
export type ColorPickerButtonProps = Pick<ColorSampleProps, 'colorName'> &
Pick<LightIconButtonProps, 'onClick'> & {
isSelected?: boolean;
};

Some files were not shown because too many files have changed in this diff Show More