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:
committed by
GitHub
parent
6afc3d33a4
commit
536b91c1dd
@@ -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>
|
||||
|
||||
+107
@@ -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>
|
||||
);
|
||||
};
|
||||
BIN
Binary file not shown.
|
After Width: | Height: | Size: 73 KiB |
BIN
Binary file not shown.
|
After Width: | Height: | Size: 65 KiB |
+31
-12
@@ -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>
|
||||
|
||||
+81
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user