[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:
@@ -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 ');
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -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/**/*",
|
||||
|
||||
+57
-31
@@ -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 =>
|
||||
|
||||
+9
@@ -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,
|
||||
]),
|
||||
);
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
export const TWENTY_UI_COMPONENT_CATEGORIES_TO_SCAN = [
|
||||
'input',
|
||||
'components',
|
||||
'display',
|
||||
'feedback',
|
||||
'layout',
|
||||
'navigation',
|
||||
'accessibility',
|
||||
];
|
||||
+8
@@ -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';
|
||||
+153
@@ -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';
|
||||
+65
@@ -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 };
|
||||
};
|
||||
+16
@@ -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),
|
||||
);
|
||||
};
|
||||
+9
@@ -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';
|
||||
+57
@@ -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);
|
||||
});
|
||||
};
|
||||
+28
@@ -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,
|
||||
),
|
||||
);
|
||||
};
|
||||
+24
@@ -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);
|
||||
};
|
||||
+63
@@ -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(')'),
|
||||
);
|
||||
}
|
||||
};
|
||||
+75
@@ -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;
|
||||
};
|
||||
+31
@@ -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;
|
||||
};
|
||||
+27
@@ -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(', ')}]`);
|
||||
+1328
-232
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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
"src/**/*.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.d.ts",
|
||||
"scripts/**/*.ts",
|
||||
".storybook/*.ts",
|
||||
".storybook/*.tsx",
|
||||
"**/__mocks__/**/*",
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user