AdminX dark mode (#18035)

refs. https://github.com/TryGhost/Product/issues/3349

- added basic support for dark mode
This commit is contained in:
Peter Zimon 2023-09-08 21:53:41 +03:00 committed by GitHub
parent 7f6bd5ec31
commit 7ebbf9d56e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 162 additions and 90 deletions

View File

@ -20,13 +20,31 @@ const preview: Preview = {
},
},
decorators: [
(Story) => (
<div className="admin-x-settings" style={{ padding: '24px' }}>
(Story, context) => {
let {scheme} = context.globals;
return (
<div className={`admin-x-settings ${scheme === 'dark' ? 'dark' : ''}`} style={{
padding: '24px',
background: (scheme === 'dark' ? '#131416' : '')
}}>
{/* 👇 Decorators in Storybook also accept a function. Replace <Story/> with Story() to enable it */}
<Story />
</div>
),
</div>);
},
],
globalTypes: {
scheme: {
name: "Scheme",
description: "Select light or dark mode",
defaultValue: "light",
toolbar: {
icon: "mirror",
items: ["light", "dark"],
dynamicTitle: true
}
}
}
};
export default preview;

View File

@ -5,6 +5,7 @@ import NiceModal from '@ebay/nice-modal-react';
import RoutingProvider, {ExternalLink} from './components/providers/RoutingProvider';
import Settings from './components/Settings';
import Sidebar from './components/Sidebar';
import clsx from 'clsx';
import {GlobalDirtyStateProvider} from './hooks/useGlobalDirtyState';
import {OfficialTheme, ServicesProvider} from './components/providers/ServiceProvider';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
@ -16,6 +17,7 @@ interface AppProps {
officialThemes: OfficialTheme[];
zapierTemplates: ZapierTemplate[];
externalNavigate: (link: ExternalLink) => void;
darkMode?: boolean;
}
const queryClient = new QueryClient({
@ -28,14 +30,19 @@ const queryClient = new QueryClient({
}
});
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate}: AppProps) {
function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate, darkMode = false}: AppProps) {
const appClassName = clsx(
'admin-x-settings h-[100vh] w-full overflow-y-auto',
darkMode && 'dark'
);
return (
<QueryClientProvider client={queryClient}>
<ServicesProvider ghostVersion={ghostVersion} officialThemes={officialThemes} zapierTemplates={zapierTemplates}>
<GlobalDataProvider>
<RoutingProvider externalNavigate={externalNavigate}>
<GlobalDirtyStateProvider>
<div className="admin-x-settings h-[100vh] w-full overflow-y-auto" id="admin-x-root" style={{
<div className={appClassName} id="admin-x-root" style={{
height: '100vh',
width: '100%'
}}
@ -54,12 +61,12 @@ function App({ghostVersion, officialThemes, zapierTemplates, externalNavigate}:
<div className='-mx-6 h-[84px] bg-white px-6 tablet:m-0 tablet:bg-transparent tablet:p-0'>
<Heading>Settings</Heading>
</div>
<div className="relative mt-[-32px] w-full overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:hidden after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-[''] tablet:w-[260px] tablet:after:!visible tablet:after:!block">
<div className="relative mt-[-32px] w-full overflow-x-hidden after:absolute after:inset-x-0 after:top-0 after:hidden after:h-[40px] after:bg-gradient-to-b after:from-white after:to-transparent after:content-[''] dark:after:from-black tablet:w-[260px] tablet:after:!visible tablet:after:!block">
<Sidebar />
</div>
</div>
<div className="relative flex-auto pt-[3vmin] tablet:ml-[300px] tablet:pt-[85px]">
<div className='pointer-events-none fixed inset-x-0 top-0 z-[5] hidden h-[80px] bg-gradient-to-t from-transparent to-white to-60% tablet:!visible tablet:!block'></div>
<div className='pointer-events-none fixed inset-x-0 top-0 z-[5] hidden h-[80px] bg-gradient-to-t from-transparent to-white to-60% dark:to-black tablet:!visible tablet:!block'></div>
<Settings />
</div>
</div>

View File

@ -37,6 +37,13 @@ export const Black: Story = {
}
};
export const Grey: Story = {
args: {
label: 'Button',
color: 'grey'
}
};
export const Green: Story = {
args: {
label: 'Button',

View File

@ -50,10 +50,10 @@ const Button: React.FC<ButtonProps> = ({
switch (color) {
case 'black':
styles += link ? ' text-black hover:text-grey-800' : ` bg-black text-white ${!disabled && 'hover:bg-grey-900'}`;
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-black text-white dark:bg-white dark:text-black ${!disabled && 'hover:bg-grey-900'}`;
break;
case 'grey':
styles += link ? ' text-black hover:text-grey-800' : ` bg-grey-100 text-black ${!disabled && 'hover:!bg-grey-300'}`;
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` bg-grey-100 text-black dark:bg-grey-900 dark:text-white ${!disabled && 'hover:!bg-grey-300 dark:hover:!bg-grey-800'}`;
break;
case 'green':
styles += link ? ' text-green hover:text-green-400' : ` bg-green text-white ${!disabled && 'hover:bg-green-400'}`;
@ -62,13 +62,13 @@ const Button: React.FC<ButtonProps> = ({
styles += link ? ' text-red hover:text-red-400' : ` bg-red text-white ${!disabled && 'hover:bg-red-400'}`;
break;
case 'white':
styles += link ? ' text-white hover:text-white' : ` bg-white text-black`;
styles += link ? ' text-white hover:text-white dark:text-black dark:hover:text-grey-800' : ` bg-white dark:bg-black text-black dark:text-white`;
break;
case 'outline':
styles += link ? ' text-black hover:text-grey-800' : ` border border-grey-300 bg-transparent text-black ${!disabled && 'hover:!border-black'}`;
styles += link ? ' text-black dark:text-white hover:text-grey-800' : `text-black border border-grey-300 bg-transparent dark:border-grey-800 dark:text-white ${!disabled && 'hover:!border-black dark:hover:!border-white'}`;
break;
default:
styles += link ? ' text-black hover:text-grey-800' : ` text-black ${!disabled && 'hover:bg-grey-200'}`;
styles += link ? ' text-black dark:text-white hover:text-grey-800' : ` text-black dark:text-white dark:hover:bg-grey-900 ${!disabled && 'hover:bg-grey-200'}`;
break;
}

View File

@ -39,7 +39,7 @@ 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';
export const Heading6StylesGrey = 'text-2xs font-semibold uppercase tracking-wider text-grey-800 dark:text-grey-500';
const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> = ({
level = 1,
@ -78,6 +78,7 @@ const Heading: React.FC<Heading1to5Props | Heading6Props | HeadingLabelProps> =
className = clsx(
styles,
'dark:text-white',
className
);

View File

@ -17,6 +17,20 @@ export const Default: Story = {
}
};
export const Error: Story = {
args: {
children: 'This is a hint that should be red',
color: 'red'
}
};
export const Success: Story = {
args: {
children: 'This is a hint that should be green',
color: 'green'
}
};
export const UsingReactNode: Story = {
args: {
children: (

View File

@ -1,8 +1,9 @@
import React from 'react';
import clsx from 'clsx';
interface HintProps {
children?: React.ReactNode;
color?: string;
color?: 'red' | 'green' | 'default' | '';
className?: string;
}
@ -11,8 +12,24 @@ const Hint: React.FC<HintProps> = ({children, color, className, ...props}) => {
return null;
}
let colorClassName = 'text-grey-700 dark:text-grey-600';
switch (color) {
case 'red':
colorClassName = 'text-red dark:text-red-500';
break;
case 'green':
colorClassName = 'text-green dark:text-green-500';
break;
}
className = clsx(
'mt-1 inline-block text-xs',
colorClassName,
className
);
return (
<span className={`mt-1 inline-block text-xs ${color ? `text-${color}` : `text-grey-700`} ${className}`} {...props}>{children}</span>
<span className={className} {...props}>{children}</span>
);
};

View File

@ -43,8 +43,8 @@ const ListItem: React.FC<ListItemProps> = ({
const listItemClasses = clsx(
'group/list-item flex items-center justify-between',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200' : 'border-y border-transparent hover:border-grey-200 first-of-type:hover:border-t-transparent',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50 dark:hover:from-black dark:hover:to-grey-950',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200 dark:border-grey-800 dark:hover:border-grey-700' : 'border-y border-transparent hover:border-grey-200 first-of-type:hover:border-t-transparent dark:hover:border-grey-700',
className
);

View File

@ -4,7 +4,7 @@ interface SeparatorProps {
className?: string;
}
const Separator: React.FC<SeparatorProps> = ({className = 'border-grey-200'}) => {
const Separator: React.FC<SeparatorProps> = ({className = 'border-grey-200 dark:border-grey-800'}) => {
return <hr className={className} />;
};

View File

@ -11,8 +11,8 @@ interface StickyFooterProps {
const StickyFooter: React.FC<StickyFooterProps> = ({
shiftY,
footerBgColorClass = 'bg-white',
contentBgColorClass = 'bg-white',
footerBgColorClass = 'bg-white dark:bg-black',
contentBgColorClass = 'bg-white dark:bg-black',
height = 96,
children
}) => {

View File

@ -44,7 +44,7 @@ function TabView<ID extends string = string>({
width === 'narrow' && 'gap-3',
width === 'normal' && 'gap-5',
width === 'wide' && 'gap-7',
border && 'border-b border-grey-300'
border && 'border-b border-grey-300 dark:border-grey-900'
);
return (
@ -55,9 +55,9 @@ function TabView<ID extends string = string>({
key={tab.id}
aria-selected={selectedTab === tab.id}
className={clsx(
'-m-b-px cursor-pointer appearance-none whitespace-nowrap py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)]',
'-m-b-px cursor-pointer appearance-none whitespace-nowrap py-1 text-sm transition-all after:invisible after:block after:h-px after:overflow-hidden after:font-bold after:text-transparent after:content-[attr(title)] dark:text-white',
border && 'border-b-[3px]',
selectedTab === tab.id && border ? 'border-black' : 'border-transparent hover:border-grey-500',
selectedTab === tab.id && border ? 'border-black dark:border-white' : 'border-transparent hover:border-grey-500',
selectedTab === tab.id && 'font-bold'
)}
id={tab.id}

View File

@ -26,9 +26,9 @@ const TableRow: React.FC<TableRowProps> = ({id, action, hideActions, className,
separator = (separator === undefined) ? true : separator;
const tableRowClasses = clsx(
'group/table-row',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50',
bgOnHover && 'hover:bg-gradient-to-r hover:from-white hover:to-grey-50 dark:hover:from-black dark:hover:to-grey-950',
onClick && 'cursor-pointer',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200' : 'border-y border-transparent first-of-type:hover:border-t-transparent',
separator ? 'border-b border-grey-100 last-of-type:border-b-transparent hover:border-grey-200 dark:border-grey-900 dark:hover:border-grey-800' : 'border-y border-transparent first-of-type:hover:border-t-transparent',
className
);

View File

@ -37,7 +37,7 @@ const Checkbox: React.FC<CheckboxProps> = ({title, label, value, onChange, disab
<label className={`flex cursor-pointer items-start ${title && '-mb-1 mt-1'}`} htmlFor={id}>
<input
checked={isChecked}
className="relative float-left mt-[3px] h-4 w-4 appearance-none border-2 border-solid border-grey-200 bg-grey-200 outline-none checked:border-black checked:bg-black checked:after:absolute checked:after:-mt-px checked:after:ml-[3px] checked:after:block checked:after:h-[11px] checked:after:w-[6px] checked:after:rotate-45 checked:after:border-[2px] checked:after:border-l-0 checked:after:border-t-0 checked:after:border-solid checked:after:border-white checked:after:bg-transparent checked:after:content-[''] hover:cursor-pointer focus:shadow-none focus:transition-[border-color_0.2s] dark:border-grey-600 dark:checked:border-green dark:checked:bg-green"
className="relative float-left mt-[3px] h-4 w-4 appearance-none border-2 border-solid border-grey-200 bg-grey-200 outline-none checked:border-black checked:bg-black checked:after:absolute checked:after:-mt-px checked:after:ml-[3px] checked:after:block checked:after:h-[11px] checked:after:w-[6px] checked:after:rotate-45 checked:after:border-[2px] checked:after:border-l-0 checked:after:border-t-0 checked:after:border-solid checked:after:border-white checked:after:bg-transparent checked:after:content-[''] hover:cursor-pointer focus:shadow-none focus:transition-[border-color_0.2s] dark:border-grey-800 dark:bg-grey-800 dark:checked:border-green dark:checked:bg-green"
disabled={disabled}
id={id}
type='checkbox'
@ -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-[1.425rem] ${hint && '-mb-1'}`}>{label}</span>
<span className={`inline-block text-[1.425rem] dark:text-white ${hint && '-mb-1'}`}>{label}</span>
{hint && <Hint color={error ? 'red' : ''}>{hint}</Hint>}
</div>
</label>

View File

@ -94,7 +94,7 @@ const ColorIndicator: React.FC<{
<button aria-label="Pick color" className="relative h-6 w-6 cursor-pointer rounded-full border border-grey-200 dark:border-grey-800" type="button" onClick={onTogglePicker}>
<div className='absolute inset-0 rounded-full bg-[conic-gradient(hsl(360,100%,50%),hsl(315,100%,50%),hsl(270,100%,50%),hsl(225,100%,50%),hsl(180,100%,50%),hsl(135,100%,50%),hsl(90,100%,50%),hsl(45,100%,50%),hsl(0,100%,50%))]' />
{value && !selectedSwatch && (
<div className="dark:border-grey-950 absolute inset-[3px] overflow-hidden rounded-full border border-white" style={{backgroundColor: value}}>
<div className="absolute inset-[3px] overflow-hidden rounded-full border border-white dark:border-grey-950" style={{backgroundColor: value}}>
{value === 'transparent' && <div className="absolute left-[3px] top-[3px] z-10 w-[136%] origin-left rotate-45 border-b border-b-red" />}
</div>
)}

View File

@ -30,7 +30,7 @@ const FileUpload: React.FC<FileUploadProps> = ({id, onUpload, children, style, u
<label htmlFor={id} style={style} {...props}>
<input key={fileKey} id={id} type="file" hidden onChange={handleFileChange} />
{(typeof children === 'string') ?
<div className={!unstyled ? `inline-flex h-[34px] cursor-pointer items-center justify-center rounded px-4 text-sm font-semibold hover:bg-grey-100` : ''}>
<div className={!unstyled ? `inline-flex h-[34px] cursor-pointer items-center justify-center rounded px-4 text-sm font-semibold hover:bg-grey-100 dark:text-white dark:hover:bg-grey-900` : ''}>
{children}
</div>
:

View File

@ -64,7 +64,7 @@ const ImageUpload: React.FC<ImageUploadProps> = ({
);
fileUploadClassName = clsx(
'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',
'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 dark:border-grey-900 dark:bg-grey-900 dark:text-grey-400',
fileUploadClassName
);

View File

@ -26,9 +26,9 @@ interface MultiSelectProps {
const multiValueColor = (color?: MultiSelectColor) => {
switch (color) {
case 'black':
return 'bg-black text-white';
return 'bg-black text-white dark:bg-white dark:text-black';
case 'grey':
return 'bg-grey-300 text-black';
return 'bg-grey-300 text-black dark:bg-grey-500';
case 'green':
return 'bg-green-500 text-white';
case 'pink':
@ -40,7 +40,7 @@ const multiValueColor = (color?: MultiSelectColor) => {
const DropdownIndicator: React.FC<DropdownIndicatorProps<MultiSelectOption, true> & {clearBg: boolean}> = ({clearBg, ...props}) => (
<components.DropdownIndicator {...props}>
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] ${clearBg ? 'right-0' : 'right-4'} `}></div>
<div className={`absolute top-[14px] block h-2 w-2 rotate-45 border-[1px] border-l-0 border-t-0 border-grey-900 content-[''] dark:border-grey-400 ${clearBg ? 'right-0' : 'right-4'} `}></div>
</components.DropdownIndicator>
);
@ -65,11 +65,11 @@ const MultiSelect: React.FC<MultiSelectProps> = ({
const id = useId();
const customClasses = {
control: `w-full cursor-pointer appearance-none min-h-[40px] border-b ${!clearBg && 'bg-grey-75 px-[10px]'} py-2 outline-none ${error ? 'border-red' : 'border-grey-500 hover:border-grey-700'} ${(title && !clearBg) && 'mt-2'}`,
control: `w-full cursor-pointer appearance-none min-h-[40px] border-b dark:text-white ${!clearBg && 'bg-grey-75 dark:bg-grey-950 px-[10px]'} py-2 outline-none ${error ? 'border-red' : 'border-grey-500 hover:border-grey-700 dark:border-grey-800 dark:hover:border-grey-700'} ${(title && !clearBg) && 'mt-2'}`,
valueContainer: 'gap-1',
placeHolder: 'text-grey-600',
menu: 'shadow py-2 rounded-b z-50 bg-white',
option: 'hover:cursor-pointer hover:bg-grey-100 px-3 py-[6px]',
placeHolder: 'text-grey-500 dark:text-grey-800',
menu: 'shadow py-2 rounded-b z-50 bg-white dark:bg-black dark:border dark:border-grey-900',
option: 'hover:cursor-pointer hover:bg-grey-100 px-3 py-[6px] dark:text-white dark:hover:bg-grey-900',
multiValue: (optionColor?: MultiSelectColor) => `rounded-sm items-center text-[14px] py-px pl-2 pr-1 gap-1.5 ${multiValueColor(optionColor || color)}`,
noOptionsMessage: 'p-3 text-grey-600',
groupHeading: 'py-[6px] px-3 text-2xs font-semibold uppercase tracking-wide text-grey-700'

View File

@ -33,7 +33,7 @@ const Radio: React.FC<RadioProps> = ({id, title, options, onSelect, error, hint,
<label key={option.value} className={`flex cursor-pointer items-start ${title && '-mb-1 mt-1'}`} htmlFor={option.value}>
<input
checked={selectedOption === option.value}
className="relative float-left mt-[3px] h-4 w-4 min-w-[16px] appearance-none rounded-full border-2 border-solid border-grey-300 after:absolute after:z-[1] after:block after:h-3 after:w-3 after:rounded-full after:content-[''] checked:border-green checked:after:absolute checked:after:left-1/2 checked:after:top-1/2 checked:after:h-[0.625rem] checked:after:w-[0.625rem] checked:after:rounded-full checked:after:border-green checked:after:bg-green checked:after:content-[''] checked:after:[transform:translate(-50%,-50%)] hover:cursor-pointer focus:shadow-none focus:outline-none focus:ring-0 checked:focus:border-green dark:border-grey-600 dark:checked:border-green dark:checked:after:border-green dark:checked:after:bg-green dark:checked:focus:border-green"
className="relative float-left mt-[3px] h-4 w-4 min-w-[16px] appearance-none rounded-full border-2 border-solid border-grey-300 after:absolute after:z-[1] after:block after:h-3 after:w-3 after:rounded-full after:content-[''] checked:border-green checked:after:absolute checked:after:left-1/2 checked:after:top-1/2 checked:after:h-[0.625rem] checked:after:w-[0.625rem] checked:after:rounded-full checked:after:border-green checked:after:bg-green checked:after:content-[''] checked:after:[transform:translate(-50%,-50%)] hover:cursor-pointer focus:shadow-none focus:outline-none focus:ring-0 checked:focus:border-green dark:border-grey-800 dark:text-white dark:checked:border-green dark:checked:after:border-green dark:checked:after:bg-green dark:checked:focus:border-green"
id={option.value}
name={id}
type='radio'
@ -41,7 +41,7 @@ const Radio: React.FC<RadioProps> = ({id, title, options, onSelect, error, hint,
onChange={handleOptionChange}
/>
<div className={`ml-2 flex flex-col ${option.hint && 'mb-2'}`}>
<span className={`inline-block text-md ${option.hint && '-mb-1'}`}>{option.label}</span>
<span className={`inline-block text-md dark:text-white ${option.hint && '-mb-1'}`}>{option.label}</span>
{option.hint && <Hint>{option.hint}</Hint>}
</div>
</label>

View File

@ -63,8 +63,8 @@ const Select: React.FC<SelectProps> = ({
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-['']`,
'relative w-full after:pointer-events-none dark:text-white',
`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-[''] dark:after:border-grey-500`,
size === 'xs' ? 'after:top-[6px]' : 'after:top-[14px]',
clearBg ? 'after:right-0' : 'after:right-4',
disabled && 'opacity-40'
@ -82,7 +82,7 @@ const Select: React.FC<SelectProps> = ({
'w-full appearance-none outline-none',
border && 'border-b',
!clearBg && 'bg-grey-75 px-[10px]',
error ? 'border-red' : 'border-grey-500 focus:border-black',
error ? '!border-red' : 'border-grey-500 focus:border-black dark:border-grey-800 dark:focus:border-grey-500',
disabled ? 'cursor-auto' : 'cursor-pointer hover:border-grey-700',
(title && !clearBg) && 'mt-2'
);

View File

@ -42,9 +42,9 @@ const TextArea: React.FC<TextAreaProps> = ({
const id = useId();
let styles = clsx(
'peer order-2 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',
'peer order-2 rounded-sm border px-3 py-2 dark:text-white',
clearBg ? 'bg-transparent' : 'bg-grey-75 dark:bg-grey-900',
error ? 'border-red' : 'border-grey-500 placeholder:text-grey-500 hover:border-grey-700 focus:border-grey-800 dark:border-grey-800 dark:placeholder:text-grey-800 dark:hover:border-grey-700 dark:focus:border-grey-500',
title && 'mt-2',
fontStyle === 'mono' && 'font-mono text-sm',
className
@ -81,7 +81,7 @@ const TextArea: React.FC<TextAreaProps> = ({
onChange={onChange}
{...props}>
</textarea>
{title && <Heading className={'order-1 !text-grey-700 peer-focus:!text-black'} htmlFor={id} useLabelTag={true}>{title}</Heading>}
{title && <Heading className={'order-1 !text-grey-700 peer-focus:!text-black dark:!text-grey-300 dark:peer-focus:!text-white'} htmlFor={id} useLabelTag={true}>{title}</Heading>}
{hint && <Hint className='order-3' color={error ? 'red' : ''}>{hint}</Hint>}
{maxLength && <Hint>Max length is {maxLength}</Hint>}
</div>

View File

@ -33,6 +33,7 @@ export const Default: Story = {
export const Disabled: Story = {
args: {
placeholder: `Here's a disabled field`,
value: 'Hello disabled field',
title: 'Disabled',
disabled: true
}

View File

@ -51,17 +51,17 @@ const TextField: React.FC<TextFieldProps> = ({
}) => {
const id = useId();
const disabledBorderClasses = border && 'border-grey-300';
const enabledBorderClasses = border && 'border-grey-500 hover:border-grey-700 focus:border-black';
const disabledBorderClasses = border && 'border-grey-300 dark:border-grey-900';
const enabledBorderClasses = border && 'border-grey-500 hover:border-grey-700 focus:border-black dark:border-grey-800 dark:hover:border-grey-700 dark:focus:border-grey-500';
const textFieldClasses = !unstyled && clsx(
'peer order-2 h-8 w-full py-1 text-sm md:h-10 md:py-2 md:text-base',
'peer order-2 h-8 w-full py-1 text-sm placeholder:text-grey-500 dark:text-white dark:placeholder:text-grey-800 md:h-10 md:py-2 md:text-base',
border && 'border-b',
!border && '-mb-1.5',
clearBg ? 'bg-transparent' : 'bg-grey-75 px-[10px]',
error && border ? `border-red` : `${disabled ? disabledBorderClasses : enabledBorderClasses}`,
(title && !hideTitle && !clearBg) && `mt-2`,
(disabled ? 'cursor-not-allowed text-grey-700' : ''),
(disabled ? 'cursor-not-allowed text-grey-700 opacity-60 dark:text-grey-800' : ''),
rightPlaceholder && 'w-0 grow',
className
);
@ -82,7 +82,7 @@ const TextField: React.FC<TextFieldProps> = ({
{...props} />;
if (rightPlaceholder) {
const rightPHEnabledBorderClasses = 'border-grey-500 peer-hover:border-grey-700 peer-focus:border-black';
const rightPHEnabledBorderClasses = 'border-grey-500 dark:border-grey-800 peer-hover:border-grey-700 peer-focus:border-black dark:peer-focus:border-grey-500';
const rightPHClasses = !unstyled && clsx(
'order-3',
border && 'border-b',
@ -115,8 +115,8 @@ const TextField: React.FC<TextFieldProps> = ({
return (
<div className={containerClassName}>
{field}
{title && <Heading className={hideTitle ? 'sr-only' : 'order-1 !text-grey-700 peer-focus:!text-black'} htmlFor={id} useLabelTag={true}>{title}</Heading>}
{hint && <Hint className={hintClassName} color={error ? 'red' : ''}>{hint}</Hint>}
{title && <Heading className={hideTitle ? 'sr-only' : 'order-1 !text-grey-700 peer-focus:!text-black dark:!text-grey-300 dark:peer-focus:!text-white'} htmlFor={id} useLabelTag={true}>{title}</Heading>}
{hint && <Hint className={hintClassName} color={error ? 'red' : 'default'}>{hint}</Hint>}
</div>
);
} else {

View File

@ -81,9 +81,9 @@ const Toggle: React.FC<ToggleProps> = ({
return (
<div>
<div className={`group flex items-start gap-2 ${direction === 'rtl' && 'justify-between'} ${separator && 'pb-2'}`}>
<div className={`group flex items-start gap-2 dark:text-white ${direction === 'rtl' && 'justify-between'} ${separator && 'pb-2'}`}>
<input checked={checked}
className={`${toggleBgClass} appearance-none rounded-full bg-grey-300 transition after:absolute after:ml-0.5 after:mt-0.5 after:rounded-full after:border-none after:bg-white after:transition-[background-color_0.2s,transform_0.2s] after:content-[''] checked:after:absolute checked:after:rounded-full checked:after:border-none checked:after:bg-white checked:after:transition-[background-color_0.2s,transform_0.2s] checked:after:content-[''] hover:cursor-pointer group-hover:opacity-80 ${sizeStyles} ${direction === 'rtl' && ' order-2'}`}
className={`${toggleBgClass} appearance-none rounded-full bg-grey-300 transition after:absolute after:ml-0.5 after:mt-0.5 after:rounded-full after:border-none after:bg-white after:transition-[background-color_0.2s,transform_0.2s] after:content-[''] checked:after:absolute checked:after:rounded-full checked:after:border-none checked:after:bg-white checked:after:transition-[background-color_0.2s,transform_0.2s] checked:after:content-[''] hover:cursor-pointer group-hover:opacity-80 dark:bg-grey-800 dark:checked:bg-green ${sizeStyles} ${direction === 'rtl' && ' order-2'}`}
id={id}
role="switch"
type="checkbox"
@ -96,7 +96,7 @@ const Toggle: React.FC<ToggleProps> = ({
:
<span>{label}</span>
}
{hint && <span className={`text-xs ${error ? 'text-red' : 'text-grey-700'}`}>{hint}</span>}
{hint && <span className={`text-xs ${error ? 'text-red' : 'text-grey-700 dark:text-grey-600'}`}>{hint}</span>}
</label>
}
</div>

View File

@ -4,7 +4,7 @@ import URLSelect from './URLSelect';
import {SelectOption} from './Select';
const meta = {
title: 'Global / Form / URL Select',
title: 'Experimental / URL Select',
component: URLSelect,
tags: ['autodocs']
} satisfies Meta<typeof URLSelect>;

View File

@ -130,7 +130,7 @@ const Modal: React.FC<ModalProps> = ({
}
let modalClasses = clsx(
'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden rounded bg-white',
'relative z-50 mx-auto flex max-h-[100%] w-full flex-col justify-between overflow-x-hidden rounded bg-white dark:bg-black',
formSheet ? 'shadow-md' : 'shadow-xl',
(animate && !formSheet) && 'animate-modal-in',
formSheet && 'animate-modal-in-reverse',
@ -256,7 +256,7 @@ const Modal: React.FC<ModalProps> = ({
(<>
{title && <Heading level={3}>{title}</Heading>}
<div className={`${topRightContent !== 'close' && 'md:!invisible md:!hidden'} ${hideXOnMobile && 'hidden'} absolute right-6 top-6`}>
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' size='sm' unstyled onClick={removeModal} />
<Button className='-m-2 cursor-pointer p-2 opacity-50 hover:opacity-100' icon='close' iconColorClass='text-black dark:text-white' size='sm' unstyled onClick={removeModal} />
</div>
</>)
:

View File

@ -78,9 +78,9 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
if (saveState === 'unsaved') {
styles += ' border-green';
} else if (isEditing){
styles += ' border-grey-300';
styles += ' border-grey-300 dark:border-grey-800';
} else {
styles += ' border-grey-200';
styles += ' border-grey-200 dark:border-grey-900';
}
let viewButtons: ButtonProps[] = [];
@ -167,7 +167,7 @@ const SettingGroup: React.FC<SettingGroupProps> = ({
border && 'border p-5 md:p-7',
!checkVisible(keywords) ? 'hidden' : 'flex',
highlight && 'before:pointer-events-none before:absolute before:inset-[1px] before:animate-setting-highlight-fade-out before:rounded before:shadow-[0_0_0_3px_rgba(48,207,67,0.45)]',
!isEditing && 'is-not-editing group',
!isEditing && 'is-not-editing group/setting-group',
styles
);

View File

@ -13,7 +13,7 @@ const SettingGroupHeader: React.FC<Props> = ({title, description, children}) =>
{(title || description) &&
<div>
<Heading level={5}>{title}</Heading>
{description && <p className="mt-0.5 hidden max-w-lg text-sm group-[.is-not-editing]:!visible group-[.is-not-editing]:!block md:!visible md:!block">{description}</p>}
{description && <p className="mt-0.5 hidden max-w-lg text-sm 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'>

View File

@ -16,7 +16,7 @@ const SettingNavItem: React.FC<Props> = ({
const {scrolledRoute} = useRouting();
const classNames = clsx(
'block px-0 py-1 text-sm',
'block px-0 py-1 text-sm dark:text-white',
(scrolledRoute === navid) && 'font-bold'
);

View File

@ -8,7 +8,7 @@ interface Props {
const SettingSectionHeader: React.FC<Props> = ({title, sticky = false}) => {
let styles = 'pb-4 text-2xs font-semibold uppercase tracking-wider text-grey-700 z-10 ';
if (sticky) {
styles += ' sticky top-0 -mt-4 pt-4 bg-white';
styles += ' sticky top-0 -mt-4 pt-4 bg-white dark:bg-black';
}
return (
<h2 className={styles}>{title}</h2>

View File

@ -28,7 +28,7 @@ const Sidebar: React.FC = () => {
<div className='tablet:h-[calc(100vh-5vmin-84px)] tablet:w-[240px] tablet:overflow-y-scroll'>
<div className='relative mb-10 md:pt-4 tablet:pt-[32px]'>
<Icon className='absolute top-2 md:top-6 tablet:top-10' colorClass='text-grey-500' name='magnifying-glass' size='sm' />
<TextField autoComplete="off" className='border-b border-grey-500 px-3 py-1.5 pl-[24px] text-sm' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={e => setFilter(e.target.value)} />
<TextField autoComplete="off" className='border-b border-grey-500 bg-transparent px-3 py-1.5 pl-[24px] text-sm dark:text-white' placeholder="Search" title="Search" value={filter} hideTitle unstyled onChange={e => setFilter(e.target.value)} />
</div>
<div className="hidden tablet:!visible tablet:!block">
<SettingNavSection title="General">

View File

@ -59,7 +59,7 @@ const Labs: React.FC<{ keywords: string[] }> = ({keywords}) => {
<TabView<'labs-migration-options' | 'labs-alpha-features' | 'labs-beta-features'> selectedTab={selectedTab} tabs={tabs} onTabChange={setSelectedTab} />
:
<div className='absolute inset-0 z-0 overflow-hidden opacity-70'>
<img className='absolute -right-6 -top-6' src={LabsBubbles} />
<img className='absolute -right-6 -top-6 dark:opacity-10' src={LabsBubbles} />
</div>
}
</SettingGroup>

View File

@ -54,16 +54,16 @@ const Facebook: React.FC<{ keywords: string[] }> = ({keywords}) => {
<FacebookLogo className='h-10 w-10' />
</div>
<div>
<div className="mb-1 font-semibold leading-none text-grey-900">{siteTitle}</div>
<div className="mb-1 font-semibold leading-none text-grey-900 dark:text-grey-300">{siteTitle}</div>
<div className="leading-none text-grey-700">2h</div>
</div>
</div>
<div>
<div className="mb-2 h-3 w-full rounded bg-grey-200"></div>
<div className="mb-4 h-3 w-3/5 rounded bg-grey-200"></div>
<SettingGroupContent className="overflow-hidden rounded-md border border-grey-300">
<div className="mb-2 h-3 w-full rounded bg-grey-200 dark:bg-grey-900"></div>
<div className="mb-4 h-3 w-3/5 rounded bg-grey-200 dark:bg-grey-900"></div>
<SettingGroupContent className="overflow-hidden rounded-md border border-grey-300 dark:border-grey-900">
<ImageUpload
fileUploadClassName='flex cursor-pointer items-center justify-center rounded rounded-b-none border border-grey-100 border-b-0 bg-grey-75 p-3 text-sm font-semibold text-grey-800 hover:text-black'
fileUploadClassName='flex cursor-pointer items-center justify-center rounded rounded-b-none border border-grey-100 border-b-0 bg-grey-75 p-3 text-sm font-semibold text-grey-800 hover:text-black dark:border-grey-900'
height='300px'
id='facebook-image'
imageURL={facebookImage}

View File

@ -32,13 +32,13 @@ const SearchEnginePreview: React.FC<SearchEnginePreviewProps> = ({
<GoogleLogo className='mr-7 h-7' />
</div>
<div className='grow'>
<div className='flex w-full items-center justify-end rounded-full bg-white p-3 px-4 shadow'>
<div className='flex w-full items-center justify-end rounded-full bg-white p-3 px-4 shadow dark:bg-grey-900'>
<MagnifyingGlass className='h-4 w-4 text-blue-600' style={{strokeWidth: '2px'}} />
</div>
</div>
</div>
<div className='mt-4 flex items-center gap-2 border-t border-grey-200 pt-4'>
<div className='flex h-7 w-7 items-center justify-center rounded-full bg-grey-200' style={{
<div className='mt-4 flex items-center gap-2 border-t border-grey-200 pt-4 dark:border-grey-900'>
<div className='flex h-7 w-7 items-center justify-center rounded-full bg-grey-200 dark:bg-grey-700' style={{
backgroundImage: icon ? `url(${icon})` : 'none'
}}>
</div>
@ -48,8 +48,8 @@ const SearchEnginePreview: React.FC<SearchEnginePreviewProps> = ({
</div>
</div>
<div className='mt-1 flex flex-col'>
<span className='text-lg text-[#1a0dab]'>{title}</span>
<span className='text-sm text-grey-900'>{description}</span>
<span className='text-lg text-[#1a0dab] dark:text-blue'>{title}</span>
<span className='text-sm text-grey-900 dark:text-grey-700'>{description}</span>
</div>
</div>
);

View File

@ -58,14 +58,14 @@ const Twitter: React.FC<{ keywords: string[] }> = ({keywords}) => {
</div>
<div className="w-full md:mr-[52px]">
<div className="mb-2">
<span className="mr-1 font-semibold text-grey-900">{siteTitle}</span>
<span className="mr-1 font-semibold text-grey-900 dark:text-grey-300">{siteTitle}</span>
<span className="text-grey-700">&#183; 2h</span>
</div>
<div className="mb-2 h-3 w-full rounded bg-grey-200"></div>
<div className="mb-4 h-3 w-3/5 rounded bg-grey-200"></div>
<SettingGroupContent className="overflow-hidden rounded-md border border-grey-300">
<div className="mb-2 h-3 w-full rounded bg-grey-200 dark:bg-grey-900"></div>
<div className="mb-4 h-3 w-3/5 rounded bg-grey-200 dark:bg-grey-900"></div>
<SettingGroupContent className="overflow-hidden rounded-md border border-grey-300 dark:border-grey-900">
<ImageUpload
fileUploadClassName='flex cursor-pointer items-center justify-center rounded rounded-b-none border border-grey-100 border-b-0 bg-grey-75 p-3 text-sm font-semibold text-grey-800 hover:text-black'
fileUploadClassName='flex cursor-pointer items-center justify-center rounded rounded-b-none border border-grey-100 border-b-0 bg-grey-75 p-3 text-sm font-semibold text-grey-800 hover:text-black dark:border-grey-900'
height='300px'
id='twitter-image'
imageURL={twitterImage}

View File

@ -11,7 +11,7 @@ import {useGlobalData} from '../../providers/GlobalDataProvider';
const StripeConnectedButton: React.FC<{className?: string; onClick: () => void;}> = ({className, onClick}) => {
className = clsx(
'group flex shrink-0 items-center justify-center whitespace-nowrap rounded border border-grey-300 px-3 py-1.5 text-sm font-semibold text-grey-900 transition-all hover:border-grey-500',
'group flex shrink-0 items-center justify-center whitespace-nowrap rounded border border-grey-300 px-3 py-1.5 text-sm font-semibold text-grey-900 transition-all hover:border-grey-500 dark:border-grey-900 dark:text-white',
className
);
return (

View File

@ -75,9 +75,9 @@ const TipsOrDonations: React.FC<{ keywords: string[] }> = ({keywords}) => {
<Heading level={6}>Shareable link &mdash;</Heading>
<button className='text-2xs font-semibold uppercase tracking-wider text-green' type="button" onClick={openPreview}>Preview</button>
</div>
<div className='w-100 group relative -m-1 mt-0 overflow-hidden rounded p-1 hover:bg-grey-50'>
<div className='w-100 group relative -m-1 mt-0 overflow-hidden rounded p-1 hover:bg-grey-50 dark:hover:bg-grey-900'>
{donateUrl}
<div className='invisible absolute right-0 top-[50%] flex translate-y-[-50%] gap-1 bg-white pl-1 group-hover:visible'>
<div className='invisible absolute right-0 top-[50%] flex translate-y-[-50%] gap-1 bg-white pl-1 group-hover:visible dark:bg-black'>
<Button color='outline' label={copied ? 'Copied' : 'Copy'} size='sm' onClick={copyDonateUrl} />
</div>
</div>

View File

@ -16,7 +16,7 @@ interface TierCardProps {
tier: Tier;
}
const cardContainerClasses = 'tablet:group flex min-[900px]:min-h-[200px] flex-col items-start justify-between gap-4 self-stretch rounded-sm border border-grey-300 p-4 transition-all hover:border-grey-400';
const cardContainerClasses = 'group/tiercard flex min-[900px]:min-h-[200px] flex-col items-start justify-between gap-4 self-stretch rounded-sm border border-grey-300 p-4 transition-all hover:border-grey-400 dark:border-grey-900 dark:hover:border-grey-700';
const TierCard: React.FC<TierCardProps> = ({tier}) => {
const {updateRoute} = useRouting();
@ -41,11 +41,11 @@ const TierCard: React.FC<TierCardProps> = ({tier}) => {
</div>
{tier.monthly_price && (
tier.active ?
<Button className='group group-hover:opacity-100 tablet:opacity-0' color='red' label='Archive' link onClick={() => {
<Button className='group group-hover/tiercard:opacity-100 tablet:opacity-0' color='red' label='Archive' link onClick={() => {
updateTier({...tier, active: false});
}}/>
:
<Button className='group group-hover:opacity-100 tablet:opacity-0' color='green' label='Activate' link onClick={() => {
<Button className='group group-hover/tiercard:opacity-100 tablet:opacity-0' color='green' label='Activate' link onClick={() => {
updateTier({...tier, active: true});
}}/>
)}

View File

@ -61,4 +61,8 @@ html, body, #root {
width: 100%;
height: 100%;
letter-spacing: unset;
}
.dark {
color: #FAFAFB;
}

View File

@ -3,6 +3,7 @@ module.exports = {
corePlugins: {
preflight: false // we're providing our own scoped CSS reset
},
darkMode: 'class',
important: '.admin-x-settings',
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
@ -30,7 +31,8 @@ module.exports = {
600: '#95A1AD',
700: '#7C8B9A',
800: '#626D79',
900: '#394047'
900: '#394047',
950: '#222427'
},
green: {
DEFAULT: '#30CF43',

View File

@ -297,6 +297,7 @@ export default class AdminXSettings extends Component {
officialThemes={officialThemes}
zapierTemplates={zapierTemplates}
externalNavigate={this.externalNavigate}
darkMode={this.feature.nightShift}
/>
</Suspense>
</ErrorHandler>