Files
twenty/packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx
T
Weiko aa4aea0f9b Fix layout edition mode dark mode text color (#19992)
Summary

Fixed the dark-mode regression in the layout customization banner at
packages/twenty-front/src/modules/layout-customization/components/LayoutCustomizationBar.tsx:

- The banner's background is themeCssVariables.color.blue
(theme-independent), so its text color must be theme-independent too.
- Replaced color: ${themeCssVariables.font.color.inverted} (which
resolves to near-black in dark mode) with color:
${GRAY_SCALE_LIGHT.gray1} (always white), matching the convention used
 in AnimatedButton.tsx for text on colored backgrounds.

## Before
<img width="1508" height="185" alt="Screenshot 2026-04-22 at 19 50 03"
src="https://github.com/user-attachments/assets/b0a95fc2-05d5-4207-9d72-64a83daf94ae"
/>

## After
<img width="1511" height="190" alt="Screenshot 2026-04-22 at 19 49 39"
src="https://github.com/user-attachments/assets/9ce33353-412f-4db2-9322-a6f293f6f1b4"
/>
2026-04-23 08:36:12 +00:00

147 lines
5.0 KiB
TypeScript

import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { AnimatePresence, motion } from 'framer-motion';
import { useAtomValue } from 'jotai';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { IconCheck, IconPaint } from 'twenty-ui/display';
import { GRAY_SCALE_LIGHT } from 'twenty-ui/theme';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { LayoutCustomizationBarMenuDropdown } from '@/layout-customization/components/LayoutCustomizationBarMenuDropdown';
import { LayoutCustomizationBarResetConfirmationModal } from '@/layout-customization/components/LayoutCustomizationBarResetConfirmationModal';
import { useCancelLayoutCustomization } from '@/layout-customization/hooks/useCancelLayoutCustomization';
import { useIsLayoutCustomizationDirty } from '@/layout-customization/hooks/useIsLayoutCustomizationDirty';
import { useSaveLayoutCustomization } from '@/layout-customization/hooks/useSaveLayoutCustomization';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { objectMetadataItemsSelector } from '@/object-metadata/states/objectMetadataItemsSelector';
import { currentPageLayoutIdState } from '@/page-layout/states/currentPageLayoutIdState';
import { pageLayoutPersistedComponentState } from '@/page-layout/states/pageLayoutPersistedComponentState';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { PageLayoutType } from '~/generated-metadata/graphql';
const StyledContainer = styled.div`
align-items: center;
background: ${themeCssVariables.color.blue};
box-sizing: border-box;
color: ${GRAY_SCALE_LIGHT.gray1};
display: flex;
justify-content: space-between;
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
width: 100%;
button,
button * {
color: ${GRAY_SCALE_LIGHT.gray1};
}
button[type='submit']:not(:disabled):not(:focus) {
border-color: color(display-p3 1 1 1 / 0.5);
}
`;
const StyledLeftSection = styled.div`
align-items: center;
display: flex;
flex: 1;
gap: ${themeCssVariables.spacing[2]};
`;
const StyledTitle = styled.span`
align-items: center;
display: flex;
flex: 1;
gap: ${themeCssVariables.spacing[2]};
justify-content: center;
text-align: center;
`;
const StyledRightSection = styled.div`
align-items: center;
display: flex;
flex: 1;
justify-content: flex-end;
`;
const LayoutCustomizationBarContent = () => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const { save, isSaving } = useSaveLayoutCustomization();
const { cancel } = useCancelLayoutCustomization();
const { isDirty } = useIsLayoutCustomizationDirty();
const currentPageLayoutId = useAtomStateValue(currentPageLayoutIdState);
const persistedPageLayout = useAtomValue(
pageLayoutPersistedComponentState.atomFamily({
instanceId: currentPageLayoutId ?? '',
}),
);
const objectMetadataItems = useAtomStateValue(objectMetadataItemsSelector);
const recordPageLayoutObject =
isDefined(currentPageLayoutId) &&
persistedPageLayout?.type === PageLayoutType.RECORD_PAGE
? objectMetadataItems.find(
(item) => item.id === persistedPageLayout.objectMetadataId,
)
: undefined;
const title = isDefined(recordPageLayoutObject)
? t`${recordPageLayoutObject.labelPlural} layout edition`
: t`Layout customization`;
return (
<motion.div
initial={{ height: 0, opacity: 0 }}
animate={{ height: 'auto', opacity: 1 }}
exit={{ height: 0, opacity: 0 }}
transition={{
duration: theme.animation.duration.normal,
ease: 'easeInOut',
}}
>
<StyledContainer data-globally-prevent-click-outside="true">
<StyledLeftSection>
{isDefined(recordPageLayoutObject) && (
<LayoutCustomizationBarMenuDropdown />
)}
</StyledLeftSection>
<StyledTitle>
<IconPaint size={theme.icon.size.md} />
{title}
</StyledTitle>
<StyledRightSection>
<SaveAndCancelButtons
onSave={save}
onCancel={cancel}
isSaveDisabled={!isDirty || isSaving}
isCancelDisabled={isSaving}
isLoading={isSaving}
inverted
saveIcon={IconCheck}
/>
</StyledRightSection>
</StyledContainer>
{isDefined(recordPageLayoutObject) && isDefined(currentPageLayoutId) && (
<LayoutCustomizationBarResetConfirmationModal
pageLayoutId={currentPageLayoutId}
/>
)}
</motion.div>
);
};
export const LayoutCustomizationBar = () => {
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
);
return (
<AnimatePresence>
{isLayoutCustomizationModeEnabled && <LayoutCustomizationBarContent />}
</AnimatePresence>
);
};