Commit Graph

15 Commits

Author SHA1 Message Date
Paul Rastoin 308e4de7a6 TDD: twenty-ui render coverage in the front-component sandbox — golden tests cataloging every sandbox gap (fully green) (#23203)
## What — TDD coverage layer, no fixes

Renders every twenty-ui component export inside the real front-component
sandbox (opaque-origin iframe + remote-dom worker, real SDK esbuild
pipeline) through the existing Playwright-backed storybook vitest suite,
and expresses **every sandbox gap as a golden known-failure test**. This
PR deliberately ships **zero fixes** and is **fully green (267/267)**:
each known issue has a test asserting the exact current broken behavior.
A golden test fails on regression (an unexpected component starts
failing) AND on fix (the documented failure disappears) — so every
future fix must flip its golden assertion to the strict one, whose
acceptance criteria are spelled out in each story's comment.

- One gallery fixture per submodule under
`src/__stories__/twenty-ui-gallery/` (~150 components)
- `component-gallery.tsx`: each component renders inside its own error
boundary; failures are aggregated with component name and error message
(`data-failed-messages`) so one crash cannot mask the rest
- Behavior-level stories that a fake fix cannot pass (MutationObserver
must actually fire; a link click must reach `hostApi.navigate`)
- `vitest.config.ts` re-export so the storybook in-UI "Run tests" button
works

## Current state: 267/267 green — mergeable; 18 golden tests encode the
catalog below

## Failure catalog (each entry = golden tests asserting today's broken
behavior)

### 1. `MutationObserver.observe is not a function` — 6 failing tests

`@remote-dom/polyfill` ships `MutationObserver` as an empty class:
construction succeeds, the first `.observe()` call crashes.

| Failing test | twenty-ui export(s) | Call site |
|---|---|---|
| Input React + Preact | `Radio`, `RadioGroup`, `CardPicker` |
`@base-ui/react` field internals observe form state |
| Surfaces React + Preact | `AppTooltip` | twenty-ui observes
`document.body` to track anchors |
| Worker Platform APIs: Mutation Observer React + Preact | (behavior
test) | asserts the observer actually fires on a React-driven insertion
— a no-op stub cannot pass it |

Validated fix (branch history, commit 94b96d01e79e): implement real
mutation semantics **locally in the worker** by tapping the same
`@remote-dom/polyfill` hooks
(`insertChild`/`removeChild`/`setAttribute`/`removeAttribute`/`setText`)
remote-dom already uses to mirror mutations to the host. The worker tree
is the source of truth — no host bridge needed.

### 2. `getComputedStyle is not a function` — 4 failing tests

The remote-dom `Window` polyfill does not implement `getComputedStyle`;
`@base-ui/react` Collapsible calls it on open.

| Failing test | twenty-ui export(s) |
|---|---|
| Layout React + Preact | `AnimatedEaseInOut`,
`AnimatedExpandableContainer` |
| Json Visualizer React + Preact | `JsonTree`, `JsonArrayNode`,
`JsonObjectNode`, `JsonNestedNode` (all via `JsonNestedNode`'s
Collapsible) |

Validated fix (commit 7af577054fd9): `getComputedStyle` is synchronous
and layout only exists host-side, so a bridge is impossible — an
inert-but-valid stub (`0s` durations, `none` animation names) is the
honest terminal state.

### 3. No router context in the sandbox — 5 failing tests

react-router's `Link` reads `NavigationContext`, which has no provider
inside the worker: `Cannot destructure property 'basename' of
'useContext(...)' as it is null`.

| Failing test | twenty-ui export(s) |
|---|---|
| Navigation React + Preact | `RawLink`, `UndecoratedLink` |
| Data Display React + Preact | `LinkChip` |
| HostApi: Router Link | acceptance test — the link must render AND its
click must reach `hostApi.navigate` |

Validated fix (commit 221c9c353dc8): SDK build plugin wraps the
component tree in a low-level react-router `<Router>` whose custom
navigator forwards `push`/`replace` to the SDK `navigate` host API (NOT
a MemoryRouter, which would render links but swallow clicks into
in-memory history). Falls back to a pass-through provider when the app
has no react-router-dom dependency.

**Security finding bundled in that commit:** clicking a mirrored `<a>`
performs a native host-page navigation before the async worker
round-trip can `preventDefault` — a front component can escape the
sandbox by rendering a link. Fix: apply the renderer's existing
preventDefault-then-forward guard (already used for form submits) to
anchor clicks, keeping `target="_blank"` native. The Router Link story's
flip-to acceptance assertions (click must reach `hostApi.navigate`) will
hard-crash the vitest browser the moment links render without this guard
— by design.

### 4. Open `Modal` hangs the React runtime — 1 failing test

base-ui Dialog portal with `isOpen` never commits under the React
runtime (no error thrown); works under Preact.

| Failing test | twenty-ui export |
|---|---|
| Modal Open React (Modal Open Preact passes) | `Modal` |

Isolated in its own fixture so the hang cannot mask the rest of the
surfaces gallery (which keeps a closed Modal for mount coverage). Root
cause not yet identified — first experiment: portal into a worker-owned
container instead of the polyfilled `document.body`.

### 5. monaco cannot load in the sandbox — 2 failing tests

`@monaco-editor/react` lazy-loads monaco via script injection,
impossible in the polyfilled worker DOM (opaque-origin CSP, no script
loading). The CodeEditor wrapper mounts; monaco's `onMount` never fires.

| Failing test | twenty-ui export |
|---|---|
| Code Editor React + Preact | `CodeEditor` |

Probably wont-fix in the worker: if front components need a code editor,
the path is a host-rendered privileged component. The failing test is
the documentation.

### Also worth knowing (no failing test possible yet)

- `ResizeObserver` / `IntersectionObserver` / `matchMedia` are absent in
the worker: components mount without them today only because nothing
crashes at mount — behavior like popup auto-resize and `useIsMobile` is
silently wrong. Real fix is a host bridge (async observers on the
component's own mirrored elements only — never the host document).
Behavior tests should land with that fix.
- `Icon`/`IconsProvider`/`useIcons` are excluded from galleries by
choice: they dynamic-import the multi-MB Tabler catalog; direct icon
imports are the supported pattern in bundled front components.

## Iteration plan

Each fix is one commit + one set of stories flipping green, in suggested
order:

1. Worker-local MutationObserver (6 tests) — cherry-pick base:
94b96d01e79e
2. `getComputedStyle` + observer/matchMedia stubs (4 tests) —
cherry-pick base: 7af577054fd9
3. Router provider + anchor click guard (5 tests) — cherry-pick base:
221c9c353dc8
4. Open-Modal React hang investigation (1 test)
5. CodeEditor: decide wont-fix + keep the failing story or convert to a
documented skip (2 tests)

## How to run

```
npx nx run twenty-front-component-renderer:storybook:prebuild
cd packages/twenty-front-component-renderer
npx vitest run --config vitest.storybook.config.ts --project storybook
```
2026-07-24 14:38:19 +00:00
Raphaël Bosi 07be5e0892 Forward editing and clipboard events to front components (#22630)
Adds the text-editing events input-heavy front components need:
`beforeinput`, `compositionstart/update/end` and `copy/paste/cut`,
allowed on `input` and `textarea` only.

These events carry payload: `beforeinput` forwards `inputType`/`data`
through a native host listener (React synthesizes `onBeforeInput`
without them), composition events forward `data`, and paste forwards
`clipboardData.getData('text')` capped at 100k chars. Clipboard text is
read only on an explicit paste into the component's own input, never on
copy/cut, and the worker synthesizes a minimal `clipboardData` so
`onPaste` handlers work. `beforeinput` is observe-only: `preventDefault`
cannot cross the async worker boundary.

Allow-listing these events makes the host bind them, so the
`buildHostReactPropsFromRemoteProps` test that pinned them as rejected
now pins events that are still unmapped.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22630?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-21 14:21:53 +00:00
Raphaël Bosi eb651180aa Widen the front component event allow-list (#22616)
Front components are third-party UI that runs in a sandboxed worker, so
every DOM event reaching them has to be on an explicit allow-list. That
list was small: mostly click, focus and pointer events.

This adds touch, drag and drop, focusin/focusout,
animationend/transitionend and scrollend, plus load/error on `<img>` and
toggle on `<details>`/`<dialog>`.

Two of them need the host to do more than forward the event:

- react-dom has no `onFocusIn`/`onFocusOut` props, so the host attaches
those two with `addEventListener` instead.
- a browser only fires `drop` on an element whose `dragover` default was
prevented, and the component's own `preventDefault` arrives too late
across the async worker boundary. The host prevents it synchronously as
soon as the component declares either handler.

Touch events carry their coordinates on `changedTouches`, so the first
touch fills the existing coordinate fields.

Still not crossing, since each would need a new serialized field: touch
lists, `animationName`/`propertyName`/`elapsedTime`, toggle `newState`
and `dataTransfer`.

The diff also renames a few things it touches (`filterProps` and
`EventToReact` in particular) so the host-side event path reads in
order.

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22616?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
2026-07-20 18:24:21 +02:00
Raphaël Bosi 60f5964c64 Run front components in a sandboxed opaque-origin iframe (#22588)
Front components run untrusted third-party React in a Web Worker. That
worker previously shared the host origin, so it could reach
origin-scoped storage (the metadata-store IndexedDB, the
`twenty-sign-out` BroadcastChannel), cookies, and same-origin resources.

This runs the worker inside a `sandbox="allow-scripts"` (no
`allow-same-origin`) iframe, giving it an opaque origin where the
browser denies localStorage, cookies, IndexedDB, and BroadcastChannel
outright. The worker is kept inside the iframe (rather than a bare
iframe) so untrusted code always runs off the main thread; the
remote-dom render path is unchanged.

- **Transport:** host ↔ iframe ↔ worker over a re-transferred
`MessagePort` (`ThreadMessagePort`); a small bootstrap script is inlined
into the iframe via `srcdoc` (bundled at build time by a prebuild step)
and relays the port to the worker it spawns. Messages across the
boundary use a typed discriminated union with a single parse/guard.
- **Network:** under the opaque origin, direct fetches to the Twenty API
would be `Origin: null`, so the component source and SDK modules are
fetched through an allowlisted, credential-omitting `hostFetch` bridge
and blobbed inside the worker. The allowlist is single-sourced on the
host (http(s) origins only) and carried in the render context. The
bridge is mandatory (rendering fails closed if it is missing), refuses
redirects except for GET/HEAD to the known file-storage URLs, and caps
response body size.
- **SDK loading:** SDK client modules now load inside the worker through
the bridge, replacing the host-side SDK-blob state/effect/provider with
a pure `getSdkClientUrls` URL builder.
- **Isolation tests:** a unit test locks the sandbox attribute
(`allow-scripts`, never `allow-same-origin`); a browser test asserts the
worker actually gets an opaque origin with storage denied, probing
cookies by writing one rather than reading an empty jar.

Also adds a "List Companies" seed front component that queries workspace
data via the SDK client (exercising the bridge end-to-end),
single-sources the command-menu confirmation-modal result event name and
detail type in `twenty-shared` (previously a hand-synced duplicate), and
decomposes the renderer (bridge, sandbox, worker orchestration) into
small single-purpose utils with unit tests.

## How it works

```mermaid
sequenceDiagram
    autonumber
    participant Host as Host window (twenty-front · host origin)
    participant Frame as Sandboxed iframe (allow-scripts · opaque origin)
    participant Worker as Worker (untrusted component · opaque origin)
    participant API as Twenty API (host origin)

    rect rgb(238,242,248)
    Note over Host,Worker: 1 — Boot handshake
    Host->>Frame: create iframe sandbox="allow-scripts", srcdoc = inlined bootstrap script
    Host->>Host: MessageChannel + ThreadMessagePort(port1)<br/>exports = host API + hostFetch
    Frame-->>Host: READY
    Host->>Frame: INIT + transfer port2
    Frame->>Worker: spawn inlined Worker + re-transfer port2
    Worker->>Worker: ThreadMessagePort(port)<br/>exports = render / updateContext
    Note over Host,Worker: Port now entangles Host ↔ Worker directly
    end

    rect rgb(246,240,248)
    Note over Host,Worker: 2 — Render
    Host->>Worker: render(connection, { componentUrl, sdkClientUrls, hostFetchOrigins, token })
    Worker->>Worker: override globalThis.fetch<br/>(Twenty origins → hostFetch)
    end

    rect rgb(248,244,238)
    Note over Worker,API: 3 — Network via hostFetch bridge (opaque Origin:null cannot reach the API directly)
    Worker->>Host: hostFetch(componentUrl, Bearer)
    Host->>Host: origin allowlist + credentials:'omit'
    Host->>API: fetch(componentUrl)
    API-->>Host: source
    Host-->>Worker: { status, headers, body }
    Worker->>Host: hostFetch(sdkClientUrls.core / .metadata)
    Host-->>Worker: SDK module sources
    Worker->>Worker: blob each source in its own opaque origin → import() → run untrusted React
    end

    rect rgb(238,248,242)
    Note over Worker,Host: 4 — Render mirror
    Worker->>Host: remote-dom mutations (RemoteConnection)
    Host->>Host: RemoteReceiver → RemoteRootRenderer → host DOM
    end

    Note over Worker: Opaque origin ⇒ browser denies localStorage,<br/>cookies, IndexedDB, BroadcastChannel
```
2026-07-10 13:10:30 +00:00
Raphaël Bosi 0dc6272da5 Remove twenty-ui reexport from the SDK and use twenty-ui directly (#22326)
## What & why

Removes the `twenty-sdk/ui` reexport. Apps now use Twenty UI by
installing
[`twenty-ui@1.0.0-alpha.1`](https://www.npmjs.com/package/twenty-ui/v/1.0.0-alpha.1)
from npm and importing its subpaths directly. The reexport re-exported
types that didn't resolve, forcing typecheck workarounds.

## Changes

- **twenty-sdk**: delete `src/ui/index.ts`, drop the `./ui` export,
remove it from the browser vite build, and rewire the CLI manifest-mock
to `twenty-ui` (`.css` falls through to the empty-CSS loader).
`twenty-ui` stays a devDependency for the CLI fixture tests.
- **Renderer + create-twenty-app template**: import from `twenty-ui`
subpaths; the template pins `twenty-ui@1.0.0-alpha.1`.
- **Docs**: new "Using Twenty UI components" section (install + subpath
imports + `useTheme()` for theme tokens), codex references, and the
cross-doc-contract validator.

The `twenty-for-twenty` / `twenty-slack` example apps are intentionally
left on `twenty-sdk/ui`: they consume the published SDK (which still
ships `./ui`), and `twenty-ui@1.0.0-alpha.1` requires react 19 + a
`monaco-editor` peer the react-18 apps can't satisfy. They migrate once
the SDK is republished.
2026-06-30 11:17:48 +02:00
Raphaël Bosi 9c9c34fccf Remove twenty-ui-deprecated and migrate frontend to twenty-ui (#21596)
Migrates `twenty-front`, `twenty-sdk`, and
`twenty-front-component-renderer` from `twenty-ui-deprecated` to
`twenty-ui` (mechanical import swap — the packages have API parity) and
deletes the deprecated package along with its workspace/CI/config
wiring.

Also adds `@linaria/react`/`@linaria/core` as direct deps of
`twenty-front` (it used them transitively via the deprecated package).

Note: move the required status check from `ci-ui-status-check` to
`ci-new-ui-status-check`.

Argos: the Storybook box-model/button-reset baseline shift (the bulk of
the visual diffs) is isolated in #21665 — Storybook now loads
twenty-ui's global `reset.scss`, which the production app already ships.
Once #21665 merges and this branch is rebased, the remaining Argos diffs
are component-level visual-parity items only.
2026-06-17 09:41:11 +00:00
Raphaël Bosi c596a5e342 Rename twenty-ui to twenty-ui-deprecated and twenty-new-ui to twenty-ui to prepare package release (#21315)
## Description

Promotes the next-gen UI library (formerly `twenty-new-ui`) to the name
**`twenty-ui`** (v0.1.0, publishable) and renames the old package to
**`twenty-ui-deprecated`**. Rewrites ~1,730 `twenty-ui` imports →
`twenty-ui-deprecated`, updates all configs/CI/Docker/deps, and migrates
twenty-front's `Toggle` to the new package (first consumer) as a
drop-in.

## Next steps
- Wire the `ui/v*` publish dispatch (`cd-deploy-tag.yaml` +
`.yarnrc.yml`), then tag `ui/v0.1.0` to publish.
- Continue migrating components from `twenty-ui-deprecated` →
`twenty-ui`.
2026-06-08 18:12:28 +02:00
Raphaël Bosi 1833fa84a5 Fix front component pointer/mouse event coordinates (#21117)
Fixes https://github.com/twentyhq/twenty/issues/21000

Front-component event handlers read standard event fields
(event.clientX, event.offsetX, …), but these were always undefined. On
the remote side, serialized event data was passed only as the
CustomEvent's detail — and CustomEvent ignores every constructor option
except detail, so the values lived at event.detail.clientX and never on
the event object itself.

- Added `applySerializedEventProperties`, to copy a curated allowlist of
event-level keys onto the event. Element/target state (value, checked,
files, scroll, media props) stays in
`applySerializedEventTargetProperties`, applied to this (the dispatch
element = event.target).
- Added x/y to `SerializedEventData` and to host-side serialization in
`createHtmlHostWrapper`.
- Added an `svg-pointer `story + `createHtmlTagPointerStory`

Note: Also pinned @types/react to v18 so the renderer stops dragging in
React 19 types and breaking typecheck.
2026-06-02 15:04:11 +00:00
qinglong e3c79c803c Fix standard React form event targets in front components (#20525)
Fixes #20354

## Problem

Front component form events currently expose serialized form state
through a sandbox-specific event shape, such as `event.detail.value` and
`event.detail.checked`.

That works for examples that explicitly read `event.detail`, but it is
surprising for app authors writing standard React form handlers:

```tsx
onChange={(event) => {
  setValue(event.target.value);
}}
Internal app code already has to defend against multiple possible shapes:

// Values may live on e.detail.value, e.value, or e.target.value.
This suggests the sandbox event shape is leaking into userland.

Solution
This change keeps the existing event.detail behavior, but also syncs serialized event target properties back onto the remote element before dispatching the event.

That means both styles work:

// Existing sandbox-specific style
event.detail.value;

// Standard React style
event.target.value;
The same applies to checked, files, scroll/media target properties, and similar serialized target state.

What Changed
Added a shared helper to apply serialized event target properties onto the remote element.
Updated generated remote element event configs to dispatch serialized events through a custom event config.
Updated the remote-dom element generator so regenerated files preserve this behavior.
Updated Storybook form-event examples to use standard React event target reads.
Added/updated Storybook coverage for input, checkbox, textarea, select, submit, and caret preservation flows.
Validation
Ran git diff --check
Ran a targeted TypeScript error scan for the changed front component renderer files
Manually verified the Storybook FrontComponent/EventForwarding form event story locally:
text input updates state
checkbox updates state
submit reflects the updated JSON
Note: local Storybook verification on Windows required temporary local build/cache fixes that are not included in this PR, to keep this change focused on front component event behavior.

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-22 14:58:33 +00:00
Charles Bochet 75c22a2119 feat(front-component-renderer): forward file input metadata (#20458)
## Summary

`<input type=\"file\">` inside front-components was silently
non-functional:
- The host-side `serializeEvent` did not read `target.files`, so the
worker received an empty `onChange` detail.
- `SerializedEventData` had no `files` field.
- The `html-input` schema in `AllowedHtmlElements` exposed neither
`accept`, `multiple`, nor `capture` — the worker could not even
configure the picker.

This PR forwards file metadata (`name`, `size`, `type`, `lastModified`)
through the existing serialized event detail and accepts the missing
attributes on the `html-input` remote element. A new Storybook play test
guards the regression by uploading single and multiple files via
`userEvent.upload`.

Reading file contents inside the worker is intentionally out of scope
here and will need a separate host API bridge (the host has the `File`
objects on the real input element; passing bytes through `postMessage`
is a bigger design call).
2026-05-11 19:30:38 +00:00
Raj Bhaskar f634a4a0c0 fix(front-component): preserve caret position on controlled input/textarea updates (#20416)
## Problem

In the front-component sandbox, typing in the middle of a pre-filled
`<input>` or `<textarea>` caused the caret to jump to the end on every
keystroke. Characters appeared at the correct position, but editing
mid-string was effectively broken.

Root cause: the remote-DOM bridge round-trips every keystroke through
the
worker. By the time the updated `value` prop arrives back at the host,
React applies it by setting `inputElement.value = X` directly, which
browsers always reset the caret to the end.

Typing at the end was unaffected, which is why this went unnoticed in
search fields and similar append-only inputs.

## Fix

For text-like `<input>` types and `<textarea>`, the `value` prop is now
applied imperatively through a ref callback instead of being passed as a
React controlled prop:

- If the DOM value already matches the incoming prop, the assignment is
  skipped entirely.
- If a write is needed and the element is focused, `selectionStart` and
  `selectionEnd` are captured before the assignment and restored
  afterwards with `setSelectionRange`.

Non-text input types (checkbox, radio, file, color, range) and all other
host elements are unaffected.

## Testing

Drop the repro from the issue into any front-component, click between
two
characters in the pre-filled value, and type — the caret should now stay
at the insertion point.

Fixes #20409

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
2026-05-11 17:47:30 +00:00
Charles Bochet eb1ca1b9ec perf(sdk): split twenty-sdk barrel into per-purpose subpaths to cut logic-function bundle ~700x (#19834)
## Summary

Logic-function bundles produced by the twenty-sdk CLI were ~1.18 MB even
for a one-line handler. Root cause: the SDK shipped as a single bundled
barrel (`twenty-sdk` → `dist/index.mjs`) that co-mingled server-side
definition factories with the front-component runtime, validation (zod),
and React. With no `\"sideEffects\"` declaration on the SDK package,
esbuild had to assume every module-level statement could have side
effects and refused to drop unused code.

This PR restructures the SDK so consumers' bundlers can tree-shake at
the leaf level:

- **Reorganized SDK source.** All server-side definition factories now
  live under `src/sdk/define/` (agents, application, fields,
  logic-functions, objects, page-layouts, roles, skills, views,
  navigation-menu-items, etc.). All front-component runtime
  (components, hooks, host APIs, command primitives) lives under
  `src/sdk/front-component/`. The legacy bare `src/sdk/index.ts` is
  removed; the bare `twenty-sdk` entry no longer exists.

- **Split the build configs by purpose / runtime env.** Replaced
  `vite.config.sdk.ts` with two purpose-specific configs:
  - `vite.config.define.ts` — node target, externals from package
    `dependencies`, emits to `dist/define/**`
  - `vite.config.front-component.ts` — browser/React target, emits to
    `dist/front-component/**`
  Both use `preserveModules: true` so each leaf ships as its own `.mjs`.

- **\`\"sideEffects\": false\`** on `twenty-sdk` so esbuild can drop
  unreferenced re-exports.

- **\`package.json\` exports + \`typesVersions\`** updated: dropped the
bare \`.\` entry, added \`./front-component\`, and pointed \`./define\`
  at the new per-module dist layout.

- **Migrated every internal/example/community app** to the new subpath
  imports (`twenty-sdk/define`, `twenty-sdk/front-component`,
  `twenty-sdk/ui`).

- **Added \`bundle-investigation\` internal app** that reproduces the
  bundle bloat and demonstrates the fix.

- Cleaned up dead \`twenty-sdk/dist/sdk/...\` references in the
  front-component story builder, the call-recording app, and the SDK
  tsconfig.

## Bundle size impact

Measured with esbuild using the same options as the SDK CLI
(\`packages/twenty-apps/internal/bundle-investigation\`):

| Variant | Imports | Before | After |
| ----------------------- |
------------------------------------------------------- | ---------- |
--------- |
| \`01-bare\` | \`defineLogicFunction\` from \`twenty-sdk/define\` |
1177 KB | **1.6 KB** |
| \`02-with-sdk-client\` | + \`CoreApiClient\` from
\`twenty-client-sdk/core\` | 1177 KB | **1.9 KB** |
| \`03-fetch-issues\` | + GitHub GraphQL fetch + JWT signing + 2
mutations | 1181 KB | **5.8 KB** |
| \`05-via-define-subpath\` | same as \`01\`, via the public subpath |
1177 KB | **1.7 KB** |

That's a ~735× reduction on the bare baseline. Knock-on benefits for
Lambda warm + cold starts, S3 upload size, and \`/tmp\` disk usage in
warm containers.

## Test plan

- [x] \`npx nx run twenty-sdk:build\` succeeds
- [x] \`npx nx run twenty-sdk:typecheck\` passes
- [x] \`npx nx run twenty-sdk:test:unit\` passes (31 files / 257 tests)
- [x] \`npx nx run-many -t typecheck
--projects=twenty-front,twenty-server,twenty-front-component-renderer,twenty-sdk,twenty-shared,bundle-investigation\`
passes
- [x] \`node
packages/twenty-apps/internal/bundle-investigation/scripts/build-variants.mjs\`
produces the sizes above
- [ ] CI green

Made with [Cursor](https://cursor.com)
2026-04-18 19:38:34 +02:00
Raphaël Bosi 48c540eb6f Add event forwarding stories to the front component renderer (#19721)
Add Storybook stories and example components to test event forwarding
through the front component renderer: form events (text input, checkbox,
focus/blur, submit), keyboard events (key/code/modifiers), and host API
calls (navigate, snackbar, progress, close panel)
2026-04-16 08:55:49 +00:00
Raphaël Bosi 7ba5fe32f8 Add new html tags to the remote elements (#19723)
- Add 72 missing HTML and SVG elements to the remote-dom component
registry (48 HTML + 24 SVG), bringing the total from 47 to 119 supported
elements
- HTML additions include semantic inline text (b, i, u, s, mark, sub,
sup, kbd, etc.), description lists, ruby annotations, structural
elements (figure, details, dialog), and form utilities (fieldset,
progress, meter, optgroup)
- SVG additions include containers (svg, g, defs), shapes (path, circle,
rect, line, polygon), text (text, tspan), gradients (linearGradient,
radialGradient, stop), and utilities (clipPath, mask, foreignObject,
marker)
- Add htmlTag override to support SVG elements with camelCase names
(e.g. clipPath, foreignObject) while keeping custom element tags
lowercase per the Web Components spec
2026-04-15 14:53:23 +00:00
Paul Rastoin 37908114fc [SDK] Extract twenty-front-component-renderer outside of twenty-sdk ( 2.8MB ) (#19021)
Followup https://github.com/twentyhq/twenty/pull/19010

## Dependency diagram

```
┌─────────────────────┐
│     twenty-front    │
│   (React frontend)  │
└─────────┬───────────┘
          │ imports runtime:
          │   FrontComponentRenderer
          │   FrontComponentRendererWithSdkClient
          │   useFrontComponentExecutionContext
          ▼
┌──────────────────────────────────┐         ┌─────────────────────────┐
│ twenty-front-component-renderer  │────────▶│       twenty-sdk        │
│   (remote-dom host + worker)     │         │  (app developer SDK)    │
│                                  │         │                         │
│  imports from twenty-sdk:        │         │  Public API:            │
│   • types only:                  │         │   defineFrontComponent  │
│     FrontComponentExecutionContext│         │   navigate, closeSide…  │
│     NavigateFunction             │         │   useFrontComponent…    │
│     CloseSidePanelFunction       │         │   Command components    │
│     CommandConfirmation…         │         │   conditional avail.    │
│     OpenCommandConfirmation…     │         │                         │
│     EnqueueSnackbarFunction      │         │  Internal only:         │
│     etc.                         │         │   frontComponentHost…   │
│                                  │         │   front-component-build │
│  owns locally:                   │         │   esbuild plugins       │
│   • ALLOWED_HTML_ELEMENTS        │         │                         │
│   • EVENT_TO_REACT               │         └────────────┬────────────┘
│   • HTML_TAG_TO_CUSTOM_ELEMENT…  │                      │
│   • SerializedEventData          │                      │ types
│   • PropertySchema               │                      ▼
│   • frontComponentHostComm…      │         ┌─────────────────────────┐
│     (local ref to globalThis)    │         │     twenty-shared       │
│   • setFrontComponentExecution…  │         │  (common types/utils)   │
│     (local impl, same keys)      │         │   AppPath, SidePanelP…  │
│                                  │         │   EnqueueSnackbarParams │
└──────────────────────────────────┘         │   isDefined, …          │
          │                                  └─────────────────────────┘
          │ also depends on
          ▼
    twenty-shared (types)
    @remote-dom/* (runtime)
    @quilted/threads (runtime)
    react (runtime)
```

**Key points:**

- **`twenty-front`** depends on the renderer, **not** on `twenty-sdk`
directly (for rendering)
- **`twenty-front-component-renderer`** depends on `twenty-sdk` for
**types only** (function signatures, `FrontComponentExecutionContext`).
The runtime bridge (`frontComponentHostCommunicationApi`) is shared via
`globalThis` keys, not module imports
- **`twenty-sdk`** has no dependency on the renderer — clean one-way
dependency
- The renderer owns all remote-dom infrastructure (element schemas,
event mappings, custom element tags) that was previously leaking through
the SDK's public API
- The SDK's `./build` entry point was removed entirely (unused)
2026-03-30 17:06:06 +00:00