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 ( -
- -
- ); -}; - -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 && ( - - )} - {okLabel && ( - - )} - -
- ); - } - - footerContent = (stickyFooter ? - - {footerContent} - - : - <> - {footerContent} - - ); - - return ( - <> - - - - ); -}); - -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 ( + + + + ); +}; + +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: , + 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 && ( + + )} + {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 ( - - ); -}; - -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 > -
- + {sidebar && -
+ {sidebarHeader ? sidebarHeader : ( -
+ = ({ )} -
+ )} -
+ {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 ( - - ); -}; - -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
-
-
- Labels -
-
VIP
-
Inner Circle
-
-
-
- Notes -
No notes.
-
-
-
- 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 ( - - ); - }))} - {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' ?