fix(twenty-front): new layout fast-follows — settings drawer, loading & command menu (#21389)

Second batch of new-layout fast-follows (master:
twentyhq/core-team-issues#2478). All changes verified live against a
running workspace.

## Settings drawer & header
- **twentyhq/core-team-issues#2489** — sidebar icons render as plain
16px icons, no background tiles.
- **twentyhq/core-team-issues#2488** — Advanced toggle spans the full
drawer width; yellow dot removed.
- **twentyhq/core-team-issues#2497** — page title stays centered in the
settings header (breadcrumb stays left).
- **twentyhq/core-team-issues#2490** — Exit Settings control aligned to
the workspace switcher (24px, matching padding/gap).
- **twentyhq/core-team-issues#2499** — 2px vertical gap restored between
collapsible drawer section items.
- **twentyhq/core-team-issues#2491** — settings drawer rhythm now
matches the main app (28px items, 2px gaps, 28px section headers).
- **twentyhq/core-team-issues#2492** — Home/Chat tab switch no longer
flickers: both tab subtrees stay mounted (a shared
`NavigationDrawerTabbedContent` toggles visibility instead of remounting
+ flashing the chat skeleton).

## Loading states
- **twentyhq/core-team-issues#2486** — metadata loading shows an empty
body (no dense skeleton rows).
- **twentyhq/core-team-issues#2487** — settings table keeps its layout
while loading, with the shimmer localized to the first row's first cell.

## Command menu & navigation
- **twentyhq/core-team-issues#2501** — navigation section header height
matches the nav item rhythm (28px).
- **twentyhq/core-team-issues#2502 (part 1)** — the page side-panel
toggle stays as the dots glyph while the command menu is open, instead
of morphing into a second close control.

## New-field flow
- **twentyhq/core-team-issues#2494** — the new-field stepper moved from
a breadcrumb dropdown into a centered secondary wizard bar (back chevron
+ Save on the configure step); breadcrumb stays clean and the object
label is the centered title.

## Descoped (substantive bugs already fixed)
- **twentyhq/core-team-issues#2500** — command-menu highlight right
gutter: the menu-item base measures full-width, so it's likely a
scrollbar gutter on the list, not the shared component. Left for a
focused follow-up.
- **twentyhq/core-team-issues#2502 part 2** — moving the command-menu
close from left to right is cosmetic (the duplicate-control bug is fixed
by part 1) and would touch the shared `SidePanelTopBar` used by
search/AI panels.

## Verification
typecheck (tsgo) + oxlint + oxfmt green for all changed files; each
change DOM-measured / screenshotted in the running app.
This commit is contained in:
Félix Malfait
2026-06-10 11:02:36 +02:00
committed by GitHub
parent 6525af5ba4
commit adba66caea
16 changed files with 161 additions and 245 deletions
@@ -39,7 +39,6 @@ export const SettingsNavigationDrawerItem = ({
label={item.label}
to={href}
Icon={item.Icon}
withIconBackground
active={isActive}
modifier={item.modifier}
onClick={item.onClick}
@@ -55,7 +54,6 @@ export const SettingsNavigationDrawerItem = ({
label={item.label}
to={href || undefined}
Icon={item.Icon}
withIconBackground
active={isActive}
modifier={item.modifier}
onClick={item.onClick}
@@ -1,6 +1,4 @@
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSectionSkeletonLoader } from '@/settings/components/SettingsSectionSkeletonLoader';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { useContext } from 'react';
@@ -39,9 +37,7 @@ export const SettingsSkeletonLoader = () => {
}
showInformationBanner={false}
>
<SettingsPageContainer>
<SettingsSectionSkeletonLoader />
</SettingsPageContainer>
{null}
</PageCardLayout>
</SkeletonTheme>
);
@@ -29,6 +29,7 @@ export const SettingsPageLayout = ({
title={title}
tag={tag}
actionButton={actionButton}
centerTitle
/>
}
secondaryBar={
@@ -1,135 +0,0 @@
import { type SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { useContext } from 'react';
import { useLocation, useParams, useSearchParams } from 'react-router-dom';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconChevronDown } from 'twenty-ui-deprecated/display';
import { Button } from 'twenty-ui-deprecated/input';
import { MenuItem } from 'twenty-ui-deprecated/navigation';
import {
ThemeContext,
themeCssVariables,
} from 'twenty-ui-deprecated/theme-constants';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
const StyledContainer = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.tertiary};
cursor: default;
display: flex;
font-size: ${themeCssVariables.font.size.md};
`;
const StyledButtonContainer = styled.div`
position: relative;
width: 100%;
`;
const StyledDownChevronContainer = styled.span`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
position: absolute;
right: ${themeCssVariables.spacing['1.5']};
top: 50%;
transform: translateY(-50%);
`;
const StyledMenuItemWrapper = styled.div<{ disabled?: boolean }>`
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
width: 100%;
`;
const StyledSpan = styled.span`
margin-left: ${themeCssVariables.spacing[2]};
`;
const StyledButtonWrapper = styled.div`
button {
color: ${themeCssVariables.font.color.primary};
padding-right: ${themeCssVariables.spacing[6]};
}
`;
export const SettingsDataModelNewFieldBreadcrumbDropDown = () => {
const { theme } = useContext(ThemeContext);
const dropdownId = `settings-object-new-field-breadcrumb-dropdown`;
const { closeDropdown } = useCloseDropdown();
const navigate = useNavigateSettings();
const location = useLocation();
const { objectNamePlural = '' } = useParams();
const [searchParams] = useSearchParams();
const fieldType = searchParams.get('fieldType') as SettingsFieldType;
const isConfigureStep = location.pathname.includes('/configure');
const handleClick = (step: 'select' | 'configure') => {
if (step === 'configure' && isDefined(fieldType)) {
navigate(
SettingsPath.ObjectNewFieldConfigure,
{ objectNamePlural },
{ fieldType },
);
} else {
navigate(
SettingsPath.ObjectNewFieldSelect,
{ objectNamePlural },
fieldType ? { fieldType } : undefined,
);
}
closeDropdown(dropdownId);
};
return (
<StyledContainer>
{t`New Field`} <StyledSpan>-</StyledSpan>
<Dropdown
dropdownPlacement="bottom-start"
dropdownId={dropdownId}
clickableComponent={
<StyledButtonContainer>
<StyledDownChevronContainer>
<IconChevronDown size={theme.icon.size.md} />
</StyledDownChevronContainer>
{isConfigureStep ? (
<StyledButtonWrapper>
<Button variant="tertiary" title={t`2. Configure`} />
</StyledButtonWrapper>
) : (
<StyledButtonWrapper>
<Button variant="tertiary" title={t`1. Type`} />
</StyledButtonWrapper>
)}
</StyledButtonContainer>
}
dropdownComponents={
<DropdownContent>
<DropdownMenuItemsContainer>
<StyledMenuItemWrapper>
<MenuItem
text={t`1. Type`}
onClick={() => handleClick('select')}
selected={!isConfigureStep}
/>
</StyledMenuItemWrapper>
<StyledMenuItemWrapper disabled={!isDefined(fieldType)}>
<MenuItem
text={t`2. Configure`}
onClick={() => handleClick('configure')}
selected={isConfigureStep}
disabled={!isDefined(fieldType)}
/>
</StyledMenuItemWrapper>
</DropdownMenuItemsContainer>
</DropdownContent>
}
/>
</StyledContainer>
);
};
@@ -71,15 +71,10 @@ const StyledLoadingMore = styled.div`
text-align: center;
`;
const StyledSkeletonContainer = styled.div`
padding: ${themeCssVariables.spacing[2]};
`;
const StyledIntersectionObserver = styled.div`
height: 1px;
`;
const SKELETON_ROW_COUNT = 8;
const EVENT_LOG_SCROLL_WRAPPER_INSTANCE_ID = 'event-log-results-table';
const buildGridTemplateColumns = (
@@ -192,19 +187,23 @@ export const EventLogResultsTable = ({
<TableHeader key={column.id}>{t(column.label)}</TableHeader>
))}
</TableRow>
<TableRow gridTemplateColumns={gridTemplateColumns}>
{baseColumns.map((column, index) => (
<TableCell key={column.id}>
{index === 0 && (
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<Skeleton width={120} height={16} />
</SkeletonTheme>
)}
</TableCell>
))}
</TableRow>
</Table>
</StyledTableContainer>
<SkeletonTheme
baseColor={theme.background.tertiary}
highlightColor={theme.background.transparent.lighter}
borderRadius={4}
>
<StyledSkeletonContainer>
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, index) => (
<Skeleton height={40} key={index} style={{ marginBottom: 4 }} />
))}
</StyledSkeletonContainer>
</SkeletonTheme>
</ScrollWrapper>
</StyledScrollWrapperContainer>
);