Added preview selection toolbar in AdminX
refs. https://github.com/TryGhost/Team/issues/3354
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-down</title><line x1="12" y1="0.75" x2="12" y2="23.25" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><polyline points="1.5 12.75 12 23.25 22.5 12.75" fill-rule="evenodd" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></polyline></svg>
|
After Width: | Height: | Size: 451 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-left</title><line x1="23.25" y1="12" x2="0.75" y2="12" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><polyline points="11.25 1.5 0.75 12 11.25 22.5" fill-rule="evenodd" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></polyline></svg>
|
After Width: | Height: | Size: 450 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-right</title><line x1="0.75" y1="12" x2="23.25" y2="12" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><polyline points="12.75 22.5 23.25 12 12.75 1.5" fill-rule="evenodd" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></polyline></svg>
|
After Width: | Height: | Size: 452 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-up</title><line x1="12" y1="23.25" x2="12" y2="0.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line><polyline points="22.5 11.25 12 0.75 1.5 11.25" fill-rule="evenodd" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></polyline></svg>
|
After Width: | Height: | Size: 448 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-down-1</title><path d="M23.25,7.311,12.53,18.03a.749.749,0,0,1-1.06,0L.75,7.311" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px" fill-rule="evenodd"></path></svg>
|
After Width: | Height: | Size: 315 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-left-1</title><path d="M16.25,23.25,5.53,12.53a.749.749,0,0,1,0-1.06L16.25.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px" fill-rule="evenodd"></path></svg>
|
After Width: | Height: | Size: 313 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-right-1</title><path d="M5.5.75,16.22,11.47a.749.749,0,0,1,0,1.06L5.5,23.25" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px" fill-rule="evenodd"></path></svg>
|
After Width: | Height: | Size: 311 B |
|
@ -0,0 +1 @@
|
|||
<svg id="Regular" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>arrow-up-1</title><path d="M.75,17.189,11.47,6.47a.749.749,0,0,1,1.06,0L23.25,17.189" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px" fill-rule="evenodd"></path></svg>
|
After Width: | Height: | Size: 314 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><defs></defs><title>laptop</title><path d="M21,14.25V4.5A1.5,1.5,0,0,0,19.5,3H4.5A1.5,1.5,0,0,0,3,4.5v9.75Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><path d="M23.121,18.891A1.5,1.5,0,0,1,21.75,21H2.25A1.5,1.5,0,0,1,.879,18.891L3,14.25H21Z" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></path><line x1="10.5" y1="18" x2="13.5" y2="18" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></svg>
|
After Width: | Height: | Size: 635 B |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><g><rect x="5.25" y="0.75" width="13.5" height="22.5" rx="3" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></rect><line x1="5.25" y1="17.75" x2="18.75" y2="17.75" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5px"></line></g></svg>
|
After Width: | Height: | Size: 398 B |
|
@ -62,7 +62,8 @@ export const LinkButton: Story = {
|
|||
export const Icon: Story = {
|
||||
args: {
|
||||
icon: 'menu-horizontal',
|
||||
color: 'green'
|
||||
color: 'green',
|
||||
iconColorClass: 'text-white'
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -70,6 +71,7 @@ export const IconSmall: Story = {
|
|||
args: {
|
||||
size: 'sm',
|
||||
icon: 'menu-horizontal',
|
||||
color: 'green'
|
||||
color: 'green',
|
||||
iconColorClass: 'text-white'
|
||||
}
|
||||
};
|
|
@ -8,6 +8,7 @@ export interface IButton {
|
|||
size?: ButtonSize;
|
||||
label?: string;
|
||||
icon?: string;
|
||||
iconColorClass?: string;
|
||||
key?: string;
|
||||
color?: string;
|
||||
fullWidth?: boolean;
|
||||
|
@ -21,6 +22,7 @@ const Button: React.FC<IButton> = ({
|
|||
size = 'md',
|
||||
label = '',
|
||||
icon = '',
|
||||
iconColorClass = 'text-black',
|
||||
color = 'clear',
|
||||
fullWidth,
|
||||
link,
|
||||
|
@ -72,7 +74,7 @@ const Button: React.FC<IButton> = ({
|
|||
onClick={onClick}
|
||||
{...props}
|
||||
>
|
||||
{icon && <Icon color={(color === 'clear' || color === 'grey' || color === 'white' ? 'black' : 'white')} name={icon} size={size === 'sm' ? 'sm' : 'md'} />}
|
||||
{icon && <Icon colorClass={iconColorClass} name={icon} size={size === 'sm' ? 'sm' : 'md'} />}
|
||||
{label}
|
||||
</button>
|
||||
);
|
||||
|
|
|
@ -1,6 +1,10 @@
|
|||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import Button from './Button';
|
||||
import ButtonGroup from './ButtonGroup';
|
||||
import DesktopChrome from './DesktopChrome';
|
||||
import URLSelect from './URLSelect';
|
||||
import {SelectOption} from './Select';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Chrome / Desktop',
|
||||
|
@ -43,28 +47,52 @@ export const WithBorder: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const NoTrafficLights: Story = {
|
||||
export const Empty: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<div className='flex items-center justify-center p-10 text-sm text-grey-500'>
|
||||
Window contents
|
||||
</div>
|
||||
),
|
||||
trafficLights: false
|
||||
toolbarLeft: <></>
|
||||
}
|
||||
};
|
||||
|
||||
export const WithHeaderContents: Story = {
|
||||
export const WithTitle: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<div className='flex items-center justify-center p-10 text-sm text-grey-500'>
|
||||
Window contents
|
||||
</div>
|
||||
),
|
||||
header: (
|
||||
<div className='flex grow justify-center text-sm font-semibold text-grey-900'>
|
||||
Window header
|
||||
toolbarCenter: 'Hello title'
|
||||
}
|
||||
};
|
||||
|
||||
const selectOptions: SelectOption[] = [
|
||||
{value: 'homepage', label: 'Homepage'},
|
||||
{value: 'post', label: 'Post'},
|
||||
{value: 'page', label: 'Page'},
|
||||
{value: 'tag-archive', label: 'Tag archive'},
|
||||
{value: 'author-archive', label: 'Author archive'}
|
||||
];
|
||||
|
||||
export const CustomToolbar: Story = {
|
||||
args: {
|
||||
children: (
|
||||
<div className='flex items-center justify-center p-10 text-sm text-grey-500'>
|
||||
Window contents
|
||||
</div>
|
||||
)
|
||||
),
|
||||
toolbarLeft: <Button icon='arrow-left' link={true} size='sm' />,
|
||||
toolbarCenter: <URLSelect options={selectOptions} onSelect={(value: string) => {
|
||||
alert(value);
|
||||
}} />,
|
||||
toolbarRight: <ButtonGroup
|
||||
buttons={[
|
||||
{icon: 'laptop', link: true, size: 'sm'},
|
||||
{icon: 'mobile', link: true, size: 'sm', iconColorClass: 'text-grey-500'}
|
||||
]}
|
||||
/>
|
||||
}
|
||||
};
|
|
@ -4,48 +4,65 @@ export type DesktopChromeSize = 'sm' | 'md';
|
|||
|
||||
interface DesktopChromeProps {
|
||||
size?: DesktopChromeSize;
|
||||
trafficLights?: boolean;
|
||||
toolbarLeft?: React.ReactNode;
|
||||
toolbarCenter?: React.ReactNode;
|
||||
toolbarRight?: React.ReactNode;
|
||||
children?: React.ReactNode;
|
||||
chromeClasses?: string;
|
||||
headerClasses?: string;
|
||||
toolbarClasses?: string;
|
||||
contentClasses?: string;
|
||||
header?: React.ReactNode;
|
||||
headerCenter?: boolean;
|
||||
border?: boolean;
|
||||
}
|
||||
|
||||
const DesktopChrome: React.FC<DesktopChromeProps> = ({
|
||||
size = 'md',
|
||||
trafficLights = true,
|
||||
toolbarLeft = '',
|
||||
toolbarCenter = '',
|
||||
toolbarRight = '',
|
||||
children,
|
||||
chromeClasses = '',
|
||||
headerClasses = '',
|
||||
toolbarClasses = '',
|
||||
contentClasses = '',
|
||||
header,
|
||||
headerCenter = true,
|
||||
border = false
|
||||
}) => {
|
||||
let containerSize = size === 'sm' ? 'h-6 p-2' : 'h-10 p-3';
|
||||
let containerSize = size === 'sm' ? 'min-h-[32px] p-2' : 'min-h-[48px] p-3';
|
||||
const trafficLightSize = size === 'sm' ? 'w-[6px] h-[6px]' : 'w-[10px] h-[10px]';
|
||||
const trafficLightContainerStyle = size === 'sm' ? 'gap-[5px] w-[36px] ' : 'gap-2 w-[56px] ';
|
||||
const trafficLightWidth = size === 'sm' ? 36 : 56;
|
||||
let trafficLightContainerStyle = size === 'sm' ? 'gap-[5px] ' : 'gap-2 ';
|
||||
trafficLightContainerStyle += `w-[${trafficLightWidth}px]`;
|
||||
|
||||
contentClasses += ' grow';
|
||||
|
||||
if (headerCenter) {
|
||||
containerSize += size === 'sm' ? ' pr-[48px]' : ' pr-[68px]';
|
||||
}
|
||||
const trafficLights = (
|
||||
<div className={`absolute left-4 flex h-full items-center ${trafficLightContainerStyle}`}>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={`flex h-full grow-0 flex-col ${border ? 'rounded-sm border border-grey-100' : ''} ${chromeClasses}`}>
|
||||
<header className={`flex items-center justify-between bg-grey-50 ${containerSize} ${headerClasses}`}>
|
||||
{trafficLights &&
|
||||
<div className={`flex items-center ${trafficLightContainerStyle}`}>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<div className={`rounded-full bg-grey-500 ${trafficLightSize}`}></div>
|
||||
<header className={`relative flex items-center justify-center bg-grey-50 ${containerSize} ${toolbarClasses}`}>
|
||||
{toolbarLeft ?
|
||||
<div className='absolute left-4 flex h-full items-center'>
|
||||
{toolbarLeft}
|
||||
</div>
|
||||
:
|
||||
trafficLights
|
||||
}
|
||||
<div className='flex grow justify-center'>
|
||||
{(typeof toolbarCenter === 'string') ?
|
||||
(<span className='text-sm font-bold'>{toolbarCenter}</span>)
|
||||
:
|
||||
(<>{toolbarCenter}</>)
|
||||
}
|
||||
</div>
|
||||
{toolbarRight &&
|
||||
<div className='absolute right-4 flex h-full items-center'>
|
||||
{toolbarRight}
|
||||
</div>
|
||||
}
|
||||
{header && header}
|
||||
</header>
|
||||
<section className={contentClasses}>
|
||||
{children}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import React, {useEffect, useState} from 'react';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface UseDynamicSVGImportOptions {
|
||||
onCompleted?: (
|
||||
|
@ -52,7 +53,7 @@ interface IconProps {
|
|||
/**
|
||||
* Accepts all colors available in the actual TailwindCSS theme, e.g. `black`, `green-100`
|
||||
*/
|
||||
color?: string;
|
||||
colorClass?: string;
|
||||
styles?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
@ -63,7 +64,7 @@ interface IconProps {
|
|||
* - all icons must have all it's children color value set `currentColor`
|
||||
* - all strokes must be paths and _NOT_ outlined objects. Stroke width should be set to 1.5px
|
||||
*/
|
||||
const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', className = ''}) => {
|
||||
const Icon: React.FC<IconProps> = ({name, size = 'md', colorClass = 'text-black', className = ''}) => {
|
||||
const {SvgComponent} = useDynamicSVGImport(name);
|
||||
|
||||
let styles = '';
|
||||
|
@ -89,9 +90,10 @@ const Icon: React.FC<IconProps> = ({name, size = 'md', color = 'black', classNam
|
|||
}
|
||||
}
|
||||
|
||||
if (color) {
|
||||
styles += ` text-${color}`;
|
||||
}
|
||||
styles = clsx(
|
||||
styles,
|
||||
colorClass
|
||||
);
|
||||
|
||||
if (SvgComponent) {
|
||||
return (
|
||||
|
|
|
@ -3,14 +3,14 @@ import React from 'react';
|
|||
|
||||
interface IconLabelProps {
|
||||
icon: string;
|
||||
iconColor?: string;
|
||||
iconColorClass?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const IconLabel: React.FC<IconLabelProps> = ({icon, iconColor, children}) => {
|
||||
const IconLabel: React.FC<IconLabelProps> = ({icon, iconColorClass, children}) => {
|
||||
return (
|
||||
<div className='flex items-center gap-2'>
|
||||
<Icon color={iconColor} name={icon} size='sm' />
|
||||
<Icon colorClass={iconColorClass} name={icon} size='sm' />
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -35,7 +35,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
|
|||
imageClassName = imageClassName || 'group relative bg-cover';
|
||||
fileUploadClassName = fileUploadClassName || 'flex cursor-pointer items-center justify-center rounded border border-grey-100 bg-grey-75 p-3 text-sm font-semibold text-grey-800 hover:text-black';
|
||||
deleteButtonClassName = deleteButtonClassName || 'invisible absolute right-4 top-4 flex h-8 w-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:!visible';
|
||||
deleteButtonContent = deleteButtonContent || <Icon color='white' name='trash' size='sm' />;
|
||||
deleteButtonContent = deleteButtonContent || <Icon colorClass='text-white' name='trash' size='sm' />;
|
||||
}
|
||||
|
||||
if (imageURL) {
|
||||
|
|
|
@ -73,59 +73,51 @@ const Modal: React.FC<ModalProps> = ({
|
|||
}
|
||||
|
||||
let modalClasses = clsx(
|
||||
'relative z-50 mx-auto flex w-full flex-col justify-between rounded bg-white shadow-xl'
|
||||
'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-y-auto overflow-x-hidden rounded bg-white shadow-xl'
|
||||
// !stickyFooter && ' overflow-hidden'
|
||||
);
|
||||
let backdropClasses = clsx('fixed inset-0 z-40 h-[100vh] w-[100vw] overflow-y-scroll ');
|
||||
|
||||
let padding = '';
|
||||
|
||||
let footerContainerBottom = '';
|
||||
|
||||
switch (size) {
|
||||
case 'sm':
|
||||
modalClasses += ' max-w-[480px] ';
|
||||
backdropClasses += ' p-[8vmin]';
|
||||
padding = 'p-8';
|
||||
footerContainerBottom = '-8vmin';
|
||||
break;
|
||||
|
||||
case 'md':
|
||||
modalClasses += ' max-w-[720px] ';
|
||||
backdropClasses += ' p-[8vmin]';
|
||||
padding = 'p-8';
|
||||
footerContainerBottom = '-8vmin';
|
||||
break;
|
||||
|
||||
case 'lg':
|
||||
modalClasses += ' max-w-[1020px] ';
|
||||
backdropClasses += ' p-[4vmin]';
|
||||
padding = 'p-12';
|
||||
footerContainerBottom = '-4vmin';
|
||||
padding = 'p-8';
|
||||
break;
|
||||
|
||||
case 'xl':
|
||||
modalClasses += ' max-w-[1240px] ';
|
||||
backdropClasses += ' p-[3vmin]';
|
||||
padding = 'p-12';
|
||||
footerContainerBottom = '-3vmin';
|
||||
padding = 'p-10';
|
||||
break;
|
||||
|
||||
case 'full':
|
||||
modalClasses += ' h-full ';
|
||||
backdropClasses += ' p-[2vmin]';
|
||||
padding = 'p-12';
|
||||
footerContainerBottom = '-2vmin';
|
||||
padding = 'p-10';
|
||||
break;
|
||||
|
||||
case 'bleed':
|
||||
modalClasses += ' h-full ';
|
||||
padding = 'p-12';
|
||||
padding = 'p-10';
|
||||
break;
|
||||
|
||||
default:
|
||||
backdropClasses += ' p-[8vmin]';
|
||||
footerContainerBottom = '-8vmin';
|
||||
padding = 'p-8';
|
||||
break;
|
||||
}
|
||||
|
@ -139,7 +131,10 @@ const Modal: React.FC<ModalProps> = ({
|
|||
'flex w-full items-center justify-between'
|
||||
);
|
||||
|
||||
let contentClasses = `${padding} h-full`;
|
||||
let contentClasses = clsx(
|
||||
padding,
|
||||
size === 'full' && 'h-full'
|
||||
);
|
||||
|
||||
if (!customFooter) {
|
||||
contentClasses += ' pb-0 ';
|
||||
|
@ -169,7 +164,7 @@ const Modal: React.FC<ModalProps> = ({
|
|||
);
|
||||
|
||||
const footer = (stickyFooter ?
|
||||
<StickyFooter shiftY={footerContainerBottom}>
|
||||
<StickyFooter height={84}>
|
||||
{footerContent}
|
||||
</StickyFooter>
|
||||
:
|
||||
|
|
|
@ -12,7 +12,7 @@ interface NoValueLabelProps {
|
|||
const NoValueLabel: React.FC<NoValueLabelProps> = ({icon, children}) => {
|
||||
return (
|
||||
<div className='my-10 flex flex-col items-center gap-1 text-sm text-grey-600'>
|
||||
{icon && <Icon className='stroke-[1px]' color='grey-500' name={icon} size='lg' />}
|
||||
{icon && <Icon className='stroke-[1px]' colorClass='text-grey-500' name={icon} size='lg' />}
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -66,7 +66,7 @@ const PreviewModal: React.FC<PreviewModalProps> = ({
|
|||
</div>
|
||||
<div className='flex h-full basis-[400px] flex-col gap-3 border-l border-grey-100'>
|
||||
{customHeader ? customHeader : (
|
||||
<div className='flex justify-between gap-3 px-7 pt-7'>
|
||||
<div className='flex justify-between gap-3 px-7 pt-5'>
|
||||
<>
|
||||
<Heading className='mt-1' level={4}>{title}</Heading>
|
||||
{customButtons ? customButtons : <ButtonGroup buttons={buttons} /> }
|
||||
|
|
|
@ -61,7 +61,7 @@ export const WithHint: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const WithDefaultSelectedOption: Story = {
|
||||
export const WithDefaultSelected: Story = {
|
||||
args: {
|
||||
title: 'Title',
|
||||
options: selectOptions,
|
||||
|
@ -70,6 +70,15 @@ export const WithDefaultSelectedOption: Story = {
|
|||
}
|
||||
};
|
||||
|
||||
export const WithCallback: Story = {
|
||||
args: {
|
||||
options: selectOptions,
|
||||
onSelect: (value: string) => {
|
||||
alert(value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const Error: Story = {
|
||||
args: {
|
||||
title: 'Title',
|
||||
|
@ -77,4 +86,11 @@ export const Error: Story = {
|
|||
hint: 'Invalid value',
|
||||
error: true
|
||||
}
|
||||
};
|
||||
|
||||
export const Unstyled: Story = {
|
||||
args: {
|
||||
options: selectOptions,
|
||||
unstyled: true
|
||||
}
|
||||
};
|
|
@ -2,6 +2,7 @@ import React, {useEffect, useId, useState} from 'react';
|
|||
|
||||
import Heading from './Heading';
|
||||
import Hint from './Hint';
|
||||
import clsx from 'clsx';
|
||||
|
||||
export interface SelectOption {
|
||||
value: string;
|
||||
|
@ -17,9 +18,26 @@ interface SelectProps {
|
|||
hint?: React.ReactNode;
|
||||
defaultSelectedOption?: string;
|
||||
clearBg?: boolean;
|
||||
containerClassName?: string;
|
||||
selectClassName?: string;
|
||||
optionClassName?: string;
|
||||
unstyled?: boolean;
|
||||
}
|
||||
|
||||
const Select: React.FC<SelectProps> = ({title, prompt, options, onSelect, error, hint, defaultSelectedOption, clearBg = false}) => {
|
||||
const Select: React.FC<SelectProps> = ({
|
||||
title,
|
||||
prompt,
|
||||
options,
|
||||
onSelect,
|
||||
error,
|
||||
hint,
|
||||
defaultSelectedOption,
|
||||
clearBg = false,
|
||||
containerClassName,
|
||||
selectClassName,
|
||||
optionClassName,
|
||||
unstyled
|
||||
}) => {
|
||||
const id = useId();
|
||||
|
||||
const [selectedOption, setSelectedOption] = useState(defaultSelectedOption);
|
||||
|
@ -36,15 +54,46 @@ const Select: React.FC<SelectProps> = ({title, prompt, options, onSelect, error,
|
|||
onSelect(selectedValue);
|
||||
};
|
||||
|
||||
let containerClasses = '';
|
||||
if (!unstyled) {
|
||||
containerClasses = clsx(
|
||||
'relative w-full after:pointer-events-none',
|
||||
`after:absolute after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`,
|
||||
title ? 'after:top-[22px]' : 'after:top-[14px]',
|
||||
clearBg ? 'after:right-0' : 'after:right-4'
|
||||
);
|
||||
}
|
||||
containerClasses = clsx(
|
||||
containerClasses,
|
||||
containerClassName
|
||||
);
|
||||
|
||||
let selectClasses = '';
|
||||
if (!unstyled) {
|
||||
selectClasses = clsx(
|
||||
'w-full cursor-pointer appearance-none border-b py-2 outline-none',
|
||||
!clearBg && 'bg-grey-75 px-[10px]',
|
||||
error ? 'border-red' : 'border-grey-500 hover:border-grey-700 focus:border-black',
|
||||
title && 'mt-2'
|
||||
);
|
||||
}
|
||||
selectClasses = clsx(
|
||||
selectClasses,
|
||||
selectClassName
|
||||
);
|
||||
|
||||
const optionClasses = optionClassName;
|
||||
|
||||
return (
|
||||
<div className='flex flex-col'>
|
||||
<>
|
||||
{title && <Heading htmlFor={id} useLabelTag={true}>{title}</Heading>}
|
||||
<div className={`relative w-full after:pointer-events-none after:absolute ${clearBg ? 'after:right-0' : 'after:right-4'} after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-[''] ${title ? 'after:top-[22px]' : 'after:top-[14px]'}`}>
|
||||
<select className={`w-full cursor-pointer appearance-none border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 outline-none ${error ? `border-red` : `border-grey-500 hover:border-grey-700 focus:border-black`} ${title && `mt-2`}`} id={id} value={selectedOption} onChange={handleOptionChange}>
|
||||
{prompt && <option value="">{prompt}</option>}
|
||||
<div className={containerClasses}>
|
||||
<select className={selectClasses} id={id} value={selectedOption} onChange={handleOptionChange}>
|
||||
{prompt && <option className={optionClasses} value="">{prompt}</option>}
|
||||
{options.map(option => (
|
||||
<option
|
||||
key={option.value}
|
||||
className={optionClasses}
|
||||
value={option.value}
|
||||
>
|
||||
{option.label}
|
||||
|
@ -53,7 +102,7 @@ const Select: React.FC<SelectProps> = ({title, prompt, options, onSelect, error,
|
|||
</select>
|
||||
</div>
|
||||
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -52,14 +52,14 @@ const Toast: React.FC<ToastProps> = ({
|
|||
<div className={classNames}>
|
||||
<div className='flex items-start gap-3'>
|
||||
{props?.icon && (typeof props.icon === 'string' ?
|
||||
<div className='mt-0.5'><Icon className='grow' color={props.type === 'success' ? 'green' : 'white'} name={props.icon} size='sm' /></div> : props.icon)}
|
||||
<div className='mt-0.5'><Icon className='grow' colorClass={props.type === 'success' ? 'text-green' : 'text-white'} name={props.icon} size='sm' /></div> : props.icon)}
|
||||
{children}
|
||||
</div>
|
||||
<button className='cursor-pointer' type='button' onClick={() => {
|
||||
toast.dismiss(t.id);
|
||||
}}>
|
||||
<div className='mt-1'>
|
||||
<Icon color='white' name='close' size='xs' />
|
||||
<Icon colorClass='text-white' name='close' size='xs' />
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import type {Meta, StoryObj} from '@storybook/react';
|
||||
|
||||
import URLSelect from './URLSelect';
|
||||
import {SelectOption} from './Select';
|
||||
|
||||
const meta = {
|
||||
title: 'Global / Select / URL Select',
|
||||
component: URLSelect,
|
||||
tags: ['autodocs']
|
||||
} satisfies Meta<typeof URLSelect>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof URLSelect>;
|
||||
|
||||
const selectOptions: SelectOption[] = [
|
||||
{value: 'homepage', label: 'Homepage'},
|
||||
{value: 'post', label: 'Post'},
|
||||
{value: 'page', label: 'Page'},
|
||||
{value: 'tag-archive', label: 'Tag archive'},
|
||||
{value: 'author-archive', label: 'Author archive'}
|
||||
];
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
options: selectOptions,
|
||||
onSelect: () => {}
|
||||
}
|
||||
};
|
31
ghost/admin-x-settings/src/admin-x-ds/global/URLSelect.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import Select, {SelectOption} from './Select';
|
||||
import clsx from 'clsx';
|
||||
|
||||
interface URLSelectProps {
|
||||
options: SelectOption[];
|
||||
onSelect: (value: string) => void;
|
||||
}
|
||||
|
||||
const URLSelect: React.FC<URLSelectProps> = ({options, onSelect}) => {
|
||||
const selectClasses = clsx(
|
||||
`!h-[unset] w-full appearance-none rounded-full border border-grey-100 bg-white px-3 py-1 text-sm`
|
||||
);
|
||||
|
||||
const containerClasses = clsx(
|
||||
'relative w-full max-w-[320px] self-center after:pointer-events-none',
|
||||
`after:absolute after:right-4 after:top-[9px] after:block after:h-2 after:w-2 after:rotate-45 after:border-[1px] after:border-l-0 after:border-t-0 after:border-grey-900 after:content-['']`
|
||||
);
|
||||
|
||||
return (
|
||||
<Select
|
||||
containerClassName={containerClasses}
|
||||
options={options}
|
||||
selectClassName={selectClasses}
|
||||
unstyled={true}
|
||||
onSelect={onSelect}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default URLSelect;
|
|
@ -33,7 +33,7 @@ const MailGun: React.FC = () => {
|
|||
{
|
||||
key: 'status',
|
||||
value: (
|
||||
<IconLabel icon='check-circle' iconColor='green'>
|
||||
<IconLabel icon='check-circle' iconColorClass='text-green'>
|
||||
Mailgun is set up
|
||||
</IconLabel>
|
||||
)
|
||||
|
|
|
@ -35,12 +35,12 @@ const LockSite: React.FC = () => {
|
|||
key: 'private',
|
||||
value: passwordEnabled ? (
|
||||
<div className='flex items-center gap-1'>
|
||||
<Icon color='yellow' name='lock-locked' size='sm' />
|
||||
<Icon colorClass='text-yellow' name='lock-locked' size='sm' />
|
||||
<span>Your site is password protected</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className='flex items-center gap-1 text-grey-900'>
|
||||
<Icon color='black' name='lock-unlocked' size='sm' />
|
||||
<Icon colorClass='text-black' name='lock-unlocked' size='sm' />
|
||||
<span>Your site is not password protected</span>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -47,7 +47,7 @@ const RoleSelector: React.FC<UserDetailProps> = ({user, setUserData}) => {
|
|||
<>
|
||||
<Heading level={6}>Role</Heading>
|
||||
<div className='flex h-[295px] flex-col items-center justify-center gap-3 bg-grey-75 px-10 py-20 text-center text-sm text-grey-800'>
|
||||
<Icon color='grey-800' name='crown' size='lg' />
|
||||
<Icon colorClass='text-grey-800' name='crown' size='lg' />
|
||||
This user is the owner of the site. To change their role, you need to transfer the ownership first.
|
||||
</div>
|
||||
</>
|
||||
|
@ -401,7 +401,7 @@ interface UserDetailModalProps {
|
|||
|
||||
const UserMenuTrigger = () => (
|
||||
<div className='flex h-8 cursor-pointer items-center justify-center rounded bg-[rgba(0,0,0,0.75)] px-3 opacity-80 hover:opacity-100'>
|
||||
<Icon color='white' name='menu-horizontal' size='sm' />
|
||||
<Icon colorClass='text-white' name='menu-horizontal' size='sm' />
|
||||
</div>
|
||||
);
|
||||
|
||||
|
@ -643,7 +643,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
<div className='relative flex items-center gap-4 px-12 pb-12 pt-60'>
|
||||
<ImageUpload
|
||||
deleteButtonClassName='invisible absolute -right-2 -top-2 flex h-8 w-8 cursor-pointer items-center justify-center rounded-full bg-[rgba(0,0,0,0.75)] text-white hover:bg-black group-hover:!visible'
|
||||
deleteButtonContent={<Icon color='white' name='trash' size='sm' />}
|
||||
deleteButtonContent={<Icon colorClass='text-white' name='trash' size='sm' />}
|
||||
fileUploadClassName='rounded-full bg-black flex items-center justify-center opacity-80 transition hover:opacity-100 -ml-2 cursor-pointer h-[80px] w-[80px]'
|
||||
id='avatar'
|
||||
imageClassName='relative rounded-full group bg-cover bg-center -ml-2 h-[80px] w-[80px]'
|
||||
|
@ -657,7 +657,7 @@ const UserDetailModal:React.FC<UserDetailModalProps> = ({user, updateUser}) => {
|
|||
handleImageUpload('profile_image', file);
|
||||
}}
|
||||
>
|
||||
<Icon color='white' name='user-add' size='lg' />
|
||||
<Icon colorClass='text-white' name='user-add' size='lg' />
|
||||
</ImageUpload>
|
||||
<div>
|
||||
<Heading styles='text-white'>{user.name}{suspendedText}</Heading>
|
||||
|
|
|
@ -1,12 +1,40 @@
|
|||
import ButtonGroup from '../../../../admin-x-ds/global/ButtonGroup';
|
||||
import DesktopChrome from '../../../../admin-x-ds/global/DesktopChrome';
|
||||
import React from 'react';
|
||||
import URLSelect from '../../../../admin-x-ds/global/URLSelect';
|
||||
import {SelectOption} from '../../../../admin-x-ds/global/Select';
|
||||
|
||||
const ThemePreview: React.FC = () => {
|
||||
const urlOptions: SelectOption[] = [
|
||||
{value: 'homepage', label: 'Homepage'},
|
||||
{value: 'post', label: 'Post'}
|
||||
];
|
||||
|
||||
const toolbarCenter = (
|
||||
<URLSelect options={urlOptions} onSelect={(value: string) => {
|
||||
alert(value);
|
||||
}} />
|
||||
);
|
||||
|
||||
const toolbarRight = (
|
||||
<ButtonGroup
|
||||
buttons={[
|
||||
{icon: 'laptop', link: true, size: 'sm'},
|
||||
{icon: 'mobile', link: true, size: 'sm', iconColorClass: 'text-grey-500'}
|
||||
]}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<DesktopChrome>
|
||||
<DesktopChrome
|
||||
chromeClasses='bg-grey-50'
|
||||
toolbarCenter={toolbarCenter}
|
||||
toolbarClasses='m-2'
|
||||
toolbarRight={toolbarRight}
|
||||
>
|
||||
<div className='flex h-full items-center justify-center bg-grey-50 text-sm text-grey-400'>
|
||||
Preview iframe
|
||||
Preview iframe
|
||||
</div>
|
||||
</DesktopChrome>
|
||||
</>
|
||||
|
|