Update record page layout card (#23654)

## Summary

- Redesign the data-model Layout card to match the record-page
customization design.
- Add dedicated light and dark cover assets and preserve the existing
customization workflow.
- Reuse a dedicated discovery-hero footer across the workspace and
record layout cards.
- Match the Figma cover, footer, icon, typography, and spacing metrics.

## Before/After

<img width="1588" height="720" alt="before-after"
src="https://github.com/user-attachments/assets/28be577c-9f1b-40f1-99e2-66eeafbac75b"
/>
This commit is contained in:
Thomas des Francs
2026-08-03 11:31:25 +02:00
committed by GitHub
parent 6afc3d33a4
commit 536b91c1dd
7 changed files with 228 additions and 19 deletions
@@ -12,12 +12,12 @@ import { Card } from 'twenty-ui/surfaces';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { FeatureFlagKey } from '~/generated-metadata/graphql';
const COVER_HEIGHT = 150;
const DEFAULT_COVER_HEIGHT = 150;
const StyledCoverContainer = styled.div`
const StyledCoverContainer = styled.div<{ coverHeight: number }>`
background: ${themeCssVariables.background.secondary};
box-sizing: border-box;
height: ${COVER_HEIGHT}px;
height: ${({ coverHeight }) => coverHeight}px;
overflow: hidden;
position: relative;
`;
@@ -50,6 +50,7 @@ type SettingsDiscoveryHeroCardProps = {
darkSrc: string;
instanceIdPrefix: string;
tabs: SettingsCustomizeVideoModalTab[];
coverHeight?: number;
footer?: ReactNode;
playButtonAriaLabel?: string;
};
@@ -59,6 +60,7 @@ export const SettingsDiscoveryHeroCard = ({
darkSrc,
instanceIdPrefix,
tabs,
coverHeight = DEFAULT_COVER_HEIGHT,
footer,
playButtonAriaLabel,
}: SettingsDiscoveryHeroCardProps) => {
@@ -78,7 +80,7 @@ export const SettingsDiscoveryHeroCard = ({
return (
<>
<Card rounded>
<StyledCoverContainer>
<StyledCoverContainer coverHeight={coverHeight}>
<StyledImage src={src} alt="" aria-hidden />
{shouldDisplayVideo && (
<StyledOverlay>
@@ -0,0 +1,107 @@
import { styled } from '@linaria/react';
import { type ReactNode, useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { type IconComponent } from 'twenty-ui/icon';
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledContent = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.secondary};
display: flex;
gap: ${themeCssVariables.spacing[3]};
padding: ${themeCssVariables.spacing[4]};
`;
const StyledIconContainer = styled.div`
align-items: center;
background-color: ${themeCssVariables.background.primary};
border: 2px solid ${themeCssVariables.border.color.light};
border-radius: ${themeCssVariables.border.radius.sm};
display: flex;
height: ${themeCssVariables.spacing[8]};
justify-content: center;
min-width: ${themeCssVariables.spacing[8]};
width: ${themeCssVariables.spacing[8]};
`;
const StyledIcon = styled.div`
align-items: center;
display: inline-flex;
justify-content: center;
transform: rotate(-4deg);
`;
const StyledTextContainer = styled.div`
align-items: flex-start;
display: flex;
flex: 1;
flex-direction: column;
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
min-width: 0;
`;
const StyledTitle = styled.div`
align-items: center;
color: ${themeCssVariables.font.color.primary};
display: flex;
font-weight: ${themeCssVariables.font.weight.medium};
min-height: ${themeCssVariables.spacing[4]};
width: 100%;
`;
const StyledDescription = styled.div`
color: ${themeCssVariables.font.color.secondary};
font-size: ${themeCssVariables.font.size.sm};
line-height: ${themeCssVariables.text.lineHeight.lg};
overflow: hidden;
text-box-edge: cap alphabetic;
text-box-trim: trim-both;
`;
const StyledActionContainer = styled.div`
flex-shrink: 0;
margin-left: auto;
`;
type SettingsDiscoveryHeroCardFooterProps = {
Icon: IconComponent;
title: ReactNode;
description?: string;
action?: ReactNode;
};
export const SettingsDiscoveryHeroCardFooter = ({
Icon,
title,
description,
action,
}: SettingsDiscoveryHeroCardFooterProps) => {
const { theme } = useContext(ThemeContext);
return (
<StyledContent>
<StyledIconContainer>
<StyledIcon>
<Icon
size={theme.icon.size.lg}
color={theme.IllustrationIcon.color.gray}
stroke={theme.icon.stroke.md}
/>
</StyledIcon>
</StyledIconContainer>
<StyledTextContainer>
<StyledTitle>{title}</StyledTitle>
{description && (
<StyledDescription>
<OverflowingTextWithTooltip text={description} />
</StyledDescription>
)}
</StyledTextContainer>
{isDefined(action) && (
<StyledActionContainer>{action}</StyledActionContainer>
)}
</StyledContent>
);
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

@@ -1,13 +1,11 @@
import { useContext } from 'react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { AppPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { IconAddressBook, IconReload } from 'twenty-ui/icon';
import { IconAddressBook, IconPencil, IconReload } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { H2Title } from 'twenty-ui/typography';
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
@@ -15,7 +13,10 @@ import { type EnrichedObjectMetadataItem } from '@/object-metadata/types/Enriche
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { useResetPageLayoutToDefault } from '@/page-layout/hooks/useResetPageLayoutToDefault';
import { recordPageLayoutByObjectMetadataIdFamilySelector } from '@/page-layout/states/selectors/recordPageLayoutByObjectMetadataIdFamilySelector';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsDiscoveryHeroCardFooter } from '@/settings/components/SettingsDiscoveryHeroCardFooter';
import recordPageLayoutCoverDark from '@/settings/data-model/object-details/assets/record-page-layout-cover-dark.png';
import recordPageLayoutCoverLight from '@/settings/data-model/object-details/assets/record-page-layout-cover-light.png';
import { ObjectOpenRecordInPicker } from '@/settings/data-model/object-details/components/tabs/ObjectOpenRecordInPicker';
import { useHasPermissionFlag } from '@/settings/roles/hooks/useHasPermissionFlag';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
@@ -32,13 +33,13 @@ const StyledContentContainer = styled.div`
`;
const RESET_PAGE_LAYOUT_MODAL_ID = 'reset-page-layout-to-default-modal';
const RECORD_PAGE_LAYOUT_HERO_INSTANCE_ID_PREFIX = 'record-page-layout-hero';
type ObjectLayoutProps = {
objectMetadataItem: EnrichedObjectMetadataItem;
};
export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const navigateApp = useNavigateApp();
const { enterLayoutCustomizationMode } = useEnterLayoutCustomizationMode();
@@ -95,12 +96,30 @@ export const ObjectLayout = ({ objectMetadataItem }: ObjectLayoutProps) => {
title={t`Record page`}
description={t`Customize the workspace record page`}
/>
<SettingsCard
title={t`Customize record page`}
description={t`Customize how your record page looks.`}
Icon={<IconAddressBook size={theme.icon.size.md} />}
onClick={handleCustomizeRecordPage}
disabled={!hasLayoutsPermission || !isDefined(firstRecord)}
<SettingsDiscoveryHeroCard
lightSrc={recordPageLayoutCoverLight}
darkSrc={recordPageLayoutCoverDark}
coverHeight={153}
instanceIdPrefix={RECORD_PAGE_LAYOUT_HERO_INSTANCE_ID_PREFIX}
tabs={[]}
footer={
<SettingsDiscoveryHeroCardFooter
Icon={IconAddressBook}
title={t`Customize record page`}
description={t`Customize how your record page looks.`}
action={
<Button
title={t`Customize`}
variant="primary"
accent="blue"
size="small"
Icon={IconPencil}
onClick={handleCustomizeRecordPage}
disabled={!hasLayoutsPermission || !isDefined(firstRecord)}
/>
}
/>
}
/>
</Section>
<Section>
@@ -0,0 +1,81 @@
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsDiscoveryHeroCardFooter } from '@/settings/components/SettingsDiscoveryHeroCardFooter';
import recordPageLayoutCoverDark from '@/settings/data-model/object-details/assets/record-page-layout-cover-dark.png';
import recordPageLayoutCoverLight from '@/settings/data-model/object-details/assets/record-page-layout-cover-light.png';
import { styled } from '@linaria/react';
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
import { IconAddressBook, IconPencil } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/input';
import { ThemeProvider, themeCssVariables } from 'twenty-ui/theme-constants';
const StyledCanvas = styled.div`
background: ${themeCssVariables.background.primary};
box-sizing: border-box;
padding: 32px;
width: 758px;
`;
const meta: Meta = {
title: 'Modules/Settings/Data Model/Object Layout Hero Card',
};
export default meta;
type Story = StoryObj;
const renderCard = (colorScheme: 'light' | 'dark') => (
<ThemeProvider colorScheme={colorScheme} applyToRoot={false}>
<StyledCanvas>
<SettingsDiscoveryHeroCard
lightSrc={recordPageLayoutCoverLight}
darkSrc={recordPageLayoutCoverDark}
coverHeight={153}
instanceIdPrefix={`object-layout-${colorScheme}`}
tabs={[]}
footer={
<SettingsDiscoveryHeroCardFooter
Icon={IconAddressBook}
title="Customize record page"
description="Customize how your record page looks."
action={
<Button
title="Customize"
variant="primary"
accent="blue"
size="small"
Icon={IconPencil}
/>
}
/>
}
/>
</StyledCanvas>
</ThemeProvider>
);
export const Light: Story = {
render: () => renderCard('light'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Customize record page')).toBeVisible();
expect(
canvas.getByText('Customize how your record page looks.'),
).toBeVisible();
expect(canvas.getByRole('button', { name: 'Customize ...' })).toBeEnabled();
},
};
export const Dark: Story = {
render: () => renderCard('dark'),
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
expect(canvas.getByText('Customize record page')).toBeVisible();
expect(
canvas.getByText('Customize how your record page looks.'),
).toBeVisible();
expect(canvas.getByRole('button', { name: 'Customize ...' })).toBeEnabled();
},
};
@@ -1,6 +1,6 @@
import { useEnterLayoutCustomizationMode } from '@/layout-customization/hooks/useEnterLayoutCustomizationMode';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
import { SettingsDiscoveryHeroCardFooter } from '@/settings/components/SettingsDiscoveryHeroCardFooter';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsLayoutItemsStats } from '@/settings/layout/components/SettingsLayoutItemsStats';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
@@ -88,11 +88,11 @@ export const SettingsLayout = () => {
tabs={heroTabs}
playButtonAriaLabel={t`Watch customization demo`}
footer={
<SettingsOptionCardContentButton
<SettingsDiscoveryHeroCardFooter
Icon={IconLayoutDashboard}
title={t`Customize layout`}
description={t`Customize how your workspace looks.`}
Button={
action={
<Button
title={t`Customize`}
variant="primary"