mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-04-15 03:01:37 -05:00
AdminX design cleanup (#17489)
refs. https://github.com/TryGhost/Product/issues/3349 - applied outline and fixed spacing for form groups - small UI refinements for static version of Newsletter settings - replaced textareas with textfields in site description, twitter and FB descriptions - unified pattern for "Save & close" and "Cancel" in user detail settings - refined checked background for logo container in Design settings - refined spacing in Tier detail modal - fixed gradient bug in Portal preview - fixed UI bugs in Portal / Links - fixed tier dropdown bug in Portal / Links. It was always showing links for the first tier - unified form input element headings - refined checkbox and toggle label typography and spacing
This commit is contained in:
parent
48ccea818a
commit
acd84fe25c
33 changed files with 199 additions and 127 deletions
|
@ -38,12 +38,13 @@ type HeadingLabelProps = {
|
|||
grey?: boolean } & HeadingBaseProps & React.LabelHTMLAttributes<HTMLLabelElement>
|
||||
|
||||
export const Heading6Styles = 'text-2xs font-semibold uppercase tracking-wider';
|
||||
export const Heading6StylesGrey = 'text-2xs font-semibold uppercase tracking-wider text-grey-800';
|
||||
|
||||
const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> = ({
|
||||
level,
|
||||
children,
|
||||
styles = '',
|
||||
grey,
|
||||
grey = true,
|
||||
separator,
|
||||
useLabelTag,
|
||||
className = '',
|
||||
|
@ -54,7 +55,7 @@ const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> =
|
|||
}
|
||||
|
||||
const newElement = `${useLabelTag ? 'label' : `h${level}`}`;
|
||||
styles += (level === 6 || useLabelTag) ? (` block text-2xs ${Heading6Styles} ${(grey && 'text-grey-700')}`) : ' ';
|
||||
styles += (level === 6 || useLabelTag) ? (` block ${grey ? Heading6StylesGrey : Heading6Styles}`) : ' ';
|
||||
|
||||
const Element = React.createElement(newElement, {className: styles + ' ' + className, key: 'heading-elem', ...props}, children);
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ const listItems = (
|
|||
<>
|
||||
<ListItem id='list-item-1' {...listItemProps}/>
|
||||
<ListItem id='list-item-2' {...listItemProps}/>
|
||||
<ListItem id='list-item-3' {...listItemProps}/>
|
||||
<ListItem id='list-item-3' separator={false} {...listItemProps}/>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import Heading from './Heading';
|
||||
import Hint from './Hint';
|
||||
import ListHeading from './ListHeading';
|
||||
import ListHeading, {ListHeadingSize} from './ListHeading';
|
||||
import React from 'react';
|
||||
import Separator from './Separator';
|
||||
import clsx from 'clsx';
|
||||
|
@ -15,6 +15,7 @@ interface ListProps {
|
|||
* When you use the list in a block and it's not the primary content of the page then you can set a title to the list
|
||||
*/
|
||||
title?: React.ReactNode;
|
||||
titleSize?: ListHeadingSize;
|
||||
titleSeparator?: boolean;
|
||||
children?: React.ReactNode;
|
||||
actions?: React.ReactNode;
|
||||
|
@ -24,10 +25,18 @@ interface ListProps {
|
|||
className?: string;
|
||||
}
|
||||
|
||||
const List: React.FC<ListProps> = ({title, titleSeparator, children, actions, hint, hintSeparator, borderTop, pageTitle, className}) => {
|
||||
titleSeparator = (titleSeparator === undefined) ? true : titleSeparator;
|
||||
hintSeparator = (hintSeparator === undefined) ? true : hintSeparator;
|
||||
|
||||
const List: React.FC<ListProps> = ({
|
||||
title,
|
||||
titleSeparator = true,
|
||||
titleSize = 'sm',
|
||||
children,
|
||||
actions,
|
||||
hint,
|
||||
hintSeparator = true,
|
||||
borderTop,
|
||||
pageTitle,
|
||||
className
|
||||
}) => {
|
||||
const listClasses = clsx(
|
||||
(borderTop || pageTitle) && 'border-t border-grey-300',
|
||||
pageTitle && 'mt-14',
|
||||
|
@ -38,15 +47,15 @@ const List: React.FC<ListProps> = ({title, titleSeparator, children, actions, hi
|
|||
<>
|
||||
{pageTitle && <Heading>{pageTitle}</Heading>}
|
||||
<section className={listClasses}>
|
||||
<ListHeading actions={actions} title={title} titleSeparator={!pageTitle && titleSeparator && borderTop} />
|
||||
<ListHeading actions={actions} title={title} titleSeparator={!pageTitle && titleSeparator && !borderTop} titleSize={titleSize} />
|
||||
<div className='flex flex-col'>
|
||||
{children}
|
||||
</div>
|
||||
{hint &&
|
||||
<>
|
||||
<div className='-mt-px'>
|
||||
{hintSeparator && <Separator />}
|
||||
<Hint>{hint}</Hint>
|
||||
</>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
</>
|
||||
|
|
|
@ -2,17 +2,28 @@ import Heading from './Heading';
|
|||
import React from 'react';
|
||||
import Separator from './Separator';
|
||||
|
||||
export type ListHeadingSize = 'sm' | 'lg';
|
||||
|
||||
interface ListHeadingProps {
|
||||
title?: React.ReactNode;
|
||||
titleSize?: ListHeadingSize,
|
||||
actions?: React.ReactNode;
|
||||
titleSeparator?: boolean;
|
||||
}
|
||||
|
||||
const ListHeading: React.FC<ListHeadingProps> = ({title, actions, titleSeparator}) => {
|
||||
const ListHeading: React.FC<ListHeadingProps> = ({
|
||||
title,
|
||||
titleSize = 'sm',
|
||||
actions,
|
||||
titleSeparator
|
||||
}) => {
|
||||
let heading;
|
||||
|
||||
if (title) {
|
||||
const headingTitle = <Heading grey={true} level={6}>{title}</Heading>;
|
||||
const headingTitle = titleSize === 'sm' ?
|
||||
<Heading grey={true} level={6}>{title}</Heading>
|
||||
:
|
||||
<Heading level={5}>{title}</Heading>;
|
||||
heading = actions ? (
|
||||
<div className='flex items-end justify-between gap-2'>
|
||||
{headingTitle}
|
||||
|
|
|
@ -21,12 +21,24 @@ interface ListItemProps {
|
|||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ListItem: React.FC<ListItemProps> = ({id, title, detail, action, hideActions, avatar, className, testId, separator, bgOnHover = true, onClick, children}) => {
|
||||
const ListItem: React.FC<ListItemProps> = ({
|
||||
id,
|
||||
title,
|
||||
detail,
|
||||
action,
|
||||
hideActions,
|
||||
avatar,
|
||||
className,
|
||||
testId,
|
||||
separator = true,
|
||||
bgOnHover = true,
|
||||
onClick,
|
||||
children
|
||||
}) => {
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
onClick?.(e);
|
||||
};
|
||||
|
||||
separator = (separator === undefined) ? true : separator;
|
||||
const listItemClasses = clsx(
|
||||
'group flex items-center justify-between',
|
||||
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50',
|
||||
|
|
|
@ -45,7 +45,7 @@ const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disab
|
|||
onChange={handleOnChange}
|
||||
/>
|
||||
<div className={`ml-2 flex flex-col ${hint && 'mb-2'}`}>
|
||||
<span className={`inline-block text-md ${hint && '-mb-1'}`}>{label}</span>
|
||||
<span className={`inline-block text-[1.425rem] ${hint && '-mb-1'}`}>{label}</span>
|
||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</div>
|
||||
</label>
|
||||
|
|
|
@ -18,7 +18,7 @@ const CheckboxGroup: React.FC<CheckboxGroupProps> = ({
|
|||
}) => {
|
||||
return (
|
||||
<div>
|
||||
{title && <Heading level={6}>{title}</Heading>}
|
||||
{title && <Heading grey={true} level={6}>{title}</Heading>}
|
||||
<div className='mt-2 flex flex-col gap-1'>
|
||||
{checkboxes?.map(({key, ...props}) => (
|
||||
<Checkbox key={key} {...props} />
|
||||
|
|
|
@ -38,4 +38,19 @@ export const LargeGap: Story = {
|
|||
children: formElements,
|
||||
gap: 'lg'
|
||||
}
|
||||
};
|
||||
|
||||
export const WithTitle: Story = {
|
||||
args: {
|
||||
title: 'Form group',
|
||||
children: formElements
|
||||
}
|
||||
};
|
||||
|
||||
export const Grouped: Story = {
|
||||
args: {
|
||||
title: 'Form group',
|
||||
children: formElements,
|
||||
grouped: true
|
||||
}
|
||||
};
|
|
@ -58,9 +58,19 @@ const Form: React.FC<FormProps> = ({
|
|||
grouped ? 'mb-2' : 'mb-4'
|
||||
);
|
||||
|
||||
if (grouped || title) {
|
||||
return (
|
||||
<div>
|
||||
{title && <Heading className={titleClasses} level={5}>{title}</Heading>}
|
||||
<div className={classes}>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes}>
|
||||
{title && <Heading className={titleClasses} level={5}>{title}</Heading>}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -47,7 +47,7 @@ const HtmlField: React.FC<HtmlFieldProps> = ({
|
|||
|
||||
return (
|
||||
<div className={`flex flex-col ${containerClassName}`}>
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={value ? true : false} useLabelTag={true}>{title}</Heading>}
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={true} useLabelTag={true}>{title}</Heading>}
|
||||
<div className={textFieldClasses}>
|
||||
<HtmlEditor {...props} value={value} />
|
||||
</div>
|
||||
|
|
|
@ -96,8 +96,8 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||
);
|
||||
|
||||
if (imageBWCheckedBg) {
|
||||
const dark = '#ddd';
|
||||
const light = '#f9f9f9';
|
||||
const dark = '#d9d9d9';
|
||||
const light = '#f1f1f1';
|
||||
image = (
|
||||
<div style={{
|
||||
backgroundImage: `
|
||||
|
|
|
@ -25,10 +25,10 @@ export const Default: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const Clear: Story = {
|
||||
export const WithBackground: Story = {
|
||||
args: {
|
||||
options: options,
|
||||
clearBg: true
|
||||
clearBg: false
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -52,7 +52,7 @@ const Option: React.FC<OptionProps<MultiSelectOption, true>> = ({children, ...op
|
|||
|
||||
const MultiSelect: React.FC<MultiSelectProps> = ({
|
||||
title = '',
|
||||
clearBg = false,
|
||||
clearBg = true,
|
||||
error = false,
|
||||
placeholder,
|
||||
color = 'grey',
|
||||
|
|
|
@ -24,13 +24,6 @@ export const Default: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const ClearBackground: Story = {
|
||||
args: {
|
||||
placeholder: 'Enter description',
|
||||
clearBg: true
|
||||
}
|
||||
};
|
||||
|
||||
export const WithValue: Story = {
|
||||
render: function Component(args) {
|
||||
const [, updateArgs] = useArgs();
|
||||
|
|
|
@ -2,6 +2,7 @@ import React, {useId} from 'react';
|
|||
|
||||
import Heading from '../Heading';
|
||||
import Hint from '../Hint';
|
||||
import clsx from 'clsx';
|
||||
|
||||
type ResizeOptions = 'both' | 'vertical' | 'horizontal' | 'none';
|
||||
|
||||
|
@ -35,7 +36,12 @@ const TextArea: React.FC<TextAreaProps> = ({
|
|||
}) => {
|
||||
const id = useId();
|
||||
|
||||
let styles = `border-b ${clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]'} py-2 ${error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`} ${(title && !clearBg) && `mt-2`}`;
|
||||
let styles = clsx(
|
||||
'rounded-sm border px-3 py-2',
|
||||
clearBg ? 'bg-transparent' : 'bg-grey-75',
|
||||
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-grey-800',
|
||||
title && 'mt-2'
|
||||
);
|
||||
|
||||
switch (resize) {
|
||||
case 'both':
|
||||
|
@ -57,7 +63,7 @@ const TextArea: React.FC<TextAreaProps> = ({
|
|||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
{title && <Heading grey={value ? true : false} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{title && <Heading grey={true} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
<textarea
|
||||
ref={inputRef}
|
||||
className={styles}
|
||||
|
|
|
@ -29,7 +29,7 @@ const TextField: React.FC<TextFieldProps> = ({
|
|||
type = 'text',
|
||||
inputRef,
|
||||
title,
|
||||
titleColor = 'auto',
|
||||
titleColor = 'grey',
|
||||
hideTitle,
|
||||
value,
|
||||
error,
|
||||
|
@ -50,7 +50,7 @@ const TextField: React.FC<TextFieldProps> = ({
|
|||
const id = useId();
|
||||
|
||||
const textFieldClasses = !unstyled && clsx(
|
||||
'h-10 border-b py-2',
|
||||
'h-10 w-full border-b py-2',
|
||||
clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]',
|
||||
error ? `border-red` : `${disabled ? 'border-grey-300' : 'border-grey-500 hover:border-grey-700 focus:border-black'}`,
|
||||
(title && !hideTitle && !clearBg) && `mt-2`,
|
||||
|
@ -91,15 +91,15 @@ const TextField: React.FC<TextFieldProps> = ({
|
|||
}
|
||||
|
||||
if (title || hint) {
|
||||
let titleGrey = false;
|
||||
if (titleColor === 'auto') {
|
||||
titleGrey = value ? true : false;
|
||||
} else {
|
||||
titleGrey = titleColor === 'grey' ? true : false;
|
||||
}
|
||||
// let titleGrey = false;
|
||||
// if (titleColor === 'auto') {
|
||||
// titleGrey = value ? true : false;
|
||||
// } else {
|
||||
// titleGrey = titleColor === 'grey' ? true : false;
|
||||
// }
|
||||
return (
|
||||
<div className={`flex flex-col ${containerClassName}`}>
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={titleGrey} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{title && <Heading className={hideTitle ? 'sr-only' : ''} grey={true} htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
{field}
|
||||
{hint && <Hint className={hintClassName} color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</div>
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import React, {useId} from 'react';
|
||||
import Separator from '../Separator';
|
||||
import clsx from 'clsx';
|
||||
import {Heading6Styles} from '../Heading';
|
||||
import {Heading6StylesGrey} from '../Heading';
|
||||
|
||||
type ToggleSizes = 'sm' | 'md' | 'lg';
|
||||
type ToggleDirections = 'ltr' | 'rtl';
|
||||
|
@ -14,7 +14,7 @@ interface ToggleProps {
|
|||
label?: React.ReactNode;
|
||||
labelStyle?: 'heading' | 'value';
|
||||
labelClasses?: string;
|
||||
toggleBg?: 'green' | 'stripetest';
|
||||
toggleBg?: 'green' | 'black' | 'stripetest';
|
||||
separator?: boolean;
|
||||
direction?: ToggleDirections;
|
||||
hint?: React.ReactNode;
|
||||
|
@ -27,7 +27,7 @@ const Toggle: React.FC<ToggleProps> = ({
|
|||
label,
|
||||
labelStyle = 'value',
|
||||
labelClasses,
|
||||
toggleBg = 'green',
|
||||
toggleBg = 'black',
|
||||
hint,
|
||||
separator,
|
||||
error,
|
||||
|
@ -70,9 +70,13 @@ const Toggle: React.FC<ToggleProps> = ({
|
|||
toggleBgClass = 'checked:bg-[#EC6803]';
|
||||
break;
|
||||
|
||||
default:
|
||||
case 'green':
|
||||
toggleBgClass = 'checked:bg-green';
|
||||
break;
|
||||
|
||||
default:
|
||||
toggleBgClass = 'checked:bg-black';
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
|
@ -88,7 +92,7 @@ const Toggle: React.FC<ToggleProps> = ({
|
|||
<label className={`flex flex-col hover:cursor-pointer ${direction === 'rtl' && 'order-1'} ${labelStyles}`} htmlFor={id}>
|
||||
{
|
||||
labelStyle === 'heading' ?
|
||||
<span className={`${Heading6Styles} mt-1`}>{label}</span>
|
||||
<span className={`${Heading6StylesGrey} mt-1`}>{label}</span>
|
||||
:
|
||||
<span>{label}</span>
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ interface ModalPageProps {
|
|||
|
||||
const ModalPage: React.FC<ModalPageProps> = ({heading, children, className}) => {
|
||||
className = clsx(
|
||||
'h-full w-full p-[8vmin] pt-5',
|
||||
'w-full p-[8vmin] pt-5',
|
||||
className
|
||||
);
|
||||
return (
|
||||
|
|
|
@ -7,6 +7,7 @@ import NiceModal, {useModal} from '@ebay/nice-modal-react';
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import Select, {SelectOption} from '../form/Select';
|
||||
import TabView, {Tab} from '../TabView';
|
||||
import clsx from 'clsx';
|
||||
import useGlobalDirtyState from '../../../hooks/useGlobalDirtyState';
|
||||
import {ButtonProps} from '../Button';
|
||||
import {confirmIfDirty} from '../../../utils/modals';
|
||||
|
@ -27,7 +28,7 @@ export interface PreviewModalProps {
|
|||
rightToolbar?: boolean;
|
||||
deviceSelector?: boolean;
|
||||
previewToolbarURLs?: SelectOption[];
|
||||
previewBgColor?: 'grey' | 'white';
|
||||
previewBgColor?: 'grey' | 'white' | 'greygradient';
|
||||
selectedURL?: string;
|
||||
previewToolbarTabs?: Tab[];
|
||||
defaultTab?: string;
|
||||
|
@ -144,8 +145,20 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||
/>
|
||||
);
|
||||
|
||||
let previewBgClass = '';
|
||||
if (previewBgColor === 'grey') {
|
||||
previewBgClass = 'bg-grey-50';
|
||||
} else if (previewBgColor === 'greygradient') {
|
||||
previewBgClass = 'bg-gradient-to-tr from-white to-[#f9f9fa]';
|
||||
}
|
||||
|
||||
const containerClasses = clsx(
|
||||
'min-w-100 absolute inset-y-0 left-0 right-[400px] flex grow flex-col overflow-y-scroll',
|
||||
previewBgClass
|
||||
);
|
||||
|
||||
preview = (
|
||||
<div className={`min-w-100 absolute inset-y-0 left-0 right-[400px] flex grow flex-col overflow-y-scroll ${previewBgColor === 'grey' ? 'bg-grey-50' : 'bg-white'}`}>
|
||||
<div className={containerClasses}>
|
||||
{previewToolbar && <header className="relative flex h-[74px] shrink-0 items-center justify-center px-3 py-5" data-testid="design-toolbar">
|
||||
{leftToolbar && <div className='absolute left-5 flex h-full items-center'>
|
||||
{toolbarLeft}
|
||||
|
@ -154,7 +167,7 @@ export const PreviewModalContent: React.FC<PreviewModalProps> = ({
|
|||
{toolbarRight}
|
||||
</div>}
|
||||
</header>}
|
||||
<div className='flex h-full grow items-center justify-center text-sm text-grey-400'>
|
||||
<div className='flex grow items-center justify-center text-sm text-grey-400'>
|
||||
{preview}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -15,9 +15,6 @@ import TextArea from '../../../../admin-x-ds/global/form/TextArea';
|
|||
import TextField from '../../../../admin-x-ds/global/form/TextField';
|
||||
import Toggle from '../../../../admin-x-ds/global/form/Toggle';
|
||||
import {PreviewModalContent} from '../../../../admin-x-ds/global/modal/PreviewModal';
|
||||
// import {ServicesContext} from '../../providers/ServiceProvider';
|
||||
// import {SettingsContext} from '../../providers/SettingsProvider';
|
||||
// import {getHomepageUrl, getSettingValues} from '../../../utils/helpers';
|
||||
|
||||
interface NewsletterDetailModalProps {
|
||||
|
||||
|
@ -29,8 +26,8 @@ interface NewsletterDetailModalProps {
|
|||
// ];
|
||||
|
||||
const selectOptions: SelectOption[] = [
|
||||
{value: 'option-1', label: 'Option 1'},
|
||||
{value: 'option-2', label: 'Option 2'}
|
||||
{value: 'option-1', label: 'Elegant serif'},
|
||||
{value: 'option-2', label: 'Modern sans-serif'}
|
||||
];
|
||||
|
||||
const Sidebar: React.FC = () => {
|
||||
|
@ -57,6 +54,7 @@ const Sidebar: React.FC = () => {
|
|||
|
||||
<Heading className="mt-5" level={5}>Member settings</Heading>
|
||||
<Toggle
|
||||
direction='rtl'
|
||||
label='Subscribe new members on signup'
|
||||
labelStyle='value'
|
||||
/>
|
||||
|
@ -180,7 +178,7 @@ const Sidebar: React.FC = () => {
|
|||
<StickyFooter height={96}>
|
||||
<div className='flex w-full items-start px-7'>
|
||||
<span>
|
||||
<Icon className='-mt-[1px] mr-2' colorClass='text-red' name='heart'/>
|
||||
<Icon className='mr-2 mt-[-1px]' colorClass='text-red' name='heart'/>
|
||||
</span>
|
||||
<Form marginBottom={false}>
|
||||
<Toggle
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
import CoverImage from '../../../../assets/images/user-cover.png';
|
||||
import Icon from '../../../../admin-x-ds/global/Icon';
|
||||
import LatestPosts1 from '../../../../assets/images/latest-posts-1.png';
|
||||
import LatestPosts2 from '../../../../assets/images/latest-posts-2.png';
|
||||
import LatestPosts3 from '../../../../assets/images/latest-posts-3.png';
|
||||
import React from 'react';
|
||||
import {ReactComponent as GhostOrb} from '../../../../admin-x-ds/assets/images/ghost-orb.svg';
|
||||
|
||||
|
@ -26,18 +30,20 @@ const NewsletterPreview: React.FC = () => {
|
|||
<div className="flex flex-col items-center pb-10 pt-12">
|
||||
<h2 className="pb-4 text-center text-5xl font-bold leading-supertight text-black">Your email newsletter</h2>
|
||||
<div className="flex w-full flex-col justify-between text-center text-sm leading-none tracking-[0.1px] text-grey-600">
|
||||
<p className="pb-2">By Djordje Vlaisavljevic<span className="before:pl-0.5 before:pr-1 before:content-['•']">17 Jul 2023</span><span className="before:pl-0.5 before:pr-1 before:content-['•']"><Icon className="-mt-[2px] inline-block" colorClass="text-grey-600" name="comment" size="sm"/></span></p>
|
||||
<p className="pb-2">By Djordje Vlaisavljevic<span className="before:pl-0.5 before:pr-1 before:content-['•']">17 Jul 2023</span><span className="before:pl-0.5 before:pr-1 before:content-['•']"><Icon className="mt-[-2px] inline-block" colorClass="text-grey-600" name="comment" size="sm"/></span></p>
|
||||
<p className="pb-2 underline"><span>View in browser</span></p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Feature image */}
|
||||
<div className="mb-2 h-[300px] w-full max-w-[600px] bg-grey-300 bg-cover bg-no-repeat"></div>
|
||||
<div className="w-full max-w-[600px] pb-[30px] text-center text-[1.3rem] text-grey-600">Feature image caption</div>
|
||||
<div className="h-[300px] w-full max-w-[600px] bg-grey-200 bg-cover bg-no-repeat">
|
||||
<img alt="Feature" className='min-h-full min-w-full shrink-0' src={CoverImage} />
|
||||
</div>
|
||||
<div className="mt-1 w-full max-w-[600px] pb-[30px] text-center text-[1.3rem] text-grey-600">Feature image caption</div>
|
||||
|
||||
<div className="max-w-[600px] border-b border-grey-200 py-5 text-[1.6rem] leading-[1.7] text-black">
|
||||
<p className="mb-5">This is what your content will look like when you send one of your posts as an email newsletter to your subscribers.</p>
|
||||
<p className="mb-5">Over there on the left you’ll see some settings that allow you to customize the look and feel of this template to make it perfectly suited to your brand. Email templates are exceptionally finnicky to make, but we’ve spent a long time optimising this one to make it work beautifully across devices, email clients and content types.</p>
|
||||
<p className="mb-5">Over there on the left you'll see some settings that allow you to customize the look and feel of this template to make it perfectly suited to your brand. Email templates are exceptionally finnicky to make, but we've spent a long time optimising this one to make it work beautifully across devices, email clients and content types.</p>
|
||||
<p className="mb-5">So, you can trust that every email you send with Ghost will look great and work well. Just like the rest of your site.</p>
|
||||
</div>
|
||||
|
||||
|
@ -67,33 +73,39 @@ const NewsletterPreview: React.FC = () => {
|
|||
|
||||
{/* Latest posts */}
|
||||
<div className="border-b border-grey-200 py-6">
|
||||
<h3 className="mb-4 mt-2 pb-1 text-[1.4rem] font-semibold uppercase">Keep reading</h3>
|
||||
<h3 className="mb-4 mt-2 pb-1 text-[1.2rem] font-semibold uppercase tracking-wide">Keep reading</h3>
|
||||
<div className="flex justify-between gap-4 py-2">
|
||||
<div>
|
||||
<h4 className="mb-1 mt-0.5 text-[1.9rem]">The three latest posts published on your site</h4>
|
||||
<p className="m-0 text-base text-grey-600">Posts sent as an email only will never be shown here.</p>
|
||||
</div>
|
||||
<div className="aspect-square h-auto w-full max-w-[100px] bg-grey-200 bg-cover bg-no-repeat" style={{backgroundImage: 'url(\'/../../../../admin-x-ds/assets/images/latest-posts-1.png'}}></div>
|
||||
<div className="aspect-square h-auto w-full max-w-[100px] bg-grey-200 bg-cover bg-no-repeat">
|
||||
<img alt="Latest post" src={LatestPosts1} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4 py-2">
|
||||
<div>
|
||||
<h4 className="mb-1 mt-0.5 text-[1.9rem]">Displayed at the bottom of each newsletter</h4>
|
||||
<p className="m-0 text-base text-grey-600">Giving your readers one more place to discover your stories.</p>
|
||||
</div>
|
||||
<div className="aspect-square h-auto w-full max-w-[100px] bg-grey-200 bg-cover bg-no-repeat"></div>
|
||||
<div className="aspect-square h-auto w-full max-w-[100px] bg-grey-200 bg-cover bg-no-repeat">
|
||||
<img alt="Latest post" src={LatestPosts2} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between gap-4 py-2">
|
||||
<div>
|
||||
<h4 className="mb-1 mt-0.5 text-[1.9rem]">To keep your work front and center</h4>
|
||||
<p className="m-0 text-base text-grey-600">Making sure that your audience stays engaged.</p>
|
||||
</div>
|
||||
<div className="aspect-square h-auto w-full max-w-[100px] bg-grey-200 bg-cover bg-no-repeat"></div>
|
||||
<div className="aspect-square h-auto w-full max-w-[100px] bg-grey-200 bg-cover bg-no-repeat">
|
||||
<img alt="Latest post" src={LatestPosts3} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Subscription details */}
|
||||
<div className="border-b border-grey-200 py-8">
|
||||
<h4 className="mb-3 text-[1.4rem] uppercase">Subscription details</h4>
|
||||
<h4 className="mb-3 text-[1.2rem] uppercase tracking-wide">Subscription details</h4>
|
||||
<p className="m-0 mb-4 text-base">You are receiving this because you are a paid subscriber to The Local Host. Your subscription will renew on 17 Jul 2024.</p>
|
||||
<div className="flex">
|
||||
<div className="shrink-0 text-base">
|
||||
|
@ -110,12 +122,12 @@ const NewsletterPreview: React.FC = () => {
|
|||
<div className="text break-words px-8 py-3 text-center text-[1.3rem] leading-base text-grey-600">This is custom email footer text.</div>
|
||||
|
||||
<div className="px-8 pb-14 pt-3 text-center text-[1.3rem] text-grey-600">
|
||||
<span>Ghosty © 2023 – </span>
|
||||
<span>Ghost © 2023 — </span>
|
||||
<span className="pointer-events-none cursor-auto underline">Unsubscribe</span>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-center pb-[40px] pt-[10px]">
|
||||
<a className="pointer-events-none inline-flex cursor-auto items-center px-2 py-1 text-[1.25rem] font-semibold tracking-tight text-grey-900" href="#">
|
||||
<a className="pointer-events-none inline-flex cursor-auto items-center px-2 py-1 text-[1.25rem] font-semibold tracking-tight text-grey-900" href="https://ghost.org">
|
||||
<GhostOrb className="mr-[6px] h-4 w-4"/>
|
||||
<span>Powered by Ghost</span>
|
||||
</a>
|
||||
|
|
|
@ -2,7 +2,6 @@ import ImageUpload from '../../../admin-x-ds/global/form/ImageUpload';
|
|||
import React, {useContext} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextArea from '../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {ReactComponent as FacebookLogo} from '../../../admin-x-ds/assets/images/facebook-logo.svg';
|
||||
|
@ -31,7 +30,7 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
updateSetting('og_title', e.target.value);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateSetting('og_description', e.target.value);
|
||||
};
|
||||
|
||||
|
@ -82,10 +81,9 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
value={facebookTitle}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
<TextArea
|
||||
<TextField
|
||||
clearBg={true}
|
||||
placeholder={siteDescription}
|
||||
rows={2}
|
||||
title="Facebook description"
|
||||
value={facebookDescription}
|
||||
onChange={handleDescriptionChange}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextArea from '../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {getSettingValues} from '../../../utils/helpers';
|
||||
|
@ -24,7 +23,7 @@ const TitleAndDescription: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
updateSetting('title', e.target.value);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateSetting('description', e.target.value);
|
||||
};
|
||||
|
||||
|
@ -56,7 +55,7 @@ const TitleAndDescription: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
value={title}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
<TextArea
|
||||
<TextField
|
||||
hint="A short description, used in your theme, meta data and search results"
|
||||
placeholder="Site description"
|
||||
title="Site description"
|
||||
|
|
|
@ -2,7 +2,6 @@ import ImageUpload from '../../../admin-x-ds/global/form/ImageUpload';
|
|||
import React, {useContext} from 'react';
|
||||
import SettingGroup from '../../../admin-x-ds/settings/SettingGroup';
|
||||
import SettingGroupContent from '../../../admin-x-ds/settings/SettingGroupContent';
|
||||
import TextArea from '../../../admin-x-ds/global/form/TextArea';
|
||||
import TextField from '../../../admin-x-ds/global/form/TextField';
|
||||
import useSettingGroup from '../../../hooks/useSettingGroup';
|
||||
import {FileService, ServicesContext} from '../../providers/ServiceProvider';
|
||||
|
@ -31,7 +30,7 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
updateSetting('twitter_title', e.target.value);
|
||||
};
|
||||
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
updateSetting('twitter_description', e.target.value);
|
||||
};
|
||||
|
||||
|
@ -84,10 +83,9 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
|
|||
value={twitterTitle}
|
||||
onChange={handleTitleChange}
|
||||
/>
|
||||
<TextArea
|
||||
<TextField
|
||||
clearBg={true}
|
||||
placeholder={siteDescription}
|
||||
rows={2}
|
||||
title="Twitter description"
|
||||
value={twitterDescription}
|
||||
onChange={handleDescriptionChange}
|
||||
|
|
|
@ -568,28 +568,22 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
}
|
||||
]);
|
||||
|
||||
let okLabel = saveState === 'saved' ? 'Saved' : 'Save';
|
||||
let okLabel = saveState === 'saved' ? 'Saved' : 'Save & close';
|
||||
if (saveState === 'saving') {
|
||||
okLabel = 'Saving...';
|
||||
} else if (saveState === 'saved') {
|
||||
okLabel = 'Saved';
|
||||
setTimeout(() => {
|
||||
mainModal.remove();
|
||||
}, 300);
|
||||
}
|
||||
|
||||
// remove saved state after 2 seconds
|
||||
useEffect(() => {
|
||||
if (saveState === 'saved') {
|
||||
setTimeout(() => {
|
||||
setSaveState('');
|
||||
}, 2000);
|
||||
}
|
||||
}, [saveState]);
|
||||
|
||||
const fileUploadButtonClasses = 'absolute right-[104px] bottom-12 bg-[rgba(0,0,0,0.75)] rounded text-sm text-white flex items-center justify-center px-3 h-8 opacity-80 hover:opacity-100 transition cursor-pointer font-medium z-10';
|
||||
|
||||
const suspendedText = userData.status === 'inactive' ? ' (Suspended)' : '';
|
||||
|
||||
return (
|
||||
<Modal
|
||||
backDropClick={false}
|
||||
cancelLabel='Close'
|
||||
okLabel={okLabel}
|
||||
size='lg'
|
||||
stickyFooter={true}
|
||||
|
|
|
@ -48,10 +48,10 @@ const PortalLinks: React.FC = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (tiers?.length) {
|
||||
if (tiers?.length && !selectedTier) {
|
||||
setSelectedTier(tiers[0].id);
|
||||
}
|
||||
}, [tiers]);
|
||||
}, [tiers, selectedTier]);
|
||||
|
||||
const tierOptions = tiers?.map((tier) => {
|
||||
return {
|
||||
|
@ -66,19 +66,19 @@ const PortalLinks: React.FC = () => {
|
|||
<ModalPage className='max-w-[920px] text-base text-black' heading='Links'>
|
||||
<p className='-mt-6 mb-16'>Use these {isDataAttributes ? 'data attributes' : 'links'} in your theme to show pages of Portal.</p>
|
||||
|
||||
<List actions={<Button color='green' label={isDataAttributes ? 'Links' : 'Data attributes'} link onClick={toggleIsDataAttributes}/>} title='Generic'>
|
||||
<List actions={<Button color='green' label={isDataAttributes ? 'Links' : 'Data attributes'} link onClick={toggleIsDataAttributes}/>} title='Generic' titleSize='lg'>
|
||||
<PortalLink name='Default' value={isDataAttributes ? 'data-portal' : `${homePageURL}#/portal`} />
|
||||
<PortalLink name='Sign in' value={isDataAttributes ? 'data-portal="signin"' : `${homePageURL}#/portal/signin`} />
|
||||
<PortalLink name='Sign up' value={isDataAttributes ? 'data-portal="signup"' : `${homePageURL}#/portal/signup`} />
|
||||
</List>
|
||||
|
||||
<List className='mt-14' title='Tiers'>
|
||||
<List className='mt-14' title='Tiers' titleSize='lg'>
|
||||
<ListItem
|
||||
hideActions
|
||||
separator
|
||||
>
|
||||
<div className='flex w-full items-center gap-5 py-2 pr-6'>
|
||||
<span className='inline-block w-[240px] shrink-0 font-bold'>Tier</span>
|
||||
<span className='inline-block w-[240px] shrink-0'>Tier</span>
|
||||
<Select
|
||||
options={tierOptions}
|
||||
selectedOption={selectedTier}
|
||||
|
@ -93,7 +93,7 @@ const PortalLinks: React.FC = () => {
|
|||
<PortalLink name='Signup / Free' value={isDataAttributes ? 'data-portal="signup/free"' : `${homePageURL}#/portal/signup/free`} />
|
||||
</List>
|
||||
|
||||
<List className='mt-14' title='Account'>
|
||||
<List className='mt-14' title='Account' titleSize='lg'>
|
||||
<PortalLink name='Account' value={isDataAttributes ? 'data-portal="account"' : `${homePageURL}#/portal/account`} />
|
||||
<PortalLink name='Account / Plans' value={isDataAttributes ? 'data-portal="account/plans"' : `${homePageURL}#/portal/account/plans`} />
|
||||
<PortalLink name='Account / Profile' value={isDataAttributes ? 'data-portal="account/profile"' : `${homePageURL}#/portal/account/profile`} />
|
||||
|
|
|
@ -162,7 +162,7 @@ const PortalModal: React.FC = () => {
|
|||
dirty={saveState === 'unsaved'}
|
||||
okLabel={okLabel}
|
||||
preview={preview}
|
||||
previewBgColor={selectedPreviewTab === 'links' ? 'white' : 'grey'}
|
||||
previewBgColor={selectedPreviewTab === 'links' ? 'white' : 'greygradient'}
|
||||
previewToolbarTabs={previewTabs}
|
||||
selectedURL={selectedPreviewTab}
|
||||
sidebar={sidebar}
|
||||
|
|
|
@ -129,13 +129,13 @@ const SignupOptions: React.FC<{
|
|||
onChange={html => updateSetting('portal_signup_terms_html', html)}
|
||||
/>
|
||||
|
||||
<Toggle
|
||||
{portalSignupTermsHtml?.toString() && <Toggle
|
||||
checked={Boolean(portalSignupCheckboxRequired)}
|
||||
disabled={isDisabled}
|
||||
label='Require agreement'
|
||||
labelStyle='heading'
|
||||
onChange={e => updateSetting('portal_signup_checkbox_required', e.target.checked)}
|
||||
/>
|
||||
/>}
|
||||
</Form>;
|
||||
};
|
||||
|
||||
|
|
|
@ -118,9 +118,9 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
handleSave();
|
||||
}}
|
||||
>
|
||||
<div className='mt-8 flex items-start gap-16'>
|
||||
<div className='flex grow flex-col gap-5'>
|
||||
<Form title='Basic'>
|
||||
<div className='-mb-8 mt-8 flex items-start gap-8'>
|
||||
<div className='flex grow flex-col gap-8'>
|
||||
<Form marginBottom={false} title='Basic' grouped>
|
||||
{!isFreeTier && <TextField
|
||||
autoComplete='off'
|
||||
error={Boolean(errors.name)}
|
||||
|
@ -204,26 +204,29 @@ const TierDetailModal: React.FC<TierDetailModalProps> = ({tier}) => {
|
|||
</div>}
|
||||
</Form>
|
||||
|
||||
<Form gap='none' title='Benefits'>
|
||||
<SortableList
|
||||
items={benefits.items}
|
||||
itemSeparator={false}
|
||||
renderItem={({id, item}) => <div className='relative flex w-full items-center gap-5'>
|
||||
<div className='absolute left-[-32px] top-[7px] flex h-6 w-6 items-center justify-center bg-white group-hover:hidden'><Icon name='check' size='sm' /></div>
|
||||
<TextField
|
||||
className='grow border-b border-grey-500 py-2 focus:border-grey-800 group-hover:border-grey-600'
|
||||
value={item}
|
||||
unstyled
|
||||
onChange={e => benefits.updateItem(id, e.target.value)}
|
||||
/>
|
||||
<Button className='absolute right-0 top-1' icon='trash' iconColorClass='opacity-0 group-hover:opacity-100' size='sm' onClick={() => benefits.removeItem(id)} />
|
||||
</div>}
|
||||
onMove={benefits.moveItem}
|
||||
/>
|
||||
<Form gap='none' title='Benefits' grouped>
|
||||
<div className='-mt-3'>
|
||||
<SortableList
|
||||
items={benefits.items}
|
||||
itemSeparator={false}
|
||||
renderItem={({id, item}) => <div className='relative flex w-full items-center gap-5'>
|
||||
<div className='absolute left-[-32px] top-[7px] flex h-6 w-6 items-center justify-center bg-white group-hover:hidden'><Icon name='check' size='sm' /></div>
|
||||
<TextField
|
||||
className='grow border-b border-grey-500 py-2 focus:border-grey-800 group-hover:border-grey-600'
|
||||
value={item}
|
||||
unstyled
|
||||
onChange={e => benefits.updateItem(id, e.target.value)}
|
||||
/>
|
||||
<Button className='absolute right-0 top-1' icon='trash' iconColorClass='opacity-0 group-hover:opacity-100' size='sm' onClick={() => benefits.removeItem(id)} />
|
||||
</div>}
|
||||
onMove={benefits.moveItem}
|
||||
/>
|
||||
</div>
|
||||
<div className="relative mt-0.5 flex items-center gap-3">
|
||||
<Icon name='check' size='sm' />
|
||||
<TextField
|
||||
className='grow'
|
||||
containerClassName='w-100'
|
||||
placeholder='Expert analysis'
|
||||
title='New benefit'
|
||||
value={benefits.newItem}
|
||||
|
|
|
@ -90,7 +90,7 @@ const TierDetailPreview: React.FC<TierDetailPreviewProps> = ({tier, isFreeTier})
|
|||
<div className="mt-1">
|
||||
<div className="flex items-baseline justify-between">
|
||||
<Heading className="pb-2" level={6} grey>{isFreeTier ? 'Free membership preview' : 'Tier preview'}</Heading>
|
||||
{!isFreeTier && <div className="flex">
|
||||
{!isFreeTier && <div className="flex gap-1">
|
||||
<Button className={`${showingYearly === true ? 'text-grey-500' : 'text-grey-900'}`} label="Monthly" link onClick={() => setShowingYearly(false)} />
|
||||
<Button className={`ml-2 ${showingYearly === true ? 'text-grey-900' : 'text-grey-500'}`} label="Yearly" link onClick={() => setShowingYearly(true)} />
|
||||
</div>}
|
||||
|
|
|
@ -214,7 +214,7 @@ module.exports = {
|
|||
full: '9999px'
|
||||
},
|
||||
fontSize: {
|
||||
'2xs': '0.95rem',
|
||||
'2xs': '1.0rem',
|
||||
base: '1.5rem',
|
||||
xs: '1.2rem',
|
||||
sm: '1.35rem',
|
||||
|
|
|
@ -40,12 +40,10 @@ test.describe('User profile', async () => {
|
|||
await modal.getByLabel(/New paid members/).uncheck();
|
||||
await modal.getByLabel(/Paid member cancellations/).check();
|
||||
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
||||
|
||||
await modal.getByRole('button', {name: 'Close'}).click();
|
||||
|
||||
await expect(listItem.getByText('New Admin')).toBeVisible();
|
||||
await expect(listItem.getByText('newadmin@test.com')).toBeVisible();
|
||||
|
||||
|
|
|
@ -61,12 +61,10 @@ test.describe('User roles', async () => {
|
|||
|
||||
await modal.locator('input[value=editor]').check();
|
||||
|
||||
await modal.getByRole('button', {name: 'Save'}).click();
|
||||
await modal.getByRole('button', {name: 'Save & close'}).click();
|
||||
|
||||
await expect(modal.getByRole('button', {name: 'Saved'})).toBeVisible();
|
||||
|
||||
await modal.getByRole('button', {name: 'Close'}).click();
|
||||
|
||||
await expect(activeTab).toHaveText(/No users found/);
|
||||
|
||||
await section.getByRole('tab', {name: 'Editors'}).click();
|
||||
|
|
Loading…
Add table
Reference in a new issue