diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json
index 4c74d3c9c28..9e937596725 100644
--- a/apps/admin-x-framework/package.json
+++ b/apps/admin-x-framework/package.json
@@ -95,7 +95,6 @@
"vitest": "catalog:"
},
"dependencies": {
- "@ebay/nice-modal-react": "catalog:",
"@sentry/react": "catalog:",
"@tanstack/react-query": "catalog:",
"@tinybirdco/charts": "0.3.0",
diff --git a/apps/admin/package.json b/apps/admin/package.json
index e778f87b099..532e69fc8f8 100644
--- a/apps/admin/package.json
+++ b/apps/admin/package.json
@@ -25,7 +25,6 @@
"@codemirror/state": "catalog:",
"@codemirror/theme-one-dark": "catalog:",
"@dnd-kit/sortable": "catalog:",
- "@ebay/nice-modal-react": "catalog:",
"@sentry/react": "catalog:",
"@svg-maps/world": "2.0.0",
"@tanstack/react-query": "catalog:",
diff --git a/apps/admin/src/settings/app/app.tsx b/apps/admin/src/settings/app/app.tsx
index 992ac686b99..3db6609d3d5 100644
--- a/apps/admin/src/settings/app/app.tsx
+++ b/apps/admin/src/settings/app/app.tsx
@@ -1,5 +1,4 @@
import MainContent from './main-content';
-import NiceModal from '@ebay/nice-modal-react';
import SettingsAppProvider, {type UpgradeStatusType} from './components/providers/settings-app-provider';
import {ConfirmationProvider} from './components/providers/confirmation-provider';
import {DialogPortalProvider} from './components/providers/dialog-portal';
@@ -31,11 +30,9 @@ export function App({upgradeStatus}: AppProps) {
-
-
-
-
-
+
+
+
diff --git a/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx b/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx
index b22fb12d597..bb19070ffe8 100644
--- a/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx
+++ b/apps/admin/src/settings/app/components/providers/confirmation-provider.tsx
@@ -30,9 +30,8 @@ export const ConfirmationProvider: React.FC<{children: React.ReactNode}> = ({chi
const show = useCallback((request: Omit): ConfirmationHandle => {
nextId.current += 1;
const id = nextId.current;
- // One request per kind, matching NiceModal's per-component keying: a
- // second show replaces the first instead of stacking (StrictMode
- // double-effects depend on this).
+ // One request per kind: a second show replaces the first instead of
+ // stacking (StrictMode double-effects depend on this).
setRequests(current => [...current.filter(r => r.kind !== request.kind), {...request, id} as ConfirmationRequest]);
return {remove: () => setRequests(current => current.filter(r => r.id !== id))};
}, []);
diff --git a/apps/admin/src/settings/app/components/settings/preview-modal.tsx b/apps/admin/src/settings/app/components/settings/preview-modal.tsx
index 5b6e7e1a4b5..347825af7f5 100644
--- a/apps/admin/src/settings/app/components/settings/preview-modal.tsx
+++ b/apps/admin/src/settings/app/components/settings/preview-modal.tsx
@@ -1,6 +1,5 @@
import React, {useEffect} from 'react';
import {ExternalLink} from 'lucide-react';
-import {useModal} from '@ebay/nice-modal-react';
import {Box, Inline, Text, type TextElement, type TextLeading, type TextSize} from '@tryghost/shade/primitives';
import {Button, type ButtonProps, Separator} from '@tryghost/shade/components';
@@ -37,8 +36,8 @@ const headingLeading: Record = {
};
/**
- * Compatibility shell for settings preview modals while the legacy NiceModal
- * flows are migrated to consumer-controlled Shade compositions.
+ * Consumer-controlled shell for the settings preview dialogs (design, portal,
+ * newsletters, offers). New modal flows should use Shade compositions directly.
*/
export interface PreviewModalProps {
testId?: string;
@@ -72,15 +71,12 @@ export interface PreviewModalProps {
onCancel?: () => void;
onOk?: () => void;
- /** Supersedes the NiceModal close path; without it the modal must be mounted through NiceModal. Keep its presence stable across renders — toggling defined/undefined remounts the modal subtree. */
- onClose?: () => void;
+ onClose: () => void;
afterClose?: () => void;
}
-type PreviewModalContentBaseProps = Omit & {requestClose: () => void};
-
-const PreviewModalContentBase: React.FC = ({
- requestClose,
+export const PreviewModalContent: React.FC = ({
+ onClose,
testId,
title,
titleHeadingLevel = 4,
@@ -188,7 +184,7 @@ const PreviewModalContentBase: React.FC = ({
const handleCancel = onCancel || (() => {
confirm(dirty, () => {
- requestClose();
+ onClose();
afterClose?.();
});
});
@@ -207,7 +203,7 @@ const PreviewModalContentBase: React.FC = ({
title=''
width={width}
hideXOnMobile
- onClose={requestClose}
+ onClose={onClose}
>
= ({
);
};
-
-const NicePreviewModalContent: React.FC> = (props) => {
- const modal = useModal();
- return modal.remove()} />;
-};
-
-export const PreviewModalContent: React.FC = ({onClose, ...props}) => {
- if (onClose) {
- return ;
- }
- return ;
-};
diff --git a/apps/shade/package.json b/apps/shade/package.json
index 98aa4f16b86..b2cf27bb1b8 100644
--- a/apps/shade/package.json
+++ b/apps/shade/package.json
@@ -107,7 +107,6 @@
"@dnd-kit/core": "catalog:",
"@dnd-kit/sortable": "catalog:",
"@dnd-kit/utilities": "catalog:",
- "@ebay/nice-modal-react": "catalog:",
"@hookform/resolvers": "5.4.0",
"@number-flow/react": "0.6.2",
"@radix-ui/react-accordion": "1.2.15",
diff --git a/apps/shade/src/components/patterns/settings-modal.stories.tsx b/apps/shade/src/components/patterns/settings-modal.stories.tsx
index 0c322108e01..81542e10575 100644
--- a/apps/shade/src/components/patterns/settings-modal.stories.tsx
+++ b/apps/shade/src/components/patterns/settings-modal.stories.tsx
@@ -1,16 +1,17 @@
-import NiceModal from '@ebay/nice-modal-react';
+import {useState} from 'react';
import type {Meta, StoryObj} from '@storybook/react-vite';
import {Button} from '@/components/ui/button';
import {Box} from '@/components/primitives/box';
import {SettingsModal, type SettingsModalProps} from '@/components/patterns/settings-modal';
-const SettingsModalStory = (props: SettingsModalProps) => {
- const StoryModal = NiceModal.create(() => );
+const SettingsModalStory = (props: Omit) => {
+ const [open, setOpen] = useState(false);
return (
-
+
+ {open && setOpen(false)} />}
);
};
@@ -22,15 +23,10 @@ const meta = {
parameters: {
docs: {
description: {
- component: 'Transitional compatibility shell for the existing settings NiceModal flows. New modal flows should use Shade Dialog primitives directly.'
+ component: 'Consumer-controlled shell for the legacy full-page settings dialogs. New modal flows should use Shade Dialog primitives directly.'
}
}
- },
- decorators: [Story => (
-
-
-
- )]
+ }
} satisfies Meta;
export default meta;
diff --git a/apps/shade/src/components/patterns/settings-modal.tsx b/apps/shade/src/components/patterns/settings-modal.tsx
index 74d78553fb0..a8c802ff6f8 100644
--- a/apps/shade/src/components/patterns/settings-modal.tsx
+++ b/apps/shade/src/components/patterns/settings-modal.tsx
@@ -1,4 +1,3 @@
-import {useModal} from '@ebay/nice-modal-react';
import {cva} from 'class-variance-authority';
import {X} from 'lucide-react';
import React, {forwardRef, useEffect, useState} from 'react';
@@ -14,8 +13,8 @@ import useGlobalDirtyState from '@/hooks/use-global-dirty-state';
import {cn} from '@/lib/utils';
/**
- * Compatibility shell for settings modals while the legacy NiceModal flows are
- * migrated to Shade's consumer-controlled Dialog primitives.
+ * Consumer-controlled settings modal shell. Legacy full-page settings dialogs
+ * still render through it; new modal flows should use Shade Dialog primitives.
*/
export type SettingsModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'bleed';
@@ -44,8 +43,7 @@ export interface SettingsModalProps {
onCancel?: () => void;
topRightContent?: 'close' | React.ReactNode;
hideXOnMobile?: boolean;
- /** Supersedes the NiceModal close path; without it the modal must be mounted through NiceModal. Keep its presence stable across renders — toggling defined/undefined remounts the modal subtree. */
- onClose?: () => void;
+ onClose: () => void;
afterClose?: () => void;
children?: React.ReactNode;
backDrop?: boolean;
@@ -124,9 +122,7 @@ const headerOffsets: Record = {
bleed: '-inset-x-10'
};
-type SettingsModalContentProps = Omit & {requestClose: () => void};
-
-const SettingsModalContent = forwardRef(({
+const SettingsModal = forwardRef(({
'aria-label': ariaLabel,
className,
size = 'md',
@@ -150,7 +146,7 @@ const SettingsModalContent = forwardRef(
onCancel,
topRightContent,
hideXOnMobile = false,
- requestClose,
+ onClose,
afterClose,
children,
backDrop = true,
@@ -173,7 +169,7 @@ const SettingsModalContent = forwardRef(
const removeModal = () => {
confirm(dirty, () => {
- requestClose();
+ onClose();
afterClose?.();
});
};
@@ -359,22 +355,6 @@ const SettingsModalContent = forwardRef(
);
});
-SettingsModalContent.displayName = 'SettingsModalContent';
-
-const NiceSettingsModal = forwardRef>((props, ref) => {
- const modal = useModal();
- return modal.remove()} />;
-});
-
-NiceSettingsModal.displayName = 'NiceSettingsModal';
-
-const SettingsModal = forwardRef(({onClose, ...props}, ref) => {
- if (onClose) {
- return ;
- }
- return ;
-});
-
SettingsModal.displayName = 'SettingsModal';
export {SettingsModal, settingsModalVariants};
diff --git a/apps/shade/test/unit/components/patterns/settings-modal.test.tsx b/apps/shade/test/unit/components/patterns/settings-modal.test.tsx
index 42cfbc834ce..2bd7a34dc37 100644
--- a/apps/shade/test/unit/components/patterns/settings-modal.test.tsx
+++ b/apps/shade/test/unit/components/patterns/settings-modal.test.tsx
@@ -1,15 +1,8 @@
-import NiceModal from '@ebay/nice-modal-react';
-import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
+import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import {describe, expect, it, vi} from 'vitest';
import {SettingsModal, settingsModalVariants, type SettingsModalSize} from '@/components/patterns/settings-modal';
-const TestSettingsModal = NiceModal.create(() => (
- undefined}>
- Modal content
-
-));
-
describe('SettingsModal', () => {
it.each(['sm', 'md', 'lg', 'xl', 'full'])('uses the standard dialog radius for the %s size', (size) => {
expect(settingsModalVariants({size})).toContain('rounded-lg');
@@ -21,27 +14,23 @@ describe('SettingsModal', () => {
expect(settingsModalVariants({size})).not.toContain('rounded-lg');
});
- it('uses content-sized outline and primary actions by default', async () => {
- render();
-
- act(() => {
- void NiceModal.show(TestSettingsModal);
- });
+ it('uses content-sized outline and primary actions by default', () => {
+ render(
+ undefined} onOk={() => undefined}>
+ Modal content
+
+ );
- const cancelButton = await screen.findByRole('button', {name: 'Cancel'});
+ const cancelButton = screen.getByRole('button', {name: 'Cancel'});
const okButton = screen.getByRole('button', {name: 'OK'});
expect(cancelButton.className).toContain('border-control-border');
expect(cancelButton.className).toContain('bg-transparent');
expect(cancelButton.className).not.toContain('hover:bg-accent');
expect(okButton.className).not.toContain('min-w-20');
-
- act(() => {
- void NiceModal.remove(TestSettingsModal);
- });
});
- it('renders without a NiceModal context and closes through onClose', () => {
+ it('closes through onClose', () => {
const onClose = vi.fn();
render(
@@ -71,23 +60,4 @@ describe('SettingsModal', () => {
expect(onClose).toHaveBeenCalledTimes(1);
});
});
-
- it('still closes through NiceModal when no onClose is passed', async () => {
- const BridgeModal = NiceModal.create(() => (
-
- Modal content
-
- ));
-
- render();
- act(() => {
- void NiceModal.show(BridgeModal);
- });
-
- fireEvent.click(await screen.findByTestId('close-modal'));
-
- await waitFor(() => {
- expect(screen.queryByTestId('close-modal')).toBeNull();
- });
- });
});
diff --git a/apps/shade/test/unit/components/ui/overlay-escape.test.tsx b/apps/shade/test/unit/components/ui/overlay-escape.test.tsx
index ad3c3d04776..71cc091f921 100644
--- a/apps/shade/test/unit/components/ui/overlay-escape.test.tsx
+++ b/apps/shade/test/unit/components/ui/overlay-escape.test.tsx
@@ -1,5 +1,4 @@
-import NiceModal from '@ebay/nice-modal-react';
-import {act, fireEvent, render, screen, waitFor} from '@testing-library/react';
+import {fireEvent, render, screen, waitFor} from '@testing-library/react';
import {useState} from 'react';
import {beforeAll, describe, expect, it, vi} from 'vitest';
@@ -55,17 +54,11 @@ describe('nested overlay Escape behavior', () => {
it.each(overlayCases)('closes an uncontrolled %s before its parent SettingsModal', async (_name, Overlay) => {
const onCancel = vi.fn();
const onOpenChange = vi.fn();
- const TestModal = NiceModal.create(() => (
-
+ render(
+
- ));
-
- render();
-
- act(() => {
- void NiceModal.show(TestModal);
- });
+ );
await screen.findByText(/Menu item|Option one|Popover content|Combobox content/);
fireEvent.keyDown(document, {key: 'Escape'});
@@ -80,10 +73,6 @@ describe('nested overlay Escape behavior', () => {
await waitFor(() => {
expect(onCancel).toHaveBeenCalledOnce();
});
-
- act(() => {
- void NiceModal.remove(TestModal);
- });
});
it('preserves controlled overlay state behavior', async () => {
@@ -102,16 +91,11 @@ describe('nested overlay Escape behavior', () => {
);
};
- const TestModal = NiceModal.create(() => (
-
+ render(
+
- ));
-
- render();
- act(() => {
- void NiceModal.show(TestModal);
- });
+ );
await screen.findByText('Controlled content');
fireEvent.keyDown(document, {key: 'Escape'});
@@ -126,8 +110,8 @@ describe('nested overlay Escape behavior', () => {
const onCancel = vi.fn();
const onOuterOpenChange = vi.fn();
const onInnerOpenChange = vi.fn();
- const TestModal = NiceModal.create(() => (
-
+ render(
+
Open outer
@@ -141,12 +125,7 @@ describe('nested overlay Escape behavior', () => {
- ));
-
- render();
- act(() => {
- void NiceModal.show(TestModal);
- });
+ );
await screen.findByText('Inner item');
fireEvent.keyDown(document, {key: 'Escape'});
@@ -179,16 +158,11 @@ describe('nested overlay Escape behavior', () => {
it('keeps the modal open when a later document listener handles Escape', async () => {
const onCancel = vi.fn();
- const TestModal = NiceModal.create(() => (
-
+ render(
+
Modal content
- ));
-
- render();
- act(() => {
- void NiceModal.show(TestModal);
- });
+ );
await screen.findByText('Modal content');
const handleNestedEscape = (event: KeyboardEvent) => {
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2a1fd2f281d..27751ee72b9 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -57,9 +57,6 @@ catalogs:
'@dnd-kit/utilities':
specifier: 3.2.2
version: 3.2.2
- '@ebay/nice-modal-react':
- specifier: 1.2.13
- version: 1.2.13
'@eslint/compat':
specifier: 2.1.0
version: 2.1.0
@@ -748,9 +745,6 @@ importers:
'@dnd-kit/sortable':
specifier: 'catalog:'
version: 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
- '@ebay/nice-modal-react':
- specifier: 'catalog:'
- version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@sentry/react':
specifier: 'catalog:'
version: 7.120.4(react@18.3.1)
@@ -990,9 +984,6 @@ importers:
apps/admin-x-framework:
dependencies:
- '@ebay/nice-modal-react':
- specifier: 'catalog:'
- version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@sentry/react':
specifier: 'catalog:'
version: 7.120.4(react@18.3.1)
@@ -1724,9 +1715,6 @@ importers:
'@dnd-kit/utilities':
specifier: 'catalog:'
version: 3.2.2(react@18.3.1)
- '@ebay/nice-modal-react':
- specifier: 'catalog:'
- version: 1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@hookform/resolvers':
specifier: 5.4.0
version: 5.4.0(react-hook-form@7.80.0(react@18.3.1))
@@ -5282,12 +5270,6 @@ packages:
react: '>=17.0.2'
react-dom: '>=17.0.2'
- '@ebay/nice-modal-react@1.2.13':
- resolution: {integrity: sha512-jx8xIWe/Up4tpNuM02M+rbnLoxdngTGk3Y8LjJsLGXXcSoKd/+eZStZcAlIO/jwxyz/bhPZnpqPJZWAmhOofuA==}
- peerDependencies:
- react: '>16.8.0'
- react-dom: '>16.8.0'
-
'@elastic/elasticsearch@8.19.2':
resolution: {integrity: sha512-LMJCju/+AZkDlJArd/MYABWTDrHi4U7j3qGTKi1hYC7+67SaiYmYFItiueACQVrj2j3ECPMcguZ+7WrZeB+Z5g==}
engines: {node: '>=20'}
@@ -24237,11 +24219,6 @@ snapshots:
react: 17.0.2
react-dom: 17.0.2(react@17.0.2)
- '@ebay/nice-modal-react@1.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
- dependencies:
- react: 18.3.1
- react-dom: 18.3.1(react@18.3.1)
-
'@elastic/elasticsearch@8.19.2(supports-color@10.2.2)':
dependencies:
'@elastic/transport': 8.10.1(supports-color@10.2.2)
diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml
index d2de342fea6..8b714738fe2 100644
--- a/pnpm-workspace.yaml
+++ b/pnpm-workspace.yaml
@@ -54,7 +54,6 @@ catalog:
'@codemirror/state': 6.7.1
'@codemirror/theme-one-dark': 6.1.3
'@codemirror/view': 6.43.6
- '@ebay/nice-modal-react': 1.2.13
'@eslint/compat': 2.1.0
'@eslint/js': 9.39.5
'@faker-js/faker': 10.5.0