feat(settings): move settings chrome into a single rounded card (#21131)

## What

Replaces `SubMenuTopBarContainer` with a settings-specific
`SettingsPageLayout` that puts the whole page chrome — breadcrumb,
centered title, actions, an optional secondary bar (tabs or wizard
step), and the 760px body — inside **one rounded card**, with
`SidePanelForDesktop` as a sibling. Title, tabs and body content share
one centered vertical axis at every card width.

Supersedes #21122. One PR, no feature flag.

## New components (`@/settings/components/layout/`)

- **SettingsPageLayout** — owns the rounded card + side-panel sibling,
`useCommandMenuHotKeys`, mobile command menu
- **SettingsPageHeader** — breadcrumb · centered title · actions in a
symmetric `1fr auto 1fr` grid (symmetric padding throughout)
- **SettingsSecondaryBar** — the secondary row, bracketed by top +
bottom borders
- **SettingsTabBar** — centered tabs reusing `activeTabIdComponentState`
+ `TabListFromUrlOptionalEffect` for URL-hash sync (does not touch the
shared `TabList`)
- **SettingsWizardStepBar** — back arrow · "N. Label" · optional
trailing slot

## Migrations

- Bulk rename across ~80 call sites (`SubMenuTopBarContainer` →
`SettingsPageLayout`); old component deleted.
- 5 tab pages (AI, APIs & Webhooks, Applications, Members, Role) + the
Data Model object-detail page render their tabs in `secondaryBar`
(object-detail keeps "See records" / "New Field" in the header actions).
- The 2 role object-level steps render the wizard step bar with working
back navigation.
- Accounts consolidated into **General / Emails / Calendars** tabs;
standalone `SettingsAccountsEmails` / `SettingsAccountsCalendars` pages
+ routes + stories removed. `SettingsPath.AccountsEmails` /
`AccountsCalendars` now resolve to `accounts#emails` /
`accounts#calendars`, so existing `getSettingsPath()` links deep-link to
the right tab via the existing hash sync — no call-site changes.

## Verification

- `nx typecheck twenty-front` and `nx lint twenty-front` both clean.
- Browser (logged-in workspace): title / tab / body / card centers align
on a single axis at multiple widths — width-invariant, so alignment
holds when the AI side panel (a sibling) shrinks the card. Rounded card
with even gaps on all four sides; tab row bracketed by two 1px lines;
no-tab pages render header → body with no lines; wizard back navigation
works; `…/accounts#emails` opens the Emails tab.

The shared `PageHeader` and `TabList` are untouched. The settings side
panel itself isn't wired to open yet — that's a follow-up PR.
This commit is contained in:
Félix Malfait
2026-06-02 14:22:57 +02:00
committed by GitHub
parent e721ebe300
commit 431f6ae98f
107 changed files with 1367 additions and 1008 deletions
@@ -1,84 +0,0 @@
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import {
Breadcrumb,
type BreadcrumbProps,
} from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { isDefined } from 'twenty-shared/utils';
import { styled } from '@linaria/react';
import { type JSX, type ReactNode } from 'react';
import { PageHeader } from './PageHeader';
import { themeCssVariables } from 'twenty-ui/theme-constants';
type SubMenuTopBarContainerProps = {
children: JSX.Element | JSX.Element[];
title?: string | JSX.Element;
reserveTitleSpace?: boolean;
actionButton?: ReactNode;
className?: string;
links: BreadcrumbProps['links'];
tag?: JSX.Element;
};
const SETTINGS_CONTENT_MAX_WIDTH = 760;
const StyledContainer = styled.div`
display: flex;
flex-direction: column;
width: 100%;
`;
// flex: 1 + min-height: 0 are required for PagePanel's overflow chain — the
// child must participate in the flex height calc rather than collapse to content.
const StyledBodyContentWrapper = styled.div`
display: flex;
flex: 1;
flex-direction: column;
margin: 0 auto;
max-width: ${SETTINGS_CONTENT_MAX_WIDTH}px;
min-height: 0;
width: 100%;
`;
const StyledTitle = styled.span<{ reserveTitleSpace?: boolean }>`
color: ${themeCssVariables.font.color.primary};
display: flex;
font-size: ${themeCssVariables.font.size.lg};
font-weight: ${themeCssVariables.font.weight.semiBold};
gap: ${themeCssVariables.spacing[2]};
line-height: 1.2;
margin: ${themeCssVariables.spacing[8]} ${themeCssVariables.spacing[8]}
${themeCssVariables.spacing[2]};
min-height: ${({ reserveTitleSpace }) =>
reserveTitleSpace ? themeCssVariables.spacing[5] : 'none'};
`;
export const SubMenuTopBarContainer = ({
children,
title,
tag,
reserveTitleSpace,
actionButton,
className,
links,
}: SubMenuTopBarContainerProps) => {
return (
<StyledContainer className={className}>
<PageHeader title={<Breadcrumb links={links} />}>
{actionButton}
</PageHeader>
<MainContainerLayoutWithSidePanel>
<StyledBodyContentWrapper>
<InformationBannerWrapper />
{(isDefined(title) || reserveTitleSpace === true) && (
<StyledTitle reserveTitleSpace={reserveTitleSpace}>
{title}
{tag}
</StyledTitle>
)}
{children}
</StyledBodyContentWrapper>
</MainContainerLayoutWithSidePanel>
</StyledContainer>
);
};
@@ -164,7 +164,7 @@ export const Settings: Story = {
<NavigationDrawerSectionTitle label="Workspace" />
<NavigationDrawerItem
label="General"
to={getSettingsPath(SettingsPath.Workspace)}
to={getSettingsPath(SettingsPath.General)}
Icon={IconSettings}
/>
<NavigationDrawerItem