Improve discoverability of unsaved settings (#20153)

DES-195

The purpose of this change is to (1) reduce the overwhelming use of green on the settings UI in general and (2) to make unsaved sections more focused and discoverable and focused when trying to quit Settings without saving so that it's easier to find.

---------

Co-authored-by: Daniël van der Winden <danielvanderwinden@ghost.org>
This commit is contained in:
Peter Zimon 2024-05-23 10:20:27 +02:00 committed by GitHub
parent 277e169f7b
commit d9390d2262
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
18 changed files with 71 additions and 53 deletions

View File

@ -3,7 +3,7 @@ import React, {HTMLProps} from 'react';
import clsx from 'clsx';
import {LoadingIndicator, LoadingIndicatorColor, LoadingIndicatorSize} from './LoadingIndicator';
export type ButtonColor = 'clear' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
export type ButtonColor = 'clear' | 'light-grey' | 'grey' | 'black' | 'green' | 'red' | 'white' | 'outline';
export type ButtonSize = 'sm' | 'md';
export interface ButtonProps extends Omit<HTMLProps<HTMLButtonElement>, 'label' | 'size' | 'children'> {
@ -75,6 +75,13 @@ const Button: React.FC<ButtonProps> = ({
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'}`,
@ -114,7 +121,7 @@ const Button: React.FC<ButtonProps> = ({
break;
default:
className = clsx(
link ? ' text-black hover:text-grey-800 dark:text-white' : `text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`,
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
);

View File

@ -33,6 +33,14 @@ export const Default: Story = {
}
};
export const Small: Story = {
args: {
buttons: defaultButtons,
link: false,
size: 'sm'
}
};
const linkButtons: ButtonProps[] = [
{
label: 'Cancel',
@ -50,21 +58,4 @@ export const LinkButtons: Story = {
buttons: linkButtons,
link: true
}
};
export const WithBackground: Story = {
args: {
buttons: linkButtons,
link: true,
clearBg: false
}
};
export const SmallWithBackground: Story = {
args: {
buttons: linkButtons,
link: true,
clearBg: false,
size: 'sm'
}
};

View File

@ -17,7 +17,7 @@ export interface ButtonGroupProps {
const ButtonGroup: React.FC<ButtonGroupProps> = ({size = 'md', buttons, link, linkWithPadding, clearBg = true, outlineOnMobile, className}) => {
let groupColorClasses = clsx(
'flex items-center justify-start rounded',
link ? 'gap-4' : 'gap-3',
link ? 'gap-4' : 'gap-2',
className
);
@ -33,7 +33,7 @@ const ButtonGroup: React.FC<ButtonGroupProps> = ({size = 'md', buttons, link, li
return (
<div className={groupColorClasses}>
{buttons.map(({key, ...props}) => (
<Button key={key} link={link} linkWithPadding={linkWithPadding} {...props} />
<Button key={key} link={link} linkWithPadding={linkWithPadding} size={size} {...props} />
))}
</div>
);

View File

@ -78,6 +78,7 @@ const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(function Sett
styles += ' border-grey-250 dark:border-grey-925';
// The links visible before editing
const viewButtons: ButtonProps[] = [];
if (!hideEditButton) {
@ -89,7 +90,7 @@ const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(function Sett
{
label,
key: 'edit',
color: 'green',
color: 'clear',
onClick: handleEdit
}
);
@ -104,6 +105,7 @@ const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(function Sett
);
}
// The buttons that show when you are editing
const editButtons: ButtonProps[] = [
{
label: 'Cancel',
@ -119,9 +121,10 @@ const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(function Sett
}
editButtons.push(
{
label,
label: label,
key: 'save',
color: 'green',
color: saveState === 'unsaved' ? 'green' : 'light-grey',
disabled: saveState !== 'unsaved',
onClick: handleSave
}
);
@ -151,18 +154,35 @@ const SettingGroup = forwardRef<HTMLDivElement, SettingGroupProps>(function Sett
styles
);
return (
<div className={containerClasses} data-testid={testId}>
<div ref={ref} className='absolute' id={navid && navid}></div>
{customHeader ? customHeader :
<SettingGroupHeader beta={beta} description={description} title={title!}>
{customButtons ? customButtons :
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} link linkWithPadding />)}
</SettingGroupHeader>
}
{children}
</div>
);
if (!isEditing) {
return (
<div className={containerClasses} data-testid={testId}>
<div ref={ref} className='absolute' id={navid && navid}></div>
{customHeader ? customHeader :
<SettingGroupHeader beta={beta} description={description} title={title!}>
{customButtons ? customButtons :
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} className={isEditing ? 'mt-[-5px] ' : '-mr-1 mt-[-5px]'} size='sm' />)
}
</SettingGroupHeader>
}
{children}
</div>
);
} else {
return (
<div className={containerClasses} data-testid={testId}>
<div ref={ref} className='absolute' id={navid && navid}></div>
{customHeader ? customHeader :
<SettingGroupHeader beta={beta} description={description} title={title!}>
{customButtons ? customButtons :
(onEditingChange && <ButtonGroup buttons={isEditing ? editButtons : viewButtons} className={isEditing ? 'mt-[-5px] ' : '-mr-1 mt-[-5px]'} size='sm' />)
}
</SettingGroupHeader>
}
{children}
</div>
);
}
});
export default SettingGroup;

View File

@ -14,10 +14,10 @@ const SettingGroupHeader: React.FC<SettingGroupHeaderProps> = ({title, descripti
{(title || description) &&
<div>
<Heading level={5}>{title}{beta && <sup className='ml-0.5 text-[10px] font-semibold uppercase tracking-wide'>Beta</sup>}</Heading>
{description && <p className="mt-1 hidden max-w-lg group-[.is-not-editing]/setting-group:!visible group-[.is-not-editing]/setting-group:!block md:!visible md:!block">{description}</p>}
{description && <p className="mt-1 hidden max-w-md group-[.is-not-editing]/setting-group:!visible group-[.is-not-editing]/setting-group:!block md:!visible md:!block">{description}</p>}
</div>
}
<div className='-mt-0.5'>
<div>
{children}
</div>
</div>

View File

@ -10,7 +10,7 @@ const CodeInjection: React.FC<{ keywords: string[] }> = ({keywords}) => {
customHeader={
<div className='z-10 flex items-start justify-between'>
<SettingGroupHeader description='Add custom code to your publication.' title='Code injection' />
<Button color='green' label='Open' link linkWithPadding onClick={() => {
<Button className='mt-[-5px]' color='clear' label='Open' size='sm' onClick={() => {
NiceModal.show(CodeModal);
}} />
</div>

View File

@ -11,7 +11,7 @@ const History: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' label='View history' link linkWithPadding onClick={openHistoryModal}/>}
customButtons={<Button className='mt-[-5px]' color='clear' label='View history' size='sm' onClick={openHistoryModal}/>}
description="View system event log"
keywords={keywords}
navid='history'

View File

@ -226,7 +226,7 @@ const Integrations: React.FC<{ keywords: string[] }> = ({keywords}) => {
] as const;
const buttons = (
<Button className='hidden md:!visible md:!block' color='green' label='Add custom integration' link linkWithPadding onClick={() => {
<Button className='mt-[-5px] hidden md:!visible md:!block' color='clear' label='Add custom integration' size='sm' onClick={() => {
updateRoute('integrations/new');
setSelectedTab('custom');
}} />

View File

@ -33,10 +33,10 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
<SettingGroupHeader description='This is a testing ground for new or experimental features. They may change, break or inexplicably disappear at any time.' title='Labs' />
{
!isOpen ?
<Button color='green' label='Open' link linkWithPadding onClick={() => {
<Button className='mt-[-5px]' color='clear' label='Open' size='sm' onClick={() => {
setIsOpen(true);
}} /> :
<Button color='green' label='Close' link linkWithPadding onClick={() => {
<Button className='mt-[-5px]' color='grey' label='Close' size='sm' onClick={() => {
setIsOpen(false);
}} />
}

View File

@ -90,7 +90,7 @@ const Newsletters: React.FC<{ keywords: string[] }> = ({keywords}) => {
}, [verifyEmailToken, handleError, verifyEmail]);
const buttons = (
<Button color='green' label='Add newsletter' link linkWithPadding onClick={() => {
<Button className='mt-[-5px]' color='clear' label='Add newsletter' size='sm' onClick={() => {
openNewsletterModal();
}} />
);

View File

@ -221,7 +221,7 @@ const Users: React.FC<{ keywords: string[], highlight?: boolean }> = ({keywords,
};
const buttons = (
<Button color='green' label='Invite people' link={true} linkWithPadding onClick={() => {
<Button className='mt-[-5px]' color='clear' label='Invite people' size='sm' linkWithPadding onClick={() => {
showInviteModal();
}} />
);

View File

@ -83,7 +83,7 @@ const Offers: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' disabled={!checkStripeEnabled(settings, config)} label={offerButtonText} link linkWithPadding onClick={offerButtonLink}/>}
customButtons={<Button className='mt-[-5px]' color='clear' disabled={!checkStripeEnabled(settings, config)} label={offerButtonText} size='sm' onClick={offerButtonLink}/>}
description={<>Create discounts & coupons to boost new subscriptions. {allOffers.length === 0 && <><a className='text-green' href="https://ghost.org/help/offers" rel="noopener noreferrer" target="_blank">{descriptionButtonText}</a></>}</>}
keywords={keywords}
navid='offers'

View File

@ -112,7 +112,7 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
};
const buttons = (
<Button className='hidden md:!visible md:!block' color='green' label='Add recommendation' link={true} onClick={() => {
<Button className='mt-[-5px] hidden md:!visible md:!block' color='clear' label='Add recommendation' size='sm' onClick={() => {
openAddNewRecommendationModal();
}} />
);
@ -130,7 +130,7 @@ const Recommendations: React.FC<{ keywords: string[] }> = ({keywords}) => {
onSave={handleSave}
>
<div className='flex justify-center rounded border border-green px-4 py-2 md:hidden'>
<Button color='green' label='Add recommendation' link onClick={() => {
<Button color='light-grey' label='Add recommendation' link onClick={() => {
openAddNewRecommendationModal();
}} />
</div>

View File

@ -11,7 +11,7 @@ const EmbedSignupForm: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' label='Embed' link onClick={openPreviewModal}/>}
customButtons={<Button className='mt-[-5px]' color='clear' label='Embed' size='sm' onClick={openPreviewModal}/>}
description="Grow your audience from anywhere on the web"
keywords={keywords}
navid='embed-signup-form'

View File

@ -16,7 +16,7 @@ const Portal: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' disabled={membersSignupAccess === 'none'} label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
customButtons={<Button className='mt-[-5px]' color='clear' disabled={membersSignupAccess === 'none'} label='Customize' size='sm' onClick={openPreviewModal}/>}
description="Customize members modal signup flow"
keywords={keywords}
navid='portal'

View File

@ -11,7 +11,7 @@ const AnnouncementBar: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openModal}/>}
customButtons={<Button className='mt-[-5px]' color='clear' label='Customize' size='sm' onClick={openModal}/>}
description="Highlight important updates or offers"
keywords={keywords}
navid='announcement-bar'

View File

@ -11,7 +11,7 @@ const DesignSetting: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
customButtons={<Button className='mt-[-5px]' color='clear' label='Customize' size='sm' onClick={openPreviewModal}/>}
description="Customize the theme, colors, and layout of your site"
keywords={keywords}
navid='design'

View File

@ -11,7 +11,7 @@ const Navigation: React.FC<{ keywords: string[] }> = ({keywords}) => {
return (
<TopLevelGroup
customButtons={<Button color='green' label='Customize' link linkWithPadding onClick={openPreviewModal}/>}
customButtons={<Button className='mt-[-5px]' color='clear' label='Customize' size='sm' onClick={openPreviewModal}/>}
description="Set up primary and secondary menus"
keywords={keywords}
navid='navigation'