mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-25 03:44:29 +03:00
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:
parent
277e169f7b
commit
d9390d2262
@ -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
|
||||
);
|
||||
|
@ -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'
|
||||
}
|
||||
};
|
@ -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>
|
||||
);
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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');
|
||||
}} />
|
||||
|
@ -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);
|
||||
}} />
|
||||
}
|
||||
|
@ -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();
|
||||
}} />
|
||||
);
|
||||
|
@ -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();
|
||||
}} />
|
||||
);
|
||||
|
@ -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'
|
||||
|
@ -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>
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
@ -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'
|
||||
|
Loading…
Reference in New Issue
Block a user