From 66d7182d026fc276feb77a54d11752f712a472db Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 3 Feb 2026 16:54:18 +0100 Subject: [PATCH] Remote dom twenty UI POC (#17652) Create a proof of concept which allows us to use twenty-ui in the remote-dom. For now only the button is allowed. https://github.com/user-attachments/assets/e5441d2c-eb63-4b99-931b-86ee14621393 Known limitation: The icons are not yet available --- .../components/FrontComponentRenderer.tsx | 14 +++--- .../utils/mockFrontComponent.ts | 14 +++++- packages/twenty-sdk/package.json | 1 + .../remote-dom/generateRemoteDomElements.ts | 41 ++++++++++++++-- .../generators/host-registry.generator.ts | 47 ++++++++++++++++++- .../scripts/remote-dom/generators/schemas.ts | 19 +++++++- .../AllowedUiComponents.ts | 26 ++++++++++ .../src/front-component-constants/index.ts | 2 + .../components/FrontComponentRenderer.tsx | 17 +++++-- .../host/generated/host-component-registry.ts | 8 ++++ .../twenty-sdk/src/front-component/index.ts | 3 ++ .../remote/generated/remote-components.ts | 29 ++++++++++++ .../remote/generated/remote-elements.ts | 27 +++++++++++ packages/twenty-ui/project.json | 2 +- yarn.lock | 1 + 15 files changed, 230 insertions(+), 21 deletions(-) create mode 100644 packages/twenty-sdk/src/front-component-constants/AllowedUiComponents.ts diff --git a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx index 3683a34770..ff588847d9 100644 --- a/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx +++ b/packages/twenty-front/src/modules/front-components/components/FrontComponentRenderer.tsx @@ -1,12 +1,10 @@ -import { useState } from 'react'; - -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { t } from '@lingui/core/macro'; -import { isDefined } from 'twenty-shared/utils'; - import { getMockFrontComponentUrl } from '@/front-components/utils/mockFrontComponent'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useTheme } from '@emotion/react'; +import { t } from '@lingui/core/macro'; +import { useState } from 'react'; import { FrontComponentRenderer as SharedFrontComponentRenderer } from 'twenty-sdk/front-component'; - +import { isDefined } from 'twenty-shared/utils'; type FrontComponentRendererProps = { frontComponentId: string; }; @@ -14,6 +12,7 @@ type FrontComponentRendererProps = { export const FrontComponentRenderer = ({ frontComponentId: _frontComponentId, }: FrontComponentRendererProps) => { + const theme = useTheme(); const [hasError, setHasError] = useState(false); const { enqueueErrorSnackBar } = useSnackBar(); @@ -35,6 +34,7 @@ export const FrontComponentRenderer = ({ return ( diff --git a/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts b/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts index 7147f702c5..781e9b58ee 100644 --- a/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts +++ b/packages/twenty-front/src/modules/front-components/utils/mockFrontComponent.ts @@ -1,8 +1,18 @@ import { isDefined } from 'twenty-shared/utils'; -// eslint-disable-next-line const mockFrontComponentCode = ` -var r=globalThis.React.useState,s=globalThis.React.useEffect;var n=globalThis.jsx,o=globalThis.jsxs,g=globalThis.React.Fragment;var e=globalThis.RemoteComponents,x=()=>{let[a,l]=r(0),[m,p]=r(0);return s(()=>{let t=setInterval(()=>{p(i=>i+1)},1e3);return()=>clearInterval(t)},[]),o(e.HtmlDiv,{style:{padding:"20px",fontFamily:"Arial, sans-serif",maxWidth:"400px",margin:"0 auto",display:"flex",flexDirection:"column",alignItems:"center"},children:[n(e.HtmlH1,{style:{color:"#333",marginBottom:"20px",fontSize:"24px"},children:"Test Component"}),o(e.HtmlP,{style:{fontSize:"18px",marginBottom:"10px",color:"#666"},children:["Count: ",a]}),o(e.HtmlP,{style:{fontSize:"18px",marginBottom:"20px",color:"#666"},children:["Timer: ",m,"s"]}),n(e.HtmlButton,{onClick:()=>l(a+1),style:{padding:"10px 20px",fontSize:"16px",backgroundColor:"#007bff",color:"white",border:"none",borderRadius:"5px",cursor:"pointer",transition:"all 0.3s ease",boxShadow:"0 2px 4px rgba(0, 0, 0, 0.2)"},onMouseEnter:t=>{t.currentTarget.style.backgroundColor="#0056b3",t.currentTarget.style.transform="translateY(-2px)",t.currentTarget.style.boxShadow="0 4px 8px rgba(0, 0, 0, 0.3)"},onMouseLeave:t=>{t.currentTarget.style.backgroundColor="#007bff",t.currentTarget.style.transform="translateY(0)",t.currentTarget.style.boxShadow="0 2px 4px rgba(0, 0, 0, 0.2)"},children:"Increment"})]})},b=globalThis.jsx(x,{});export{b as default}; +const { jsx } = globalThis; +const { TwentyUiButton } = globalThis.RemoteComponents; + +export default jsx(TwentyUiButton, { + variant: 'primary', + disabled: false, + fullWidth: false, + onClick: () => { + console.log('Button clicked'); + }, + title: 'Click me', +}); `; let cachedMockBlobUrl: string | null = null; diff --git a/packages/twenty-sdk/package.json b/packages/twenty-sdk/package.json index 2a9d4975af..be7548f017 100644 --- a/packages/twenty-sdk/package.json +++ b/packages/twenty-sdk/package.json @@ -63,6 +63,7 @@ "jsonc-parser": "^3.2.0", "lodash.camelcase": "^4.3.0", "react": "^18.0.0", + "twenty-ui": "workspace:*", "typescript": "^5.9.2", "uuid": "^13.0.0", "vite": "^7.0.0", diff --git a/packages/twenty-sdk/scripts/remote-dom/generateRemoteDomElements.ts b/packages/twenty-sdk/scripts/remote-dom/generateRemoteDomElements.ts index 823aac42ff..ebaa64215b 100644 --- a/packages/twenty-sdk/scripts/remote-dom/generateRemoteDomElements.ts +++ b/packages/twenty-sdk/scripts/remote-dom/generateRemoteDomElements.ts @@ -1,10 +1,10 @@ -/* eslint-disable no-console */ import * as prettier from '@prettier/sync'; import * as fs from 'fs'; import * as path from 'path'; import { IndentationText, Project, QuoteKind } from 'ts-morph'; import { ALLOWED_HTML_ELEMENTS } from '../../src/front-component-constants/AllowedHtmlElements'; +import { ALLOWED_UI_COMPONENTS } from '../../src/front-component-constants/AllowedUiComponents'; import { COMMON_HTML_EVENTS } from '../../src/front-component-constants/CommonHtmlEvents'; import { EVENT_TO_REACT } from '../../src/front-component-constants/EventToReact'; import { HTML_COMMON_PROPERTIES } from '../../src/front-component-constants/HtmlCommonProperties'; @@ -17,6 +17,7 @@ import { generateRemoteElements, HtmlElementConfigArrayZ, OUTPUT_FILES, + UiComponentConfigArrayZ, } from './generators'; const SCRIPT_DIR = path.dirname(new URL(import.meta.url).pathname); @@ -59,6 +60,28 @@ const getHtmlElementSchemas = (): ComponentSchema[] => { })); }; +const getUiComponentSchemas = (): ComponentSchema[] => { + const result = UiComponentConfigArrayZ.safeParse(ALLOWED_UI_COMPONENTS); + + if (!result.success) { + throw new Error( + `Invalid UI component configuration:\n${formatZodError(result.error)}`, + ); + } + + return result.data.map((component) => ({ + name: component.name, + tagName: component.name, + customElementName: component.tag, + properties: component.properties, + events: COMMON_HTML_EVENTS, + isHtmlElement: false, + htmlTag: undefined, + componentImport: component.componentImport, + componentPath: component.componentPath, + })); +}; + const createProject = (): Project => { return new Project({ manipulationSettings: { @@ -99,8 +122,11 @@ const main = (): void => { console.log('📖 Generating remote DOM elements...\n'); let htmlElements: ComponentSchema[]; + let uiComponents: ComponentSchema[]; + try { htmlElements = getHtmlElementSchemas(); + uiComponents = getUiComponentSchemas(); } catch (error) { console.error('❌ Validation failed:', error); process.exit(1); @@ -113,8 +139,15 @@ const main = (): void => { console.log( ` Events: ${COMMON_HTML_EVENTS.length} common events per element`, ); + + console.log(`\nUI Components: ${uiComponents.length} components`); + console.log( + ` Tags: ${uiComponents.map((component) => component.customElementName).join(', ')}`, + ); console.log(''); + const allComponents = [...htmlElements, ...uiComponents]; + ensureDirectoriesExist(); const project = createProject(); @@ -122,7 +155,7 @@ const main = (): void => { console.log('Host files:'); const hostRegistry = generateHostRegistry( project, - htmlElements, + allComponents, EVENT_TO_REACT, ); writeGeneratedFile( @@ -134,7 +167,7 @@ const main = (): void => { console.log('\nRemote files:'); const remoteElements = generateRemoteElements( project, - htmlElements, + allComponents, HTML_COMMON_PROPERTIES, COMMON_HTML_EVENTS, ); @@ -144,7 +177,7 @@ const main = (): void => { remoteElements.getFullText(), ); - const remoteComponents = generateRemoteComponents(project, htmlElements); + const remoteComponents = generateRemoteComponents(project, allComponents); writeGeneratedFile( REMOTE_GENERATED_DIR, OUTPUT_FILES.REMOTE_COMPONENTS, diff --git a/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts b/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts index f7b5ad672f..64996538b1 100644 --- a/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts +++ b/packages/twenty-sdk/scripts/remote-dom/generators/host-registry.generator.ts @@ -1,5 +1,6 @@ import type { Project, SourceFile } from 'ts-morph'; +import { isDefined } from 'twenty-shared/utils'; import { CUSTOM_ELEMENT_NAMES } from './constants'; import { type ComponentSchema } from './schemas'; import { addFileHeader, addStatement } from './utils'; @@ -65,12 +66,25 @@ const filterProps = (props: Record) => { };`; }; -const generateWrapperComponent = (component: ComponentSchema): string => { +const generateHtmlWrapperComponent = (component: ComponentSchema): string => { return `const ${component.name}Wrapper = ({ children, ...props }: { children?: React.ReactNode } & Record) => { return React.createElement('${component.htmlTag}', filterProps(props), children); };`; }; +const generateUiWrapperComponent = (component: ComponentSchema): string => { + return `const ${component.name}Wrapper = ({ children, ...props }: { children?: React.ReactNode } & Record) => { + return React.createElement(${component.componentImport}, filterProps(props), children); +};`; +}; + +const generateWrapperComponent = (component: ComponentSchema): string => { + if (component.isHtmlElement) { + return generateHtmlWrapperComponent(component); + } + return generateUiWrapperComponent(component); +}; + const generateRegistryMap = (components: ComponentSchema[]): string => { const entries = components .map( @@ -89,6 +103,28 @@ ${entries} ]);`; }; +const groupImportsByPath = ( + components: ComponentSchema[], +): Map => { + const importsByPath = new Map(); + + for (const component of components) { + if ( + !component.isHtmlElement && + isDefined(component.componentPath) && + isDefined(component.componentImport) + ) { + const existing = importsByPath.get(component.componentPath) ?? []; + if (!existing.includes(component.componentImport)) { + existing.push(component.componentImport); + } + importsByPath.set(component.componentPath, existing); + } + } + + return importsByPath; +}; + export const generateHostRegistry = ( project: Project, components: ComponentSchema[], @@ -110,6 +146,15 @@ export const generateHostRegistry = ( namedImports: ['RemoteFragmentRenderer', 'createRemoteComponentRenderer'], }); + const uiImports = groupImportsByPath(components); + + for (const [modulePath, namedImports] of uiImports) { + sourceFile.addImportDeclaration({ + moduleSpecifier: modulePath, + namedImports, + }); + } + addStatement(sourceFile, generateRuntimeUtilities(eventToReactMapping)); for (const component of components) { diff --git a/packages/twenty-sdk/scripts/remote-dom/generators/schemas.ts b/packages/twenty-sdk/scripts/remote-dom/generators/schemas.ts index aaeb4d7c29..c3b230517e 100644 --- a/packages/twenty-sdk/scripts/remote-dom/generators/schemas.ts +++ b/packages/twenty-sdk/scripts/remote-dom/generators/schemas.ts @@ -15,6 +15,20 @@ 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), @@ -22,9 +36,12 @@ export const ComponentSchemaZ = z.object({ properties: z.record(z.string(), PropertySchemaZ), events: z.array(z.string()).readonly(), isHtmlElement: z.boolean(), - htmlTag: z.string().min(1), + htmlTag: z.string().optional(), + componentImport: z.string().optional(), + componentPath: z.string().optional(), }); export type PropertySchema = z.infer; export type HtmlElementConfig = z.infer; +export type UiComponentConfig = z.infer; export type ComponentSchema = z.infer; diff --git a/packages/twenty-sdk/src/front-component-constants/AllowedUiComponents.ts b/packages/twenty-sdk/src/front-component-constants/AllowedUiComponents.ts new file mode 100644 index 0000000000..b9fb719a75 --- /dev/null +++ b/packages/twenty-sdk/src/front-component-constants/AllowedUiComponents.ts @@ -0,0 +1,26 @@ +import { type PropertySchema } from '../front-component/types/PropertySchema'; + +export type AllowedUiComponent = { + tag: string; + name: string; + properties: Record; + 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', + }, +]; diff --git a/packages/twenty-sdk/src/front-component-constants/index.ts b/packages/twenty-sdk/src/front-component-constants/index.ts index 9f5919390c..b5a290afe7 100644 --- a/packages/twenty-sdk/src/front-component-constants/index.ts +++ b/packages/twenty-sdk/src/front-component-constants/index.ts @@ -9,6 +9,8 @@ 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'; diff --git a/packages/twenty-sdk/src/front-component/host/components/FrontComponentRenderer.tsx b/packages/twenty-sdk/src/front-component/host/components/FrontComponentRenderer.tsx index 5e68c903bf..a34a877662 100644 --- a/packages/twenty-sdk/src/front-component/host/components/FrontComponentRenderer.tsx +++ b/packages/twenty-sdk/src/front-component/host/components/FrontComponentRenderer.tsx @@ -2,19 +2,24 @@ import { type RemoteReceiver, RemoteRootRenderer, } from '@remote-dom/react/host'; -import React, { useState } from 'react'; +import { useState } from 'react'; import { isDefined } from 'twenty-shared/utils'; + +import { ThemeProvider } from '@emotion/react'; +import { type ThemeType } from 'twenty-ui/theme'; import { FrontComponentWorkerEffect } from '../../remote/components/FrontComponentWorkerEffect'; import { componentRegistry } from '../generated/host-component-registry'; type FrontComponentContentProps = { componentUrl: string; onError: (error?: Error) => void; + theme: ThemeType; }; export const FrontComponentRenderer = ({ componentUrl, onError, + theme, }: FrontComponentContentProps) => { const [receiver, setReceiver] = useState(null); @@ -27,10 +32,12 @@ export const FrontComponentRenderer = ({ /> {isDefined(receiver) && ( - + + + )} ); diff --git a/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts b/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts index f39b002ff0..d3cdb969d5 100644 --- a/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts +++ b/packages/twenty-sdk/src/front-component/host/generated/host-component-registry.ts @@ -12,6 +12,7 @@ import { RemoteFragmentRenderer, createRemoteComponentRenderer, } from '@remote-dom/react/host'; +import { Button } from 'twenty-ui/input'; const INTERNAL_PROPS = new Set(['element', 'receiver', 'components']); const EVENT_NAME_MAP: Record = { @@ -345,6 +346,12 @@ const HtmlHrWrapper = ({ }: { children?: React.ReactNode } & Record) => { return React.createElement('hr', filterProps(props), children); }; +const TwentyUiButtonWrapper = ({ + children, + ...props +}: { children?: React.ReactNode } & Record) => { + return React.createElement(Button, filterProps(props), children); +}; type ComponentRegistryValue = | ReturnType | typeof RemoteFragmentRenderer; @@ -393,5 +400,6 @@ export const componentRegistry: Map = new Map([ ['html-td', createRemoteComponentRenderer(HtmlTdWrapper)], ['html-br', createRemoteComponentRenderer(HtmlBrWrapper)], ['html-hr', createRemoteComponentRenderer(HtmlHrWrapper)], + ['twenty-ui-button', createRemoteComponentRenderer(TwentyUiButtonWrapper)], ['remote-fragment', RemoteFragmentRenderer], ]); diff --git a/packages/twenty-sdk/src/front-component/index.ts b/packages/twenty-sdk/src/front-component/index.ts index 21d35f5940..32c9b1c6ae 100644 --- a/packages/twenty-sdk/src/front-component/index.ts +++ b/packages/twenty-sdk/src/front-component/index.ts @@ -54,6 +54,7 @@ export { HtmlTd, HtmlBr, HtmlHr, + TwentyUiButton, } from './remote/generated/remote-components'; export type { HtmlCommonProperties, @@ -69,6 +70,7 @@ export type { HtmlButtonProperties, HtmlThProperties, HtmlTdProperties, + TwentyUiButtonProperties, } from './remote/generated/remote-elements'; export { HtmlDivElement, @@ -114,6 +116,7 @@ export { HtmlTdElement, HtmlBrElement, HtmlHrElement, + TwentyUiButtonElement, RemoteRootElement, RemoteFragmentElement, } from './remote/generated/remote-elements'; diff --git a/packages/twenty-sdk/src/front-component/remote/generated/remote-components.ts b/packages/twenty-sdk/src/front-component/remote/generated/remote-components.ts index b2dec29bea..5b3014ceac 100644 --- a/packages/twenty-sdk/src/front-component/remote/generated/remote-components.ts +++ b/packages/twenty-sdk/src/front-component/remote/generated/remote-components.ts @@ -52,6 +52,7 @@ import { HtmlTdElement, HtmlBrElement, HtmlHrElement, + TwentyUiButtonElement, } from './remote-elements'; export const HtmlDiv = createRemoteComponent('html-div', HtmlDivElement, { @@ -1126,3 +1127,31 @@ export const HtmlHr = createRemoteComponent('html-hr', HtmlHrElement, { onDrag: { event: 'drag' }, }, }); +export const TwentyUiButton = createRemoteComponent( + 'twenty-ui-button', + TwentyUiButtonElement, + { + eventProps: { + onClick: { event: 'click' }, + onDblclick: { event: 'dblclick' }, + onMousedown: { event: 'mousedown' }, + onMouseup: { event: 'mouseup' }, + onMouseover: { event: 'mouseover' }, + onMouseout: { event: 'mouseout' }, + onMouseenter: { event: 'mouseenter' }, + onMouseleave: { event: 'mouseleave' }, + onKeydown: { event: 'keydown' }, + onKeyup: { event: 'keyup' }, + onKeypress: { event: 'keypress' }, + onFocus: { event: 'focus' }, + onBlur: { event: 'blur' }, + onChange: { event: 'change' }, + onInput: { event: 'input' }, + onSubmit: { event: 'submit' }, + onScroll: { event: 'scroll' }, + onWheel: { event: 'wheel' }, + onContextmenu: { event: 'contextmenu' }, + onDrag: { event: 'drag' }, + }, + }, +); diff --git a/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts b/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts index cdcda20e06..254606d588 100644 --- a/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts +++ b/packages/twenty-sdk/src/front-component/remote/generated/remote-elements.ts @@ -608,6 +608,31 @@ export const HtmlHrElement = createRemoteElement< properties: HTML_COMMON_PROPERTIES_CONFIG, events: [...HTML_COMMON_EVENTS_ARRAY], }); + +export type TwentyUiButtonProperties = HtmlCommonProperties & { + variant?: string; + accent?: string; + size?: string; + disabled?: boolean; + fullWidth?: boolean; +}; + +export const TwentyUiButtonElement = createRemoteElement< + TwentyUiButtonProperties, + Record, + Record, + HtmlCommonEvents +>({ + properties: { + ...HTML_COMMON_PROPERTIES_CONFIG, + variant: { type: String }, + accent: { type: String }, + size: { type: String }, + disabled: { type: Boolean }, + fullWidth: { type: Boolean }, + }, + events: [...HTML_COMMON_EVENTS_ARRAY], +}); customElements.define('html-div', HtmlDivElement); customElements.define('html-span', HtmlSpanElement); customElements.define('html-section', HtmlSectionElement); @@ -651,6 +676,7 @@ customElements.define('html-th', HtmlThElement); customElements.define('html-td', HtmlTdElement); customElements.define('html-br', HtmlBrElement); customElements.define('html-hr', HtmlHrElement); +customElements.define('twenty-ui-button', TwentyUiButtonElement); customElements.define('remote-root', RemoteRootElement); customElements.define('remote-fragment', RemoteFragmentElement); @@ -700,6 +726,7 @@ declare global { 'html-td': InstanceType; 'html-br': InstanceType; 'html-hr': InstanceType; + 'twenty-ui-button': InstanceType; 'remote-root': InstanceType; 'remote-fragment': InstanceType; } diff --git a/packages/twenty-ui/project.json b/packages/twenty-ui/project.json index 7605e1044d..adb758f9b6 100644 --- a/packages/twenty-ui/project.json +++ b/packages/twenty-ui/project.json @@ -4,7 +4,7 @@ "sourceRoot": "packages/twenty-ui/src", "projectType": "library", "tags": [ - "scope:frontend" + "scope:shared" ], "targets": { "build": { diff --git a/yarn.lock b/yarn.lock index db4de530ed..9cdb9e8c23 100644 --- a/yarn.lock +++ b/yarn.lock @@ -57675,6 +57675,7 @@ __metadata: react: "npm:^18.0.0" ts-morph: "npm:^25.0.0" tsx: "npm:^4.7.0" + twenty-ui: "workspace:*" typescript: "npm:^5.9.2" uuid: "npm:^13.0.0" vite: "npm:^7.0.0"