From 0c8323b77b84da688b7db1f3d9b15b5635434c10 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 22 Jul 2026 17:50:39 -0500 Subject: [PATCH 1/6] Updated Settings notifications to use shade (#29546) no ref - replace legacy Settings and Admin-X framework toast calls with Sonner - remove the react-hot-toast renderers, provider, dependencies, and workspace catalog entry - preserve bottom-left positioning, five-second duration, close controls, stale-toast dismissal, and route-safe staff deletion feedback - restore the destructive red Leave action in the shared dirty-confirm dialog - update acceptance locators and regression coverage for the new notification host --- apps/admin-x-design-system/package.json | 3 +- .../src/global/toast.stories.tsx | 136 ------------------ .../src/global/toast.tsx | 120 ---------------- apps/admin-x-design-system/src/index.ts | 2 - .../src/providers/design-system-provider.tsx | 2 - apps/admin-x-framework/package.json | 4 +- .../src/hooks/use-handle-error.ts | 16 +-- apps/admin-x-framework/src/utils/toast.tsx | 49 ------- .../test/unit/api/labels.test.tsx | 9 +- .../test/unit/hooks/use-handle-error.test.tsx | 70 +++------ apps/admin-x-settings/package.json | 1 - .../settings/advanced/danger-zone.tsx | 19 +-- .../settings/advanced/integrations.tsx | 11 +- .../advanced/integrations/pintura-modal.tsx | 8 +- .../advanced/integrations/slack-modal.tsx | 13 +- .../advanced/integrations/webhooks-table.tsx | 8 +- .../settings/advanced/labs/beta-features.tsx | 13 +- .../advanced/labs/migration-options.tsx | 8 +- .../advanced/labs/yaml-file-editor-modal.tsx | 8 +- .../newsletters/newsletter-detail-modal.tsx | 19 +-- .../settings/general/invite-user-modal.tsx | 15 +- .../settings/general/user-detail-modal.tsx | 35 ++--- .../src/components/settings/general/users.tsx | 15 +- .../general/users/change-password-form.tsx | 8 +- .../growth/explore/testimonials-modal.tsx | 8 +- .../growth/offers/add-offer-modal.tsx | 20 +-- .../growth/offers/edit-offer-modal.tsx | 22 +-- .../offers/edit-retention-offer-modal.tsx | 12 +- .../settings/growth/offers/offers-index.tsx | 8 +- .../add-recommendation-modal-confirm.tsx | 15 +- .../add-recommendation-modal.tsx | 10 +- .../edit-recommendation-modal.tsx | 17 +-- .../components/settings/membership/access.tsx | 8 +- .../custom-fields/custom-field-modal.tsx | 15 +- .../settings/membership/member-emails.tsx | 9 +- .../stripe/stripe-connect-modal.tsx | 12 +- .../membership/tiers/tier-detail-modal.tsx | 8 +- .../settings/site/announcement-bar-modal.tsx | 8 +- .../components/settings/site/theme-modal.tsx | 9 +- .../site/theme/advanced-theme-settings.tsx | 9 +- .../site/theme/theme-code-editor-modal.tsx | 68 ++------- .../site/theme/theme-installed-modal.tsx | 9 +- apps/admin-x-settings/src/main-content.tsx | 12 +- .../general/staff-actions.acceptance.test.tsx | 1 + apps/admin/src/settings/offers.screen.ts | 2 +- apps/admin/src/settings/settings.screen.ts | 9 +- .../patterns/dirty-confirm-dialog.tsx | 2 +- apps/shade/src/providers/shade-provider.tsx | 2 + .../patterns/dirty-confirm-dialog.test.tsx | 7 +- pnpm-lock.yaml | 38 +---- pnpm-workspace.yaml | 1 - 51 files changed, 202 insertions(+), 731 deletions(-) delete mode 100644 apps/admin-x-design-system/src/global/toast.stories.tsx delete mode 100644 apps/admin-x-design-system/src/global/toast.tsx delete mode 100644 apps/admin-x-framework/src/utils/toast.tsx diff --git a/apps/admin-x-design-system/package.json b/apps/admin-x-design-system/package.json index 7c069b3ec33..924e8e65de8 100644 --- a/apps/admin-x-design-system/package.json +++ b/apps/admin-x-design-system/package.json @@ -64,8 +64,7 @@ "@radix-ui/react-tooltip": "catalog:", "@sentry/react": "catalog:", "@tryghost/shade": "workspace:*", - "clsx": "catalog:", - "react-hot-toast": "catalog:" + "clsx": "catalog:" }, "peerDependencies": { "react": "^18.2.0", diff --git a/apps/admin-x-design-system/src/global/toast.stories.tsx b/apps/admin-x-design-system/src/global/toast.stories.tsx deleted file mode 100644 index 89a04d7c860..00000000000 --- a/apps/admin-x-design-system/src/global/toast.stories.tsx +++ /dev/null @@ -1,136 +0,0 @@ -import type {Meta, StoryObj} from '@storybook/react-vite'; -import {ReactNode} from 'react'; - -import Button from './button'; -import {ShowToastProps, showToast} from './toast'; - -/** - * This component uses `react-hot-toast` which requires the `` component to be included in the app. - * The design system already does this so you don't have to — just call `showToast()` in any event and it'll work. - */ -const ToastContainer: React.FC = ({...props}) => { - return ( - <> - - - ); -}; - -export default Toast; - -export const showToast = ({ - title, - message, - type = 'neutral', - icon = '', - options = { - position: 'bottom-left', - duration: 5000 - } -}: ShowToastProps): void => { - if (!options.position) { - options.position = 'bottom-left'; - } - - if (type === 'pageError') { - type = 'error'; - options.position = 'top-center'; - options.duration = Infinity; - } - - toast.custom(t => ( - -
- {title && {title}} - {message && -
{message}
- } -
-
- ), - { - ...options - } - ); -}; - -export const dismissAllToasts = (): void => { - toast.dismiss(); -}; diff --git a/apps/admin-x-design-system/src/index.ts b/apps/admin-x-design-system/src/index.ts index 84b10522f2d..9248a370fdb 100644 --- a/apps/admin-x-design-system/src/index.ts +++ b/apps/admin-x-design-system/src/index.ts @@ -40,8 +40,6 @@ export {LoadingIndicator} from './global/loading-indicator'; export type {LoadingIndicatorProps} from './global/loading-indicator'; export {default as Separator} from './global/separator'; export type {SeparatorProps} from './global/separator'; -export {default as Toast, dismissAllToasts, showToast} from './global/toast'; -export type {ToastProps} from './global/toast'; export {default as Tooltip} from './global/tooltip'; export type {TooltipProps} from './global/tooltip'; export {default as PageHeader} from './global/layout/page-header'; diff --git a/apps/admin-x-design-system/src/providers/design-system-provider.tsx b/apps/admin-x-design-system/src/providers/design-system-provider.tsx index cfd8bc38c8d..7ca3284e736 100644 --- a/apps/admin-x-design-system/src/providers/design-system-provider.tsx +++ b/apps/admin-x-design-system/src/providers/design-system-provider.tsx @@ -1,7 +1,6 @@ // FocusContext.tsx import NiceModal from '@ebay/nice-modal-react'; import React, {createContext, useContext} from 'react'; -import {Toaster} from 'react-hot-toast'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export type FetchKoenigLexical = () => Promise; @@ -27,7 +26,6 @@ interface DesignSystemProviderProps { const DesignSystemProvider: React.FC = ({fetchKoenigLexical, darkMode, children}) => { return ( - {children} diff --git a/apps/admin-x-framework/package.json b/apps/admin-x-framework/package.json index 36a1db16b27..232dfeff738 100644 --- a/apps/admin-x-framework/package.json +++ b/apps/admin-x-framework/package.json @@ -114,8 +114,8 @@ "bson-objectid": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-hot-toast": "catalog:", - "react-router": "catalog:" + "react-router": "catalog:", + "sonner": "catalog:" }, "peerDependencies": { "react": "^18.2.0", diff --git a/apps/admin-x-framework/src/hooks/use-handle-error.ts b/apps/admin-x-framework/src/hooks/use-handle-error.ts index 96dff7428e8..1ac8b55314d 100644 --- a/apps/admin-x-framework/src/hooks/use-handle-error.ts +++ b/apps/admin-x-framework/src/hooks/use-handle-error.ts @@ -1,16 +1,12 @@ import * as Sentry from '@sentry/react'; import {useCallback} from 'react'; -import toast from 'react-hot-toast'; +import {toast} from 'sonner'; import {useFramework} from '../providers/framework-provider'; import {APIError, SessionExpiredError, getErrorMessage} from '../utils/errors'; -import {showToast} from '../utils/toast'; function showErrorToast(message: React.ReactNode) { - toast.remove(); - showToast({ - message, - type: 'error' - }); + toast.dismiss(); + toast.error(message); } /** @@ -49,12 +45,12 @@ const useHandleError = () => { if (error instanceof APIError && error.response?.status === 418) { // We use this status in tests to indicate the API request was not mocked - // don't show a toast because it may block clicking things in the test, - // but still clear lingering toasts that would block clicks the same way - toast.remove(); + // but still clear lingering toasts that would block clicks the same way. + toast.dismiss(); } else if (error instanceof SessionExpiredError) { // Session-expiry 401s trigger a redirect to signin in the fetch // layer - a toast would only flash while the page unloads - toast.remove(); + toast.dismiss(); } else if (error instanceof APIError) { showErrorToast(getErrorMessage(error, error.message)); } else { diff --git a/apps/admin-x-framework/src/utils/toast.tsx b/apps/admin-x-framework/src/utils/toast.tsx deleted file mode 100644 index 5f960a6f7b0..00000000000 --- a/apps/admin-x-framework/src/utils/toast.tsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import toast from 'react-hot-toast'; - -// Markup, classes and icons are copied from admin-x-design-system's Toast so error -// toasts render identically inside its react-hot-toast . -const ErrorIcon: React.FC<{className?: string}> = ({className}) => ( - - - -); - -const CloseIcon: React.FC<{className?: string}> = ({className}) => ( - - - - -); - -export interface ShowToastProps { - message: React.ReactNode; - type: 'error'; -} - -export const showToast = ({message, type}: ShowToastProps): void => { - toast.custom(t => ( -
-
-
- -
-
-
{message}
-
-
- -
- ), {position: 'bottom-left', duration: 5000}); -}; diff --git a/apps/admin-x-framework/test/unit/api/labels.test.tsx b/apps/admin-x-framework/test/unit/api/labels.test.tsx index ac61f2baf39..690e33a7a4f 100644 --- a/apps/admin-x-framework/test/unit/api/labels.test.tsx +++ b/apps/admin-x-framework/test/unit/api/labels.test.tsx @@ -5,15 +5,10 @@ import {renderHookWithProviders} from '../../../src/test/test-utils'; import {useCreateLabel, useEditLabel, useFindLabelByName} from '../../../src/api/labels'; import {withMockFetch} from '../../utils/mock-fetch'; -const {mockShowToast, mockSonnerError} = vi.hoisted(() => ({ - mockShowToast: vi.fn(), +const {mockSonnerError} = vi.hoisted(() => ({ mockSonnerError: vi.fn() })); -vi.mock('../../../src/utils/toast', () => ({ - showToast: mockShowToast -})); - vi.mock('sonner', () => ({ toast: { error: mockSonnerError, @@ -63,7 +58,6 @@ describe('labels api', () => { await expect(result.current.mutateAsync({name: 'Existing label'})).rejects.toBeInstanceOf(ValidationError); }); - expect(mockShowToast).not.toHaveBeenCalled(); expect(mockSonnerError).not.toHaveBeenCalled(); }); }); @@ -130,7 +124,6 @@ describe('labels api', () => { await expect(result.current.mutateAsync({id: 'label-1', name: 'Existing label'})).rejects.toBeInstanceOf(ValidationError); }); - expect(mockShowToast).not.toHaveBeenCalled(); expect(mockSonnerError).not.toHaveBeenCalled(); }); }); diff --git a/apps/admin-x-framework/test/unit/hooks/use-handle-error.test.tsx b/apps/admin-x-framework/test/unit/hooks/use-handle-error.test.tsx index 452f87f625c..5c58e5a38dc 100644 --- a/apps/admin-x-framework/test/unit/hooks/use-handle-error.test.tsx +++ b/apps/admin-x-framework/test/unit/hooks/use-handle-error.test.tsx @@ -14,22 +14,20 @@ vi.mock('@sentry/react', () => ({ ErrorBoundary: ({children}: {children: any}) => children })); -vi.mock('../../../src/utils/toast', () => ({ - showToast: vi.fn() +const {mockToastDismiss, mockToastError} = vi.hoisted(() => ({ + mockToastDismiss: vi.fn(), + mockToastError: vi.fn() })); -vi.mock('react-hot-toast', () => ({ - default: { - remove: vi.fn() +vi.mock('sonner', () => ({ + toast: { + dismiss: mockToastDismiss, + error: mockToastError } })); -const mockShowToast = vi.fn(); -const mockToastRemove = vi.fn(); - import * as Sentry from '@sentry/react'; -import toast from 'react-hot-toast'; -import {showToast} from '../../../src/utils/toast'; +import {toast} from 'sonner'; const createWrapper = (sentryDSN?: string): React.FC<{children: ReactNode}> => { const TestWrapper: React.FC<{children: ReactNode}> = ({children}) => ( @@ -68,9 +66,6 @@ describe('useHandleError', () => { callback(scope); }); - (showToast as any).mockImplementation(mockShowToast); - (toast.remove as any).mockImplementation(mockToastRemove); - // Reset console.error mock vi.spyOn(console, 'error').mockImplementation(() => {}); }); @@ -151,7 +146,7 @@ describe('useHandleError', () => { result.current(error); - expect(toast.remove).toHaveBeenCalled(); + expect(toast.dismiss).toHaveBeenCalled(); }); it('does not show toast when withToast is false', () => { @@ -161,7 +156,7 @@ describe('useHandleError', () => { result.current(error, {withToast: false}); - expect(showToast).not.toHaveBeenCalled(); + expect(toast.error).not.toHaveBeenCalled(); }); it('does not show toast for 418 status (test indicator)', () => { @@ -173,7 +168,7 @@ describe('useHandleError', () => { result.current(error); - expect(showToast).not.toHaveBeenCalled(); + expect(toast.error).not.toHaveBeenCalled(); }); it('still clears lingering toasts for 418 status', () => { @@ -185,9 +180,7 @@ describe('useHandleError', () => { result.current(error); - // A stale toast can cover UI and block clicks in tests, so the - // unmocked-request path must clear toasts even without showing one - expect(toast.remove).toHaveBeenCalled(); + expect(toast.dismiss).toHaveBeenCalled(); }); it('does not send session expiry errors to Sentry', () => { @@ -213,8 +206,8 @@ describe('useHandleError', () => { // The fetch layer redirects to signin on session expiry, so the // error handler must not flash a toast over the unloading page - expect(showToast).not.toHaveBeenCalled(); - expect(toast.remove).toHaveBeenCalled(); + expect(toast.error).not.toHaveBeenCalled(); + expect(toast.dismiss).toHaveBeenCalled(); }); it('shows toast for unauthorized errors that do not trigger a redirect', () => { @@ -226,10 +219,7 @@ describe('useHandleError', () => { result.current(error); - expect(showToast).toHaveBeenCalledWith({ - message: 'You are not authorised to make this request.', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('You are not authorised to make this request.'); }); it('shows validation error message from context', () => { @@ -255,10 +245,7 @@ describe('useHandleError', () => { result.current(error); - expect(showToast).toHaveBeenCalledWith({ - message: 'This field must be filled out', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('This field must be filled out'); }); it('shows validation error message when no context available', () => { @@ -284,10 +271,7 @@ describe('useHandleError', () => { result.current(error); - expect(showToast).toHaveBeenCalledWith({ - message: 'Field is required', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('Field is required'); }); it('shows API error message', () => { @@ -298,10 +282,7 @@ describe('useHandleError', () => { result.current(error); - expect(showToast).toHaveBeenCalledWith({ - message: 'API Error occurred', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('API Error occurred'); }); it('shows generic error message for unknown errors', () => { @@ -312,10 +293,7 @@ describe('useHandleError', () => { result.current(error); - expect(showToast).toHaveBeenCalledWith({ - message: 'Something went wrong, please try again.', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('Something went wrong, please try again.'); }); it('handles string errors', () => { @@ -325,10 +303,7 @@ describe('useHandleError', () => { result.current('String error'); expect(console.error).toHaveBeenCalledWith('String error'); // eslint-disable-line no-console - expect(showToast).toHaveBeenCalledWith({ - message: 'Something went wrong, please try again.', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('Something went wrong, please try again.'); }); it('handles null/undefined errors', () => { @@ -338,9 +313,6 @@ describe('useHandleError', () => { result.current(null); expect(console.error).toHaveBeenCalledWith(null); // eslint-disable-line no-console - expect(showToast).toHaveBeenCalledWith({ - message: 'Something went wrong, please try again.', - type: 'error' - }); + expect(toast.error).toHaveBeenCalledWith('Something went wrong, please try again.'); }); }); diff --git a/apps/admin-x-settings/package.json b/apps/admin-x-settings/package.json index 6fa5671d6af..829bacd0e22 100644 --- a/apps/admin-x-settings/package.json +++ b/apps/admin-x-settings/package.json @@ -47,7 +47,6 @@ "lucide-react": "catalog:", "react": "catalog:", "react-dom": "catalog:", - "react-hot-toast": "catalog:", "semver": "catalog:", "sonner": "catalog:", "validator": "catalog:" diff --git a/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx b/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx index bc218dd2e9a..125b187779a 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx @@ -4,8 +4,10 @@ import TopLevelGroup from '../../top-level-group'; import trackEvent from '../../../utils/analytics'; import useStaffUsers from '../../../hooks/use-staff-users'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, SettingGroupHeader, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, SettingGroupHeader} from '@tryghost/admin-x-design-system'; +import {formatNumber} from '@tryghost/shade/utils'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; +import {toast} from 'sonner'; import {useDeleteAllContent} from '@tryghost/admin-x-framework/api/db'; import {useGlobalData} from '../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -40,10 +42,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { onOk: async (modal) => { try { await deleteAllContent(null); - showToast({ - title: 'All content deleted from database.', - type: 'success' - }); + toast.success('All content deleted from database.'); modal?.remove(); await client.refetchQueries(); } catch (e) { @@ -75,10 +74,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { const result = response?.security_action?.[0]; const keys = result?.api_keys_rotated ?? 0; const users = result?.users_locked ?? 0; - showToast({ - title: `Rotated ${keys} API ${keys === 1 ? 'key' : 'keys'} and locked ${users} ${users === 1 ? 'user' : 'users'}. You will be signed out shortly.`, - type: 'success' - }); + toast.success(`Rotated ${formatNumber(keys)} API ${keys === 1 ? 'key' : 'keys'} and locked ${formatNumber(users)} ${users === 1 ? 'user' : 'users'}. You will be signed out shortly.`); modal?.remove(); window.location.href = getGhostPaths().adminRoot; } catch (e) { @@ -100,10 +96,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { const response = await removeAllGiftLinks(null); const count = response?.meta?.count ?? 0; trackEvent('All Gift Links Reset'); - showToast({ - title: `Reset ${count} gift ${count === 1 ? 'link' : 'links'}.`, - type: 'success' - }); + toast.success(`Reset ${formatNumber(count)} gift ${count === 1 ? 'link' : 'links'}.`); modal?.remove(); } catch (e) { handleError(e); diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx index ca94efe7c23..141826127fd 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx @@ -4,10 +4,11 @@ import React, {useState} from 'react'; import TopLevelGroup from '../../top-level-group'; import usePinturaEditor from '../../../hooks/use-pintura-editor'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, NoValueLabel, NoValueLabelIcon, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, Icon, SettingGroupHeader, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, Icon, SettingGroupHeader} from '@tryghost/admin-x-design-system'; import {type Integration, useBrowseIntegrations, useDeleteIntegration} from '@tryghost/admin-x-framework/api/integrations'; import {Plug} from 'lucide-react'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useGlobalData} from '../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -226,13 +227,7 @@ const CustomIntegrations: React.FC<{integrations: Integration[]}> = ({integratio try { await deleteIntegration(integration.id); confirmModal?.remove(); - showToast({ - title: 'Integration deleted', - type: 'info', - options: { - position: 'bottom-left' - } - }); + toast.info('Integration deleted', {position: 'bottom-left'}); } catch (e) { handleError(e); } 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 45bb849ced2..11890e32efa 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 @@ -2,8 +2,9 @@ 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, FieldLabel, Switch} from '@tryghost/shade/components'; -import {Form, Icon, Modal, showToast} from '@tryghost/admin-x-design-system'; +import {Form, Icon, Modal} from '@tryghost/admin-x-design-system'; import {type Setting, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useEffect, useState} from 'react'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -62,10 +63,7 @@ const PinturaModal = NiceModal.create(() => { await editSettings(updates); - showToast({ - type: 'success', - title: `Pintura ${form} uploaded` - }); + toast.success(`Pintura ${form} uploaded`); } catch (e) { handleError(e); } finally { 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 76713bdfbb6..33f549722d2 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 @@ -1,10 +1,10 @@ import IntegrationHeader from './integration-header'; import NiceModal from '@ebay/nice-modal-react'; -import toast from 'react-hot-toast'; import useSettingGroup from '../../../../hooks/use-setting-group'; import validator from 'validator'; -import {Button, Form, Icon, Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; import {getSettingValues, useTestSlack} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useRouting} from '@tryghost/admin-x-framework/routing'; const SlackModal = NiceModal.create(() => { @@ -27,13 +27,10 @@ const SlackModal = NiceModal.create(() => { const {mutateAsync: testSlack} = useTestSlack(); const handleTestClick = async () => { - toast.remove(); + toast.dismiss(); if (await handleSave()) { await testSlack(null); - showToast({ - title: 'Check your Slack channel for the test message', - type: 'info' - }); + toast.info('Check your Slack channel for the test message'); } }; @@ -51,7 +48,7 @@ const SlackModal = NiceModal.create(() => { testId='slack-modal' title='' onOk={async () => { - toast.remove(); + toast.dismiss(); await handleSave(); }} > diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations/webhooks-table.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations/webhooks-table.tsx index 456f236c5f3..07d2d56aaf2 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations/webhooks-table.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations/webhooks-table.tsx @@ -1,10 +1,11 @@ import NiceModal from '@ebay/nice-modal-react'; import WebhookModal from './webhook-modal'; -import {Button, ConfirmationModal, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal} from '@tryghost/admin-x-design-system'; import {type Integration} from '@tryghost/admin-x-framework/api/integrations'; import {Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components'; import {formatNumber} from '@tryghost/shade/utils'; import {getWebhookEventLabel} from './webhook-event-options'; +import {toast} from 'sonner'; import {useDeleteWebhook} from '@tryghost/admin-x-framework/api/webhooks'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -22,10 +23,7 @@ const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => { try { await deleteWebhook(id); confirmModal?.remove(); - showToast({ - message: 'Webhook deleted', - type: 'info' - }); + toast.info('Webhook deleted'); } catch (e) { handleError(e); } diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx index f3959e11cc3..9874c7f9fa9 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx @@ -4,11 +4,12 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import YamlFileEditorModal from './yaml-file-editor-modal'; import {ActionList, Dropzone, Button as ShadeButton} from '@tryghost/shade/components'; -import {Button, showToast} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/admin-x-design-system'; import {Inline, Stack} from '@tryghost/shade/primitives'; import {downloadRedirects, useUploadRedirects} from '@tryghost/admin-x-framework/api/redirects'; import {downloadRoutes, useUploadRoutes} from '@tryghost/admin-x-framework/api/routes'; import {getSettingValue} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -87,10 +88,7 @@ const BetaFeatures: React.FC = () => { try { setRedirectsUploading(true); await uploadRedirects(file); - showToast({ - title: 'Redirects uploaded', - type: 'success' - }); + toast.success('Redirects uploaded'); } catch (e) { handleError(e); } finally { @@ -117,10 +115,7 @@ const BetaFeatures: React.FC = () => { try { setRoutesUploading(true); await uploadRoutes(file); - showToast({ - type: 'success', - title: 'Routes uploaded' - }); + toast.success('Routes uploaded'); } catch (e) { handleError(e); } finally { diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx index f0a5edf79b3..5a87bc0e812 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx @@ -2,8 +2,9 @@ import LabItem from './lab-item'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import {ActionList, Dropzone} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal} from '@tryghost/admin-x-design-system'; import {downloadAllContent, useDeleteAllContent, useImportContent} from '@tryghost/admin-x-framework/api/db'; +import {toast} from 'sonner'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useQueryClient} from '@tanstack/react-query'; @@ -65,10 +66,7 @@ const MigrationOptions: React.FC = () => { onOk: async (modal) => { try { await deleteAllContent(null); - showToast({ - type: 'success', - title: 'All content deleted from database.' - }); + toast.success('All content deleted from database.'); modal?.remove(); await client.refetchQueries(); } catch (e) { 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 275a085b607..76e38c8a40c 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 @@ -2,9 +2,10 @@ import CodeEditor from '../../../code-editor'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useEffect, useMemo, useState} from 'react'; import {APIError, JSONError} from '@tryghost/admin-x-framework/errors'; -import {ButtonGroup, Modal, showToast} from '@tryghost/admin-x-design-system'; +import {ButtonGroup, Modal} from '@tryghost/admin-x-design-system'; import {Text} from '@tryghost/shade/primitives'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; +import {toast} from 'sonner'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; export interface YamlFileEditorModalProps { @@ -111,10 +112,7 @@ const YamlFileEditorModal: React.FC = ({ const file = new File([content], uploadFilename, {type: 'text/yaml'}); await onUpload(file); - showToast({ - type: 'success', - title: successMessage - }); + toast.success(successMessage); closeModal(); } catch (error) { 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 c4f6158cda0..cc964e36e0f 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 @@ -6,7 +6,7 @@ import React, {useCallback, useEffect, useState} from 'react'; import useFeatureFlag from '../../../../hooks/use-feature-flag'; import useSettingGroup from '../../../../hooks/use-setting-group'; import validator from 'validator'; -import {Button, ButtonGroup, ConfirmationModal, Form, Icon, LimitModal, PreviewModalContent, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ButtonGroup, ConfirmationModal, Form, Icon, LimitModal, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {Field, FieldContent, FieldDescription, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea} from '@tryghost/shade/components'; import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter'; @@ -21,6 +21,7 @@ import {getSettingValue, getSettingValues} from '@tryghost/admin-x-framework/api import {hasSendingDomain, isManagedEmail, sendingDomain} from '@tryghost/admin-x-framework/api/config'; import {renderReplyToEmail, renderSenderEmail} from '../../../../utils/newsletter-emails'; import {textColorForBackgroundColor} from '@tryghost/color-utils'; +import {toast} from 'sonner'; import {useGlobalData} from '../../../providers/global-data-provider'; const ReplyToEmailField: React.FC<{ @@ -143,10 +144,7 @@ const Sidebar: React.FC<{ try { await editNewsletter({...newsletter, status: 'archived'}); modal?.remove(); - showToast({ - type: 'success', - message: 'Newsletter archived' - }); + toast.success('Newsletter archived'); } catch (e) { handleError(e); } @@ -176,10 +174,7 @@ const Sidebar: React.FC<{ onOk: async (modal) => { await editNewsletter({...newsletter, status: 'active'}); modal?.remove(); - showToast({ - type: 'success', - message: 'Newsletter reactivated' - }); + toast.success('Newsletter reactivated'); } }); } @@ -795,11 +790,7 @@ const NewsletterDetailModalContent: React.FC<{newsletter: Newsletter; onlyOne: b } if (toastMessage) { - showToast({ - icon: 'email', - message: toastMessage, - type: 'info' - }); + toast.info(toastMessage); } }, onSaveError: handleError, 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 f62099f2001..50c9a960ed8 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,8 @@ import validator from 'validator'; import {APIError, ValidationError} from '@tryghost/admin-x-framework/errors'; import {Field, FieldContent, FieldDescription, FieldError, FieldLabel, FieldLegend, FieldSeparator, FieldSet, RadioGroup, RadioGroupItem} from '@tryghost/shade/components'; import {HostLimitError, useLimiter} from '../../../hooks/use-limiter'; -import {Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Modal, TextField} from '@tryghost/admin-x-design-system'; +import {toast} from 'sonner'; import {useAddInvite, useBrowseInvites} from '@tryghost/admin-x-framework/api/invites'; import {useBrowseRoles} from '@tryghost/admin-x-framework/api/roles'; import {useBrowseUsers} from '@tryghost/admin-x-framework/api/users'; @@ -128,11 +129,7 @@ const InviteUserModal = NiceModal.create(() => { setSaveState('saved'); - showToast({ - title: `Invitation sent`, - message: `${email}`, - type: 'success' - }); + toast.success(`Invitation sent`, {description: `${email}`}); modal.remove(); updateRoute('staff?tab=invited'); @@ -157,11 +154,7 @@ const InviteUserModal = NiceModal.create(() => { message = (Check your Mailgun configuration.); } } - showToast({ - title, - message, - type: 'error' - }); + toast.error(title, {description: message}); handleError(e, {withToast: false}); return; } 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 039ca633980..077eed36b91 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 @@ -9,7 +9,7 @@ import useStaffUsers from '../../../hooks/use-staff-users'; import validator from 'validator'; import {APIError} from '@tryghost/admin-x-framework/errors'; import {Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Dropzone, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; -import {ConfirmationModal, Icon, LimitModal, Modal, showToast} from '@tryghost/admin-x-design-system'; +import {ConfirmationModal, Icon, LimitModal, Modal} from '@tryghost/admin-x-design-system'; import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {HostLimitError, useLimiter} from '../../../hooks/use-limiter'; import {ImageUpload, ImageUploadAction, ImageUploadActions, ImageUploadDropzone, ImageUploadImage, ImageUploadPreview} from '@tryghost/shade/patterns'; @@ -19,6 +19,7 @@ import {SOCIAL_PLATFORM_CONFIGS, SOCIAL_PLATFORM_KEYS, getSocialValidationError} 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'; +import {toast} from 'sonner'; import {useGlobalData} from '../../providers/global-data-provider'; const validators: Record) => string> = { @@ -70,7 +71,7 @@ export interface UserDetailProps { clearError: (key: keyof User) => void; } -const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { +const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDeleting: boolean) => void}> = ({user, onDeletingUserChange}) => { const {updateRoute, route} = useRouting(); const getTabFromPath = (path: string): string => { @@ -202,10 +203,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { await updateUser(updatedUserData); setFormState(() => updatedUserData); modal?.remove(); - showToast({ - title: _user.status === 'inactive' ? 'User un-suspended' : 'User suspended', - type: 'success' - }); + toast.success(_user.status === 'inactive' ? 'User un-suspended' : 'User suspended'); } catch (e) { handleError(e); } @@ -225,16 +223,16 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { okLabel: 'Delete user', okColor: 'red', onOk: async (modal) => { + onDeletingUserChange(true); try { await deleteUser(_user?.id); modal?.remove(); mainModal?.remove(); navigateOnClose(); - showToast({ - title: 'User deleted', - type: 'success' - }); + // Let the destination route mount its toaster before publishing the success state. + setTimeout(() => toast.success('User deleted'), 100); } catch (e) { + onDeletingUserChange(false); handleError(e); } } @@ -251,10 +249,7 @@ const UserDetailModalContent: React.FC<{user: User}> = ({user}) => { try { await makeOwner(user.id); modal?.remove(); - showToast({ - title: 'Ownership transferred', - type: 'success' - }); + toast.success('Ownership transferred'); } catch (e) { handleError(e); } @@ -463,6 +458,7 @@ const UserDetailModal: React.FC = ({params}) => { const {currentUser} = useGlobalData(); const {updateRoute} = useRouting(); const handleError = useHandleError(); + const [isDeletingUser, setIsDeletingUser] = useState(false); // Skip API call if it's the current user (we already have their data) const isCurrentUser = currentUser.slug === params?.slug; @@ -502,15 +498,12 @@ const UserDetailModal: React.FC = ({params}) => { const notFoundHandledRef = useRef(null); useEffect(() => { - if (!notFoundSlug || notFoundHandledRef.current === notFoundSlug) { + if (!notFoundSlug || isDeletingUser || notFoundHandledRef.current === notFoundSlug) { return; } notFoundHandledRef.current = notFoundSlug; - showToast({ - type: 'error', - message: 'User not found' - }); + toast.error('User not found'); if (canAccessSettings(currentUser)) { // Replace the history entry so the back button doesn't return @@ -519,9 +512,9 @@ const UserDetailModal: React.FC = ({params}) => { } else { updateRoute({isExternal: true, route: ''}); } - }, [notFoundSlug, currentUser, updateRoute]); + }, [notFoundSlug, isDeletingUser, currentUser, updateRoute]); - return displayUser ? : null; + return displayUser ? : null; }; export default NiceModal.create(UserDetailModal); diff --git a/apps/admin-x-settings/src/components/settings/general/users.tsx b/apps/admin-x-settings/src/components/settings/general/users.tsx index 39a08978ef6..f7d09c59515 100644 --- a/apps/admin-x-settings/src/components/settings/general/users.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users.tsx @@ -4,12 +4,13 @@ import clsx from 'clsx'; import useQueryParams from '../../../hooks/use-query-params'; import useStaffUsers from '../../../hooks/use-staff-users'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Avatar, NoValueLabel, NoValueLabelIcon, Separator, Switch, Tabs, TabsContent, TabsList, TabsTrigger, TabsTriggerCount} from '@tryghost/shade/components'; -import {Button, showToast} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/admin-x-design-system'; import {type User, hasAdminAccess, isContributorUser, isEditorUser} from '@tryghost/admin-x-framework/api/users'; import {type UserInvite, useAddInvite, useDeleteInvite} from '@tryghost/admin-x-framework/api/invites'; import {UserRoundX} from 'lucide-react'; import {formatNumber} from '@tryghost/shade/utils'; import {getSettingValue, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useGlobalData} from '../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -140,11 +141,7 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { try { setRevokeState('progress'); await deleteInvite(invite.id); - showToast({ - title: `Invitation revoked`, - message: invite.email, - type: 'success' - }); + toast.success(`Invitation revoked`, {description: invite.email}); } catch (e) { handleError(e); } finally { @@ -165,11 +162,7 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { email: invite.email, roleId: invite.role_id }); - showToast({ - title: `Invitation resent`, - message: invite.email, - type: 'success' - }); + toast.success(`Invitation resent`, {description: invite.email}); } catch (e) { handleError(e); } finally { diff --git a/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx b/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx index c2bc437e6e1..0acf3b743a9 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx @@ -1,6 +1,7 @@ -import {Button, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, TextField} from '@tryghost/admin-x-design-system'; import {type User, useUpdatePassword} from '@tryghost/admin-x-framework/api/users'; import {ValidationError} from '@tryghost/admin-x-framework/errors'; +import {toast} from 'sonner'; import {useEffect, useRef, useState} from 'react'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -236,10 +237,7 @@ const ChangePasswordForm: React.FC<{user: User}> = ({user}) => { setSaveState('saved'); } catch (e) { setSaveState(''); - showToast({ - type: 'error', - title: e instanceof ValidationError ? e.message : `Couldn't update password. Please try again.` - }); + toast.error(e instanceof ValidationError ? e.message : `Couldn't update password. Please try again.`); handleError(e, {withToast: false}); } }} 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 2f2ed9a56aa..ed1762b86ce 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 @@ -4,8 +4,9 @@ import JoelWarner from '../../../../assets/images/joel-warner.png'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React from 'react'; import {Avatar, Field, FieldError, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components'; -import {Button, Form, Modal, showToast} from '@tryghost/admin-x-design-system'; +import {Button, Form, Modal} from '@tryghost/admin-x-design-system'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -62,10 +63,7 @@ const TestimonialsModal = NiceModal.create(() => { throw new Error('Something went wrong, please try again later.'); } - showToast({ - message: 'Thank you for your testimonial!', - type: 'success' - }); + toast.success('Thank you for your testimonial!'); updateRoute('explore'); modal.remove(); 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 f9c99cf20e5..3d765b488ef 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 @@ -1,15 +1,15 @@ import PortalFrame from '../../membership/portal/portal-frame'; -import toast from 'react-hot-toast'; import {Button} from '@tryghost/admin-x-design-system'; import {type ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks'; import {Field, FieldContent, FieldDescription, FieldError, FieldLabel, InputGroup, InputGroupAddon, InputGroupInput, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components'; -import {Form, PreviewModalContent, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Form, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {JSONError} from '@tryghost/admin-x-framework/errors'; 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'; import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {getTiersCadences} from '../../../../utils/get-tiers-cadences'; +import {toast} from 'sonner'; import {useAddOffer} from '@tryghost/admin-x-framework/api/offers'; import {useBrowseOffers} from '@tryghost/admin-x-framework/api/offers'; import {useEffect, useMemo, useState} from 'react'; @@ -685,12 +685,8 @@ const AddOfferModal = () => { validate(); const isErrorsEmpty = Object.values(errors).every(error => !error); if (!isErrorsEmpty) { - toast.remove(); - showToast({ - title: 'Can\'t save offer', - type: 'info', - message: 'Make sure you filled all required fields' - }); + toast.dismiss(); + toast.info('Can\'t save offer', {description: 'Make sure you filled all required fields'}); return; } @@ -705,13 +701,9 @@ const AddOfferModal = () => { message = e.data.errors[0].context || e.data.errors[0].message; } - toast.remove(); + toast.dismiss(); if (message) { - showToast({ - title: 'Can\'t save offer', - type: 'error', - message: message || 'Please try again later' - }); + toast.error('Can\'t save offer', {description: message || 'Please try again later'}); } } }} 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 ad748b48b4f..2dd9aeb2aee 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 @@ -1,8 +1,7 @@ import NiceModal from '@ebay/nice-modal-react'; import PortalFrame from '../../membership/portal/portal-frame'; import SettingsBreadcrumbs from '../../settings-breadcrumbs'; -import toast from 'react-hot-toast'; -import {Button, ConfirmationModal, Form, PreviewModalContent, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, Form, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {Field, FieldLabel, Textarea} from '@tryghost/shade/components'; import {JSONError} from '@tryghost/admin-x-framework/errors'; @@ -11,6 +10,7 @@ import {createOfferRedemptionFilterUrl} from './offer-helpers'; 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'; +import {toast} from 'sonner'; import {useEffect, useState} from 'react'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -68,10 +68,7 @@ const Sidebar: React.FC<{ try { await editOffer({...offer, status: 'archived'}); modal?.remove(); - showToast({ - type: 'success', - title: 'Offer archived' - }); + toast.success('Offer archived'); updateRoute('offers/edit'); } catch (e) { handleError(e); @@ -89,10 +86,7 @@ const Sidebar: React.FC<{ try { await editOffer({...offer, status: 'active'}); modal?.remove(); - showToast({ - type: 'success', - title: 'Offer reactivated' - }); + toast.success('Offer reactivated'); updateRoute('offers/edit'); } catch (e) { handleError(e); @@ -297,13 +291,9 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => { message = e.data.errors[0].context || e.data.errors[0].message; } - toast.remove(); + toast.dismiss(); if (message) { - showToast({ - title: 'Can\'t save offer', - type: 'error', - message: 'Please try again later' - }); + toast.error('Can\'t save offer', {description: 'Please try again later'}); } } }} /> : null; 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 d612739d5d8..0106f7ca3c3 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 @@ -1,15 +1,15 @@ import PortalFrame from '../../membership/portal/portal-frame'; import SettingsBreadcrumbs from '../../settings-breadcrumbs'; -import toast from 'react-hot-toast'; import {type ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks'; import {Field, FieldContent, FieldDescription, FieldLabel, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Switch, Textarea} from '@tryghost/shade/components'; -import {Form, PreviewModalContent, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Form, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {JSONError} from '@tryghost/admin-x-framework/errors'; import {type Offer, useAddOffer, useBrowseOffers, useEditOffer, useInvalidateOffers} from '@tryghost/admin-x-framework/api/offers'; import {createOfferRedemptionsFilterUrl, formatOfferTimestamp, generateRetentionOfferName} from './offer-helpers'; import {formatNumber} from '@tryghost/shade/utils'; import {getOfferPortalPreviewUrl, type offerPortalPreviewUrlTypes} from '../../../../utils/get-offers-portal-preview-url'; import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; +import {toast} from 'sonner'; import {useEffect, useMemo, useState} from 'react'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -431,12 +431,8 @@ const EditRetentionOfferModal: React.FC<{id: string}> = ({id}) => { message = error.data.errors[0].context || error.data.errors[0].message || message; } - toast.remove(); - showToast({ - title: 'Failed to save offer', - type: 'error', - message - }); + toast.dismiss(); + toast.error('Failed to save offer', {description: message}); }; const {formState, setFormState, updateForm, handleSave, saveState, okProps, errors, clearError} = useForm({ 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 2563e9af7f8..fa9a9f18098 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,5 +1,5 @@ import {Badge, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components'; -import {Button, type ButtonProps, showToast} from '@tryghost/admin-x-design-system'; +import {Button, type ButtonProps} from '@tryghost/admin-x-design-system'; import {ButtonGroup} from '@tryghost/admin-x-design-system'; import {Icon} from '@tryghost/admin-x-design-system'; import {Inline, Stack} from '@tryghost/shade/primitives'; @@ -10,6 +10,7 @@ import {type RetentionOffer, getRetentionOffers} from './offers-retention'; import {type Tier, getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {createOfferRedemptionFilterUrl, createOfferRedemptionsFilterUrl} from './offer-helpers'; import {currencyToDecimal, getSymbol} from '../../../../utils/currency'; +import {toast} from 'sonner'; import {useModal} from '@ebay/nice-modal-react'; import {useOffersShowArchived, useSortingState} from '../../../providers/settings-app-provider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -343,10 +344,7 @@ export const OffersIndexModal: React.FC = () => { color: 'green' as const, onClick: () => { if (paidActiveTiers.length === 0) { - showToast({ - type: 'info', - title: 'You must have an active tier to create an offer.' - }); + toast.info('You must have an active tier to create an offer.'); } else { updateRoute('offers/new'); } 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 48e7e8fe0c9..0ae5325aef4 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 @@ -4,7 +4,8 @@ import React from 'react'; import RecommendationDescriptionForm, {validateDescriptionForm} from './recommendation-description-form'; import trackEvent from '../../../../utils/analytics'; import {type EditOrAddRecommendation, useAddRecommendation} from '@tryghost/admin-x-framework/api/recommendations'; -import {Modal, dismissAllToasts, showToast} from '@tryghost/admin-x-design-system'; +import {Modal} from '@tryghost/admin-x-design-system'; +import {toast} from 'sonner'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -26,10 +27,7 @@ const AddRecommendationModalConfirm: React.FC = ({r onSave: async (state) => { await addRecommendation(state); modal.remove(); - showToast({ - title: 'Recommendation added', - type: 'success' - }); + toast.success('Recommendation added'); trackEvent('Recommendation Added', { oneClickSubscribe: state.one_click_subscribe }); @@ -106,14 +104,11 @@ const AddRecommendationModalConfirm: React.FC = ({r return; } - dismissAllToasts(); + toast.dismiss(); try { await handleSave({force: true}); } catch { - showToast({ - type: 'error', - title: 'Something went wrong when adding this recommendation, please try again.' - }); + toast.error('Something went wrong when adding this recommendation, please try again.'); } }} > 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 315ad5f0818..ee30dd23555 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 @@ -4,10 +4,11 @@ import React, {useEffect, useState} from 'react'; 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 {Form, Modal, TextField, dismissAllToasts, showToast} from '@tryghost/admin-x-design-system'; +import {Form, Modal, TextField} from '@tryghost/admin-x-design-system'; import {LoadingIndicator} from '@tryghost/shade/components'; import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; import {formatUrl} from '../../../../utils/format-url'; +import {toast} from 'sonner'; interface AddRecommendationModalProps { recommendation?: EditOrAddRecommendation, @@ -124,17 +125,14 @@ const AddRecommendationModal: React.FC { - dismissAllToasts(); + toast.dismiss(); try { await handleSave({force: true}); } catch { - showToast({ - title: 'Something went wrong', - type: 'error', - message: 'Please try again later.' - }); + toast.error('Something went wrong', {description: 'Please try again later.'}); } }} > diff --git a/apps/admin-x-settings/src/components/settings/membership/access.tsx b/apps/admin-x-settings/src/components/settings/membership/access.tsx index 3b5c933f19c..418eab743f4 100644 --- a/apps/admin-x-settings/src/components/settings/membership/access.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/access.tsx @@ -3,8 +3,9 @@ import TopLevelGroup from '../../top-level-group'; import useSettingGroup from '../../../hooks/use-setting-group'; import {Banner, Combobox, ComboboxContent, ComboboxTrigger, ComboboxValue, Field, FieldDescription, FieldError, FieldLabel, InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, MultiSelectCombobox, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Button as ShadeButton} from '@tryghost/shade/components'; import {RefreshCw} from 'lucide-react'; -import {SettingGroupContent, showToast} from '@tryghost/admin-x-design-system'; +import {SettingGroupContent} from '@tryghost/admin-x-design-system'; import {getSettingValues, isSettingReadOnly, useRegenerateAccessCode} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {useGlobalData} from '../../providers/global-data-provider'; import {useLimiter} from '../../../hooks/use-limiter'; @@ -167,10 +168,7 @@ const Access: React.FC<{ keywords: string[] }> = ({keywords}) => { clearError('password'); } } catch { - showToast({ - type: 'error', - title: 'Could not regenerate access code' - }); + toast.error('Could not regenerate access code'); } finally { setIsRegenerating(false); } 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 08249941548..d994040fbad 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 @@ -1,9 +1,10 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React from 'react'; -import {Button, ConfirmationModal, Form, Icon, Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, FieldDescription, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@tryghost/shade/components'; 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'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import type {MemberCustomField} from '@tryghost/admin-x-framework/api/member-custom-fields'; @@ -100,9 +101,9 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) // and only valid on an already-archived field. await editField({key: field!.key, status: 'archived'}); archiveModal?.remove(); - showToast({type: 'success', title: 'Custom field archived'}); + toast.success('Custom field archived'); } catch (e) { - showToast({type: 'error', title: 'Failed to archive the custom field'}); + toast.error('Failed to archive the custom field'); handleError(e, {withToast: false}); } } @@ -128,9 +129,9 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) try { await editField({key: field!.key, status: 'active'}); reactivateModal?.remove(); - showToast({type: 'success', title: 'Custom field reactivated'}); + toast.success('Custom field reactivated'); } catch (e) { - showToast({type: 'error', title: 'Failed to reactivate the custom field'}); + toast.error('Failed to reactivate the custom field'); handleError(e, {withToast: false}); } } @@ -158,9 +159,9 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) try { await deleteField(field!.key); deleteModal?.remove(); - showToast({type: 'success', title: 'Custom field deleted'}); + toast.success('Custom field deleted'); } catch (e) { - showToast({type: 'error', title: 'Failed to delete the custom field'}); + toast.error('Failed to delete the custom field'); handleError(e, {withToast: false}); } } diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx index ca7b8defbde..90f0bbb3d70 100644 --- a/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/member-emails.tsx @@ -6,9 +6,10 @@ import WelcomeEmailModal from './member-emails/welcome-email-modal'; import useQueryParams from '../../../hooks/use-query-params'; import {APIError} from '@tryghost/admin-x-framework/errors'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Switch} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, Icon, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, Icon} from '@tryghost/admin-x-design-system'; import {WELCOME_EMAIL_SLUGS, type WelcomeEmailType, getDefaultWelcomeEmailRecord, getDefaultWelcomeEmailValues} from './member-emails/default-welcome-email-values'; import {checkStripeEnabled, getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useAddAutomatedEmail, useBrowseAutomatedEmails, useEditAutomatedEmail, useVerifyAutomatedEmailSender} from '@tryghost/admin-x-framework/api/automated-emails'; import {useGlobalData} from '../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -236,13 +237,13 @@ const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => { try { if (!existing) { await createAutomatedEmail(emailType, 'active'); - showToast({type: 'success', title: `${label} welcome email enabled`}); + toast.success(`${label} welcome email enabled`); } else if (existing.status === 'active') { await editAutomatedEmail({...existing, status: 'inactive'}); - showToast({type: 'success', title: `${label} welcome email disabled`}); + toast.success(`${label} welcome email disabled`); } else { await editAutomatedEmail({...existing, status: 'active'}); - showToast({type: 'success', title: `${label} welcome email enabled`}); + toast.success(`${label} welcome email enabled`); } } catch (e) { handleError(e); 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 bd8ae6c840a..e81dba1c04b 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 @@ -7,14 +7,14 @@ import StripeButton from '../../../stripe-button'; import StripeLogo from '../../../../assets/images/stripe-emblem.svg'; import StripeVerifiedBadge from '../../../../assets/images/stripe-verified.svg'; import useSettingGroup from '../../../../hooks/use-setting-group'; -import {Button, ConfirmationModal, Form, LimitModal, Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, Form, LimitModal, Modal, TextField} from '@tryghost/admin-x-design-system'; import {Field, FieldError, FieldLabel, Switch, Textarea} from '@tryghost/shade/components'; import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter'; import {JSONError} from '@tryghost/admin-x-framework/errors'; 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'; -import {toast} from 'react-hot-toast'; +import {toast} from 'sonner'; import {useBrowseMembers} from '@tryghost/admin-x-framework/api/members'; import {useBrowseTiers, useEditTier} from '@tryghost/admin-x-framework/api/tiers'; import {useGlobalData} from '../../../providers/global-data-provider'; @@ -221,16 +221,12 @@ const Direct: React.FC<{onClose: () => void}> = ({onClose}) => { const onSubmit = async () => { try { - toast.remove(); + toast.dismiss(); await handleSave(); onClose(); } catch (e) { if (e instanceof JSONError) { - showToast({ - title: 'Failed to save settings', - type: 'error', - message: 'Check you copied both keys correctly' - }); + toast.error('Failed to save settings', {description: 'Check you copied both keys correctly'}); return; } 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 f5eceb8e699..0d996984389 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 @@ -5,7 +5,7 @@ import useCurrencyInput from '../../../../hooks/use-currency-input'; import useSettingGroup from '../../../../hooks/use-setting-group'; import useSortableIndexedList from '../../../../hooks/use-sortable-indexed-list'; import useUrlInput from '../../../../hooks/use-url-input'; -import {Button, type ButtonProps, ConfirmationModal, Form, Icon, Modal, TextField, showToast} from '@tryghost/admin-x-design-system'; +import {Button, type ButtonProps, ConfirmationModal, Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; import {Combobox, ComboboxContent, ComboboxTrigger, ComboboxValue, Field, FieldDescription, FieldError, FieldLabel, Input, InputGroup, InputGroupAddon, InputGroupInput, InputGroupText, MultiSelectCombobox, SortableList, Switch} from '@tryghost/shade/components'; import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; @@ -13,6 +13,7 @@ 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'; import {getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; export type TierFormState = Partial> & { trial_days: string; @@ -161,10 +162,7 @@ const TierDetailModalContent: React.FC<{tier?: Tier}> = ({tier}) => { onOk: (confirmModal) => { updateTier({...tier, active: !tier.active}); confirmModal?.remove(); - showToast({ - type: 'success', - title: `Tier ${tier.active ? 'archived' : 'reactivated'}` - }); + toast.success(`Tier ${tier.active ? 'archived' : 'reactivated'}`); } }); } 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 ad2188e90cf..de14be5fe71 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 @@ -5,11 +5,12 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {useRef, useState} from 'react'; import useSettingGroup from '../../../hooks/use-setting-group'; import {Checkbox, Field, FieldGroup, FieldLabel, FieldLegend, FieldSet, Tabs, TabsList, TabsTrigger, ToggleGroup, ToggleGroupItem} from '@tryghost/shade/components'; -import {DesktopChrome, Form, MobileChrome, PreviewModalContent, showToast} from '@tryghost/admin-x-design-system'; +import {DesktopChrome, Form, MobileChrome, PreviewModalContent} from '@tryghost/admin-x-design-system'; import {Laptop, Smartphone} from 'lucide-react'; import {debounce} from '../../../utils/debounce'; import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; +import {toast} from 'sonner'; import {useBrowsePosts} from '@tryghost/admin-x-framework/api/posts'; import {useGlobalData} from '../../providers/global-data-provider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -237,10 +238,7 @@ const AnnouncementBarModal: React.FC = () => { titleHeadingLevel={5} onOk={async () => { if (!(await handleSave({fakeWhenUnchanged: true}))) { - showToast({ - type: 'error', - message: 'An error occurred while saving your changes. Please try again.' - }); + toast.error('An error occurred while saving your changes. Please try again.'); } }} />; 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 44b4008af43..0ca043aab16 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 @@ -5,11 +5,12 @@ import OfficialThemes from './theme/official-themes'; import React, {useEffect, useState} from 'react'; import ThemeInstalledModal from './theme/theme-installed-modal'; import ThemePreview from './theme/theme-preview'; -import {Button, ConfirmationModal, LimitModal, Modal, PageHeader, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, LimitModal, Modal, PageHeader} from '@tryghost/admin-x-design-system'; import {Dropzone, 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 {type OfficialTheme} from '../../providers/settings-app-provider'; +import {toast} from 'sonner'; import {useCheckThemeLimitError} from '../../../hooks/use-check-theme-limit-error'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -369,11 +370,7 @@ const ChangeThemeModal: React.FC = ({source, themeRef}) = data = await installTheme(themeRef); if (data?.themes[0]) { await activateTheme(data.themes[0].name); - showToast({ - title: 'Theme activated', - type: 'success', - message:
{data.themes[0].name} is now your active theme
- }); + toast.success('Theme activated', {description:
{data.themes[0].name} is now your active theme
}); } confirmModal?.remove(); updateRoute(''); diff --git a/apps/admin-x-settings/src/components/settings/site/theme/advanced-theme-settings.tsx b/apps/admin-x-settings/src/components/settings/site/theme/advanced-theme-settings.tsx index 6a944b2899e..7d19f34878f 100644 --- a/apps/admin-x-settings/src/components/settings/site/theme/advanced-theme-settings.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/advanced-theme-settings.tsx @@ -3,11 +3,12 @@ import NiceModal from '@ebay/nice-modal-react'; import React from 'react'; import useCustomFonts from '../../../../hooks/use-custom-fonts'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, LimitModal, ModalPage, showToast} from '@tryghost/admin-x-design-system'; +import {Button, ConfirmationModal, LimitModal, ModalPage} from '@tryghost/admin-x-design-system'; import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger} from '@tryghost/shade/components'; import {JSONError} from '@tryghost/admin-x-framework/errors'; import {type Theme, isActiveTheme, isDefaultTheme, isDeletableTheme, isLegacyTheme, useActivateTheme, useDeleteTheme} from '@tryghost/admin-x-framework/api/themes'; import {downloadFile, getGhostPaths} from '@tryghost/admin-x-framework/helpers'; +import {toast} from 'sonner'; import {useCheckThemeLimitError} from '../../../../hooks/use-check-theme-limit-error'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -62,11 +63,7 @@ const ThemeActions: React.FC = ({ try { await activateTheme(theme.name); refreshActiveThemeData(); - showToast({ - title: 'Theme activated', - type: 'success', - message:
{theme.name} is now your active theme
- }); + toast.success('Theme activated', {description:
{theme.name} is now your active theme
}); } catch (e) { let fatalErrors: FatalErrors | null = null; if (e instanceof JSONError && e.response?.status === 422 && e.data?.errors) { diff --git a/apps/admin-x-settings/src/components/settings/site/theme/theme-code-editor-modal.tsx b/apps/admin-x-settings/src/components/settings/site/theme/theme-code-editor-modal.tsx index fa0034900aa..2a179ba8dbf 100644 --- a/apps/admin-x-settings/src/components/settings/site/theme/theme-code-editor-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/theme-code-editor-modal.tsx @@ -22,7 +22,8 @@ import { import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {oneDark} from '@codemirror/theme-one-dark'; import {search} from '@codemirror/search'; -import {showToast} from '@tryghost/admin-x-design-system'; + +import {toast} from 'sonner'; import {useBrowseThemes} from '@tryghost/admin-x-framework/api/themes'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useQueryClient} from '@tanstack/react-query'; @@ -500,19 +501,12 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { } if (!isEditablePath(nextPath)) { - showToast({ - type: 'error', - title: 'Only text files can be created here', - message: 'Use a text-based theme file extension such as .hbs, .css, .js, or .json.' - }); + toast.error('Only text files can be created here', {description: 'Use a text-based theme file extension such as .hbs, .css, .js, or .json.'}); return; } if (currentFiles[nextPath]) { - showToast({ - type: 'error', - title: 'File already exists' - }); + toast.error('File already exists'); return; } @@ -558,27 +552,17 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { const fileToRename = currentFiles[selectedNode.path]; if (!isEditablePath(nextPath) && fileToRename.editable) { - showToast({ - type: 'error', - title: 'Text files must keep a text file extension' - }); + toast.error('Text files must keep a text file extension'); return; } if (wouldRenameBinaryFileToEditable(fileToRename, nextPath)) { - showToast({ - type: 'error', - title: 'Binary files cannot be renamed to a text file', - message: 'Rename this file with a non-text extension to keep its contents intact.' - }); + toast.error('Binary files cannot be renamed to a text file', {description: 'Rename this file with a non-text extension to keep its contents intact.'}); return; } if (currentFiles[nextPath]) { - showToast({ - type: 'error', - title: 'A file with that name already exists' - }); + toast.error('A file with that name already exists'); return; } @@ -598,20 +582,14 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { const nextDirectoryPath = `${nextPath}/`; if (nextDirectoryPath.startsWith(selectedNode.path)) { - showToast({ - type: 'error', - title: 'A folder cannot be renamed inside itself' - }); + toast.error('A folder cannot be renamed inside itself'); return; } const conflictingPath = Object.keys(currentFiles).find(path => path.startsWith(nextDirectoryPath)); if (conflictingPath) { - showToast({ - type: 'error', - title: 'A folder with that path already exists' - }); + toast.error('A folder with that path already exists'); return; } @@ -719,19 +697,12 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { } if (!THEME_NAME_PATTERN.test(nextName)) { - showToast({ - type: 'error', - title: 'Invalid theme name', - message: 'Use 1-64 characters, starting with a letter or number. Allowed: letters, numbers, dashes, and underscores.' - }); + toast.error('Invalid theme name', {description: 'Use 1-64 characters, starting with a letter or number. Allowed: letters, numbers, dashes, and underscores.'}); return null; } if (isDefaultThemeName(nextName)) { - showToast({ - type: 'error', - title: 'Built-in themes cannot be overwritten' - }); + toast.error('Built-in themes cannot be overwritten'); return null; } @@ -744,10 +715,7 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { } if (changes.length === 0) { - showToast({ - type: 'info', - title: 'No changes to save' - }); + toast.info('No changes to save'); return; } @@ -821,11 +789,7 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { const serverError = (data as {errors?: Array} | null)?.errors?.[0]; if (serverError?.code && UPLOAD_SIZE_LIMIT_TITLES[serverError.code]) { - showToast({ - type: 'error', - title: UPLOAD_SIZE_LIMIT_TITLES[serverError.code], - message: buildUploadSizeLimitMessage(serverError) - }); + toast.error(UPLOAD_SIZE_LIMIT_TITLES[serverError.code], {description: buildUploadSizeLimitMessage(serverError)}); return; } @@ -852,11 +816,7 @@ const ThemeCodeEditorModal: React.FC<{themeName: string}> = ({themeName}) => { installedTheme: uploadedTheme }); } else { - showToast({ - type: 'success', - title: 'Theme saved', - message:
{uploadedTheme.name} has been updated.
- }); + toast.success('Theme saved', {description:
{uploadedTheme.name} has been updated.
}); } } catch (error) { handleError(error); diff --git a/apps/admin-x-settings/src/components/settings/site/theme/theme-installed-modal.tsx b/apps/admin-x-settings/src/components/settings/site/theme/theme-installed-modal.tsx index 36ec458e786..49970d66d49 100644 --- a/apps/admin-x-settings/src/components/settings/site/theme/theme-installed-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/site/theme/theme-installed-modal.tsx @@ -1,10 +1,11 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {type ReactNode} from 'react'; import useCustomFonts from '../../../../hooks/use-custom-fonts'; -import {ConfirmationModalContent, showToast} from '@tryghost/admin-x-design-system'; +import {ConfirmationModalContent} from '@tryghost/admin-x-design-system'; import {type InstalledTheme, useActivateTheme} from '@tryghost/admin-x-framework/api/themes'; import {OutcomeBanner, ThemeValidationDetailsDisclosure, getIssuesFromInstalledTheme} from './theme-validation-details'; import {getHomepageUrl, useBrowseSite} from '@tryghost/admin-x-framework/api/site'; +import {toast} from 'sonner'; import {useBrowseConfig} from '@tryghost/admin-x-framework/api/config'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -79,11 +80,7 @@ const ThemeInstalledModal: React.FC<{ const updatedTheme = resData.themes[0]; refreshActiveThemeData(); - showToast({ - title: 'Theme activated', - type: 'success', - message:
{updatedTheme.name} is now your active theme.
- }); + toast.success('Theme activated', {description:
{updatedTheme.name} is now your active theme.
}); } catch (e) { handleError(e); } diff --git a/apps/admin-x-settings/src/main-content.tsx b/apps/admin-x-settings/src/main-content.tsx index b56e02d19c4..503e63e23e9 100644 --- a/apps/admin-x-settings/src/main-content.tsx +++ b/apps/admin-x-settings/src/main-content.tsx @@ -6,7 +6,7 @@ import {DirtyConfirmDialog, 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 'react-hot-toast'; +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'; @@ -45,6 +45,11 @@ const MainContent: React.FC = () => { return Boolean(document.querySelector(OPEN_SHADE_MODAL_SELECTOR)); }; + useEffect(() => { + // Reset any toasts that may have been left open before entering Settings. + toast.dismiss(); + }, []); + useEffect(() => { const handleKeyDown = (event: KeyboardEvent) => { if (event.key === 'Escape') { @@ -66,11 +71,6 @@ const MainContent: React.FC = () => { }; }, [confirm, isDirty]); - useEffect(() => { - // resets any toasts that may have been left open on initial load - toast.remove(); - }, []); - // Contributors/Authors only see their profile modal (rendered via routing) // Don't render the main settings content for them if (!canAccessSettings(currentUser)) { diff --git a/apps/admin/src/settings/general/staff-actions.acceptance.test.tsx b/apps/admin/src/settings/general/staff-actions.acceptance.test.tsx index 0a10e4ffefb..1c344726c8e 100644 --- a/apps/admin/src/settings/general/staff-actions.acceptance.test.tsx +++ b/apps/admin/src/settings/general/staff-actions.acceptance.test.tsx @@ -50,6 +50,7 @@ describe("Staff actions", () => { await settingsScreen.confirmationModal().getByRole("button", {name: "Delete user"}).click(); await expect.element(settingsScreen.successToast()).toHaveTextContent("User deleted"); + await expect(settingsScreen.notification("User not found")).toHaveCount(0); await expect(settingsScreen.userDetailModal()).toHaveCount(0); await expect(settingsScreen.users().getByText(author.email, {exact: true})).toHaveCount(0); expect(deleteApi.requests).toHaveLength(1); diff --git a/apps/admin/src/settings/offers.screen.ts b/apps/admin/src/settings/offers.screen.ts index 3d5b46400c4..dd91ae2f75b 100644 --- a/apps/admin/src/settings/offers.screen.ts +++ b/apps/admin/src/settings/offers.screen.ts @@ -27,7 +27,7 @@ export const offersScreen = { selectOptions: () => page.getByRole("option"), selectOption: (label: string) => page.getByRole("option").filter({ hasText: label }), portalPreview: () => page.getByTestId(testIds.portalPreview), - errorToast: () => page.getByTestId(testIds.toastError), + errorToast: () => page.getByRole("region", { name: /Notifications/ }).getByRole("listitem"), /** Open the offers list modal from the Growth section's button. */ async openListModal(): Promise { diff --git a/apps/admin/src/settings/settings.screen.ts b/apps/admin/src/settings/settings.screen.ts index 9692e3c7caf..0ef66f770cb 100644 --- a/apps/admin/src/settings/settings.screen.ts +++ b/apps/admin/src/settings/settings.screen.ts @@ -1,6 +1,8 @@ import { page } from "vitest/browser"; import * as sel from "@tryghost/test-data/selectors/settings"; +const toast = () => page.getByRole("region", { name: /Notifications/ }).getByRole("listitem"); + /** Settings locators and gestures shared by the acceptance batches; no assertions. */ export const settingsScreen = { section: (testId: string) => page.getByTestId(testId), @@ -20,8 +22,8 @@ export const settingsScreen = { seoTabView: () => page.getByTestId(sel.seoTabView), selectOption: (name: string) => page.getByRole("option").filter({ hasText: name }), selectOptionExact: (name: string) => page.getByRole("option", { name: new RegExp(`^${name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}(?:\\s|$)`) }), - errorToast: () => page.getByTestId(sel.toastError), - successToast: () => page.getByTestId(sel.toastSuccess), + errorToast: toast, + successToast: toast, inviteUserModal: () => page.getByTestId(sel.inviteUserModal), limitModal: () => page.getByTestId(sel.limitModal), enableNewsletters: () => page.getByTestId(sel.enableNewsletters), @@ -33,7 +35,8 @@ export const settingsScreen = { mailgun: () => page.getByTestId(sel.mailgun), addNewsletterModal: () => page.getByTestId(sel.addNewsletterModal), newsletterModal: () => page.getByTestId(sel.newsletterModal), - infoToast: () => page.getByTestId(sel.toastInfo), + infoToast: toast, + notification: (text: string) => page.getByRole("region", { name: /Notifications/ }).getByText(text, { exact: true }), access: () => page.getByTestId(sel.access), customFields: () => page.getByTestId(sel.customFields), customFieldModal: () => page.getByTestId(sel.customFieldModal), diff --git a/apps/shade/src/components/patterns/dirty-confirm-dialog.tsx b/apps/shade/src/components/patterns/dirty-confirm-dialog.tsx index f7db4db99b8..4b48ea5159f 100644 --- a/apps/shade/src/components/patterns/dirty-confirm-dialog.tsx +++ b/apps/shade/src/components/patterns/dirty-confirm-dialog.tsx @@ -101,7 +101,7 @@ export function DirtyConfirmDialog({ - + diff --git a/apps/shade/src/providers/shade-provider.tsx b/apps/shade/src/providers/shade-provider.tsx index 786f1753b12..ed10d88cfd8 100644 --- a/apps/shade/src/providers/shade-provider.tsx +++ b/apps/shade/src/providers/shade-provider.tsx @@ -42,6 +42,7 @@ const ToasterPortal = () => { ? createPortal(
, success: , @@ -59,6 +60,7 @@ const ToasterPortal = () => { maxWidth: '290px' } }} + closeButton />
, document.body diff --git a/apps/shade/test/unit/components/patterns/dirty-confirm-dialog.test.tsx b/apps/shade/test/unit/components/patterns/dirty-confirm-dialog.test.tsx index bfc722dba6b..0abba392790 100644 --- a/apps/shade/test/unit/components/patterns/dirty-confirm-dialog.test.tsx +++ b/apps/shade/test/unit/components/patterns/dirty-confirm-dialog.test.tsx @@ -43,7 +43,12 @@ describe('DirtyConfirmDialog', () => { render(); fireEvent.click(screen.getByRole('button', {name: 'Navigate away'})); - fireEvent.click(screen.getByRole('button', {name: 'Leave'})); + const leaveButton = screen.getByRole('button', {name: 'Leave'}); + + assert.match(leaveButton.className, /bg-destructive/); + assert.doesNotMatch(leaveButton.className, /bg-primary/); + + fireEvent.click(leaveButton); assert.equal(onLeave.mock.calls.length, 1); assert.equal(screen.queryByRole('alertdialog'), null); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3cdd7c86ef1..1c64fe6c643 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -339,9 +339,6 @@ catalogs: react-dom: specifier: 18.3.1 version: 18.3.1 - react-hot-toast: - specifier: 2.6.0 - version: 2.6.0 react-router: specifier: 7.18.1 version: 7.18.1 @@ -865,9 +862,6 @@ importers: clsx: specifier: 'catalog:' version: 2.1.1 - react-hot-toast: - specifier: 'catalog:' - version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@internal/cfg-eslint-react': specifier: workspace:* @@ -989,12 +983,12 @@ importers: react-dom: specifier: 'catalog:' version: 18.3.1(react@18.3.1) - react-hot-toast: - specifier: 'catalog:' - version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-router: specifier: 'catalog:' version: 7.18.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sonner: + specifier: 'catalog:' + version: 2.0.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@internal/cfg-eslint-react': specifier: workspace:* @@ -1134,9 +1128,6 @@ importers: react-dom: specifier: 'catalog:' version: 18.3.1(react@18.3.1) - react-hot-toast: - specifier: 'catalog:' - version: 2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) semver: specifier: 'catalog:' version: 7.8.5 @@ -15187,11 +15178,6 @@ packages: globrex@0.1.2: resolution: {integrity: sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==} - goober@2.1.19: - resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} - peerDependencies: - csstype: ^3.0.10 - google-caja-bower@https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd: resolution: {gitHosted: true, integrity: sha512-mmCXdxGKGKDznjgkNzVqzTslaldslk5KMb/A7l8rxWnqyxzwsdPhuBJ6oT1Kh/Y3k4jN54ISee/2AgjFyCBxYw==, tarball: https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd} version: 6011.0.0 @@ -19909,13 +19895,6 @@ packages: peerDependencies: react: ^16.8.0 || ^17 || ^18 || ^19 - react-hot-toast@2.6.0: - resolution: {integrity: sha512-bH+2EBMZ4sdyou/DPrfgIouFpcRLCJ+HoCA32UoAYHn6T3Ur5yfcDCeSr5mwldl6pFOsiocmrXMuoCJ1vV8bWg==} - engines: {node: '>=10'} - peerDependencies: - react: '>=16' - react-dom: '>=16' - react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} @@ -37542,10 +37521,6 @@ snapshots: globrex@0.1.2: {} - goober@2.1.19(csstype@3.2.3): - dependencies: - csstype: 3.2.3 - google-caja-bower@https://codeload.github.com/acburdine/google-caja-bower/tar.gz/275cb75249f038492094a499756a73719ae071fd: {} gopd@1.2.0: {} @@ -43467,13 +43442,6 @@ snapshots: dependencies: react: 18.3.1 - react-hot-toast@2.6.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): - dependencies: - csstype: 3.2.3 - goober: 2.1.19(csstype@3.2.3) - react: 18.3.1 - react-dom: 18.3.1(react@18.3.1) - react-is@16.13.1: {} react-is@17.0.2: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 80d8c324126..381b90715f6 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -148,7 +148,6 @@ catalog: preact: ^10.29.2 react: 18.3.1 react-dom: 18.3.1 - react-hot-toast: 2.6.0 react-router: 7.18.1 semver: 7.8.5 sinon: 22.0.0 From 46d469f38750073a11920d0156287c04be80acd7 Mon Sep 17 00:00:00 2001 From: "tryghost-renovate[bot]" <269725441+tryghost-renovate[bot]@users.noreply.github.com> Date: Thu, 23 Jul 2026 00:57:22 +0000 Subject: [PATCH 2/6] Update tryghost/actions digest to eee499f (#29469) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR contains the following updates: | Package | Type | Update | Change | |---|---|---|---| | [tryghost/actions](https://redirect.github.com/tryghost/actions) ([changelog](https://redirect.github.com/tryghost/actions/compare/3ab34d3d69687764be21eba3768f51f4ecc40ddd..eee499fcb6bc0efc75ba43f651282b95a96102b0)) | action | digest | `3ab34d3` → `eee499f` | --- ### Configuration 📅 **Schedule**: (in timezone Etc/UTC) - Branch creation - Only on Sunday and Saturday (`* * * * 0,6`) - Between 11:00 PM and 11:59 PM, Monday through Friday (`* 23 * * 1-5`) - Between 12:00 AM and 04:59 AM, Monday through Saturday (`* 0-4 * * 1-6`) - Automerge - Only on Sunday and Saturday (`* * * * 0,6`) - Between 11:00 PM and 11:59 PM, Monday through Friday (`* 23 * * 1-5`) - Between 12:00 AM and 04:59 AM, Monday through Saturday (`* 0-4 * * 1-6`) 🚦 **Automerge**: Enabled. ♻ **Rebasing**: Whenever PR is behind base branch, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://redirect.github.com/renovatebot/renovate). Co-authored-by: tryghost-renovate[bot] <269725441+tryghost-renovate[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 20 ++++++++++---------- .github/workflows/label-actions.yml | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a247227c79d..19ed61b0519 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -372,7 +372,7 @@ jobs: - name: Lint boundaries run: pnpm nx run ghost-monorepo:lint:boundaries - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -438,7 +438,7 @@ jobs: name: admin-coverage path: apps/*/coverage/cobertura-coverage.xml - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -515,7 +515,7 @@ jobs: exit 1 fi - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -664,7 +664,7 @@ jobs: ghost/*/coverage-e2e/cobertura-coverage.xml ghost/*/coverage-integration/cobertura-coverage.xml - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -754,7 +754,7 @@ jobs: exit 1 fi - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -840,7 +840,7 @@ jobs: path: ${{ steps.app_name.outputs.root }}/playwright-report retention-days: 30 - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -902,7 +902,7 @@ jobs: run: | [ -f ~/.ghost/logs/*.log ] && cat ~/.ghost/logs/*.log - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -984,7 +984,7 @@ jobs: retention-days: 7 if-no-files-found: error - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -1067,7 +1067,7 @@ jobs: retention-days: 7 if-no-files-found: error - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} @@ -1673,7 +1673,7 @@ jobs: path: e2e/test-results retention-days: 7 - - uses: tryghost/actions/actions/slack-build@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/slack-build@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main if: failure() && github.event_name == 'push' && github.ref == 'refs/heads/main' with: status: ${{ job.status }} diff --git a/.github/workflows/label-actions.yml b/.github/workflows/label-actions.yml index 047644ecec7..f3ef90bd530 100644 --- a/.github/workflows/label-actions.yml +++ b/.github/workflows/label-actions.yml @@ -18,4 +18,4 @@ jobs: runs-on: ubuntu-slim if: github.repository_owner == 'TryGhost' steps: - - uses: tryghost/actions/actions/label-actions@3ab34d3d69687764be21eba3768f51f4ecc40ddd # main + - uses: tryghost/actions/actions/label-actions@eee499fcb6bc0efc75ba43f651282b95a96102b0 # main From 5ef529f68025fc9aca46a37e7dadd98ffcce2c95 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Wed, 22 Jul 2026 20:07:23 -0500 Subject: [PATCH 3/6] Removed legacy Settings button components (#29548) no ref - migrated Settings button usage and design-system internals to Shade Button primitives - removed the legacy Button, ButtonGroup, and ButtonProps implementation and exports - changed modal/form action contracts to use semantic Shade variants directly - preserved destructive states and accessible action names, and refreshed the Shade adoption snapshot --- .../src/global/button-group.stories.tsx | 61 -- .../src/global/button-group.tsx | 70 --- .../src/global/button.stories.tsx | 92 --- .../src/global/button.tsx | 166 ------ .../chrome/desktop-chrome-header.stories.tsx | 11 +- .../src/global/form/text-field.stories.tsx | 3 +- .../src/global/layout/app-menu.tsx | 7 +- .../src/global/layout/global-actions.tsx | 5 +- .../src/global/layout/page.stories.tsx | 25 +- .../src/global/layout/page.tsx | 6 +- .../global/layout/view-container.stories.tsx | 64 +- .../src/global/layout/view-container.tsx | 22 +- .../modal/confirmation-modal.stories.tsx | 7 +- .../src/global/modal/confirmation-modal.tsx | 8 +- .../src/global/modal/limit-modal.stories.tsx | 5 +- .../src/global/modal/limit-modal.tsx | 2 +- .../src/global/modal/modal.stories.tsx | 12 +- .../src/global/modal/modal.tsx | 67 +-- .../global/modal/preview-modal.stories.tsx | 7 +- .../src/global/modal/preview-modal.tsx | 43 +- .../src/global/tooltip.stories.tsx | 13 +- apps/admin-x-design-system/src/index.ts | 4 - .../settings/setting-group-header.stories.tsx | 21 +- .../src/settings/setting-group-header.tsx | 2 +- .../src/settings/setting-group.stories.tsx | 3 +- .../src/settings/setting-group.tsx | 73 +-- apps/admin-x-framework/src/hooks/use-form.ts | 15 +- .../test/unit/hooks/use-form.test.ts | 8 +- .../src/components/exit-settings-button.tsx | 7 +- .../settings/advanced/code-injection.tsx | 7 +- .../settings/advanced/code/code-modal.tsx | 29 +- .../settings/advanced/danger-zone.tsx | 16 +- .../settings/advanced/history-modal.tsx | 6 +- .../components/settings/advanced/history.tsx | 4 +- .../settings/advanced/integrations.tsx | 26 +- .../integrations/add-integration-modal.tsx | 2 +- .../integrations/content-api-modal.tsx | 9 +- .../integrations/custom-integration-modal.tsx | 2 +- .../integrations/first-promoter-modal.tsx | 2 +- .../advanced/integrations/pintura-modal.tsx | 2 +- .../advanced/integrations/slack-modal.tsx | 7 +- .../integrations/transistor-modal.tsx | 2 +- .../advanced/integrations/unsplash-modal.tsx | 2 +- .../advanced/integrations/webhook-modal.tsx | 2 +- .../advanced/integrations/webhooks-table.tsx | 22 +- .../advanced/integrations/zapier-modal.tsx | 12 +- .../src/components/settings/advanced/labs.tsx | 10 +- .../settings/advanced/labs/beta-features.tsx | 11 +- .../advanced/labs/migration-options.tsx | 12 +- .../advanced/labs/yaml-file-editor-modal.tsx | 22 +- .../migration-tools-export.tsx | 7 +- .../migration-tools-import.tsx | 91 +-- .../universal-import-modal.tsx | 6 +- .../email-design/email-design-modal.tsx | 8 +- .../src/components/settings/email/emails.tsx | 8 +- .../components/settings/email/newsletters.tsx | 20 +- .../newsletters/add-newsletter-modal.tsx | 2 +- .../newsletters/newsletter-detail-modal.tsx | 228 +++---- .../email/newsletters/newsletters-list.tsx | 10 +- .../newsletters/newsletters-tab-content.tsx | 16 +- .../settings/general/invite-user-modal.tsx | 2 +- .../settings/general/user-detail-modal.tsx | 12 +- .../src/components/settings/general/users.tsx | 44 +- .../general/users/change-password-form.tsx | 16 +- .../settings/general/users/staff-token.tsx | 2 +- .../growth/embed-signup/embed-signup-form.tsx | 4 +- .../embed-signup/embed-signup-sidebar.tsx | 32 +- .../components/settings/growth/explore.tsx | 10 +- .../growth/explore/testimonials-modal.tsx | 12 +- .../src/components/settings/growth/offers.tsx | 6 +- .../growth/offers/add-offer-modal.tsx | 7 +- .../growth/offers/edit-offer-modal.tsx | 12 +- .../offers/edit-retention-offer-modal.tsx | 2 +- .../settings/growth/offers/offer-success.tsx | 14 +- .../settings/growth/offers/offers-index.tsx | 37 +- .../settings/growth/recommendations.tsx | 10 +- .../add-recommendation-modal-confirm.tsx | 23 +- .../add-recommendation-modal.tsx | 2 +- .../edit-recommendation-modal.tsx | 18 +- .../incoming-recommendation-list.tsx | 7 +- .../recommendations/recommendation-list.tsx | 24 +- .../settings/membership/custom-fields.tsx | 8 +- .../custom-fields/custom-field-modal.tsx | 44 +- .../settings/membership/member-emails.tsx | 15 +- .../member-emails/test-email-dropdown.tsx | 10 +- .../welcome-email-customize-modal.tsx | 2 +- .../member-emails/welcome-email-modal.tsx | 12 +- .../components/settings/membership/portal.tsx | 4 +- .../membership/portal/portal-links.tsx | 25 +- .../membership/portal/portal-modal.tsx | 2 +- .../stripe/stripe-connect-modal.tsx | 19 +- .../components/settings/membership/tiers.tsx | 25 +- .../membership/tiers/tier-detail-modal.tsx | 43 +- .../membership/tiers/tier-detail-preview.tsx | 7 +- .../settings/site/announcement-bar-modal.tsx | 2 +- .../settings/site/announcement-bar.tsx | 4 +- .../components/settings/site/change-theme.tsx | 8 +- .../components/settings/site/design-modal.tsx | 2 +- .../settings/site/design-setting.tsx | 4 +- .../components/settings/site/navigation.tsx | 4 +- .../site/navigation/navigation-edit-form.tsx | 8 +- .../components/settings/site/theme-modal.tsx | 16 +- .../site/theme/advanced-theme-settings.tsx | 17 +- .../site/theme/invalid-theme-modal.tsx | 2 +- .../site/theme/theme-code-editor-modal.tsx | 12 +- .../site/theme/theme-editor-confirm-modal.tsx | 7 +- .../site/theme/theme-installed-modal.tsx | 2 +- .../settings/site/theme/theme-preview.tsx | 42 +- .../src/components/stripe-button.tsx | 21 +- apps/shade/src/components/ui/button.tsx | 2 +- apps/shade/src/docs/adoption-data.json | 560 +++++++----------- .../test/unit/components/ui/button.test.tsx | 3 +- 112 files changed, 886 insertions(+), 1774 deletions(-) delete mode 100644 apps/admin-x-design-system/src/global/button-group.stories.tsx delete mode 100644 apps/admin-x-design-system/src/global/button-group.tsx delete mode 100644 apps/admin-x-design-system/src/global/button.stories.tsx delete mode 100644 apps/admin-x-design-system/src/global/button.tsx diff --git a/apps/admin-x-design-system/src/global/button-group.stories.tsx b/apps/admin-x-design-system/src/global/button-group.stories.tsx deleted file mode 100644 index 1e65fd52463..00000000000 --- a/apps/admin-x-design-system/src/global/button-group.stories.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import type {Meta, StoryObj} from '@storybook/react-vite'; - -import ButtonGroup from './button-group'; -import {ButtonProps} from './button'; - -const ButtonGroupMeta = { - title: 'Global / Button Group', - component: ButtonGroup, - tags: ['autodocs'], - decorators: [(_story: () => React.ReactNode) => (
{_story()}
)] -} satisfies Meta; - -export default ButtonGroupMeta; - -type Story = StoryObj; - -const defaultButtons: ButtonProps[] = [ - { - label: 'Cancel', - key: 'cancel' - }, - { - label: 'Save', - key: 'save', - color: 'black' - } -]; - -export const Default: Story = { - args: { - buttons: defaultButtons, - link: false - } -}; - -export const Small: Story = { - args: { - buttons: defaultButtons, - link: false, - size: 'sm' - } -}; - -const linkButtons: ButtonProps[] = [ - { - label: 'Cancel', - key: 'cancel' - }, - { - label: 'Save', - key: 'save', - color: 'green' - } -]; - -export const LinkButtons: Story = { - args: { - buttons: linkButtons, - link: true - } -}; diff --git a/apps/admin-x-design-system/src/global/button-group.tsx b/apps/admin-x-design-system/src/global/button-group.tsx deleted file mode 100644 index f72e4b306f3..00000000000 --- a/apps/admin-x-design-system/src/global/button-group.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import React from 'react'; -import Button, {ButtonSize} from './button'; -import Tooltip from './tooltip'; - -import {ButtonProps} from './button'; -import clsx from 'clsx'; - -export interface ButtonGroupButtonProps extends ButtonProps { - tooltip?: string | React.ReactNode; -} - -export interface ButtonGroupProps { - size?: ButtonSize; - buttons: Array; - link?: boolean; - linkWithPadding?: boolean; - clearBg?: boolean; - outlineOnMobile?: boolean; - className?: string; - activeKey?: string; -} - -const ButtonGroup: React.FC = ({size = 'md', buttons, link, linkWithPadding, clearBg = true, outlineOnMobile, className, activeKey}) => { - let groupColorClasses = clsx( - 'flex items-center justify-start rounded', - link ? 'gap-4' : 'gap-2', - !link && !clearBg && 'gap-0! rounded-lg bg-grey-100 dark:bg-grey-900', - className - ); - - if (link && !clearBg) { - groupColorClasses = clsx( - 'transition-all hover:bg-grey-200 dark:hover:bg-grey-900', - size === 'sm' ? 'h-7 px-3' : 'h-[34px] px-4', - outlineOnMobile && 'border border-grey-300 hover:border-transparent md:border-transparent', - groupColorClasses - ); - } - - return ( -
- {buttons.map(({key, ...props}, index) => { - const buttonKey = key ?? `button-${index}`; - const buttonProps = {...props}; - - if (!link && !clearBg) { - buttonProps.className = clsx(props.className, 'w-8 rounded-lg border px-0!'); - - if (buttonKey === activeKey) { - buttonProps.color = 'white'; - buttonProps.className = clsx(buttonProps.className, 'border-grey-300 shadow-xs dark:border-grey-800'); - } else { - buttonProps.className = clsx(buttonProps.className, 'border-transparent'); - } - } - - return ( - (props.tooltip ? - -
- ); -}; - -export default ButtonGroup; diff --git a/apps/admin-x-design-system/src/global/button.stories.tsx b/apps/admin-x-design-system/src/global/button.stories.tsx deleted file mode 100644 index 7ae9891e3e4..00000000000 --- a/apps/admin-x-design-system/src/global/button.stories.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import type {Meta, StoryObj} from '@storybook/react-vite'; - -import Button from './button'; - -const meta = { - title: 'Global / Button', - component: Button, - tags: ['autodocs'], - argTypes: { - color: { - control: 'select' - } - } -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - args: { - label: 'Button' - } -}; - -export const Small: Story = { - args: { - size: 'sm', - label: 'Button', - color: 'black' - } -}; - -export const Black: Story = { - args: { - label: 'Button', - color: 'black' - } -}; - -export const Grey: Story = { - args: { - label: 'Button', - color: 'grey' - } -}; - -export const Green: Story = { - args: { - label: 'Button', - color: 'green' - } -}; - -export const Red: Story = { - args: { - label: 'Button', - color: 'red' - } -}; - -export const LinkButton: Story = { - args: { - label: 'Button', - color: 'green', - link: true - } -}; - -export const Icon: Story = { - args: { - icon: 'ellipsis', - color: 'green', - iconColorClass: 'text-white' - } -}; - -export const IconSmall: Story = { - args: { - size: 'sm', - icon: 'ellipsis', - color: 'green', - iconColorClass: 'text-white' - } -}; - -export const Loading: Story = { - args: { - loading: true, - color: 'green', - label: 'Button' - } -}; diff --git a/apps/admin-x-design-system/src/global/button.tsx b/apps/admin-x-design-system/src/global/button.tsx deleted file mode 100644 index 89c329ce323..00000000000 --- a/apps/admin-x-design-system/src/global/button.tsx +++ /dev/null @@ -1,166 +0,0 @@ -import Icon, {IconSize} from './icon'; -import React, {HTMLProps} from 'react'; -import clsx from 'clsx'; -import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './loading-indicator'; - -export type ButtonColor = 'clear' | 'light-grey' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline'; -export type ButtonSize = 'sm' | 'md'; - -export interface ButtonProps extends Omit, 'label' | 'size' | 'children'> { - size?: ButtonSize; - label?: React.ReactNode; - hideLabel?: boolean; - icon?: string; - iconSize?: IconSize; - iconColorClass?: string; - key?: string; - color?: ButtonColor; - fullWidth?: boolean; - link?: boolean; - linkWithPadding?: boolean; - disabled?: boolean; - unstyled?: boolean; - className?: string; - tag?: string; - loading?: boolean; - loadingIndicatorSize?: LoadingIndicatorSize; - loadingIndicatorColor?: LoadingIndicatorColor; - outlineOnMobile?: boolean; - onClick?: (e?:React.MouseEvent) => void; - testId?: string; -} - -const Button: React.FC = React.forwardRef(({ - testId, - size = 'md', - label = '', - hideLabel = false, - icon = '', - iconSize, - iconColorClass, - color = 'clear', - fullWidth, - link, - linkWithPadding = false, - disabled, - unstyled = false, - className = '', - tag = 'button', - loading = false, - loadingIndicatorColor, - outlineOnMobile = false, - onClick, - ...props -}, ref) => { - if (!color) { - color = 'clear'; - } - - if (!unstyled) { - className = clsx( - 'text inline-flex items-center justify-center rounded-md whitespace-nowrap transition', - ((link && color !== 'clear' && color !== 'black') || (!link && color !== 'clear')) ? 'font-bold' : 'font-semibold', - !link ? `${size === 'sm' ? 'h-7' : 'h-[34px]'}` : '', - !link ? `${size === 'sm' || label && icon ? 'px-3' : 'px-4'}` : '', - (link && linkWithPadding) && '-m-1 p-1', - className - ); - - switch (color) { - case 'black': - className = clsx( - link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`, - className - ); - loadingIndicatorColor = 'light'; - iconColorClass = iconColorClass || 'text-white'; - break; - case 'light-grey': - className = clsx( - link ? 'text-grey-800 hover:text-green-400 dark:text-white' : `bg-grey-200 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:bg-grey-300! dark:hover:bg-grey-800!'}`, - className - ); - loadingIndicatorColor = 'dark'; - break; - case 'grey': - className = clsx( - link ? 'text-black hover:text-grey-800 dark:text-white' : `bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:bg-grey-300! dark:hover:bg-grey-800!'}`, - className - ); - loadingIndicatorColor = 'dark'; - break; - case 'green': - className = clsx( - link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`, - className - ); - loadingIndicatorColor = 'light'; - iconColorClass = iconColorClass || 'text-white'; - break; - case 'red': - className = clsx( - link ? 'text-red hover:text-red-400' : `bg-red text-white ${!disabled && 'hover:bg-red-400'}`, - className - ); - loadingIndicatorColor = 'light'; - iconColorClass = iconColorClass || 'text-white'; - break; - case 'white': - className = clsx( - link ? 'text-white hover:text-white dark:text-black dark:hover:text-grey-800' : `bg-white text-black dark:bg-black dark:text-white`, - className - ); - loadingIndicatorColor = 'dark'; - break; - case 'outline': - className = clsx( - link ? 'text-black hover:text-grey-800 dark:text-white' : `border border-grey-300 bg-transparent text-black dark:border-grey-800 dark:text-white ${!disabled && 'hover:border-black! dark:hover:border-white!'}`, - className - ); - loadingIndicatorColor = 'dark'; - break; - default: - className = clsx( - link ? ' text-black hover:text-grey-800 dark:text-white' : `text-grey-900 dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200 hover:text-black'}`, - (outlineOnMobile && !link) && 'border border-grey-300 hover:border-transparent md:border-transparent', - className - ); - loadingIndicatorColor = 'dark'; - break; - } - - className = clsx( - (fullWidth && !link) && ' w-full', - disabled ? 'opacity-40' : 'cursor-pointer', - className - ); - } - - const iconClasses = label && icon && !hideLabel ? 'mr-1.5' : ''; - - let labelClasses = ''; - labelClasses += (label && hideLabel) ? 'sr-only' : ''; - labelClasses += loading ? 'invisible' : ''; - - iconSize = iconSize || ((size === 'sm') || (label && icon) ? 'sm' : 'md'); - - const buttonChildren = <> - {icon && } - {label} - {loading &&
Loading...
} - ; - - const buttonElement = React.createElement(tag, {className: className, - 'data-testid': testId, - disabled: disabled, - type: 'button', - onClick: onClick, - ref: ref, - ...props}, buttonChildren); - - return buttonElement; -}); - -Button.displayName = 'Button'; - -export default Button; diff --git a/apps/admin-x-design-system/src/global/chrome/desktop-chrome-header.stories.tsx b/apps/admin-x-design-system/src/global/chrome/desktop-chrome-header.stories.tsx index c60cb2e8c5b..1e434a8ba72 100644 --- a/apps/admin-x-design-system/src/global/chrome/desktop-chrome-header.stories.tsx +++ b/apps/admin-x-design-system/src/global/chrome/desktop-chrome-header.stories.tsx @@ -1,7 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import Button from '../button'; -import ButtonGroup from '../button-group'; import DesktopChromeHeader from './desktop-chrome-header'; const meta = { @@ -43,13 +41,8 @@ export const WithTitle: Story = { export const CustomToolbar: Story = { args: { - toolbarLeft: ); }; 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 index 32e03085cde..7df6a9caf22 100644 --- a/apps/admin-x-design-system/src/global/layout/global-actions.tsx +++ b/apps/admin-x-design-system/src/global/layout/global-actions.tsx @@ -1,9 +1,10 @@ import React from 'react'; -import Button from '../button'; +import Icon from '../icon'; +import {Button} from '@tryghost/shade/components'; const GlobalActions: React.FC = () => { return ( - ); }; 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 index ca079c5aa86..bc455ac4448 100644 --- a/apps/admin-x-design-system/src/global/layout/page.stories.tsx +++ b/apps/admin-x-design-system/src/global/layout/page.stories.tsx @@ -4,7 +4,6 @@ import Page, {CustomGlobalAction} from './page'; import ViewContainer from './view-container'; import {exampleActions as exampleActionButtons} from './view-container.stories'; -import Button from '../button'; import {Text} from '@tryghost/shade/primitives'; const meta = { @@ -24,6 +23,7 @@ const dummyContent =
Placeho const customGlobalActions: CustomGlobalAction[] = [ { iconName: 'heart', + ariaLabel: 'Favorite', onClick: () => { alert('Clicked on custom action'); } @@ -100,10 +100,7 @@ const exampleCardViewContent = ( New idea action slot} title='Ideas' type='page' > @@ -155,7 +152,7 @@ const mockPosts = () => { opened
-
); @@ -167,10 +164,7 @@ const examplePostsContent = ( New post action slot} title='Posts' type='page' > @@ -205,12 +199,7 @@ export const ExampleDetailScreen: Story = {
Colombus, OH
} - primaryAction={ - { - icon: 'ellipsis', - color: 'outline' - } - } + primaryAction={Member action slot} type='page' >
@@ -235,7 +224,7 @@ export const ExampleDetailScreen: Story = {
Member data -
Name @@ -293,7 +282,7 @@ export const ExampleDetailScreen: Story = {
Activity -
Logged in diff --git a/apps/admin-x-design-system/src/global/layout/page.tsx b/apps/admin-x-design-system/src/global/layout/page.tsx index 7fc62141d53..925e99a15fa 100644 --- a/apps/admin-x-design-system/src/global/layout/page.tsx +++ b/apps/admin-x-design-system/src/global/layout/page.tsx @@ -2,11 +2,13 @@ import React from 'react'; import clsx from 'clsx'; import AppMenu from './app-menu'; import GlobalActions from './global-actions'; -import Button from '../button'; +import Icon from '../icon'; import PageHeader from './page-header'; +import {Button} from '@tryghost/shade/components'; export interface CustomGlobalAction { iconName: string; + ariaLabel: string; onClick?: () => void; } @@ -88,7 +90,7 @@ const Page: React.FC = ({
{(customGlobalActions?.map((action, idx) => { return ( - ); }))} {showGlobalActions && } diff --git a/apps/admin-x-design-system/src/global/layout/view-container.stories.tsx b/apps/admin-x-design-system/src/global/layout/view-container.stories.tsx index 9656301343b..d04a44091f4 100644 --- a/apps/admin-x-design-system/src/global/layout/view-container.stories.tsx +++ b/apps/admin-x-design-system/src/global/layout/view-container.stories.tsx @@ -1,8 +1,6 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; import ViewContainer, {PrimaryActionProps} from './view-container'; -import Button from '../button'; -import ButtonGroup from '../button-group'; const meta = { title: 'Global / Layout / View Container', @@ -25,42 +23,13 @@ export default meta; type Story = StoryObj; export const exampleActions = [ -
{actions} - {primaryActionContents} + {primaryAction}
@@ -190,7 +176,7 @@ const ViewContainer: React.FC = ({ return (
- {(title || actions || headerContent || tabs) && toolbar} + {(title || actions || primaryAction || headerContent || tabs) && toolbar}
{mainContent}
diff --git a/apps/admin-x-design-system/src/global/modal/confirmation-modal.stories.tsx b/apps/admin-x-design-system/src/global/modal/confirmation-modal.stories.tsx index 4f40cfc220b..b4b53f93eba 100644 --- a/apps/admin-x-design-system/src/global/modal/confirmation-modal.stories.tsx +++ b/apps/admin-x-design-system/src/global/modal/confirmation-modal.stories.tsx @@ -2,14 +2,13 @@ import type {Meta, StoryContext, StoryObj} from '@storybook/react-vite'; import {ReactNode} from 'react'; import NiceModal from '@ebay/nice-modal-react'; -import Button from '../button'; import ConfirmationModal, {ConfirmationModalProps} from './confirmation-modal'; const ConfirmationModalContainer: React.FC = ({...props}) => { return ( - ); }; @@ -40,6 +39,6 @@ export const CustomButtons: Story = { title: 'You want to delete?', cancelLabel: 'Meh', okLabel: 'Alrite', - okColor: 'red' + okVariant: 'destructive' } }; diff --git a/apps/admin-x-design-system/src/global/modal/confirmation-modal.tsx b/apps/admin-x-design-system/src/global/modal/confirmation-modal.tsx index 24b2226f9eb..7ec2ba47737 100644 --- a/apps/admin-x-design-system/src/global/modal/confirmation-modal.tsx +++ b/apps/admin-x-design-system/src/global/modal/confirmation-modal.tsx @@ -1,7 +1,7 @@ import Modal from './modal'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useState} from 'react'; -import {ButtonColor} from '../button'; +import type {ButtonProps} from '@tryghost/shade/components'; export interface ConfirmationModalProps { title?: React.ReactNode; @@ -9,7 +9,7 @@ export interface ConfirmationModalProps { cancelLabel?: string; okLabel?: string; okRunningLabel?: string; - okColor?: ButtonColor; + okVariant?: ButtonProps['variant']; onCancel?: () => void; onOk?: (modal?: { remove: () => void; @@ -25,7 +25,7 @@ export const ConfirmationModalContent: React.FC = ({ cancelLabel = 'Cancel', okLabel = 'OK', okRunningLabel = '...', - okColor = 'black', + okVariant = 'default', onCancel, onOk, customFooter, @@ -41,8 +41,8 @@ export const ConfirmationModalContent: React.FC = ({ cancelLabel={cancelLabel} footer={customFooter} formSheet={formSheet} - okColor={okColor} okLabel={taskState === 'running' ? okRunningLabel : okLabel} + okVariant={okVariant} stickyFooter={stickyFooter} testId='confirmation-modal' title={title} diff --git a/apps/admin-x-design-system/src/global/modal/limit-modal.stories.tsx b/apps/admin-x-design-system/src/global/modal/limit-modal.stories.tsx index 969641906d8..8fcf9d493ef 100644 --- a/apps/admin-x-design-system/src/global/modal/limit-modal.stories.tsx +++ b/apps/admin-x-design-system/src/global/modal/limit-modal.stories.tsx @@ -2,14 +2,13 @@ import type {Meta, StoryContext, StoryObj} from '@storybook/react-vite'; import {ReactNode} from 'react'; import NiceModal from '@ebay/nice-modal-react'; -import Button from '../button'; import LimitModal, {LimitModalProps} from './limit-modal'; const LimitModalContainer: React.FC = ({...props}) => { return ( - ); }; diff --git a/apps/admin-x-design-system/src/global/modal/limit-modal.tsx b/apps/admin-x-design-system/src/global/modal/limit-modal.tsx index 3ac42fa0197..ef3085775d7 100644 --- a/apps/admin-x-design-system/src/global/modal/limit-modal.tsx +++ b/apps/admin-x-design-system/src/global/modal/limit-modal.tsx @@ -23,8 +23,8 @@ export const LimitModalContent: React.FC = ({ = ({children, ...props}) => { }); return (
-
); }; @@ -186,12 +185,7 @@ export const CompletePage: Story = { export const CustomButtons: Story = { args: { - leftButtonProps: { - label: 'Left button', - onClick: () => { - alert('Left button click'); - } - }, + leftButton: Left action slot, cancelLabel: 'Nope', okLabel: 'Yep', onOk: () => { diff --git a/apps/admin-x-design-system/src/global/modal/modal.tsx b/apps/admin-x-design-system/src/global/modal/modal.tsx index c0a87c4d6b6..537b9bfac49 100644 --- a/apps/admin-x-design-system/src/global/modal/modal.tsx +++ b/apps/admin-x-design-system/src/global/modal/modal.tsx @@ -1,12 +1,11 @@ import {useModal} from '@ebay/nice-modal-react'; import clsx from 'clsx'; import React, {useEffect, useState, forwardRef} from 'react'; -import Button, {ButtonColor, ButtonProps} from '../button'; -import ButtonGroup from '../button-group'; -import {StickyFooter} from '@tryghost/shade/components'; +import {Button, type ButtonProps, LoadingIndicator, StickyFooter} from '@tryghost/shade/components'; import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns'; -import {Text} from '@tryghost/shade/primitives'; +import {Inline, Text} from '@tryghost/shade/primitives'; import {useGlobalDirtyState} from '@tryghost/shade/utils'; +import Icon from '../icon'; export type ModalSize = 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'bleed'; @@ -23,10 +22,10 @@ export interface ModalProps { testId?: string; title?: React.ReactNode; okLabel?: string; - okColor?: ButtonColor; + okVariant?: ButtonProps['variant']; okLoading?: boolean; cancelLabel?: string; - leftButtonProps?: ButtonProps; + leftButton?: React.ReactNode; buttonsDisabled?: boolean; okDisabled?: boolean; footer?: boolean | React.ReactNode; @@ -64,12 +63,12 @@ const Modal = forwardRef(({ cancelLabel = 'Cancel', footer, header, - leftButtonProps, + leftButton, buttonsDisabled, okDisabled, padding = true, onOk, - okColor = 'black', + okVariant = 'default', onCancel, topRightContent, hideXOnMobile = false, @@ -161,8 +160,6 @@ const Modal = forwardRef(({ } }); - const buttons: ButtonProps[] = []; - let contentClasses; const removeModal = () => { @@ -172,34 +169,6 @@ const Modal = forwardRef(({ }); }; - if (!footer) { - if (cancelLabel) { - buttons.push({ - key: 'cancel-modal', - label: cancelLabel, - color: 'outline', - testId: 'cancel-modal', - onClick: (onCancel ? onCancel : () => { - removeModal(); - }), - disabled: buttonsDisabled - }); - } - - if (okLabel) { - buttons.push({ - key: 'ok-modal', - label: okLabel, - color: okColor, - className: 'min-w-[80px]', - testId: 'ok-modal', - onClick: onOk, - disabled: buttonsDisabled || okDisabled, - loading: okLoading - }); - } - } - let modalClasses = clsx( 'relative z-50 flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden bg-white dark:bg-black', align === 'center' && 'mx-auto', @@ -413,11 +382,21 @@ const Modal = forwardRef(({ footerContent = (
- {leftButtonProps &&
-
- + {leftButton}
+ + {cancelLabel && ( + + )} + {okLabel && ( + + )} +
); } @@ -448,7 +427,9 @@ const Modal = forwardRef(({ (
{title && {title}}
-
) : 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 index 5aabeb750ce..2680ea0a9db 100644 --- 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 @@ -2,15 +2,14 @@ import type {Meta, StoryContext, StoryObj} from '@storybook/react-vite'; import {ReactNode} from 'react'; import NiceModal from '@ebay/nice-modal-react'; -import Button from '../button'; import PreviewModal, {PreviewModalProps} from './preview-modal'; import {Text} from '@tryghost/shade/primitives'; const PreviewModalContainer: React.FC = ({...props}) => { return ( - ); }; @@ -64,7 +63,7 @@ export const CustomButtons: Story = { ...Default.args, cancelLabel: 'Meh', okLabel: 'Alrite', - okColor: 'green' + okVariant: 'default' } }; 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 727adf3cd05..b3aaf046126 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,12 +1,11 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import clsx from 'clsx'; import React, {useEffect} from 'react'; -import {ButtonColor, ButtonProps} from '../button'; -import ButtonGroup from '../button-group'; import Icon from '../icon'; import Modal, {ModalSize} from './modal'; +import {Button, type ButtonProps} from '@tryghost/shade/components'; import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns'; -import {Text, type TextElement, type TextLeading, type TextSize} from '@tryghost/shade/primitives'; +import {Inline, Text, type TextElement, type TextLeading, type TextSize} from '@tryghost/shade/primitives'; import {useGlobalDirtyState} from '@tryghost/shade/utils'; type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6; @@ -50,7 +49,7 @@ export interface PreviewModalProps { dirty?: boolean cancelLabel?: string; okLabel?: string; - okColor?: ButtonColor; + okVariant?: ButtonProps['variant']; buttonsDisabled?: boolean previewToolbar?: boolean; leftToolbar?: boolean; @@ -85,7 +84,7 @@ export const PreviewModalContent: React.FC = ({ dirty = false, cancelLabel = 'Cancel', okLabel = 'OK', - okColor = 'black', + okVariant = 'default', previewToolbar = true, leftToolbar = true, rightToolbar = true, @@ -181,29 +180,12 @@ export const PreviewModalContent: React.FC = ({ ); } - const buttons: ButtonProps[] = []; - - if (!sidebarButtons) { - buttons.push({ - key: 'cancel-modal', - label: cancelLabel, - onClick: (onCancel ? onCancel : () => { - confirm(dirty, () => { - modal.remove(); - afterClose?.(); - }); - }), - disabled: buttonsDisabled + const handleCancel = onCancel || (() => { + confirm(dirty, () => { + modal.remove(); + afterClose?.(); }); - - buttons.push({ - key: 'ok-modal', - label: okLabel, - color: okColor, - onClick: onOk, - disabled: buttonsDisabled - }); - } + }); return ( = ({ > {title} - {sidebarButtons ? sidebarButtons : } + {sidebarButtons || ( + + + + + )}
)}
diff --git a/apps/admin-x-design-system/src/global/tooltip.stories.tsx b/apps/admin-x-design-system/src/global/tooltip.stories.tsx index 0feffd0bde8..536ad5f338a 100644 --- a/apps/admin-x-design-system/src/global/tooltip.stories.tsx +++ b/apps/admin-x-design-system/src/global/tooltip.stories.tsx @@ -1,6 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import Button from './button'; import Tooltip from './tooltip'; const meta = { @@ -20,14 +19,14 @@ type Story = StoryObj; export const Default: Story = { args: { content: 'Hello tooltip', - children: } }; export const MediumSize: Story = { args: { content: 'Hello tooltip', - children: , size: 'md' } }; @@ -35,7 +34,7 @@ export const MediumSize: Story = { export const Left: Story = { args: { content: 'Hello tooltip on the left', - children: , origin: 'start' } }; @@ -43,7 +42,7 @@ export const Left: Story = { export const Center: Story = { args: { content: 'Hello center tooltip', - children: , origin: 'center' } }; @@ -51,7 +50,7 @@ export const Center: Story = { export const Right: Story = { args: { content: 'Hello right tooltip', - children: , origin: 'end' } }; @@ -59,7 +58,7 @@ export const Right: Story = { export const Long: Story = { args: { content: `You're the best evil son an evil dad could ever ask for.`, - children: , size: 'md', origin: 'start' } diff --git a/apps/admin-x-design-system/src/index.ts b/apps/admin-x-design-system/src/index.ts index 9248a370fdb..44d88430e9f 100644 --- a/apps/admin-x-design-system/src/index.ts +++ b/apps/admin-x-design-system/src/index.ts @@ -28,10 +28,6 @@ export type {PreviewModalProps} from './global/modal/preview-modal'; export {default as Banner} from './global/banner'; export type {BannerProps} from './global/banner'; -export {default as Button} from './global/button'; -export type {ButtonColor, ButtonProps} from './global/button'; -export {default as ButtonGroup} from './global/button-group'; -export type {ButtonGroupProps} from './global/button-group'; export {default as ErrorBoundary} from './global/error-boundary'; export type {ErrorBoundaryProps} from './global/error-boundary'; export {default as Icon} from './global/icon'; diff --git a/apps/admin-x-design-system/src/settings/setting-group-header.stories.tsx b/apps/admin-x-design-system/src/settings/setting-group-header.stories.tsx index f7f91fe541e..38beadb2850 100644 --- a/apps/admin-x-design-system/src/settings/setting-group-header.stories.tsx +++ b/apps/admin-x-design-system/src/settings/setting-group-header.stories.tsx @@ -1,6 +1,5 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; -import ButtonGroup from '../global/button-group'; import SettingGroupHeader from './setting-group-header'; import {Text} from '@tryghost/shade/primitives'; @@ -17,10 +16,7 @@ export const Default: Story = { args: { title: 'Section group title', description: 'Section group description', - children: + children: Edit action slot } }; @@ -28,15 +24,7 @@ export const Editing: Story = { args: { title: 'Section group title', description: 'Section group description', - children: + children: Cancel and save action slots } }; @@ -49,10 +37,7 @@ export const CustomHeader: Story = { Cristofer Vaccaro — Owner cristofer@example.com
- + Invite users action slot } }; diff --git a/apps/admin-x-design-system/src/settings/setting-group-header.tsx b/apps/admin-x-design-system/src/settings/setting-group-header.tsx index 67c9df56ce3..27938d65e63 100644 --- a/apps/admin-x-design-system/src/settings/setting-group-header.tsx +++ b/apps/admin-x-design-system/src/settings/setting-group-header.tsx @@ -17,7 +17,7 @@ const SettingGroupHeader: React.FC = ({title, descripti {description &&

{description}

}
} -
+
{children}
diff --git a/apps/admin-x-design-system/src/settings/setting-group.stories.tsx b/apps/admin-x-design-system/src/settings/setting-group.stories.tsx index 5de25744567..dfc731464f6 100644 --- a/apps/admin-x-design-system/src/settings/setting-group.stories.tsx +++ b/apps/admin-x-design-system/src/settings/setting-group.stories.tsx @@ -4,7 +4,6 @@ import type {Meta, StoryObj} from '@storybook/react-vite'; import * as SettingGroupContentStories from './setting-group-content.stories'; import * as SettingGroupHeaderStories from './setting-group-header.stories'; -import ButtonGroup from '../global/button-group'; import SettingGroup from './setting-group'; import SettingGroupContent from './setting-group-content'; import SettingGroupHeader from './setting-group-header'; @@ -24,7 +23,7 @@ const meta = { export default meta; type Story = StoryObj; -const customButtons = ; +const customButtons = Custom action slot; const customHeader = ; const singleColContent = ; const twoColView = ; diff --git a/apps/admin-x-design-system/src/settings/setting-group.tsx b/apps/admin-x-design-system/src/settings/setting-group.tsx index f5b780d4757..c67382704db 100644 --- a/apps/admin-x-design-system/src/settings/setting-group.tsx +++ b/apps/admin-x-design-system/src/settings/setting-group.tsx @@ -1,8 +1,8 @@ import clsx from 'clsx'; import React, {forwardRef, useEffect} from 'react'; -import {ButtonProps} from '../global/button'; -import ButtonGroup from '../global/button-group'; import SettingGroupHeader from './setting-group-header'; +import {Button} from '@tryghost/shade/components'; +import {Inline} from '@tryghost/shade/primitives'; export interface SettingGroupProps { navid?:string; @@ -78,57 +78,20 @@ const SettingGroup = forwardRef(function Sett styles += ' border-grey-200 dark:border-grey-900 dark:hover:border-grey-800'; - // The links visible before editing - const viewButtons: ButtonProps[] = []; - - if (!hideEditButton) { - let label = 'Edit'; - if (saveState === 'saved') { - label = 'Saved'; - } - viewButtons.push( - { - label, - key: 'edit', - color: 'clear', - onClick: handleEdit - } - ); - } else if (saveState === 'saved') { - viewButtons.push( - { - label: 'Saved', - key: 'edit', - color: 'green', - onClick: handleEdit - } - ); - } - - // The buttons that show when you are editing - const editButtons: ButtonProps[] = [ - { - label: 'Cancel', - key: 'cancel', - onClick: handleCancel - } - ]; - - if (saveState === 'unsaved' || alwaysShowSaveButton) { - let label = 'Save'; - if (saveState === 'saving') { - label = 'Saving...'; - } - editButtons.push( - { - label: label, - key: 'save', - color: saveState === 'unsaved' ? 'green' : 'light-grey', - disabled: saveState !== 'unsaved', - onClick: handleSave - } - ); - } + const buttons = isEditing ? ( + + + {(saveState === 'unsaved' || alwaysShowSaveButton) && ( + + )} + + ) : (!hideEditButton || saveState === 'saved') ? ( + + ) : null; useEffect(() => { const handleCMDS = (e: KeyboardEvent) => { @@ -161,7 +124,7 @@ const SettingGroup = forwardRef(function Sett {customHeader ? customHeader : {customButtons ? customButtons : - (onEditingChange && ) + (onEditingChange && buttons) } } @@ -175,7 +138,7 @@ const SettingGroup = forwardRef(function Sett {customHeader ? customHeader : {customButtons ? customButtons : - (onEditingChange && ) + (onEditingChange && buttons) } } diff --git a/apps/admin-x-framework/src/hooks/use-form.ts b/apps/admin-x-framework/src/hooks/use-form.ts index 76a9d002ee6..c5e3ce88eda 100644 --- a/apps/admin-x-framework/src/hooks/use-form.ts +++ b/apps/admin-x-framework/src/hooks/use-form.ts @@ -1,8 +1,7 @@ import {useCallback, useEffect, useState} from 'react'; +import type {ButtonProps} from '@tryghost/shade/components'; -// Structurally matches admin-x-design-system's ButtonColor so okProps.color stays -// assignable to its Button color prop. -export type ButtonColor = 'clear' | 'light-grey' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline'; +export type ButtonVariant = NonNullable; export type Dirtyable = Data & { dirty?: boolean; @@ -14,7 +13,7 @@ export type ErrorMessages = Record export interface OkProps { disabled: boolean; - color: ButtonColor; + variant: ButtonVariant; label?: string; } @@ -126,11 +125,11 @@ const useForm = ({initialState, savingDelay, savedDelay = 2000, onSave, o setSaveState('unsaved'); }, []); - let okColor: ButtonColor = 'black'; + let okVariant: ButtonVariant = 'default'; if (saveState === 'saved') { - okColor = 'green'; + okVariant = 'default'; } else if (saveState === 'error') { - okColor = 'red'; + okVariant = 'destructive'; } let okLabel = ''; @@ -144,7 +143,7 @@ const useForm = ({initialState, savingDelay, savedDelay = 2000, onSave, o const okProps: OkProps = { disabled: saveState === 'saving', - color: okColor, + variant: okVariant, label: okLabel || undefined }; diff --git a/apps/admin-x-framework/test/unit/hooks/use-form.test.ts b/apps/admin-x-framework/test/unit/hooks/use-form.test.ts index 3391693d283..b8284c9d792 100644 --- a/apps/admin-x-framework/test/unit/hooks/use-form.test.ts +++ b/apps/admin-x-framework/test/unit/hooks/use-form.test.ts @@ -427,7 +427,7 @@ describe('useForm', () => { expect(result.current!.okProps).toEqual({ disabled: false, - color: 'black', + variant: 'default', label: undefined }); }); @@ -455,7 +455,7 @@ describe('useForm', () => { // Check that we're in saving state expect(result.current!.okProps).toEqual({ disabled: true, - color: 'black', + variant: 'default', label: 'Saving...' }); }); @@ -475,7 +475,7 @@ describe('useForm', () => { expect(result.current!.okProps).toEqual({ disabled: false, - color: 'green', + variant: 'default', label: 'Saved' }); }); @@ -498,7 +498,7 @@ describe('useForm', () => { expect(result.current!.okProps).toEqual({ disabled: false, - color: 'red', + variant: 'destructive', label: 'Retry' }); }); diff --git a/apps/admin-x-settings/src/components/exit-settings-button.tsx b/apps/admin-x-settings/src/components/exit-settings-button.tsx index ad64ce7e563..1ef2bfa68a5 100644 --- a/apps/admin-x-settings/src/components/exit-settings-button.tsx +++ b/apps/admin-x-settings/src/components/exit-settings-button.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {DirtyConfirmDialog, useDirtyConfirmation} from '@tryghost/shade/patterns'; +import {Icon} from '@tryghost/admin-x-design-system'; import {useGlobalDirtyState} from '@tryghost/shade/utils'; const ExitSettingsButton: React.FC = () => { @@ -13,7 +14,9 @@ const ExitSettingsButton: React.FC = () => { return ( <> - ); diff --git a/apps/admin-x-settings/src/components/settings/advanced/code-injection.tsx b/apps/admin-x-settings/src/components/settings/advanced/code-injection.tsx index ef75fe501c4..27dca675ce0 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/code-injection.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/code-injection.tsx @@ -2,7 +2,8 @@ import CodeModal from './code/code-modal'; import NiceModal from '@ebay/nice-modal-react'; import React from 'react'; import TopLevelGroup from '../../top-level-group'; -import {Button, SettingGroupHeader} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; +import {SettingGroupHeader} from '@tryghost/admin-x-design-system'; import {withErrorBoundary} from '../../error-boundary'; const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => { @@ -11,9 +12,9 @@ const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => { customHeader={
-
} description="Add custom code to your publication" 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 25b2e768301..b614b469057 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 @@ -2,10 +2,10 @@ import CodeEditor from '../../../code-editor'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useEffect, useMemo, useRef, useState} from 'react'; import useSettingGroup from '../../../../hooks/use-setting-group'; -import {ButtonGroup, Modal} from '@tryghost/admin-x-design-system'; +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 {Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; -import {Text} from '@tryghost/shade/primitives'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; import {useSaveButton} from '../../../../hooks/use-save-button'; @@ -74,22 +74,13 @@ const CodeModal: React.FC = ({afterClose}) => {
Code injection - { - modal.remove(); - afterClose?.(); - } - }, - { - disabled: isSaving, - label: savingTitle, - color: savingTitle === 'Saved' ? 'green' : 'black', - onClick: onSaveClick - } - ]} /> + + + +
setSelectedTab(value as typeof selectedTab)}> diff --git a/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx b/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx index 125b187779a..e484d58466f 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/danger-zone.tsx @@ -3,8 +3,8 @@ import React from 'react'; import TopLevelGroup from '../../top-level-group'; import trackEvent from '../../../utils/analytics'; import useStaffUsers from '../../../hooks/use-staff-users'; -import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, SettingGroupHeader} from '@tryghost/admin-x-design-system'; +import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button} from '@tryghost/shade/components'; +import {ConfirmationModal, SettingGroupHeader} from '@tryghost/admin-x-design-system'; import {formatNumber} from '@tryghost/shade/utils'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {toast} from 'sonner'; @@ -37,7 +37,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { NiceModal.show(ConfirmationModal, { title: 'Would you really like to delete all content from your blog?', prompt: 'This is permanent! No backups, no restores, no magic undo button. We warned you, k?', - okColor: 'red', + okVariant: 'destructive', okLabel: 'Delete', onOk: async (modal) => { try { @@ -67,7 +67,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { ), okLabel: 'Reset all authentication', okRunningLabel: 'Resetting...', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { try { const response = await resetAuth(null); @@ -90,7 +90,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => { prompt: 'This immediately invalidates every active gift link across your site. Anyone holding one will lose access. New gift links can still be created afterwards.', okLabel: 'Reset all gift links', okRunningLabel: 'Resetting...', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { try { const response = await removeAllGiftLinks(null); @@ -120,7 +120,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
Delete all content
Permanently delete all posts and tags from the database.
- {resetAuthEnabled && ( @@ -128,7 +128,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
Reset all authentication
Rotate every API key, sign out every staff user, and require a password reset. Use after a suspected credential compromise.
-
)} @@ -136,7 +136,7 @@ const DangerZone: React.FC<{ keywords: string[] }> = ({keywords}) => {
Reset all gift links
Invalidate every active gift link across your site. Anyone holding one will lose access.
-
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 41d0f2eb395..f3c5a0c8c26 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 @@ -1,9 +1,9 @@ import InfiniteScrollListener from '../../infinite-scroll-listener'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import {type Action, getActionTitle, getContextResource, getLinkTarget, isBulkAction, useBrowseActions} from '@tryghost/admin-x-framework/api/actions'; -import {ActionList, ActionListItem, ActionListItemContent, Avatar, Field, FieldLabel, LoadingIndicator, MultiSelectCombobox, NoValueLabel, NoValueLabelIcon, Popover, PopoverContent, PopoverTrigger, Switch, inputSurface} from '@tryghost/shade/components'; -import {Button, Icon, Modal} from '@tryghost/admin-x-design-system'; +import {ActionList, ActionListItem, ActionListItemContent, Avatar, Button, Field, FieldLabel, LoadingIndicator, MultiSelectCombobox, NoValueLabel, NoValueLabelIcon, Popover, PopoverContent, PopoverTrigger, Switch, inputSurface} from '@tryghost/shade/components'; import {ChevronDown, History, X} from 'lucide-react'; +import {Icon, Modal} from '@tryghost/admin-x-design-system'; import {Inline, Stack} from '@tryghost/shade/primitives'; import {type RoutingModalProps, useRouting} from '@tryghost/admin-x-framework/routing'; import {type User} from '@tryghost/admin-x-framework/api/users'; @@ -153,7 +153,7 @@ const HistoryFilter: React.FC<{ - diff --git a/apps/admin-x-settings/src/components/settings/advanced/history.tsx b/apps/admin-x-settings/src/components/settings/advanced/history.tsx index 85b4b07b96f..43eb3c4bf56 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/history.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/history.tsx @@ -1,6 +1,6 @@ import React from 'react'; import TopLevelGroup from '../../top-level-group'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {useRouting} from '@tryghost/admin-x-framework/routing'; import {withErrorBoundary} from '../../error-boundary'; @@ -12,7 +12,7 @@ const History: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={} description="View system event log" keywords={keywords} navid='history' diff --git a/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx b/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx index 141826127fd..b85ec3bc2e2 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/integrations.tsx @@ -3,8 +3,8 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import TopLevelGroup from '../../top-level-group'; import usePinturaEditor from '../../../hooks/use-pintura-editor'; -import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, NoValueLabel, NoValueLabelIcon, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, Icon, SettingGroupHeader} from '@tryghost/admin-x-design-system'; +import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button, NoValueLabel, NoValueLabelIcon, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; +import {ConfirmationModal, Icon, SettingGroupHeader} from '@tryghost/admin-x-design-system'; import {type Integration, useBrowseIntegrations, useDeleteIntegration} from '@tryghost/admin-x-framework/api/integrations'; import {Plug} from 'lucide-react'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; @@ -66,11 +66,11 @@ const IntegrationItem: React.FC = ({ }; const buttons = custom ? - : (disabled ? - : + ); return @@ -221,7 +221,7 @@ const CustomIntegrations: React.FC<{integrations: Integration[]}> = ({integratio NiceModal.show(ConfirmationModal, { title: 'Are you sure?', prompt: 'Deleting this integration will remove all webhooks and api keys associated with it.', - okColor: 'red', + okVariant: 'destructive', okLabel: 'Delete Integration', onOk: async (confirmModal) => { try { @@ -253,15 +253,15 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => { const buttons = ( ); return ( @@ -275,10 +275,10 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => {
{ - }
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 4719f7f47d7..346327f6944 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 @@ -34,8 +34,8 @@ const AddIntegrationModal: React.FC = () => { afterClose={() => { updateRoute('integrations'); }} - okColor='black' okLabel='Add' + okVariant='default' size='sm' testId='add-integration-modal' title='Add integration' 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 212770d45ed..b2fd970a2df 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 @@ -1,7 +1,8 @@ import APIKeys from './api-keys'; import IntegrationHeader from './integration-header'; import NiceModal from '@ebay/nice-modal-react'; -import {Button, Icon, Modal} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; +import {Icon, Modal} from '@tryghost/admin-x-design-system'; 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'; @@ -22,11 +23,11 @@ const ContentApiModal = NiceModal.create(() => { cancelLabel='' footer={
- +
} testId='content-api-modal' 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 6d48f888a9b..36c15e4884f 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 @@ -85,8 +85,8 @@ const CustomIntegrationModalContent: React.FC<{integration: Integration}> = ({in buttonsDisabled={okProps.disabled} cancelLabel='Close' dirty={saveState === 'unsaved'} - okColor={okProps.color} okLabel={okProps.label || 'Save'} + okVariant={okProps.variant} size='md' testId='custom-integration-modal' title={formState.name || 'Custom integration'} 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 cc15f53c621..3d8c30290d0 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 @@ -62,8 +62,8 @@ const FirstPromoterModal = NiceModal.create(() => { }} cancelLabel='Close' dirty={enabled !== firstPromoterEnabled || accountId !== firstPromoterId} - okColor={okLabel === 'Saved' ? 'green' : 'black'} okLabel={okLabel} + okVariant='default' testId='firstpromoter-modal' title='' onOk={async () => { 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 11890e32efa..91cc79138a9 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 @@ -80,8 +80,8 @@ const PinturaModal = NiceModal.create(() => { }} cancelLabel='Close' dirty={isDirty} - okColor={okLabel === 'Saved' ? 'green' : 'black'} okLabel={okLabel} + okVariant='default' testId='pintura-modal' title='' onOk={handleToggleChange} 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 33f549722d2..4a65f750fd0 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 @@ -2,7 +2,8 @@ import IntegrationHeader from './integration-header'; import NiceModal from '@ebay/nice-modal-react'; import useSettingGroup from '../../../../hooks/use-setting-group'; import validator from 'validator'; -import {Button, Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; +import {Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; import {getSettingValues, useTestSlack} from '@tryghost/admin-x-framework/api/settings'; import {toast} from 'sonner'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -43,8 +44,8 @@ const SlackModal = NiceModal.create(() => { }} cancelLabel='Close' dirty={isDirty} - okColor={okProps.color} okLabel={okProps.label || 'Save'} + okVariant={okProps.variant} testId='slack-modal' title='' onOk={async () => { @@ -79,7 +80,7 @@ const SlackModal = NiceModal.create(() => { value={slackUsername} onChange={e => updateSetting('slack_username', e.target.value)} /> -
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 905a50d9009..7b5313a0c64 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 @@ -94,8 +94,8 @@ const TransistorModal = NiceModal.create(() => { }} cancelLabel='Close' dirty={enabled !== transistorEnabled} - okColor={okLabel === 'Saved' ? 'green' : 'black'} okLabel={okLabel} + okVariant='default' testId='transistor-modal' title='' onOk={handleSave} 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 e73d75487b6..580151b28e7 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 @@ -50,8 +50,8 @@ const UnsplashModal = NiceModal.create(() => { }} cancelLabel='Close' dirty={isDirty} - okColor={okLabel === 'Saved' ? 'green' : 'black'} okLabel={okLabel} + okVariant='default' testId='unsplash-modal' title='' onOk={handleToggleChange} 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 6d01daf1a64..130bdd073c6 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 @@ -53,8 +53,8 @@ const WebhookModal: React.FC = ({webhook, integrationId}) => }); return = ({integration}) => { NiceModal.show(ConfirmationModal, { title: 'Are you sure?', prompt: 'Deleting this webhook may prevent the integration from functioning.', - okColor: 'red', + okVariant: 'destructive', okLabel: 'Delete Webhook', onOk: async (confirmModal) => { try { @@ -71,10 +71,10 @@ const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => { })} - ))} @@ -82,17 +82,15 @@ const WebhooksTable: React.FC<{integration: Integration}> = ({integration}) => {
); }; 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 fe592d129d8..68004f6a791 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 @@ -2,8 +2,8 @@ import APIKeys from './api-keys'; import IntegrationHeader from './integration-header'; import NiceModal from '@ebay/nice-modal-react'; import ZapierLogo from '../../../../assets/images/zapier-logo.svg'; -import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent} from '@tryghost/shade/components'; -import {Button, ConfirmationModal, Icon, Modal} from '@tryghost/admin-x-design-system'; +import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button} from '@tryghost/shade/components'; +import {ConfirmationModal, Icon, Modal} from '@tryghost/admin-x-design-system'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {useBrowseIntegrations} from '@tryghost/admin-x-framework/api/integrations'; import {useEffect, useState} from 'react'; @@ -79,13 +79,13 @@ const ZapierModal = NiceModal.create(() => { target='_blank'> View more Ghost integrations powered by Zapier - } - okColor='black' okLabel='Close' + okVariant='default' testId='zapier-modal' title='' stickyFooter @@ -123,7 +123,7 @@ const ZapierModal = NiceModal.create(() => { {template.title} - ))} diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs.tsx index 783aeaecc1a..cb8b3dab774 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs.tsx @@ -3,8 +3,8 @@ import LabsBubbles from '../../../assets/images/labs-bg.svg'; import PrivateFeatures from './labs/private-features'; import React, {useState} from 'react'; import TopLevelGroup from '../../top-level-group'; -import {Button, SettingGroupHeader} from '@tryghost/admin-x-design-system'; -import {Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; +import {Button, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; +import {SettingGroupHeader} from '@tryghost/admin-x-design-system'; import {useAutoExpandable} from '../../../hooks/use-auto-expandable'; import {useGlobalData} from '../../providers/global-data-provider'; import {withErrorBoundary} from '../../error-boundary'; @@ -26,11 +26,11 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => { /> } isEditing={isOpen} diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx index 9874c7f9fa9..cf9d866528f 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx @@ -3,8 +3,7 @@ import LabItem from './lab-item'; import NiceModal from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import YamlFileEditorModal from './yaml-file-editor-modal'; -import {ActionList, Dropzone, Button as ShadeButton} from '@tryghost/shade/components'; -import {Button} from '@tryghost/admin-x-design-system'; +import {ActionList, Button, Dropzone} from '@tryghost/shade/components'; import {Inline, Stack} from '@tryghost/shade/primitives'; import {downloadRedirects, useUploadRedirects} from '@tryghost/admin-x-framework/api/redirects'; import {downloadRoutes, useUploadRoutes} from '@tryghost/admin-x-framework/api/routes'; @@ -98,9 +97,9 @@ const BetaFeatures: React.FC = () => { > {redirectsUploading ? 'Uploading ...' : 'Upload redirects file'} - Edit + - } detail={<>Configure redirects for old or moved content,
more info in the docs} testId='redirects' @@ -125,9 +124,9 @@ const BetaFeatures: React.FC = () => { > {routesUploading ? 'Uploading ...' : 'Upload routes file'} - Edit + - } detail='Configure dynamic routing by modifying the routes.yaml file' testId='routes' diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx index 5a87bc0e812..a1cf288e6e9 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/migration-options.tsx @@ -1,8 +1,8 @@ import LabItem from './lab-item'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useState} from 'react'; -import {ActionList, Dropzone} from '@tryghost/shade/components'; -import {Button, ConfirmationModal} from '@tryghost/admin-x-design-system'; +import {ActionList, Button, Dropzone} from '@tryghost/shade/components'; +import {ConfirmationModal} from '@tryghost/admin-x-design-system'; import {downloadAllContent, useDeleteAllContent, useImportContent} from '@tryghost/admin-x-framework/api/db'; import {toast} from 'sonner'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -61,7 +61,7 @@ const MigrationOptions: React.FC = () => { NiceModal.show(ConfirmationModal, { title: 'Would you really like to delete all content from your blog?', prompt: 'This is permanent! No backups, no restores, no magic undo button. We warned you, k?', - okColor: 'red', + okVariant: 'destructive', okLabel: 'Delete', onOk: async (modal) => { try { @@ -79,15 +79,15 @@ const MigrationOptions: React.FC = () => { return ( } + action={} detail='Import posts from a JSON or zip file' title='Import content' /> downloadAllContent()} />} + action={} detail='Download all of your posts and settings in a single, glorious JSON file' title='Export your content' /> } + action={} detail='Permanently delete all posts and tags from the database, a hard reset' title='Delete all content' /> 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 76e38c8a40c..8d508d6ec12 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 @@ -2,8 +2,9 @@ import CodeEditor from '../../../code-editor'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useEffect, useMemo, useState} from 'react'; import {APIError, JSONError} from '@tryghost/admin-x-framework/errors'; -import {ButtonGroup, Modal} from '@tryghost/admin-x-design-system'; -import {Text} from '@tryghost/shade/primitives'; +import {Button} from '@tryghost/shade/components'; +import {Inline, Text} from '@tryghost/shade/primitives'; +import {Modal} from '@tryghost/admin-x-design-system'; import {getGhostPaths} from '@tryghost/admin-x-framework/helpers'; import {toast} from 'sonner'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -153,19 +154,10 @@ const YamlFileEditorModal: React.FC = ({
{title} - void handleSave() - } - ]} /> + + + +
{(loadError || saveError) && ( diff --git a/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-export.tsx b/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-export.tsx index afd38c049cf..d101fe2f0ff 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-export.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-export.tsx @@ -1,5 +1,6 @@ import React from 'react'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button, LoadingIndicator} from '@tryghost/shade/components'; +import {Icon} from '@tryghost/admin-x-design-system'; import {blobDownloadFromEndpoint} from '@tryghost/admin-x-framework/helpers'; import {downloadAllContent} from '@tryghost/admin-x-framework/api/db'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -26,8 +27,8 @@ const MigrationToolsExport: React.FC = () => { return (
- +
); }; diff --git a/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-import.tsx b/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-import.tsx index 774bfc3783b..d28c48befd3 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-import.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/migration-tools/migration-tools-import.tsx @@ -1,34 +1,10 @@ import NiceModal from '@ebay/nice-modal-react'; import React from 'react'; import UniversalImportModal from './universal-import-modal'; -import clsx from 'clsx'; +import {Button} from '@tryghost/shade/components'; import {Icon} from '@tryghost/admin-x-design-system'; import {useRouting} from '@tryghost/admin-x-framework/routing'; -const ImportButton: React.FC<{ - icon?: React.ReactNode, - title?: string, - onClick?: () => void -}> = ({ - icon, - title, - onClick -}) => { - const classNames = clsx( - 'flex h-9 cursor-pointer items-center justify-center gap-2 rounded-md bg-grey-100 px-2 font-semibold transition-all hover:bg-grey-200 dark:bg-grey-900' - ); - if (onClick) { - return ( - - ); - } else { - return <>; - } -}; - const MigrationToolsImport: React.FC = () => { const {updateRoute} = useRouting(); @@ -36,57 +12,24 @@ const MigrationToolsImport: React.FC = () => { NiceModal.show(UniversalImportModal); }; + const importers = [ + {icon: , title: 'Substack', onClick: () => updateRoute({isExternal: true, route: '/migrate/substack'})}, + {icon: , title: 'beehiiv', onClick: () => updateRoute({isExternal: true, route: '/migrate/beehiiv'})}, + {icon: , title: 'WordPress', onClick: () => updateRoute({isExternal: true, route: '/migrate/wordpress'})}, + {icon: , title: 'Squarespace', onClick: () => updateRoute({isExternal: true, route: '/migrate/squarespace'})}, + {icon: , title: 'Medium', onClick: () => updateRoute({isExternal: true, route: '/migrate/medium'})}, + {icon: , title: 'Mailchimp', onClick: () => updateRoute({isExternal: true, route: '/migrate/mailchimp'})}, + {icon: , title: 'Universal import', onClick: handleImportContent} + ]; + return (
- - } - title='Substack' - onClick={() => updateRoute({isExternal: true, route: '/migrate/substack'})} - /> - - } - title='beehiiv' - onClick={() => updateRoute({isExternal: true, route: '/migrate/beehiiv'})} - /> - - } - title='WordPress' - onClick={() => updateRoute({isExternal: true, route: '/migrate/wordpress'})} - /> - - } - title='Squarespace' - onClick={() => updateRoute({isExternal: true, route: '/migrate/squarespace'})} - /> - - } - title='Medium' - onClick={() => updateRoute({isExternal: true, route: '/migrate/medium'})} - /> - - } - title='Mailchimp' - onClick={() => updateRoute({isExternal: true, route: '/migrate/mailchimp'})} - /> - - } - title='Universal import' - onClick={handleImportContent} - /> + {importers.map(importer => ( + + ))}
); }; 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 f7d5cecbae8..2b8ee04a699 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 @@ -1,7 +1,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {useState} from 'react'; -import {Button, ConfirmationModal, Modal} from '@tryghost/admin-x-design-system'; -import {Dropzone} from '@tryghost/shade/components'; +import {Button, Dropzone} from '@tryghost/shade/components'; +import {ConfirmationModal, Modal} from '@tryghost/admin-x-design-system'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useImportContent} from '@tryghost/admin-x-framework/api/db'; @@ -17,7 +17,7 @@ const UniversalImportModal: React.FC = () => { footer={
Learn about importing -
} okLabel='' diff --git a/apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx b/apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx index ca712f6f005..ac029dad3cb 100644 --- a/apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/email-design/email-design-modal.tsx @@ -11,7 +11,7 @@ interface EmailDesignModalProps { sidebar: React.ReactNode; dirty?: boolean; isLoading?: boolean; - okProps?: Pick; + okProps?: Pick; onSave: () => void; onClose: () => void; afterClose?: () => void; @@ -94,11 +94,11 @@ const EmailDesignModal: React.FC = ({
{title}
- + - + ); @@ -135,7 +135,7 @@ const EmailsGroup: React.FC<{ keywords: string[]; newslettersEnabled: boolean }> }; const customButtons = newslettersEnabled && selectedTab === 'newsletters' ? ( - ) : undefined; return ( diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters.tsx index ac39f986490..ec1ae707a24 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters.tsx @@ -4,10 +4,10 @@ import React, {type ReactNode, useEffect, useState} from 'react'; import TopLevelGroup from '../../top-level-group'; import useQueryParams from '../../../hooks/use-query-params'; import {APIError} from '@tryghost/admin-x-framework/errors'; -import {Button, ConfirmationModal} from '@tryghost/admin-x-design-system'; +import {Button, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; +import {ConfirmationModal} from '@tryghost/admin-x-design-system'; import {type InfiniteData, useQueryClient} from '@tryghost/admin-x-framework'; import {type Newsletter, type NewslettersResponseType, newslettersDataType, useBrowseNewsletters, useEditNewsletter, useVerifyNewsletterEmail} from '@tryghost/admin-x-framework/api/newsletters'; -import {Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; import {arrayMove} from '@dnd-kit/sortable'; import {formatNumber} from '@tryghost/shade/utils'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -18,10 +18,10 @@ const NavigateToNewsletter = ({id, children}: {id: string; children: ReactNode}) const modal = useModal(); const {updateRoute} = useRouting(); - return ; + }}>{children}; }; const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { @@ -93,9 +93,9 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { }, [verifyEmailToken, handleError, verifyEmail]); const buttons = ( - ); const sortedActiveNewsletters = newsletters.filter(n => n.status === 'active').sort((a, b) => a.sort_order - b.sort_order) || []; @@ -154,11 +154,9 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => { - {isEnd === false && } ); }; 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 070b0fe4f22..19e7d0a43f4 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 @@ -93,10 +93,10 @@ const AddNewsletterModal: React.FC = () => { updateRoute(returnRoute); }} backDropClick={false} - okColor='black' okDisabled={saveState === 'saving'} okLabel='Create' okLoading={saveState === 'saving'} + okVariant='default' size='sm' testId='add-newsletter-modal' title='Create newsletter' 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 cc964e36e0f..ad059f45252 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 @@ -6,9 +6,9 @@ import React, {useCallback, useEffect, useState} from 'react'; import useFeatureFlag from '../../../../hooks/use-feature-flag'; import useSettingGroup from '../../../../hooks/use-setting-group'; import validator from 'validator'; -import {Button, ButtonGroup, ConfirmationModal, Form, Icon, LimitModal, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; +import {Button, Field, FieldContent, FieldDescription, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea, ToggleGroup, ToggleGroupItem, Tooltip, TooltipContent, TooltipTrigger} from '@tryghost/shade/components'; +import {ConfirmationModal, Form, Icon, LimitModal, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; -import {Field, FieldContent, FieldDescription, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Separator, Switch, Tabs, TabsContent, TabsList, TabsTrigger, Textarea} from '@tryghost/shade/components'; import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter'; import {ImageUpload, ImageUploadAction, ImageUploadActions, ImageUploadDropzone, ImageUploadImage, ImageUploadPreview} from '@tryghost/shade/patterns'; import {type Newsletter, useBrowseNewsletters, useEditNewsletter} from '@tryghost/admin-x-framework/api/newsletters'; @@ -24,6 +24,33 @@ import {textColorForBackgroundColor} from '@tryghost/color-utils'; import {toast} from 'sonner'; import {useGlobalData} from '../../../providers/global-data-provider'; +interface IconToggleOption { + value: string; + label: string; + icon: string; + disabled?: boolean; +} + +const IconToggleGroup: React.FC<{ + label: string; + value: string; + options: IconToggleOption[]; + onValueChange: (value: string) => void; +}> = ({label, value, options, onValueChange}) => ( + nextValue && onValueChange(nextValue)}> + {options.map(option => ( + + + + + + + {option.label} + + ))} + +); + const ReplyToEmailField: React.FC<{ newsletter: Newsletter; updateNewsletter: (fields: Partial) => void; @@ -139,7 +166,7 @@ const Sidebar: React.FC<{
Existing posts previously sent as this newsletter will remain unchanged.
, okLabel: 'Archive', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { try { await editNewsletter({...newsletter, status: 'archived'}); @@ -279,7 +306,7 @@ const Sidebar: React.FC<{
- {newsletter.status === 'active' ? (!onlyOne && ) : }
}, @@ -501,32 +528,15 @@ const Sidebar: React.FC<{
Title alignment
- updateNewsletter({title_alignment: 'left'}), - disabled: !newsletter.show_post_title_section - }, - { - key: 'center', - icon: 'align-center', - iconSize: 14, - label: 'Align center', - tooltip: 'Center', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({title_alignment: 'center'}), - disabled: !newsletter.show_post_title_section - } - ]} clearBg={false} /> + updateNewsletter({title_alignment: titleAlignment})} + />
@@ -575,68 +585,28 @@ const Sidebar: React.FC<{
Button style
- updateNewsletter({button_style: 'fill'}) - }, - { - key: 'outline', - icon: 'squircle', - iconSize: 14, - label: 'Outline', - tooltip: 'Outline', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({button_style: 'outline'}) - } - ]} clearBg={false} /> + updateNewsletter({button_style: buttonStyle})} + />
Button corners
- updateNewsletter({button_corners: 'square'}) - }, - { - key: 'rounded', - icon: 'squircle', - iconSize: 14, - label: 'Rounded', - tooltip: 'Rounded', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({button_corners: 'rounded'}) - }, - { - key: 'pill', - icon: 'circle', - iconSize: 14, - label: 'Pill', - tooltip: 'Pill', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({button_corners: 'pill'}) - } - ]} clearBg={false} /> + updateNewsletter({button_corners: buttonCorners})} + />
Link style
- updateNewsletter({link_style: 'underline'}) - }, - { - key: 'regular', - icon: 'text-regular', - iconSize: 14, - label: 'Regular', - tooltip: 'Regular', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({link_style: 'regular'}) - }, - { - key: 'bold', - icon: 'text-bold', - iconSize: 14, - label: 'Bold', - tooltip: 'Bold', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({link_style: 'bold'}) - } - ]} clearBg={false} /> + updateNewsletter({link_style: linkStyle})} + />
Image corners
- updateNewsletter({image_corners: 'square'}) - }, - { - key: 'rounded', - icon: 'squircle', - iconSize: 14, - label: 'Rounded', - tooltip: 'Rounded', - hideLabel: true, - link: false, - size: 'sm', - onClick: () => updateNewsletter({image_corners: 'rounded'}) - } - ]} clearBg={false} /> + updateNewsletter({image_corners: imageCorners})} + />
> = ( const container = ( - {(props.dragHandleAttributes || isDragging) &&
+ {(props.dragHandleAttributes || isDragging) && -
} + }
-
); diff --git a/apps/admin-x-settings/src/components/settings/email/newsletters/newsletters-tab-content.tsx b/apps/admin-x-settings/src/components/settings/email/newsletters/newsletters-tab-content.tsx index fb2798103d0..46388f6597e 100644 --- a/apps/admin-x-settings/src/components/settings/email/newsletters/newsletters-tab-content.tsx +++ b/apps/admin-x-settings/src/components/settings/email/newsletters/newsletters-tab-content.tsx @@ -3,10 +3,12 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React, {type ReactNode, useEffect, useState} from 'react'; import useQueryParams from '../../../../hooks/use-query-params'; import {APIError} from '@tryghost/admin-x-framework/errors'; -import {Button, ConfirmationModal} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; +import {ConfirmationModal} from '@tryghost/admin-x-design-system'; import {type InfiniteData, useQueryClient} from '@tryghost/admin-x-framework'; import {type Newsletter, type NewslettersResponseType, newslettersDataType, useBrowseNewsletters, useEditNewsletter, useVerifyNewsletterEmail} from '@tryghost/admin-x-framework/api/newsletters'; import {arrayMove} from '@dnd-kit/sortable'; +import {formatNumber} from '@tryghost/shade/utils'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; import {withErrorBoundary} from '../../../error-boundary'; @@ -15,10 +17,10 @@ const NavigateToNewsletter = ({id, children}: {id: string; children: ReactNode}) const modal = useModal(); const {updateRoute} = useRouting(); - return ; + }}>{children}; }; const isNewsletterVerificationRoute = () => { @@ -144,11 +146,9 @@ const NewslettersTabContent: React.FC = ({filter}) = ) : ( )} - {isEnd === false && } ); }; 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 50c9a960ed8..32a8115bf7a 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 @@ -212,8 +212,8 @@ const InviteUserModal = NiceModal.create(() => { updateRoute('staff'); }} cancelLabel='' - okColor={saveState === 'error' || !!errors.email ? 'red' : 'black'} okLabel={okLabel} + okVariant={saveState === 'error' || !!errors.email ? 'destructive' : 'default'} testId='invite-user-modal' title='Invite a new staff user' width={540} 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 077eed36b91..0c64079d440 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 @@ -193,7 +193,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel ), okLabel: _user.status === 'inactive' ? 'Un-suspend' : 'Suspend', okRunningLabel: _user.status === 'inactive' ? 'Un-suspending...' : 'Suspending...', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { const updatedUserData = { ..._user, @@ -221,7 +221,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel ), okLabel: 'Delete user', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { onDeletingUserChange(true); try { @@ -244,7 +244,7 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel title: 'Transfer Ownership', prompt: 'Are you sure you want to transfer the ownership of this blog? You will not be able to undo this action.', okLabel: 'Yep — I\'m sure', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { try { await makeOwner(user.id); @@ -328,8 +328,8 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel cancelLabel='Close' dirty={saveState === 'unsaved'} hideXOnMobile={true} - okColor={okProps.color} okLabel={okProps.label || 'Save'} + okVariant={okProps.variant} size={canAccessSettings(currentUser) ? 'md' : 'bleed'} stickyFooter={true} testId='user-detail-modal' @@ -369,11 +369,11 @@ const UserDetailModalContent: React.FC<{user: User; onDeletingUserChange: (isDel
{formState.cover_image ?
- {editor.isEnabled && } - +
: handleImageUpload('cover_image', files[0])}>Upload cover image} {showMenu &&
diff --git a/apps/admin-x-settings/src/components/settings/general/users.tsx b/apps/admin-x-settings/src/components/settings/general/users.tsx index f7d09c59515..cc0bb1c4cdf 100644 --- a/apps/admin-x-settings/src/components/settings/general/users.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users.tsx @@ -4,7 +4,7 @@ import clsx from 'clsx'; import useQueryParams from '../../../hooks/use-query-params'; import useStaffUsers from '../../../hooks/use-staff-users'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Avatar, NoValueLabel, NoValueLabelIcon, Separator, Switch, Tabs, TabsContent, TabsList, TabsTrigger, TabsTriggerCount} from '@tryghost/shade/components'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {type User, hasAdminAccess, isContributorUser, isEditorUser} from '@tryghost/admin-x-framework/api/users'; import {type UserInvite, useAddInvite, useDeleteInvite} from '@tryghost/admin-x-framework/api/invites'; import {UserRoundX} from 'lucide-react'; @@ -50,7 +50,10 @@ const Owner: React.FC = ({user}) => {
- {user.name} — Owner {hasAdminAccess(currentUser) && } + {user.name} — Owner {hasAdminAccess(currentUser) && } {user.email}
@@ -107,7 +110,7 @@ const UsersList: React.FC = ({users, groupname}) => {
)} - {canEdit && } ); })} @@ -134,9 +137,11 @@ const UserInviteActions: React.FC<{invite: UserInvite}> = ({invite}) => { return (
); }; @@ -228,9 +234,9 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords, }; const buttons = ( - ); const tabParam = useQueryParams().getParam('tab'); @@ -280,15 +286,15 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords, )} {hasNextPage && selectedTab !== 'invited' && } {invitesHasNextPage && selectedTab === 'invited' && } {config?.security?.staffDeviceVerification && hasAdminAccess(currentUser) && (
1 || invites.length > 0 ? '-mt-6' : ''}`}> diff --git a/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx b/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx index 0acf3b743a9..7eaf64a1ac0 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/change-password-form.tsx @@ -1,4 +1,5 @@ -import {Button, TextField} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; +import {TextField} from '@tryghost/admin-x-design-system'; import {type User, useUpdatePassword} from '@tryghost/admin-x-framework/api/users'; import {ValidationError} from '@tryghost/admin-x-framework/errors'; import {toast} from 'sonner'; @@ -205,8 +206,8 @@ const ChangePasswordForm: React.FC<{user: User}> = ({user}) => { />
); @@ -249,7 +249,7 @@ const ChangePasswordForm: React.FC<{user: User}> = ({user}) => { const initialView = (
-
); diff --git a/apps/admin-x-settings/src/components/settings/general/users/staff-token.tsx b/apps/admin-x-settings/src/components/settings/general/users/staff-token.tsx index 4bf07658a9b..13f3c9a3bd9 100644 --- a/apps/admin-x-settings/src/components/settings/general/users/staff-token.tsx +++ b/apps/admin-x-settings/src/components/settings/general/users/staff-token.tsx @@ -29,7 +29,7 @@ const StaffToken: React.FC = () => { title: 'Regenerate your Staff Access Token', prompt: 'You can regenerate your Staff Access Token any time, but any scripts or applications using it will need to be updated.', okLabel: 'Regenerate your Staff Access Token', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { try { const newAPI = await newApiKey([]); diff --git a/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form.tsx b/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form.tsx index d23c34e574f..912e19fea1f 100644 --- a/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-form.tsx @@ -1,6 +1,6 @@ import React from 'react'; import TopLevelGroup from '../../../top-level-group'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {useRouting} from '@tryghost/admin-x-framework/routing'; const EmbedSignupForm: React.FC<{ keywords: string[] }> = ({keywords}) => { @@ -11,7 +11,7 @@ const EmbedSignupForm: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={} description="Grow your audience from anywhere on the web" keywords={keywords} navid='embed-signup-form' diff --git a/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-sidebar.tsx b/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-sidebar.tsx index a82f6009796..b669739ed4c 100644 --- a/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-sidebar.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/embed-signup/embed-signup-sidebar.tsx @@ -1,7 +1,7 @@ import ColorPickerField from '../../../color-picker-field'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {Button, ButtonGroup, Form} from '@tryghost/admin-x-design-system'; -import {Combobox, ComboboxContent, ComboboxTrigger, ComboboxValue, Field, FieldDescription, FieldLabel, MultiSelectCombobox, StickyFooter, Textarea} from '@tryghost/shade/components'; +import {Button, Combobox, ComboboxContent, ComboboxTrigger, ComboboxValue, Field, FieldDescription, FieldLabel, MultiSelectCombobox, StickyFooter, Textarea, ToggleGroup, ToggleGroupItem} from '@tryghost/shade/components'; +import {Form} from '@tryghost/admin-x-design-system'; import {type Label} from '@tryghost/admin-x-framework/api/labels'; import {Plus} from 'lucide-react'; import {Text} from '@tryghost/shade/primitives'; @@ -102,26 +102,10 @@ const EmbedSignupSidebar: React.FC = ({selectedLayout,
Layout
- handleLayoutSelect('all-in-one') - }, - { - key: 'minimal', - label: 'Minimal', - size: 'md', - className: 'w-auto px-3!', - onClick: () => handleLayoutSelect('minimal') - } - ]} - clearBg={false} - /> + value && handleLayoutSelect(value)}> + Branded + Minimal +
{ selectedLayout === 'all-in-one' && @@ -208,8 +192,8 @@ const EmbedSignupSidebar: React.FC = ({selectedLayout,
- +
diff --git a/apps/admin-x-settings/src/components/settings/growth/explore.tsx b/apps/admin-x-settings/src/components/settings/growth/explore.tsx index 732896f465d..5422eb39c61 100644 --- a/apps/admin-x-settings/src/components/settings/growth/explore.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/explore.tsx @@ -3,8 +3,9 @@ import React, {useEffect, useState} from 'react'; import SettingImg from '../../../assets/images/ghost-explore.png'; import TopLevelGroup from '../../top-level-group'; import useSettingGroup from '../../../hooks/use-setting-group'; -import {Button, Icon, SettingGroupContent} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {Field, FieldContent, FieldDescription, FieldLabel, Separator, Switch} from '@tryghost/shade/components'; +import {Icon, SettingGroupContent} from '@tryghost/admin-x-design-system'; import {type Setting, getSettingValue, getSettingValues, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; import {abbreviateNumber} from '@tryghost/shade/utils'; import {useBrowseMembers} from '@tryghost/admin-x-framework/api/members'; @@ -125,9 +126,12 @@ const Explore: React.FC<{ keywords: string[] }> = ({keywords}) => { Get featured on the Ghost.org homepage Send us a quote we can use to highlight your site
-
: 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 ed1762b86ce..9e319fc3b82 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 @@ -4,7 +4,8 @@ import JoelWarner from '../../../../assets/images/joel-warner.png'; import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React from 'react'; import {Avatar, Field, FieldError, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components'; -import {Button, Form, Modal} from '@tryghost/admin-x-design-system'; +import {Button, LoadingIndicator} from '@tryghost/shade/components'; +import {Form, Modal} from '@tryghost/admin-x-design-system'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; import {toast} from 'sonner'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; @@ -219,14 +220,15 @@ const TestimonialsModal = NiceModal.create(() => {
diff --git a/apps/admin-x-settings/src/components/settings/growth/offers.tsx b/apps/admin-x-settings/src/components/settings/growth/offers.tsx index 3dee9ab6c02..4792489d2bb 100644 --- a/apps/admin-x-settings/src/components/settings/growth/offers.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/offers.tsx @@ -1,6 +1,6 @@ import React from 'react'; import TopLevelGroup from '../../top-level-group'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {checkStripeEnabled} from '@tryghost/admin-x-framework/api/settings'; import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {useBrowseOffers} from '@tryghost/admin-x-framework/api/offers'; @@ -29,7 +29,7 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={} description={<>Create discounts & coupons to boost new subscriptions and retain existing members.{' '}Learn more} keywords={keywords} navid='offers' @@ -40,7 +40,7 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => { (
You must have an active tier to create an offer. {` `} -
) : '' } 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 3d765b488ef..734dc9693bf 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 @@ -1,7 +1,6 @@ import PortalFrame from '../../membership/portal/portal-frame'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button, Field, FieldContent, FieldDescription, FieldError, FieldLabel, InputGroup, InputGroupAddon, InputGroupInput, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components'; import {type ErrorMessages, useForm} from '@tryghost/admin-x-framework/hooks'; -import {Field, FieldContent, FieldDescription, FieldError, FieldLabel, InputGroup, InputGroupAddon, InputGroupInput, RadioGroup, RadioGroupItem, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Textarea} from '@tryghost/shade/components'; import {Form, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {JSONError} from '@tryghost/admin-x-framework/errors'; import {formatNumber} from '@tryghost/shade/utils'; @@ -307,7 +306,7 @@ const Sidebar: React.FC = ({tierOptions,
{homepageUrl}{overrides.code.value}
: null)} placeholder='black-friday' title='Offer code' value={overrides.code.value} @@ -671,8 +670,8 @@ const AddOfferModal = () => { cancelLabel='Cancel' dirty={saveState === 'unsaved'} height='full' - okColor={okProps.color} okLabel='Publish' + okVariant={okProps.variant} preview={iframe} previewToolbar={false} sidebar={sidebar} 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 2dd9aeb2aee..91d0b3fe89f 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 @@ -1,9 +1,9 @@ import NiceModal from '@ebay/nice-modal-react'; import PortalFrame from '../../membership/portal/portal-frame'; import SettingsBreadcrumbs from '../../settings-breadcrumbs'; -import {Button, ConfirmationModal, Form, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; +import {Button, Field, FieldLabel, Textarea} from '@tryghost/shade/components'; +import {ConfirmationModal, Form, PreviewModalContent, TextField} from '@tryghost/admin-x-design-system'; import {type ErrorMessages, useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; -import {Field, FieldLabel, Textarea} from '@tryghost/shade/components'; import {JSONError} from '@tryghost/admin-x-framework/errors'; import {type Offer, useBrowseOffersById, useEditOffer} from '@tryghost/admin-x-framework/api/offers'; import {createOfferRedemptionFilterUrl} from './offer-helpers'; @@ -63,7 +63,7 @@ const Sidebar: React.FC<{

All members that previously redeemed {offer?.name} will remain unchanged.

, okLabel: 'Archive', - okColor: 'red', + okVariant: 'destructive', onOk: async (modal) => { try { await editOffer({...offer, status: 'archived'}); @@ -144,7 +144,7 @@ const Sidebar: React.FC<{ error={Boolean(errors.code)} hint={errors.code || (offer?.code !== '' ? {homepageUrl}{offer?.code} : null)} placeholder='black-friday' - rightPlaceholder={offer?.code !== '' ? : null} title='Offer code' value={offer?.code ?? ''} onChange={e => updateOffer({code: e.target.value})} @@ -167,7 +167,7 @@ const Sidebar: React.FC<{
- {offer?.status === 'active' ? : }
); @@ -263,8 +263,8 @@ const EditOfferModal: React.FC<{id: string}> = ({id}) => { cancelLabel='Cancel' dirty={saveState === 'unsaved'} height='full' - okColor={okProps.color} okLabel={okProps.label || 'Save'} + okVariant={okProps.variant} preview={iframe} previewToolbarBreadcrumbs={ = ({id}) => { cancelLabel='Cancel' dirty={saveState === 'unsaved'} height='full' - okColor={okProps.color} okLabel={okProps.label || 'Save'} + okVariant={okProps.variant} preview={preview} previewToolbarBreadcrumbs={ = ({id}) => { discount = offer?.amount + '% discount'; break; case 'fixed': - discount = numberWithCommas(currencyToDecimal(offer?.amount)) + ' ' + offer?.currency + ' discount'; + discount = formatNumber(currencyToDecimal(offer?.amount), {maximumFractionDigits: 2}) + ' ' + offer?.currency + ' discount'; break; case 'trial': discount = offer?.amount + ' days free trial'; @@ -95,13 +95,13 @@ const OfferSuccess: React.FC<{id: string}> = ({id}) => {
-
OR
- + +
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 fa9a9f18098..5cb792eab0d 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,6 +1,4 @@ -import {Badge, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components'; -import {Button, type ButtonProps} from '@tryghost/admin-x-design-system'; -import {ButtonGroup} from '@tryghost/admin-x-design-system'; +import {Badge, Button, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuTrigger, Table, TableBody, TableCell, TableHead, TableHeader, TableRow} from '@tryghost/shade/components'; import {Icon} from '@tryghost/admin-x-design-system'; import {Inline, Stack} from '@tryghost/shade/primitives'; import {LucideIcon, formatNumber} from '@tryghost/shade/utils'; @@ -76,7 +74,9 @@ const OffersFilterMenu: React.FC<{ return ( - Sort by @@ -328,29 +328,24 @@ export const OffersIndexModal: React.FC = () => { return offer.status === 'archived' || offerTier?.active === false; }; - const buttons: ButtonProps[] = [ - { - key: 'cancel-modal', - label: 'Close', - onClick: () => { + const actions = ( + + + + + ); const listLayoutOutput =
@@ -429,7 +424,7 @@ export const OffersIndexModal: React.FC = () => { size='lg' testId='offers-modal' title='Offers' - topRightContent={} + topRightContent={actions} width={1140} > diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations.tsx index 3ebf2e95788..226325b7007 100644 --- a/apps/admin-x-settings/src/components/settings/growth/recommendations.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/recommendations.tsx @@ -3,7 +3,7 @@ import React, {useState} from 'react'; import RecommendationList from './recommendations/recommendation-list'; import TopLevelGroup from '../../top-level-group'; import useSettingGroup from '../../../hooks/use-setting-group'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {Tabs, TabsContent, TabsList, TabsTrigger, TabsTriggerCount} from '@tryghost/shade/components'; import {formatNumber} from '@tryghost/shade/utils'; import {keepPreviousData} from '@tanstack/react-query'; @@ -101,9 +101,9 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => { }; const buttons = ( - ); return ( @@ -119,9 +119,9 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => { onSave={handleSave} >
-
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 0ae5325aef4..1858b497962 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 @@ -3,8 +3,9 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React from 'react'; import RecommendationDescriptionForm, {validateDescriptionForm} from './recommendation-description-form'; import trackEvent from '../../../../utils/analytics'; +import {Button} from '@tryghost/shade/components'; import {type EditOrAddRecommendation, useAddRecommendation} from '@tryghost/admin-x-framework/api/recommendations'; -import {Modal} from '@tryghost/admin-x-design-system'; +import {Icon, Modal} from '@tryghost/admin-x-design-system'; import {toast} from 'sonner'; import {useForm, useHandleError} from '@tryghost/admin-x-framework/hooks'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -49,13 +50,8 @@ const AddRecommendationModalConfirm: React.FC = ({r okLabel = 'Added'; } - const leftButtonProps = { - label: 'Back', - icon: 'arrow-left', - iconColorClass: 'text-black dark:text-white', - link: true, - size: 'sm' as const, - onClick: () => { + const leftButton = ( + + ); return { @@ -82,10 +81,10 @@ const AddRecommendationModalConfirm: React.FC = ({r backDropClick={false} cancelLabel={'Cancel'} dirty={true} - leftButtonProps={leftButtonProps} - okColor='black' + leftButton={leftButton} okLabel={okLabel} okLoading={loadingState} + okVariant='default' size='sm' testId='add-recommendation-modal' title={'Add recommendation'} 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 ee30dd23555..8d32417d44f 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 @@ -183,9 +183,9 @@ const AddRecommendationModal: React.FC { + const leftButton = ( + + ); return { @@ -70,9 +68,9 @@ const EditRecommendationModal: React.FC
-
)} @@ -98,7 +97,7 @@ const IncomingRecommendationList: React.FC = ({ {incomingRecommendations.map(rec => )} - {showMore?.hasMore &&
} + {showMore?.hasMore &&
} ; } else { return diff --git a/apps/admin-x-settings/src/components/settings/growth/recommendations/recommendation-list.tsx b/apps/admin-x-settings/src/components/settings/growth/recommendations/recommendation-list.tsx index bb8a51f5b47..f289913180b 100644 --- a/apps/admin-x-settings/src/components/settings/growth/recommendations/recommendation-list.tsx +++ b/apps/admin-x-settings/src/components/settings/growth/recommendations/recommendation-list.tsx @@ -3,8 +3,8 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {useState} from 'react'; import RecommendationIcon from './recommendation-icon'; import useSettingGroup from '../../../../hooks/use-setting-group'; -import {ActionList, ActionListItem, ActionListItemContent, LoadingIndicator, NoValueLabel, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@tryghost/shade/components'; -import {Button} from '@tryghost/admin-x-design-system'; +import {ActionList, ActionListItem, ActionListItemContent, Button, LoadingIndicator, NoValueLabel, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from '@tryghost/shade/components'; +import {Icon} from '@tryghost/admin-x-design-system'; import {Inline} from '@tryghost/shade/primitives'; import {type Recommendation} from '@tryghost/admin-x-framework/api/recommendations'; import {formatNumber} from '@tryghost/shade/utils'; @@ -95,22 +95,20 @@ const RecommendationList: React.FC = ({recommendations, {recommendations.map(recommendation => )}
- {showMore?.hasMore && } + {showMore?.hasMore && }
Shared with new members after signup, or anytime using this link {copied ? 'Copied' : 'Copy link'} @@ -120,9 +118,9 @@ const RecommendationList: React.FC = ({recommendations, ; } else { return - + }}>Add first recommendation Need inspiration? Explore thousands of sites ; } diff --git a/apps/admin-x-settings/src/components/settings/membership/custom-fields.tsx b/apps/admin-x-settings/src/components/settings/membership/custom-fields.tsx index ac24306d08d..463eeb479eb 100644 --- a/apps/admin-x-settings/src/components/settings/membership/custom-fields.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/custom-fields.tsx @@ -3,8 +3,8 @@ import NiceModal from '@ebay/nice-modal-react'; import React, {useEffect, useRef, useState} from 'react'; import TopLevelGroup from '../../top-level-group'; import useFeatureFlag from '../../../hooks/use-feature-flag'; -import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, NoValueLabel, NoValueLabelIcon, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; -import {Button, Icon} from '@tryghost/admin-x-design-system'; +import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Button, NoValueLabel, NoValueLabelIcon, Tabs, TabsContent, TabsList, TabsTrigger} from '@tryghost/shade/components'; +import {Icon} from '@tryghost/admin-x-design-system'; import {TextCursorInput} from 'lucide-react'; import {useBrowseMemberCustomFieldsIncludingArchived, userTypeForField} from '@tryghost/admin-x-framework/api/member-custom-fields'; import {withErrorBoundary} from '../../error-boundary'; @@ -56,7 +56,7 @@ const FieldList: React.FC<{ - ); @@ -112,7 +112,7 @@ const CustomFields: React.FC<{keywords: string[]}> = ({keywords}) => { return ( openModal()} />} + customButtons={} description='Create and manage custom fields to store extra information about your members' keywords={keywords} navid='custom-fields' 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 d994040fbad..afd2861124c 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 @@ -1,7 +1,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react'; import React from 'react'; -import {Button, ConfirmationModal, Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; -import {DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, FieldDescription, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@tryghost/shade/components'; +import {Button, DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, Field, FieldDescription, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@tryghost/shade/components'; +import {ConfirmationModal, Form, Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; 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'; @@ -79,12 +79,8 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) // The modal's third action mirrors the field's state: an active field can // be archived, an archived one reactivated. Both confirm first (the // newsletters pattern) — they change what every collection surface shows. - const archiveButtonProps = { - label: 'Archive', - link: true, - color: 'red' as const, - size: 'sm' as const, - onClick: () => { + const archiveButton = ( + + ); - const reactivateButtonProps = { - label: 'Reactivate', - link: true, - color: 'green' as const, - size: 'sm' as const, - onClick: () => { + const reactivateButton = ( + + ); - let leftButtonProps; + let leftButton; if (isEdit) { - leftButtonProps = isArchived ? reactivateButtonProps : archiveButtonProps; + leftButton = isArchived ? reactivateButton : archiveButton; } // Permanent deletion hides behind the header menu — one deliberate click @@ -154,7 +146,7 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) title: 'Delete custom field', prompt: <>{field!.name} and every value collected from your members will be permanently deleted from the database. This can’t be undone., okLabel: 'Delete', - okColor: 'red', + okVariant: 'destructive', onOk: async (deleteModal) => { try { await deleteField(field!.key); @@ -171,7 +163,9 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) const archivedFieldMenu = ( - @@ -186,9 +180,9 @@ const CustomFieldModal = NiceModal.create<{field?: MemberCustomField}>(({field}) )} - +
@@ -282,11 +281,13 @@ const MemberEmails: React.FC<{ keywords: string[] }> = ({keywords}) => { customButtons={( )} description="Create and manage automated emails for your members" keywords={keywords} diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails/test-email-dropdown.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails/test-email-dropdown.tsx index 636ae8bd760..21ec1d419b9 100644 --- a/apps/admin-x-settings/src/components/settings/membership/member-emails/test-email-dropdown.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/member-emails/test-email-dropdown.tsx @@ -1,7 +1,8 @@ import validator from 'validator'; -import {Button, TextField} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {FieldError, PopoverContent} from '@tryghost/shade/components'; import {JSONError} from '@tryghost/admin-x-framework/errors'; +import {TextField} from '@tryghost/admin-x-design-system'; import {useCurrentUser} from '@tryghost/admin-x-framework/api/current-user'; import {useEffect, useRef, useState} from 'react'; import {useSendTestWelcomeEmail} from '@tryghost/admin-x-framework/api/automated-emails'; @@ -97,11 +98,12 @@ const TestEmailDropdown: React.FC = ({
{testEmailError && {testEmailError}} ); diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx index 0600e8eb61a..5c0a434f374 100644 --- a/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-customize-modal.tsx @@ -517,7 +517,7 @@ const WelcomeEmailCustomizeModal = NiceModal.create(() => { const fetchErrorMessage = 'Unable to load email design settings. Please try again.'; const modalOkProps = hasSaveError ? { ...okProps, - color: 'red' as const, + variant: 'destructive' as const, label: 'Retry' } : okProps; 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 228c07011b9..ec5958f8613 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} from '@tryghost/shade/components'; -import {Button as LegacyButton, Modal, TextField} from '@tryghost/admin-x-design-system'; +import {Icon, Modal, TextField} from '@tryghost/admin-x-design-system'; 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'; @@ -234,9 +234,10 @@ const WelcomeEmailModal = NiceModal.create(({emailType = className='dark:bg-[#151719]' headerActions={ <> - + {showTestDropdown && ( diff --git a/apps/admin-x-settings/src/components/settings/membership/portal.tsx b/apps/admin-x-settings/src/components/settings/membership/portal.tsx index bbf4594473c..940a08ed98a 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal.tsx @@ -2,7 +2,7 @@ import FakeLogo from '../../../assets/images/portal-splash-default-logo.png'; import React from 'react'; import TopLevelGroup from '../../top-level-group'; import UserAddIcon from '../../../assets/images/portal-splash-user-add.png'; -import {Button} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; import {useGlobalData} from '../../providers/global-data-provider'; import {useRouting} from '@tryghost/admin-x-framework/routing'; @@ -37,7 +37,7 @@ const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => { return ( } + customButtons={} description="Customize members modal signup flow" keywords={keywords} navid='portal' diff --git a/apps/admin-x-settings/src/components/settings/membership/portal/portal-links.tsx b/apps/admin-x-settings/src/components/settings/membership/portal/portal-links.tsx index 24c4a5ef434..646146c8531 100644 --- a/apps/admin-x-settings/src/components/settings/membership/portal/portal-links.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/portal/portal-links.tsx @@ -1,6 +1,7 @@ import React, {useEffect, useId, useState} from 'react'; import {ActionList, ActionListItem, ActionListItemActions, ActionListItemContent, Field, FieldLabel, Select, SelectContent, SelectItem, SelectTrigger, SelectValue} from '@tryghost/shade/components'; -import {Button, ModalPage, TextField} from '@tryghost/admin-x-design-system'; +import {Button} from '@tryghost/shade/components'; +import {ModalPage, TextField} from '@tryghost/admin-x-design-system'; import {getHomepageUrl} from '@tryghost/admin-x-framework/api/site'; import {getPaidActiveTiers, useBrowseTiers} from '@tryghost/admin-x-framework/api/tiers'; import {getSettingValues} from '@tryghost/admin-x-framework/api/settings'; @@ -20,14 +21,18 @@ const PortalLink: React.FC = ({name, value}) => { - ); }; @@ -66,7 +71,7 @@ const PortalLinks: React.FC = () => {

Generic

-
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 e61ca1b1342..d79cb46c3d5 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 @@ -231,8 +231,8 @@ const PortalModal: React.FC = () => { buttonsDisabled={okProps.disabled} cancelLabel='Close' dirty={saveState === 'unsaved'} - okColor={okProps.color} okLabel={okProps.label || 'Save'} + okVariant={okProps.variant} preview={preview} previewBgColor={selectedPreviewTab === 'links' ? 'white' : 'greygradient'} previewToolbarTabs={previewTabs} 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 e81dba1c04b..71d113078f6 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 @@ -7,8 +7,8 @@ import StripeButton from '../../../stripe-button'; import StripeLogo from '../../../../assets/images/stripe-emblem.svg'; import StripeVerifiedBadge from '../../../../assets/images/stripe-verified.svg'; import useSettingGroup from '../../../../hooks/use-setting-group'; -import {Button, ConfirmationModal, Form, LimitModal, Modal, TextField} from '@tryghost/admin-x-design-system'; -import {Field, FieldError, FieldLabel, Switch, Textarea} from '@tryghost/shade/components'; +import {Button, Field, FieldError, FieldLabel, Switch, Textarea} from '@tryghost/shade/components'; +import {ConfirmationModal, Form, Icon, LimitModal, Modal, TextField} from '@tryghost/admin-x-design-system'; import {HostLimitError, useLimiter} from '../../../../hooks/use-limiter'; import {JSONError} from '@tryghost/admin-x-framework/errors'; import {Text} from '@tryghost/shade/primitives'; @@ -135,14 +135,14 @@ const Connect: React.FC = () => {
Click on the “Connect with Stripe” button to generate a secure key that connects your Ghost site with Stripe.
- + Step 2 — Paste secure key Secure key