From 4cb987e33fe40fad85492202d0b5e96f0d131509 Mon Sep 17 00:00:00 2001
From: Steve Larson <9larsons@gmail.com>
Date: Thu, 23 Jul 2026 17:50:45 -0500
Subject: [PATCH 1/6] Moved settings modals to Shade (#29568)
no ref
- adds a transitional SettingsModal pattern in Shade with the existing
settings sizing, backdrop, sticky chrome, dirty-state, keyboard-save,
and NiceModal behavior
- switches all direct settings modal consumers and PreviewModalContent
to the Shade pattern
- removes the legacy admin-x-design-system Modal implementation and
refreshes Shade adoption data
---
.../src/global/modal/modal.stories.tsx | 289 -----------
.../src/global/modal/modal.tsx | 452 ------------------
.../src/global/modal/preview-modal.tsx | 8 +-
apps/admin-x-design-system/src/index.ts | 2 -
.../settings/advanced/code/code-modal.tsx | 6 +-
.../settings/advanced/history-modal.tsx | 6 +-
.../integrations/add-integration-modal.tsx | 6 +-
.../integrations/content-api-modal.tsx | 6 +-
.../integrations/custom-integration-modal.tsx | 6 +-
.../integrations/first-promoter-modal.tsx | 6 +-
.../advanced/integrations/pintura-modal.tsx | 6 +-
.../advanced/integrations/slack-modal.tsx | 6 +-
.../integrations/transistor-modal.tsx | 6 +-
.../advanced/integrations/unsplash-modal.tsx | 6 +-
.../advanced/integrations/webhook-modal.tsx | 6 +-
.../advanced/integrations/zapier-modal.tsx | 6 +-
.../settings/advanced/labs/feature-toggle.tsx | 6 +-
.../advanced/labs/yaml-file-editor-modal.tsx | 6 +-
.../universal-import-modal.tsx | 6 +-
.../newsletters/add-newsletter-modal.tsx | 6 +-
.../src/components/settings/general/about.tsx | 6 +-
.../settings/general/invite-user-modal.tsx | 6 +-
.../settings/general/user-detail-modal.tsx | 8 +-
.../embed-signup/embed-signup-form-modal.tsx | 6 +-
.../growth/explore/testimonials-modal.tsx | 6 +-
.../settings/growth/offers/offer-success.tsx | 6 +-
.../settings/growth/offers/offers-index.tsx | 6 +-
.../add-recommendation-modal-confirm.tsx | 6 +-
.../add-recommendation-modal.tsx | 10 +-
.../edit-recommendation-modal.tsx | 6 +-
.../custom-fields/custom-field-modal.tsx | 6 +-
.../member-emails/welcome-email-modal.tsx | 6 +-
.../stripe/stripe-connect-modal.tsx | 6 +-
.../membership/tiers/tier-detail-modal.tsx | 6 +-
.../settings/site/navigation-modal.tsx | 6 +-
.../components/settings/site/theme-modal.tsx | 7 +-
.../site/theme/theme-editor-confirm-modal.tsx | 6 +-
.../site/theme/theme-editor-input-modal.tsx | 6 +-
apps/admin-x-settings/src/main-content.tsx | 3 +-
apps/shade/package.json | 1 +
.../patterns/settings-modal.stories.tsx | 158 ++++++
.../components/patterns/settings-modal.tsx | 351 ++++++++++++++
apps/shade/src/docs/adoption-data.json | 94 ++--
apps/shade/src/patterns.ts | 2 +
apps/shade/tailwind.theme.css | 2 +
apps/shade/theme-variables.css | 2 +
pnpm-lock.yaml | 3 +
47 files changed, 661 insertions(+), 917 deletions(-)
delete mode 100644 apps/admin-x-design-system/src/global/modal/modal.stories.tsx
delete mode 100644 apps/admin-x-design-system/src/global/modal/modal.tsx
create mode 100644 apps/shade/src/components/patterns/settings-modal.stories.tsx
create mode 100644 apps/shade/src/components/patterns/settings-modal.tsx
diff --git a/apps/admin-x-design-system/src/global/modal/modal.stories.tsx b/apps/admin-x-design-system/src/global/modal/modal.stories.tsx
deleted file mode 100644
index e90afa48a0e..00000000000
--- a/apps/admin-x-design-system/src/global/modal/modal.stories.tsx
+++ /dev/null
@@ -1,289 +0,0 @@
-import type {Meta, StoryContext, StoryObj} from '@storybook/react-vite';
-import {ReactNode} from 'react';
-
-import NiceModal from '@ebay/nice-modal-react';
-import Modal, {ModalProps} from './modal';
-
-const ModalContainer: React.FC = ({children, ...props}) => {
- const modal = NiceModal.create(() => {
- return (
-
-
- {children}
-
-
- );
- });
- return (
-
- {
- NiceModal.show(modal);
- }}>Open modal
-
- );
-};
-
-const meta = {
- title: 'Global / Modal',
- component: Modal,
- tags: ['autodocs'],
- argTypes: {
- topRightContent: {
- control: {
- type: 'text'
- }
- }
- },
- decorators: [(_story: () => ReactNode, context: StoryContext) => (
-
-
-
- )]
-
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-const modalContent = (Modal content
);
-
-export const Default: Story = {
- args: {
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- topRightContent: 'close',
- title: 'Modal dialog',
- children: modalContent
- }
-};
-
-export const Small: Story = {
- args: {
- size: 'sm',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Small modal',
- children: modalContent
- }
-};
-
-export const Medium: Story = {
- args: {
- size: 'md',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Medium modal (default size)',
- children: modalContent
- }
-};
-
-export const Large: Story = {
- args: {
- size: 'lg',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Large modal',
- children: modalContent
- }
-};
-
-export const ExtraLarge: Story = {
- args: {
- size: 'xl',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Extra large modal',
- children: modalContent
- }
-};
-
-export const Full: Story = {
- args: {
- size: 'full',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Full modal',
- children: modalContent
- }
-};
-
-export const Bleed: Story = {
- args: {
- size: 'bleed',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Full bleed modal',
- children: modalContent
- }
-};
-
-export const CustomWidth: Story = {
- args: {
- width: 600,
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Custom width modal',
- children: modalContent
- }
-};
-
-export const CustomHeight: Story = {
- args: {
- size: 'md',
- height: 'full',
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Custom height modal',
- children: modalContent
- }
-};
-
-export const Square: Story = {
- args: {
- width: 320,
- height: 320,
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Square modal',
- children: modalContent
- }
-};
-
-export const CompletePage: Story = {
- args: {
- size: 'full',
- footer: <>>,
- padding: false,
- children: <>
- Full-page modal content
- >
- }
-};
-
-export const CustomButtons: Story = {
- args: {
- leftButton: Left action slot ,
- cancelLabel: 'Nope',
- okLabel: 'Yep',
- onOk: () => {
- alert('Clicked Yep!');
- },
- onCancel: undefined,
- title: 'Custom buttons',
- children: modalContent
- }
-};
-
-export const RightDrawer: Story = {
- args: {
- size: 'bleed',
- align: 'right',
- animate: false,
- width: 600,
- footer: <>>,
- children: <>
- This is a drawer style on the right
- >
- }
-};
-
-export const LeftDrawer: Story = {
- args: {
- size: 'bleed',
- align: 'left',
- animate: false,
- width: 600,
- footer: <>>,
- children: <>
- This is a drawer style on the right
- >
- }
-};
-
-const longContent = (
- <>
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure. Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure. Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure. Esse ex officia ipsum et magna reprehenderit ullamco dolore cillum cupidatat ullamco culpa. In et irure irure est id cillum officia pariatur et proident. Nulla nulla dolore qui excepteur magna eu adipisicing mollit. Eiusmod eu irure cupidatat consequat consectetur irure.
- >
-);
-
-export const StickyHeader: Story = {
- args: {
- size: 'md',
- stickyHeader: true,
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Sticky header',
- stickyFooter: true,
- children: longContent
- }
-};
-
-export const StickyFooter: Story = {
- args: {
- size: 'md',
- stickyFooter: true,
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Sticky footer',
- children: longContent
- }
-};
-
-export const Dirty: Story = {
- args: {
- size: 'md',
- dirty: true,
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- title: 'Dirty modal',
- children: Simulates if there were unsaved changes of a form. Click on Cancel
- }
-};
-
-export const FormSheet: Story = {
- args: {
- onOk: () => {
- alert('Clicked OK!');
- },
- onCancel: undefined,
- size: 'sm',
- title: 'Form sheet',
- formSheet: true,
- children: Slightly differently styled modal that can be used to display small forms inside other modals . Use it sparingly!
- }
-};
diff --git a/apps/admin-x-design-system/src/global/modal/modal.tsx b/apps/admin-x-design-system/src/global/modal/modal.tsx
deleted file mode 100644
index e92f19fa149..00000000000
--- a/apps/admin-x-design-system/src/global/modal/modal.tsx
+++ /dev/null
@@ -1,452 +0,0 @@
-import {useModal} from '@ebay/nice-modal-react';
-import clsx from 'clsx';
-import React, {useEffect, useState, forwardRef} from 'react';
-import {Button, type ButtonProps, LoadingIndicator, StickyFooter} from '@tryghost/shade/components';
-import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns';
-import {Inline, Text} from '@tryghost/shade/primitives';
-import {LucideIcon, useGlobalDirtyState} from '@tryghost/shade/utils';
-
-export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'bleed';
-
-export interface ModalProps {
-
- /**
- * Possible values are: `sm`, `md`, `lg`, `xl, `full`, `bleed`. Yu can also use any number to set an arbitrary width.
- */
- size?: ModalSize;
- width?: 'full' | 'toSidebar' | number;
- height?: 'full' | number;
- align?: 'center' | 'left' | 'right';
-
- testId?: string;
- title?: React.ReactNode;
- okLabel?: string;
- okVariant?: ButtonProps['variant'];
- okLoading?: boolean;
- cancelLabel?: string;
- leftButton?: React.ReactNode;
- buttonsDisabled?: boolean;
- okDisabled?: boolean;
- footer?: boolean | React.ReactNode;
- header?: boolean;
- padding?: boolean;
- onOk?: () => void;
- onCancel?: () => void;
- topRightContent?: 'close' | React.ReactNode;
- hideXOnMobile?: boolean;
- afterClose?: () => void;
- children?: React.ReactNode;
- backDrop?: boolean;
- backDropClick?: boolean;
- stickyFooter?: boolean;
- stickyHeader?:boolean;
- scrolling?: boolean;
- dirty?: boolean;
- animate?: boolean;
- formSheet?: boolean;
- enableCMDS?: boolean;
- allowBackgroundInteraction?: boolean;
-}
-
-export const topLevelBackdropClasses = 'bg-[rgba(98,109,121,0.2)] backdrop-blur-[3px]';
-
-const Modal = forwardRef(({
- size = 'md',
- align = 'center',
- width,
- height,
- testId,
- title,
- okLabel = 'OK',
- okLoading = false,
- cancelLabel = 'Cancel',
- footer,
- header,
- leftButton,
- buttonsDisabled,
- okDisabled,
- padding = true,
- onOk,
- okVariant = 'default',
- onCancel,
- topRightContent,
- hideXOnMobile = false,
- afterClose,
- children,
- backDrop = true,
- backDropClick = true,
- stickyFooter = false,
- stickyHeader = false,
- scrolling = true,
- dirty = false,
- animate = true,
- formSheet = false,
- enableCMDS = true,
- allowBackgroundInteraction = false
-}, ref) => {
- const modal = useModal();
- const {setGlobalDirtyState} = useGlobalDirtyState();
- const {confirm, dialogProps} = useDirtyConfirmation();
- const [animationFinished, setAnimationFinished] = useState(false);
-
- useEffect(() => {
- setGlobalDirtyState(dirty);
- }, [dirty, setGlobalDirtyState]);
-
- useEffect(() => {
- const handleEscapeKey = (event: KeyboardEvent) => {
- if (event.key === 'Escape') {
- // Don't close modal if user is in Koenig's link input (which handles ESC itself)
- const activeEl = document.activeElement;
- if (activeEl?.hasAttribute('data-kg-link-input')) {
- return;
- }
-
- // Fix for Safari - if an element in the modal is focused, closing it will jump to
- // the bottom of the page because Safari tries to focus the "next" element in the DOM
- if (document.activeElement && document.activeElement instanceof HTMLElement) {
- document.activeElement.blur();
- }
- // Close the modal on the next tick so that the blur registers
- setTimeout(() => {
- if (onCancel) {
- onCancel();
- } else {
- confirm(dirty, () => {
- modal.remove();
- afterClose?.();
- });
- }
- });
-
- // Prevent the event from bubbling up to the window level
- event.stopPropagation();
- }
- };
-
- document.addEventListener('keydown', handleEscapeKey);
-
- // Clean up the event listener when the modal is closed
- return () => {
- document.removeEventListener('keydown', handleEscapeKey);
- };
- }, [modal, dirty, afterClose, onCancel, confirm]);
-
- // The animation classes apply a transform to the modal, which breaks anything inside using position:fixed
- // We should remove the class as soon as the animation is finished
- useEffect(() => {
- const timeout = setTimeout(() => {
- setAnimationFinished(true);
- }, 250);
-
- return () => clearTimeout(timeout);
- }, []);
-
- useEffect(() => {
- if (onOk) {
- const handleCMDS = (e: KeyboardEvent) => {
- if ((e.metaKey || e.ctrlKey) && e.key === 's') {
- e.preventDefault();
- onOk();
- }
- };
- if (enableCMDS) {
- window.addEventListener('keydown', handleCMDS);
- return () => {
- window.removeEventListener('keydown', handleCMDS);
- };
- }
- }
- });
-
- let contentClasses;
-
- const removeModal = () => {
- confirm(dirty, () => {
- modal.remove();
- afterClose?.();
- });
- };
-
- let modalClasses = clsx(
- 'relative z-50 flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden bg-background text-foreground',
- align === 'center' && 'mx-auto',
- align === 'left' && 'mr-auto',
- align === 'right' && 'ml-auto',
- size !== 'bleed' && 'rounded',
- formSheet ? 'shadow-md' : 'shadow-xl',
- (animate && !formSheet && !animationFinished && align === 'center') && 'animate-modal-in',
- (animate && !formSheet && !animationFinished && align === 'right') && 'animate-modal-in-from-right',
- (formSheet && !animationFinished) && 'animate-modal-in-reverse',
- scrolling ? 'overflow-y-auto' : 'overflow-y-hidden'
- );
-
- let backdropClasses = clsx(
- 'fixed inset-0 z-[1000] h-[100dvh] w-[100dvw]',
- allowBackgroundInteraction && 'pointer-events-none'
- );
-
- let paddingClasses = '';
- let headerClasses = clsx(
- (!topRightContent || topRightContent === 'close') ? '' : 'flex items-center justify-between gap-5'
- );
-
- if (stickyHeader) {
- headerClasses = clsx(
- headerClasses,
- 'sticky -top-px z-[300] -mb-4 bg-background pb-4!'
- );
- }
-
- switch (size) {
- case 'sm':
- modalClasses = clsx(
- modalClasses,
- 'max-w-[480px]'
- );
- backdropClasses = clsx(
- backdropClasses,
- 'p-4 md:p-[8vmin]'
- );
- paddingClasses = 'p-8';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-8'
- );
- break;
-
- case 'md':
- modalClasses = clsx(
- modalClasses,
- 'max-w-[720px]'
- );
- backdropClasses = clsx(
- backdropClasses,
- 'p-4 md:p-[8vmin]'
- );
- paddingClasses = 'p-8';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-8'
- );
- break;
-
- case 'lg':
- modalClasses = clsx(
- modalClasses,
- 'max-w-[1020px]'
- );
- backdropClasses = clsx(
- backdropClasses,
- 'p-4 md:p-[4vmin]'
- );
- paddingClasses = 'p-7';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-8'
- );
- break;
-
- case 'xl':
- modalClasses = clsx(
- modalClasses,
- 'max-w-[1240px]0'
- );
- backdropClasses = clsx(
- backdropClasses,
- 'p-4 md:p-[3vmin]'
- );
- paddingClasses = 'p-10';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-10 -top-10'
- );
- break;
-
- case 'full':
- modalClasses = clsx(
- modalClasses,
- 'h-full'
- );
- backdropClasses = clsx(
- backdropClasses,
- 'p-4 md:p-[3vmin]'
- );
- paddingClasses = 'p-10';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-10'
- );
- break;
-
- case 'bleed':
- modalClasses = clsx(
- modalClasses,
- 'h-full'
- );
- paddingClasses = 'p-10';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-10'
- );
- break;
-
- default:
- backdropClasses = clsx(
- backdropClasses,
- 'p-4 md:p-[8vmin]'
- );
- paddingClasses = 'p-8';
- headerClasses = clsx(
- headerClasses,
- '-inset-x-8'
- );
- break;
- }
-
- if (!padding) {
- paddingClasses = 'p-0';
- }
-
- modalClasses = clsx(
- modalClasses
- );
-
- headerClasses = clsx(
- headerClasses,
- paddingClasses,
- 'pb-0'
- );
-
- contentClasses = clsx(
- paddingClasses,
- 'py-0'
- );
-
- // Set bottom padding for backdrop when the menu is on
- backdropClasses = clsx(
- backdropClasses,
- 'max-[800px]:!pb-20'
- );
-
- const footerClasses = clsx(
- `${paddingClasses} ${stickyFooter ? 'py-6' : ''}`,
- 'flex w-full items-center justify-between'
- );
-
- contentClasses = clsx(
- contentClasses,
- ((size === 'full' || size === 'bleed' || height === 'full' || typeof height === 'number') && 'grow')
- );
-
- const handleBackdropClick = (e: React.MouseEvent) => {
- if (e.target === e.currentTarget && backDropClick) {
- removeModal();
- }
- };
-
- const modalStyles:{width?: string; height?: string; maxWidth?: string; maxHeight?: string;} = {};
-
- if (typeof width === 'number') {
- modalStyles.width = '100%';
- modalStyles.maxWidth = width + 'px';
- } else if (width === 'full') {
- modalClasses = clsx(
- modalClasses,
- 'w-full'
- );
- } else if (width === 'toSidebar') {
- modalClasses = clsx(
- modalClasses,
- 'w-full max-w-[calc(100dvw_-_280px)] lg:max-w-full min-[1280px]:max-w-[calc(100dvw_-_320px)]'
- );
- }
-
- if (typeof height === 'number') {
- modalStyles.height = '100%';
- modalStyles.maxHeight = height + 'px';
- } else if (height === 'full') {
- modalClasses = clsx(
- modalClasses,
- 'h-full'
- );
- }
-
- let footerContent;
- if (footer) {
- footerContent = footer;
- } else if (footer === false) {
- contentClasses += ' pb-0 ';
- } else {
- footerContent = (
-
-
- {leftButton}
-
-
- {cancelLabel && (
-
- {cancelLabel}
-
- )}
- {okLabel && (
-
- {okLoading && }
- {okLabel}
-
- )}
-
-
- );
- }
-
- footerContent = (stickyFooter ?
-
- {footerContent}
-
- :
- <>
- {footerContent}
- >
- );
-
- return (
- <>
-
-
-
- {header === false ? '' : (!topRightContent || topRightContent === 'close' ?
- (
- {title && {title} }
-
-
-
-
-
- )
- :
- (
- {title && {title} }
- {topRightContent}
- ))}
-
- {children}
-
- {footerContent}
-
-
-
- >
- );
-});
-
-Modal.displayName = 'Modal';
-
-export default Modal;
diff --git a/apps/admin-x-design-system/src/global/modal/preview-modal.tsx b/apps/admin-x-design-system/src/global/modal/preview-modal.tsx
index 568b266e149..642480dc2eb 100644
--- a/apps/admin-x-design-system/src/global/modal/preview-modal.tsx
+++ b/apps/admin-x-design-system/src/global/modal/preview-modal.tsx
@@ -1,7 +1,7 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import clsx from 'clsx';
import React, {useEffect} from 'react';
-import Modal, {ModalSize} from './modal';
+import {SettingsModal, type SettingsModalSize} from '@tryghost/shade/patterns';
import {Button, type ButtonProps} from '@tryghost/shade/components';
import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns';
import {Inline, Text, type TextElement, type TextLeading, type TextSize} from '@tryghost/shade/primitives';
@@ -40,7 +40,7 @@ export interface PreviewModalProps {
testId?: string;
title?: string;
titleHeadingLevel?: HeadingLevel;
- size?: ModalSize;
+ size?: SettingsModalSize;
width?: 'full' | number;
height?: 'full' | number;
sidebar?: boolean | React.ReactNode;
@@ -187,7 +187,7 @@ export const PreviewModalContent: React.FC = ({
});
return (
- = ({
}
-
+
);
};
diff --git a/apps/admin-x-design-system/src/index.ts b/apps/admin-x-design-system/src/index.ts
index 350d99264ff..21e659a4e4d 100644
--- a/apps/admin-x-design-system/src/index.ts
+++ b/apps/admin-x-design-system/src/index.ts
@@ -6,8 +6,6 @@ export {default as XLogo} from './assets/images/x-logo.svg?react';
export {default as DesktopChromeHeader} from './global/chrome/desktop-chrome-header';
export type {DesktopChromeHeaderProps} from './global/chrome/desktop-chrome-header';
-export {default as Modal, topLevelBackdropClasses} from './global/modal/modal';
-export type {ModalProps} from './global/modal/modal';
export {default as PreviewModal, PreviewModalContent} from './global/modal/preview-modal';
export type {PreviewModalProps} from './global/modal/preview-modal';
diff --git a/apps/admin-x-settings/src/components/settings/advanced/code/code-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/code/code-modal.tsx
index b614b469057..72566fa2b05 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/code/code-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/code/code-modal.tsx
@@ -4,8 +4,8 @@ import React, {useEffect, useMemo, useRef, useState} from 'react';
import useSettingGroup from '../../../../hooks/use-setting-group';
import {Button, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components';
import {Inline, Text} from '@tryghost/shade/primitives';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type ReactCodeMirrorRef} from '@uiw/react-codemirror';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useSaveButton} from '../../../../hooks/use-save-button';
@@ -62,7 +62,7 @@ const CodeModal: React.FC = ({afterClose}) => {
};
});
- return = ({afterClose}) => {
- ;
+ ;
};
export default NiceModal.create(CodeModal);
diff --git a/apps/admin-x-settings/src/components/settings/advanced/history-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/history-modal.tsx
index 1475cccbe4a..2ce1cb0cbd6 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/history-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/history-modal.tsx
@@ -4,8 +4,8 @@ import {type Action, getActionTitle, getContextResource, getLinkTarget, isBulkAc
import {ActionList, ActionListItem, ActionListItemContent, Avatar, Button, Field, FieldLabel, LoadingIndicator, MultiSelectCombobox, NoValueLabel, NoValueLabelIcon, Popover, PopoverContent, PopoverTrigger, Switch, inputSurface} from '@tryghost/shade/components';
import {ChevronDown, History, Pen, Plus, Trash2, X} from 'lucide-react';
import {Inline, Stack} from '@tryghost/shade/primitives';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {type User} from '@tryghost/admin-x-framework/api/users';
import {formatNumber} from '@tryghost/shade/utils';
import {keepPreviousData} from '@tanstack/react-query';
@@ -311,7 +311,7 @@ const HistoryModal = NiceModal.create(({params}) => {
const hasActiveFilters = excludedEvents.length > 0 || excludedResources.length > 0 || params?.user;
return (
- {
updateRoute('history');
}}
@@ -387,7 +387,7 @@ const HistoryModal = NiceModal.create(({params}) => {
{data?.isEnd && data.actions.length > 0 && End of history log
}
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/add-integration-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/add-integration-modal.tsx
index 512587631d1..7bd9760857c 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/add-integration-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/add-integration-modal.tsx
@@ -3,8 +3,8 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useEffect, useState} from 'react';
import {Field, FieldError, FieldGroup, FieldLabel, Input} from '@tryghost/shade/components';
import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {useCreateIntegration} from '@tryghost/admin-x-framework/api/integrations';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -32,7 +32,7 @@ const AddIntegrationModal: React.FC = () => {
}
}, [limiter, modal, updateRoute]);
- return {
updateRoute('integrations');
}}
@@ -65,7 +65,7 @@ const AddIntegrationModal: React.FC = () => {
- ;
+ ;
};
export default NiceModal.create(AddIntegrationModal);
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/content-api-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/content-api-modal.tsx
index 079732db332..f38d3b4753d 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/content-api-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/content-api-modal.tsx
@@ -3,7 +3,7 @@ import IntegrationHeader from './integration-header';
import NiceModal from '@ebay/nice-modal-react';
import {Button} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@@ -17,7 +17,7 @@ const ContentApiModal = NiceModal.create(() => {
const contentApiKey = integration?.api_keys?.find(key => key.type === 'content');
return (
- {
updateRoute('integrations');
}}
@@ -51,7 +51,7 @@ const ContentApiModal = NiceModal.create(() => {
{id: 'api-url', label: 'API URL', text: window.location.origin + getGhostPaths().subdir}
]} />
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/custom-integration-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/custom-integration-modal.tsx
index c93efd8f09c..13b11fa6a7b 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/custom-integration-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/custom-integration-modal.tsx
@@ -8,8 +8,8 @@ import {type APIKey, useRefreshAPIKey} from '@tryghost/admin-x-framework/api/api
import {Field, FieldError, FieldGroup, FieldLabel, Input} from '@tryghost/shade/components';
import {ImageUpload, ImageUploadAction, ImageUploadActions, ImageUploadDropzone, ImageUploadImage, ImageUploadPreview} from '@tryghost/shade/patterns';
import {type Integration, useBrowseIntegrations, useEditIntegration} from '@tryghost/admin-x-framework/api/integrations';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {Trash2} from 'lucide-react';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
@@ -80,7 +80,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
});
};
- return {
updateRoute('integrations');
}}
@@ -166,7 +166,7 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in
- ;
+ ;
};
const CustomIntegrationModal: React.FC = ({params}) => {
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/first-promoter-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/first-promoter-modal.tsx
index 27ee2b6be59..ca819754ae0 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/first-promoter-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/first-promoter-modal.tsx
@@ -2,8 +2,8 @@ import BrandIcon from '../../../icons/brand-icon';
import IntegrationHeader from './integration-header';
import NiceModal from '@ebay/nice-modal-react';
import {Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, Input, Switch} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/global-data-provider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -57,7 +57,7 @@ const FirstPromoterModal = NiceModal.create(() => {
};
return (
- {
updateRoute('integrations');
}}
@@ -108,7 +108,7 @@ const FirstPromoterModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/pintura-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/pintura-modal.tsx
index 1fecd8dc4fe..fcaf7935f16 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/pintura-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/pintura-modal.tsx
@@ -3,8 +3,8 @@ import IntegrationHeader from './integration-header';
import NiceModal from '@ebay/nice-modal-react';
import pinturaScreenshot from '../../../../assets/images/pintura-screenshot.png';
import {Dropzone, Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, Switch} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {toast} from 'sonner';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/global-data-provider';
@@ -75,7 +75,7 @@ const PinturaModal = NiceModal.create(() => {
const isDirty = !(enabled === pinturaEnabled);
return (
- {
updateRoute('integrations');
}}
@@ -140,7 +140,7 @@ const PinturaModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/slack-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/slack-modal.tsx
index dece8590c39..d2d21986326 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/slack-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/slack-modal.tsx
@@ -4,7 +4,7 @@ import NiceModal from '@ebay/nice-modal-react';
import useSettingGroup from '../../../../hooks/use-setting-group';
import validator from 'validator';
import {Button, Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, Input} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getSettingValues, useTestSlack} from '@tryghost/admin-x-framework/api/settings';
import {toast} from 'sonner';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@@ -39,7 +39,7 @@ const SlackModal = NiceModal.create(() => {
const isDirty = localSettings.some(setting => setting.dirty);
return (
- {
updateRoute('integrations');
}}
@@ -81,7 +81,7 @@ const SlackModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/transistor-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/transistor-modal.tsx
index a036cb57ccb..5221d41e437 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/transistor-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/transistor-modal.tsx
@@ -5,8 +5,8 @@ import ConfirmationModal from '../../../confirmation-modal';
import IntegrationHeader from './integration-header';
import NiceModal from '@ebay/nice-modal-react';
import {Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, FieldLegend, FieldSet, Switch} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations';
import {useEffect, useState} from 'react';
@@ -90,7 +90,7 @@ const TransistorModal = NiceModal.create(() => {
};
return (
- {
updateRoute('integrations');
}}
@@ -146,7 +146,7 @@ const TransistorModal = NiceModal.create(() => {
}
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/unsplash-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/unsplash-modal.tsx
index 2ec5e77a281..81c8e654a37 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/unsplash-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/unsplash-modal.tsx
@@ -2,8 +2,8 @@ import BrandIcon from '../../../icons/brand-icon';
import IntegrationHeader from './integration-header';
import NiceModal from '@ebay/nice-modal-react';
import {Field, FieldContent, FieldDescription, FieldGroup, FieldLabel, Switch} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {useEffect, useState} from 'react';
import {useGlobalData} from '../../../providers/global-data-provider';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -45,7 +45,7 @@ const UnsplashModal = NiceModal.create(() => {
const isDirty = !(enabled === unsplashEnabled);
return (
- {
updateRoute('integrations');
}}
@@ -73,7 +73,7 @@ const UnsplashModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/webhook-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/webhook-modal.tsx
index c7489b4da04..fa44425bf12 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/webhook-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/webhook-modal.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import validator from 'validator';
import webhookEventOptions from './webhook-event-options';
import {Field, FieldError, FieldGroup, FieldLabel, Input, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {type Webhook, useCreateWebhook, useEditWebhook} from '@tryghost/admin-x-framework/api/webhooks';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -52,7 +52,7 @@ const WebhookModal: React.FC = ({webhook, integrationId}) =>
}
});
- return = ({webhook, integrationId}) =>
- ;
+ ;
};
export default NiceModal.create(WebhookModal);
diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/zapier-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/zapier-modal.tsx
index 9d948c1487d..2dad2112bbf 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/integrations/zapier-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/zapier-modal.tsx
@@ -6,7 +6,7 @@ import NiceModal from '@ebay/nice-modal-react';
import ZapierLogo from '../../../../assets/images/zapier-logo.svg';
import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations';
import {useEffect, useState} from 'react';
@@ -68,7 +68,7 @@ const ZapierModal = NiceModal.create(() => {
};
return (
- {
updateRoute('integrations');
}}
@@ -130,7 +130,7 @@ const ZapierModal = NiceModal.create(() => {
))}
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/feature-toggle.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/feature-toggle.tsx
index 549f313f906..54bb1a10fcc 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/labs/feature-toggle.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/labs/feature-toggle.tsx
@@ -2,7 +2,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import trackEvent from '../../../../utils/analytics';
import {type ConfigResponseType, configDataType} from '@tryghost/admin-x-framework/api/config';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {Switch} from '@tryghost/shade/components';
import {getSettingValue, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {useGlobalData} from '../../../providers/global-data-provider';
@@ -55,7 +55,7 @@ const FeatureToggleConfirmationModal = NiceModal.create
{prompt}
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/yaml-file-editor-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/yaml-file-editor-modal.tsx
index 8d508d6ec12..ff266a3b8bf 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/labs/yaml-file-editor-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/labs/yaml-file-editor-modal.tsx
@@ -4,7 +4,7 @@ import React, {useEffect, useMemo, useState} from 'react';
import {APIError, JSONError} from '@tryghost/admin-x-framework/errors';
import {Button} from '@tryghost/shade/components';
import {Inline, Text} from '@tryghost/shade/primitives';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
import {toast} from 'sonner';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -142,7 +142,7 @@ const YamlFileEditorModal: React.FC = ({
const canSave = !isLoading && !loadError && !isSaving;
return (
- = ({
)}
-
+
);
};
diff --git a/apps/admin-x-settings/src/components/settings/advanced/migration-tools/universal-import-modal.tsx b/apps/admin-x-settings/src/components/settings/advanced/migration-tools/universal-import-modal.tsx
index 6a9263a5262..a6ec042eeb7 100644
--- a/apps/admin-x-settings/src/components/settings/advanced/migration-tools/universal-import-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/advanced/migration-tools/universal-import-modal.tsx
@@ -2,7 +2,7 @@ import ConfirmationModal from '../../../confirmation-modal';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import {Button, Dropzone} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useImportContent} from '@tryghost/admin-x-framework/api/db';
@@ -13,7 +13,7 @@ const UniversalImportModal: React.FC = () => {
const handleError = useHandleError();
return (
-
@@ -57,7 +57,7 @@ const UniversalImportModal: React.FC = () => {
-
+
);
};
diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/add-newsletter-modal.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/add-newsletter-modal.tsx
index 6d573a01c8d..a1f61270900 100644
--- a/apps/admin-x-settings/src/components/settings/email/newsletters/add-newsletter-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/email/newsletters/add-newsletter-modal.tsx
@@ -4,8 +4,8 @@ import React, {useEffect, useState} from 'react';
import useFeatureFlag from '../../../../hooks/use-feature-flag';
import {Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, Input, Switch, Textarea} from '@tryghost/shade/components';
import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {formatNumber} from '@tryghost/shade/utils';
import {useAddNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
import {useBrowseMembers} from '@tryghost/admin-x-framework/api/members';
@@ -89,7 +89,7 @@ const AddNewsletterModal: React.FC = () => {
return null;
}
- return {
updateRoute(returnRoute);
}}
@@ -130,7 +130,7 @@ const AddNewsletterModal: React.FC = () => {
updateForm(state => ({...state, optInExistingSubscribers: checked}))} />
- ;
+ ;
};
export default NiceModal.create(AddNewsletterModal);
diff --git a/apps/admin-x-settings/src/components/settings/general/about.tsx b/apps/admin-x-settings/src/components/settings/general/about.tsx
index ee2216380f6..1102a65c1c6 100644
--- a/apps/admin-x-settings/src/components/settings/general/about.tsx
+++ b/apps/admin-x-settings/src/components/settings/general/about.tsx
@@ -1,8 +1,8 @@
import NiceModal from '@ebay/nice-modal-react';
import {GhostLogo, Separator} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {linkToGitHubReleases} from '../../../utils/link-to-github-releases';
import {showDatabaseWarning} from '../../../utils/show-database-warning';
import {useGlobalData} from '../../providers/global-data-provider';
@@ -51,7 +51,7 @@ const AboutModal = NiceModal.create(() => {
}
return (
- {
updateRoute('');
}}
@@ -113,7 +113,7 @@ const AboutModal = NiceModal.create(() => {
Copyright © 2013 – {copyrightYear()} Ghost Foundation, released under the MIT license . Ghost is a registered trademark of Ghost Foundation Ltd .
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/general/invite-user-modal.tsx b/apps/admin-x-settings/src/components/settings/general/invite-user-modal.tsx
index 3488c9beeed..08c8439ce06 100644
--- a/apps/admin-x-settings/src/components/settings/general/invite-user-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/general/invite-user-modal.tsx
@@ -3,7 +3,7 @@ import validator from 'validator';
import {APIError, ValidationError} from '@tryghost/admin-x-framework/errors';
import {Field, FieldContent, FieldDescription, FieldError, FieldLabel, FieldLegend, FieldSeparator, FieldSet, Input, RadioGroup, RadioGroupItem} from '@tryghost/shade/components';
import {HostLimitError, useLimiter} from '../../../hooks/use-limiter';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {toast} from 'sonner';
import {useAddInvite, useBrowseInvites} from '@tryghost/admin-x-framework/api/invites';
import {useBrowseRoles} from '@tryghost/admin-x-framework/api/roles';
@@ -207,7 +207,7 @@ const InviteUserModal = NiceModal.create(() => {
}
return (
- {
updateRoute('staff');
}}
@@ -263,7 +263,7 @@ const InviteUserModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx b/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx
index 162dd4185eb..d34e18212cd 100644
--- a/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/general/user-detail-modal.tsx
@@ -15,10 +15,10 @@ import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-fra
import {HostLimitError, useLimiter} from '../../../hooks/use-limiter';
import {ImageUpload, ImageUploadAction, ImageUploadActions, ImageUploadDropzone, ImageUploadImage, ImageUploadPreview} from '@tryghost/shade/patterns';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
import {Pencil, Trash2} from 'lucide-react';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {SOCIAL_PLATFORM_CONFIGS, SOCIAL_PLATFORM_KEYS, getSocialValidationError} from '../../../utils/social-urls/index';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {Text} from '@tryghost/shade/primitives';
import {type User, canAccessSettings, hasAdminAccess, isAdminUser, isAuthorOrContributor, isEditorUser, isOwnerUser, useDeleteUser, useEditUser, useGetUserBySlug, useMakeOwner} from '@tryghost/admin-x-framework/api/users';
import {getImageUrl, useUploadImage} from '@tryghost/admin-x-framework/api/images';
@@ -323,7 +323,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel
};
return (
-
- {/* legacy Modal overlay is z-[1000]; keep the portalled menu above it */}
+ {/* legacy SettingsModal overlay is z-[1000]; keep the portalled menu above it */}
{canMakeOwner && (
@@ -449,7 +449,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel
-
+
);
};
diff --git a/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form-modal.tsx
index 1afe4d27519..42c5046b5a6 100644
--- a/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form-modal.tsx
@@ -2,7 +2,7 @@ import EmbedSignupPreview from './embed-signup-preview';
import EmbedSignupSidebar, {type SelectedLabelTypes} from './embed-signup-sidebar';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import useSettingGroup from '../../../../hooks/use-setting-group';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {generateCode} from '../../../../utils/generate-embed-code';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useEffect, useState} from 'react';
@@ -93,7 +93,7 @@ const EmbedSignupFormModal = NiceModal.create(() => {
};
return (
- {
updateRoute('embed-signup-form');
}}
@@ -127,7 +127,7 @@ const EmbedSignupFormModal = NiceModal.create(() => {
setCustomColor={setCustomColor}
/>
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/growth/explore/testimonials-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/explore/testimonials-modal.tsx
index 0fce4bf0250..289692498a7 100644
--- a/apps/admin-x-settings/src/components/settings/growth/explore/testimonials-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/explore/testimonials-modal.tsx
@@ -5,7 +5,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import {Avatar, Field, FieldError, FieldGroup, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components';
import {Button, LoadingIndicator} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {toast} from 'sonner';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -96,7 +96,7 @@ const TestimonialsModal = NiceModal.create(() => {
];
return (
- {
updateRoute('explore');
}}
@@ -233,7 +233,7 @@ const TestimonialsModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/offer-success.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/offer-success.tsx
index 4461675ccb5..125795635a3 100644
--- a/apps/admin-x-settings/src/components/settings/growth/offers/offer-success.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/offers/offer-success.tsx
@@ -2,8 +2,8 @@ import BrandIcon from '../../../icons/brand-icon';
import SettingsBreadcrumbs from '../../settings-breadcrumbs';
import {Button, Input} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Offer, useBrowseOffersById} from '@tryghost/admin-x-framework/api/offers';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {currencyToDecimal} from '../../../../utils/currency';
import {formatNumber} from '@tryghost/shade/utils';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
@@ -69,7 +69,7 @@ const OfferSuccess: React.FC<{id: string}> = ({id}) => {
window.open(`http://www.linkedin.com/shareArticle?mini=true&url=${encodeURI(offerLink)}&title=${getShareText()}`, '_blank');
};
- return {
updateRoute('offers');
}}
@@ -105,7 +105,7 @@ const OfferSuccess: React.FC<{id: string}> = ({id}) => {
- ;
+ ;
};
export default OfferSuccess;
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/offers-index.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/offers-index.tsx
index 15b13eb01fa..1c08d1260f1 100644
--- a/apps/admin-x-settings/src/components/settings/growth/offers/offers-index.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/offers/offers-index.tsx
@@ -1,9 +1,9 @@
import {Badge, Button, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components';
import {Inline, Stack} from '@tryghost/shade/primitives';
import {LucideIcon, formatNumber} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Offer, useBrowseOffers} from '@tryghost/admin-x-framework/api/offers';
import {type RetentionOffer, getRetentionOffers} from './offers-retention';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {type Tier, getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers';
import {createOfferRedemptionFilterUrl, createOfferRedemptionsFilterUrl} from './offer-helpers';
import {currencyToDecimal, getSymbol} from '../../../../utils/currency';
@@ -410,7 +410,7 @@ export const OffersIndexModal: React.FC = () => {
;
- return {
updateRoute('offers');
}}
@@ -428,5 +428,5 @@ export const OffersIndexModal: React.FC = () => {
{listLayoutOutput}
- ;
+ ;
};
diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal-confirm.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal-confirm.tsx
index ae76b536c08..8ddee4ae9e1 100644
--- a/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal-confirm.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal-confirm.tsx
@@ -6,7 +6,7 @@ import trackEvent from '../../../../utils/analytics';
import {Button} from '@tryghost/shade/components';
import {type EditOrAddRecommendation, useAddRecommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {toast} from 'sonner';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {useRouting} from '@tryghost/admin-x-framework/routing';
@@ -73,7 +73,7 @@ const AddRecommendationModalConfirm: React.FC = ({r
);
- return {
// Closed without saving: reset route
updateRoute('recommendations');
@@ -113,7 +113,7 @@ const AddRecommendationModalConfirm: React.FC = ({r
}}
>
- ;
+ ;
};
export default NiceModal.create(AddRecommendationModalConfirm);
diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal.tsx
index 7939efebc95..f1cd4df437d 100644
--- a/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/recommendations/add-recommendation-modal.tsx
@@ -5,8 +5,8 @@ import {AlreadyExistsError} from '@tryghost/admin-x-framework/errors';
import {type EditOrAddRecommendation, useCheckRecommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {type ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks';
import {Field, FieldDescription, FieldError, FieldGroup, FieldLabel, Input, LoadingIndicator} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {formatUrl} from '../../../../utils/format-url';
import {toast} from 'sonner';
@@ -157,7 +157,7 @@ const AddRecommendationModal: React.FC {
// Closed without saving: reset route
updateRoute('recommendations');
@@ -173,10 +173,10 @@ const AddRecommendationModal: React.FC
- ;
+ ;
}
- return {
// Closed without saving: reset route
updateRoute('recommendations');
@@ -221,7 +221,7 @@ const AddRecommendationModal: React.FC{errors.url} : Need inspiration? Explore thousands of sites to recommend }
- ;
+ ;
};
export default NiceModal.create(AddRecommendationModal);
diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations/edit-recommendation-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations/edit-recommendation-modal.tsx
index 56154d0977d..320d2b2ca1d 100644
--- a/apps/admin-x-settings/src/components/settings/growth/recommendations/edit-recommendation-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/recommendations/edit-recommendation-modal.tsx
@@ -3,9 +3,9 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import RecommendationDescriptionForm, {validateDescriptionForm} from './recommendation-description-form';
import {Button} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type Recommendation, useDeleteRecommendation, useEditRecommendation} from '@tryghost/admin-x-framework/api/recommendations';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {toast} from 'sonner';
import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -60,7 +60,7 @@ const EditRecommendationModal: React.FCDelete
);
- return {
// Closed without saving: reset route
updateRoute('recommendations');
@@ -86,7 +86,7 @@ const EditRecommendationModal: React.FC
- ;
+ ;
};
export default NiceModal.create(EditRecommendationModal);
diff --git a/apps/admin-x-settings/src/components/settings/membership/custom-fields/custom-field-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/custom-fields/custom-field-modal.tsx
index bb8c8da04fa..1aa410becab 100644
--- a/apps/admin-x-settings/src/components/settings/membership/custom-fields/custom-field-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/membership/custom-fields/custom-field-modal.tsx
@@ -4,7 +4,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
import {Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, FieldDescription, FieldError, FieldGroup, FieldLabel, Input, Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@tryghost/shade/components';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {ValidationError, getErrorMessage} from '@tryghost/admin-x-framework/errors';
import {memberCustomFieldUserTypes, useCreateMemberCustomField, useDeleteMemberCustomField, useEditMemberCustomField, userTypeForField} from '@tryghost/admin-x-framework/api/member-custom-fields';
import {toast} from 'sonner';
@@ -180,7 +180,7 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field})
);
return (
- (({field})
{isEdit && Type can’t be changed after creation }
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
index ed0accb5c74..dd668fb02b0 100644
--- a/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx
@@ -6,7 +6,7 @@ import MemberEmailEditor from './member-email-editor';
import WelcomeEmailPreviewFrame from './welcome-email-preview-frame';
import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns';
import {FieldError, Input} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {getWelcomeEmailValidationErrors} from './welcome-email-validation';
import {useBrowseAutomatedEmails, useEditAutomatedEmail, usePreviewWelcomeEmail} from '@tryghost/admin-x-framework/api/automated-emails';
@@ -203,7 +203,7 @@ const WelcomeEmailModal = NiceModal.create(({emailType =
}, [setFormState, updateForm]);
return (
- {
updateRoute('memberemails');
}}
@@ -330,7 +330,7 @@ const WelcomeEmailModal = NiceModal.create(({emailType =
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/membership/stripe/stripe-connect-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/stripe/stripe-connect-modal.tsx
index ef006400c59..de56923381f 100644
--- a/apps/admin-x-settings/src/components/settings/membership/stripe/stripe-connect-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/membership/stripe/stripe-connect-modal.tsx
@@ -13,7 +13,7 @@ import {Button, Field, FieldError, FieldGroup, FieldLabel, Input, Switch, Textar
import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter';
import {JSONError} from '@tryghost/admin-x-framework/errors';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {Text} from '@tryghost/shade/primitives';
import {checkStripeEnabled, getSettingValue, getSettingValues, useDeleteStripeSettings, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {getGhostPaths} from '@tryghost/admin-x-framework/helpers';
@@ -313,7 +313,7 @@ const StripeConnectModal: React.FC = () => {
contents = ;
}
- return {
updateRoute('tiers');
}}
@@ -325,7 +325,7 @@ const StripeConnectModal: React.FC = () => {
hideXOnMobile
>
{contents}
- ;
+ ;
};
export default NiceModal.create(StripeConnectModal);
diff --git a/apps/admin-x-settings/src/components/settings/membership/tiers/tier-detail-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/tiers/tier-detail-modal.tsx
index c092f9f17ee..dc9d34ba454 100644
--- a/apps/admin-x-settings/src/components/settings/membership/tiers/tier-detail-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/membership/tiers/tier-detail-modal.tsx
@@ -9,8 +9,8 @@ import useUrlInput from '../../../../hooks/use-url-input';
import {Button, Combobox, ComboboxContent, ComboboxTrigger, ComboboxValue, Field, FieldDescription, FieldError, FieldGroup, FieldLabel, FieldLegend, FieldSet, Input, InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, MultiSelectCombobox, SortableList, Switch} from '@tryghost/shade/components';
import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {LucideIcon} from '@tryghost/shade/utils';
-import {Modal} from '@tryghost/admin-x-design-system';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {Text} from '@tryghost/shade/primitives';
import {type Tier, useAddTier, useBrowseTiers, useEditTier} from '@tryghost/admin-x-framework/api/tiers';
import {currencies, currencySelectGroups, validateCurrencyAmount} from '../../../../utils/currency';
@@ -179,7 +179,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
}
}
- return {
updateRoute('tiers');
}}
@@ -382,7 +382,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => {
- ;
+ ;
};
const TierDetailModal: React.FC = ({params}) => {
diff --git a/apps/admin-x-settings/src/components/settings/site/navigation-modal.tsx b/apps/admin-x-settings/src/components/settings/site/navigation-modal.tsx
index 6308330e698..efaccec7bc4 100644
--- a/apps/admin-x-settings/src/components/settings/site/navigation-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/navigation-modal.tsx
@@ -2,7 +2,7 @@ import NavigationEditForm from './navigation/navigation-edit-form';
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import useNavigationEditor, {type NavigationItem} from '../../../hooks/site/use-navigation-editor';
import useSettingGroup from '../../../hooks/use-setting-group';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
import {useCallback, useMemo, useState} from 'react';
@@ -45,7 +45,7 @@ const NavigationModal = NiceModal.create(() => {
const [selectedTab, setSelectedTab] = useState('primary-nav');
return (
- {
updateRoute('navigation');
}}
@@ -76,7 +76,7 @@ const NavigationModal = NiceModal.create(() => {
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx b/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
index f002f527ee2..9f22483b80f 100644
--- a/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
@@ -10,8 +10,9 @@ import ThemePreview from './theme/theme-preview';
import {Button, Dropzone, LoadingIndicator, Tabs, TabsList, TabsTrigger} from '@tryghost/shade/components';
import {type InstalledTheme, type Theme, type ThemesInstallResponseType, isDefaultOrLegacyTheme, useActivateTheme, useBrowseThemes, useInstallTheme, useUploadTheme} from '@tryghost/admin-x-framework/api/themes';
import {JSONError} from '@tryghost/admin-x-framework/errors';
-import {Modal, PageHeader} from '@tryghost/admin-x-design-system';
import {type OfficialTheme} from '../../providers/settings-app-provider';
+import {PageHeader} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import {toast} from 'sonner';
import {useCheckThemeLimitError} from '../../../hooks/use-check-theme-limit-error';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -502,7 +503,7 @@ const ChangeThemeModal: React.FC = ({source, themeRef}) =
}
return (
- {
updateRoute('');
}}
@@ -553,7 +554,7 @@ const ChangeThemeModal: React.FC = ({source, themeRef}) =
}
-
+
);
};
diff --git a/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-confirm-modal.tsx b/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-confirm-modal.tsx
index 4e88afd4773..296457de574 100644
--- a/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-confirm-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-confirm-modal.tsx
@@ -1,6 +1,6 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React from 'react';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
import type {ButtonProps} from '@tryghost/shade/components';
export type ThemeEditorConfirmModalProps = {
@@ -26,7 +26,7 @@ const ThemeEditorConfirmModal = NiceModal.create((
};
return (
- ((
{prompt}
-
+
);
});
diff --git a/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-input-modal.tsx b/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-input-modal.tsx
index 07185c570a5..b2467573539 100644
--- a/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-input-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/theme/theme-editor-input-modal.tsx
@@ -1,7 +1,7 @@
import NiceModal, {useModal} from '@ebay/nice-modal-react';
import React, {useState} from 'react';
import {Field, FieldLabel, Input} from '@tryghost/shade/components';
-import {Modal} from '@tryghost/admin-x-design-system';
+import {SettingsModal} from '@tryghost/shade/patterns';
export type ThemeEditorInputModalProps = {
title: string;
@@ -31,7 +31,7 @@ const ThemeEditorInputModal = NiceModal.create(({
};
return (
- (({
setValue(event.target.value)} />
-
+
);
});
diff --git a/apps/admin-x-settings/src/main-content.tsx b/apps/admin-x-settings/src/main-content.tsx
index 503e63e23e9..e738025cfcb 100644
--- a/apps/admin-x-settings/src/main-content.tsx
+++ b/apps/admin-x-settings/src/main-content.tsx
@@ -2,12 +2,11 @@ import ExitSettingsButton from './components/exit-settings-button';
import Settings from './components/settings';
import Sidebar from './components/sidebar';
import Users from './components/settings/general/users';
-import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns';
+import {DirtyConfirmDialog, topLevelBackdropClasses, useDirtyConfirmation} from '@tryghost/shade/patterns';
import {type ReactNode, useEffect} from 'react';
import {Text} from '@tryghost/shade/primitives';
import {canAccessSettings, isEditorUser} from '@tryghost/admin-x-framework/api/users';
import {toast} from 'sonner';
-import {topLevelBackdropClasses} from '@tryghost/admin-x-design-system';
import {useGlobalData} from './components/providers/global-data-provider';
import {useGlobalDirtyState} from '@tryghost/shade/utils';
import {useRouting} from '@tryghost/admin-x-framework/routing';
diff --git a/apps/shade/package.json b/apps/shade/package.json
index 06c981ff5ca..10a3f2de808 100644
--- a/apps/shade/package.json
+++ b/apps/shade/package.json
@@ -108,6 +108,7 @@
"vitest": "catalog:"
},
"dependencies": {
+ "@ebay/nice-modal-react": "catalog:",
"@dnd-kit/core": "catalog:",
"@dnd-kit/sortable": "catalog:",
"@dnd-kit/utilities": "catalog:",
diff --git a/apps/shade/src/components/patterns/settings-modal.stories.tsx b/apps/shade/src/components/patterns/settings-modal.stories.tsx
new file mode 100644
index 00000000000..0c322108e01
--- /dev/null
+++ b/apps/shade/src/components/patterns/settings-modal.stories.tsx
@@ -0,0 +1,158 @@
+import NiceModal from '@ebay/nice-modal-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(() => );
+
+ return (
+
+ NiceModal.show(StoryModal)}>Open modal
+
+ );
+};
+
+const meta = {
+ title: 'Patterns / Settings Modal',
+ component: SettingsModalStory,
+ tags: ['autodocs'],
+ parameters: {
+ docs: {
+ description: {
+ component: 'Transitional compatibility shell for the existing settings NiceModal flows. New modal flows should use Shade Dialog primitives directly.'
+ }
+ }
+ },
+ decorators: [Story => (
+
+
+
+ )]
+} satisfies Meta;
+
+export default meta;
+type Story = StoryObj;
+
+export const Default: Story = {
+ args: {
+ title: 'Modal dialog',
+ children: Modal content ,
+ onOk: () => undefined
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Default modal for a focused settings task with confirm and cancel actions.'
+ }
+ }
+ }
+};
+
+export const Small: Story = {
+ args: {
+ title: 'Small modal',
+ size: 'sm',
+ children: Compact modal content ,
+ onOk: () => undefined
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Small modal for concise forms and lightweight actions.'
+ }
+ }
+ }
+};
+
+export const Full: Story = {
+ args: {
+ title: 'Full modal',
+ size: 'full',
+ children: Full-height modal content ,
+ onOk: () => undefined
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Full-height modal for complex settings editors.'
+ }
+ }
+ }
+};
+
+export const RightDrawer: Story = {
+ args: {
+ 'aria-label': 'Drawer settings',
+ size: 'bleed',
+ align: 'right',
+ width: 600,
+ animate: false,
+ footer: false,
+ children: Right-aligned drawer content
+ },
+ parameters: {
+ docs: {
+ description: {
+ story: 'Bleed variant aligned to the right for drawer-style editors.'
+ }
+ }
+ }
+};
+
+export const Dirty: Story = {
+ args: {
+ title: 'Unsaved settings',
+ dirty: true,
+ children: Close or cancel to see the dirty-state confirmation. ,
+ onOk: () => undefined
+ }
+};
+
+export const StickyChrome: Story = {
+ args: {
+ title: 'Scrollable settings',
+ height: 420,
+ stickyHeader: true,
+ stickyFooter: true,
+ children: Scroll to verify the header and footer remain visible. ,
+ onOk: () => undefined
+ }
+};
+
+export const FormSheet: Story = {
+ args: {
+ title: 'Form sheet',
+ formSheet: true,
+ children: Form-sheet backdrop and elevation. ,
+ onOk: () => undefined
+ }
+};
+
+export const CustomActions: Story = {
+ args: {
+ title: 'Custom actions',
+ leftButton: Archive ,
+ okLabel: 'Save',
+ okLoading: true,
+ cancelLabel: 'Close',
+ children: Loading, disabled, and destructive action states. ,
+ onOk: () => undefined
+ }
+};
+
+export const BackgroundInteraction: Story = {
+ args: {
+ 'aria-label': 'Interactive settings drawer',
+ size: 'bleed',
+ align: 'right',
+ width: 600,
+ allowBackgroundInteraction: true,
+ backDrop: false,
+ backDropClick: false,
+ footer: false,
+ children: The page behind this drawer remains interactive.
+ }
+};
diff --git a/apps/shade/src/components/patterns/settings-modal.tsx b/apps/shade/src/components/patterns/settings-modal.tsx
new file mode 100644
index 00000000000..a4d1ee9a871
--- /dev/null
+++ b/apps/shade/src/components/patterns/settings-modal.tsx
@@ -0,0 +1,351 @@
+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';
+
+import {Button, type ButtonProps} from '@/components/ui/button';
+import {LoadingIndicator} from '@/components/ui/loading-indicator';
+import {StickyFooter} from '@/components/ui/sticky-footer';
+import {Box} from '@/components/primitives/box';
+import {Inline} from '@/components/primitives/inline';
+import {Text} from '@/components/primitives/text';
+import {DirtyConfirmDialog, useDirtyConfirmation} from '@/components/patterns/dirty-confirm-dialog';
+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.
+ */
+export type SettingsModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'bleed';
+
+export interface SettingsModalProps {
+ 'aria-label'?: string;
+ className?: string;
+ size?: SettingsModalSize;
+ width?: 'full' | 'toSidebar' | number;
+ height?: 'full' | number;
+ align?: 'center' | 'left' | 'right';
+ testId?: string;
+ title?: React.ReactNode;
+ okLabel?: string;
+ okVariant?: ButtonProps['variant'];
+ okLoading?: boolean;
+ cancelLabel?: string;
+ leftButton?: React.ReactNode;
+ buttonsDisabled?: boolean;
+ okDisabled?: boolean;
+ footer?: boolean | React.ReactNode;
+ header?: boolean;
+ padding?: boolean;
+ onOk?: () => void;
+ onCancel?: () => void;
+ topRightContent?: 'close' | React.ReactNode;
+ hideXOnMobile?: boolean;
+ afterClose?: () => void;
+ children?: React.ReactNode;
+ backDrop?: boolean;
+ backDropClick?: boolean;
+ stickyFooter?: boolean;
+ stickyHeader?: boolean;
+ scrolling?: boolean;
+ dirty?: boolean;
+ animate?: boolean;
+ formSheet?: boolean;
+ enableCMDS?: boolean;
+ allowBackgroundInteraction?: boolean;
+}
+
+export const topLevelBackdropClasses = 'bg-modal-backdrop backdrop-blur-[3px]';
+
+const settingsModalVariants = cva(
+ 'relative z-50 flex max-h-full w-full flex-col justify-between overflow-x-hidden bg-background text-foreground',
+ {
+ variants: {
+ size: {
+ sm: 'max-w-[480px] rounded',
+ md: 'max-w-[720px] rounded',
+ lg: 'max-w-[1020px] rounded',
+ xl: 'max-w-[1240px] rounded',
+ full: 'h-full rounded',
+ bleed: 'h-full'
+ },
+ align: {
+ center: 'mx-auto',
+ left: 'mr-auto',
+ right: 'ml-auto'
+ },
+ scrolling: {
+ true: 'overflow-y-auto',
+ false: 'overflow-y-hidden'
+ },
+ formSheet: {
+ true: 'shadow-md',
+ false: 'shadow-xl'
+ }
+ },
+ defaultVariants: {
+ size: 'md',
+ align: 'center',
+ scrolling: true,
+ formSheet: false
+ }
+ }
+);
+
+const backdropPadding: Record = {
+ sm: 'p-4 md:p-[8vmin]',
+ md: 'p-4 md:p-[8vmin]',
+ lg: 'p-4 md:p-[4vmin]',
+ xl: 'p-4 md:p-[3vmin]',
+ full: 'p-4 md:p-[3vmin]',
+ bleed: ''
+};
+
+const contentPadding: Record = {
+ sm: 'p-8',
+ md: 'p-8',
+ lg: 'p-7',
+ xl: 'p-10',
+ full: 'p-10',
+ bleed: 'p-10'
+};
+
+const headerOffsets: Record = {
+ sm: '-inset-x-8',
+ md: '-inset-x-8',
+ lg: '-inset-x-8',
+ xl: '-inset-x-10 -top-10',
+ full: '-inset-x-10',
+ bleed: '-inset-x-10'
+};
+
+const SettingsModal = forwardRef(({
+ 'aria-label': ariaLabel,
+ className,
+ size = 'md',
+ align = 'center',
+ width,
+ height,
+ testId,
+ title,
+ okLabel = 'OK',
+ okLoading = false,
+ cancelLabel = 'Cancel',
+ footer,
+ header,
+ leftButton,
+ buttonsDisabled,
+ okDisabled,
+ padding = true,
+ onOk,
+ okVariant = 'default',
+ onCancel,
+ topRightContent,
+ hideXOnMobile = false,
+ afterClose,
+ children,
+ backDrop = true,
+ backDropClick = true,
+ stickyFooter = false,
+ stickyHeader = false,
+ scrolling = true,
+ dirty = false,
+ animate = true,
+ formSheet = false,
+ enableCMDS = true,
+ allowBackgroundInteraction = false
+}, ref) => {
+ const modal = useModal();
+ const {setGlobalDirtyState} = useGlobalDirtyState();
+ const {confirm, dialogProps} = useDirtyConfirmation();
+ const [animationFinished, setAnimationFinished] = useState(false);
+ useEffect(() => {
+ setGlobalDirtyState(dirty);
+ }, [dirty, setGlobalDirtyState]);
+
+ const removeModal = () => {
+ confirm(dirty, () => {
+ modal.remove();
+ afterClose?.();
+ });
+ };
+
+ useEffect(() => {
+ const handleEscapeKey = (event: KeyboardEvent) => {
+ if (event.key !== 'Escape') {
+ return;
+ }
+
+ const activeElement = document.activeElement;
+ if (activeElement?.hasAttribute('data-kg-link-input')) {
+ return;
+ }
+
+ if (activeElement instanceof HTMLElement) {
+ activeElement.blur();
+ }
+
+ setTimeout(() => {
+ if (onCancel) {
+ onCancel();
+ } else {
+ removeModal();
+ }
+ });
+
+ event.stopPropagation();
+ };
+
+ document.addEventListener('keydown', handleEscapeKey);
+ return () => document.removeEventListener('keydown', handleEscapeKey);
+ });
+
+ useEffect(() => {
+ const timeout = setTimeout(() => setAnimationFinished(true), 250);
+ return () => clearTimeout(timeout);
+ }, []);
+
+ useEffect(() => {
+ if (!onOk || !enableCMDS) {
+ return;
+ }
+
+ const handleCMDS = (event: KeyboardEvent) => {
+ if ((event.metaKey || event.ctrlKey) && event.key === 's') {
+ event.preventDefault();
+ onOk();
+ }
+ };
+
+ window.addEventListener('keydown', handleCMDS);
+ return () => window.removeEventListener('keydown', handleCMDS);
+ }, [enableCMDS, onOk]);
+
+ const paddingClasses = padding ? contentPadding[size] : 'p-0';
+ const modalClasses = cn(
+ settingsModalVariants({size, align, scrolling, formSheet}),
+ className,
+ animate && !formSheet && !animationFinished && align === 'center' && 'animate-modal-in',
+ animate && !formSheet && !animationFinished && align === 'right' && 'animate-modal-in-from-right',
+ formSheet && !animationFinished && 'animate-modal-in-reverse',
+ width === 'full' && 'w-full',
+ width === 'toSidebar' && 'w-full max-w-[calc(100dvw_-_280px)] lg:max-w-full min-[1280px]:max-w-[calc(100dvw_-_320px)]',
+ height === 'full' && 'h-full',
+ allowBackgroundInteraction && 'pointer-events-auto'
+ );
+ const backdropClasses = cn(
+ 'fixed inset-0 z-[1000] h-[100dvh] w-[100dvw] max-[800px]:pb-20',
+ backdropPadding[size],
+ allowBackgroundInteraction && 'pointer-events-none'
+ );
+ const headerClasses = cn(
+ paddingClasses,
+ 'pb-0',
+ headerOffsets[size],
+ topRightContent && topRightContent !== 'close' && 'flex items-center justify-between gap-5',
+ stickyHeader && 'sticky -top-px z-[300] -mb-4 bg-background pb-4'
+ );
+ const contentClasses = cn(
+ paddingClasses,
+ 'py-0',
+ (size === 'full' || size === 'bleed' || height === 'full' || typeof height === 'number') && 'grow',
+ footer === false && 'pb-0'
+ );
+ const footerClasses = cn(
+ paddingClasses,
+ 'flex w-full items-center justify-between',
+ stickyFooter && 'py-6'
+ );
+ const modalStyles: React.CSSProperties = {
+ ...(typeof width === 'number' ? {width: '100%', maxWidth: `${width}px`} : {}),
+ ...(typeof height === 'number' ? {height: '100%', maxHeight: `${height}px`} : {})
+ };
+
+ const handleBackdropClick = (event: React.MouseEvent) => {
+ if (event.target === event.currentTarget && backDropClick) {
+ removeModal();
+ }
+ };
+
+ let footerContent: React.ReactNode;
+ if (footer) {
+ footerContent = footer;
+ } else if (footer !== false) {
+ footerContent = (
+
+ {leftButton}
+
+ {cancelLabel && (
+
+ {cancelLabel}
+
+ )}
+ {okLabel && (
+
+ {okLoading && }
+ {okLabel}
+
+ )}
+
+
+ );
+ }
+
+ if (stickyFooter) {
+ footerContent = {footerContent} ;
+ }
+
+ const titleContent = title && (
+
+ {title}
+
+ );
+ const headerContent = topRightContent && topRightContent !== 'close' ? (
+
+ {titleContent}
+ {topRightContent}
+
+ ) : (
+
+ {titleContent}
+
+
+
+
+
+
+ );
+
+ return (
+ <>
+
+
+
+ {header !== false && headerContent}
+ {children}
+ {footerContent}
+
+
+
+ >
+ );
+});
+
+SettingsModal.displayName = 'SettingsModal';
+
+export {SettingsModal, settingsModalVariants};
diff --git a/apps/shade/src/docs/adoption-data.json b/apps/shade/src/docs/adoption-data.json
index d3af438df1a..4130fe38fbe 100644
--- a/apps/shade/src/docs/adoption-data.json
+++ b/apps/shade/src/docs/adoption-data.json
@@ -1,15 +1,15 @@
{
"snapshot": {
- "generatedAt": "2026-07-23T16:03:50.691Z",
- "sha": "cadcb2fb49b0ea46452b404422b57413ab3c2608",
- "branch": "codex/admin-settings-shade-confirmation"
+ "generatedAt": "2026-07-23T21:16:04.661Z",
+ "sha": "526cf1eae7992ea02ea8e7c0e1fa93867f7ea7e2",
+ "branch": "codex/admin-settings-shade-modal"
},
"summary": {
- "adminReactFilesTotal": 705,
- "adminReactFilesUsingShade": 375,
- "shadeAdoptionPct": 53.2,
- "uniqueShadeComponentsUsed": 295,
- "adminXDsComponentsStillUsed": 19
+ "adminReactFilesTotal": 706,
+ "adminReactFilesUsingShade": 376,
+ "shadeAdoptionPct": 53.3,
+ "uniqueShadeComponentsUsed": 299,
+ "adminXDsComponentsStillUsed": 13
},
"apps": [
{
@@ -1200,9 +1200,9 @@
},
{
"name": "admin-x-settings",
- "files": 252,
- "shadeFiles": 148,
- "adminXDsFiles": 55,
+ "files": 253,
+ "shadeFiles": 149,
+ "adminXDsFiles": 15,
"shadeComponents": [
{
"name": "Button",
@@ -1228,6 +1228,10 @@
"name": "Input",
"count": 35
},
+ {
+ "name": "SettingsModal",
+ "count": 34
+ },
{
"name": "FieldError",
"count": 31
@@ -1480,6 +1484,14 @@
"name": "useFocusContext",
"count": 3
},
+ {
+ "name": "PreviewChrome",
+ "count": 3
+ },
+ {
+ "name": "ModalPage",
+ "count": 3
+ },
{
"name": "SortableList",
"count": 3
@@ -1600,6 +1612,10 @@
"name": "GhostOrb",
"count": 2
},
+ {
+ "name": "topLevelBackdropClasses",
+ "count": 1
+ },
{
"name": "SettingGroupActions",
"count": 1
@@ -1754,30 +1770,10 @@
}
],
"adminXDsComponents": [
- {
- "name": "Modal",
- "count": 34
- },
- {
- "name": "LimitModal",
- "count": 10
- },
{
"name": "PreviewModalContent",
"count": 7
},
- {
- "name": "DesktopChrome",
- "count": 3
- },
- {
- "name": "MobileChrome",
- "count": 3
- },
- {
- "name": "ModalPage",
- "count": 3
- },
{
"name": "useDesignSystem",
"count": 2
@@ -1786,10 +1782,6 @@
"name": "PageHeader",
"count": 2
},
- {
- "name": "topLevelBackdropClasses",
- "count": 1
- },
{
"name": "DesignSystemApp",
"count": 1
@@ -1874,6 +1866,10 @@
"name": "FieldError",
"count": 34
},
+ {
+ "name": "SettingsModal",
+ "count": 34
+ },
{
"name": "Tabs",
"count": 31
@@ -1909,37 +1905,13 @@
{
"name": "Skeleton",
"count": 26
- },
- {
- "name": "Select",
- "count": 26
}
],
"adminXDsComponentsAggregate": [
- {
- "name": "Modal",
- "count": 34
- },
- {
- "name": "LimitModal",
- "count": 10
- },
{
"name": "PreviewModalContent",
"count": 7
},
- {
- "name": "DesktopChrome",
- "count": 3
- },
- {
- "name": "MobileChrome",
- "count": 3
- },
- {
- "name": "ModalPage",
- "count": 3
- },
{
"name": "useDesignSystem",
"count": 2
@@ -1948,10 +1920,6 @@
"name": "PageHeader",
"count": 2
},
- {
- "name": "topLevelBackdropClasses",
- "count": 1
- },
{
"name": "DesignSystemApp",
"count": 1
diff --git a/apps/shade/src/patterns.ts b/apps/shade/src/patterns.ts
index b132197373a..bece536325c 100644
--- a/apps/shade/src/patterns.ts
+++ b/apps/shade/src/patterns.ts
@@ -14,6 +14,8 @@ export {ColorPickerTrigger, ColorSwatch, ColorSwatchRow} from './components/patt
export type {ColorPickerProps, ColorPickerTriggerProps, ColorSwatchOption, ColorSwatchProps, ColorSwatchRowProps} from './components/patterns/color-picker';
export {default as ShareModal} from './components/patterns/share-modal';
export type {ShareModalPreviewProps, ShareModalSocialLink} from './components/patterns/share-modal';
+export {SettingsModal, settingsModalVariants, topLevelBackdropClasses} from './components/patterns/settings-modal';
+export type {SettingsModalProps, SettingsModalSize} from './components/patterns/settings-modal';
export * from './components/patterns/table-filter-tabs';
export * from './components/patterns/utm-campaign-tabs';
export type {CampaignType, TabType} from './components/patterns/utm-campaign-tabs';
diff --git a/apps/shade/tailwind.theme.css b/apps/shade/tailwind.theme.css
index 74a066f3c30..537c450ad01 100644
--- a/apps/shade/tailwind.theme.css
+++ b/apps/shade/tailwind.theme.css
@@ -168,6 +168,8 @@
--color-surface-panel: var(--surface-panel);
--color-surface-elevated: var(--surface-elevated);
--color-surface-elevated-2: var(--surface-elevated-2);
+ --color-modal-backdrop: var(--modal-backdrop);
+ --color-form-sheet-backdrop: var(--form-sheet-backdrop);
--color-surface-inverse: var(--surface-inverse);
--color-surface-inverse-foreground: var(--surface-inverse-foreground);
--color-text-primary: var(--text-primary);
diff --git a/apps/shade/theme-variables.css b/apps/shade/theme-variables.css
index cfa218842d8..e5dca02b861 100644
--- a/apps/shade/theme-variables.css
+++ b/apps/shade/theme-variables.css
@@ -27,6 +27,8 @@
--surface-elevated-2: var(--color-white);
--surface-overlay: var(--color-white);
--surface-overlay-foreground: var(--color-black);
+ --modal-backdrop: rgb(98 109 121 / 0.2);
+ --form-sheet-backdrop: rgb(98 109 121 / 0.08);
--surface-inverse: var(--color-black);
--surface-inverse-foreground: var(--color-white);
--text-primary: var(--color-black);
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index f5692cdd3ba..528a92ce8aa 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -1829,6 +1829,9 @@ 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))
From fe3185cd4594087a6679d7a7939308f7129f5007 Mon Sep 17 00:00:00 2001
From: Steve Larson <9larsons@gmail.com>
Date: Thu, 23 Jul 2026 18:44:28 -0500
Subject: [PATCH 2/6] Moved settings preview modals out of the legacy design
system (#29569)
no ref
- moved the settings-only `PreviewModalContent` compatibility shell from
`admin-x-design-system` into `admin-x-settings`
- switched Portal, Design, Announcement bar, Newsletter, and offer flows
to the local shell
- rebuilt its layout from Shade primitives and `SettingsModal` while
preserving the existing NiceModal contract
- added semantic preview-canvas tokens so the light and dark preview
surfaces retain their current appearance without raw colour or `dark:`
utilities
- removed the legacy export and story, reducing Settings legacy
design-system usage from 13 components to 12
---
.../global/modal/preview-modal.stories.tsx | 96 ---------------
apps/admin-x-design-system/src/index.ts | 3 -
.../newsletters/newsletter-detail-modal.tsx | 2 +-
.../growth/offers/add-offer-modal.tsx | 2 +-
.../growth/offers/edit-offer-modal.tsx | 2 +-
.../offers/edit-retention-offer-modal.tsx | 2 +-
.../membership/portal/portal-modal.tsx | 2 +-
.../components/settings}/preview-modal.tsx | 82 +++++++------
.../settings/site/announcement-bar-modal.tsx | 2 +-
.../components/settings/site/design-modal.tsx | 2 +-
apps/shade/src/docs/adoption-data.json | 112 ++++++++++--------
apps/shade/tailwind.theme.css | 3 +
apps/shade/theme-variables.css | 6 +
13 files changed, 122 insertions(+), 194 deletions(-)
delete mode 100644 apps/admin-x-design-system/src/global/modal/preview-modal.stories.tsx
rename apps/{admin-x-design-system/src/global/modal => admin-x-settings/src/components/settings}/preview-modal.tsx (70%)
diff --git a/apps/admin-x-design-system/src/global/modal/preview-modal.stories.tsx b/apps/admin-x-design-system/src/global/modal/preview-modal.stories.tsx
deleted file mode 100644
index 2680ea0a9db..00000000000
--- a/apps/admin-x-design-system/src/global/modal/preview-modal.stories.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import type {Meta, StoryContext, StoryObj} from '@storybook/react-vite';
-import {ReactNode} from 'react';
-
-import NiceModal from '@ebay/nice-modal-react';
-import PreviewModal, {PreviewModalProps} from './preview-modal';
-import {Text} from '@tryghost/shade/primitives';
-
-const PreviewModalContainer: React.FC = ({...props}) => {
- return (
- {
- NiceModal.show(PreviewModal, {...props});
- }}>Open preview modal
- );
-};
-
-const meta = {
- title: 'Global / Modal / Preview Modal',
- component: PreviewModal,
- tags: ['autodocs'],
- decorators: [(_story: () => ReactNode, context: StoryContext) => (
-
-
-
- )],
- argTypes: {
- sidebar: {control: 'text'},
- preview: {control: 'text'},
- sidebarButtons: {control: 'text'},
- sidebarHeader: {control: 'text'}
- }
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- title: 'Preview modal',
- preview: (
-
- Scrollable preview area
-
- ),
- sidebar: (
-
- Scrollable sidebar area
-
- ),
- previewToolbarTabs: Preview tabs slot ,
- deviceSelector: Device selector slot
- }
-};
-
-export const NoPreviewToolbar: Story = {
- args: {
- ...Default.args,
- previewToolbar: false
- }
-};
-
-export const CustomButtons: Story = {
- args: {
- ...Default.args,
- cancelLabel: 'Meh',
- okLabel: 'Alrite',
- okVariant: 'default'
- }
-};
-
-export const CustomSidebarHeader: Story = {
- args: {
- ...Default.args,
- sidebarHeader: (
-
- A custom header here
-
- )
- }
-};
-
-export const FullBleed: Story = {
- args: {
- ...Default.args,
- size: 'bleed'
- }
-};
-
-export const BreadcrumbsToolbar: Story = {
- args: {
- ...Default.args,
- previewToolbarTabs: undefined,
- previewToolbarBreadcrumbs: (
- Toolbar breadcrumbs slot
- )
- }
-};
diff --git a/apps/admin-x-design-system/src/index.ts b/apps/admin-x-design-system/src/index.ts
index 21e659a4e4d..aa0b3513382 100644
--- a/apps/admin-x-design-system/src/index.ts
+++ b/apps/admin-x-design-system/src/index.ts
@@ -6,9 +6,6 @@ export {default as XLogo} from './assets/images/x-logo.svg?react';
export {default as DesktopChromeHeader} from './global/chrome/desktop-chrome-header';
export type {DesktopChromeHeaderProps} from './global/chrome/desktop-chrome-header';
-export {default as PreviewModal, PreviewModalContent} from './global/modal/preview-modal';
-export type {PreviewModalProps} from './global/modal/preview-modal';
-
export {default as Banner} from './global/banner';
export type {BannerProps} from './global/banner';
export {default as ErrorBoundary} from './global/error-boundary';
diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-detail-modal.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-detail-modal.tsx
index 8a3011f6b27..3f5e8edee26 100644
--- a/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-detail-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/email/newsletters/newsletter-detail-modal.tsx
@@ -14,7 +14,7 @@ import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter';
import {ImageUpload, ImageUploadAction, ImageUploadActions, ImageUploadDropzone, ImageUploadImage, ImageUploadPreview} from '@tryghost/shade/patterns';
import {LucideIcon, formatNumber} from '@tryghost/shade/utils';
import {type Newsletter, useBrowseNewsletters, useEditNewsletter} from '@tryghost/admin-x-framework/api/newsletters';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../../preview-modal';
import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing';
import {Stack, Text} from '@tryghost/shade/primitives';
import {Trash2} from 'lucide-react';
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/add-offer-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/add-offer-modal.tsx
index 345349c1289..3a9ae2dff61 100644
--- a/apps/admin-x-settings/src/components/settings/growth/offers/add-offer-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/offers/add-offer-modal.tsx
@@ -2,7 +2,7 @@ import PortalFrame from '../../membership/portal/portal-frame';
import {Button, Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, Input, InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components';
import {type ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks';
import {JSONError} from '@tryghost/admin-x-framework/errors';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../../preview-modal';
import {formatNumber} from '@tryghost/shade/utils';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
import {getOfferPortalPreviewUrl, type offerPortalPreviewUrlTypes} from '../../../../utils/get-offers-portal-preview-url';
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/edit-offer-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/edit-offer-modal.tsx
index e32eea9eeff..615321fa7e3 100644
--- a/apps/admin-x-settings/src/components/settings/growth/offers/edit-offer-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/offers/edit-offer-modal.tsx
@@ -6,7 +6,7 @@ import {Button, Field, FieldDescription, FieldError, FieldGroup, FieldLabel, Inp
import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
import {JSONError} from '@tryghost/admin-x-framework/errors';
import {type Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../../preview-modal';
import {createOfferRedemptionFilterUrl} from './offer-helpers';
import {formatNumber} from '@tryghost/shade/utils';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers/edit-retention-offer-modal.tsx b/apps/admin-x-settings/src/components/settings/growth/offers/edit-retention-offer-modal.tsx
index f07fc0cf8c5..5a0579aa70b 100644
--- a/apps/admin-x-settings/src/components/settings/growth/offers/edit-retention-offer-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/growth/offers/edit-retention-offer-modal.tsx
@@ -4,7 +4,7 @@ import {type ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks';
import {Field, FieldContent, FieldDescription, FieldError, FieldGroup, FieldLabel, Input, InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea} from '@tryghost/shade/components';
import {JSONError} from '@tryghost/admin-x-framework/errors';
import {type Offer, useAddOffer, useBrowseOffers, useEditOffer, useInvalidateOffers} from '@tryghost/admin-x-framework/api/offers';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../../preview-modal';
import {createOfferRedemptionsFilterUrl, formatOfferTimestamp, generateRetentionOfferName} from './offer-helpers';
import {formatNumber} from '@tryghost/shade/utils';
import {getOfferPortalPreviewUrl, type offerPortalPreviewUrlTypes} from '../../../../utils/get-offers-portal-preview-url';
diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/portal-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/portal-modal.tsx
index 721773d4023..1a621654971 100644
--- a/apps/admin-x-settings/src/components/settings/membership/portal/portal-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/membership/portal/portal-modal.tsx
@@ -7,7 +7,7 @@ import React, {useEffect, useState} from 'react';
import SignupOptions from './signup-options';
import useQueryParams from '../../../../hooks/use-query-params';
import {type Dirtyable, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../../preview-modal';
import {type Setting, type SettingValue, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components';
import {type Tier, useBrowseTiers, useEditTier} from '@tryghost/admin-x-framework/api/tiers';
diff --git a/apps/admin-x-design-system/src/global/modal/preview-modal.tsx b/apps/admin-x-settings/src/components/settings/preview-modal.tsx
similarity index 70%
rename from apps/admin-x-design-system/src/global/modal/preview-modal.tsx
rename to apps/admin-x-settings/src/components/settings/preview-modal.tsx
index 642480dc2eb..409d4605ee0 100644
--- a/apps/admin-x-design-system/src/global/modal/preview-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/preview-modal.tsx
@@ -1,11 +1,11 @@
-import NiceModal, {useModal} from '@ebay/nice-modal-react';
-import clsx from 'clsx';
import React, {useEffect} from 'react';
-import {SettingsModal, type SettingsModalSize} from '@tryghost/shade/patterns';
+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} from '@tryghost/shade/components';
-import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns';
-import {Inline, Text, type TextElement, type TextLeading, type TextSize} from '@tryghost/shade/primitives';
-import {LucideIcon, useGlobalDirtyState} from '@tryghost/shade/utils';
+import {DirtyConfirmDialog, SettingsModal, type SettingsModalSize, useDirtyConfirmation} from '@tryghost/shade/patterns';
+import {cn, useGlobalDirtyState} from '@tryghost/shade/utils';
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
@@ -36,6 +36,10 @@ const headingLeading: Record = {
6: 'body'
};
+/**
+ * Compatibility shell for settings preview modals while the legacy NiceModal
+ * flows are migrated to consumer-controlled Shade compositions.
+ */
export interface PreviewModalProps {
testId?: string;
title?: string;
@@ -45,11 +49,11 @@ export interface PreviewModalProps {
height?: 'full' | number;
sidebar?: boolean | React.ReactNode;
preview?: React.ReactNode;
- dirty?: boolean
+ dirty?: boolean;
cancelLabel?: string;
okLabel?: string;
okVariant?: ButtonProps['variant'];
- buttonsDisabled?: boolean
+ buttonsDisabled?: boolean;
previewToolbar?: boolean;
leftToolbar?: boolean;
rightToolbar?: boolean;
@@ -142,12 +146,12 @@ export const PreviewModalContent: React.FC = ({
let previewBgClass = '';
if (previewBgColor === 'grey') {
- previewBgClass = 'bg-grey-50 dark:bg-black';
+ previewBgClass = 'bg-preview-canvas';
} else if (previewBgColor === 'greygradient') {
- previewBgClass = 'bg-gradient-to-tr from-white to-[#f9f9fa] dark:from-grey-950 dark:to-black';
+ previewBgClass = 'bg-gradient-to-tr from-preview-gradient-start to-preview-gradient-end';
}
- const containerClasses = clsx(
+ const containerClasses = cn(
'absolute inset-y-0 right-[400px] left-0 flex w-full min-w-100 grow flex-col overflow-y-auto',
previewBgClass
);
@@ -155,27 +159,27 @@ export const PreviewModalContent: React.FC = ({
let viewSiteButton;
if (siteLink) {
viewSiteButton = (
-
+
+ View site
+
);
}
preview = (
-
- {previewToolbar &&
- {leftToolbar &&
+
+ {previewToolbar &&
+ {leftToolbar &&
{toolbarLeft}
-
}
- {rightToolbar &&
+ }
+ {rightToolbar &&
{deviceSelector}
{viewSiteButton}
-
}
- }
-
+ }
+ }
+
{preview}
-
-
+
+
);
}
@@ -201,14 +205,17 @@ export const PreviewModalContent: React.FC = ({
width={width}
hideXOnMobile
>
-
-
+
+
{preview}
-
+
{sidebar &&
-
+
{sidebarHeader ? sidebarHeader : (
-
+
= ({
{okLabel}
)}
-
+
)}
-
+
{sidebar}
-
-
+
+
}
-
+
);
};
-
-export default NiceModal.create(PreviewModalContent);
diff --git a/apps/admin-x-settings/src/components/settings/site/announcement-bar-modal.tsx b/apps/admin-x-settings/src/components/settings/site/announcement-bar-modal.tsx
index f0598d1dac8..bfcf7229309 100644
--- a/apps/admin-x-settings/src/components/settings/site/announcement-bar-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/announcement-bar-modal.tsx
@@ -6,7 +6,7 @@ import React, {useRef, useState} from 'react';
import useSettingGroup from '../../../hooks/use-setting-group';
import {Checkbox, Field, FieldGroup, FieldLabel, FieldLegend, FieldSet, PreviewChrome, Tabs, TabsList, TabsTrigger, ToggleGroup, ToggleGroupItem} from '@tryghost/shade/components';
import {Laptop, Smartphone} from 'lucide-react';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../preview-modal';
import {debounce} from '../../../utils/debounce';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
import {getSettingValues} from '@tryghost/admin-x-framework/api/settings';
diff --git a/apps/admin-x-settings/src/components/settings/site/design-modal.tsx b/apps/admin-x-settings/src/components/settings/site/design-modal.tsx
index 411130567d7..d4e073851a5 100644
--- a/apps/admin-x-settings/src/components/settings/site/design-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/design-modal.tsx
@@ -6,7 +6,7 @@ import useQueryParams from '../../../hooks/use-query-params';
import {type CustomThemeSetting, useBrowseCustomThemeSettings, useEditCustomThemeSettings} from '@tryghost/admin-x-framework/api/custom-theme-settings';
import {Laptop, Smartphone} from 'lucide-react';
import {PreviewChrome, Tabs, TabsContent, TabsList, TabsTrigger, ToggleGroup, ToggleGroupItem} from '@tryghost/shade/components';
-import {PreviewModalContent} from '@tryghost/admin-x-design-system';
+import {PreviewModalContent} from '../preview-modal';
import {type Setting, type SettingValue, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings';
import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site';
import {useBrowsePosts} from '@tryghost/admin-x-framework/api/posts';
diff --git a/apps/shade/src/docs/adoption-data.json b/apps/shade/src/docs/adoption-data.json
index 4130fe38fbe..e86915b4843 100644
--- a/apps/shade/src/docs/adoption-data.json
+++ b/apps/shade/src/docs/adoption-data.json
@@ -1,15 +1,15 @@
{
"snapshot": {
- "generatedAt": "2026-07-23T21:16:04.661Z",
- "sha": "526cf1eae7992ea02ea8e7c0e1fa93867f7ea7e2",
- "branch": "codex/admin-settings-shade-modal"
+ "generatedAt": "2026-07-23T23:01:47.383Z",
+ "sha": "4cb987e33fe40fad85492202d0b5e96f0d131509",
+ "branch": "codex/admin-settings-shade-preview-modal"
},
"summary": {
- "adminReactFilesTotal": 706,
- "adminReactFilesUsingShade": 376,
+ "adminReactFilesTotal": 707,
+ "adminReactFilesUsingShade": 377,
"shadeAdoptionPct": 53.3,
- "uniqueShadeComponentsUsed": 299,
- "adminXDsComponentsStillUsed": 13
+ "uniqueShadeComponentsUsed": 303,
+ "adminXDsComponentsStillUsed": 12
},
"apps": [
{
@@ -1200,13 +1200,13 @@
},
{
"name": "admin-x-settings",
- "files": 253,
- "shadeFiles": 149,
- "adminXDsFiles": 15,
+ "files": 254,
+ "shadeFiles": 150,
+ "adminXDsFiles": 8,
"shadeComponents": [
{
"name": "Button",
- "count": 70
+ "count": 71
},
{
"name": "FieldLabel",
@@ -1225,12 +1225,12 @@
"count": 36
},
{
- "name": "Input",
+ "name": "SettingsModal",
"count": 35
},
{
- "name": "SettingsModal",
- "count": 34
+ "name": "Input",
+ "count": 35
},
{
"name": "FieldError",
@@ -1286,15 +1286,15 @@
},
{
"name": "Text",
- "count": 17
+ "count": 18
},
{
- "name": "TabsContent",
+ "name": "Inline",
"count": 17
},
{
- "name": "Inline",
- "count": 16
+ "name": "TabsContent",
+ "count": 17
},
{
"name": "ActionList",
@@ -1340,6 +1340,10 @@
"name": "ToggleGroupItem",
"count": 9
},
+ {
+ "name": "cn",
+ "count": 8
+ },
{
"name": "ImageUpload",
"count": 8
@@ -1389,12 +1393,16 @@
"count": 7
},
{
- "name": "cn",
+ "name": "Stack",
"count": 7
},
{
- "name": "Stack",
- "count": 7
+ "name": "DirtyConfirmDialog",
+ "count": 6
+ },
+ {
+ "name": "useDirtyConfirmation",
+ "count": 6
},
{
"name": "Dropzone",
@@ -1420,14 +1428,6 @@
"name": "ComboboxValue",
"count": 6
},
- {
- "name": "DirtyConfirmDialog",
- "count": 5
- },
- {
- "name": "useDirtyConfirmation",
- "count": 5
- },
{
"name": "DropdownMenu",
"count": 5
@@ -1444,10 +1444,18 @@
"name": "DropdownMenuTrigger",
"count": 5
},
+ {
+ "name": "useGlobalDirtyState",
+ "count": 4
+ },
{
"name": "SettingGroup",
"count": 4
},
+ {
+ "name": "ButtonProps",
+ "count": 4
+ },
{
"name": "InputGroupButton",
"count": 4
@@ -1468,14 +1476,6 @@
"name": "PopoverTrigger",
"count": 4
},
- {
- "name": "useGlobalDirtyState",
- "count": 3
- },
- {
- "name": "ButtonProps",
- "count": 3
- },
{
"name": "Badge",
"count": 3
@@ -1552,6 +1552,10 @@
"name": "ColorSwatchRow",
"count": 2
},
+ {
+ "name": "Box",
+ "count": 2
+ },
{
"name": "Checkbox",
"count": 2
@@ -1697,11 +1701,23 @@
"count": 1
},
{
- "name": "Indicator",
+ "name": "TextElement",
"count": 1
},
{
- "name": "Box",
+ "name": "TextLeading",
+ "count": 1
+ },
+ {
+ "name": "TextSize",
+ "count": 1
+ },
+ {
+ "name": "SettingsModalSize",
+ "count": 1
+ },
+ {
+ "name": "Indicator",
"count": 1
},
{
@@ -1770,10 +1786,6 @@
}
],
"adminXDsComponents": [
- {
- "name": "PreviewModalContent",
- "count": 7
- },
{
"name": "useDesignSystem",
"count": 2
@@ -1828,7 +1840,7 @@
"topShadeComponents": [
{
"name": "Button",
- "count": 185
+ "count": 186
},
{
"name": "LucideIcon",
@@ -1856,18 +1868,18 @@
},
{
"name": "cn",
- "count": 45
+ "count": 46
},
{
"name": "FieldDescription",
"count": 37
},
{
- "name": "FieldError",
- "count": 34
+ "name": "SettingsModal",
+ "count": 35
},
{
- "name": "SettingsModal",
+ "name": "FieldError",
"count": 34
},
{
@@ -1908,10 +1920,6 @@
}
],
"adminXDsComponentsAggregate": [
- {
- "name": "PreviewModalContent",
- "count": 7
- },
{
"name": "useDesignSystem",
"count": 2
diff --git a/apps/shade/tailwind.theme.css b/apps/shade/tailwind.theme.css
index 537c450ad01..56a8d4293f5 100644
--- a/apps/shade/tailwind.theme.css
+++ b/apps/shade/tailwind.theme.css
@@ -170,6 +170,9 @@
--color-surface-elevated-2: var(--surface-elevated-2);
--color-modal-backdrop: var(--modal-backdrop);
--color-form-sheet-backdrop: var(--form-sheet-backdrop);
+ --color-preview-canvas: var(--preview-canvas);
+ --color-preview-gradient-start: var(--preview-gradient-start);
+ --color-preview-gradient-end: var(--preview-gradient-end);
--color-surface-inverse: var(--surface-inverse);
--color-surface-inverse-foreground: var(--surface-inverse-foreground);
--color-text-primary: var(--text-primary);
diff --git a/apps/shade/theme-variables.css b/apps/shade/theme-variables.css
index e5dca02b861..87213b90b22 100644
--- a/apps/shade/theme-variables.css
+++ b/apps/shade/theme-variables.css
@@ -29,6 +29,9 @@
--surface-overlay-foreground: var(--color-black);
--modal-backdrop: rgb(98 109 121 / 0.2);
--form-sheet-backdrop: rgb(98 109 121 / 0.08);
+ --preview-canvas: var(--color-gray-50);
+ --preview-gradient-start: var(--color-white);
+ --preview-gradient-end: var(--color-gray-50);
--surface-inverse: var(--color-black);
--surface-inverse-foreground: var(--color-white);
--text-primary: var(--color-black);
@@ -148,6 +151,9 @@
--surface-elevated-2: oklch(0.235 0.004 260);
--surface-overlay: var(--color-black);
--surface-overlay-foreground: var(--color-gray-400);
+ --preview-canvas: var(--color-black);
+ --preview-gradient-start: var(--color-gray-950);
+ --preview-gradient-end: var(--color-black);
--surface-inverse: var(--color-white);
--surface-inverse-foreground: var(--color-black);
--text-primary: var(--color-gray-200);
From 9605e363e931885d96f8f4bebaf8fcc8a1ed70a9 Mon Sep 17 00:00:00 2001
From: Steve Larson <9larsons@gmail.com>
Date: Thu, 23 Jul 2026 19:31:08 -0500
Subject: [PATCH 3/6] Moved settings page headers to Shade (#29570)
no ref
- moved the theme browser and theme preview headers from the legacy
Admin X design system to Shade's `PageHeader`
- preserved the theme browser's 92px sticky toolbar, including its 1px
top overlap and stacking layer so theme cards cannot paint over it
- switched the theme preview surface to Shade's semantic background
token
- removed the now-unused legacy `Page` and `PageHeader` layout
implementation, stories, helpers, and exports
---
.../src/global/layout/app-menu.tsx | 13 -
.../src/global/layout/global-actions.tsx | 11 -
.../src/global/layout/page-header.stories.tsx | 67 ----
.../src/global/layout/page-header.tsx | 77 -----
.../src/global/layout/page.stories.tsx | 306 ------------------
.../src/global/layout/page.tsx | 127 --------
apps/admin-x-design-system/src/index.ts | 4 -
.../components/settings/site/theme-modal.tsx | 12 +-
.../settings/site/theme/theme-preview.tsx | 13 +-
9 files changed, 19 insertions(+), 611 deletions(-)
delete mode 100644 apps/admin-x-design-system/src/global/layout/app-menu.tsx
delete mode 100644 apps/admin-x-design-system/src/global/layout/global-actions.tsx
delete mode 100644 apps/admin-x-design-system/src/global/layout/page-header.stories.tsx
delete mode 100644 apps/admin-x-design-system/src/global/layout/page-header.tsx
delete mode 100644 apps/admin-x-design-system/src/global/layout/page.stories.tsx
delete mode 100644 apps/admin-x-design-system/src/global/layout/page.tsx
diff --git a/apps/admin-x-design-system/src/global/layout/app-menu.tsx b/apps/admin-x-design-system/src/global/layout/app-menu.tsx
deleted file mode 100644
index 8f11e0aeddd..00000000000
--- a/apps/admin-x-design-system/src/global/layout/app-menu.tsx
+++ /dev/null
@@ -1,13 +0,0 @@
-import React from 'react';
-import {Button} from '@tryghost/shade/components';
-import {LucideIcon} from '@tryghost/shade/utils';
-
-const PageMenu: React.FC = () => {
- return (
- {
- alert('Clicked on hamburger');
- }}>
- );
-};
-
-export default PageMenu;
diff --git a/apps/admin-x-design-system/src/global/layout/global-actions.tsx b/apps/admin-x-design-system/src/global/layout/global-actions.tsx
deleted file mode 100644
index 030345f2780..00000000000
--- a/apps/admin-x-design-system/src/global/layout/global-actions.tsx
+++ /dev/null
@@ -1,11 +0,0 @@
-import React from 'react';
-import {Button} from '@tryghost/shade/components';
-import {LucideIcon} from '@tryghost/shade/utils';
-
-const GlobalActions: React.FC = () => {
- return (
- {}}>
- );
-};
-
-export default GlobalActions;
diff --git a/apps/admin-x-design-system/src/global/layout/page-header.stories.tsx b/apps/admin-x-design-system/src/global/layout/page-header.stories.tsx
deleted file mode 100644
index 1d95769b378..00000000000
--- a/apps/admin-x-design-system/src/global/layout/page-header.stories.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import type {Meta, StoryObj} from '@storybook/react-vite';
-
-import PageHeader from './page-header';
-
-const meta = {
- title: 'Global / Layout / Page Header',
- component: PageHeader,
- tags: ['autodocs']
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {
- args: {
- left: 'Left content',
- center: 'Center content',
- right: 'Right content'
- }
-};
-
-export const CustomContainer: Story = {
- args: {
- left: 'Left content',
- center: 'Center content',
- right: 'Right content',
- containerClassName: 'bg-grey-50'
- }
-};
-
-export const LeftAndRight: Story = {
- args: {
- left: 'Left content',
- right: 'Right content'
- }
-};
-
-export const LeftOnly: Story = {
- args: {
- left: 'Left content'
- }
-};
-
-export const CenterOnly: Story = {
- args: {
- center: 'Center content'
- }
-};
-
-export const RightOnly: Story = {
- args: {
- right: 'Right content'
- }
-};
-
-export const CustomContent: Story = {
- args: {
- children: (
-
-
This
-
is
-
custom
-
content!
-
- )
- }
-};
diff --git a/apps/admin-x-design-system/src/global/layout/page-header.tsx b/apps/admin-x-design-system/src/global/layout/page-header.tsx
deleted file mode 100644
index 43bbd50c2c6..00000000000
--- a/apps/admin-x-design-system/src/global/layout/page-header.tsx
+++ /dev/null
@@ -1,77 +0,0 @@
-import clsx from 'clsx';
-import React from 'react';
-
-export interface PageHeaderProps {
-
- /**
- * Use these to specifically place elements on the left | center | right of the header.
- */
- left?: React.ReactNode;
- center?: React.ReactNode;
- right?: React.ReactNode;
-
- sticky?: boolean;
- containerClassName?: string;
-
- /**
- * Or you can simply use the whole container to make sure header spacing is consistent. `children` takes precedence over `left`, `center` and `right`.
- */
- children?: React.ReactNode;
-}
-
-const PageHeader: React.FC = ({
- left,
- center,
- right,
- sticky = true,
- containerClassName,
- children
-}) => {
- const containerClasses = clsx(
- 'z-50 h-22 min-h-[92px] p-8',
- !children && 'flex items-center justify-between gap-3',
- sticky && 'sticky top-0',
- containerClassName
- );
-
- if (!children) {
- if (left) {
- const leftClasses = clsx(
- 'flex flex-auto items-center',
- (right && center) && 'basis-1/3',
- ((!right && center)) && 'basis-1/2'
- );
- left = {left}
;
- }
- if (center) {
- const centerClasses = clsx(
- 'flex flex-auto items-center justify-center',
- (left && right) && 'basis-1/3',
- ((left && !right) || (!left && right)) && 'basis-1/2'
- );
- center = {center}
;
- }
- if (right) {
- const rightClasses = clsx(
- 'flex flex-auto items-center justify-end',
- (left && center) && 'basis-1/3',
- ((!left && center)) && 'basis-1/2'
- );
- right = {right}
;
- }
- }
-
- return (
-
- {children ? children :
- <>
- {left}
- {center}
- {right}
- >
- }
-
- );
-};
-
-export default PageHeader;
diff --git a/apps/admin-x-design-system/src/global/layout/page.stories.tsx b/apps/admin-x-design-system/src/global/layout/page.stories.tsx
deleted file mode 100644
index a220f1c0207..00000000000
--- a/apps/admin-x-design-system/src/global/layout/page.stories.tsx
+++ /dev/null
@@ -1,306 +0,0 @@
-import type {Meta, StoryObj} from '@storybook/react-vite';
-
-import Page, {CustomGlobalAction} from './page';
-import ViewContainer from './view-container';
-
-import {exampleActions as exampleActionButtons} from './view-container.stories';
-import {Text} from '@tryghost/shade/primitives';
-import {LucideIcon} from '@tryghost/shade/utils';
-
-const meta = {
- title: 'Global / Layout / Page',
- component: Page,
- tags: ['autodocs'],
- parameters: {
- layout: 'fullscreen'
- }
-} satisfies Meta;
-
-export default meta;
-type Story = StoryObj;
-
-const dummyContent = Placeholder content
;
-
-const customGlobalActions: CustomGlobalAction[] = [
- {
- key: 'heart',
- icon: ,
- ariaLabel: 'Favorite',
- onClick: () => {
- alert('Clicked on custom action');
- }
- }
-];
-
-const pageTabs = Page tabs slot
;
-
-export const Default: Story = {
- args: {
- pageTabs: pageTabs,
- children: dummyContent
- }
-};
-
-export const LimitToolbarWidth: Story = {
- args: {
- pageTabs: pageTabs,
- children: dummyContent,
- fullBleedToolbar: false
- }
-};
-
-export const WithHamburger: Story = {
- args: {
- pageTabs: pageTabs,
- showAppMenu: true,
- children: dummyContent
- }
-};
-
-export const WithGlobalActions: Story = {
- args: {
- pageTabs: pageTabs,
- showAppMenu: true,
- showGlobalActions: true,
- children: dummyContent
- }
-};
-
-export const CustomGlobalActions: Story = {
- args: {
- pageTabs: pageTabs,
- showAppMenu: true,
- showGlobalActions: true,
- children: dummyContent,
- customGlobalActions: customGlobalActions
- }
-};
-
-const mockIdeaCards = () => {
- const cards = [];
-
- for (let i = 0; i < 11; i++) {
- cards.push(
-
-
- {i % 3 === 0 && 'Sunset drinks cruise eat sleep repeat'}
- {i % 3 === 1 && 'Elegance Rolls Royce on my private jet'}
- {i % 3 === 2 && 'Down to the wire Bathurst 5000 Le Tour'}
-
-
- {i % 3 === 0 && 'Numea captain’s table crystal waters paradise island the scenic route great adventure. Pirate speak the road less travelled seas the day '}
- {i % 3 === 1 && 'Another day in paradise cruise life adventure bound gap year cruise time languid afternoons let the sea set you free'}
- {i % 3 === 2 && No body text }
-
-
- );
- }
- return cards;
-};
-
-const exampleCardViewContent = (
- New idea action slot}
- title='Ideas'
- type='page'
- >
-
- {mockIdeaCards()}
-
-
-);
-
-export const ExampleCardView: Story = {
- name: 'Example: Card View',
- args: {
- pageTabs: pageTabs,
- showAppMenu: true,
- showGlobalActions: true,
- children: exampleCardViewContent
- }
-};
-
-const mockPosts = () => {
- const posts = [];
-
- for (let i = 0; i < 11; i++) {
- posts.push(
-
-
-
-
-
-
-
- {i % 3 === 0 && 'Sunset drinks cruise eat sleep repeat'}
- {i % 3 === 1 && 'Elegance Rolls Royce on my private jet'}
- {i % 3 === 2 && 'Down to the wire Bathurst 5000 Le Tour'}
-
-
- {i % 3 === 0 && 'Numea captain’s table crystal waters paradise island the scenic route great adventure. Pirate speak the road less travelled seas the day '}
- {i % 3 === 1 && 'Another day in paradise cruise life adventure bound gap year cruise time languid afternoons let the sea set you free'}
- {i % 3 === 2 && 'Grand Prix gamble responsibly intensity is not a perfume The Datsun 180B Aerial ping pong knock for six watch with the boys total hospital pass.'}
-
-
-
-
- 15%
- viewed
-
-
- 55%
- opened
-
-
- Post action slot
-
-
- );
- }
- return posts;
-};
-
-const examplePostsContent = (
- New post action slot}
- title='Posts'
- type='page'
- >
-
- {<>{mockPosts()}>}
-
-
-);
-
-export const ExampleAlternativeList: Story = {
- name: 'Example: Alternative List',
- args: {
- pageTabs: pageTabs,
- showAppMenu: true,
- showGlobalActions: true,
- children: examplePostsContent
- }
-};
-
-export const ExampleDetailScreen: Story = {
- name: 'Example: Detail Page',
- args: {
- showAppMenu: true,
- breadCrumbs: Breadcrumbs slot ,
- showGlobalActions: true,
- children: <>
-
- Emerson Vaccaro
- Colombus, OH
-
- }
- primaryAction={Member action slot }
- type='page'
- >
-
-
- Last seen on 22 June 2023
- Created on 27 Jan 2021
-
-
- Emails received
- 181
-
-
- Emails opened
- 104
-
-
- Average open rate
- 57%
-
-
-
-
-
- Member data
- Edit action slot
-
-
-
Name
-
Emerson Vaccaro
-
-
-
Email
-
emerson@vaccaro.com
-
-
-
-
-
-
Newsletters
-
-
-
- Daily news
-
-
-
- Weekly roundup
-
-
-
- The Inner Circle
-
-
- This member cannot receive emails due to permanent failure (bounce).
-
-
-
-
-
Subscriptions
-
-
- $5
- Yearly
-
-
- Gold
- Renews 21 Jan 2024
-
-
-
-
-
- Activity
- View all action slot
-
-
- Logged in
- 13 days ago
-
-
- Subscribed to Daily News
- 17 days ago
-
-
- Logged in
- 21 days ago
-
-
-
-
- >
- }
-};
diff --git a/apps/admin-x-design-system/src/global/layout/page.tsx b/apps/admin-x-design-system/src/global/layout/page.tsx
deleted file mode 100644
index 17bc4f7d601..00000000000
--- a/apps/admin-x-design-system/src/global/layout/page.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import React from 'react';
-import clsx from 'clsx';
-import AppMenu from './app-menu';
-import GlobalActions from './global-actions';
-import PageHeader from './page-header';
-import {Button} from '@tryghost/shade/components';
-
-export interface CustomGlobalAction {
- key: string;
- icon: React.ReactNode;
- ariaLabel: string;
- onClick?: () => void;
-}
-
-interface PageProps {
- mainContainerClassName?: string;
- mainClassName?: string;
- fullBleedPage?: boolean;
-
- /**
- * The pageToolbar is a WIP part of this component, it's unused ATM in Ghost Admin.
- */
- pageToolbarClassName?: string;
- fullBleedToolbar?: boolean;
-
- /**
- * TK. Part of the Page Toolbar
- */
- showAppMenu?: boolean;
-
- /**
- * Show
- */
- showGlobalActions?: boolean;
-
- /**
- * TK. Part of the Page Toolbar
- */
- customGlobalActions?: CustomGlobalAction[];
- breadCrumbs?: React.ReactNode;
-
- /**
- * TK. Part of the Page Toolbar
- */
- pageTabs?: React.ReactNode,
-
- children?: React.ReactNode;
-}
-
-/**
- * The page component is the main container in Ghost Admin. It consists of a
- * page level toolbar (`pageToolbar` — unused ATM, it's for page level views and
- * navigation in the future), and the main content area.
- *
- * ### Examples
- * You can find several examples in the sidebar. If you're building a page for the
- * current Admin you can use the ["List in Current Admin"](/story/global-layout-page--example-current-admin-list)
- * example as a starting point. The rest of the examples are showing a potential direction for a
- * future structure.
- */
-const Page: React.FC = ({
- fullBleedPage = true,
- mainContainerClassName,
- mainClassName,
- pageToolbarClassName,
- fullBleedToolbar = true,
- showAppMenu = false,
- showGlobalActions = false,
- customGlobalActions,
- breadCrumbs,
- pageTabs,
- children
-}) => {
- const left: React.ReactNode = (
- (showAppMenu || breadCrumbs || pageTabs) &&
- {showAppMenu && (
-
- )}
- {breadCrumbs}
- {pageTabs}
-
);
-
- mainClassName = clsx(
- 'flex w-full flex-auto flex-col',
- mainClassName
- );
-
- const globalActions = (
- (customGlobalActions?.length || showGlobalActions) &&
-
- {(customGlobalActions?.map((action) => {
- return (
- {action.icon}
- );
- }))}
- {showGlobalActions && }
-
);
-
- mainContainerClassName = clsx(
- 'flex h-[100vh] w-full flex-col overflow-x-hidden overflow-y-auto',
- !fullBleedPage && 'mx-auto max-w-7xl',
- mainContainerClassName
- );
-
- pageToolbarClassName = clsx(
- 'sticky top-0 z-50 flex h-22 min-h-[92px] w-full items-center justify-between gap-5 bg-white p-8 dark:bg-black',
- !fullBleedToolbar && 'mx-auto max-w-7xl',
- pageToolbarClassName
- );
-
- return (
-
- {(left || globalActions) &&
-
- }
-
- {children}
-
-
- );
-};
-
-export default Page;
diff --git a/apps/admin-x-design-system/src/index.ts b/apps/admin-x-design-system/src/index.ts
index aa0b3513382..486488ab966 100644
--- a/apps/admin-x-design-system/src/index.ts
+++ b/apps/admin-x-design-system/src/index.ts
@@ -16,10 +16,6 @@ export {default as Separator} from './global/separator';
export type {SeparatorProps} from './global/separator';
export {default as Tooltip} from './global/tooltip';
export type {TooltipProps} from './global/tooltip';
-export {default as PageHeader} from './global/layout/page-header';
-export type {PageHeaderProps} from './global/layout/page-header';
-export {default as Page} from './global/layout/page';
-export type {CustomGlobalAction} from './global/layout/page';
export {default as ViewContainer} from './global/layout/view-container';
export type {View} from './global/layout/view-container';
export type {PrimaryActionProps} from './global/layout/view-container';
diff --git a/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx b/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
index 9f22483b80f..a84da87d125 100644
--- a/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/theme-modal.tsx
@@ -11,8 +11,7 @@ import {Button, Dropzone, LoadingIndicator, Tabs, TabsList, TabsTrigger} from '@
import {type InstalledTheme, type Theme, type ThemesInstallResponseType, isDefaultOrLegacyTheme, useActivateTheme, useBrowseThemes, useInstallTheme, useUploadTheme} from '@tryghost/admin-x-framework/api/themes';
import {JSONError} from '@tryghost/admin-x-framework/errors';
import {type OfficialTheme} from '../../providers/settings-app-provider';
-import {PageHeader} from '@tryghost/admin-x-design-system';
-import {SettingsModal} from '@tryghost/shade/patterns';
+import {PageHeader, SettingsModal} from '@tryghost/shade/patterns';
import {toast} from 'sonner';
import {useCheckThemeLimitError} from '../../../hooks/use-check-theme-limit-error';
import {useHandleError} from '@tryghost/admin-x-framework/hooks';
@@ -259,7 +258,14 @@ const ThemeToolbar: React.FC = ({
;
return (<>
-
+
+
+ {left}
+
+
+ {right}
+
+
diff --git a/apps/admin-x-settings/src/components/settings/site/theme/theme-preview.tsx b/apps/admin-x-settings/src/components/settings/site/theme/theme-preview.tsx
index 04ea46f00dd..af76bda0ffe 100644
--- a/apps/admin-x-settings/src/components/settings/site/theme/theme-preview.tsx
+++ b/apps/admin-x-settings/src/components/settings/site/theme/theme-preview.tsx
@@ -4,7 +4,7 @@ import {Button, Field, FieldLabel, PreviewChrome, Select, SelectContent, SelectI
import {Inline} from '@tryghost/shade/primitives';
import {LucideIcon} from '@tryghost/shade/utils';
import {type OfficialTheme, type ThemeVariant} from '../../../providers/settings-app-provider';
-import {PageHeader} from '@tryghost/admin-x-design-system';
+import {PageHeader} from '@tryghost/shade/patterns';
import {type Theme, isDefaultOrLegacyTheme} from '@tryghost/admin-x-framework/api/themes';
const hasVariants = (theme: OfficialTheme) => theme.variants && theme.variants.length > 0;
@@ -122,8 +122,15 @@ const ThemePreview: React.FC<{
return (
-
-
+
+
+ {left}
+
+
+ {right}
+
+
+
{previewMode === 'desktop' ?