Merge branch 'black-mode' into 'main'

Black mode

See merge request soapbox-pub/soapbox!2959
This commit is contained in:
marcin mikołajczak 2024-03-24 23:24:11 +00:00
commit a4bea06b76
67 changed files with 137 additions and 103 deletions

View file

@ -33,7 +33,7 @@ const AnnouncementsPanel = () => {
return (
<Widget title={<FormattedMessage id='announcements.title' defaultMessage='Announcements' />}>
<Card className='relative' size='md' variant='rounded'>
<Card className='relative black:rounded-xl black:border black:border-gray-800' size='md' variant='rounded'>
<ReactSwipeableViews animateHeight index={index} onChangeIndex={handleChangeIndex}>
{announcements.map((announcement) => (
<Announcement

View file

@ -199,8 +199,8 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
key={key}
data-index={i}
className={clsx({
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
'px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group black:hover:bg-gray-900 black:focus:bg-gray-900': true,
'bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 black:bg-gray-900 black:hover:bg-gray-900': i === selectedSuggestion,
})}
onMouseDown={this.onSuggestionClick}
onTouchEnd={this.onSuggestionClick}

View file

@ -86,7 +86,7 @@ const DropdownMenuItem = ({ index, item, onClick }: IDropdownMenuItem) => {
title={item.text}
className={
clsx({
'flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-gray-800 focus:outline-none cursor-pointer': true,
'flex px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-gray-800 focus:outline-none cursor-pointer black:hover:bg-gray-900 black:focus:bg-gray-900': true,
'text-danger-600 dark:text-danger-400': item.destructive,
})
}

View file

@ -293,7 +293,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
data-testid='dropdown-menu'
ref={refs.setFloating}
className={
clsx('z-[1001] w-56 rounded-md bg-white py-1 shadow-lg transition-opacity duration-100 focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
clsx('z-[1001] w-56 rounded-md bg-white py-1 shadow-lg transition-opacity duration-100 focus:outline-none black:bg-black dark:bg-gray-900 dark:ring-2 dark:ring-primary-700', {
'opacity-0 pointer-events-none': !isOpen,
})
}
@ -318,7 +318,7 @@ const DropdownMenu = (props: IDropdownMenu) => {
<div
ref={arrowRef}
style={arrowProps}
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white dark:bg-gray-900'
className='pointer-events-none absolute z-[-1] h-3 w-3 bg-white black:bg-black dark:bg-gray-900'
/>
</div>
</Portal>

View file

@ -51,7 +51,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
));
return (
<div className={clsx('relative w-full overflow-hidden rounded-lg bg-gray-100 dark:bg-primary-800', className)}>
<div className={clsx('relative w-full overflow-hidden rounded-lg bg-gray-100 black:border black:border-gray-800 black:bg-black dark:bg-primary-800', className)}>
<div className='absolute right-3 top-28'>
{floatingAction && action}
</div>

View file

@ -17,7 +17,7 @@ interface IGroupCard {
const GroupCard: React.FC<IGroupCard> = ({ group }) => {
return (
<Stack
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'
className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white black:bg-white dark:border-primary-800 dark:bg-primary-900'
data-testid='group-card'
>
{/* Group Cover Image */}

View file

@ -45,7 +45,7 @@ const GroupPopover = (props: IGroupPopoverContainer) => {
content={
<Stack space={4} className='w-80 pb-4'>
<Stack
className='relative h-60 rounded-lg bg-white dark:border-primary-800 dark:bg-primary-900'
className='relative h-60 rounded-lg bg-white black:bg-white dark:border-primary-800 dark:bg-primary-900'
data-testid='group-card'
>
{/* Group Cover Image */}

View file

@ -2,7 +2,7 @@ import React from 'react';
/** Fullscreen gradient used as a backdrop to public pages. */
const LandingGradient: React.FC = () => (
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-gradient-end/10 dark:from-primary-900/50 dark:via-primary-900 dark:to-primary-800/50' />
<div className='fixed h-screen w-full bg-gradient-to-tr from-primary-50 via-white to-gradient-end/10 black:hidden dark:from-primary-900/50 dark:via-primary-900 dark:to-primary-800/50' />
);
export default LandingGradient;

View file

@ -232,7 +232,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
<div
role='presentation'
id='modal-overlay'
className='fixed inset-0 bg-gray-500/90 backdrop-blur dark:bg-gray-700/90'
className='fixed inset-0 bg-gray-500/90 backdrop-blur black:bg-gray-900/90 dark:bg-gray-700/90'
onClick={handleOnClose}
/>

View file

@ -226,7 +226,6 @@ const ScrollableList = React.forwardRef<VirtuosoHandle, IScrollableList>(({
ref={ref}
id={id}
useWindowScroll={useWindowScroll}
className={className}
data={data}
startReached={onScrollToTop}
endReached={handleEndReached}

View file

@ -144,7 +144,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
}
>
<div
className='fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90'
className='fixed inset-0 bg-gray-500/90 black:bg-gray-900/90 dark:bg-gray-700/90'
role='button'
onClick={handleClose}
/>
@ -153,7 +153,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
<div
className={
clsx({
'flex flex-col flex-1 bg-white dark:bg-primary-900 -translate-x-full rtl:translate-x-full w-full max-w-xs': true,
'flex flex-col flex-1 bg-white black:bg-black dark:bg-primary-900 -translate-x-full rtl:translate-x-full w-full max-w-xs': true,
'!translate-x-0': sidebarOpen,
})
}
@ -350,7 +350,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
</button>
{switcher && (
<div className='border-t-2 border-solid border-gray-100 dark:border-gray-800'>
<div className='border-t-2 border-solid border-gray-100 black:border-t dark:border-gray-800'>
{otherAccounts.map(account => renderAccount(account))}
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>

View file

@ -49,7 +49,7 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
count={count}
countMax={countMax}
className={clsx('h-5 w-5', {
'text-gray-600 dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400': !isActive,
'text-gray-600 black:text-white dark:text-gray-500 group-hover:text-primary-500 dark:group-hover:text-primary-400': !isActive,
'text-primary-500 dark:text-primary-400': isActive,
})}
/>

View file

@ -74,7 +74,7 @@ const SiteErrorBoundary: React.FC<ISiteErrorBoundary> = ({ children }) => {
}
const fallback = (
<div className='flex h-screen flex-col bg-white pb-12 pt-16 dark:bg-primary-900'>
<div className='flex h-screen flex-col bg-white pb-12 pt-16 black:bg-black dark:bg-primary-900'>
<main className='mx-auto flex w-full max-w-7xl grow flex-col justify-center px-4 sm:px-6 lg:px-8'>
<div className='flex shrink-0 justify-center'>
<a href='/' className='inline-flex'>

View file

@ -15,7 +15,7 @@ const SiteLogo: React.FC<ISiteLogo> = ({ className, theme, ...rest }) => {
const { logo, logoDarkMode } = useSoapboxConfig();
const { demo } = useSettings();
let darkMode = useTheme() === 'dark';
let darkMode = ['dark', 'black'].includes(useTheme());
if (theme === 'dark') darkMode = true;
/** Soapbox logo. */

View file

@ -56,6 +56,7 @@ const StatusList: React.FC<IStatusList> = ({
isPartial,
showAds = false,
showGroup = true,
className,
...other
}) => {
const soapboxConfig = useSoapboxConfig();
@ -233,7 +234,7 @@ const StatusList: React.FC<IStatusList> = ({
ref={node}
className={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', {
'divide-none': divideType !== 'border',
})}
}, className)}
itemClassName={clsx({
'pb-3': divideType !== 'border',
})}

View file

@ -35,7 +35,7 @@ const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax,
src={src}
className={clsx({
'h-5 w-5': true,
'text-gray-600': !active,
'text-gray-600 black:text-white': !active,
'text-primary-500': active,
})}
count={count}
@ -46,7 +46,7 @@ const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax,
src={src}
className={clsx({
'h-5 w-5': true,
'text-gray-600': !active,
'text-gray-600 black:text-white': !active,
'text-primary-500': active,
})}
/>

View file

@ -38,9 +38,10 @@ const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'def
ref={ref}
{...filteredProps}
className={clsx({
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none': variant === 'rounded',
'bg-white dark:bg-primary-900 black:bg-black text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none': variant === 'rounded',
[sizes[size]]: variant === 'rounded',
'py-4': variant === 'slim',
'black:rounded-none': size !== 'xl',
}, className)}
>
{children}

View file

@ -102,8 +102,8 @@ const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedR
backHref={backHref}
className={clsx({
'rounded-t-3xl': !isScrolled && !transparent,
'sticky top-12 z-10 bg-white/90 dark:bg-primary-900/90 backdrop-blur lg:top-16': !transparent,
'p-4 sm:p-0 sm:pb-4': transparent,
'sticky top-12 z-10 bg-white/90 dark:bg-primary-900/90 black:bg-black/90 backdrop-blur lg:top-16': !transparent,
'p-4 sm:p-0 sm:pb-4 black:p-4': transparent,
'-mt-4 -mx-4 p-4': size !== 'lg' && !transparent,
'-mt-4 -mx-4 p-4 sm:-mt-6 sm:-mx-6 sm:p-6': size === 'lg' && !transparent,
})}

View file

@ -13,7 +13,7 @@ interface IDivider {
const Divider = ({ text, textSize = 'md' }: IDivider) => (
<div className='relative' data-testid='divider'>
<div className='absolute inset-0 flex items-center' aria-hidden='true'>
<div className='w-full border-t-2 border-solid border-gray-100 dark:border-gray-800' />
<div className='w-full border-t-2 border-solid border-gray-100 black:border-t dark:border-gray-800' />
</div>
{text && (

View file

@ -92,7 +92,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
['normal', 'search'].includes(theme),
'text-gray-900 dark:text-gray-100': !props.disabled,
'text-gray-600': props.disabled,
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800 black:bg-black': theme === 'normal',
'rounded-full bg-gray-200 border-gray-200 dark:bg-gray-800 dark:border-gray-800 focus:bg-white dark:focus:bg-gray-900': theme === 'search',
'pr-10 rtl:pl-10 rtl:pr-3': isPassword || append,
'pl-8': typeof icon !== 'undefined',

View file

@ -21,8 +21,8 @@ interface LayoutComponent extends React.FC<ILayout> {
/** Layout container, to hold Sidebar, Main, and Aside. */
const Layout: LayoutComponent = ({ children }) => (
<div className='relative sm:pt-4'>
<div className='mx-auto max-w-3xl sm:px-6 md:grid md:max-w-7xl md:grid-cols-12 md:gap-8 md:px-8'>
<div className='relative flex grow flex-col black:pt-0 sm:pt-4'>
<div className='mx-auto w-full max-w-3xl grow sm:px-6 md:grid md:max-w-7xl md:grid-cols-12 md:gap-8 md:px-8'>
{children}
</div>
</div>
@ -41,7 +41,7 @@ const Sidebar: React.FC<ISidebar> = ({ children }) => (
const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className }) => (
<main
className={clsx({
'md:col-span-12 lg:col-span-9 xl:col-span-6 pb-36': true,
'md:col-span-12 lg:col-span-9 xl:col-span-6 pb-36 black:border-gray-800 lg:black:border-l xl:black:border-r': true,
}, className)}
>
{children}

View file

@ -28,7 +28,7 @@ const MenuList: React.FC<IMenuList> = (props) => {
<MenuItems
onKeyDown={(event) => event.nativeEvent.stopImmediatePropagation()}
className={
clsx(className, 'shadow-menu rounded-lg bg-white py-1 dark:bg-primary-900')
clsx(className, 'shadow-menu rounded-lg bg-white py-1 black:bg-black dark:bg-primary-900')
}
{...filteredProps}
/>
@ -37,6 +37,6 @@ const MenuList: React.FC<IMenuList> = (props) => {
};
/** Divides menu items. */
const MenuDivider = () => <hr className='mx-2 my-1 border-t-2 border-gray-100 dark:border-gray-800' />;
const MenuDivider = () => <hr className='mx-2 my-1 border-t-2 border-gray-100 black:border-t dark:border-gray-800' />;
export { Menu, MenuButton, MenuDivider, MenuItems, MenuItem, MenuList, MenuLink };

View file

@ -95,7 +95,7 @@ const Modal = React.forwardRef<HTMLDivElement, IModal>(({
<div
ref={ref}
data-testid='modal'
className={clsx(className, 'pointer-events-auto mx-auto block w-full rounded-2xl bg-white p-6 text-start align-middle text-gray-900 shadow-xl transition-all dark:bg-primary-900 dark:text-gray-100', widths[width])}
className={clsx(className, 'pointer-events-auto mx-auto block w-full rounded-2xl bg-white p-6 text-start align-middle text-gray-900 shadow-xl transition-all black:bg-black dark:bg-primary-900 dark:text-gray-100', widths[width])}
>
<div className='w-full justify-between sm:flex sm:items-start'>
<div className='w-full'>

View file

@ -13,7 +13,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
<select
ref={ref}
className={clsx(
'w-full truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500',
'w-full truncate rounded-md border-gray-300 py-2 pl-3 pr-10 text-base focus:border-primary-500 focus:outline-none focus:ring-primary-500 disabled:opacity-50 black:bg-black sm:text-sm dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:border-primary-500 dark:focus:ring-primary-500',
className,
)}
{...filteredProps}

View file

@ -106,7 +106,7 @@ const Toast = (props: IToast) => {
data-testid='toast'
className={
clsx({
'p-4 pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white dark:bg-gray-900 shadow-lg dark:ring-2 dark:ring-gray-800': true,
'p-4 pointer-events-auto w-full max-w-sm overflow-hidden rounded-lg bg-white black:bg-black dark:bg-gray-900 shadow-lg dark:ring-2 dark:ring-gray-800': true,
'animate-enter': t.visible,
'animate-leave': !t.visible,
})

View file

@ -110,7 +110,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
return (
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6'>
<div>
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
</div>
<div className='px-4 sm:px-6'>
@ -620,7 +620,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
)}
<div>
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
<div className='relative isolate flex h-32 w-full flex-col justify-center overflow-hidden bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
{renderHeader()}
<div className='absolute left-2 top-2'>

View file

@ -11,7 +11,7 @@ import DropdownMenu from 'soapbox/components/dropdown-menu';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import StatusList from 'soapbox/components/status-list';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useTheme } from 'soapbox/hooks';
import toast from 'soapbox/toast';
const messages = defineMessages({
@ -39,6 +39,7 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const history = useHistory();
const theme = useTheme();
const folderId = params?.id;
@ -107,13 +108,14 @@ const Bookmarks: React.FC<IBookmarks> = ({ params }) => {
>
<PullToRefresh onRefresh={handleRefresh}>
<StatusList
className='black:p-4 black:sm:p-5'
statusIds={statusIds}
scrollKey='bookmarked_statuses'
hasMore={hasMore}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
onLoadMore={() => handleLoadMore(dispatch, folderId)}
emptyMessage={emptyMessage}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>

View file

@ -60,15 +60,15 @@ const ChatPage: React.FC<IChatPage> = ({ chatId }) => {
<div
ref={containerRef}
style={{ height }}
className='h-screen overflow-hidden bg-white text-gray-900 shadow-lg sm:rounded-t-xl dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'
className='h-screen overflow-hidden bg-white text-gray-900 shadow-lg black:bg-transparent sm:rounded-t-xl dark:bg-primary-900 dark:text-gray-100 dark:shadow-none'
>
{isOnboarded ? (
<div
className='grid h-full grid-cols-9 overflow-hidden dark:divide-x-2 dark:divide-solid dark:divide-gray-800'
className='grid h-full grid-cols-9 overflow-hidden black:divide-x dark:divide-x-2 dark:divide-solid dark:divide-gray-800'
data-testid='chat-page'
>
<Stack
className={clsx('dark:inset col-span-9 overflow-hidden bg-gradient-to-r from-white to-gray-100 sm:col-span-3 dark:bg-gray-900 dark:bg-none', {
className={clsx('dark:inset col-span-9 overflow-hidden bg-gradient-to-r from-white to-gray-100 black:bg-black sm:col-span-3 dark:bg-gray-900 dark:bg-none', {
'hidden sm:block': isSidebarHidden,
})}
>

View file

@ -12,7 +12,7 @@ interface IPane {
const Pane: React.FC<IPane> = ({ isOpen = false, children }) => {
return (
<div
className={clsx('fixed bottom-0 z-[99] flex w-96 flex-col rounded-t-lg bg-white shadow-3xl ltr:right-5 rtl:left-5 dark:bg-gray-900', {
className={clsx('fixed bottom-0 z-[99] flex w-96 flex-col rounded-t-lg bg-white shadow-3xl black:border black:border-b-0 black:border-gray-800 black:bg-black ltr:right-5 rtl:left-5 dark:bg-gray-900', {
'h-[550px] max-h-[100vh]': isOpen,
'h-16': !isOpen,
})}

View file

@ -5,7 +5,7 @@ import { expandCommunityTimeline } from 'soapbox/actions/timelines';
import { useCommunityStream } from 'soapbox/api/hooks';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useSettings, useTheme } from 'soapbox/hooks';
import Timeline from '../ui/components/timeline';
@ -16,6 +16,7 @@ const messages = defineMessages({
const CommunityTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const theme = useTheme();
const settings = useSettings();
const onlyMedia = settings.community.other.onlyMedia;
@ -41,12 +42,13 @@ const CommunityTimeline = () => {
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>

View file

@ -36,7 +36,7 @@ const ReplyIndicator: React.FC<IReplyIndicator> = ({ className, status, hideActi
}
return (
<Stack space={2} className={clsx('max-h-72 overflow-y-auto rounded-lg bg-gray-100 p-4 dark:bg-gray-800', className)}>
<Stack space={2} className={clsx('max-h-72 overflow-y-auto rounded-lg bg-gray-100 p-4 black:bg-gray-900 dark:bg-gray-800', className)}>
<AccountContainer
{...actions}
id={status.getIn(['account', 'id']) as string}

View file

@ -378,8 +378,8 @@ const AutosuggestPlugin = ({
key={key}
data-index={i}
className={clsx({
'snap-start snap-always px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group': true,
'snap-start snap-always bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800': i === selectedSuggestion,
'snap-start snap-always px-4 py-2.5 text-sm text-gray-700 dark:text-gray-500 hover:bg-gray-100 dark:hover:bg-gray-800 focus:bg-gray-100 dark:focus:bg-primary-800 group black:hover:bg-gray-900 black:focus:bg-gray-900': true,
'snap-start snap-always bg-gray-100 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-800 black:bg-gray-900 black:hover:bg-gray-900': i === selectedSuggestion,
})}
onMouseDown={handleSelectSuggestion}
>

View file

@ -24,7 +24,7 @@ const AvatarPicker = React.forwardRef<HTMLInputElement, IMediaInput>(({ classNam
<label
ref={picker}
className={clsx(
'absolute bottom-0 left-1/2 h-20 w-20 -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full ring-2',
'absolute bottom-0 left-1/2 h-20 w-20 -translate-x-1/2 translate-y-1/2 cursor-pointer rounded-full bg-primary-300 ring-2',
{
'border-2 border-primary-600 border-dashed !z-[99] overflow-hidden': isDragging,
'ring-white dark:ring-primary-900': !isDraggedOver,

View file

@ -80,7 +80,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
return (
<>
<div className='-mx-4 -mt-4'>
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
</div>
<PlaceholderEventHeader />
@ -363,13 +363,13 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
return (
<>
<div className='-mx-4 -mt-4'>
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50'>
{banner && (
<a href={banner.url} onClick={handleHeaderClick} target='_blank'>
<StillImage
src={banner.url}
alt={intl.formatMessage(messages.bannerHeader)}
className='absolute inset-0 h-full object-cover md:rounded-t-xl'
className='absolute inset-0 h-full object-cover black:rounded-t-none md:rounded-t-xl'
/>
</a>
)}

View file

@ -36,7 +36,7 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
return (
<div className='-mx-4 -mt-4 sm:-mx-6 sm:-mt-6' data-testid='group-header-missing'>
<div>
<div className='relative h-32 w-full bg-gray-200 md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
<div className='relative h-32 w-full bg-gray-200 black:rounded-t-none md:rounded-t-xl lg:h-48 dark:bg-gray-900/50' />
</div>
<div className='px-4 sm:px-6'>

View file

@ -7,7 +7,7 @@ import { useHashtagStream } from 'soapbox/api/hooks';
import List, { ListItem } from 'soapbox/components/list';
import { Column, Toggle } from 'soapbox/components/ui';
import Timeline from 'soapbox/features/ui/components/timeline';
import { useAppDispatch, useAppSelector, useFeatures, useLoggedIn } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useFeatures, useLoggedIn, useTheme } from 'soapbox/hooks';
interface IHashtagTimeline {
params?: {
@ -23,6 +23,7 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
const tag = useAppSelector((state) => state.tags.get(id));
const next = useAppSelector(state => state.timelines.get(`hashtag:${id}`)?.next);
const { isLoggedIn } = useLoggedIn();
const theme = useTheme();
const handleLoadMore = (maxId: string) => {
dispatch(expandHashtagTimeline(id, { url: next, maxId }));
@ -63,11 +64,12 @@ export const HashtagTimeline: React.FC<IHashtagTimeline> = ({ params }) => {
</List>
)}
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey='hashtag_timeline'
timelineId={`hashtag:${id}`}
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.hashtag' defaultMessage='There is nothing in this hashtag yet.' />}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</Column>
);

View file

@ -6,7 +6,7 @@ import { expandHomeTimeline } from 'soapbox/actions/timelines';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column, Stack, Text } from 'soapbox/components/ui';
import Timeline from 'soapbox/features/ui/components/timeline';
import { useAppSelector, useAppDispatch, useFeatures, useInstance } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useFeatures, useInstance, useTheme } from 'soapbox/hooks';
const messages = defineMessages({
title: { id: 'column.home', defaultMessage: 'Home' },
@ -17,6 +17,7 @@ const HomeTimeline: React.FC = () => {
const dispatch = useAppDispatch();
const features = useFeatures();
const instance = useInstance();
const theme = useTheme();
const polling = useRef<NodeJS.Timeout | null>(null);
@ -62,10 +63,11 @@ const HomeTimeline: React.FC = () => {
<Column label={intl.formatMessage(messages.title)} transparent withHeader={false}>
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey='home_timeline'
onLoadMore={handleLoadMore}
timelineId='home'
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
showAds
emptyMessage={
<Stack space={1}>

View file

@ -5,7 +5,7 @@ import { expandCommunityTimeline } from 'soapbox/actions/timelines';
import { useCommunityStream } from 'soapbox/api/hooks';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useInstance } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useInstance, useTheme } from 'soapbox/hooks';
import AboutPage from '../about';
import Timeline from '../ui/components/timeline';
@ -15,6 +15,7 @@ import { SiteBanner } from './components/site-banner';
const LandingTimeline = () => {
const dispatch = useAppDispatch();
const instance = useInstance();
const theme = useTheme();
const timelineEnabled = !instance.pleroma.metadata.restrict_unauthenticated.timelines.local;
const next = useAppSelector(state => state.timelines.get('community')?.next);
@ -50,12 +51,13 @@ const LandingTimeline = () => {
{timelineEnabled ? (
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey={`${timelineId}_timeline`}
timelineId={timelineId}
prefix='home'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.community' defaultMessage='The local timeline is empty. Write something publicly to get the ball rolling!' />}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</PullToRefresh>
) : (

View file

@ -8,13 +8,14 @@ import { expandListTimeline } from 'soapbox/actions/timelines';
import { useListStream } from 'soapbox/api/hooks';
import MissingIndicator from 'soapbox/components/missing-indicator';
import { Column, Button, Spinner } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useTheme } from 'soapbox/hooks';
import Timeline from '../ui/components/timeline';
const ListTimeline: React.FC = () => {
const dispatch = useAppDispatch();
const { id } = useParams<{ id: string }>();
const theme = useTheme();
const list = useAppSelector((state) => state.lists.get(id));
const next = useAppSelector(state => state.timelines.get(`list:${id}`)?.next);
@ -61,11 +62,12 @@ const ListTimeline: React.FC = () => {
return (
<Column label={title} transparent>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey='list_timeline'
timelineId={`list:${id}`}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</Column>
);

View file

@ -165,7 +165,7 @@ const Notifications = () => {
onScrollToTop={handleScrollToTop}
onScroll={handleScroll}
className={clsx({
'divide-y divide-gray-200 dark:divide-primary-800 divide-solid': notifications.size > 0,
'divide-y divide-gray-200 black:divide-gray-800 dark:divide-primary-800 divide-solid': notifications.size > 0,
'space-y-2': notifications.size === 0,
})}
>

View file

@ -9,7 +9,7 @@ const PlaceholderEventPreview = () => {
const nameLength = randomIntFromInterval(5, 15);
return (
<div className='relative w-full animate-pulse overflow-hidden rounded-lg bg-gray-100 text-primary-50 dark:bg-primary-800 dark:text-primary-800'>
<div className='relative w-full animate-pulse overflow-hidden rounded-lg bg-gray-100 text-primary-50 black:border black:border-gray-800 black:bg-black dark:bg-primary-800 dark:text-primary-800'>
<div className='h-40 bg-primary-200 dark:bg-gray-600' />
<Stack className='p-2.5' space={2}>
<Text weight='semibold'>{generateText(eventNameLength)}</Text>

View file

@ -9,7 +9,7 @@ const PlaceholderGroupCard = () => {
return (
<div className='animate-pulse'>
<Stack className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900'>
<Stack className='relative h-[240px] rounded-lg border border-solid border-gray-300 bg-white black:bg-white dark:border-primary-800 dark:bg-primary-900'>
{/* Group Cover Image */}
<div className='relative grow basis-1/2 rounded-t-lg bg-gray-300 dark:bg-gray-800' />

View file

@ -8,7 +8,7 @@ import PlaceholderStatusContent from './placeholder-status-content';
/** Fake notification to display while data is loading. */
const PlaceholderNotification = () => (
<div className='bg-white px-4 py-6 sm:p-6 dark:bg-primary-900'>
<div className='bg-white px-4 py-6 black:bg-black sm:p-6 dark:bg-primary-900'>
<div className='w-full animate-pulse'>
<div className='mb-2'>
<PlaceholderStatusContent minLength={20} maxLength={20} />

View file

@ -15,7 +15,7 @@ interface IPlaceholderStatus {
const PlaceholderStatus: React.FC<IPlaceholderStatus> = ({ variant }) => (
<div
className={clsx({
'status-placeholder bg-white dark:bg-primary-900': true,
'status-placeholder bg-white black:bg-black dark:bg-primary-900': true,
'shadow-xl dark:shadow-none sm:rounded-xl px-4 py-6 sm:p-5': variant === 'rounded',
'py-4': variant === 'slim',
})}

View file

@ -7,7 +7,7 @@ import { expandPublicTimeline } from 'soapbox/actions/timelines';
import { usePublicStream } from 'soapbox/api/hooks';
import PullToRefresh from 'soapbox/components/pull-to-refresh';
import { Accordion, Column } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useInstance, useSettings } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useInstance, useSettings, useTheme } from 'soapbox/hooks';
import PinnedHostsPicker from '../remote-timeline/components/pinned-hosts-picker';
import Timeline from '../ui/components/timeline';
@ -20,6 +20,7 @@ const messages = defineMessages({
const CommunityTimeline = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const theme = useTheme();
const instance = useInstance();
const settings = useSettings();
@ -57,7 +58,7 @@ const CommunityTimeline = () => {
<Column className='-mt-3 sm:mt-0' label={intl.formatMessage(messages.title)} transparent>
<PinnedHostsPicker />
{showExplanationBox && <div className='mb-4'>
{showExplanationBox && <div className='mb-4 black:mx-4'>
<Accordion
headline={<FormattedMessage id='fediverse_tab.explanation_box.title' defaultMessage='What is the Fediverse?' />}
action={dismissExplanationBox}
@ -86,12 +87,13 @@ const CommunityTimeline = () => {
</div>}
<PullToRefresh onRefresh={handleRefresh}>
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
prefix='home'
onLoadMore={handleLoadMore}
emptyMessage={<FormattedMessage id='empty_column.public' defaultMessage='There is nothing here! Write something publicly, or manually follow users from other servers to fill it up' />}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</PullToRefresh>
</Column>

View file

@ -7,7 +7,7 @@ import { useParams } from 'react-router-dom';
import { expandStatusQuotes, fetchStatusQuotes } from 'soapbox/actions/status-quotes';
import StatusList from 'soapbox/components/status-list';
import { Column } from 'soapbox/components/ui';
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
import { useAppDispatch, useAppSelector, useTheme } from 'soapbox/hooks';
const messages = defineMessages({
heading: { id: 'column.quotes', defaultMessage: 'Post quotes' },
@ -20,6 +20,7 @@ const Quotes: React.FC = () => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { statusId } = useParams<{ statusId: string }>();
const theme = useTheme();
const statusIds = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'items'], ImmutableOrderedSet<string>()));
const isLoading = useAppSelector((state) => state.status_lists.getIn([`quotes:${statusId}`, 'isLoading'], true));
@ -38,6 +39,7 @@ const Quotes: React.FC = () => {
return (
<Column label={intl.formatMessage(messages.heading)} transparent>
<StatusList
className='black:p-4 black:sm:p-5'
statusIds={statusIds as ImmutableOrderedSet<string>}
scrollKey={`quotes:${statusId}`}
hasMore={hasMore}
@ -45,7 +47,7 @@ const Quotes: React.FC = () => {
onLoadMore={() => handleLoadMore(statusId, dispatch)}
onRefresh={handleRefresh}
emptyMessage={emptyMessage}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</Column>
);

View file

@ -6,7 +6,7 @@ import { expandRemoteTimeline } from 'soapbox/actions/timelines';
import { useRemoteStream } from 'soapbox/api/hooks';
import IconButton from 'soapbox/components/icon-button';
import { Column, HStack, Text } from 'soapbox/components/ui';
import { useAppSelector, useAppDispatch, useSettings } from 'soapbox/hooks';
import { useAppSelector, useAppDispatch, useSettings, useTheme } from 'soapbox/hooks';
import Timeline from '../ui/components/timeline';
@ -22,6 +22,7 @@ interface IRemoteTimeline {
const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
const history = useHistory();
const dispatch = useAppDispatch();
const theme = useTheme();
const instance = params?.instance as string;
const settings = useSettings();
@ -64,6 +65,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
)}
<Timeline
className='black:p-4 black:sm:p-5'
scrollKey={`${timelineId}_${instance}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}:${instance}`}
onLoadMore={handleLoadMore}
@ -74,7 +76,7 @@ const RemoteTimeline: React.FC<IRemoteTimeline> = ({ params }) => {
values={{ instance }}
/>
}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</Column>
);

View file

@ -22,7 +22,7 @@ const SitePreview: React.FC<ISitePreview> = ({ soapbox }) => {
const userTheme = settings.get('themeMode');
const systemTheme = useSystemTheme();
const dark = userTheme === 'dark' || (userTheme === 'system' && systemTheme === 'dark');
const dark = ['dark', 'black'].includes(userTheme as string) || (userTheme === 'system' && systemTheme === 'dark');
const bodyClass = clsx(
'site-preview',

View file

@ -3,7 +3,7 @@ import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
import { importFetchedStatuses } from 'soapbox/actions/importer';
import { expandTimelineSuccess } from 'soapbox/actions/timelines';
import { useAppDispatch } from 'soapbox/hooks';
import { useAppDispatch, useTheme } from 'soapbox/hooks';
import { Column } from '../../components/ui';
import Timeline from '../ui/components/timeline';
@ -31,6 +31,7 @@ const onlyMedia = false;
const TestTimeline: React.FC = () => {
const intl = useIntl();
const dispatch = useAppDispatch();
const theme = useTheme();
React.useEffect(() => {
dispatch(importFetchedStatuses(MOCK_STATUSES));
@ -43,7 +44,7 @@ const TestTimeline: React.FC = () => {
scrollKey={`${timelineId}_timeline`}
timelineId={`${timelineId}${onlyMedia ? ':media' : ''}`}
emptyMessage={<FormattedMessage id='empty_column.test' defaultMessage='The test timeline is empty.' />}
divideType='space'
divideType={theme === 'black' ? 'border' : 'space'}
/>
</Column>
);

View file

@ -8,7 +8,7 @@ interface IBackgroundShapes {
/** Gradient that appears in the background of the UI. */
const BackgroundShapes: React.FC<IBackgroundShapes> = ({ position = 'fixed' }) => (
<div className={clsx(position, 'pointer-events-none inset-x-0 top-0 flex justify-center overflow-hidden')}>
<div className={clsx(position, 'pointer-events-none inset-x-0 top-0 flex justify-center overflow-hidden black:hidden')}>
<div className='bg-gradient-sm lg:bg-gradient-light lg:dark:bg-gradient-dark h-screen w-screen' />
</div>
);

View file

@ -70,7 +70,7 @@ const Navbar = () => {
if (mfaToken) return <Redirect to={`/login?token=${encodeURIComponent(mfaToken)}`} />;
return (
<nav className='sticky top-0 z-50 bg-white shadow dark:bg-primary-900' ref={node} data-testid='navbar'>
<nav className='sticky top-0 z-50 bg-white shadow black:border-b black:border-b-gray-800 black:bg-black dark:bg-primary-900' ref={node} data-testid='navbar'>
<div className='mx-auto max-w-7xl px-2 sm:px-6 lg:px-8'>
<div className='relative flex h-12 justify-between lg:h-16'>
{account && (

View file

@ -124,7 +124,7 @@ const ProfileDropdown: React.FC<IProfileDropdown> = ({ account, children }) => {
{visible && (
<div
ref={refs.setFloating}
className='z-[1003] mt-2 max-w-xs rounded-md bg-white shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
className='z-[1003] mt-2 max-w-xs rounded-md bg-white shadow-lg focus:outline-none black:bg-black dark:bg-gray-900 dark:ring-2 dark:ring-primary-700'
style={{
position: strategy,
top: y ?? 0,

View file

@ -6,6 +6,7 @@ import { Icon, Select } from 'soapbox/components/ui';
const messages = defineMessages({
light: { id: 'theme_toggle.light', defaultMessage: 'Light' },
dark: { id: 'theme_toggle.dark', defaultMessage: 'Dark' },
black: { id: 'theme_toggle.black', defaultMessage: 'Black' },
system: { id: 'theme_toggle.system', defaultMessage: 'System' },
});
@ -26,6 +27,8 @@ const ThemeSelector: React.FC<IThemeSelector> = ({ value, onChange }) => {
return require('@tabler/icons/sun.svg');
case 'dark':
return require('@tabler/icons/moon.svg');
case 'black':
return require('@tabler/icons/shadow.svg');
default:
return null;
}
@ -50,6 +53,7 @@ const ThemeSelector: React.FC<IThemeSelector> = ({ value, onChange }) => {
<option value='system'>{intl.formatMessage(messages.system)}</option>
<option value='light'>{intl.formatMessage(messages.light)}</option>
<option value='dark'>{intl.formatMessage(messages.dark)}</option>
<option value='black'>{intl.formatMessage(messages.black)}</option>
</Select>
<div className='pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3'>

View file

@ -483,7 +483,7 @@ const UI: React.FC<IUI> = ({ children }) => {
<BackgroundShapes />
<div className='z-10 flex flex-col'>
<div className='z-10 flex min-h-screen flex-col'>
<Navbar />
<Layout>

View file

@ -1,7 +1,7 @@
import { useSettings } from './useSettings';
import { useSystemTheme } from './useSystemTheme';
type Theme = 'light' | 'dark';
type Theme = 'light' | 'dark' | 'black';
/**
* Returns the actual theme being displayed (eg "light" or "dark")
@ -11,8 +11,7 @@ const useTheme = (): Theme => {
const { themeMode } = useSettings();
const systemTheme = useSystemTheme();
const darkMode = themeMode === 'dark' || (themeMode === 'system' && systemTheme === 'dark');
return darkMode ? 'dark' : 'light';
return themeMode === 'system' ? systemTheme : themeMode;
};
export { useTheme };

View file

@ -22,12 +22,12 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
const { locale, direction } = useLocale();
const { demo, reduceMotion, underlineLinks, demetricator } = useSettings();
const soapboxConfig = useSoapboxConfig();
const theme = useTheme();
const darkMode = useTheme() === 'dark';
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
const dsn = soapboxConfig.sentryDsn;
const bodyClass = clsx('h-full bg-white text-base dark:bg-gray-800', {
const bodyClass = clsx('h-full bg-white text-base black:bg-black dark:bg-gray-800', {
'no-reduce-motion': !reduceMotion,
'underline-links': underlineLinks,
'demetricator': demetricator,
@ -42,10 +42,10 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
return (
<>
<Helmet>
<html lang={locale} className={clsx('h-full', { dark: darkMode })} />
<html lang={locale} className={clsx('h-full', { 'dark': theme === 'dark', 'dark black': theme === 'black' })} />
<body className={bodyClass} dir={direction} />
{themeCss && <style id='theme' type='text/css'>{`:root{${themeCss}}`}</style>}
{darkMode && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
{['dark', 'black'].includes(theme) && <style type='text/css'>{':root { color-scheme: dark; }'}</style>}
<meta name='theme-color' content={soapboxConfig.brandColor} />
</Helmet>

View file

@ -1518,6 +1518,7 @@
"theme_editor.restore": "Restore default theme",
"theme_editor.save": "Save theme",
"theme_editor.saved": "Theme updated!",
"theme_toggle.black": "Black",
"theme_toggle.dark": "Dark",
"theme_toggle.light": "Light",
"theme_toggle.system": "System",

View file

@ -7,7 +7,7 @@ interface IChatsPage {
/** Custom layout for chats on desktop. */
const ChatsPage: React.FC<IChatsPage> = ({ children }) => {
return (
<div className='md:col-span-12 lg:col-span-9'>
<div className='black:border-gray-800 md:col-span-12 lg:col-span-9 lg:black:border-l'>
{children}
</div>
);

View file

@ -50,10 +50,10 @@ const HomePage: React.FC<IHomePage> = ({ children }) => {
return (
<>
<Layout.Main className='space-y-3 pt-3 sm:pt-0 dark:divide-gray-800'>
<Layout.Main className='space-y-3 pt-3 black:space-y-0 sm:pt-0 dark:divide-gray-800'>
{me && (
<Card
className={clsx('relative z-[1] transition', {
className={clsx('relative z-[1] transition black:border-b black:border-gray-800', {
'border-2 border-primary-600 border-dashed z-[99]': isDragging,
'ring-2 ring-offset-2 ring-primary-600': isDraggedOver,
})}

View file

@ -23,7 +23,7 @@ const settingsSchema = z.object({
missingDescriptionModal: z.boolean().catch(false),
defaultPrivacy: z.enum(['public', 'unlisted', 'private', 'direct']).catch('public'),
defaultContentType: z.enum(['text/plain', 'text/markdown']).catch('text/plain'),
themeMode: z.enum(['system', 'light', 'dark']).catch('system'),
themeMode: z.enum(['system', 'light', 'dark', 'black']).catch('system'),
locale: z.string().catch(navigator.language).pipe(z.enum(locales)).catch('en'),
showExplanationBox: z.boolean().catch(true),
explanationBox: z.boolean().catch(true),

View file

@ -128,7 +128,7 @@
}
&__dropdown {
@apply absolute bg-white dark:bg-gray-900 z-[1000] rounded-md shadow-lg ml-10 text-sm overflow-hidden;
@apply absolute bg-white dark:bg-gray-900 z-[1000] rounded-md shadow-lg ml-10 text-sm overflow-hidden black:bg-black black:border black:border-gray-800;
&.top {
transform-origin: 50% 100%;
@ -140,10 +140,10 @@
}
&__option {
@apply flex p-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer;
@apply flex p-2.5 text-sm text-gray-700 dark:text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-800 cursor-pointer black:hover:bg-gray-900;
&.active {
@apply bg-gray-100 dark:bg-gray-800;
@apply bg-gray-100 dark:bg-gray-800 black:bg-gray-900;
}
&:hover,

View file

@ -1,9 +1,9 @@
.react-datepicker {
@apply dark:bg-gray-900 dark:border-gray-700 p-4 font-sans text-xs text-gray-900 dark:text-gray-300 border border-solid border-gray-200 rounded-lg;
@apply black:bg-black dark:bg-gray-900 dark:border-gray-700 p-4 font-sans text-xs text-gray-900 dark:text-gray-300 border border-solid border-gray-200 rounded-lg;
}
.react-datepicker__input-container > input {
@apply dark:bg-gray-900 dark:text-gray-100 block w-full sm:text-sm border-gray-400 dark:border-gray-800 rounded-md focus:ring-primary-500 focus:border-primary-500;
@apply black:bg-black dark:bg-gray-900 dark:text-gray-100 block w-full sm:text-sm border-gray-400 dark:border-gray-800 rounded-md focus:ring-primary-500 focus:border-primary-500;
&.has-error {
@apply text-red-600 border-red-600;
@ -12,7 +12,7 @@
.react-datepicker-popper[data-placement^='bottom'] .react-datepicker__triangle::before,
.react-datepicker-popper[data-placement^='bottom'] .react-datepicker__triangle::after {
@apply border-b-white dark:border-b-gray-900;
@apply border-b-white dark:border-b-gray-900 black:border-b-black;
}
.react-datepicker-popper[data-placement^='bottom'] .react-datepicker__triangle::before {
@ -24,7 +24,7 @@
}
.react-datepicker__header {
@apply bg-white dark:bg-gray-900 border-b-0 py-1 px-0;
@apply bg-white black:bg-black dark:bg-gray-900 border-b-0 py-1 px-0;
}
.react-datepicker__current-month,
@ -85,11 +85,13 @@
}
.react-datepicker__time {
@apply dark:bg-gray-900;
&-container & {
@apply dark:bg-gray-900 black:bg-black;
}
}
.react-datepicker__time-container {
@apply dark:border-gray-700;
@apply dark:border-gray-700 black:border-gray-800;
}
.react-datepicker__day-name,

View file

@ -1,5 +1,5 @@
.thread {
@apply bg-white dark:bg-primary-900;
@apply bg-white black:bg-black dark:bg-primary-900;
&__status {
@apply relative pb-4;

View file

@ -78,7 +78,7 @@
}
.actions-modal {
@apply flex-col relative text-gray-400 overflow-hidden w-full max-w-lg m-auto bg-white dark:bg-gray-900 shadow-xl rounded-2xl;
@apply flex-col relative text-gray-400 overflow-hidden w-full max-w-lg m-auto bg-white black:bg-black dark:bg-gray-900 shadow-xl rounded-2xl;
max-height: calc(100vh - 3rem);
&__item-label {
@ -86,7 +86,7 @@
}
.dropdown-menu__separator {
@apply block m-2 h-[1px] bg-gray-200 dark:bg-gray-600;
@apply block m-2 h-[1px] bg-gray-200 dark:bg-gray-600 black:bg-gray-800;
}
&__status {

View file

@ -8,3 +8,7 @@ em-emoji-picker {
.dark em-emoji-picker {
--rgb-background: var(--color-primary-900);
}
.black em-emoji-picker {
--rgb-background: var(--color-gray-900);
}

View file

@ -1,5 +1,5 @@
.thumb-navigation {
@apply fixed lg:hidden bottom-0 bg-white/90 dark:bg-primary-900/90 backdrop-blur-md border-t border-solid border-gray-200 dark:border-gray-800 left-0 right-0 shadow-2xl w-full flex z-50;
@apply fixed lg:hidden bottom-0 bg-white/90 black:bg-black/90 dark:bg-primary-900/90 backdrop-blur-md border-t border-solid border-gray-200 dark:border-gray-800 left-0 right-0 shadow-2xl w-full flex z-50;
padding-bottom: env(safe-area-inset-bottom); /* iOS PWA */
overflow-x: auto;
scrollbar-width: thin;

View file

@ -2,9 +2,12 @@ import aspectRatioPlugin from '@tailwindcss/aspect-ratio';
import formsPlugin from '@tailwindcss/forms';
import typographyPlugin from '@tailwindcss/typography';
import { type Config } from 'tailwindcss';
import plugin from 'tailwindcss/plugin';
import { parseColorMatrix } from './tailwind/colors';
const blackVariantPlugin = plugin(({ addVariant }) => addVariant('black', '.black &'));
const config: Config = {
content: ['./src/**/*.{html,js,ts,tsx}', './custom/instance/**/*.html', './index.html'],
darkMode: 'class',
@ -105,6 +108,7 @@ const config: Config = {
aspectRatioPlugin,
formsPlugin,
typographyPlugin,
blackVariantPlugin,
],
};