diff --git a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx
index db526b9472..aba9881ac6 100644
--- a/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx
+++ b/packages/twenty-front/src/loading/components/UserOrMetadataLoader.tsx
@@ -1,9 +1,10 @@
import { styled } from '@linaria/react';
import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { NAVIGATION_DRAWER_CONSTRAINTS } from '@/ui/layout/resizable-panel/constants/NavigationDrawerConstraints';
import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
+import { ModalBackdrop } from 'twenty-ui/layout';
import { LeftPanelSkeletonLoader } from '~/loading/components/LeftPanelSkeletonLoader';
import { RightPanelSkeletonLoader } from '~/loading/components/RightPanelSkeletonLoader';
@@ -29,7 +30,12 @@ export const UserOrMetadataLoader = () => {
return (
- {showAuthModal && }
+ {showAuthModal && (
+
+ )}
diff --git a/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx b/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx
index 6895f3e013..01c119b3b2 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/components/ActionModal.tsx
@@ -65,7 +65,7 @@ export const ActionModal = ({
{isModalOpened && (
-
+
{previewedAttachment.name}
@@ -256,11 +245,11 @@ export const AttachmentList = ({
/>
-
+
-
+
@@ -275,9 +264,9 @@ export const AttachmentList = ({
documentUrl={getAttachmentUrl(previewedAttachment)}
/>
-
+
- ,
+ ,
document.body,
)}
>
diff --git a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx
index 612bce2fbd..72afad4e5a 100644
--- a/packages/twenty-front/src/modules/auth/components/AuthModal.tsx
+++ b/packages/twenty-front/src/modules/auth/components/AuthModal.tsx
@@ -1,7 +1,7 @@
import { AuthModalMountEffect } from '@/auth/components/AuthModalMountEffect';
import { AUTH_MODAL_ID } from '@/auth/constants/AuthModalId';
import { getAuthModalConfig } from '@/auth/utils/getAuthModalConfig';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { styled } from '@linaria/react';
import React from 'react';
@@ -23,11 +23,11 @@ export const AuthModal = ({ children }: AuthModalProps) => {
return (
<>
-
{config.showScrollWrapper ? (
@@ -36,7 +36,7 @@ export const AuthModal = ({ children }: AuthModalProps) => {
) : (
<>{children}>
)}
-
+
>
);
};
diff --git a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx
index 8f87ee9eb4..02ff8e5192 100644
--- a/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx
+++ b/packages/twenty-front/src/modules/auth/components/VerifyEmailEffect.tsx
@@ -8,7 +8,7 @@ import { useVerifyLogin } from '@/auth/hooks/useVerifyLogin';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useLingui } from '@lingui/react/macro';
import { useEffect, useState } from 'react';
import { useSearchParams } from 'react-router-dom';
@@ -126,9 +126,9 @@ export const VerifyEmailEffect = () => {
if (isError) {
return (
-
+
-
+
);
}
diff --git a/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx b/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx
index 60edafa73d..b1f9f3320c 100644
--- a/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx
+++ b/packages/twenty-front/src/modules/auth/components/__stories__/VerifyEmailEffect.stories.tsx
@@ -5,14 +5,14 @@ import { MemoryRouter, Route, Routes } from 'react-router-dom';
// Mock component that just renders the error state of VerifyEmailEffect directly
// (since normal VerifyEmailEffect has async logic that's hard to test in Storybook)
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
const VerifyEmailEffectErrorState = ({ email = 'user@example.com' }) => {
return (
-
+
-
+
);
};
@@ -29,7 +29,7 @@ const meta: Meta = {
],
parameters: {
codeSection: {
- docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmailEffect, always wrap it with Modal.Content to maintain consistent styling.',
+ docs: 'IMPORTANT: When rendering EmailVerificationSent from VerifyEmailEffect, always wrap it with ModalContent to maintain consistent styling.',
},
},
};
diff --git a/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts b/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts
index 31b58c79ce..2f6ec34b72 100644
--- a/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts
+++ b/packages/twenty-front/src/modules/auth/constants/AuthModalConfig.ts
@@ -1,12 +1,9 @@
-import {
- type ModalSize,
- type ModalVariants,
-} from '@/ui/layout/modal/components/Modal';
+import { type ModalOverlay, type ModalSize } from 'twenty-ui/layout';
import { AppPath } from 'twenty-shared/types';
type AuthModalConfigType = {
size: ModalSize;
- variant: ModalVariants;
+ overlay: ModalOverlay;
showScrollWrapper: boolean;
};
@@ -16,12 +13,12 @@ export const AUTH_MODAL_CONFIG: {
} = {
default: {
size: 'medium',
- variant: 'primary',
+ overlay: 'dark',
showScrollWrapper: true,
},
[AppPath.BookCall]: {
size: 'extraLarge',
- variant: 'transparent',
+ overlay: 'transparent',
showScrollWrapper: false,
},
};
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/EmailVerificationSent.stories.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/EmailVerificationSent.stories.tsx
index 2c88fe0c08..7da27a7c05 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/EmailVerificationSent.stories.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/__stories__/EmailVerificationSent.stories.tsx
@@ -1,24 +1,17 @@
import { type Meta, type StoryObj } from '@storybook/react-vite';
import { EmailVerificationSent } from '@/auth/sign-in-up/components/EmailVerificationSent';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { ComponentDecorator } from 'twenty-ui/testing';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
-// Wrap the component in Modal.Content to reflect how it's used in the app
-const RenderWithModal = (
+const RenderWithModalContent = (
args: React.ComponentProps,
) => {
return (
-
-
-
-
-
+
+
+
);
};
@@ -28,10 +21,10 @@ const meta: Meta = {
decorators: [ComponentDecorator, SnackBarDecorator],
parameters: {
codeSection: {
- docs: 'This component should always be wrapped with Modal.Content in the app.\n\nCorrect usage:\n```tsx\n\n \n\n```\n',
+ docs: 'This component should always be wrapped with ModalContent in the app.\n\nCorrect usage:\n```tsx\n\n \n\n```\n',
},
},
- render: RenderWithModal,
+ render: RenderWithModalContent,
};
export default meta;
diff --git a/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx b/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx
index c9c4ec21b7..c525bda7c0 100644
--- a/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx
+++ b/packages/twenty-front/src/modules/billing/components/SettingsBillingSubscriptionInfo.tsx
@@ -507,7 +507,7 @@ export const SettingsBillingSubscriptionInfo = ({
)}
1
? t`Remove ${favoriteCount} favorites?`
diff --git a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx
index ce403cb489..2383ffb7ce 100644
--- a/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx
+++ b/packages/twenty-front/src/modules/navigation-menu-item/components/CurrentWorkspaceMemberNavigationMenuItems.tsx
@@ -317,7 +317,7 @@ export const CurrentWorkspaceMemberNavigationMenuItems = ({
{isModalOpened &&
createPortal(
1
? t`Remove ${navigationMenuItemCount} navigation menu items?`
diff --git a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx
index 688f6271e3..3808efe9c5 100644
--- a/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field-list/record-detail-section/relation/components/RecordDetailRelationRecordsListItem.tsx
@@ -278,7 +278,7 @@ export const RecordDetailRelationRecordsListItem = ({
{createPortal(
diff --git a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx
index 9977208905..941b093676 100644
--- a/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-group/components/RecordGroupReorderConfirmationModal.tsx
@@ -21,7 +21,7 @@ export const RecordGroupReorderConfirmationModal = ({
<>
{createPortal(
{
return (
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminDeleteJobsConfirmationModal.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminDeleteJobsConfirmationModal.tsx
index dadb58cc66..f404950b88 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminDeleteJobsConfirmationModal.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminDeleteJobsConfirmationModal.tsx
@@ -2,14 +2,14 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { plural, t } from '@lingui/core/macro';
type SettingsAdminDeleteJobsConfirmationModalProps = {
- modalId: string;
+ modalInstanceId: string;
jobCount: number;
onConfirm: () => void;
onClose?: () => void;
};
export const SettingsAdminDeleteJobsConfirmationModal = ({
- modalId,
+ modalInstanceId,
jobCount,
onConfirm,
onClose,
@@ -26,7 +26,7 @@ export const SettingsAdminDeleteJobsConfirmationModal = ({
return (
0 ? selectedCount : failedJobs.length}
onConfirm={confirmRetrySelected}
/>
diff --git a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminRetryJobsConfirmationModal.tsx b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminRetryJobsConfirmationModal.tsx
index d0c895f0ac..d37fdca29a 100644
--- a/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminRetryJobsConfirmationModal.tsx
+++ b/packages/twenty-front/src/modules/settings/admin-panel/health-status/components/SettingsAdminRetryJobsConfirmationModal.tsx
@@ -2,14 +2,14 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { plural, t } from '@lingui/core/macro';
type SettingsAdminRetryJobsConfirmationModalProps = {
- modalId: string;
+ modalInstanceId: string;
jobCount: number;
onConfirm: () => void;
onClose?: () => void;
};
export const SettingsAdminRetryJobsConfirmationModal = ({
- modalId,
+ modalInstanceId,
jobCount,
onConfirm,
onClose,
@@ -26,7 +26,7 @@ export const SettingsAdminRetryJobsConfirmationModal = ({
return (
)}
diff --git a/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx b/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx
index 028e204a20..1fbe4f3897 100644
--- a/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx
+++ b/packages/twenty-front/src/modules/settings/members/components/MemberPermissionsTab.tsx
@@ -150,7 +150,7 @@ export const MemberPermissionsTab = ({
{pendingRole && (
{
@@ -109,7 +109,7 @@ export const DeleteAccount = () => {
diff --git a/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx b/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx
index c5cc85691b..52c01672be 100644
--- a/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx
+++ b/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx
@@ -43,7 +43,7 @@ export const DeleteWorkspace = () => {
/>
{
void;
};
export const SpreadSheetImportModalWrapper = ({
- modalId,
+ modalInstanceId,
children,
onClose,
}: SpreadSheetImportModalWrapperProps) => {
const { rtl } = useSpreadsheetImportInternal();
return (
-
-
-
- {children}
-
-
+
+
+
+ {children}
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx
index 1e64f28dc1..332549dc60 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx
@@ -1,19 +1,16 @@
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { Modal } from '@/ui/layout/modal/components/Modal';
import { t } from '@lingui/core/macro';
import { CircularProgressBar } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
+import { ModalFooter } from 'twenty-ui/layout';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
-const StyledFooter = styled(Modal.Footer)`
+const StyledFooter = styled(ModalFooter)`
border-top: 1px solid ${themeCssVariables.border.color.medium};
box-shadow: ${themeCssVariables.boxShadow.strong};
- gap: 10px;
justify-content: space-between;
- padding: ${themeCssVariables.spacing[4]};
- height: auto;
`;
type StepNavigationButtonProps = {
@@ -34,7 +31,7 @@ export const StepNavigationButton = ({
isContinueDisabled = false,
}: StepNavigationButtonProps) => {
return (
-
+
{!isUndefinedOrNull(onBack) && (
{
return (
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
index 496797e0bf..05cca8ce0c 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ImportDataStep.tsx
@@ -3,19 +3,12 @@ import { StepNavigationButton } from '@/spreadsheet-import/components/StepNaviga
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { spreadsheetImportCreatedRecordsProgressState } from '@/spreadsheet-import/states/spreadsheetImportCreatedRecordsProgressState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { t } from '@lingui/core/macro';
import { Loader } from 'twenty-ui/feedback';
-const StyledContent = styled(Modal.Content)`
- align-items: center;
- display: flex;
- justify-content: center;
- padding: 0px;
-`;
-
const StyledHeader = styled.span`
color: ${themeCssVariables.font.color.primary};
font-size: ${themeCssVariables.font.size.md};
@@ -50,11 +43,11 @@ export const ImportDataStep = ({
return (
<>
-
+
{t`Importing Data ...`}
{t`${formattedCreatedRecordsProgress} out of ${formattedRecordsToImportCount} records imported.`}
-
+
>
);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx
index eefa904c91..eccf420a6b 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep.tsx
@@ -14,7 +14,7 @@ import { setIgnoreColumn } from '@/spreadsheet-import/utils/setIgnoreColumn';
import { setSubColumn } from '@/spreadsheet-import/utils/setSubColumn';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { DO_NOT_IMPORT_OPTION_KEY } from '@/spreadsheet-import/constants/DoNotImportOptionKey';
import { ColumnGrid } from '@/spreadsheet-import/steps/components/MatchColumnsStep/components/ColumnGrid';
@@ -33,11 +33,6 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { Trans, useLingui } from '@lingui/react/macro';
import { themeCssVariables } from 'twenty-ui/theme-constants';
-const StyledContent = styled(Modal.Content)`
- align-items: center;
- padding: 0px;
-`;
-
const StyledColumnsContainer = styled.div`
align-items: center;
display: flex;
@@ -276,7 +271,7 @@ export const MatchColumnsStep = ({
return (
<>
-
+
-
+
-
+
-
+
-
+
setValue(value)} value={value}>
@@ -124,7 +116,7 @@ export const SelectSheetStep = ({
))}
-
+
handleOnContinue(value)}
onBack={onBack}
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx
index 37c16883c6..3cf1083d63 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepper.tsx
@@ -1,10 +1,9 @@
-import { styled } from '@linaria/react';
import { useCallback, useContext, useState } from 'react';
import { ThemeContext } from 'twenty-ui/theme';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { ImportDataStep } from '@/spreadsheet-import/steps/components/ImportDataStep';
import { type SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep';
@@ -16,12 +15,6 @@ import { SelectSheetStep } from './SelectSheetStep/SelectSheetStep';
import { UploadStep } from './UploadStep/UploadStep';
import { ValidationStep } from './ValidationStep/ValidationStep';
-const StyledProgressBarContainer = styled(Modal.Content)`
- align-items: center;
- display: flex;
- justify-content: center;
-`;
-
type SpreadsheetImportStepperProps = {
nextStep: () => void;
prevStep: () => void;
@@ -137,13 +130,13 @@ export const SpreadsheetImportStepper = ({
case SpreadsheetImportStepType.loading:
default:
return (
-
+
-
+
);
}
};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx
index 8c593ed396..0386f0f749 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/SpreadsheetImportStepperContainer.tsx
@@ -1,5 +1,3 @@
-import { styled } from '@linaria/react';
-
import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -7,24 +5,12 @@ import { StepBar } from '@/ui/navigation/step-bar/components/StepBar';
import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar';
import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState';
-import { Modal } from '@/ui/layout/modal/components/Modal';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
-import { MOBILE_VIEWPORT, themeCssVariables } from 'twenty-ui/theme-constants';
+import { themeCssVariables } from 'twenty-ui/theme-constants';
+import { ModalHeader } from 'twenty-ui/layout';
import { SpreadsheetImportStepper } from './SpreadsheetImportStepper';
-const StyledHeader = styled(Modal.Header)`
- background-color: ${themeCssVariables.background.secondary};
- border-bottom: 1px solid ${themeCssVariables.border.color.medium};
- padding: 0px ${themeCssVariables.spacing[30]};
- height: 60px;
- flex-shrink: 0;
- @media (max-width: ${MOBILE_VIEWPORT}px) {
- padding-left: ${themeCssVariables.spacing[4]};
- padding-right: ${themeCssVariables.spacing[4]};
- }
-`;
-
export const SpreadsheetImportStepperContainer = () => {
const { t } = useLingui();
@@ -50,7 +36,11 @@ export const SpreadsheetImportStepperContainer = () => {
return (
<>
-
+
{spreadsheetImportDialog.isStepBarVisible && (
{steps.map((key) => (
@@ -62,7 +52,7 @@ export const SpreadsheetImportStepperContainer = () => {
))}
)}
-
+
>
);
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx
index c911280664..18e47f28fe 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/UploadStep.tsx
@@ -1,10 +1,8 @@
import { t } from '@lingui/core/macro';
-import { styled } from '@linaria/react';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useCallback, useState } from 'react';
import { type WorkBook } from 'xlsx-ugnis';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useComputeColumnSuggestionsAndAutoMatch } from '@/spreadsheet-import/hooks/useComputeColumnSuggestionsAndAutoMatch';
import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal';
@@ -14,10 +12,6 @@ import { exceedsMaxRecords } from '@/spreadsheet-import/utils/exceedsMaxRecords'
import { mapWorkbook } from '@/spreadsheet-import/utils/mapWorkbook';
import { DropZone } from './components/DropZone';
-const StyledContent = styled(Modal.Content)`
- padding: ${themeCssVariables.spacing[6]};
-`;
-
type UploadStepProps = {
setUploadedFile: (file: File) => void;
setCurrentStepState: (data: any) => void;
@@ -118,8 +112,8 @@ export const UploadStep = ({
);
return (
-
+
-
+
);
};
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx
index 1dcdd5cca8..5018501ad5 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx
@@ -12,7 +12,7 @@ import { type SpreadsheetColumns } from '@/spreadsheet-import/types/SpreadsheetC
import { SpreadsheetColumnType } from '@/spreadsheet-import/types/SpreadsheetColumnType';
import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations';
import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -31,8 +31,10 @@ import { Button, Toggle } from 'twenty-ui/input';
import { generateColumns } from './components/columns';
import { type ImportedStructuredRowMetadata } from './types';
-const StyledContent = styled(Modal.Content)`
- padding: 0px;
+const StyledContentWrapper = styled.div`
+ display: flex;
+ flex: 1 1 0%;
+ flex-direction: column;
position: relative;
`;
@@ -282,53 +284,55 @@ export const ValidationStep = ({
return (
<>
-
- {filterByErrors && tableData.length === 0 ? (
-
- No rows with errors
-
- ) : (
-
-
- {filterByErrors
- ? t`No data containing errors`
- : t`No data found`}
-
- ),
- }}
+
+
+ {filterByErrors && tableData.length === 0 ? (
+
+ No rows with errors
+
+ ) : (
+
+
+ {filterByErrors
+ ? t`No data containing errors`
+ : t`No data found`}
+
+ ),
+ }}
+ />
+
+ )}
+
+
+ setFilterByErrors(!filterByErrors)}
+ toggleSize="small"
+ />
+
+ Show only rows with errors
+
+
+
-
- )}
-
-
- setFilterByErrors(!filterByErrors)}
- toggleSize="small"
- />
-
- Show only rows with errors
-
-
-
-
-
+
+
+
(
>
null}
>
(
>
null}
>
(
>
null}
>
(
value={{ instanceId: 'dialog-manager' }}
>
- null}>
+ null}
+ >
null}
setCurrentStepState={() => null}
diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx
index 8a16f33a66..815287dac9 100644
--- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx
+++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx
@@ -46,7 +46,7 @@ export const Default = () => (
>
null}
>
{
return (
<>
{createPortal(
-
@@ -141,7 +141,7 @@ export const GlobalFilePreviewModal = (): JSX.Element | null => {
- ,
+ ,
document.body,
)}
>
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx
index d68cb6abea..d533b09ff2 100644
--- a/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx
@@ -4,17 +4,22 @@ import { useDebouncedCallback } from 'use-debounce';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
-import { Modal, type ModalVariants } from '@/ui/layout/modal/components/Modal';
+import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { msg } from '@lingui/core/macro';
import { useLingui } from '@lingui/react/macro';
import { H1Title, H1TitleFontColor } from 'twenty-ui/display';
import { Button, type ButtonAccent } from 'twenty-ui/input';
-import { Section, SectionAlignment, SectionFontColor } from 'twenty-ui/layout';
+import {
+ Section,
+ SectionAlignment,
+ SectionFontColor,
+ type ModalOverlay,
+} from 'twenty-ui/layout';
import { themeCssVariables } from 'twenty-ui/theme-constants';
export type ConfirmationModalProps = {
- modalId: string;
+ modalInstanceId: string;
title: string;
loading?: boolean;
subtitle: ReactNode;
@@ -25,18 +30,11 @@ export type ConfirmationModalProps = {
confirmationValue?: string;
confirmButtonAccent?: ButtonAccent;
AdditionalButtons?: React.ReactNode;
- modalVariant?: ModalVariants;
+ overlay?: ModalOverlay;
};
-const StyledConfirmationModal = styled(Modal)`
- border-radius: ${themeCssVariables.spacing[1]};
- width: calc(400px - ${themeCssVariables.spacing[32]});
- height: auto;
-`;
-
export const StyledCenteredButton = styled(Button)`
box-sizing: border-box;
- justify-content: center;
margin-top: ${themeCssVariables.spacing[2]};
`;
@@ -62,7 +60,7 @@ export const StyledConfirmationButton = styled(StyledCenteredButton)`
const defaultConfirmButtonText = msg`Confirm`;
export const ConfirmationModal = ({
- modalId,
+ modalInstanceId,
title,
loading,
subtitle,
@@ -73,7 +71,7 @@ export const ConfirmationModal = ({
confirmationPlaceholder,
confirmButtonAccent = 'danger',
AdditionalButtons,
- modalVariant = 'primary',
+ overlay = 'dark',
}: ConfirmationModalProps) => {
const { i18n, t } = useLingui();
const translatedConfirmButtonText =
@@ -97,12 +95,12 @@ export const ConfirmationModal = ({
const { closeModal } = useModal();
const handleConfirmClick = () => {
- closeModal(modalId);
+ closeModal(modalInstanceId);
onConfirmClick();
};
const handleCancelClick = () => {
- closeModal(modalId);
+ closeModal(modalInstanceId);
onClose?.();
};
@@ -113,17 +111,20 @@ export const ConfirmationModal = ({
};
return (
- {
onClose?.();
}}
onEnter={handleEnter}
isClosable={true}
padding="large"
- modalVariant={modalVariant}
+ overlay={overlay}
dataGloballyPreventClickOutside
- ignoreContainer
+ renderInDocumentBody
+ smallBorderRadius
+ narrowWidth
+ autoHeight
>
@@ -153,6 +154,7 @@ export const ConfirmationModal = ({
variant="secondary"
title={t`Cancel`}
fullWidth
+ justify="center"
dataTestId="confirmation-modal-cancel-button"
/>
@@ -165,8 +167,9 @@ export const ConfirmationModal = ({
title={translatedConfirmButtonText}
disabled={!isValidValue || loading}
fullWidth
+ justify="center"
dataTestId="confirmation-modal-confirm-button"
/>
-
+
);
};
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx
deleted file mode 100644
index fd72d1e253..0000000000
--- a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx
+++ /dev/null
@@ -1,322 +0,0 @@
-import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
-import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect';
-import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
-import { useModalContainer } from '@/ui/layout/modal/contexts/ModalContainerContext';
-import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
-
-import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
-import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
-import { useModal } from '@/ui/layout/modal/hooks/useModal';
-import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
-import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
-import { styled } from '@linaria/react';
-import { AnimatePresence, motion } from 'framer-motion';
-import React, { useContext, useRef } from 'react';
-import { createPortal } from 'react-dom';
-import { isDefined } from 'twenty-shared/utils';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
-import { ThemeContext } from 'twenty-ui/theme';
-const StyledModalDivBase = styled.div<{
- size?: ModalSize;
- padding?: ModalPadding;
- isMobile: boolean;
- modalVariant: ModalVariants;
-}>`
- display: flex;
- flex-direction: column;
- box-shadow: ${({ modalVariant }) =>
- modalVariant === 'primary'
- ? themeCssVariables.boxShadow.superHeavy
- : modalVariant === 'transparent'
- ? 'none'
- : themeCssVariables.boxShadow.strong};
- background: ${({ modalVariant }) =>
- modalVariant === 'transparent'
- ? 'transparent'
- : themeCssVariables.background.primary};
- color: ${themeCssVariables.font.color.primary};
- border-radius: ${({ isMobile, modalVariant }) => {
- if (isMobile || modalVariant === 'transparent') return `0`;
- return themeCssVariables.border.radius.md;
- }};
- overflow-x: hidden;
- overflow-y: auto;
- z-index: ${RootStackingContextZIndices.RootModal}; // should be higher than Backdrop's z-index
-
- width: ${({ isMobile, size }) => {
- if (isMobile)
- return themeCssVariables.modal.size.fullscreen.width ?? 'auto';
- switch (size) {
- case 'small':
- return themeCssVariables.modal.size.sm.width ?? 'auto';
- case 'medium':
- return themeCssVariables.modal.size.md.width ?? 'auto';
- case 'large':
- return themeCssVariables.modal.size.lg.width ?? 'auto';
- case 'extraLarge':
- return themeCssVariables.modal.size.xl.width ?? 'auto';
- default:
- return 'auto';
- }
- }};
-
- padding: ${({ padding }) => {
- switch (padding) {
- case 'none':
- return themeCssVariables.spacing[0];
- case 'small':
- return themeCssVariables.spacing[2];
- case 'medium':
- return themeCssVariables.spacing[4];
- case 'large':
- return themeCssVariables.spacing[6];
- default:
- return 'auto';
- }
- }};
- height: ${({ isMobile, size }) => {
- if (isMobile)
- return themeCssVariables.modal.size.fullscreen.height ?? 'auto';
-
- switch (size) {
- case 'extraLarge':
- return themeCssVariables.modal.size.xl.height ?? 'auto';
- default:
- return 'auto';
- }
- }};
- max-height: ${({ isMobile }) => (isMobile ? 'none' : '90dvh')};
-`;
-const StyledModalDiv = motion.create(StyledModalDivBase);
-
-const StyledHeader = styled.div`
- align-items: center;
- display: flex;
- flex-direction: row;
- height: 60px;
- overflow: hidden;
- padding: ${themeCssVariables.spacing[5]};
-`;
-
-const StyledContent = styled.div<{
- isVerticalCentered?: boolean;
- isHorizontalCentered?: boolean;
-}>`
- display: flex;
- flex: 1;
- flex: 1 1 0%;
- flex-direction: column;
- padding: ${themeCssVariables.spacing[10]};
- align-items: ${({ isVerticalCentered }) =>
- isVerticalCentered ? 'center' : 'stretch'};
- justify-content: ${({ isHorizontalCentered }) =>
- isHorizontalCentered ? 'center' : 'flex-start'};
-`;
-
-const StyledFooter = styled.div`
- align-items: center;
- display: flex;
- flex-direction: row;
- height: 60px;
- overflow: hidden;
- padding: ${themeCssVariables.spacing[5]};
-`;
-
-const StyledBackDropBase = styled.div<{
- modalVariant: ModalVariants;
- isInContainer?: boolean;
-}>`
- align-items: center;
- background: ${({ modalVariant, isInContainer }) =>
- isInContainer
- ? themeCssVariables.background.overlayTertiary
- : modalVariant === 'primary' || modalVariant === 'transparent'
- ? themeCssVariables.background.overlayPrimary
- : modalVariant === 'secondary'
- ? themeCssVariables.background.overlaySecondary
- : themeCssVariables.background.overlayTertiary};
- display: flex;
- height: 100%;
- justify-content: center;
- left: 0;
- pointer-events: auto;
- position: ${({ isInContainer }) => (isInContainer ? 'absolute' : 'fixed')};
- top: 0;
- width: 100%;
- z-index: ${RootStackingContextZIndices.RootModalBackDrop};
- user-select: none;
-`;
-const StyledBackDrop = motion.create(StyledBackDropBase);
-
-type ModalHeaderProps = React.PropsWithChildren & {
- className?: string;
-};
-
-const ModalHeader = ({ children, className }: ModalHeaderProps) => (
- {children}
-);
-
-type ModalContentProps = React.PropsWithChildren & {
- className?: string;
- isVerticalCentered?: boolean;
- isHorizontalCentered?: boolean;
-};
-
-const ModalContent = ({
- children,
- className,
- isVerticalCentered,
- isHorizontalCentered,
-}: ModalContentProps) => (
-
- {children}
-
-);
-type ModalFooterProps = React.PropsWithChildren & {
- className?: string;
-};
-
-const ModalFooter = ({ children, className }: ModalFooterProps) => (
- {children}
-);
-
-export type ModalSize = 'small' | 'medium' | 'large' | 'extraLarge';
-export type ModalPadding = 'none' | 'small' | 'medium' | 'large';
-export type ModalVariants =
- | 'primary'
- | 'secondary'
- | 'tertiary'
- | 'transparent';
-
-export type ModalProps = React.PropsWithChildren & {
- modalId: string;
- size?: ModalSize;
- padding?: ModalPadding;
- className?: string;
- onEnter?: () => void;
- modalVariant?: ModalVariants;
- dataGloballyPreventClickOutside?: boolean;
- shouldCloseModalOnClickOutsideOrEscape?: boolean;
- ignoreContainer?: boolean;
-} & (
- | { isClosable: true; onClose?: () => void }
- | { isClosable?: false; onClose?: never }
- );
-
-const modalAnimation = {
- hidden: { opacity: 0 },
- visible: { opacity: 1 },
- exit: { opacity: 0 },
-};
-
-export const Modal = ({
- modalId,
- children,
- size = 'medium',
- padding = 'medium',
- className,
- onEnter,
- isClosable = false,
- onClose,
- modalVariant = 'primary',
- dataGloballyPreventClickOutside = false,
- shouldCloseModalOnClickOutsideOrEscape = true,
- ignoreContainer = false,
-}: ModalProps) => {
- const isMobile = useIsMobile();
- const modalRef = useRef(null);
- const { container } = useModalContainer();
- const effectiveContainer = ignoreContainer
- ? isDefined(document)
- ? document.body
- : null
- : container;
- const isInContainer = isDefined(container) && !ignoreContainer;
-
- const { theme } = useContext(ThemeContext);
-
- const stopEventPropagation = (e: React.MouseEvent) => {
- e.stopPropagation();
- };
-
- const isModalOpened = useAtomComponentStateValue(
- isModalOpenedComponentState,
- modalId,
- );
-
- const { closeModal } = useModal();
-
- const handleClose = () => {
- onClose?.();
- if (shouldCloseModalOnClickOutsideOrEscape) closeModal(modalId);
- };
-
- const modalContent = (
-
- {isModalOpened && (
-
-
-
-
-
- {children}
-
-
-
-
- )}
-
- );
-
- if (isDefined(effectiveContainer)) {
- return createPortal(modalContent, effectiveContainer);
- }
-
- return modalContent;
-};
-
-Modal.Header = ModalHeader;
-Modal.Content = ModalContent;
-Modal.Footer = ModalFooter;
-Modal.Backdrop = StyledBackDrop;
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx
index 76f5b548aa..7a4f8227fa 100644
--- a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect.tsx
@@ -9,7 +9,7 @@ type ModalHotkeysAndClickOutsideEffectProps = {
onEnter?: () => void;
isClosable?: boolean;
onClose?: () => void;
- modalId: string;
+ modalInstanceId: string;
};
export const ModalHotkeysAndClickOutsideEffect = ({
@@ -17,14 +17,14 @@ export const ModalHotkeysAndClickOutsideEffect = ({
onEnter,
isClosable = false,
onClose,
- modalId,
+ modalInstanceId,
}: ModalHotkeysAndClickOutsideEffectProps) => {
useHotkeysOnFocusedElement({
keys: [Key.Enter],
callback: () => {
onEnter?.();
},
- focusId: modalId,
+ focusId: modalInstanceId,
dependencies: [onEnter],
});
@@ -35,7 +35,7 @@ export const ModalHotkeysAndClickOutsideEffect = ({
onClose();
}
},
- focusId: modalId,
+ focusId: modalInstanceId,
dependencies: [isClosable, onClose],
});
@@ -45,7 +45,7 @@ export const ModalHotkeysAndClickOutsideEffect = ({
MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID,
DIALOG_CLICK_OUTSIDE_ID,
],
- listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalId}`,
+ listenerId: `MODAL_CLICK_OUTSIDE_LISTENER_ID_${modalInstanceId}`,
callback: () => {
if (isClosable && onClose !== undefined) {
onClose();
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ModalStatefulWrapper.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalStatefulWrapper.tsx
new file mode 100644
index 0000000000..a439d5c6d9
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ModalStatefulWrapper.tsx
@@ -0,0 +1,96 @@
+import { ModalHotkeysAndClickOutsideEffect } from '@/ui/layout/modal/components/ModalHotkeysAndClickOutsideEffect';
+import { MODAL_BACKDROP_CLICK_OUTSIDE_ID } from '@/ui/layout/modal/constants/ModalBackdropClickOutsideId';
+import { MODAL_CLICK_OUTSIDE_LISTENER_EXCLUDED_ID } from '@/ui/layout/modal/constants/ModalClickOutsideListenerExcludedClassName';
+import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
+import { ModalComponentInstanceContext } from '@/ui/layout/modal/contexts/ModalComponentInstanceContext';
+import { useModalContainer } from '@/ui/layout/modal/contexts/ModalContainerContext';
+import { useModal } from '@/ui/layout/modal/hooks/useModal';
+import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
+import { type ModalStatefulWrapperProps } from '@/ui/layout/modal/types/ModalStatefulWrapperProps';
+import { ClickOutsideListenerContext } from '@/ui/utilities/pointer-event/contexts/ClickOutsideListenerContext';
+import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
+import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
+import { useRef } from 'react';
+import { isDefined } from 'twenty-shared/utils';
+import { Modal } from 'twenty-ui/layout';
+
+export const ModalStatefulWrapper = ({
+ modalInstanceId,
+ children,
+ size = 'medium',
+ padding = 'medium',
+ onEnter,
+ isClosable = false,
+ onClose,
+ overlay = 'dark',
+ dataGloballyPreventClickOutside = false,
+ shouldCloseModalOnClickOutsideOrEscape = true,
+ renderInDocumentBody = false,
+ gap,
+ smallBorderRadius,
+ narrowWidth,
+ autoHeight,
+}: ModalStatefulWrapperProps) => {
+ const isMobile = useIsMobile();
+ const modalRef = useRef(null);
+ const { container } = useModalContainer();
+
+ const effectiveContainer = renderInDocumentBody ? document.body : container;
+ const isInContainer = isDefined(container) && !renderInDocumentBody;
+
+ const isModalOpened = useAtomComponentStateValue(
+ isModalOpenedComponentState,
+ modalInstanceId,
+ );
+
+ const { closeModal } = useModal();
+
+ const handleClose = () => {
+ onClose?.();
+ if (shouldCloseModalOnClickOutsideOrEscape) {
+ closeModal(modalInstanceId);
+ }
+ };
+
+ return (
+
+
+ {isModalOpened && (
+
+ )}
+
+ {children}
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx
index 7e9476aeef..693c005012 100644
--- a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/ConfirmationModal.stories.tsx
@@ -54,7 +54,7 @@ const confirmMock = fn();
export const Default: Story = {
args: {
- modalId: 'confirmation-modal',
+ modalInstanceId: 'confirmation-modal',
title: 'Pariatur labore.',
subtitle: 'Velit dolore aliquip laborum occaecat fugiat.',
confirmButtonText: 'Delete',
@@ -72,7 +72,7 @@ export const InputConfirmation: Story = {
export const CloseOnEscape: Story = {
args: {
- modalId: 'confirmation-modal',
+ modalInstanceId: 'confirmation-modal',
title: 'Escape Key Test',
subtitle: 'This modal should close when pressing the Escape key.',
confirmButtonText: 'Confirm',
@@ -95,7 +95,7 @@ export const CloseOnEscape: Story = {
export const CloseOnClickOutside: Story = {
args: {
- modalId: 'confirmation-modal',
+ modalInstanceId: 'confirmation-modal',
title: 'Click Outside Test',
subtitle: 'This modal should close when clicking outside of it.',
confirmButtonText: 'Confirm',
@@ -121,7 +121,7 @@ export const CloseOnClickOutside: Story = {
export const ConfirmWithEnterKey: Story = {
args: {
- modalId: 'confirmation-modal',
+ modalInstanceId: 'confirmation-modal',
title: 'Enter Key Test',
subtitle: 'This modal should confirm when pressing the Enter key.',
confirmButtonText: 'Confirm',
@@ -142,7 +142,7 @@ export const ConfirmWithEnterKey: Story = {
export const CancelButtonClick: Story = {
args: {
- modalId: 'confirmation-modal',
+ modalInstanceId: 'confirmation-modal',
title: 'Cancel Button Test',
subtitle: 'Clicking the cancel button should close the modal',
confirmButtonText: 'Confirm',
@@ -166,7 +166,7 @@ export const CancelButtonClick: Story = {
export const ConfirmButtonClick: Story = {
args: {
- modalId: 'confirmation-modal',
+ modalInstanceId: 'confirmation-modal',
title: 'Confirm Button Test',
subtitle: 'Clicking the confirm button should trigger the confirm action',
confirmButtonText: 'Confirm',
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx
index 6257e3876a..5cee006074 100644
--- a/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/modal/components/__stories__/Modal.stories.tsx
@@ -5,11 +5,12 @@ import {
} from '@storybook/react-vite';
import { expect, fn, userEvent, waitFor, within } from 'storybook/test';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalStatefulWrapper } from '@/ui/layout/modal/components/ModalStatefulWrapper';
import { isModalOpenedComponentState } from '@/ui/layout/modal/states/isModalOpenedComponentState';
import { focusStackState } from '@/ui/utilities/focus/states/focusStackState';
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
+import { ModalContent, ModalFooter, ModalHeader } from 'twenty-ui/layout';
import { ComponentDecorator } from 'twenty-ui/testing';
import { RootDecorator } from '~/testing/decorators/RootDecorator';
import { sleep } from '~/utils/sleep';
@@ -37,9 +38,9 @@ const JotaiInitDecorator: Decorator = (Story) => {
return ;
};
-const meta: Meta = {
- title: 'UI/Layout/Modal/Modal',
- component: Modal,
+const meta: Meta = {
+ title: 'UI/Layout/Modal/ModalStatefulWrapper',
+ component: ModalStatefulWrapper,
decorators: [JotaiInitDecorator, RootDecorator, ComponentDecorator],
parameters: {
disableHotkeyInitialization: true,
@@ -47,26 +48,26 @@ const meta: Meta = {
};
export default meta;
-type Story = StoryObj;
+type Story = StoryObj;
const closeMock = fn();
export const Default: Story = {
args: {
- modalId: 'modal-id',
+ modalInstanceId: 'modal-id',
size: 'medium',
padding: 'medium',
children: (
<>
- Stay in touch
-
+ Stay in touch
+
This is a dummy newletter form so don't bother trying to test it. Not
that I expect you to, anyways. :)
-
-
+
+
By using Twenty, you're opting for the finest CRM experience you'll
ever encounter.
-
+
>
),
},
@@ -74,17 +75,17 @@ export const Default: Story = {
export const CloseClosableModalOnClickOutside: Story = {
args: {
- modalId: 'modal-id',
+ modalInstanceId: 'modal-id',
size: 'medium',
padding: 'medium',
isClosable: true,
onClose: closeMock,
children: (
<>
- Click Outside Test
-
+ Click Outside Test
+
This modal should close when clicking outside of it.
-
+
>
),
},
@@ -94,7 +95,6 @@ export const CloseClosableModalOnClickOutside: Story = {
await canvas.findByText('Click Outside Test');
const backdrop = await canvas.findByTestId('modal-backdrop');
- // We need to wait for the outside click listener to be registered
await sleep(100);
await userEvent.click(backdrop);
@@ -106,17 +106,17 @@ export const CloseClosableModalOnClickOutside: Story = {
export const CloseClosableModalOnEscape: Story = {
args: {
- modalId: 'modal-id',
+ modalInstanceId: 'modal-id',
size: 'medium',
padding: 'medium',
isClosable: true,
onClose: closeMock,
children: (
<>
- Escape Key Test
-
+ Escape Key Test
+
This modal should close when pressing the Escape key.
-
+
>
),
},
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx
index c53dc77f20..e5cb023e6e 100644
--- a/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/modal/hooks/useModal.tsx
@@ -14,9 +14,11 @@ export const useModal = () => {
const store = useStore();
const closeModal = useCallback(
- (modalId: string) => {
+ (modalInstanceId: string) => {
const isModalOpen = store.get(
- isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
+ isModalOpenedComponentState.atomFamily({
+ instanceId: modalInstanceId,
+ }),
);
if (!isModalOpen) {
@@ -24,11 +26,13 @@ export const useModal = () => {
}
removeFocusItemFromFocusStackById({
- focusId: modalId,
+ focusId: modalInstanceId,
});
store.set(
- isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
+ isModalOpenedComponentState.atomFamily({
+ instanceId: modalInstanceId,
+ }),
false,
);
},
@@ -36,9 +40,11 @@ export const useModal = () => {
);
const openModal = useCallback(
- (modalId: string) => {
+ (modalInstanceId: string) => {
const isModalOpened = store.get(
- isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
+ isModalOpenedComponentState.atomFamily({
+ instanceId: modalInstanceId,
+ }),
);
if (isModalOpened) {
@@ -46,15 +52,17 @@ export const useModal = () => {
}
store.set(
- isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
+ isModalOpenedComponentState.atomFamily({
+ instanceId: modalInstanceId,
+ }),
true,
);
pushFocusItemToFocusStack({
- focusId: modalId,
+ focusId: modalInstanceId,
component: {
type: FocusComponentType.MODAL,
- instanceId: modalId,
+ instanceId: modalInstanceId,
},
globalHotkeysConfig: {
enableGlobalHotkeysWithModifiers: false,
@@ -66,15 +74,17 @@ export const useModal = () => {
);
const toggleModal = useCallback(
- (modalId: string) => {
+ (modalInstanceId: string) => {
const isModalOpen = store.get(
- isModalOpenedComponentState.atomFamily({ instanceId: modalId }),
+ isModalOpenedComponentState.atomFamily({
+ instanceId: modalInstanceId,
+ }),
);
if (isModalOpen) {
- closeModal(modalId);
+ closeModal(modalInstanceId);
} else {
- openModal(modalId);
+ openModal(modalInstanceId);
}
},
[store, closeModal, openModal],
diff --git a/packages/twenty-front/src/modules/ui/layout/modal/types/ModalStatefulWrapperProps.ts b/packages/twenty-front/src/modules/ui/layout/modal/types/ModalStatefulWrapperProps.ts
new file mode 100644
index 0000000000..ddbc98fc2d
--- /dev/null
+++ b/packages/twenty-front/src/modules/ui/layout/modal/types/ModalStatefulWrapperProps.ts
@@ -0,0 +1,23 @@
+import type React from 'react';
+import { type ModalProps } from 'twenty-ui/layout';
+
+export type ModalStatefulWrapperProps = Pick<
+ ModalProps,
+ | 'size'
+ | 'padding'
+ | 'overlay'
+ | 'gap'
+ | 'smallBorderRadius'
+ | 'narrowWidth'
+ | 'autoHeight'
+> &
+ React.PropsWithChildren & {
+ modalInstanceId: string;
+ onEnter?: () => void;
+ dataGloballyPreventClickOutside?: boolean;
+ shouldCloseModalOnClickOutsideOrEscape?: boolean;
+ renderInDocumentBody?: boolean;
+ } & (
+ | { isClosable: true; onClose?: () => void }
+ | { isClosable?: false; onClose?: never }
+ );
diff --git a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx
index f342eccb26..2ccd9b5c9b 100644
--- a/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx
+++ b/packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx
@@ -42,7 +42,7 @@ export const OverrideWorkflowDraftConfirmationModal = ({
return (
<>
{
return (
isTokenValid && (
-
+
{
)}
-
+
)
);
};
diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx
index b34e029d24..f76b5c492a 100644
--- a/packages/twenty-front/src/pages/auth/SignInUp.tsx
+++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx
@@ -29,7 +29,7 @@ import { SignInUpTwoFactorAuthenticationProvision } from '@/auth/sign-in-up/comp
import { SignInUpTOTPVerification } from '@/auth/sign-in-up/components/internal/SignInUpTwoFactorAuthenticationVerification';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useLingui } from '@lingui/react/macro';
import { useSearchParams } from 'react-router-dom';
import { isDefined } from 'twenty-shared/utils';
@@ -61,7 +61,7 @@ const StandardContent = ({
onClickOnLogo: () => void;
}) => {
return (
-
+
}
-
+
);
};
@@ -205,9 +205,9 @@ export const SignInUp = () => {
if (signInUpStep === SignInUpStep.EmailVerification) {
return (
-
+
-
+
);
}
diff --git a/packages/twenty-front/src/pages/onboarding/BookCall.tsx b/packages/twenty-front/src/pages/onboarding/BookCall.tsx
index 14961ff6df..4eb043cc61 100644
--- a/packages/twenty-front/src/pages/onboarding/BookCall.tsx
+++ b/packages/twenty-front/src/pages/onboarding/BookCall.tsx
@@ -1,12 +1,11 @@
import Cal from '@calcom/embed-react';
-import { styled } from '@linaria/react';
import { useContext } from 'react';
import { Link } from 'react-router-dom';
import { currentUserState } from '@/auth/states/currentUserState';
import { calendarBookingPageIdState } from '@/client-config/states/calendarBookingPageIdState';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent, ModalFooter } from 'twenty-ui/layout';
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useLingui } from '@lingui/react/macro';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
@@ -14,24 +13,12 @@ import { AppPath } from 'twenty-shared/types';
import { IconChevronLeft, IconChevronRightPipe } from 'twenty-ui/display';
import { LightButton } from 'twenty-ui/input';
import { ThemeContext } from 'twenty-ui/theme';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useIsMobile } from 'twenty-ui/utilities';
import {
OnboardingStatus,
useSkipBookOnboardingStepMutation,
} from '~/generated-metadata/graphql';
-const StyledModalFooter = styled(Modal.Footer)`
- height: auto;
- justify-content: center;
- padding: ${themeCssVariables.spacing[3]};
-`;
-
-const StyledModalContent = styled(Modal.Content)`
- overflow: hidden;
- padding: 0;
-`;
-
export const BookCall = () => {
const { t } = useLingui();
const { theme } = useContext(ThemeContext);
@@ -51,7 +38,12 @@ export const BookCall = () => {
return (
<>
-
+
{
}}
/>
-
-
+
+
{isPlanRequired ? (
@@ -79,7 +71,7 @@ export const BookCall = () => {
onClick={handleCompleteOnboarding}
/>
)}
-
+
>
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx b/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx
index bb614926b6..9d7e17c2d8 100644
--- a/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx
+++ b/packages/twenty-front/src/pages/onboarding/BookCallDecision.tsx
@@ -1,7 +1,7 @@
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { Trans, useLingui } from '@lingui/react/macro';
import { Link } from 'react-router-dom';
@@ -17,10 +17,6 @@ const StyledCoverImage = styled.img`
width: 320px;
`;
-const StyledModalContent = styled(Modal.Content)`
- gap: ${themeCssVariables.spacing[8]};
-`;
-
const StyledTitleContainer = styled.div`
align-items: center;
display: flex;
@@ -51,7 +47,7 @@ export const BookCallDecision = () => {
};
return (
-
+
Book your onboarding
@@ -70,6 +66,6 @@ export const BookCallDecision = () => {
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx
index e71dcab80f..768c4b5842 100644
--- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx
+++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx
@@ -1,4 +1,4 @@
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { styled } from '@linaria/react';
import { isDefined } from 'twenty-shared/utils';
import { ChooseYourPlanContent } from '~/pages/onboarding/internal/ChooseYourPlanContent';
@@ -14,12 +14,12 @@ export const ChooseYourPlan = () => {
const { isPlansLoaded } = usePlans();
const billing = useAtomStateValue(billingState);
return (
-
+
{isDefined(billing) && isPlansLoaded ? (
) : (
)}
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
index 5885f5037d..48fb791f96 100644
--- a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
+++ b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx
@@ -18,7 +18,7 @@ import { WorkspaceMemberPictureUploader } from '@/settings/workspace-member/comp
import { PageFocusId } from '@/types/PageFocusId';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { ApolloError } from '@apollo/client';
import { i18n } from '@lingui/core';
@@ -172,7 +172,7 @@ export const CreateProfile = () => {
});
return (
-
+
Create profile
@@ -251,6 +251,6 @@ export const CreateProfile = () => {
fullWidth
/>
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx
index 4e9c688f3f..df5c47465a 100644
--- a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx
+++ b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx
@@ -14,7 +14,7 @@ import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboard
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useLoadCurrentUser } from '@/users/hooks/useLoadCurrentUser';
import { ApolloError } from '@apollo/client';
import { Trans, useLingui } from '@lingui/react/macro';
@@ -152,7 +152,7 @@ export const CreateWorkspace = () => {
};
return (
-
+
{pendingCreationLoaderStep !== PendingCreationLoaderStep.None && (
<>
{
>
)}
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx
index 3a22476cd3..e49664fceb 100644
--- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx
+++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx
@@ -6,7 +6,7 @@ import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboard
import { PageFocusId } from '@/types/PageFocusId';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { TextInput } from '@/ui/input/components/TextInput';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { styled } from '@linaria/react';
import { zodResolver } from '@hookform/resolvers/zod';
@@ -169,7 +169,7 @@ export const InviteTeam = () => {
});
return (
-
+
Invite your team
@@ -229,6 +229,6 @@ export const InviteTeam = () => {
Skip
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx
index 62c5dc538d..5e6432190b 100644
--- a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx
+++ b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx
@@ -2,7 +2,7 @@ import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { currentUserState } from '@/auth/states/currentUserState';
import { OnboardingModalCircularIcon } from '@/onboarding/components/OnboardingModalCircularIcon';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
@@ -13,15 +13,10 @@ import { isDefined } from 'twenty-shared/utils';
import { IconCheck } from 'twenty-ui/display';
import { Loader } from 'twenty-ui/feedback';
import { MainButton } from 'twenty-ui/input';
-import { themeCssVariables } from 'twenty-ui/theme-constants';
import { AnimatedEaseIn } from 'twenty-ui/utilities';
import { useGetCurrentUserLazyQuery } from '~/generated-metadata/graphql';
import { useNavigateApp } from '~/hooks/useNavigateApp';
-const StyledModalContent = styled(Modal.Content)`
- gap: ${themeCssVariables.spacing[8]};
-`;
-
const StyledTitleContainer = styled.div`
display: flex;
flex-direction: column;
@@ -67,7 +62,7 @@ export const PaymentSuccess = () => {
};
return (
-
+
@@ -82,6 +77,6 @@ export const PaymentSuccess = () => {
Icon={() => (isLoading ? : null)}
disabled={isLoading}
/>
-
+
);
};
diff --git a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx
index 183f869f4b..07ce8713c7 100644
--- a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx
+++ b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx
@@ -15,7 +15,7 @@ import { isMicrosoftCalendarEnabledState } from '@/client-config/states/isMicros
import { isMicrosoftMessagingEnabledState } from '@/client-config/states/isMicrosoftMessagingEnabledState';
import { useTriggerApisOAuth } from '@/settings/accounts/hooks/useTriggerApiOAuth';
import { PageFocusId } from '@/types/PageFocusId';
-import { Modal } from '@/ui/layout/modal/components/Modal';
+import { ModalContent } from 'twenty-ui/layout';
import { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
import { t } from '@lingui/core/macro';
import { AppPath, ConnectedAccountProvider } from 'twenty-shared/types';
@@ -116,7 +116,7 @@ export const SyncEmails = () => {
});
return (
-
+
{t`Emails and Calendar`}
{t`Sync your Emails and Calendar with Twenty. Choose your privacy settings.`}
@@ -175,6 +175,6 @@ export const SyncEmails = () => {
{t`Continue without sync`}
-
+
);
};
diff --git a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx
index d1c06a55d5..af037b5768 100644
--- a/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx
+++ b/packages/twenty-front/src/pages/settings/admin-panel/SettingsAdminConfigVariableDetails.tsx
@@ -195,7 +195,7 @@ export const SettingsAdminConfigVariableDetails = () => {
{
diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx
index 9188fa65c4..f45f6ede40 100644
--- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx
+++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentEvalsTab.tsx
@@ -198,7 +198,7 @@ export const SettingsAgentEvalsTab = ({
{
@@ -609,7 +609,7 @@ export const SettingsApplicationRegistrationDetails = () => {
diff --git a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab.tsx b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab.tsx
index 92f09658a0..914e0d67de 100644
--- a/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab.tsx
+++ b/packages/twenty-front/src/pages/settings/applications/tabs/SettingsApplicationDetailAboutTab.tsx
@@ -110,7 +110,7 @@ export const SettingsApplicationDetailAboutTab = ({
diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
index bae2a6ed88..27b13d5d9f 100644
--- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
+++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx
@@ -429,7 +429,7 @@ export const SettingsObjectFieldEdit = () => {
{fieldMetadataItem?.isCustom && (
{
@@ -340,7 +340,7 @@ export const SettingsDevelopersApiKeyDetail = () => {
diff --git a/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx b/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx
index bdd6e037c4..d45bc405f2 100644
--- a/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx
+++ b/packages/twenty-front/src/pages/settings/domains/SettingsDomain.tsx
@@ -237,7 +237,7 @@ export const SettingsDomain = () => {
{
diff --git a/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx b/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx
index fc7356b0f5..2589f91e46 100644
--- a/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx
+++ b/packages/twenty-front/src/pages/settings/members/SettingsWorkspaceMember.tsx
@@ -219,7 +219,7 @@ export const SettingsWorkspaceMember = () => {
[0];
export default defineConfig(({ command, mode }) => {
@@ -107,9 +109,9 @@ export default defineConfig(({ command, mode }) => {
configPath: path.resolve(__dirname, './lingui.config.ts'),
}),
checker(checkers),
- {
- ...wyw({
- include: ['**/*.{ts,tsx}'],
+ createWywProfilingPlugin(
+ wyw({
+ include: [path.resolve(__dirname, 'src') + '/**/*.{ts,tsx}'],
exclude: [
'**/generated-metadata/**',
'**/testing/mock-data/generated/**',
@@ -135,8 +137,7 @@ export default defineConfig(({ command, mode }) => {
plugins: ['@babel/plugin-transform-export-namespace-from'],
},
}),
- enforce: 'pre',
- },
+ ),
visualizer({
open: true,
gzipSize: true,
diff --git a/packages/twenty-shared/package.json b/packages/twenty-shared/package.json
index 469350840d..d1eea9c69f 100644
--- a/packages/twenty-shared/package.json
+++ b/packages/twenty-shared/package.json
@@ -95,6 +95,11 @@
"import": "./dist/utils.mjs",
"require": "./dist/utils.cjs"
},
+ "./vite": {
+ "types": "./dist/vite/index.d.ts",
+ "import": "./dist/vite.mjs",
+ "require": "./dist/vite.cjs"
+ },
"./workflow": {
"types": "./dist/workflow/index.d.ts",
"import": "./dist/workflow.mjs",
@@ -118,6 +123,7 @@
"translations",
"types",
"utils",
+ "vite",
"workflow",
"workspace"
],
@@ -153,6 +159,9 @@
"utils": [
"dist/utils/index.d.ts"
],
+ "vite": [
+ "dist/vite/index.d.ts"
+ ],
"workflow": [
"dist/workflow/index.d.ts"
],
diff --git a/packages/twenty-shared/project.json b/packages/twenty-shared/project.json
index f4d41461d3..8f13e07a10 100644
--- a/packages/twenty-shared/project.json
+++ b/packages/twenty-shared/project.json
@@ -40,6 +40,8 @@
"{projectRoot}/types/dist",
"{projectRoot}/utils/package.json",
"{projectRoot}/utils/dist",
+ "{projectRoot}/vite/package.json",
+ "{projectRoot}/vite/dist",
"{projectRoot}/workflow/package.json",
"{projectRoot}/workflow/dist",
"{projectRoot}/workspace/package.json",
diff --git a/packages/twenty-shared/src/vite/createWywProfilingPlugin.ts b/packages/twenty-shared/src/vite/createWywProfilingPlugin.ts
new file mode 100644
index 0000000000..cb184a4374
--- /dev/null
+++ b/packages/twenty-shared/src/vite/createWywProfilingPlugin.ts
@@ -0,0 +1,98 @@
+/* eslint-disable no-console */
+import { type Plugin } from 'vite';
+
+const LINARIA_IMPORT_RE = /@linaria/;
+
+type WywProfilingOptions = {
+ slowThresholdMs?: number;
+ topSlowFilesCount?: number;
+ progressIntervalFiles?: number;
+};
+
+export const createWywProfilingPlugin = (
+ wywPlugin: Plugin,
+ options?: WywProfilingOptions,
+): Plugin => {
+ const slowThresholdMs = options?.slowThresholdMs ?? 50;
+ const topSlowFilesCount = options?.topSlowFilesCount ?? 10;
+ const progressIntervalFiles = options?.progressIntervalFiles ?? 50;
+
+ let totalMs = 0;
+ let fileCount = 0;
+ let skippedCount = 0;
+ const slowFiles: { id: string; ms: number }[] = [];
+ const originalTransform = wywPlugin.transform;
+
+ console.log(
+ `[linaria/wyw] CSS pre-build profiling enabled (slow threshold: ${slowThresholdMs}ms)`,
+ );
+
+ return {
+ ...wywPlugin,
+ enforce: 'pre' as const,
+ transform(code: string, id: string, ...rest: unknown[]) {
+ if (!LINARIA_IMPORT_RE.test(code)) {
+ skippedCount++;
+ return null;
+ }
+
+ const start = performance.now();
+ const result = (originalTransform as Function).call(
+ this,
+ code,
+ id,
+ ...rest,
+ );
+
+ const handleTiming = (elapsed: number) => {
+ totalMs += elapsed;
+ fileCount++;
+
+ if (elapsed > slowThresholdMs) {
+ slowFiles.push({ id, ms: elapsed });
+ }
+
+ if (fileCount % progressIntervalFiles === 0) {
+ console.log(
+ `[linaria/wyw] CSS pre-build progress: ${fileCount} transformed, ${skippedCount} skipped, ${totalMs.toFixed(0)}ms total`,
+ );
+ }
+ };
+
+ if (result && typeof result === 'object' && 'then' in result) {
+ return (result as Promise).then((res) => {
+ handleTiming(performance.now() - start);
+ return res;
+ });
+ }
+
+ handleTiming(performance.now() - start);
+ return result;
+ },
+ buildEnd() {
+ console.log('\n[linaria/wyw] ===== CSS PRE-BUILD TIMING SUMMARY =====');
+ console.log(`[linaria/wyw] Files transformed: ${fileCount}`);
+ console.log(`[linaria/wyw] Files skipped (no @linaria): ${skippedCount}`);
+ console.log(`[linaria/wyw] Transform time: ${totalMs.toFixed(0)}ms`);
+ console.log(
+ `[linaria/wyw] Avg per transformed file: ${fileCount > 0 ? (totalMs / fileCount).toFixed(1) : 0}ms`,
+ );
+
+ if (slowFiles.length > 0) {
+ console.log(
+ `[linaria/wyw] Slow CSS pre-build files (>${slowThresholdMs}ms):`,
+ );
+ slowFiles
+ .sort((a, b) => b.ms - a.ms)
+ .slice(0, topSlowFilesCount)
+ .forEach((slowFile) =>
+ console.log(
+ `[linaria/wyw] ${slowFile.ms.toFixed(0)}ms ${slowFile.id.replace(process.cwd(), '')}`,
+ ),
+ );
+ }
+
+ console.log('[linaria/wyw] ==========================================\n');
+ },
+ };
+};
diff --git a/packages/twenty-shared/src/vite/index.ts b/packages/twenty-shared/src/vite/index.ts
new file mode 100644
index 0000000000..06d82f00c5
--- /dev/null
+++ b/packages/twenty-shared/src/vite/index.ts
@@ -0,0 +1,10 @@
+/*
+ * _____ _
+ *|_ _|_ _____ _ __ | |_ _ _
+ * | | \ \ /\ / / _ \ '_ \| __| | | | Auto-generated file
+ * | | \ V V / __/ | | | |_| |_| | Any edits to this will be overridden
+ * |_| \_/\_/ \___|_| |_|\__|\__, |
+ * |___/
+ */
+
+export { createWywProfilingPlugin } from './createWywProfilingPlugin';
diff --git a/packages/twenty-ui/eslint.config.mjs b/packages/twenty-ui/eslint.config.mjs
index 4820d47f2f..02927e0d87 100644
--- a/packages/twenty-ui/eslint.config.mjs
+++ b/packages/twenty-ui/eslint.config.mjs
@@ -14,6 +14,7 @@ export default [
{
ignores: [
'**/node_modules/**',
+ '**/generated/**',
],
},
diff --git a/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx
index 24de8c13d8..b12baff556 100644
--- a/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx
+++ b/packages/twenty-ui/src/components/avatar-or-icon/AvatarOrIcon.tsx
@@ -67,10 +67,7 @@ export const AvatarOrIcon = ({
if (isIconInverted || isDefined(IconBackgroundColor)) {
return (
-
+
+
diff --git a/packages/twenty-ui/src/individual-entry.ts b/packages/twenty-ui/src/individual-entry.ts
index d762a51893..11fa297fe9 100644
--- a/packages/twenty-ui/src/individual-entry.ts
+++ b/packages/twenty-ui/src/individual-entry.ts
@@ -13,4 +13,3 @@ export * from './layout';
export * from './navigation';
export * from './theme';
export * from './utilities';
-
diff --git a/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx
index a29ff56806..5fe3df3d4c 100644
--- a/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx
+++ b/packages/twenty-ui/src/json-visualizer/components/JsonNestedNode.tsx
@@ -41,10 +41,12 @@ const StyledJsonListBase = styled.ul<{
padding: 0;
display: grid;
row-gap: ${themeCssVariables.spacing[2]};
- padding-left: ${({ depth }) => (depth > 0 ? themeCssVariables.spacing[8] : '0')};
+ padding-left: ${({ depth }) =>
+ depth > 0 ? themeCssVariables.spacing[8] : '0'};
> :first-of-type {
- margin-top: ${({ depth }) => (depth > 0 ? themeCssVariables.spacing[2] : '0')};
+ margin-top: ${({ depth }) =>
+ depth > 0 ? themeCssVariables.spacing[2] : '0'};
}
`;
diff --git a/packages/twenty-ui/src/layout/index.ts b/packages/twenty-ui/src/layout/index.ts
index 29939a08bf..9531b3115c 100644
--- a/packages/twenty-ui/src/layout/index.ts
+++ b/packages/twenty-ui/src/layout/index.ts
@@ -39,6 +39,18 @@ export { Card } from './card/components/Card';
export { CardContent } from './card/components/CardContent';
export { CardFooter } from './card/components/CardFooter';
export { CardHeader } from './card/components/CardHeader';
+export { Modal } from './modal/components/Modal';
+export { ModalBackdrop } from './modal/components/ModalBackdrop';
+export type { ModalContentProps } from './modal/components/ModalContent';
+export { ModalContent } from './modal/components/ModalContent';
+export type { ModalFooterProps } from './modal/components/ModalFooter';
+export { ModalFooter } from './modal/components/ModalFooter';
+export type { ModalHeaderProps } from './modal/components/ModalHeader';
+export { ModalHeader } from './modal/components/ModalHeader';
+export type { ModalOverlay } from './modal/types/ModalOverlay';
+export type { ModalPadding } from './modal/types/ModalPadding';
+export type { ModalProps } from './modal/types/ModalProps';
+export type { ModalSize } from './modal/types/ModalSize';
export {
SectionAlignment,
SectionFontColor,
diff --git a/packages/twenty-ui/src/layout/modal/components/Modal.tsx b/packages/twenty-ui/src/layout/modal/components/Modal.tsx
new file mode 100644
index 0000000000..bd7d71e9e8
--- /dev/null
+++ b/packages/twenty-ui/src/layout/modal/components/Modal.tsx
@@ -0,0 +1,182 @@
+import { styled } from '@linaria/react';
+import { AnimatePresence, motion } from 'framer-motion';
+import React, { useContext, useRef } from 'react';
+import { createPortal } from 'react-dom';
+import { isDefined } from 'twenty-shared/utils';
+import { themeCssVariables } from '@ui/theme-constants';
+import { ThemeContext } from '@ui/theme';
+
+import { type ModalOverlay } from '../types/ModalOverlay';
+import { type ModalPadding } from '../types/ModalPadding';
+import { type ModalProps } from '../types/ModalProps';
+import { type ModalSize } from '../types/ModalSize';
+import { ModalBackdrop } from './ModalBackdrop';
+
+const DEFAULT_MODAL_Z_INDEX = 40;
+const DEFAULT_BACKDROP_Z_INDEX = 39;
+
+const StyledModalDiv = styled.div<{
+ size?: ModalSize;
+ padding?: ModalPadding;
+ isMobile: boolean;
+ overlay: ModalOverlay;
+ gap?: number;
+ smallBorderRadius?: boolean;
+ narrowWidth?: boolean;
+ autoHeight?: boolean;
+ modalZIndex: number;
+}>`
+ display: flex;
+ flex-direction: column;
+ box-shadow: ${({ overlay }) =>
+ overlay === 'dark'
+ ? themeCssVariables.boxShadow.superHeavy
+ : overlay === 'transparent'
+ ? 'none'
+ : themeCssVariables.boxShadow.strong};
+ background: ${({ overlay }) =>
+ overlay === 'transparent'
+ ? 'transparent'
+ : themeCssVariables.background.primary};
+ color: ${themeCssVariables.font.color.primary};
+ border-radius: ${({ isMobile, overlay, smallBorderRadius }) => {
+ if (isMobile === true || overlay === 'transparent') return '0';
+ if (smallBorderRadius === true) return themeCssVariables.spacing[1];
+ return themeCssVariables.border.radius.md;
+ }};
+ overflow-x: hidden;
+ overflow-y: auto;
+ z-index: ${({ modalZIndex }) => modalZIndex};
+
+ gap: ${({ gap }) =>
+ gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'};
+
+ width: ${({ isMobile, size, narrowWidth }) => {
+ if (narrowWidth === true)
+ return `calc(400px - ${themeCssVariables.spacing[32]})`;
+ if (isMobile)
+ return themeCssVariables.modal.size.fullscreen.width ?? 'auto';
+ switch (size) {
+ case 'small':
+ return themeCssVariables.modal.size.sm.width ?? 'auto';
+ case 'medium':
+ return themeCssVariables.modal.size.md.width ?? 'auto';
+ case 'large':
+ return themeCssVariables.modal.size.lg.width ?? 'auto';
+ case 'extraLarge':
+ return themeCssVariables.modal.size.xl.width ?? 'auto';
+ default:
+ return 'auto';
+ }
+ }};
+
+ padding: ${({ padding }) => {
+ switch (padding) {
+ case 'none':
+ return themeCssVariables.spacing[0];
+ case 'small':
+ return themeCssVariables.spacing[2];
+ case 'medium':
+ return themeCssVariables.spacing[4];
+ case 'large':
+ return themeCssVariables.spacing[6];
+ default:
+ return 'auto';
+ }
+ }};
+ height: ${({ isMobile, size, autoHeight }) => {
+ if (autoHeight === true) return 'auto';
+ if (isMobile)
+ return themeCssVariables.modal.size.fullscreen.height ?? 'auto';
+ switch (size) {
+ case 'extraLarge':
+ return themeCssVariables.modal.size.xl.height ?? 'auto';
+ default:
+ return 'auto';
+ }
+ }};
+ max-height: ${({ isMobile }) => (isMobile ? 'none' : '90dvh')};
+`;
+
+const AnimatedModalDiv = motion.create(StyledModalDiv);
+const AnimatedBackdrop = motion.create(ModalBackdrop);
+
+const modalAnimation = {
+ hidden: { opacity: 0 },
+ visible: { opacity: 1 },
+ exit: { opacity: 0 },
+};
+
+export const Modal = ({
+ isOpen,
+ children,
+ size = 'medium',
+ padding = 'medium',
+ overlay = 'dark',
+ isMobile = false,
+ isInContainer = false,
+ container,
+ gap,
+ smallBorderRadius,
+ narrowWidth,
+ autoHeight,
+ modalZIndex = DEFAULT_MODAL_Z_INDEX,
+ backdropZIndex = DEFAULT_BACKDROP_Z_INDEX,
+ backdropTestId = 'modal-backdrop',
+ backdropClickOutsideId,
+ preventClickOutside,
+ onBackdropMouseDown,
+ modalRef: externalRef,
+}: ModalProps) => {
+ const internalRef = useRef(null);
+ const resolvedRef = externalRef ?? internalRef;
+ const { theme } = useContext(ThemeContext);
+
+ const handleBackdropMouseDown = (e: React.MouseEvent) => {
+ e.stopPropagation();
+ onBackdropMouseDown?.(e);
+ };
+
+ const content = (
+
+ {isOpen && (
+
+
+ {children}
+
+
+ )}
+
+ );
+
+ if (isDefined(container)) {
+ return createPortal(content, container);
+ }
+
+ return content;
+};
diff --git a/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.tsx b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.tsx
new file mode 100644
index 0000000000..8dadbe4b04
--- /dev/null
+++ b/packages/twenty-ui/src/layout/modal/components/ModalBackdrop.tsx
@@ -0,0 +1,28 @@
+import { styled } from '@linaria/react';
+import { themeCssVariables } from '@ui/theme-constants';
+
+import { type ModalOverlay } from '../types/ModalOverlay';
+
+const StyledModalBackdrop = styled.div<{
+ overlay: ModalOverlay;
+ backdropZIndex: number;
+ isInContainer?: boolean;
+}>`
+ align-items: center;
+ background: ${({ overlay, isInContainer }) =>
+ isInContainer || overlay === 'light'
+ ? themeCssVariables.background.overlayTertiary
+ : themeCssVariables.background.overlayPrimary};
+ display: flex;
+ height: 100%;
+ justify-content: center;
+ left: 0;
+ pointer-events: auto;
+ position: ${({ isInContainer }) => (isInContainer ? 'absolute' : 'fixed')};
+ top: 0;
+ width: 100%;
+ z-index: ${({ backdropZIndex }) => backdropZIndex};
+ user-select: none;
+`;
+
+export const ModalBackdrop = StyledModalBackdrop;
diff --git a/packages/twenty-ui/src/layout/modal/components/ModalContent.tsx b/packages/twenty-ui/src/layout/modal/components/ModalContent.tsx
new file mode 100644
index 0000000000..7276349ea7
--- /dev/null
+++ b/packages/twenty-ui/src/layout/modal/components/ModalContent.tsx
@@ -0,0 +1,59 @@
+import { styled } from '@linaria/react';
+import React from 'react';
+import { themeCssVariables } from '@ui/theme-constants';
+
+const StyledContent = styled.div<{
+ isVerticallyCentered?: boolean;
+ isHorizontallyCentered?: boolean;
+ noPadding?: boolean;
+ overflowHidden?: boolean;
+ gap?: number;
+ contentPadding?: number;
+}>`
+ align-items: ${({ isVerticallyCentered }) =>
+ isVerticallyCentered ? 'center' : 'stretch'};
+ display: flex;
+ flex: 1 1 0%;
+ flex-direction: column;
+ gap: ${({ gap }) =>
+ gap !== undefined ? `var(--t-spacing-${gap})` : 'unset'};
+ justify-content: ${({ isHorizontallyCentered }) =>
+ isHorizontallyCentered ? 'center' : 'flex-start'};
+ overflow: ${({ overflowHidden }) => (overflowHidden ? 'hidden' : 'visible')};
+ padding: ${({ noPadding, contentPadding }) => {
+ if (noPadding === true) return '0';
+ if (contentPadding !== undefined)
+ return `var(--t-spacing-${contentPadding})`;
+ return themeCssVariables.spacing[10];
+ }};
+`;
+
+export type ModalContentProps = React.PropsWithChildren & {
+ isVerticallyCentered?: boolean;
+ isHorizontallyCentered?: boolean;
+ noPadding?: boolean;
+ overflowHidden?: boolean;
+ gap?: number;
+ contentPadding?: number;
+};
+
+export const ModalContent = ({
+ children,
+ isVerticallyCentered,
+ isHorizontallyCentered,
+ noPadding,
+ overflowHidden,
+ gap,
+ contentPadding,
+}: ModalContentProps) => (
+
+ {children}
+
+);
diff --git a/packages/twenty-ui/src/layout/modal/components/ModalFooter.tsx b/packages/twenty-ui/src/layout/modal/components/ModalFooter.tsx
new file mode 100644
index 0000000000..737a52f017
--- /dev/null
+++ b/packages/twenty-ui/src/layout/modal/components/ModalFooter.tsx
@@ -0,0 +1,43 @@
+import { styled } from '@linaria/react';
+import React from 'react';
+import { themeCssVariables } from '@ui/theme-constants';
+
+const StyledFooter = styled.div<{
+ autoHeight?: boolean;
+ centered?: boolean;
+ smallPadding?: boolean;
+}>`
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ gap: ${themeCssVariables.spacing[2]};
+ height: ${({ autoHeight }) => (autoHeight ? 'auto' : '60px')};
+ justify-content: ${({ centered }) => (centered ? 'center' : 'flex-end')};
+ overflow: hidden;
+ padding: ${({ smallPadding }) =>
+ smallPadding ? themeCssVariables.spacing[3] : themeCssVariables.spacing[5]};
+`;
+
+export type ModalFooterProps = React.PropsWithChildren & {
+ autoHeight?: boolean;
+ centered?: boolean;
+ smallPadding?: boolean;
+ className?: string;
+};
+
+export const ModalFooter = ({
+ children,
+ autoHeight,
+ centered,
+ smallPadding,
+ className,
+}: ModalFooterProps) => (
+
+ {children}
+
+);
diff --git a/packages/twenty-ui/src/layout/modal/components/ModalHeader.tsx b/packages/twenty-ui/src/layout/modal/components/ModalHeader.tsx
new file mode 100644
index 0000000000..82d9671098
--- /dev/null
+++ b/packages/twenty-ui/src/layout/modal/components/ModalHeader.tsx
@@ -0,0 +1,65 @@
+import { styled } from '@linaria/react';
+import React from 'react';
+import { MOBILE_VIEWPORT, themeCssVariables } from '@ui/theme-constants';
+
+const StyledHeader = styled.div<{
+ noPadding?: boolean;
+ autoHeight?: boolean;
+ hasBorderBottom?: boolean;
+ paddingHorizontal?: number;
+ backgroundColor?: string;
+}>`
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ flex-shrink: 0;
+ height: ${({ autoHeight }) => (autoHeight ? 'auto' : '60px')};
+ overflow: hidden;
+ padding: ${({ noPadding, paddingHorizontal }) => {
+ if (paddingHorizontal !== undefined)
+ return `0 var(--t-spacing-${paddingHorizontal})`;
+ if (noPadding === true) return '0';
+ return themeCssVariables.spacing[5];
+ }};
+ background-color: ${({ backgroundColor }) => backgroundColor ?? 'unset'};
+ border-bottom: ${({ hasBorderBottom }) =>
+ hasBorderBottom
+ ? `1px solid ${themeCssVariables.border.color.medium}`
+ : 'none'};
+ @media (max-width: ${MOBILE_VIEWPORT}px) {
+ ${({ paddingHorizontal }) =>
+ paddingHorizontal !== undefined
+ ? `padding-left: ${themeCssVariables.spacing[4]}; padding-right: ${themeCssVariables.spacing[4]};`
+ : ''}
+ }
+`;
+
+export type ModalHeaderProps = React.PropsWithChildren & {
+ noPadding?: boolean;
+ autoHeight?: boolean;
+ hasBorderBottom?: boolean;
+ paddingHorizontal?: number;
+ backgroundColor?: string;
+ className?: string;
+};
+
+export const ModalHeader = ({
+ children,
+ noPadding,
+ autoHeight,
+ hasBorderBottom,
+ paddingHorizontal,
+ backgroundColor,
+ className,
+}: ModalHeaderProps) => (
+
+ {children}
+
+);
diff --git a/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.tsx b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.tsx
new file mode 100644
index 0000000000..995d144f43
--- /dev/null
+++ b/packages/twenty-ui/src/layout/modal/components/__stories__/Modal.stories.tsx
@@ -0,0 +1,244 @@
+import { styled } from '@linaria/react';
+import { type Meta, type StoryObj } from '@storybook/react-vite';
+import { useState } from 'react';
+import { H1Title, H1TitleFontColor, H2Title, IconX } from '@ui/display';
+import { Button, IconButton } from '@ui/input';
+import { Section, SectionAlignment, SectionFontColor } from '@ui/layout';
+import { ComponentDecorator } from '@ui/testing';
+import { themeCssVariables } from '@ui/theme-constants';
+
+import { Modal } from '../Modal';
+import { ModalContent } from '../ModalContent';
+import { ModalFooter } from '../ModalFooter';
+import { ModalHeader } from '../ModalHeader';
+
+const StyledCenteredTitle = styled.div`
+ text-align: center;
+`;
+
+const StyledSection = styled(Section)`
+ margin-bottom: ${themeCssVariables.spacing[6]};
+`;
+
+const meta: Meta = {
+ title: 'UI/Layout/Modal/Modal',
+ component: Modal,
+ decorators: [ComponentDecorator],
+ argTypes: {
+ size: {
+ control: 'select',
+ options: ['small', 'medium', 'large', 'extraLarge'],
+ },
+ padding: {
+ control: 'select',
+ options: ['none', 'small', 'medium', 'large'],
+ },
+ overlay: {
+ control: 'select',
+ options: ['light', 'dark', 'transparent'],
+ },
+ },
+};
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ isOpen: true,
+ size: 'medium',
+ padding: 'none',
+ overlay: 'dark',
+ },
+ render: ({ isOpen, size, padding, overlay }) => (
+
+
+
+
+
+
+ Workspace name and subdomain can be changed from the settings panel.
+ These changes will be reflected across all members.
+
+
+
+
+
+
+
+ ),
+};
+
+export const Confirmation: Story = {
+ args: {
+ isOpen: true,
+ padding: 'large',
+ overlay: 'dark',
+ smallBorderRadius: true,
+ narrowWidth: true,
+ autoHeight: true,
+ gap: 2,
+ },
+ render: ({
+ isOpen,
+ padding,
+ overlay,
+ smallBorderRadius,
+ narrowWidth,
+ autoHeight,
+ gap,
+ }) => (
+
+
+
+
+
+ This action cannot be undone. The record and all of its data will be
+ permanently removed.
+
+
+
+
+ ),
+};
+
+export const Small: Story = {
+ args: {
+ isOpen: true,
+ size: 'small',
+ padding: 'none',
+ overlay: 'dark',
+ },
+ render: ({ isOpen, size, padding, overlay }) => (
+
+
+
+
+
+ Are you sure you want to archive this item?
+
+
+
+
+
+
+ ),
+};
+
+export const ExtraLarge: Story = {
+ args: {
+ isOpen: true,
+ size: 'extraLarge',
+ padding: 'none',
+ overlay: 'dark',
+ },
+ render: ({ isOpen, size, padding, overlay }) => (
+
+
+
+
+
+
+ The file should include columns for name, email, phone, and company.
+ Drag and drop your CSV file here, or click to browse.
+
+
+
+
+
+
+
+ ),
+};
+
+export const Closed: Story = {
+ args: {
+ isOpen: false,
+ size: 'medium',
+ padding: 'medium',
+ overlay: 'dark',
+ },
+ render: ({ isOpen, size, padding, overlay }) => (
+
+ This should not be visible.
+
+ ),
+};
+
+const InteractiveModal = () => {
+ const [isOpen, setIsOpen] = useState(false);
+
+ return (
+ <>
+