From 3f0236b590dd372b758559ec578e5bda5ebd9a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Tue, 28 Jul 2026 13:47:07 +0200 Subject: [PATCH] fix(page-layout): let the column surface win over the solo presentation (#23412) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Why In the side panel and on mobile, a tab holding a single widget renders with no gutter at all: the field list sits flush against the panel border. Regression from #23109. `getWidgetCardVariant` checked the derived presentation before the surface: ```ts if (presentation === 'solo') return 'solo'; const isSideColumnContext = isInPinnedTab || isMobile || isInSidePanel; ``` So a single-widget tab resolved to `'solo'` even in the side panel or on mobile, and `'solo'` has no branch in `WidgetCard`'s padding switch, so it falls through to `0`. The same widget used to match `variant === 'side-column' && !isEditable` and get `spacing[3]` (12px). The pinned left panel escaped this only because `PageLayoutLeftPanel` hardcodes `presentation: 'stack'` — the rule was already there ("the pinned left panel is always a column, a surface rule not a widget rule"), just applied at one call site instead of being the rule. ## Why only the Home tab looks broken Every widget that used to live on a `CANVAS` tab carries its own gutter, so losing the card padding costs them nothing: | Widget | Own horizontal padding | |---|---| | Timeline | `spacing[6]` | | Notes | `spacing[6]` | | Files | `spacing[6]` | | Tasks | `spacing[6]` | | **Fields** | **none** | `Fields` was the only widget on a `VERTICAL_LIST` tab, so it was the only one relying on the card for its gutter, and the only one that ends up flush. ## What Resolve the surface first: a column surface (pinned panel, side panel, mobile) is always a column of cards, whatever the tab presentation is. Solo stays a main-tab-area concept. Header visibility is untouched: `showHeader` keys off `presentation`, not the variant, so a solo tab still shows no bare title row. The `Fields` widget does not regain the header it lost in #23109. ## Measured Side panel, custom object whose Home tab holds a single Fields widget (1600x1000, panel at x=1200): | | Card padding | First label x | |---|---|---| | main | `0px` | 1221 | | this PR | `12px` | 1233 | 12px restored, matching what the pinned left panel gives the same widget. ## Trade-off worth a second opinion In the side panel and on mobile, the activity widgets now resolve to `'side-column'` instead of `'solo'`, so they pick up the card's 12px on top of their own 24px, i.e. 36px instead of 24px. Nothing overlaps or clips, but it is a visible change on those tabs. If you would rather keep them at 24px, the follow-up is to drop the intrinsic `spacing[6]` from the activity cards and let the surface own the gutter everywhere. ## Test plan - `getWidgetCardVariant` tests extended: `'side-column'` now wins over `'solo'` for each of `isInPinnedTab` / `isMobile` / `isInSidePanel`. 13 tests pass. - 88 suites / 620 tests across `page-layout/widgets` pass. - `lint:diff-with-main twenty-front` clean. - Verified against a local stack: side panel on a single-widget Home tab, before and after. --- .../__tests__/getWidgetCardVariant.test.ts | 18 ++++++++++++++++++ .../widgets/utils/getWidgetCardVariant.ts | 10 +++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts b/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts index b72a81a34a..7d69954ed0 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/utils/__tests__/getWidgetCardVariant.test.ts @@ -72,5 +72,23 @@ describe('getWidgetCardVariant', () => { }), ).toBe('side-column'); }); + + it.each([ + ['isInPinnedTab', { isInPinnedTab: true }], + ['isMobile', { isMobile: true }], + ['isInSidePanel', { isInSidePanel: true }], + ])( + "returns 'side-column' over 'solo' when %s is true", + (_label, override) => { + expect( + getWidgetCardVariant({ + ...baseParams, + ...override, + presentation: 'solo', + pageLayoutType: PageLayoutType.RECORD_PAGE, + }), + ).toBe('side-column'); + }, + ); }); }); diff --git a/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts b/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts index 4b7d9243a0..9cf200d513 100644 --- a/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts +++ b/packages/twenty-front/src/modules/page-layout/widgets/utils/getWidgetCardVariant.ts @@ -17,12 +17,16 @@ export const getWidgetCardVariant = ({ isMobile, isInSidePanel, }: GetWidgetCardVariantParams): WidgetCardVariant => { + const isSideColumnContext = isInPinnedTab || isMobile || isInSidePanel; + + if (isSideColumnContext) { + return 'side-column'; + } + if (presentation === 'solo') { return 'solo'; } - const isSideColumnContext = isInPinnedTab || isMobile || isInSidePanel; - switch (pageLayoutType) { case PageLayoutType.DASHBOARD: return 'dashboard'; @@ -31,6 +35,6 @@ export const getWidgetCardVariant = ({ case PageLayoutType.RECORD_PAGE: case PageLayoutType.RECORD_INDEX: case null: - return isSideColumnContext ? 'side-column' : 'record-page'; + return 'record-page'; } };