feat(twenty-front): generalize the page primary/secondary bars (flat redesign) (#21308)

Replaces #21279 and #21282 with one clean PR from `main`.

Generalizes the settings primary-bar / secondary-bar card chrome to the
record index, record show and standalone pages via a shared
`PageCardLayout` + `PageCardHeader` (the side panel sits as a sibling of
the content card), and applies the new flat design direction: square
corners on the card, side panel and loading skeletons.

Iterating toward the new design (Figma node 102282-221623); the
confirmed direction and the explicit "remove rounded corners" change are
in, remaining designer specifics to follow.
This commit is contained in:
Félix Malfait
2026-06-08 22:47:05 +02:00
committed by GitHub
parent a48c158a66
commit bfefcd3755
29 changed files with 528 additions and 517 deletions
@@ -1,83 +0,0 @@
import { CommandMenuForMobile } from '@/command-menu/components/CommandMenuForMobile';
import { SidePanelForDesktop } from '@/side-panel/components/SidePanelForDesktop';
import { useCommandMenuHotKeys } from '@/command-menu/hooks/useCommandMenuHotKeys';
import { PageBody } from '@/ui/layout/page/components/PageBody';
import { styled } from '@linaria/react';
import { type ReactNode } from 'react';
import { useIsMobile } from 'twenty-ui-deprecated/utilities';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
type MainContainerLayoutWithSidePanelProps = {
children: ReactNode;
};
const StyledMainContainerLayoutForDesktop = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding-bottom: ${themeCssVariables.spacing[3]};
padding-right: ${themeCssVariables.spacing[3]};
`;
const StyledPageBodyForDesktopContainer = styled.div`
display: flex;
flex: 1 1 0;
flex-direction: column;
min-width: 0;
width: 0;
> * {
padding-bottom: 0;
padding-right: 0;
}
`;
const StyledMainContainerLayoutForMobile = styled.div`
display: flex;
flex: 1;
min-height: 0;
padding: 0;
`;
const StyledPageBodyForMobileContainer = styled.div`
display: flex;
flex: 1 1 0;
flex-direction: column;
min-height: 0;
min-width: 0;
width: 0;
> * {
padding-bottom: 0;
padding-left: ${themeCssVariables.spacing[1]};
padding-right: ${themeCssVariables.spacing['1.5']};
}
`;
export const MainContainerLayoutWithSidePanel = ({
children,
}: MainContainerLayoutWithSidePanelProps) => {
const isMobile = useIsMobile();
useCommandMenuHotKeys();
if (isMobile) {
return (
<StyledMainContainerLayoutForMobile>
<StyledPageBodyForMobileContainer>
<PageBody>{children}</PageBody>
</StyledPageBodyForMobileContainer>
<CommandMenuForMobile />
</StyledMainContainerLayoutForMobile>
);
}
return (
<StyledMainContainerLayoutForDesktop>
<StyledPageBodyForDesktopContainer>
<PageBody>{children}</PageBody>
</StyledPageBodyForDesktopContainer>
<SidePanelForDesktop />
</StyledMainContainerLayoutForDesktop>
);
};
@@ -1,21 +1,16 @@
import { styled } from '@linaria/react';
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
import { RecordBoardContainer } from '@/object-record/record-board/components/RecordBoardContainer';
import { RecordIndexTableContainer } from '@/object-record/record-index/components/RecordIndexTableContainer';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { RecordIndexCalendarContainer } from '@/object-record/record-index/components/RecordIndexCalendarContainer';
import { RecordIndexEmptyStateNotShared } from '@/object-record/record-index/components/RecordIndexEmptyStateNotShared';
import { RecordIndexFiltersToContextStoreEffect } from '@/object-record/record-index/components/RecordIndexFiltersToContextStoreEffect';
import { useHasCurrentViewNonReadableFields } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
@@ -24,7 +19,6 @@ const StyledContainer = styled.div`
flex-direction: column;
height: 100%;
overflow: hidden;
width: 100%;
`;
@@ -38,67 +32,43 @@ const StyledContainerWithPadding = styled.div`
export const RecordIndexContainer = () => {
const recordIndexViewType = useAtomStateValue(recordIndexViewTypeState);
const {
objectNamePlural,
recordIndexId,
objectMetadataItem,
objectNameSingular,
} = useRecordIndexContextOrThrow();
const { recordIndexId, objectMetadataItem, objectNameSingular } =
useRecordIndexContextOrThrow();
const { hasCurrentViewNonReadableFields, nonReadableViewFieldInfo } =
useHasCurrentViewNonReadableFields(objectMetadataItem);
return (
<>
<StyledContainer>
<InformationBannerWrapper />
<SpreadsheetImportProvider>
<ViewBar
isReadOnly={hasCurrentViewNonReadableFields}
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.TABLE}
<StyledContainer>
{hasCurrentViewNonReadableFields ? (
<RecordIndexEmptyStateNotShared
nonReadableViewFieldInfo={nonReadableViewFieldInfo}
/>
) : (
<>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.TABLE && (
<RecordIndexTableContainer recordTableId={recordIndexId} />
)}
{recordIndexViewType === ViewType.KANBAN && (
<StyledContainerWithPadding>
<RecordBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
{hasCurrentViewNonReadableFields ? (
<RecordIndexEmptyStateNotShared
nonReadableViewFieldInfo={nonReadableViewFieldInfo}
/>
) : (
<>
<RecordIndexFiltersToContextStoreEffect />
{recordIndexViewType === ViewType.TABLE && (
<RecordIndexTableContainer recordTableId={recordIndexId} />
)}
{recordIndexViewType === ViewType.KANBAN && (
<StyledContainerWithPadding>
<RecordBoardContainer
recordBoardId={recordIndexId}
viewBarId={recordIndexId}
objectNameSingular={objectNameSingular}
/>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.CALENDAR && (
<StyledContainerWithPadding>
<RecordIndexCalendarContainer
recordCalendarInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</>
)}
</StyledContainer>
</>
</StyledContainerWithPadding>
)}
{recordIndexViewType === ViewType.CALENDAR && (
<StyledContainerWithPadding>
<RecordIndexCalendarContainer
recordCalendarInstanceId={recordIndexId}
viewBarInstanceId={recordIndexId}
/>
</StyledContainerWithPadding>
)}
</>
)}
</StyledContainer>
);
};
@@ -3,7 +3,7 @@ import { RecordIndexContextProvider } from '@/object-record/record-index/context
import { getCommandMenuIdFromRecordIndexId } from '@/command-menu-item/utils/getCommandMenuIdFromRecordIndexId';
import { CommandMenuComponentInstanceContext } from '@/command-menu/states/contexts/CommandMenuComponentInstanceContext';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject';
import { MainContainerLayoutWithSidePanel } from '@/object-record/components/MainContainerLayoutWithSidePanel';
import { RecordIndexViewBar } from '@/object-record/record-index/components/RecordIndexViewBar';
import { RecordComponentInstanceContextsWrapper } from '@/object-record/components/RecordComponentInstanceContextsWrapper';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { lastShowPageRecordIdState } from '@/object-record/record-field/ui/states/lastShowPageRecordId';
@@ -16,6 +16,7 @@ import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hook
import { useRecordIndexFieldMetadataDerivedStates } from '@/object-record/record-index/hooks/useRecordIndexFieldMetadataDerivedStates';
import { useRecordIndexIdFromCurrentContextStore } from '@/object-record/record-index/hooks/useRecordIndexIdFromCurrentContextStore';
import { RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS } from '@/ui/utilities/drag-select/constants/RecordIndecDragSelectBoundaryClass';
import { PageCardLayout } from '@/ui/layout/page/components/PageCardLayout';
import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle';
import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext';
import { styled } from '@linaria/react';
@@ -91,8 +92,12 @@ export const RecordIndexContainerGater = () => {
}}
>
<PageTitle title={objectMetadataItem.labelPlural} />
<RecordIndexPageHeader />
<MainContainerLayoutWithSidePanel>
<PageCardLayout
header={<RecordIndexPageHeader />}
secondaryBar={
hasObjectReadPermissions && <RecordIndexViewBar />
}
>
<StyledIndexContainer
className={RECORD_INDEX_DRAG_SELECT_BOUNDARY_CLASS}
>
@@ -105,7 +110,7 @@ export const RecordIndexContainerGater = () => {
<RecordIndexEmptyStateNotShared />
)}
</StyledIndexContainer>
</MainContainerLayoutWithSidePanel>
</PageCardLayout>
</CommandMenuComponentInstanceContext.Provider>
</RecordComponentInstanceContextsWrapper>
<RecordIndexLoadBaseOnContextStoreEffect />
@@ -1,5 +1,4 @@
import { RecordIndexCommandMenu } from '@/command-menu-item/components/RecordIndexCommandMenu';
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState';
import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState';
@@ -7,7 +6,8 @@ import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/st
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { RecordIndexPageHeaderIcon } from '@/object-record/record-index/components/RecordIndexPageHeaderIcon';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { PageHeader } from '@/ui/layout/page/components/PageHeader';
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
import { PageCardHeader } from '@/ui/layout/page/components/PageCardHeader';
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
@@ -68,18 +68,19 @@ export const RecordIndexPageHeader = () => {
);
return (
<PageHeader
title={pageHeaderTitle}
Icon={() => (
<PageCardHeader
icon={
<RecordIndexPageHeaderIcon objectMetadataItem={objectMetadataItem} />
)}
>
{isDefined(contextStoreCurrentViewId) && (
<>
<RecordIndexCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</>
)}
</PageHeader>
}
title={pageHeaderTitle}
actionButton={
isDefined(contextStoreCurrentViewId) ? (
<>
<RecordIndexCommandMenu />
{!isLayoutCustomizationModeEnabled && <SidePanelToggleButton />}
</>
) : undefined
}
/>
);
};
@@ -1,8 +1,33 @@
import { PageContainer } from '@/ui/layout/page/components/PageContainer';
import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader';
import { styled } from '@linaria/react';
import Skeleton from 'react-loading-skeleton';
import { themeCssVariables } from 'twenty-ui-deprecated/theme-constants';
import { PageContentSkeletonLoader } from '~/loading/components/PageContentSkeletonLoader';
const StyledSecondaryBar = styled.div`
align-items: center;
border-bottom: 1px solid ${themeCssVariables.border.color.light};
box-sizing: border-box;
display: flex;
flex-shrink: 0;
justify-content: space-between;
min-height: ${themeCssVariables.spacing[10]};
padding: 0 ${themeCssVariables.spacing[3]};
`;
export const RecordIndexSkeletonLoader = () => (
<PageContainer>
<PageContentSkeletonLoader />
</PageContainer>
<PageContentSkeletonLoader
secondaryBar={
<StyledSecondaryBar>
<Skeleton
width={120}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
<Skeleton
width={180}
height={SKELETON_LOADER_HEIGHT_SIZES.standard.s}
/>
</StyledSecondaryBar>
}
/>
);
@@ -0,0 +1,39 @@
import { ObjectOptionsDropdown } from '@/object-record/object-options-dropdown/components/ObjectOptionsDropdown';
import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect';
import { useRecordIndexContextOrThrow } from '@/object-record/record-index/contexts/RecordIndexContext';
import { useHasCurrentViewNonReadableFields } from '@/object-record/record-index/hooks/useHasCurrentViewNonReadableFields';
import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState';
import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { ViewBar } from '@/views/components/ViewBar';
import { ViewType } from '@/views/types/ViewType';
export const RecordIndexViewBar = () => {
const recordIndexViewType = useAtomStateValue(recordIndexViewTypeState);
const { objectNamePlural, recordIndexId, objectMetadataItem } =
useRecordIndexContextOrThrow();
const { hasCurrentViewNonReadableFields } =
useHasCurrentViewNonReadableFields(objectMetadataItem);
return (
<SpreadsheetImportProvider>
<ViewBar
isReadOnly={hasCurrentViewNonReadableFields}
viewBarId={recordIndexId}
optionsDropdownButton={
<ObjectOptionsDropdown
recordIndexId={recordIndexId}
objectMetadataItem={objectMetadataItem}
viewType={recordIndexViewType ?? ViewType.TABLE}
/>
}
/>
<RecordIndexViewBarEffect
objectNamePlural={objectNamePlural}
viewBarId={recordIndexId}
/>
</SpreadsheetImportProvider>
);
};