fix(page-layout): let the column surface win over the solo presentation (#23412)

## 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.
This commit is contained in:
Félix Malfait
2026-07-28 13:47:07 +02:00
committed by GitHub
parent 38e9d231bc
commit 3f0236b590
2 changed files with 25 additions and 3 deletions
@@ -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');
},
);
});
});
@@ -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';
}
};