Resumen
Los componentes de frontend son componentes de React que se renderizan directamente dentro de la UI de Twenty. Se ejecutan en un Web Worker aislado usando Remote DOM: tu código se ejecuta dentro de un iframe de origen opaco y aislado (sandboxed), pero su interfaz de usuario sigue renderizándose de forma nativa en la página en lugar de quedar confinada a ese iframe.Dónde se pueden usar los componentes de front
Los componentes de front pueden renderizarse en cuatro ubicaciones dentro de Twenty:- Panel lateral — Los componentes de front no headless se abren en el panel lateral derecho. Este es el comportamiento predeterminado cuando un componente de front se activa desde el menú de comandos.
- Widgets (tableros y páginas de registros) — Los componentes de front pueden incrustarse como widgets dentro de los diseños de página. Al configurar un tablero o el diseño de una página de registro, los usuarios pueden agregar un widget de componente de front.
- Configuración de la aplicación — Señalada por una declaración de
defineSettingsMenuItem(), el componente de frontend renderiza la página detrás de uno de los elementos del menú de configuración de la aplicación. Una aplicación puede declarar varios. - Chat de IA — Una función lógica expuesta como herramienta de IA puede nombrar un componente de front, que luego renderiza las llamadas de esa herramienta integradas en el chat en lugar de la fila de pasos predeterminada.
- Emparejarlo con un elemento del menú de comandos: lo registra en el menú de comandos (Cmd+K) y, de forma opcional, como una acción rápida fijada.
- Incrustarlo como widget en un diseño de página: lo coloca en la página de detalles de un registro o en un tablero.
- Señala con una
defineSettingsMenuItem()— la renderiza detrás de uno de los elementos del menú de configuración de la aplicación. - Nómbralo desde los ajustes de
toolTriggerSettingsde una función lógica — renderiza las llamadas de la herramienta en el chat de IA.
Ejemplo básico
La forma más rápida de ver un componente de front en acción es emparejarlo con undefineCommandMenuItem, de modo que aparezca como un botón de acción rápida en la esquina superior derecha de la página:
src/front-components/hello-world.tsx
src/command-menu-items/hello-world.command-menu-item.ts
twenty app dev (o ejecutar una sola vez twenty app apply), la acción rápida aparece en la esquina superior derecha de la página:

Campos de configuración
Colocar un componente de frontend en una página
Más allá de los comandos, puedes incrustar un componente de frontend directamente en una página de registro agregándolo como un widget en un diseño de página. Consulta Diseños de página para más detalles.widgetAvailability decide qué selector de widgets ofrece el componente mientras se edita un diseño de página. Un componente front se crea para páginas de registro, donde funciona con el registro mostrado, o para paneles, por lo que declara un pageLayoutType:
widgetAvailability para un componente que solo se abre desde un comando, un elemento del menú de configuración o una fila de línea de tiempo. Nunca limita dónde se muestra un componente: los comandos, los elementos del menú de configuración y los diseños de página que declara su aplicación lo renderizan independientemente de su valor. La compilación advierte sobre un componente visible que no declara ninguno y no se abre desde ningún otro lugar.
Elementos del menú de configuración
Una aplicación puede añadir sus propias entradas al menú de configuración, cada una de las cuales renderiza un componente de frontend. Declara uno condefineSettingsMenuItem() en su propio archivo, apuntando al componente que lo renderiza:
src/settings-menu-items/sync-settings.ts
position.
Declara tantos como necesites. El elemento propio General de Twenty — actualización automática, URL de la aplicación y conexiones — siempre aparece primero y no puede ser anulado por una aplicación; la URL de la aplicación solo se muestra cuando la aplicación tiene funciones activadas por HTTP.
Campos
position es un decimal, por lo que se puede insertar un elemento entre dos existentes sin renumerarlos. Los elementos se ordenan por position ascendente, y los empates se resuelven mediante universalIdentifier, de modo que el orden es el mismo en todos los espacios de trabajo. Dos elementos de una aplicación no pueden compartir una posición en el mismo ámbito: la sincronización lo rechaza en lugar de permitir que el orden de instalación decida cuál se renderiza primero.
URL de las pestañas
La pestaña de cada elemento se identifica mediante un hash en la URL de la página, por lo que puedes enlazar directamente a ella; por ejemplo,/settings/applications/\<applicationId>#sync o /settings/app-preferences/\<applicationId>#sync. Establece slug para elegir ese hash; sin uno, la pestaña se identifica mediante el universalIdentifier del elemento. Añadir un slug más adelante mantiene funcionando los enlaces que usan el universalIdentifier: abren la misma pestaña.
Un slug está en kebab-case en minúsculas (letras, dígitos y guiones simples, como sync o sync-settings) y tiene como máximo 64 caracteres. general y variables están reservados para las pestañas integradas, se rechaza un slug con forma de UUID porque podría ocultar el enlace de otra pestaña y dos elementos de una aplicación no pueden compartir un slug, ni siquiera entre ámbitos. Tanto la compilación como la sincronización rechazan un slug que incumpla estas reglas.
Después de que un miembro conecte o vuelva a conectar una cuenta en Configuración > Preferencias de aplicaciones, llega a la primera pestaña de elemento USER de la aplicación, según el orden de position, o a General cuando la aplicación no declara ninguna.
Ámbito de espacio de trabajo y de usuario
scope indica quién configura un elemento:
WORKSPACE(predeterminado) — se configura una vez para todo el espacio de trabajo, junto con las demás configuraciones del espacio de trabajo. Estos son los elementos que se renderizan como pestañas en la página de la aplicación.USER— cada miembro lo configura para sí mismo, en su propia configuración de usuario.
Un elemento
USER se muestra como una pestaña en la página de la aplicación en Configuración > Preferencias
de la aplicación, donde solo lo ve el miembro que inició sesión y únicamente cuando su
rol concede el permiso User Apps. Esa página todavía no está habilitada en
todos los espacios de trabajo: donde no lo está, un elemento USER se conserva y sincroniza,
pero no hay nada que lo muestre.USER se ejecuta como el miembro que lo visualiza. Para almacenar algo por persona, declara la variable con scope: 'USER': getApplicationVariable devuelve el valor del miembro que está visualizando y tu componente lo guarda con updateMyUserApplicationVariable, que solo escribe el valor de ese miembro y necesita SystemPermissionFlag.USER_APPS en el rol de tu aplicación, si declara uno (consulta Lectura y actualización de variables en tiempo de ejecución). Al igual que un elemento WORKSPACE, un elemento USER reemplaza el formulario generado Variables, por lo que debe permitir que el miembro establezca todas las variables de usuario que necesita su aplicación. Una variable de espacio de trabajo contiene un valor compartido por todos, por lo que el guardado de un miembro sobrescribiría el de otro.
defineSettingsFrontComponent() está obsoleta en favor de
defineSettingsMenuItem(). Una aplicación que todavía la usa sigue funcionando — su
componente se trata como un único elemento de menú Configuración — pero solo puede
declarar uno, una limitación que defineSettingsMenuItem() existe para eliminar.Headless vs no headless
Los componentes de front vienen en dos modos de renderizado controlados por la opciónisHeadless:
No headless (predeterminado) — El componente renderiza una UI visible. Cuando se activa desde el menú de comandos, se abre en el panel lateral. Este es el comportamiento predeterminado cuando isHeadless es false o se omite.
Headless (isHeadless: true) — El componente se monta de forma invisible en segundo plano. No abre el panel lateral. Los componentes headless están diseñados para acciones que ejecutan lógica y luego se desmontan — por ejemplo, ejecutar una tarea asíncrona, navegar a una página o mostrar un modal de confirmación. Se combinan de forma natural con los componentes Command del SDK descritos a continuación.
src/front-components/sync-tracker.tsx
null, Twenty omite renderizar un contenedor para él — no aparece espacio vacío en el diseño. El componente sigue teniendo acceso a todos los hooks y a la API de comunicación con el host.
Componentes Command del SDK
El paquetetwenty-sdk proporciona cuatro componentes auxiliares Command diseñados para componentes de front headless. Cada componente ejecuta una acción al montarse, gestiona los errores mostrando una notificación tipo snackbar y desmonta automáticamente el componente de front al finalizar.
Impórtalos desde twenty-sdk/front-component:
Command— Ejecuta un callback asíncrono mediante la propexecute.CommandLink— Navega a una ruta de la aplicación. Props:to,params,queryParams,options.CommandModal— Abre un modal de confirmación. Si el usuario confirma, ejecuta el callbackexecute. Props:title,subtitle,execute,confirmButtonText,confirmButtonAccent.CommandOpenSidePanelPage— Abre contenido en el panel lateral. Las páginas enrutadas usan las mismas propiedades tipadasto,paramsyqueryParamsqueCommandLink, además de las propiedades opcionaleshash,pageTitleyresetNavigationStack. El registro de rutas del espacio de trabajo determina si elAppPathsolicitado admite el panel lateral.pageTitlereemplaza el título del encabezado del panel, mientras queresetNavigationStackborra las páginas anteriores del panel antes de abrir la ruta. Las herramientas del panel diseñadas específicamente usan sus propias props específicas de página.
Command para ejecutar una acción desde el menú de comandos:
src/front-components/run-action.tsx
src/command-menu-items/run-action.command-menu-item.ts
CommandModal para pedir confirmación antes de ejecutar:
src/front-components/delete-draft.tsx
CommandOpenSidePanelPage para abrir el registro actual en el panel lateral en una pestaña específica. hash es un id de pestaña de diseño de página sin el # inicial (los diseños predeterminados usan ids como company-tab-emails o company-tab-timeline; los diseños personalizados usan el id propio de la pestaña). Si el id no existe en el diseño del registro, en su lugar se abre la pestaña predeterminada:
src/front-components/open-company-emails.tsx
Llamar a una función de lógica
Los componentes de front se ejecutan en el navegador dentro de un Web Worker aislado (sandboxed) dentro de un iframe de origen opaco, mientras que las funciones de lógica se ejecutan en el servidor. No hay una llamada directa en el mismo proceso entre ambos; en su lugar, un componente de front accede a una función de lógica a través de HTTP. Una función de lógica declarada conhttpRouteTriggerSettings es accesible por HTTP en su ruta. RestApiClient trata las rutas que comienzan con /s/ como rutas de la aplicación, las resuelve a la URL desde la que se sirven tus funciones y las autentica con TWENTY_APP_ACCESS_TOKEN.
En Twenty Cloud, las funciones de lógica activadas por HTTP se sirven en un dominio dedicado por espacio de trabajo en https://\<your-workspace-subdomain>.withtwenty.com\<path>. Para clientes externos, copia la URL exacta desde la configuración de HTTP trigger de la función o desde la pestaña Settings de la aplicación.
Un componente de front sin interfaz (headless) puede ejecutar la llamada al montar mediante el componente Command y luego desmontarse automáticamente:
src/front-components/sync-prs.tsx
RestApiClient es el httpRouteTriggerSettings.path de la función lógica, con el prefijo /s. Mantén isAuthRequired: true; el TWENTY_APP_ACCESS_TOKEN que Twenty crea para tu componente autentica la solicitud:
src/logic-functions/fetch-prs.logic-function.ts
TWENTY_APP_ACCESS_TOKEN se inyecta automáticamente — consulta Variables
de la aplicación. Dado que las variables de aplicación secretas nunca se exponen a los componentes de front, mantén las claves de API y otra lógica confidencial en la función de lógica, no en el componente de front.Llamar a la API REST de Twenty
Para llamar a rutas HTTP de la aplicación o leer y escribir registros de Twenty desde un componente de interfaz, utilizaRestApiClient de twenty-client-sdk/rest. Envía las rutas /s/... a la URL base de las funciones de tu espacio de trabajo y cualquier otra ruta, incluidas /rest/..., a TWENTY_API_URL.
Siempre actúa como la persona que está mirando la página. runAs: 'application' es una opción exclusiva de las funciones lógicas: un componente nunca recibe el propio token de tu aplicación, por lo que solicitarlo aquí genera un error. Coloca el trabajo que necesita el acceso propio de la aplicación detrás de una función lógica y llama a esa en su lugar.
options acepta headers, query (un registro de parámetros de cadena de consulta; los valores nulos o indefinidos se omiten) y un AbortSignal mediante signal. Un objeto body que no sea de tipo FormData se serializa automáticamente como JSON. Ante un 401, el cliente actualiza el token de acceso una vez a través del host y vuelve a intentar la solicitud.
La URL base y el token se resuelven desde el entorno de forma predeterminada. Pasa opciones de sobrescritura al constructor cuando sea necesario — por ejemplo, en pruebas:
RestApiClientError que expone status, statusText, url y el body analizado:
Publicación en la bandeja de entrada
sendInboxMessage y los asistentes de menciones de la bandeja de entrada también se exportan desde twenty-sdk/front-component, por lo que un componente puede publicar un mensaje sin una función de lógica intermedia. El mensaje procede de tu aplicación y la llamada incluye el token del componente, por lo que el miembro que consulta la página necesita el permiso AI, como todos los destinatarios.
Para mostrar la conversación después, navega hasta ella con el threadId que devuelve la llamada:
workspaceMemberIdsacepta los IDs de miembros del espacio de trabajo, no el ID de usuario que devuelveuseUserId().- Una conversación se abre solo para sus participantes, así que navega hasta ella cuando el miembro que consulta la página sea uno de los destinatarios.
- La bandeja de entrada es una ruta de página principal:
navigatela abre allí yopenSidePanelPageno puede abrirla en el panel lateral.
Acceder al contexto de ejecución
Dentro de tu componente, usa hooks del SDK para acceder al usuario actual, el registro y la instancia del componente:src/front-components/record-info.tsx
Metadatos del objeto
UseuseSelectedObjectMetadata() para identificar el objeto asociado a un widget de página de registro, una fila de cronología personalizada, un comando con ámbito de objeto o un componente de interfaz abierto en el panel lateral con un objeto (desde un comando o mediante openSidePanelPage con un objectNameSingular). Esto permite que una integración seleccione plantillas u otra configuración para el objeto actual sin buscar un ID de registro entre objetos.
id (el UUID de metadatos del objeto del espacio de trabajo), nameSingular y namePlural. Los nombres son nombres de metadatos, como person y people, en lugar de etiquetas de visualización.
Los metadatos del objeto pueden estar disponibles incluso cuando no hay registros seleccionados. Pertenece al contexto de este componente de interfaz, por lo que un panel lateral puede mostrar un objeto diferente del de la página principal. El hook se actualiza cuando cambia ese contexto y devuelve null cuando el objeto no se puede resolver, cuando no se proporciona ningún contexto de objeto o cuando se ejecuta en un host más antiguo que no proporciona metadatos de objetos.
Ubicación del host
UsauseLocation() para leer la ruta que aloja tu componente frontal. Devuelve un objeto FrontComponentLocation con cuatro campos de cadena obligatorios: pathname, search, hash y origin. Por ejemplo, una página en https://myworkspace.twenty.com/page/page-layout-id?project=abc#details proporciona /page/page-layout-id, ?project=abc, #details y https://myworkspace.twenty.com, respectivamente. hash incluye su # inicial y es '' cuando no hay ningún fragmento.
pathname, search y hash siguen el contexto de React Router del componente, incluida la propia ruta de un panel lateral con ruta. origin proviene de la aplicación host. El hook se actualiza cuando cambia la ruta. Los hosts más antiguos pueden omitir executionContext.location, por lo que useLocation() devuelve null cuando la ubicación no está disponible.
Para los parámetros de consulta, lee useLocation()?.search ?? '' y analiza el resultado con URLSearchParams. search incluye su ? inicial y es '' cuando la ruta no tiene cadena de consulta. El valor de reserva también proporciona '' cuando el host no proporciona la ubicación.
Variables de aplicación
Las variables de aplicación definidas endefineApplication() con isSecret: false están disponibles dentro de los componentes de front mediante la utilidad getApplicationVariable:
src/front-components/greeting.tsx
getApplicationVariable siempre devuelve una cadena (o undefined), independientemente del type declarado de la variable. La cadena se serializa de forma coherente según el tipo (booleanos como "true" / "false", números como cadenas decimales, arrays / objetos como JSON), el mismo formato que se usa para la función lógica process.env — parsea tú mismo (Number(...), JSON.parse(...), === 'true'). Consulta Tipos de variables.
Lectura y actualización de variables en tiempo de ejecución
getApplicationVariable devuelve los valores inyectados cuando se cargó el componente, por lo que un valor escrito posteriormente solo aparece en la siguiente carga. Para leer los valores almacenados o escribir uno (lo que hace un elemento del menú de configuración cuando guarda lo que escribió el usuario), usa MetadataApiClient:
src/front-components/app-settings.tsx
- El token de tu componente está vinculado a tu propia aplicación.
updateOneApplicationVariablela selecciona como destino, por lo queapplicationIdes opcional y se rechaza nombrar otra aplicación. La lectura de lasapplicationVariablesde otra aplicación se rechaza de la misma manera, y una aplicación que pertenece a otro espacio de trabajo se interpreta como no encontrada. - La clave debe ser una que tu aplicación declare en
defineApplication(). Escribir una clave que no está declarada se considera no encontrada. - Las variables secretas se leen enmascaradas, nunca como el valor almacenado. Escribirlas funciona normalmente.
- La lectura del
valuede una variable y su escritura requieren el permiso de configuración de Workspace Apps tanto en el rol de tu aplicación como en el rol de la persona que inició sesión, ya que el token del componente es la intersección de ambos. AñadeSystemPermissionFlag.WORKSPACE_APPSapermissionFlagUniversalIdentifiersen tu rol de aplicación. - Una variable de usuario (
scope: 'USER') se inyecta con el valor del miembro que está viendo la página, a menos que sea secreta.updateOneApplicationVariablelo rechaza: guárdelo conupdateMyUserApplicationVariable, como lo hacesaveMyLanguageanteriormente. Escribe únicamente el valor del miembro que está viendo la página y no permite indicar otra aplicación. No necesita permiso de Workspace Apps, pero necesita el permiso User Apps en el rol del miembro que está visualizando y en el rol de tu aplicación, si declara uno: añadeSystemPermissionFlag.USER_APPSapermissionFlagUniversalIdentifiersen tu rol de aplicación. Consulte Variables de espacio de trabajo y de usuario.
Variables del sistema
Las siguientes variables de sistema siempre están disponibles a través deprocess.env:
TWENTY_FUNCTIONS_URL
Twenty también inyecta TWENTY_FUNCTIONS_URL en los componentes de interfaz y en las funciones lógicas: la URL base desde la que se sirven las funciones de lógica activadas por HTTP de tu aplicación.
Existe porque esa URL no siempre es el propio servidor de Twenty. En Twenty Cloud, las rutas de la aplicación se sirven en un dominio dedicado por espacio de trabajo (https://\<your-workspace-subdomain>.withtwenty.com, o el dominio público principal de la aplicación cuando se configure uno) para que las respuestas definidas por la aplicación se ejecuten en su propio origen en lugar de en el origen de la aplicación de Twenty. Las instancias autoalojadas y locales sirven las rutas de la aplicación bajo el prefijo /s en el propio servidor y es posible que no establezcan la variable en absoluto. Dado que la URL base varía por espacio de trabajo y por instancia, tu código no puede codificarla de forma rígida: el servidor inyecta el valor correcto en tiempo de ejecución.
Rara vez necesitas leerla directamente. Llama a tus rutas a través de RestApiClient con una ruta con el prefijo /s/ y el cliente resuelve la URL por ti: elimina el prefijo /s y apunta a TWENTY_FUNCTIONS_URL, recurriendo a \<TWENTY_API_URL>/s cuando la variable no está establecida. Utiliza resolveUrl('/s/\<path>') para obtener la URL absoluta sin enviar una solicitud, por ejemplo, para un enlace. Lee la variable directamente solo cuando construyas una URL manualmente:
API de comunicación con el host
Los componentes de frontend pueden activar la navegación, los modales y las notificaciones mediante funciones detwenty-sdk/front-component:
Use
openCommandConfirmationModal de twenty-sdk/front-component para los cuadros de diálogo. Acepta opciones estructuradas de título, subtítulo y botón, y se resuelve como 'confirm' o 'cancel'. Twenty se encarga del modal, la gestión del foco, el cierre y el desplazamiento de la página. Los componentes de frontend no deben renderizar directamente elementos Dialog, AlertDialog, dialog nativos ni ventanas emergentes nativas del navegador. Proporcionar un contenedor de portal local no implica que se admita la modalidad directa propiedad de la aplicación.
Aquí tienes un ejemplo que usa la API del host para mostrar un snackbar y cerrar el panel lateral después de que una acción finaliza:
src/front-components/archive-record.tsx
Almacenamiento
localStorage y sessionStorage funcionan como lo hacen en una página normal, con la API síncrona estándar. Tus claves están limitadas al ámbito de la instalación de tu aplicación y del usuario que ha iniciado sesión: ninguna otra aplicación puede leerlas, y otro usuario que inicie sesión en el mismo navegador comenzará con un almacenamiento vacío. Los valores escritos en localStorage permanecen en el dispositivo a través de las recargas; sessionStorage dura mientras dure la sesión del navegador.
src/front-components/note-draft.tsx
QuotaExceededError, como la API del navegador.
Trabajar con varios registros
UsauseSelectedRecordIds() para manejar varios registros seleccionados. Esto es útil para operaciones por lotes:
src/front-components/bulk-export.tsx
src/command-menu-items/bulk-export.command-menu-item.ts
Seleccionar todo
Cuando el usuario selecciona todos los registros de una vista,useSelectedRecordIds() devuelve una matriz vacía: la selección se define mediante los filtros, la búsqueda y las filas no marcadas de la vista, y puede contener muchos más registros de los que caben en la memoria. useSelectedRecordsFilter() devuelve un filtro que coincide exactamente con los registros seleccionados en todos los casos, por lo que realiza consultas con él en lugar de enumerar los ID:
Recursos públicos
Los componentes de frontend pueden acceder a archivos del directoriopublic/ de la aplicación usando getPublicAssetUrl:
Dependencias compartidas en los componentes frontales
Por defecto, cada componente frontal agrupa su propia copia de las librerías que importa, por lo que una aplicación con cinco componentes viene React cinco veces. Declara las dependencias compartidas en el paquetepackage.json para construir esas librerías una vez y tener cada componente de la aplicación cargarlas desde un solo archivo caché:
package.json
src/front-components/counter.tsx
- Un paquete de dependencias compartidas por aplicación. El paquete se construye a partir de las propias dependencias de tu aplicación, por lo que conservas el control total de las versiones que envías.
- Enumera los especificadores exactos que importas.
twenty-ui/primitives/inputytwenty-ui/primitives/data-displayson dos entradas; el nombre de un paquete por sí solo no cubre sus subrutas. La listareactcubre automáticamentereact/jsx-runtime. - Comparte
react-dom/clientjunto areact. Cada componente renderiza a través decreateRoot, así que dejarlo fuera significa que cada componente todavía incluye React DOM. - **El paquete está en caché. * Se sirve bajo una URL de contenido hash con un caché inmutable de larga duración, para que se descargue una vez y se vuelva a utilizar a través de todos los componentes de la aplicación hasta que una de sus dependencias cambie.
- Los componentes que no importan ninguno de los paquetes compartidos nunca lo descargan.
Estilo
Los componentes de frontend admiten varios enfoques de estilos. Puedes usar:- Estilos en línea —
style={{ color: 'red' }} - Componentes de UI de Twenty: la propia biblioteca de componentes de Twenty; consulta Uso de los componentes de UI de Twenty más abajo
- Emotion — CSS-in-JS con
@emotion/react - Styled-components — patrones de
styled.div - Tailwind CSS — clases utilitarias
- Cualquier librería CSS-in-JS compatible con React
Uso de los componentes de UI de Twenty
Twenty distribuye su biblioteca de componentes como el paquetetwenty-ui. Prefiere sus controles y tokens de tema para dar a los componentes front la misma apariencia e interacciones que el resto de Twenty. Explora los componentes compartidos y las primitivas para ver ejemplos, props y API de composición.
Instalación
Las nuevas aplicaciones creadas contwenty app init ya incluyen twenty-ui. Para una aplicación existente, añádelo como dependencia de desarrollo, fijado a la misma versión que twenty-sdk y twenty-client-sdk. Por ejemplo, si tus paquetes SDK usan 2.51.0:
twenty-ui se incluye en el momento de la compilación, ya sea en cada componente front o en el paquete de dependencias compartidas de tu aplicación. Twenty carga las hojas de estilo de los componentes y del tema, y aplica el esquema de color activo del espacio de trabajo. No necesitas importar twenty-ui/style.css, las hojas de estilo del tema ni añadir un ThemeProvider raíz como lo harías en una aplicación independiente de React.
Importar componentes
Importa desde la subruta pública de la familia correspondiente para mantener las dependencias centradas: Los controles fundamentales se agrupan entwenty-ui/primitives/\<family>. Los preajustes de diseño compartidos y los bloques de creación de aplicaciones se agrupan en twenty-ui/components/\<family>. El punto de entrada agregado twenty-ui/components sigue disponible, pero las importaciones por familia facilitan compartir solo las dependencias que usas.
Usa
MainButton para acciones destacadas y LightButton para acciones ligeras. Usa Button, Field, Input, Select y otras primitivas de twenty-ui/primitives/input para controles y formularios fundamentales. Sigue las partes documentadas de cada control compuesto, como Field.Root, Field.Label y Field.Description.
Pasa alignItemWithTrigger={false} a Select.Positioner. Alinear el elemento seleccionado con el desencadenador, el valor predeterminado de Base UI, necesita mediciones de diseño síncronas que el entorno aislado no puede proporcionar, por lo que la ventana emergente se contrae al volver a abrirse.
Usa Dropdown de twenty-ui/components/navigation para menús de acciones, selectores de opciones y paneles de formularios no modales. Usa enqueueSnackbar de twenty-sdk/front-component para las notificaciones del anfitrión.
Las API independientes Dialog y AlertDialog también son exportadas por Twenty UI, pero se prohíbe su uso directo en los componentes de frontend. Use la API de comunicación del host para los cuadros de diálogo.
Estos puntos de entrada no requieren Monaco. El punto de entrada independiente twenty-ui/components/code-editor depende de Monaco, cuyo cargador de scripts no es compatible con el sandbox de componentes frontales, por lo que CodeEditor no puede volverse interactivo allí.
Iconos
Importa iconos individuales desdetwenty-ui/icon:
twenty-ui/icon/provider (IconsProvider, useIcons y el Icon por nombre) en los componentes front. Los componentes de Front se empaquetan sin división de código, por lo que el catálogo dinámico de iconos del proveedor (el conjunto completo de Tabler, aproximadamente 2 MB minificado) termina en tu paquete. Consulta la referencia de iconos para ver las exportaciones disponibles y las props compatibles.
Temas y tokens de tema
Los componentes de UI de Twenty coinciden automáticamente con el tema claro y oscuro del espacio de trabajo. El renderizador aplica el esquema de color activo en el anfitrión, y los componentes resuelven sus variables CSS con respecto a él. Para usar los mismos tokens de diseño en tus propios estilos en línea, llama auseTheme() dentro del cuerpo del componente. En los componentes frontales, devuelve referencias de variables CSS de forma predeterminada, por lo que los estilos siguen el tema activo del anfitrión sin un ThemeProvider local:
themeCssVariables desde twenty-ui/theme. Léelas dentro del cuerpo del componente: la CLI simula twenty-ui durante la extracción del manifiesto, por lo que desreferenciar constantes de tema importadas en el ámbito del módulo puede provocar un error de compilación.
Las cadenas de referencias CSS no son números de JavaScript resueltos, incluso para tokens como theme.icon.size.md. No realices operaciones aritméticas con ellas ni las pases a API que requieran números. Consulta Temas y Tokens para ver las rutas semánticas y los nombres de variables CSS.
Para hacer bifurcaciones explícitamente según el esquema activo, léelo con useColorScheme() de twenty-sdk/front-component, que devuelve 'light' o 'dark'.
Limitaciones actuales
Los componentes de Front están en desarrollo activo. El renderizado, el estilo, el manejo de eventos, la medición de elementos, los portales del cuerpo y el almacenamiento del navegador funcionan bien. Cualquier cosa que vaya más allá de eso (llamar a un método del DOM en un ref, por ejemplo) falta o está incompleta hoy, y la mayoría falla silenciosamente: no hay excepción ni error de TypeScript tampoco, ya que el andamiaje está tipado contra el DOM completo del navegador. Si una de estas cosas te bloquea, abre una incidencia para que se priorice.Diseño y medición
Los elementos pueden medirse a sí mismos: el host refleja la geometría en el sandbox, de modo que las lecturas se responden localmente pero pueden tener hasta un fotograma de desfase, y la primera lectura de un elemento nunca medido devuelve ceros. Después de escribir, vuelve a leer en un callback derequestAnimationFrame o en un efecto.
El posicionamiento desde
getBoundingClientRect y la observación de tamaño mediante ResizeObserver funcionan desde el mismo espejo, por lo que las devoluciones de llamada de redimensionamiento se retrasan respecto al host aproximadamente un fotograma. Aun así, prefiere usar CSS para el diseño: tu hoja de estilos llega a la página real, por lo que flexbox, grid, aspect-ratio, clamp() y @container se comportan con normalidad, sin desfase de fotogramas.
matchMedia() evalúa width, height, orientation, -webkit-device-pixel-ratio, resolution, hover, pointer y prefers-color-scheme, en la forma min-/max-, la forma de intervalo de nivel 4 ((width >= 600px), (400px \<= width \<= 800px)) y la forma sin valor ((width)). Los anchos, las alturas y orientation describen la caja de tu widget, no la ventana gráfica del navegador, por lo que siguen document.body.clientWidth en lugar de window.innerWidth, y em y rem equivalen a 16px. hover y pointer describen la entrada principal del dispositivo del usuario. Cualquier otra condición se considera desconocida, como los navegadores tratan las características desconocidas: el elemento separado por comas en el que aparece se evalúa como false, incluso bajo not, a menos que el resto de ese elemento ya determine el resultado, como en not print and (prefers-reduced-motion: reduce). Esto abarca las características desconocidas y la sintaxis que este analizador no implementa dentro de una condición, como los grupos anidados tipo ((min-width: 600px)), not dentro de un grupo y valores calc(). Un or entre condiciones hace que su elemento sea false. Los constructores MediaQueryList y MediaQueryListEvent tampoco se exponen, por lo que las comprobaciones instanceof con ellos lanzan un ReferenceError.
Dado que esa hoja de estilos llega a la página real, el host evalúa una regla @media (min-width: ...) con respecto a la ventana gráfica del navegador, mientras que matchMedia('(min-width: ...)') responde para la caja de tu widget. Para que CSS siga la misma caja que ve tu JavaScript, declara tu propio container-type en un contenedor envolvente y consúltalo: una regla @container sin uno se resuelve con respecto al contenedor host más cercano, no a tu widget. La misma separación se aplica al esquema de colores, donde una hoja de estilos @media (prefers-color-scheme: dark) sigue la preferencia del navegador y del SO, mientras que matchMedia('(prefers-color-scheme: dark)') sigue el tema seleccionado en Twenty, así que aplica estilos usando tokens de useTheme() cuando ambos deban coincidir.
La altura que se te proporciona también corresponde a tu propia caja y, dependiendo de dónde te monte el host, esa caja puede dimensionarse según tu propio contenido. La ramificación del diseño según min-height, max-height u orientation puede entonces retroalimentarse y oscilar. Prefiere consultas basadas en el ancho o @container con container-type: inline-size para tomar decisiones de diseño.
requestAnimationFrame funciona, con o sin el prefijo window.. Solo
Chrome ejecuta un fotograma de animación real dentro de un worker; Firefox y Safari reciben un
temporizador de 16 ms, así que úsalo para aplazar una nueva lectura después de una escritura, no para controlar una
animación.Desplazamiento
Al asignarscrollTop o scrollLeft, llamar a scrollTo() o a su alias scroll(), y llamar a scrollIntoView() se desplaza el elemento correspondiente en la página:
- Los cambios de desplazamiento se aplican después del siguiente renderizado, en el orden en que los realizó tu código. Dentro de un mismo renderizado, los cambios en la selección de entrada se aplican primero, por lo que llamar a
setSelectionRange()seguido de una escritura enscrollLeftmantiene visible el cursor. - Las lecturas devuelven el valor que escribiste hasta que la página informa de la posición real, que el navegador limita al intervalo desplazable. En los contenedores de derecha a izquierda,
scrollLeftes cero o negativo, como en los navegadores. scrollIntoView()aceptablockeinlineconstart,center,endynearest, alineandoblockverticalmente einlinehorizontalmente, por lo que los modos de escritura verticales no se mapean. Solo desplaza los propios contenedores de desplazamiento de tu componente, nunca el marco del widget, la página ni otro componente.focus()sigue mostrando el elemento enfocado tal como lo hace el navegador.- El desplazamiento es instantáneo: se ignoran
behaviory la opcióncontainer, yscrollBy()no está disponible. - Volver a escribir una posición que acabas de leer, como
element.scrollTop = element.scrollTop, no hace nada, por lo que no puede deshacer un desplazamiento que el usuario haya realizado mientras tanto. Esto solo se cumple una vez que la página ha medido el elemento; hasta entonces, se aplica la escritura. - Desplazar
document.documentElement,document.bodyowindowse mantiene dentro del entorno aislado y nunca desplaza la página;window.scrollTo(),scroll()yscrollBy()no hacen nada. - La primera lectura de un contenedor que nunca se midió devuelve ceros (consulta Diseño y medición), por lo que una comprobación de desplazabilidad realizada justo después de que aparezca el contenedor puede indicar que no es desplazable.
Ámbito global
Los elementos nativos propios del worker también tienen alias enwindow, por lo que funcionan con o sin el prefijo window.: fetch, setTimeout/clearTimeout, setInterval/clearInterval, queueMicrotask, console, crypto, performance, structuredClone, btoa/atob, URL/URLSearchParams, AbortController/AbortSignal, Headers/Request/Response, Blob, File, FormData y TextEncoder/TextDecoder.
Acceso al DOM
Unref te da un elemento sandbox, no un HTMLElement.
element.classList funciona, respaldado por el atributo class: add, remove, toggle, replace, contains, item, length, value y la iteración se comportan como lo hacen en un navegador, y className se mantiene sincronizado. element.dataset funciona de la misma manera, respaldado por los atributos data-*; escribe los nombres data-* en minúsculas, ya que el sandbox conserva los nombres de los atributos tal como se escribieron en lugar de convertirlos a minúsculas como lo hace un navegador.
Los portales del cuerpo pueden extenderse hasta 200px más allá de cada borde de tu componente, incluso fuera del marco de desplazamiento de un widget. Las superposiciones más grandes se recortan a esta área, así que proporciona a los menús largos su propio contenedor de desplazamiento. La navegación con teclado en las listas de Twenty UI, como Autocomplete y Select, muestra la opción resaltada dentro de ese contenedor. Las API nativas de capa superior, como showModal(), showPopover() y requestFullscreen(), están bloqueadas.
Eventos
Eventos de ratón, puntero, tacto, arrastre, teclado, foco,input/change/submit, scroll/wheel/contextmenu y animationend/transitionend pasan al host, además de algunos específicos por elemento: load/error en img, portapapeles y composición en input/textarea, medios en video/audio, toggle en details/dialog. Cualquier otra cosa (onAuxClick, onSelect, onInvalid, onReset, onAnimationStart, captura de puntero, onLoad fuera de img) se descarta sin aviso.
Los manejadores reciben eventos tipados creados a partir del evento de la página, PointerEvent, MouseEvent, KeyboardEvent, InputEvent, WheelEvent o FocusEvent, y esos constructores existen en el sandbox para dispatchEvent(). Cada evento reenviado también incluye la interfaz de eventos sintéticos de React (nativeEvent, isDefaultPrevented(), isPropagationStopped(), persist()), por lo que el código escrito para eventos de React sigue funcionando aunque el sandbox entregue el objeto de evento directamente a tu manejador.
Un evento de página cruza cuando el elemento con el que interactuó el usuario, o uno de sus antepasados en tu componente, tiene un controlador para ese tipo de evento, o cuando es una pulsación que esperan tus escuchadores de document (consulta más abajo). Cruza una vez y luego se propaga dentro del sandbox como lo hace en la página: event.target es el elemento con el que interactuó el usuario, currentTarget es el elemento cuyo manejador se está ejecutando, stopPropagation() impide que se ejecuten los manejadores de los antecesores y relatedTarget apunta al elemento del sandbox desde o hacia el que se movieron el foco o el puntero, o es null cuando ese elemento está fuera de tu componente. Como en React, onFocus y onBlur también se ejecutan para cambios de foco dentro del elemento, y onClickCapture y los demás manejadores de captura se ejecutan durante la fase de captura.
Los manejadores que una biblioteca añade con React.cloneElement, como los que están detrás de la prop render de Base UI, se ejecutan después de los propios manejadores del elemento en lugar de reemplazarlos, por lo que pasar onClick: undefined a cloneElement no elimina el manejador del elemento. Un manejador en un elemento render de Base UI todavía puede llamar a event.preventBaseUIHandler() para omitir el propio manejador de Base UI.
event.target.value y checked reflejan el control una vez que el navegador ha aplicado el cambio del usuario: en los manejadores input, change, click, blur y focusout, y en los manejadores keyup para value. En los manejadores keydown, beforeinput y de puntero, contienen el valor y el estado marcado del control a partir de su última edición, independientemente de lo que escuche tu componente: un manejador Enter lee el texto escrito hasta el momento, pero un manejador para una tecla imprimible aún no ve esa tecla, así que lee valores en tiempo real de una entrada controlada o de un manejador onChange.
Los escuchadores de document reciben los eventos que cruzan desde tu componente, con una excepción para las pulsaciones. Mientras tu componente escucha pointerdown, pointerup, pointercancel, mousedown, mouseup o click en document, como hace una ventana emergente abierta para cerrarse al pulsar fuera, cada pulsación llega a esos escuchadores: una pulsación dentro de tu componente se envía al elemento que pulsó el usuario incluso cuando nada la controla, y una pulsación en otra parte de la página se envía a document.body con el estado de sus botones, el número de clics y el tipo de puntero, pero sin coordenadas ni teclas modificadoras. Los clics sin una pulsación, como cuando el teclado activa un control fuera de tu componente, y las pulsaciones en las barras de desplazamiento de la página no se envían, y un gesto de deslizamiento táctil fuera de tu componente solo llega como pointerdown y pointercancel, por lo que no cierra una ventana emergente abierta. Nada más del resto de la página cruza, así que, para capturar atajos con document.addEventListener('keydown', ...), coloca un onKeyDown en un elemento que envuelva tu componente. Los listeners de window solo reciben error y unhandledrejection. Los movimientos del puntero solo cruzan sobre elementos que tienen un manejador de movimiento, y la captura del puntero permanece en la página, por lo que un arrastre se detiene en cuanto el puntero sale de esos elementos. event.preventDefault() tampoco cruza; el envío de formularios, dragover/drop y los clics en enlaces ya están protegidos por ti.
Atributos y estilos
Cada elemento reenvía sus propias propiedades al DOM del host (href en a, src/alt en img, value/placeholder/disabled en input, etc.), además de un conjunto común en cada elemento: id, className, style, title, tabIndex, role, draggable y cualquier atributo aria-* / data-* (con guiones, por lo que ariaLabel se descarta). Cualquier cosa fuera de eso se descarta silenciosamente, así que expresa el estado personalizado como data-*.
El CSS del componente, ya sea desde import './styles.css', CSS-in-JS o un elemento style, se inyecta en el head de la página del host sin ámbito. Así que los nombres de clase colisionan con los de Twenty (ponles un prefijo y nunca escribas div { ... } como selectores), y @media coincide con la ventana del navegador en lugar de con tu widget (usa @container con tu propio container-type). Las props de style en línea no se ven afectadas.
Almacenamiento y red
localStorage y sessionStorage son proporcionados por Twenty en lugar del navegador: el componente se ejecuta en un worker en un origen opaco, por lo que el host almacena los valores en nombre de tu aplicación. Consulta almacenamiento para ver su ámbito y límites. IndexedDB, las cookies, la Cache API y BroadcastChannel siguen sin estar disponibles. Para mantener el estado entre dispositivos, llama a una función de lógica y usa su almacenamiento de clave-valor.
fetch funciona, con matices:
- Las llamadas a la API de Twenty y a las rutas de tu aplicación se hacen por proxy a través del host, así que da prioridad a
RestApiClient. En las llamadas con proxy,AbortSignaly las demás opciones deRequestInitse descartan, y solo se admiten cuerpos de tipostringyURLSearchParams. - Otros orígenes salen del sandbox con
Origin: null, por lo que una API de terceros responde solo si envíaAccess-Control-Allow-Origin: *. Llámala desde una función de lógica en su lugar. fetch('/rest/people')nunca se hace coincidir con la API de Twenty, porque el sandbox no tiene URL de página contra la que resolver una ruta relativa.
Selección de archivos
Las entradas de archivos proporcionan objetosFile nativos con metadatos y contenido legible. Usa text() o arrayBuffer() para leer un archivo seleccionado, o pásalo a la función host uploadFile. Para abrir el selector nativo, es necesario llamar a input.click() en el plazo de un segundo desde un clic de confianza dentro de tu componente. Los flujos de React y Preact creados con el SDK se verifican en Chromium.
Las URL de objetos creadas por el worker asignadas a img.src admiten vistas previas. Revoca tus URL al reemplazar la imagen o desmontar tu componente. Tu aplicación se encarga de la validación de archivos, el progreso de carga y la cancelación.
Captura de medios
Declaramicrophone, camera o ambos en la matriz requestedCapabilities que se pasa a defineApplication() para que estas capacidades aparezcan durante la instalación. La aprobación se aplica a todo el espacio de trabajo y requiere un miembro que pueda administrar aplicaciones.
Si una aplicación instalada llama a navigator.mediaDevices.getUserMedia() sin el acceso requerido, Twenty pausa la captura y muestra un cuadro de diálogo de aprobación. Esto también admite aplicaciones antiguas sin declaraciones de capacidades. Los miembros que no pueden administrar aplicaciones deben pedir a un administrador del espacio de trabajo que apruebe el acceso. Cancelar el cuadro de diálogo rechaza la solicitud antes de que se soliciten los permisos del navegador o del sistema operativo.
Las actualizaciones de paquetes conservan las autorizaciones existentes del espacio de trabajo y no conceden automáticamente acceso adicional a medios. Las aplicaciones de desarrollo sincronizadas localmente reciben las capacidades declaradas en su manifiesto. Una vez aprobado el acceso, Twenty no vuelve a solicitarlo para cada captura, pero los permisos del navegador y del sistema operativo siguen aplicándose.
Un punto naranja en la cabecera del espacio de trabajo indica una captura activa. Pase el cursor sobre el punto o selecciónelo para ver las aplicaciones que usan el micrófono o la cámara y detener su captura. Las solicitudes pendientes aparecen como un punto delineado con un control de cancelación.
getUserMedia() y MediaRecorder se ejecutan mediante polyfills de sandbox, por lo que el código de grabación estándar y MediaRecorder.isTypeSupported() funcionan sin cambios tras la aprobación. Se aceptan objetos de restricciones detallados, pero no se reenvían. El anfitrión captura con sus valores predeterminados para los tipos solicitados. Distintas aplicaciones pueden capturar simultáneamente, y detener una aplicación afecta solo a sus propias sesiones. Cada componente frontal admite una captura pendiente o activa a la vez. Almacena un Blob grabado con la función host uploadFile.
Sitios web incrustados con acceso a medios
Para incrustar una herramienta de teléfono o video, declare su origen HTTPS enrequestedIframeMediaPermissions de su aplicación, con el acceso al micrófono y a la cámara configurados por separado. La instalación de la aplicación concede estos sitios web al espacio de trabajo. Las actualizaciones de las aplicaciones publicadas conservan las concesiones existentes y no conceden automáticamente acceso adicional a medios. Las aplicaciones de desarrollo sincronizadas localmente reciben automáticamente los sitios web declarados en su manifiesto. Un miembro que puede administrar aplicaciones puede aprobar nuevas solicitudes desde la pestaña General de la aplicación instalada, en Acceso a medios de sitios web incrustados > Revisar acceso a medios. Esta aprobación utiliza una confirmación nativa del navegador que muestra la aplicación, los orígenes de los sitios web y el acceso solicitado al micrófono o la cámara. Twenty lee los sitios web autorizados cuando se carga una pestaña, por lo que debe recargar las pestañas abiertas de Twenty después de la instalación o aprobación.
Solicite las funciones correspondientes en el iframe:
allow="microphone; camera" y declare ambos para el origen. Twenty solo delega los medios concedidos a su aplicación para ese origen. Los miembros deben seleccionar el control Habilitar de la incrustación y aprobar una confirmación nativa del navegador que muestre la aplicación, el origen del sitio web y el acceso solicitado al micrófono o la cámara antes de que reciba acceso a medios. Los permisos del navegador y del sistema operativo siguen aplicándose. srcDoc y las incrustaciones en el propio origen de Twenty no pueden recibir esta delegación. Los widgets de diseño de página IFRAME definidos por la aplicación usan los orígenes concedidos de su aplicación propietaria y el mismo flujo de confirmación.
Las incrustaciones habilitadas aparecen en el mismo indicador de encabezado que la captura nativa, con Acceso al micrófono habilitado o Acceso a la cámara habilitado en lugar de afirmar que la grabación está activa. El indicador permanece visible mientras el acceso está habilitado, incluso si la incrustación está oculta. Deshabilitar descarga el iframe y lo vuelve a cargar con los medios bloqueados. La captura de la aplicación mediante getUserMedia() sigue informando la captura real y conserva su propio control Detener.
El contenido del iframe permanece aislado. Las incrustaciones de medios externos aprobadas conservan su propio origen después de que el miembro habilita el acceso, lo que permite al sitio web usar su sesión y solicitar permisos de medios del navegador. Otros iframes de componentes de interfaz conservan un origen de aislamiento opaco.
Otras carencias
- Cargas útiles de arrastrar y soltar. Los eventos de arrastre se disparan, pero
event.dataTransferesundefined. - Integraciones nativas de Node.
fs,pathynode:cryptohacen que la compilación falle, así que mueve ese trabajo a una función de lógica. Web Crypto reemplaza anode:crypto, yfetch,TextEncoderyURLtambién están disponibles; consulta el ámbito global, pero nada reemplaza afs: el entorno aislado no tiene sistema de archivos.