sforkowany z mirror/soapbox
Merge remote-tracking branch 'soapbox/develop' into instancev2
commit
200b863e0e
|
@ -5,6 +5,7 @@ module.exports = {
|
|||
'eslint:recommended',
|
||||
'plugin:import/typescript',
|
||||
'plugin:compat/recommended',
|
||||
'plugin:tailwindcss/recommended',
|
||||
],
|
||||
|
||||
env: {
|
||||
|
@ -61,6 +62,9 @@ module.exports = {
|
|||
'URL', // core-js
|
||||
'URLSearchParams', // core-js
|
||||
],
|
||||
tailwindcss: {
|
||||
config: 'tailwind.config.cjs',
|
||||
},
|
||||
},
|
||||
|
||||
rules: {
|
||||
|
@ -235,18 +239,7 @@ module.exports = {
|
|||
},
|
||||
],
|
||||
'import/newline-after-import': 'error',
|
||||
'import/no-extraneous-dependencies': [
|
||||
'error',
|
||||
// {
|
||||
// devDependencies: [
|
||||
// 'webpack/**',
|
||||
// 'app/soapbox/test_setup.js',
|
||||
// 'app/soapbox/test_helpers.js',
|
||||
// 'app/**/__tests__/**',
|
||||
// 'app/**/__mocks__/**',
|
||||
// ],
|
||||
// },
|
||||
],
|
||||
'import/no-extraneous-dependencies': 'error',
|
||||
'import/no-unresolved': 'error',
|
||||
'import/no-webpack-loader-syntax': 'error',
|
||||
'import/order': [
|
||||
|
@ -271,6 +264,9 @@ module.exports = {
|
|||
'promise/catch-or-return': 'error',
|
||||
|
||||
'react-hooks/rules-of-hooks': 'error',
|
||||
|
||||
'tailwindcss/classnames-order': 'error',
|
||||
'tailwindcss/migration-from-tailwind-2': 'error',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
|
|
|
@ -149,9 +149,9 @@ pages:
|
|||
|
||||
docker:
|
||||
stage: deploy
|
||||
image: docker:20.10.23
|
||||
image: docker:23.0.0
|
||||
services:
|
||||
- docker:20.10.23-dind
|
||||
- docker:23.0.0-dind
|
||||
tags:
|
||||
- dind
|
||||
# https://medium.com/devops-with-valentine/how-to-build-a-docker-image-and-push-it-to-the-gitlab-container-registry-from-a-gitlab-ci-pipeline-acac0d1f26df
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
import sharedConfig from '../webpack/shared';
|
||||
|
||||
import type { StorybookConfig } from '@storybook/core-common';
|
||||
|
||||
const config: StorybookConfig = {
|
||||
stories: [
|
||||
'../stories/**/*.stories.mdx',
|
||||
'../stories/**/*.stories.@(js|jsx|ts|tsx)'
|
||||
],
|
||||
addons: [
|
||||
'@storybook/addon-links',
|
||||
'@storybook/addon-essentials',
|
||||
'@storybook/addon-interactions',
|
||||
'storybook-react-intl',
|
||||
{
|
||||
name: '@storybook/addon-postcss',
|
||||
options: {
|
||||
postcssLoaderOptions: {
|
||||
implementation: require('postcss'),
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
framework: '@storybook/react',
|
||||
core: {
|
||||
builder: '@storybook/builder-webpack5',
|
||||
},
|
||||
webpackFinal: async (config) => {
|
||||
config.resolve!.alias = {
|
||||
...sharedConfig.resolve!.alias,
|
||||
...config.resolve!.alias,
|
||||
};
|
||||
|
||||
config.resolve!.modules = [
|
||||
...sharedConfig.resolve!.modules!,
|
||||
...config.resolve!.modules!,
|
||||
];
|
||||
|
||||
return config;
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
|
@ -0,0 +1,22 @@
|
|||
import '../app/styles/tailwind.css';
|
||||
import '../stories/theme.css';
|
||||
|
||||
import { addDecorator, Story } from '@storybook/react';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
import React from 'react';
|
||||
|
||||
const withProvider = (Story: Story) => (
|
||||
<IntlProvider locale='en'><Story /></IntlProvider>
|
||||
);
|
||||
|
||||
addDecorator(withProvider);
|
||||
|
||||
export const parameters = {
|
||||
actions: { argTypesRegex: '^on[A-Z].*' },
|
||||
controls: {
|
||||
matchers: {
|
||||
color: /(background|color)$/i,
|
||||
date: /Date$/,
|
||||
},
|
||||
},
|
||||
};
|
|
@ -1 +1 @@
|
|||
nodejs 18.13.0
|
||||
nodejs 18.14.0
|
||||
|
|
|
@ -13,11 +13,19 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
- Compatibility: improved browser support for older browsers.
|
||||
- Events: allow to repost events in event menu.
|
||||
- Groups: Initial support for groups.
|
||||
- Profile: Add RSS link to user profiles.
|
||||
- Reactions: adds support for reacting to chat messages.
|
||||
- Groups: initial support for groups.
|
||||
- Profile: add RSS link to user profiles.
|
||||
- Posts: fix posts filtering.
|
||||
|
||||
### Changed
|
||||
- Chats: improved display of media attachments.
|
||||
- ServiceWorker: switch to a network-first strategy. The "An update is available!" prompt goes away.
|
||||
- Posts: increased font size of focused status in threads.
|
||||
- Posts: let "mute conversation" be clicked from any feed, not just noficiations.
|
||||
- Posts: display all emoji reactions.
|
||||
- Reactions: improved UI of reactions on statuses.
|
||||
|
||||
### Fixed
|
||||
- Chats: media attachments rendering at the wrong size and/or causing the chat to scroll on load.
|
||||
|
|
|
@ -32,8 +32,8 @@ const getSoapboxConfig = createSelector([
|
|||
}
|
||||
|
||||
// If RGI reacts aren't supported, strip VS16s
|
||||
// // https://git.pleroma.social/pleroma/pleroma/-/issues/2355
|
||||
if (!features.emojiReactsRGI) {
|
||||
// https://git.pleroma.social/pleroma/pleroma/-/issues/2355
|
||||
if (features.emojiReactsNonRGI) {
|
||||
soapboxConfig.set('allowedEmoji', soapboxConfig.allowedEmoji.map(removeVS16s));
|
||||
}
|
||||
});
|
||||
|
|
|
@ -2,7 +2,7 @@ import { getSettings } from 'soapbox/actions/settings';
|
|||
import messages from 'soapbox/locales/messages';
|
||||
import { ChatKeys, IChat, isLastMessage } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { getUnreadChatsCount, updateChatListItem } from 'soapbox/utils/chats';
|
||||
import { getUnreadChatsCount, updateChatListItem, updateChatMessage } from 'soapbox/utils/chats';
|
||||
import { removePageItem } from 'soapbox/utils/queries';
|
||||
import { play, soundCache } from 'soapbox/utils/sounds';
|
||||
|
||||
|
@ -170,6 +170,9 @@ const connectTimelineStream = (
|
|||
}
|
||||
});
|
||||
break;
|
||||
case 'chat_message.reaction': // TruthSocial
|
||||
updateChatMessage(JSON.parse(data.payload));
|
||||
break;
|
||||
case 'pleroma:follow_relationships_update':
|
||||
dispatch(updateFollowRelationships(JSON.parse(data.payload)));
|
||||
break;
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
import { render, screen } from '../../jest/test-helpers';
|
||||
import EmojiSelector from '../emoji-selector';
|
||||
|
||||
describe('<EmojiSelector />', () => {
|
||||
it('renders correctly', () => {
|
||||
const children = <EmojiSelector />;
|
||||
// @ts-ignore
|
||||
children.__proto__.addEventListener = () => {};
|
||||
|
||||
render(children);
|
||||
|
||||
expect(screen.queryAllByRole('button')).toHaveLength(6);
|
||||
});
|
||||
});
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -72,17 +72,17 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
|
|||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
|
||||
className='absolute inset-y-0 right-0 flex cursor-pointer items-center px-3'
|
||||
onClick={handleClear}
|
||||
>
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/search.svg')}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||
className={clsx('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||
/>
|
||||
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/x.svg')}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||
className={clsx('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -43,11 +43,11 @@ const InstanceFavicon: React.FC<IInstanceFavicon> = ({ account, disabled }) => {
|
|||
|
||||
return (
|
||||
<button
|
||||
className='w-4 h-4 flex-none focus:ring-primary-500 focus:ring-2 focus:ring-offset-2'
|
||||
className='h-4 w-4 flex-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||
onClick={handleClick}
|
||||
disabled={disabled}
|
||||
>
|
||||
<img src={account.favicon} alt='' title={account.domain} className='w-full max-h-full' />
|
||||
<img src={account.favicon} alt='' title={account.domain} className='max-h-full w-full' />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
@ -147,7 +147,7 @@ const Account = ({
|
|||
src={actionIcon}
|
||||
title={actionTitle}
|
||||
onClick={handleAction}
|
||||
className='bg-transparent text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
|
||||
className='bg-transparent text-gray-600 hover:text-gray-700 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
iconClassName='w-4 h-4'
|
||||
/>
|
||||
);
|
||||
|
@ -193,7 +193,7 @@ const Account = ({
|
|||
const LinkEl: any = withLinkToProfile ? Link : 'div';
|
||||
|
||||
return (
|
||||
<div data-testid='account' className='flex-shrink-0 group block w-full' ref={overflowRef}>
|
||||
<div data-testid='account' className='group block w-full shrink-0' ref={overflowRef}>
|
||||
<HStack alignItems={actionAlignment} justifyContent='between'>
|
||||
<HStack alignItems={withAccountNote || note ? 'top' : 'center'} space={3}>
|
||||
<ProfilePopper
|
||||
|
@ -208,14 +208,14 @@ const Account = ({
|
|||
<Avatar src={account.avatar} size={avatarSize} />
|
||||
{emoji && (
|
||||
<Emoji
|
||||
className='w-5 h-5 absolute -bottom-1.5 -right-1.5'
|
||||
className='absolute -bottom-1.5 -right-1.5 h-5 w-5'
|
||||
emoji={emoji}
|
||||
/>
|
||||
)}
|
||||
</LinkEl>
|
||||
</ProfilePopper>
|
||||
|
||||
<div className='flex-grow'>
|
||||
<div className='grow'>
|
||||
<ProfilePopper
|
||||
condition={showProfileHoverCard}
|
||||
wrapper={(children) => <HoverRefWrapper accountId={account.id} inline>{children}</HoverRefWrapper>}
|
||||
|
|
|
@ -50,7 +50,7 @@ const AnimatedNumber: React.FC<IAnimatedNumber> = ({ value, obfuscate }) => {
|
|||
return (
|
||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||
{items => (
|
||||
<span className='inline-flex flex-col items-stretch relative overflow-hidden'>
|
||||
<span className='relative inline-flex flex-col items-stretch overflow-hidden'>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<span key={key} style={{ position: (direction * style.y) > 0 ? 'absolute' : 'static', transform: `translateY(${style.y * 100}%)` }}>{obfuscate ? obfuscatedCount(data) : <FormattedNumber value={data} />}</span>
|
||||
))}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { List as ImmutableList, Map as ImmutableMap } from 'immutable';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -52,7 +52,7 @@ const AnnouncementsPanel = () => {
|
|||
key={i}
|
||||
tabIndex={0}
|
||||
onClick={() => setIndex(i)}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'w-2 h-2 rounded-full focus:ring-primary-600 focus:ring-2 focus:ring-offset-2': true,
|
||||
'bg-gray-200 hover:bg-gray-300': i !== index,
|
||||
'bg-primary-600': i === index,
|
||||
|
|
|
@ -24,7 +24,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
|||
return (
|
||||
<img
|
||||
draggable='false'
|
||||
className='emojione block m-0'
|
||||
className='emojione m-0 block'
|
||||
alt={emoji}
|
||||
title={title}
|
||||
src={joinPublicPath(`packs/emoji/${filename}.svg`)}
|
||||
|
@ -37,7 +37,7 @@ const Emoji: React.FC<IEmoji> = ({ emoji, emojiMap, hovered }) => {
|
|||
return (
|
||||
<img
|
||||
draggable='false'
|
||||
className='emojione block m-0'
|
||||
className='emojione m-0 block'
|
||||
alt={shortCode}
|
||||
title={shortCode}
|
||||
src={filename as string}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import AnimatedNumber from 'soapbox/components/animated-number';
|
||||
|
@ -43,7 +43,7 @@ const Reaction: React.FC<IReaction> = ({ announcementId, reaction, addReaction,
|
|||
|
||||
return (
|
||||
<button
|
||||
className={classNames('flex shrink-0 items-center gap-1.5 bg-gray-100 dark:bg-primary-900 rounded-sm px-1.5 py-1 transition-colors', {
|
||||
className={clsx('flex shrink-0 items-center gap-1.5 rounded-sm bg-gray-100 px-1.5 py-1 transition-colors dark:bg-primary-900', {
|
||||
'bg-gray-200 dark:bg-primary-800': hovered,
|
||||
'bg-primary-200 dark:bg-primary-500': reaction.me,
|
||||
})}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { TransitionMotion, spring } from 'react-motion';
|
||||
|
||||
|
@ -42,7 +42,7 @@ const ReactionsBar: React.FC<IReactionsBar> = ({ announcementId, reactions, addR
|
|||
return (
|
||||
<TransitionMotion styles={styles} willEnter={willEnter} willLeave={willLeave}>
|
||||
{items => (
|
||||
<div className={classNames('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||
<div className={clsx('flex flex-wrap items-center gap-1', { 'reactions-bar--empty': visibleReactions.isEmpty() })}>
|
||||
{items.map(({ key, data, style }) => (
|
||||
<Reaction
|
||||
key={key}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Portal } from '@reach/portal';
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
|
@ -199,7 +199,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
tabIndex={0}
|
||||
key={key}
|
||||
data-index={i}
|
||||
className={classNames({
|
||||
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,
|
||||
})}
|
||||
|
@ -235,7 +235,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
|
||||
return menu.map((item, i) => (
|
||||
<a
|
||||
className={classNames('flex items-center space-x-2 px-4 py-2.5 text-sm cursor-pointer 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', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
className={clsx('flex cursor-pointer items-center space-x-2 px-4 py-2.5 text-sm text-gray-700 hover:bg-gray-100 focus:bg-gray-100 dark:text-gray-500 dark:hover:bg-gray-800 dark:focus:bg-primary-800', { selected: suggestions.size - selectedSuggestion === i })}
|
||||
href='#'
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
|
@ -302,7 +302,7 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
<Portal key='portal'>
|
||||
<div
|
||||
style={this.setPortalPosition()}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'fixed w-full z-[1001] shadow bg-white dark:bg-gray-900 rounded-lg py-1 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||
hidden: !visible,
|
||||
block: visible,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Portal } from '@reach/portal';
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
|
@ -201,7 +201,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
tabIndex={0}
|
||||
key={key}
|
||||
data-index={i}
|
||||
className={classNames({
|
||||
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,
|
||||
})}
|
||||
|
@ -244,7 +244,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
|
||||
<Textarea
|
||||
ref={this.setTextarea}
|
||||
className={classNames('transition-[min-height] motion-reduce:transition-none dark:bg-transparent px-0 border-0 text-gray-800 dark:text-white placeholder:text-gray-600 dark:placeholder:text-gray-600 resize-none w-full focus:shadow-none focus:border-0 focus:ring-0', {
|
||||
className={clsx('w-full resize-none border-0 px-0 text-gray-800 transition-[min-height] placeholder:text-gray-600 focus:border-0 focus:shadow-none focus:ring-0 motion-reduce:transition-none dark:bg-transparent dark:text-white dark:placeholder:text-gray-600', {
|
||||
'min-h-[40px]': condensed,
|
||||
'min-h-[100px]': !condensed,
|
||||
})}
|
||||
|
@ -271,7 +271,7 @@ class AutosuggestTextarea extends ImmutablePureComponent<IAutosuggesteTextarea>
|
|||
<Portal key='portal'>
|
||||
<div
|
||||
style={this.setPortalPosition()}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'fixed z-1000 shadow bg-white dark:bg-gray-900 rounded-lg py-1 space-y-0 dark:ring-2 dark:ring-primary-700 focus:outline-none': true,
|
||||
hidden: suggestionsHidden || suggestions.isEmpty(),
|
||||
block: !suggestionsHidden && !suggestions.isEmpty(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IBadge {
|
||||
|
@ -12,13 +12,13 @@ const Badge: React.FC<IBadge> = ({ title, slug }) => {
|
|||
return (
|
||||
<span
|
||||
data-testid='badge'
|
||||
className={classNames('inline-flex items-center px-2 py-0.5 rounded text-xs font-medium', {
|
||||
className={clsx('inline-flex items-center rounded px-2 py-0.5 text-xs font-medium', {
|
||||
'bg-fuchsia-700 text-white': slug === 'patron',
|
||||
'bg-emerald-800 text-white': slug === 'badge:donor',
|
||||
'bg-black text-white': slug === 'admin',
|
||||
'bg-cyan-600 text-white': slug === 'moderator',
|
||||
'bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100': fallback,
|
||||
'bg-white bg-opacity-75 text-gray-900': slug === 'opaque',
|
||||
'bg-white/75 text-gray-900': slug === 'opaque',
|
||||
})}
|
||||
>
|
||||
{title}
|
||||
|
|
|
@ -113,7 +113,7 @@ const BirthdayInput: React.FC<IBirthdayInput> = ({ value, onChange, required })
|
|||
const handleChange = (date: Date) => onChange(date ? new Date(date.getTime() - (date.getTimezoneOffset() * 60000)).toISOString().slice(0, 10) : '');
|
||||
|
||||
return (
|
||||
<div className='mt-1 relative rounded-md shadow-sm'>
|
||||
<div className='relative mt-1 rounded-md shadow-sm'>
|
||||
<BundleContainer fetchComponent={DatePicker}>
|
||||
{Component => (<Component
|
||||
selected={selected}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { supportsPassiveEvents } from 'detect-passive-events';
|
||||
import React from 'react';
|
||||
import { spring } from 'react-motion';
|
||||
|
@ -182,7 +182,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
const { text, href, to, newTab, isLogout, icon, count, destructive } = option;
|
||||
|
||||
return (
|
||||
<li className={classNames('dropdown-menu__item truncate', { destructive })} key={`${text}-${i}`}>
|
||||
<li className={clsx('dropdown-menu__item truncate', { destructive })} key={`${text}-${i}`}>
|
||||
<a
|
||||
href={href || to || '#'}
|
||||
role='button'
|
||||
|
@ -196,7 +196,7 @@ class DropdownMenu extends React.PureComponent<IDropdownMenu, IDropdownMenuState
|
|||
data-method={isLogout ? 'delete' : undefined}
|
||||
title={text}
|
||||
>
|
||||
{icon && <SvgIcon src={icon} className='mr-3 rtl:ml-3 rtl:mr-0 h-5 w-5 flex-none' />}
|
||||
{icon && <SvgIcon src={icon} className='mr-3 h-5 w-5 flex-none rtl:ml-3 rtl:mr-0' />}
|
||||
|
||||
<span className='truncate'>{text}</span>
|
||||
|
||||
|
@ -392,7 +392,7 @@ class Dropdown extends React.PureComponent<IDropdown, IDropdownState> {
|
|||
) : (
|
||||
<IconButton
|
||||
disabled={disabled}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'text-gray-600 hover:text-gray-700 dark:hover:text-gray-500': true,
|
||||
'text-gray-700 dark:text-gray-500': open,
|
||||
})}
|
||||
|
|
|
@ -1,142 +0,0 @@
|
|||
// import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import ImmutablePureComponent from 'react-immutable-pure-component';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||
import { EmojiSelector as RealEmojiSelector } from 'soapbox/components/ui';
|
||||
|
||||
import type { List as ImmutableList } from 'immutable';
|
||||
import type { RootState } from 'soapbox/store';
|
||||
|
||||
const mapStateToProps = (state: RootState) => ({
|
||||
allowedEmoji: getSoapboxConfig(state).allowedEmoji,
|
||||
});
|
||||
|
||||
interface IEmojiSelector {
|
||||
allowedEmoji: ImmutableList<string>,
|
||||
onReact: (emoji: string) => void,
|
||||
onUnfocus: () => void,
|
||||
visible: boolean,
|
||||
focused?: boolean,
|
||||
}
|
||||
|
||||
class EmojiSelector extends ImmutablePureComponent<IEmojiSelector> {
|
||||
|
||||
static defaultProps: Partial<IEmojiSelector> = {
|
||||
onReact: () => { },
|
||||
onUnfocus: () => { },
|
||||
visible: false,
|
||||
};
|
||||
|
||||
node?: HTMLDivElement = undefined;
|
||||
|
||||
handleBlur: React.FocusEventHandler<HTMLDivElement> = e => {
|
||||
const { focused, onUnfocus } = this.props;
|
||||
|
||||
if (focused && (!e.currentTarget || !e.currentTarget.classList.contains('emoji-react-selector__emoji'))) {
|
||||
onUnfocus();
|
||||
}
|
||||
};
|
||||
|
||||
_selectPreviousEmoji = (i: number): void => {
|
||||
if (!this.node) return;
|
||||
|
||||
if (i !== 0) {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i})`);
|
||||
button?.focus();
|
||||
} else {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:last-child');
|
||||
button?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
_selectNextEmoji = (i: number) => {
|
||||
if (!this.node) return;
|
||||
|
||||
if (i !== this.props.allowedEmoji.size - 1) {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector(`.emoji-react-selector__emoji:nth-child(${i + 2})`);
|
||||
button?.focus();
|
||||
} else {
|
||||
const button: HTMLButtonElement | null = this.node.querySelector('.emoji-react-selector__emoji:first-child');
|
||||
button?.focus();
|
||||
}
|
||||
};
|
||||
|
||||
handleKeyDown = (i: number): React.KeyboardEventHandler => e => {
|
||||
const { onUnfocus } = this.props;
|
||||
|
||||
switch (e.key) {
|
||||
case 'Tab':
|
||||
e.preventDefault();
|
||||
if (e.shiftKey) this._selectPreviousEmoji(i);
|
||||
else this._selectNextEmoji(i);
|
||||
break;
|
||||
case 'Left':
|
||||
case 'ArrowLeft':
|
||||
this._selectPreviousEmoji(i);
|
||||
break;
|
||||
case 'Right':
|
||||
case 'ArrowRight':
|
||||
this._selectNextEmoji(i);
|
||||
break;
|
||||
case 'Escape':
|
||||
onUnfocus();
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
handleReact = (emoji: string) => (): void => {
|
||||
const { onReact, focused, onUnfocus } = this.props;
|
||||
|
||||
onReact(emoji);
|
||||
|
||||
if (focused) {
|
||||
onUnfocus();
|
||||
}
|
||||
};
|
||||
|
||||
handlers = {
|
||||
open: () => { },
|
||||
};
|
||||
|
||||
setRef = (c: HTMLDivElement): void => {
|
||||
this.node = c;
|
||||
};
|
||||
|
||||
render() {
|
||||
const { visible, focused, allowedEmoji, onReact } = this.props;
|
||||
|
||||
return (
|
||||
<HotKeys handlers={this.handlers}>
|
||||
{/*<div
|
||||
className={classNames('flex absolute bg-white dark:bg-gray-500 px-2 py-3 rounded-full shadow-md opacity-0 pointer-events-none duration-100 w-max', { 'opacity-100 pointer-events-auto z-[999]': visible || focused })}
|
||||
onBlur={this.handleBlur}
|
||||
ref={this.setRef}
|
||||
>
|
||||
{allowedEmoji.map((emoji, i) => (
|
||||
<button
|
||||
key={i}
|
||||
className='emoji-react-selector__emoji'
|
||||
onClick={this.handleReact(emoji)}
|
||||
onKeyDown={this.handleKeyDown(i)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
>
|
||||
<Emoji emoji={emoji} />
|
||||
</button>
|
||||
))}
|
||||
</div>*/}
|
||||
<RealEmojiSelector
|
||||
emojis={allowedEmoji.toArray()}
|
||||
onReact={onReact}
|
||||
visible={visible}
|
||||
focused={focused}
|
||||
/>
|
||||
</HotKeys>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default connect(mapStateToProps)(EmojiSelector);
|
|
@ -113,17 +113,17 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
const errorText = this.getErrorText();
|
||||
|
||||
return (
|
||||
<div className='h-screen pt-16 pb-12 flex flex-col bg-white dark:bg-primary-900'>
|
||||
<main className='flex-grow flex flex-col justify-center max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<div className='flex-shrink-0 flex justify-center'>
|
||||
<div className='flex h-screen flex-col bg-white pt-16 pb-12 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'>
|
||||
<SiteLogo alt='Logo' className='h-12 w-auto cursor-pointer' />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div className='py-8'>
|
||||
<div className='text-center max-w-xl mx-auto space-y-2'>
|
||||
<h1 className='text-3xl font-extrabold text-gray-900 dark:text-gray-500 tracking-tight sm:text-4xl'>
|
||||
<div className='mx-auto max-w-xl space-y-2 text-center'>
|
||||
<h1 className='text-3xl font-extrabold tracking-tight text-gray-900 dark:text-gray-500 sm:text-4xl'>
|
||||
<FormattedMessage id='alert.unexpected.message' defaultMessage='Something went wrong.' />
|
||||
</h1>
|
||||
<p className='text-lg text-gray-700 dark:text-gray-600'>
|
||||
|
@ -132,7 +132,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
defaultMessage="We're sorry for the interruption. If the problem persists, please reach out to our support team. You may also try to {clearCookies} (this will log you out)."
|
||||
values={{
|
||||
clearCookies: (
|
||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 dark:text-accent-blue hover:underline'>
|
||||
<a href='/' onClick={this.clearCookies} className='text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage
|
||||
id='alert.unexpected.clear_cookies'
|
||||
defaultMessage='clear cookies and browser data'
|
||||
|
@ -150,7 +150,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</Text>
|
||||
|
||||
<div className='mt-10'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 dark:text-accent-blue hover:underline'>
|
||||
<a href='/' className='text-base font-medium text-primary-600 hover:underline dark:text-accent-blue'>
|
||||
<FormattedMessage id='alert.unexpected.return_home' defaultMessage='Return Home' />
|
||||
<span aria-hidden='true'> →</span>
|
||||
</a>
|
||||
|
@ -158,11 +158,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</div>
|
||||
|
||||
{!isProduction && (
|
||||
<div className='py-16 max-w-lg mx-auto space-y-4'>
|
||||
<div className='mx-auto max-w-lg space-y-4 py-16'>
|
||||
{errorText && (
|
||||
<textarea
|
||||
ref={this.setTextareaRef}
|
||||
className='h-48 p-4 shadow-sm bg-gray-100 text-gray-900 dark:text-gray-100 dark:bg-gray-800 focus:ring-2 focus:ring-primary-500 focus:border-primary-500 block w-full sm:text-sm border-gray-300 dark:border-gray-700 rounded-md font-mono'
|
||||
className='block h-48 w-full rounded-md border-gray-300 bg-gray-100 p-4 font-mono text-gray-900 shadow-sm focus:border-primary-500 focus:ring-2 focus:ring-primary-500 dark:border-gray-700 dark:bg-gray-800 dark:text-gray-100 sm:text-sm'
|
||||
value={errorText}
|
||||
onClick={this.handleCopy}
|
||||
readOnly
|
||||
|
@ -180,11 +180,11 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
</div>
|
||||
</main>
|
||||
|
||||
<footer className='flex-shrink-0 max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8'>
|
||||
<footer className='mx-auto w-full max-w-7xl shrink-0 px-4 sm:px-6 lg:px-8'>
|
||||
<HStack justifyContent='center' space={4} element='nav'>
|
||||
{links.get('status') && (
|
||||
<>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
<a href={links.get('status')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.status' defaultMessage='Status' />
|
||||
</a>
|
||||
</>
|
||||
|
@ -193,7 +193,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
{links.get('help') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
<a href={links.get('help')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.help' defaultMessage='Help Center' />
|
||||
</a>
|
||||
</>
|
||||
|
@ -202,7 +202,7 @@ class ErrorBoundary extends React.PureComponent<Props, State> {
|
|||
{links.get('support') && (
|
||||
<>
|
||||
<span className='inline-block border-l border-gray-300' aria-hidden='true' />
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 dark:text-gray-600 hover:underline'>
|
||||
<a href={links.get('support')} className='text-sm font-medium text-gray-700 hover:underline dark:text-gray-600'>
|
||||
<FormattedMessage id='alert.unexpected.links.support' defaultMessage='Support' />
|
||||
</a>
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -51,11 +51,11 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
));
|
||||
|
||||
return (
|
||||
<div className={classNames('w-full rounded-lg bg-gray-100 dark:bg-primary-800 relative overflow-hidden', className)}>
|
||||
<div className={clsx('relative w-full overflow-hidden rounded-lg bg-gray-100 dark:bg-primary-800', className)}>
|
||||
<div className='absolute top-28 right-3'>
|
||||
{floatingAction && action}
|
||||
</div>
|
||||
<div className='bg-primary-200 dark:bg-gray-600 h-40'>
|
||||
<div className='h-40 bg-primary-200 dark:bg-gray-600'>
|
||||
{banner && <img className='h-full w-full object-cover' src={banner.url} alt={intl.formatMessage(messages.eventBanner)} />}
|
||||
</div>
|
||||
<Stack className='p-2.5' space={2}>
|
||||
|
@ -65,7 +65,7 @@ const EventPreview: React.FC<IEventPreview> = ({ status, className, hideAction,
|
|||
{!floatingAction && action}
|
||||
</HStack>
|
||||
|
||||
<div className='flex gap-y-1 gap-x-2 flex-wrap text-gray-700 dark:text-gray-600'>
|
||||
<div className='flex flex-wrap gap-y-1 gap-x-2 text-gray-700 dark:text-gray-600'>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Icon src={require('@tabler/icons/user.svg')} />
|
||||
<HStack space={1} alignItems='center' grow>
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
* @see soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
export interface IForkAwesomeIcon extends React.HTMLAttributes<HTMLLIElement> {
|
||||
|
@ -25,7 +25,7 @@ const ForkAwesomeIcon: React.FC<IForkAwesomeIcon> = ({ id, className, fixedWidth
|
|||
<i
|
||||
role='img'
|
||||
// alt={alt}
|
||||
className={classNames('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
className={clsx('fa', `fa-${id}`, className, { 'fa-fw': fixedWidth })}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
|
@ -30,8 +30,8 @@ const GdprBanner: React.FC = () => {
|
|||
}
|
||||
|
||||
return (
|
||||
<Banner theme='opaque' className={classNames('transition-transform', { 'translate-y-full': slideout })}>
|
||||
<div className='flex flex-col space-y-4 lg:space-y-0 lg:space-x-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between'>
|
||||
<Banner theme='opaque' className={clsx('transition-transform', { 'translate-y-full': slideout })}>
|
||||
<div className='flex flex-col space-y-4 rtl:space-x-reverse lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4'>
|
||||
<Stack space={2}>
|
||||
<Text size='xl' weight='bold'>
|
||||
<FormattedMessage id='gdpr.title' defaultMessage='{siteTitle} uses cookies' values={{ siteTitle: instance.title }} />
|
||||
|
|
|
@ -18,9 +18,9 @@ const GroupCard: React.FC<IGroupCard> = ({ group }) => {
|
|||
|
||||
return (
|
||||
<div className='overflow-hidden'>
|
||||
<Stack className='bg-white dark:bg-primary-900 border border-solid border-gray-300 dark:border-primary-800 rounded-lg sm:rounded-xl'>
|
||||
<div className='bg-primary-100 dark:bg-gray-800 h-[120px] relative -m-[1px] mb-0 rounded-t-lg sm:rounded-t-xl'>
|
||||
{group.header && <img className='h-full w-full object-cover rounded-t-lg sm:rounded-t-xl' src={group.header} alt={intl.formatMessage(messages.groupHeader)} />}
|
||||
<Stack className='rounded-lg border border-solid border-gray-300 bg-white dark:border-primary-800 dark:bg-primary-900 sm:rounded-xl'>
|
||||
<div className='relative -m-[1px] mb-0 h-[120px] rounded-t-lg bg-primary-100 dark:bg-gray-800 sm:rounded-t-xl'>
|
||||
{group.header && <img className='h-full w-full rounded-t-lg object-cover sm:rounded-t-xl' src={group.header} alt={intl.formatMessage(messages.groupHeader)} />}
|
||||
<div className='absolute left-1/2 bottom-0 -translate-x-1/2 translate-y-1/2'>
|
||||
<Avatar className='ring-2 ring-white dark:ring-primary-900' src={group.avatar} size={64} />
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
|
@ -47,7 +47,7 @@ export const HoverRefWrapper: React.FC<IHoverRefWrapper> = ({ accountId, childre
|
|||
return (
|
||||
<Elem
|
||||
ref={ref}
|
||||
className={classNames('hover-ref-wrapper', className)}
|
||||
className={clsx('hover-ref-wrapper', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useRef } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
@ -45,7 +45,7 @@ export const HoverStatusWrapper: React.FC<IHoverStatusWrapper> = ({ statusId, ch
|
|||
return (
|
||||
<Elem
|
||||
ref={ref}
|
||||
className={classNames('hover-status-wrapper', className)}
|
||||
className={clsx('hover-status-wrapper', className)}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import Icon from 'soapbox/components/icon';
|
||||
|
@ -66,7 +66,7 @@ const IconButton: React.FC<IIconButton> = ({
|
|||
}
|
||||
};
|
||||
|
||||
const classes = classNames(className, 'icon-button', {
|
||||
const classes = clsx(className, 'icon-button', {
|
||||
active,
|
||||
disabled,
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* @module soapbox/components/icon
|
||||
*/
|
||||
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import InlineSVG from 'react-inlinesvg'; // eslint-disable-line no-restricted-imports
|
||||
|
||||
|
@ -17,7 +17,7 @@ export interface IIcon extends React.HTMLAttributes<HTMLDivElement> {
|
|||
const Icon: React.FC<IIcon> = ({ src, alt, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('svg-icon', className)}
|
||||
className={clsx('svg-icon', className)}
|
||||
{...rest}
|
||||
>
|
||||
<InlineSVG src={src} title={alt} loader={<></>} />
|
||||
|
|
|
@ -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 dark:from-primary-900/50 via-white dark:via-primary-900 to-gradient-end/10 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 dark:from-primary-900/50 dark:via-primary-900 dark:to-primary-800/50' />
|
||||
);
|
||||
|
||||
export default LandingGradient;
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Link as Comp, LinkProps } from 'react-router-dom';
|
|||
const Link = (props: LinkProps) => (
|
||||
<Comp
|
||||
{...props}
|
||||
className='text-primary-600 dark:text-accent-blue hover:underline'
|
||||
className='text-primary-600 hover:underline dark:text-accent-blue'
|
||||
/>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
|
@ -45,7 +45,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
|
||||
return React.cloneElement(child, {
|
||||
id: domId,
|
||||
className: classNames({
|
||||
className: clsx({
|
||||
'w-auto': isSelect,
|
||||
}, child.props.className),
|
||||
});
|
||||
|
@ -57,7 +57,7 @@ const ListItem: React.FC<IListItem> = ({ label, hint, children, onClick, onSelec
|
|||
|
||||
return (
|
||||
<Comp
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'flex items-center justify-between px-3 py-2 first:rounded-t-lg last:rounded-b-lg bg-gradient-to-r from-gradient-start/10 to-gradient-end/10': true,
|
||||
'cursor-pointer hover:from-gradient-start/20 hover:to-gradient-end/20 dark:hover:from-gradient-start/5 dark:hover:to-gradient-end/5': typeof onClick !== 'undefined' || typeof onSelect !== 'undefined',
|
||||
})}
|
||||
|
|
|
@ -9,7 +9,7 @@ const LoadingScreen: React.FC = () => {
|
|||
<div className='fixed h-screen w-screen'>
|
||||
<LandingGradient />
|
||||
|
||||
<div className='fixed d-screen w-screen flex items-center justify-center z-10'>
|
||||
<div className='d-screen fixed z-10 flex w-screen items-center justify-center'>
|
||||
<div className='p-4'>
|
||||
<Spinner size={40} withText={false} />
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { OrderedSet as ImmutableOrderedSet } from 'immutable';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
|
@ -100,8 +100,8 @@ const LocationSearch: React.FC<ILocationSearch> = ({ onSelected }) => {
|
|||
renderSuggestion={AutosuggestLocation}
|
||||
/>
|
||||
<div role='button' tabIndex={0} className='search__icon' onClick={handleClear}>
|
||||
<Icon src={require('@tabler/icons/search.svg')} className={classNames('svg-icon--search', { active: isEmpty() })} />
|
||||
<Icon src={require('@tabler/icons/backspace.svg')} className={classNames('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
<Icon src={require('@tabler/icons/search.svg')} className={clsx('svg-icon--search', { active: isEmpty() })} />
|
||||
<Icon src={require('@tabler/icons/backspace.svg')} className={clsx('svg-icon--backspace', { active: !isEmpty() })} aria-label={intl.formatMessage(messages.placeholder)} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useRef, useLayoutEffect } from 'react';
|
||||
|
||||
import Blurhash from 'soapbox/components/blurhash';
|
||||
|
@ -152,7 +152,7 @@ const Item: React.FC<IItem> = ({
|
|||
);
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<div className={clsx('media-gallery__item', { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<a className='media-gallery__item-thumbnail' href={attachment.url} target='_blank' style={{ cursor: 'pointer' }}>
|
||||
<Blurhash hash={attachment.blurhash} className='media-gallery__preview' />
|
||||
<span className='media-gallery__item__icons'>{attachmentIcon}</span>
|
||||
|
@ -171,7 +171,7 @@ const Item: React.FC<IItem> = ({
|
|||
target='_blank'
|
||||
>
|
||||
<StillImage
|
||||
className='w-full h-full'
|
||||
className='h-full w-full'
|
||||
src={mediaPreview ? attachment.preview_url : attachment.url}
|
||||
alt={attachment.description}
|
||||
letterboxed={letterboxed}
|
||||
|
@ -189,7 +189,7 @@ const Item: React.FC<IItem> = ({
|
|||
}
|
||||
|
||||
thumbnail = (
|
||||
<div className={classNames('media-gallery__gifv', { autoplay: autoPlayGif })}>
|
||||
<div className={clsx('media-gallery__gifv', { autoplay: autoPlayGif })}>
|
||||
<video
|
||||
className='media-gallery__item-gifv-thumbnail'
|
||||
aria-label={attachment.description}
|
||||
|
@ -211,7 +211,7 @@ const Item: React.FC<IItem> = ({
|
|||
const ext = attachment.url.split('.').pop()?.toUpperCase();
|
||||
thumbnail = (
|
||||
<a
|
||||
className={classNames('media-gallery__item-thumbnail')}
|
||||
className={clsx('media-gallery__item-thumbnail')}
|
||||
href={attachment.url}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
|
@ -225,7 +225,7 @@ const Item: React.FC<IItem> = ({
|
|||
const ext = attachment.url.split('.').pop()?.toUpperCase();
|
||||
thumbnail = (
|
||||
<a
|
||||
className={classNames('media-gallery__item-thumbnail')}
|
||||
className={clsx('media-gallery__item-thumbnail')}
|
||||
href={attachment.url}
|
||||
onClick={handleClick}
|
||||
target='_blank'
|
||||
|
@ -245,7 +245,7 @@ const Item: React.FC<IItem> = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery__item', `media-gallery__item--${attachment.type}`, { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
<div className={clsx('media-gallery__item', `media-gallery__item--${attachment.type}`, { standalone })} key={attachment.id} style={{ position, float, left, top, right, bottom, height, width: `${width}%` }}>
|
||||
{last && total > ATTACHMENT_LIMIT && (
|
||||
<div className='media-gallery__item-overflow'>
|
||||
+{total - ATTACHMENT_LIMIT + 1}
|
||||
|
@ -546,7 +546,7 @@ const MediaGallery: React.FC<IMediaGallery> = (props) => {
|
|||
}, [node.current]);
|
||||
|
||||
return (
|
||||
<div className={classNames('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
|
||||
<div className={clsx('media-gallery', { 'media-gallery--compact': compact })} style={sizeData.style} ref={node}>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import 'wicg-inert';
|
||||
import { FormattedMessage, defineMessages, useIntl } from 'react-intl';
|
||||
|
@ -232,7 +232,7 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'fixed top-0 left-0 z-[100] w-full h-full overflow-x-hidden overflow-y-auto': true,
|
||||
'pointer-events-none': !visible,
|
||||
})}
|
||||
|
@ -241,13 +241,13 @@ const ModalRoot: React.FC<IModalRoot> = ({ children, onCancel, onClose, type })
|
|||
<div
|
||||
role='presentation'
|
||||
id='modal-overlay'
|
||||
className='fixed inset-0 bg-gray-500/90 dark:bg-gray-700/90 backdrop-blur'
|
||||
className='fixed inset-0 bg-gray-500/90 backdrop-blur dark:bg-gray-700/90'
|
||||
onClick={handleOnClose}
|
||||
/>
|
||||
|
||||
<div
|
||||
role='dialog'
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'my-2 mx-auto relative pointer-events-none flex items-center min-h-[calc(100%-3.5rem)]': true,
|
||||
'p-4 md:p-0': type !== 'MEDIA',
|
||||
})}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IOutlineBox extends React.HTMLAttributes<HTMLDivElement> {
|
||||
|
@ -10,7 +10,7 @@ interface IOutlineBox extends React.HTMLAttributes<HTMLDivElement> {
|
|||
const OutlineBox: React.FC<IOutlineBox> = ({ children, className, ...rest }) => {
|
||||
return (
|
||||
<div
|
||||
className={classNames('p-4 rounded-lg border border-solid border-gray-300 dark:border-gray-800', className)}
|
||||
className={clsx('rounded-lg border border-solid border-gray-300 p-4 dark:border-gray-800', className)}
|
||||
{...rest}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Motion, presets, spring } from 'react-motion';
|
||||
|
@ -20,7 +20,7 @@ const PollPercentageBar: React.FC<{ percent: number, leading: boolean }> = ({ pe
|
|||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(percent, { ...presets.gentle, precision: 0.1 }) }}>
|
||||
{({ width }) => (
|
||||
<span
|
||||
className='absolute inset-0 h-full inline-block bg-primary-100 dark:bg-primary-500 rounded-l-md'
|
||||
className='absolute inset-0 inline-block h-full rounded-l-md bg-primary-100 dark:bg-primary-500'
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
)}
|
||||
|
@ -46,7 +46,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
return (
|
||||
<label
|
||||
className={
|
||||
classNames('flex relative p-2 bg-white dark:bg-primary-900 cursor-pointer rounded-3xl border border-solid hover:bg-primary-50 dark:hover:bg-primary-800/50', {
|
||||
clsx('relative flex cursor-pointer rounded-3xl border border-solid bg-white p-2 hover:bg-primary-50 dark:bg-primary-900 dark:hover:bg-primary-800/50', {
|
||||
'border-primary-600 ring-1 ring-primary-600 bg-primary-50 dark:bg-primary-800/50 dark:border-primary-300 dark:ring-primary-300': active,
|
||||
'border-primary-300 dark:border-primary-500': !active,
|
||||
})
|
||||
|
@ -61,8 +61,8 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
onChange={handleOptionChange}
|
||||
/>
|
||||
|
||||
<div className='grid items-center w-full'>
|
||||
<div className='col-start-1 row-start-1 justify-self-center ml-4 mr-6'>
|
||||
<div className='grid w-full items-center'>
|
||||
<div className='col-start-1 row-start-1 ml-4 mr-6 justify-self-center'>
|
||||
<div className='text-primary-600 dark:text-white'>
|
||||
<Text
|
||||
theme='inherit'
|
||||
|
@ -72,9 +72,9 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className='col-start-1 row-start-1 justify-self-end flex items-center'>
|
||||
<div className='col-start-1 row-start-1 flex items-center justify-self-end'>
|
||||
<span
|
||||
className={classNames('flex items-center justify-center w-6 h-6 flex-none border border-solid rounded-full', {
|
||||
className={clsx('flex h-6 w-6 flex-none items-center justify-center rounded-full border border-solid', {
|
||||
'bg-primary-600 border-primary-600 dark:bg-primary-300 dark:border-primary-300': active,
|
||||
'border-primary-300 bg-white dark:bg-primary-900 dark:border-primary-500': !active,
|
||||
})}
|
||||
|
@ -85,7 +85,7 @@ const PollOptionText: React.FC<IPollOptionText> = ({ poll, option, index, active
|
|||
aria-label={option.title}
|
||||
>
|
||||
{active && (
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='text-white dark:text-primary-900 w-4 h-4' />
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='h-4 w-4 text-white dark:text-primary-900' />
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
@ -123,7 +123,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<HStack
|
||||
justifyContent='between'
|
||||
alignItems='center'
|
||||
className='relative p-2 w-full bg-white dark:bg-primary-800 rounded-md overflow-hidden'
|
||||
className='relative w-full overflow-hidden rounded-md bg-white p-2 dark:bg-primary-800'
|
||||
>
|
||||
<PollPercentageBar percent={percent} leading={leading} />
|
||||
|
||||
|
@ -141,7 +141,7 @@ const PollOption: React.FC<IPollOption> = (props): JSX.Element | null => {
|
|||
<Icon
|
||||
src={require('@tabler/icons/circle-check.svg')}
|
||||
alt={intl.formatMessage(messages.voted)}
|
||||
className='text-primary-600 dark:text-primary-800 dark:fill-white w-4 h-4'
|
||||
className='h-4 w-4 text-primary-600 dark:fill-white dark:text-primary-800'
|
||||
/>
|
||||
) : (
|
||||
<div className='svg-icon' />
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { useIntl, FormattedMessage } from 'react-intl';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
@ -95,7 +95,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'absolute transition-opacity w-[320px] z-[101] top-0 left-0': true,
|
||||
'opacity-100': visible,
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
|
@ -123,7 +123,7 @@ export const ProfileHoverCard: React.FC<IProfileHoverCard> = ({ visible = true }
|
|||
<HStack alignItems='center' space={0.5}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/calendar.svg')}
|
||||
className='w-4 h-4 text-gray-800 dark:text-gray-200'
|
||||
className='h-4 w-4 text-gray-800 dark:text-gray-200'
|
||||
/>
|
||||
|
||||
<Text size='sm'>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IProgressCircle {
|
||||
|
@ -30,7 +30,7 @@ const ProgressCircle: React.FC<IProgressCircle> = ({ progress, radius = 12, stro
|
|||
strokeWidth={stroke}
|
||||
/>
|
||||
<circle
|
||||
className={classNames('stroke-primary-500', {
|
||||
className={clsx('stroke-primary-500', {
|
||||
'stroke-secondary-500': progress > 1,
|
||||
})}
|
||||
style={{
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { MouseEventHandler, useEffect, useRef, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
@ -94,7 +94,7 @@ const QuotedStatus: React.FC<IQuotedStatus> = ({ status, onCancel, compose }) =>
|
|||
return (
|
||||
<OutlineBox
|
||||
data-testid='quoted-status'
|
||||
className={classNames('cursor-pointer', {
|
||||
className={clsx('cursor-pointer', {
|
||||
'hover:bg-gray-100 dark:hover:bg-gray-800': !compose,
|
||||
})}
|
||||
>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import throttle from 'lodash/throttle';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { useIntl, MessageDescriptor } from 'react-intl';
|
||||
|
@ -36,7 +36,7 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
|||
|
||||
const visible = count > 0 && scrolled;
|
||||
|
||||
const classes = classNames('left-1/2 -translate-x-1/2 fixed top-20 z-50', {
|
||||
const classes = clsx('fixed left-1/2 top-20 z-50 -translate-x-1/2', {
|
||||
'hidden': !visible,
|
||||
});
|
||||
|
||||
|
@ -83,7 +83,7 @@ const ScrollTopButton: React.FC<IScrollTopButton> = ({
|
|||
|
||||
return (
|
||||
<div className={classes}>
|
||||
<a className='flex items-center bg-primary-600 hover:bg-primary-700 hover:scale-105 active:scale-100 transition-transform text-white rounded-full px-4 py-2 space-x-1.5 cursor-pointer whitespace-nowrap' onClick={handleClick}>
|
||||
<a className='flex cursor-pointer items-center space-x-1.5 whitespace-nowrap rounded-full bg-primary-600 px-4 py-2 text-white transition-transform hover:scale-105 hover:bg-primary-700 active:scale-100' onClick={handleClick}>
|
||||
<Icon src={require('@tabler/icons/arrow-bar-to-up.svg')} />
|
||||
|
||||
{(count > 0) && (
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable jsx-a11y/interactive-supports-focus */
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
import { Link, NavLink } from 'react-router-dom';
|
||||
|
@ -53,8 +53,8 @@ interface ISidebarLink {
|
|||
const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick }) => {
|
||||
const body = (
|
||||
<HStack space={2} alignItems='center'>
|
||||
<div className='bg-primary-50 dark:bg-gray-800 relative rounded-full inline-flex p-2'>
|
||||
<Icon src={icon} className='text-primary-500 h-5 w-5' />
|
||||
<div className='relative inline-flex rounded-full bg-primary-50 p-2 dark:bg-gray-800'>
|
||||
<Icon src={icon} className='h-5 w-5 text-primary-500' />
|
||||
</div>
|
||||
|
||||
<Text tag='span' weight='medium' theme='inherit'>{text}</Text>
|
||||
|
@ -63,14 +63,14 @@ const SidebarLink: React.FC<ISidebarLink> = ({ href, to, icon, text, onClick })
|
|||
|
||||
if (to) {
|
||||
return (
|
||||
<NavLink className='group rounded-full text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800' to={to} onClick={onClick}>
|
||||
<NavLink className='group rounded-full text-gray-900 hover:bg-gray-50 dark:text-gray-100 dark:hover:bg-gray-800' to={to} onClick={onClick}>
|
||||
{body}
|
||||
</NavLink>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<a className='group rounded-full text-gray-900 dark:text-gray-100 hover:bg-gray-50 dark:hover:bg-gray-800' href={href} target='_blank' onClick={onClick}>
|
||||
<a className='group rounded-full text-gray-900 hover:bg-gray-50 dark:text-gray-100 dark:hover:bg-gray-800' href={href} target='_blank' onClick={onClick}>
|
||||
{body}
|
||||
</a>
|
||||
);
|
||||
|
@ -138,7 +138,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
<div
|
||||
aria-expanded={sidebarOpen}
|
||||
className={
|
||||
classNames({
|
||||
clsx({
|
||||
'z-[1000]': sidebarOpen,
|
||||
hidden: !sidebarOpen,
|
||||
})
|
||||
|
@ -153,7 +153,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
<div className='fixed inset-0 z-[1000] flex'>
|
||||
<div
|
||||
className={
|
||||
classNames({
|
||||
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,
|
||||
'!translate-x-0': sidebarOpen,
|
||||
})
|
||||
|
@ -165,10 +165,10 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
src={require('@tabler/icons/x.svg')}
|
||||
ref={closeButtonRef}
|
||||
iconClassName='h-6 w-6'
|
||||
className='absolute top-0 right-0 -mr-11 mt-2 text-gray-600 dark:text-gray-400 hover:text-gray-600 dark:hover:text-gray-300'
|
||||
className='absolute top-0 right-0 -mr-11 mt-2 text-gray-600 hover:text-gray-600 dark:text-gray-400 dark:hover:text-gray-300'
|
||||
/>
|
||||
|
||||
<div className='relative overflow-y-scroll overflow-auto h-full w-full'>
|
||||
<div className='relative h-full w-full overflow-auto overflow-y-scroll'>
|
||||
<div className='p-4'>
|
||||
<Stack space={4}>
|
||||
<Link to={`/@${account.acct}`} onClick={onClose}>
|
||||
|
@ -334,7 +334,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
|
||||
<Icon
|
||||
src={require('@tabler/icons/chevron-down.svg')}
|
||||
className={classNames('w-4 h-4 text-gray-900 dark:text-gray-100 transition-transform', {
|
||||
className={clsx('h-4 w-4 text-gray-900 transition-transform dark:text-gray-100', {
|
||||
'rotate-180': switcher,
|
||||
})}
|
||||
/>
|
||||
|
@ -342,11 +342,11 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
</button>
|
||||
|
||||
{switcher && (
|
||||
<div className='border-t-2 border-gray-100 dark:border-gray-800 border-solid'>
|
||||
<div className='border-t-2 border-solid border-gray-100 dark:border-gray-800'>
|
||||
{otherAccounts.map(account => renderAccount(account))}
|
||||
|
||||
<NavLink className='flex items-center py-2 space-x-1' to='/login/add' onClick={handleClose}>
|
||||
<Icon className='text-primary-500 w-4 h-4' src={require('@tabler/icons/plus.svg')} />
|
||||
<NavLink className='flex items-center space-x-1 py-2' to='/login/add' onClick={handleClose}>
|
||||
<Icon className='h-4 w-4 text-primary-500' src={require('@tabler/icons/plus.svg')} />
|
||||
<Text size='sm' weight='medium'>{intl.formatMessage(messages.addAccount)}</Text>
|
||||
</NavLink>
|
||||
</div>
|
||||
|
@ -361,7 +361,7 @@ const SidebarMenu: React.FC = (): JSX.Element | null => {
|
|||
{/* Dummy element to keep Close Icon visible */}
|
||||
<div
|
||||
aria-hidden
|
||||
className='w-14 flex-shrink-0'
|
||||
className='w-14 shrink-0'
|
||||
onClick={handleClose}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { NavLink } from 'react-router-dom';
|
||||
|
||||
|
@ -38,7 +38,7 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
|||
to={to}
|
||||
ref={ref}
|
||||
onClick={handleClick}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'flex items-center px-4 py-3.5 text-base font-semibold space-x-4 rtl:space-x-reverse rounded-full group text-gray-600 hover:text-gray-900 dark:text-gray-500 dark:hover:text-gray-100 hover:bg-primary-200 dark:hover:bg-primary-900': true,
|
||||
'dark:text-gray-100 text-gray-900': isActive,
|
||||
})}
|
||||
|
@ -48,7 +48,7 @@ const SidebarNavigationLink = React.forwardRef((props: ISidebarNavigationLink, r
|
|||
src={icon}
|
||||
count={count}
|
||||
countMax={countMax}
|
||||
className={classNames('h-5 w-5', {
|
||||
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-primary-500 dark:text-primary-400': isActive,
|
||||
})}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { useSoapboxConfig, useSettings, useTheme } from 'soapbox/hooks';
|
||||
|
@ -36,7 +36,7 @@ const SiteLogo: React.FC<ISiteLogo> = ({ className, theme, ...rest }) => {
|
|||
return (
|
||||
// eslint-disable-next-line jsx-a11y/alt-text
|
||||
<img
|
||||
className={classNames('object-contain', className)}
|
||||
className={clsx('object-contain', className)}
|
||||
src={getSrc()}
|
||||
{...rest}
|
||||
/>
|
||||
|
|
|
@ -14,8 +14,8 @@ import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions
|
|||
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||
import { initReport } from 'soapbox/actions/reports';
|
||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||
import EmojiButtonWrapper from 'soapbox/components/emoji-button-wrapper';
|
||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||
import StatusReactionWrapper from 'soapbox/components/status-reaction-wrapper';
|
||||
import { HStack } from 'soapbox/components/ui';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
||||
import { useAppDispatch, useAppSelector, useFeatures, useOwnAccount, useSettings, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
@ -98,7 +98,6 @@ const messages = defineMessages({
|
|||
|
||||
interface IStatusActionBar {
|
||||
status: Status,
|
||||
withDismiss?: boolean,
|
||||
withLabels?: boolean,
|
||||
expandable?: boolean,
|
||||
space?: 'expand' | 'compact',
|
||||
|
@ -106,7 +105,6 @@ interface IStatusActionBar {
|
|||
|
||||
const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||
status,
|
||||
withDismiss = false,
|
||||
withLabels = false,
|
||||
expandable = true,
|
||||
space = 'compact',
|
||||
|
@ -387,14 +385,13 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
|
||||
menu.push(null);
|
||||
|
||||
if (ownAccount || withDismiss) {
|
||||
menu.push({
|
||||
text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
|
||||
action: handleConversationMuteClick,
|
||||
icon: mutingConversation ? require('@tabler/icons/bell.svg') : require('@tabler/icons/bell-off.svg'),
|
||||
});
|
||||
menu.push(null);
|
||||
}
|
||||
menu.push({
|
||||
text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation),
|
||||
action: handleConversationMuteClick,
|
||||
icon: mutingConversation ? require('@tabler/icons/bell.svg') : require('@tabler/icons/bell-off.svg'),
|
||||
});
|
||||
|
||||
menu.push(null);
|
||||
|
||||
if (ownAccount) {
|
||||
if (publicStatus) {
|
||||
|
@ -632,7 +629,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
)}
|
||||
|
||||
{features.emojiReacts ? (
|
||||
<EmojiButtonWrapper statusId={status.id}>
|
||||
<StatusReactionWrapper statusId={status.id}>
|
||||
<StatusActionButton
|
||||
title={meEmojiTitle}
|
||||
icon={require('@tabler/icons/heart.svg')}
|
||||
|
@ -643,7 +640,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
|||
emoji={meEmojiReact}
|
||||
text={withLabels ? meEmojiTitle : undefined}
|
||||
/>
|
||||
</EmojiButtonWrapper>
|
||||
</StatusReactionWrapper>
|
||||
) : (
|
||||
<StatusActionButton
|
||||
title={intl.formatMessage(messages.favourite)}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { Text, Icon, Emoji } from 'soapbox/components/ui';
|
||||
|
@ -41,15 +41,15 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
|||
const renderIcon = () => {
|
||||
if (emoji) {
|
||||
return (
|
||||
<span className='flex w-6 h-6 items-center justify-center'>
|
||||
<Emoji className='w-full h-full p-0.5' emoji={emoji} />
|
||||
<span className='flex h-6 w-6 items-center justify-center'>
|
||||
<Emoji className='h-full w-full p-0.5' emoji={emoji} />
|
||||
</span>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Icon
|
||||
src={icon}
|
||||
className={classNames(
|
||||
className={clsx(
|
||||
{
|
||||
'fill-accent-300 hover:fill-accent-300': active && filled && color === COLORS.accent,
|
||||
},
|
||||
|
@ -78,11 +78,11 @@ const StatusActionButton = React.forwardRef<HTMLButtonElement, IStatusActionButt
|
|||
<button
|
||||
ref={ref}
|
||||
type='button'
|
||||
className={classNames(
|
||||
'flex items-center p-1 rounded-full rtl:space-x-reverse',
|
||||
className={clsx(
|
||||
'flex items-center rounded-full p-1 rtl:space-x-reverse',
|
||||
'text-gray-600 hover:text-gray-600 dark:hover:text-white',
|
||||
'bg-white dark:bg-transparent',
|
||||
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500 dark:ring-offset-0',
|
||||
'focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0',
|
||||
{
|
||||
'text-black dark:text-white': active && emoji,
|
||||
'hover:text-gray-600 dark:hover:text-white': !filteredProps.disabled,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useRef, useLayoutEffect, useMemo } from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
@ -25,7 +25,7 @@ interface IReadMoreButton {
|
|||
|
||||
/** Button to expand a truncated status (due to too much content) */
|
||||
const ReadMoreButton: React.FC<IReadMoreButton> = ({ onClick }) => (
|
||||
<button className='flex items-center text-gray-900 dark:text-gray-300 border-0 bg-transparent p-0 pt-2 hover:underline active:underline' onClick={onClick}>
|
||||
<button className='flex items-center border-0 bg-transparent p-0 pt-2 text-gray-900 hover:underline active:underline dark:text-gray-300' onClick={onClick}>
|
||||
<FormattedMessage id='status.read_more' defaultMessage='Read more' />
|
||||
<Icon className='inline-block h-5 w-5' src={require('@tabler/icons/chevron-right.svg')} />
|
||||
</button>
|
||||
|
@ -153,7 +153,7 @@ const StatusContent: React.FC<IStatusContent> = ({
|
|||
|
||||
const content = { __html: parsedHtml };
|
||||
const direction = isRtl(status.search_index) ? 'rtl' : 'ltr';
|
||||
const className = classNames(baseClassName, {
|
||||
const className = clsx(baseClassName, {
|
||||
'cursor-pointer': onClick,
|
||||
'whitespace-normal': withSpoiler,
|
||||
'max-h-[300px]': collapsed,
|
||||
|
@ -183,14 +183,14 @@ const StatusContent: React.FC<IStatusContent> = ({
|
|||
output.push(<Poll id={status.poll} key='poll' status={status.url} />);
|
||||
}
|
||||
|
||||
return <div className={classNames({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</div>;
|
||||
return <div className={clsx({ 'bg-gray-100 dark:bg-primary-800 rounded-md p-4': hasPoll })}>{output}</div>;
|
||||
} else {
|
||||
const output = [
|
||||
<Markup
|
||||
ref={node}
|
||||
tabIndex={0}
|
||||
key='content'
|
||||
className={classNames(baseClassName, {
|
||||
className={clsx(baseClassName, {
|
||||
'leading-normal big-emoji': onlyEmoji,
|
||||
})}
|
||||
direction={direction}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
@ -79,7 +79,7 @@ export const StatusHoverCard: React.FC<IStatusHoverCard> = ({ visible = true })
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'absolute transition-opacity w-[500px] z-50 top-0 left-0': true,
|
||||
'opacity-100': visible,
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { Map as ImmutableMap } from 'immutable';
|
||||
import debounce from 'lodash/debounce';
|
||||
import React, { useRef, useCallback } from 'react';
|
||||
|
@ -244,10 +244,10 @@ const StatusList: React.FC<IStatusList> = ({
|
|||
placeholderComponent={PlaceholderStatus}
|
||||
placeholderCount={20}
|
||||
ref={node}
|
||||
className={classNames('divide-y divide-solid divide-gray-200 dark:divide-gray-800', {
|
||||
className={clsx('divide-y divide-solid divide-gray-200 dark:divide-gray-800', {
|
||||
'divide-none': divideType !== 'border',
|
||||
})}
|
||||
itemClassName={classNames({
|
||||
itemClassName={clsx({
|
||||
'pb-3': divideType !== 'border',
|
||||
})}
|
||||
{...other}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
||||
import { simpleEmojiReact } from 'soapbox/actions/emoji-reacts';
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
|
@ -9,13 +7,13 @@ import { useAppDispatch, useAppSelector, useOwnAccount, useSoapboxConfig } from
|
|||
import { isUserTouching } from 'soapbox/is-mobile';
|
||||
import { getReactForStatus } from 'soapbox/utils/emoji-reacts';
|
||||
|
||||
interface IEmojiButtonWrapper {
|
||||
interface IStatusReactionWrapper {
|
||||
statusId: string,
|
||||
children: JSX.Element,
|
||||
}
|
||||
|
||||
/** Provides emoji reaction functionality to the underlying button component */
|
||||
const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const StatusReactionWrapper: React.FC<IStatusReactionWrapper> = ({ statusId, children }): JSX.Element | null => {
|
||||
const dispatch = useAppDispatch();
|
||||
const ownAccount = useOwnAccount();
|
||||
const status = useAppSelector(state => state.statuses.get(statusId));
|
||||
|
@ -23,24 +21,8 @@ const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children
|
|||
|
||||
const timeout = useRef<NodeJS.Timeout>();
|
||||
const [visible, setVisible] = useState(false);
|
||||
// const [focused, setFocused] = useState(false);
|
||||
|
||||
// `useRef` won't trigger a re-render, while `useState` does.
|
||||
// https://popper.js.org/react-popper/v2/
|
||||
const [referenceElement, setReferenceElement] = useState<HTMLDivElement | null>(null);
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const { styles, attributes } = usePopper(referenceElement, popperElement, {
|
||||
placement: 'top-start',
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [-10, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
|
@ -116,28 +98,6 @@ const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children
|
|||
}));
|
||||
};
|
||||
|
||||
// const handleUnfocus: React.EventHandler<React.KeyboardEvent> = () => {
|
||||
// setFocused(false);
|
||||
// };
|
||||
|
||||
const selector = (
|
||||
<div
|
||||
className={classNames('z-50 transition-opacity duration-100', {
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
<EmojiSelector
|
||||
emojis={soapboxConfig.allowedEmoji}
|
||||
onReact={handleReact}
|
||||
// focused={focused}
|
||||
// onUnfocus={handleUnfocus}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className='relative' onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave}>
|
||||
{React.cloneElement(children, {
|
||||
|
@ -145,9 +105,14 @@ const EmojiButtonWrapper: React.FC<IEmojiButtonWrapper> = ({ statusId, children
|
|||
ref: setReferenceElement,
|
||||
})}
|
||||
|
||||
{selector}
|
||||
<EmojiSelector
|
||||
placement='top-start'
|
||||
referenceElement={referenceElement}
|
||||
onReact={handleReact}
|
||||
visible={visible}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default EmojiButtonWrapper;
|
||||
export default StatusReactionWrapper;
|
|
@ -73,7 +73,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
|
||||
if (to.size > 2) {
|
||||
accounts.push(
|
||||
<span key='more' className='hover:underline cursor-pointer' role='button' onClick={handleOpenMentionsModal} tabIndex={0}>
|
||||
<span key='more' className='cursor-pointer hover:underline' role='button' onClick={handleOpenMentionsModal} tabIndex={0}>
|
||||
<FormattedMessage id='reply_mentions.more' defaultMessage='{count} more' values={{ count: to.size - 2 }} />
|
||||
</span>,
|
||||
);
|
||||
|
@ -93,7 +93,7 @@ const StatusReplyMentions: React.FC<IStatusReplyMentions> = ({ status, hoverable
|
|||
<HoverStatusWrapper statusId={status.in_reply_to_id} inline>
|
||||
<span
|
||||
key='hoverstatus'
|
||||
className='hover:underline cursor-pointer'
|
||||
className='cursor-pointer hover:underline'
|
||||
role='presentation'
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { HotKeys } from 'react-hotkeys';
|
||||
import { useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
|
@ -53,7 +53,6 @@ export interface IStatus {
|
|||
hoverable?: boolean,
|
||||
variant?: 'default' | 'rounded',
|
||||
showGroup?: boolean,
|
||||
withDismiss?: boolean,
|
||||
accountAction?: React.ReactElement,
|
||||
}
|
||||
|
||||
|
@ -74,7 +73,6 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
hideActionBar,
|
||||
variant = 'rounded',
|
||||
showGroup = true,
|
||||
withDismiss,
|
||||
} = props;
|
||||
|
||||
const intl = useIntl();
|
||||
|
@ -291,8 +289,10 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={minHandlers}>
|
||||
<div className={classNames('status__wrapper', 'status__wrapper--filtered', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
<div className={clsx('status__wrapper text-center', { focusable })} tabIndex={focusable ? 0 : undefined} ref={node}>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='status.filtered' defaultMessage='Filtered' />
|
||||
</Text>
|
||||
</div>
|
||||
</HotKeys>
|
||||
);
|
||||
|
@ -341,7 +341,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
return (
|
||||
<HotKeys handlers={handlers} data-testid='status'>
|
||||
<div
|
||||
className={classNames('status cursor-pointer', { focusable })}
|
||||
className={clsx('status cursor-pointer', { focusable })}
|
||||
tabIndex={focusable && !muted ? 0 : undefined}
|
||||
data-featured={featured ? 'true' : null}
|
||||
aria-label={textForScreenReader(intl, actualStatus, rebloggedByText)}
|
||||
|
@ -351,7 +351,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
>
|
||||
<Card
|
||||
variant={variant}
|
||||
className={classNames('status__wrapper space-y-4', `status-${actualStatus.visibility}`, {
|
||||
className={clsx('status__wrapper space-y-4', `status-${actualStatus.visibility}`, {
|
||||
'py-6 sm:p-5': variant === 'rounded',
|
||||
'status-reply': !!status.in_reply_to_id,
|
||||
muted,
|
||||
|
@ -421,7 +421,7 @@ const Status: React.FC<IStatus> = (props) => {
|
|||
|
||||
{(!hideActionBar && !isUnderReview) && (
|
||||
<div className='pt-4'>
|
||||
<StatusActionBar status={actualStatus} withDismiss={withDismiss} />
|
||||
<StatusActionBar status={actualStatus} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -91,7 +91,7 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
|||
|
||||
return (
|
||||
<div
|
||||
className={classNames('absolute z-40', {
|
||||
className={clsx('absolute z-40', {
|
||||
'cursor-default backdrop-blur-lg rounded-lg w-full h-full border-0 flex justify-center': !visible,
|
||||
'bg-gray-800/75 inset-0': !visible,
|
||||
'bottom-1 right-1': visible,
|
||||
|
@ -107,8 +107,8 @@ const SensitiveContentOverlay = React.forwardRef<HTMLDivElement, ISensitiveConte
|
|||
size='sm'
|
||||
/>
|
||||
) : (
|
||||
<div className='flex justify-center items-center max-h-screen'>
|
||||
<div className='text-center w-3/4 mx-auto space-y-4' ref={ref}>
|
||||
<div className='flex max-h-screen items-center justify-center'>
|
||||
<div className='mx-auto w-3/4 space-y-4 text-center' ref={ref}>
|
||||
<div className='space-y-1'>
|
||||
<Text theme='white' weight='semibold'>
|
||||
{intl.formatMessage(isUnderReview ? messages.underReviewTitle : messages.sensitiveTitle)}
|
||||
|
|
|
@ -21,7 +21,7 @@ const StatusInfo = (props: IStatusInfo) => {
|
|||
return (
|
||||
<Container
|
||||
{...containerProps}
|
||||
className='flex items-center text-gray-700 dark:text-gray-600 text-xs font-medium space-x-3 rtl:space-x-reverse hover:underline'
|
||||
className='flex items-center space-x-3 text-xs font-medium text-gray-700 hover:underline rtl:space-x-reverse dark:text-gray-600'
|
||||
>
|
||||
<div
|
||||
className='flex justify-end'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useRef } from 'react';
|
||||
|
||||
import { useSettings } from 'soapbox/hooks';
|
||||
|
@ -39,7 +39,7 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
|
|||
};
|
||||
|
||||
/** ClassNames shared between the `<img>` and `<canvas>` elements. */
|
||||
const baseClassName = classNames('w-full h-full block', {
|
||||
const baseClassName = clsx('block h-full w-full', {
|
||||
'object-contain': letterboxed,
|
||||
'object-cover': !letterboxed,
|
||||
});
|
||||
|
@ -47,7 +47,7 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
|
|||
return (
|
||||
<div
|
||||
data-testid='still-image-container'
|
||||
className={classNames(className, 'relative group overflow-hidden isolate')}
|
||||
className={clsx(className, 'group relative isolate overflow-hidden')}
|
||||
style={style}
|
||||
>
|
||||
<img
|
||||
|
@ -55,7 +55,7 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
|
|||
alt={alt}
|
||||
ref={img}
|
||||
onLoad={handleImageLoad}
|
||||
className={classNames(baseClassName, {
|
||||
className={clsx(baseClassName, {
|
||||
'invisible group-hover:visible': hoverToPlay,
|
||||
})}
|
||||
/>
|
||||
|
@ -63,14 +63,14 @@ const StillImage: React.FC<IStillImage> = ({ alt, className, src, style, letterb
|
|||
{hoverToPlay && (
|
||||
<canvas
|
||||
ref={canvas}
|
||||
className={classNames(baseClassName, {
|
||||
className={clsx(baseClassName, {
|
||||
'group-hover:invisible': hoverToPlay,
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
|
||||
{(hoverToPlay && showExt) && (
|
||||
<div className='group-hover:hidden absolute opacity-90 left-2 bottom-2 pointer-events-none'>
|
||||
<div className='pointer-events-none absolute left-2 bottom-2 opacity-90 group-hover:hidden'>
|
||||
<ExtensionBadge ext='GIF' />
|
||||
</div>
|
||||
)}
|
||||
|
@ -86,7 +86,7 @@ interface IExtensionBadge {
|
|||
/** Badge displaying a file extension. */
|
||||
const ExtensionBadge: React.FC<IExtensionBadge> = ({ ext }) => {
|
||||
return (
|
||||
<div className='inline-flex items-center px-2 py-0.5 rounded text-sm font-medium bg-gray-100 dark:bg-gray-800 text-gray-900 dark:text-gray-100'>
|
||||
<div className='inline-flex items-center rounded bg-gray-100 px-2 py-0.5 text-sm font-medium text-gray-900 dark:bg-gray-800 dark:text-gray-100'>
|
||||
{ext}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { NavLink, useLocation } from 'react-router-dom';
|
||||
|
||||
|
@ -33,7 +33,7 @@ const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax,
|
|||
{count !== undefined ? (
|
||||
<IconWithCounter
|
||||
src={src}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': !active,
|
||||
'text-primary-500': active,
|
||||
|
@ -44,7 +44,7 @@ const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax,
|
|||
) : (
|
||||
<Icon
|
||||
src={src}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'h-5 w-5': true,
|
||||
'text-gray-600': !active,
|
||||
'text-primary-500': active,
|
||||
|
@ -56,7 +56,7 @@ const ThumbNavigationLink: React.FC<IThumbNavigationLink> = ({ count, countMax,
|
|||
tag='span'
|
||||
size='xs'
|
||||
weight='medium'
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'text-gray-600': !active,
|
||||
'text-primary-500': active,
|
||||
})}
|
||||
|
|
|
@ -19,7 +19,7 @@ const Tombstone: React.FC<ITombstone> = ({ id, onMoveUp, onMoveDown }) => {
|
|||
|
||||
return (
|
||||
<HotKeys handlers={handlers}>
|
||||
<div className='p-9 flex items-center justify-center sm:rounded-xl bg-gray-100 border border-solid border-gray-200 dark:bg-gray-900 dark:border-gray-800 focusable' tabIndex={0}>
|
||||
<div className='focusable flex items-center justify-center border border-solid border-gray-200 bg-gray-100 p-9 dark:border-gray-800 dark:bg-gray-900 sm:rounded-xl' tabIndex={0}>
|
||||
<Text>
|
||||
<FormattedMessage id='statuses.tombstone' defaultMessage='One or more posts are unavailable.' />
|
||||
</Text>
|
||||
|
|
|
@ -6,7 +6,7 @@ import { translateStatus, undoStatusTranslation } from 'soapbox/actions/statuses
|
|||
import { useAppDispatch, useAppSelector, useFeatures, useInstance } from 'soapbox/hooks';
|
||||
import { isLocal } from 'soapbox/utils/accounts';
|
||||
|
||||
import { Icon, Stack } from './ui';
|
||||
import { Stack, Button, Text } from './ui';
|
||||
|
||||
import type { Account, Status } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -44,32 +44,36 @@ const TranslateButton: React.FC<ITranslateButton> = ({ status }) => {
|
|||
|
||||
if (!features.translations || !renderTranslate || !supportsLanguages) return null;
|
||||
|
||||
const buttonClassName = 'flex items-center gap-0.5 w-fit px-2 py-1 border-gray-600 hover:border-gray-700 dark:hover:border-gray-500 border-solid border text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 text-start text-sm rounded-full';
|
||||
|
||||
if (status.translation) {
|
||||
const languageNames = new Intl.DisplayNames([intl.locale], { type: 'language' });
|
||||
const languageName = languageNames.of(status.language!);
|
||||
const provider = status.translation.get('provider');
|
||||
|
||||
return (
|
||||
<Stack className='text-gray-700 dark:text-gray-600 text-sm' space={1} alignItems='start'>
|
||||
<span>
|
||||
<Stack space={3} alignItems='start'>
|
||||
<Button
|
||||
theme='muted'
|
||||
text={<FormattedMessage id='status.show_original' defaultMessage='Show original' />}
|
||||
icon={require('@tabler/icons/language.svg')}
|
||||
onClick={handleTranslate}
|
||||
/>
|
||||
<Text theme='muted'>
|
||||
<FormattedMessage id='status.translated_from_with' defaultMessage='Translated from {lang} using {provider}' values={{ lang: languageName, provider }} />
|
||||
</span>
|
||||
|
||||
<button className={buttonClassName} onClick={handleTranslate}>
|
||||
<Icon className='h-5 w-5 stroke-[1.25]' src={require('@tabler/icons/language.svg')} strokeWidth={1.25} />
|
||||
<FormattedMessage id='status.show_original' defaultMessage='Show original' />
|
||||
</button>
|
||||
</Text>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<button className={buttonClassName} onClick={handleTranslate}>
|
||||
<Icon className='h-5 w-5' src={require('@tabler/icons/language.svg')} strokeWidth={1.25} />
|
||||
<FormattedMessage id='status.translate' defaultMessage='Translate' />
|
||||
</button>
|
||||
<div>
|
||||
<Button
|
||||
theme='muted'
|
||||
text={<FormattedMessage id='status.translate' defaultMessage='Translate' />}
|
||||
icon={require('@tabler/icons/language.svg')}
|
||||
onClick={handleTranslate}
|
||||
/>
|
||||
</div>
|
||||
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -36,13 +36,13 @@ const Accordion: React.FC<IAccordion> = ({ headline, children, menu, expanded =
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='bg-white dark:bg-primary-800 text-gray-900 dark:text-gray-100 rounded-lg shadow dark:shadow-none'>
|
||||
<div className='rounded-lg bg-white text-gray-900 shadow dark:bg-primary-800 dark:text-gray-100 dark:shadow-none'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={handleToggle}
|
||||
title={intl.formatMessage(expanded ? messages.collapse : messages.expand)}
|
||||
aria-expanded={expanded}
|
||||
className='px-4 py-3 font-semibold flex items-center justify-between w-full'
|
||||
className='flex w-full items-center justify-between px-4 py-3 font-semibold'
|
||||
>
|
||||
<span>{headline}</span>
|
||||
|
||||
|
@ -55,14 +55,14 @@ const Accordion: React.FC<IAccordion> = ({ headline, children, menu, expanded =
|
|||
)}
|
||||
<Icon
|
||||
src={expanded ? require('@tabler/icons/chevron-up.svg') : require('@tabler/icons/chevron-down.svg')}
|
||||
className='text-gray-700 dark:text-gray-600 h-5 w-5'
|
||||
className='h-5 w-5 text-gray-700 dark:text-gray-600'
|
||||
/>
|
||||
</HStack>
|
||||
</button>
|
||||
|
||||
<div
|
||||
className={
|
||||
classNames({
|
||||
clsx({
|
||||
'p-4 rounded-b-lg border-t border-solid border-gray-100 dark:border-primary-900': true,
|
||||
'h-0 hidden': !expanded,
|
||||
})
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import StillImage from 'soapbox/components/still-image';
|
||||
|
@ -25,7 +25,7 @@ const Avatar = (props: IAvatar) => {
|
|||
|
||||
return (
|
||||
<StillImage
|
||||
className={classNames('rounded-full', className)}
|
||||
className={clsx('rounded-full', className)}
|
||||
style={style}
|
||||
src={src}
|
||||
alt='Avatar'
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
interface IBanner {
|
||||
|
@ -12,12 +12,12 @@ const Banner: React.FC<IBanner> = ({ theme, children, className }) => {
|
|||
return (
|
||||
<div
|
||||
data-testid='banner'
|
||||
className={classNames('fixed bottom-0 left-0 right-0 py-8 z-50', {
|
||||
className={clsx('fixed inset-x-0 bottom-0 z-50 py-8', {
|
||||
'backdrop-blur bg-primary-800/80 dark:bg-primary-900/80': theme === 'frosted',
|
||||
'bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 shadow-3xl dark:shadow-inset': theme === 'opaque',
|
||||
}, className)}
|
||||
>
|
||||
<div className='max-w-4xl mx-auto px-4'>
|
||||
<div className='mx-auto max-w-4xl px-4'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
|
@ -63,7 +63,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
|||
return null;
|
||||
}
|
||||
|
||||
return <Icon src={icon} className='w-4 h-4' />;
|
||||
return <Icon src={icon} className='h-4 w-4' />;
|
||||
};
|
||||
|
||||
const handleClick: React.MouseEventHandler<HTMLButtonElement> = React.useCallback((event) => {
|
||||
|
@ -74,7 +74,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
|||
|
||||
const renderButton = () => (
|
||||
<button
|
||||
className={classNames('space-x-2 rtl:space-x-reverse', themeClass, className)}
|
||||
className={clsx('space-x-2 rtl:space-x-reverse', themeClass, className)}
|
||||
disabled={disabled}
|
||||
onClick={handleClick}
|
||||
ref={ref}
|
||||
|
@ -100,4 +100,7 @@ const Button = React.forwardRef<HTMLButtonElement, IButton>((props, ref): JSX.El
|
|||
return renderButton();
|
||||
});
|
||||
|
||||
export default Button;
|
||||
export {
|
||||
Button as default,
|
||||
Button,
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
|
||||
const themes = {
|
||||
primary:
|
||||
|
@ -11,6 +11,7 @@ const themes = {
|
|||
danger: 'border-transparent bg-danger-100 dark:bg-danger-900 text-danger-600 dark:text-danger-200 hover:bg-danger-600 hover:text-gray-100 dark:hover:text-gray-100 dark:hover:bg-danger-500 focus:bg-danger-800 dark:focus:bg-danger-600',
|
||||
transparent: 'border-transparent text-gray-800 backdrop-blur-sm bg-white/75 hover:bg-white/80',
|
||||
outline: 'border-gray-100 border-2 bg-transparent text-gray-100 hover:bg-white/10',
|
||||
muted: 'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500',
|
||||
};
|
||||
|
||||
const sizes = {
|
||||
|
@ -37,7 +38,7 @@ const useButtonStyles = ({
|
|||
disabled,
|
||||
size,
|
||||
}: IButtonStyles) => {
|
||||
const buttonStyle = classNames({
|
||||
const buttonStyle = clsx({
|
||||
'inline-flex items-center border font-medium rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 appearance-none transition-all': true,
|
||||
'select-none disabled:opacity-75 disabled:cursor-default': disabled,
|
||||
[`${themes[theme]}`]: true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
@ -32,7 +32,7 @@ const Card = React.forwardRef<HTMLDivElement, ICard>(({ children, variant = 'def
|
|||
<div
|
||||
ref={ref}
|
||||
{...filteredProps}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-lg dark:shadow-none overflow-hidden': variant === 'rounded',
|
||||
[sizes[size]]: variant === 'rounded',
|
||||
}, className)}
|
||||
|
@ -64,7 +64,7 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
|
|||
const backAttributes = backHref ? { to: backHref } : { onClick: onBackClick };
|
||||
|
||||
return (
|
||||
<Comp {...backAttributes} className='text-gray-900 dark:text-gray-100 focus:ring-primary-500 focus:ring-2' aria-label={intl.formatMessage(messages.back)}>
|
||||
<Comp {...backAttributes} className='text-gray-900 focus:ring-2 focus:ring-primary-500 dark:text-gray-100' aria-label={intl.formatMessage(messages.back)}>
|
||||
<SvgIcon src={require('@tabler/icons/arrow-left.svg')} className='h-6 w-6 rtl:rotate-180' />
|
||||
<span className='sr-only' data-testid='back-button'>{intl.formatMessage(messages.back)}</span>
|
||||
</Comp>
|
||||
|
@ -72,7 +72,7 @@ const CardHeader: React.FC<ICardHeader> = ({ className, children, backHref, onBa
|
|||
};
|
||||
|
||||
return (
|
||||
<HStack alignItems='center' space={2} className={classNames('mb-4', className)}>
|
||||
<HStack alignItems='center' space={2} className={clsx('mb-4', className)}>
|
||||
{renderBackButton()}
|
||||
|
||||
{children}
|
||||
|
|
|
@ -9,7 +9,7 @@ const Checkbox = React.forwardRef<HTMLInputElement, ICheckbox>((props, ref) => {
|
|||
{...props}
|
||||
ref={ref}
|
||||
type='checkbox'
|
||||
className='border-2 dark:bg-gray-900 dark:border-gray-800 focus:ring-primary-500 h-4 w-4 text-primary-600 border-gray-300 rounded'
|
||||
className='h-4 w-4 rounded border-2 border-gray-300 text-primary-600 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900'
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -74,7 +74,7 @@ const Column: React.FC<IColumn> = React.forwardRef((props, ref: React.ForwardedR
|
|||
<ColumnHeader
|
||||
label={label}
|
||||
backHref={backHref}
|
||||
className={classNames({ 'px-4 pt-4 sm:p-0': transparent })}
|
||||
className={clsx({ 'px-4 pt-4 sm:p-0': transparent })}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ interface ICounter {
|
|||
/** A simple counter for notifications, etc. */
|
||||
const Counter: React.FC<ICounter> = ({ count, countMax }) => {
|
||||
return (
|
||||
<span className='h-5 min-w-[20px] max-w-[26px] flex items-center justify-center bg-secondary-500 text-xs font-medium text-white rounded-full ring-2 ring-white dark:ring-gray-800'>
|
||||
<span className='flex h-5 min-w-[20px] max-w-[26px] items-center justify-center rounded-full bg-secondary-500 text-xs font-medium text-white ring-2 ring-white dark:ring-gray-800'>
|
||||
{shortNumberFormat(count, countMax)}
|
||||
</span>
|
||||
);
|
||||
|
|
|
@ -31,7 +31,7 @@ const Datepicker = ({ onChange }: IDatepicker) => {
|
|||
}, [month, day, year]);
|
||||
|
||||
return (
|
||||
<div className='grid grid-cols-1 gap-y-2 gap-x-2 sm:grid-cols-3'>
|
||||
<div className='grid grid-cols-1 gap-2 sm:grid-cols-3'>
|
||||
<div className='sm:col-span-1'>
|
||||
<Stack>
|
||||
<Text size='sm' weight='medium' theme='muted'>
|
||||
|
|
|
@ -13,12 +13,12 @@ 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-gray-100 dark:border-gray-800 border-solid' />
|
||||
<div className='w-full border-t-2 border-solid border-gray-100 dark:border-gray-800' />
|
||||
</div>
|
||||
|
||||
{text && (
|
||||
<div className='relative flex justify-center'>
|
||||
<span className='px-2 bg-white dark:bg-gray-900 text-gray-700 dark:text-gray-600' data-testid='divider-text'>
|
||||
<span className='bg-white px-2 text-gray-700 dark:bg-gray-900 dark:text-gray-600' data-testid='divider-text'>
|
||||
<Text size={textSize} tag='span' theme='inherit'>{text}</Text>
|
||||
</span>
|
||||
</div>
|
||||
|
|
|
@ -1,13 +1,16 @@
|
|||
import classNames from 'clsx';
|
||||
import React from 'react';
|
||||
import { Placement } from '@popperjs/core';
|
||||
import clsx from 'clsx';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { usePopper } from 'react-popper';
|
||||
|
||||
import { Emoji, HStack } from 'soapbox/components/ui';
|
||||
import { useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
interface IEmojiButton {
|
||||
/** Unicode emoji character. */
|
||||
emoji: string,
|
||||
/** Event handler when the emoji is clicked. */
|
||||
onClick: React.EventHandler<React.MouseEvent>,
|
||||
onClick(emoji: string): void
|
||||
/** Extra class name on the <button> element. */
|
||||
className?: string,
|
||||
/** Tab order of the button. */
|
||||
|
@ -16,48 +19,103 @@ interface IEmojiButton {
|
|||
|
||||
/** Clickable emoji button that scales when hovered. */
|
||||
const EmojiButton: React.FC<IEmojiButton> = ({ emoji, className, onClick, tabIndex }): JSX.Element => {
|
||||
const handleClick: React.EventHandler<React.MouseEvent> = (event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
onClick(emoji);
|
||||
};
|
||||
|
||||
return (
|
||||
<button className={classNames(className)} onClick={onClick} tabIndex={tabIndex}>
|
||||
<Emoji className='w-8 h-8 duration-100 hover:scale-125' emoji={emoji} />
|
||||
<button className={clsx(className)} onClick={handleClick} tabIndex={tabIndex}>
|
||||
<Emoji className='h-6 w-6 duration-100 hover:scale-110' emoji={emoji} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface IEmojiSelector {
|
||||
/** List of Unicode emoji characters. */
|
||||
emojis: Iterable<string>,
|
||||
onClose?(): void
|
||||
/** Event handler when an emoji is clicked. */
|
||||
onReact: (emoji: string) => void,
|
||||
onReact(emoji: string): void
|
||||
/** Element that triggers the EmojiSelector Popper */
|
||||
referenceElement: HTMLElement | null
|
||||
placement?: Placement
|
||||
/** Whether the selector should be visible. */
|
||||
visible?: boolean,
|
||||
/** Whether the selector should be focused. */
|
||||
focused?: boolean,
|
||||
visible?: boolean
|
||||
}
|
||||
|
||||
/** Panel with a row of emoji buttons. */
|
||||
const EmojiSelector: React.FC<IEmojiSelector> = ({ emojis, onReact, visible = false, focused = false }): JSX.Element => {
|
||||
const EmojiSelector: React.FC<IEmojiSelector> = ({
|
||||
referenceElement,
|
||||
onClose,
|
||||
onReact,
|
||||
placement = 'top',
|
||||
visible = false,
|
||||
}): JSX.Element => {
|
||||
const soapboxConfig = useSoapboxConfig();
|
||||
|
||||
const handleReact = (emoji: string): React.EventHandler<React.MouseEvent> => {
|
||||
return (e) => {
|
||||
onReact(emoji);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
// `useRef` won't trigger a re-render, while `useState` does.
|
||||
// https://popper.js.org/react-popper/v2/
|
||||
const [popperElement, setPopperElement] = useState<HTMLDivElement | null>(null);
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (referenceElement?.contains(event.target as Node) || popperElement?.contains(event.target as Node)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const { styles, attributes, update } = usePopper(referenceElement, popperElement, {
|
||||
placement,
|
||||
modifiers: [
|
||||
{
|
||||
name: 'offset',
|
||||
options: {
|
||||
offset: [-10, 0],
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handleClickOutside);
|
||||
};
|
||||
}, [referenceElement]);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible && update) {
|
||||
update();
|
||||
}
|
||||
}, [visible, update]);
|
||||
|
||||
return (
|
||||
<HStack
|
||||
className={classNames('gap-2 bg-white dark:bg-gray-900 p-3 rounded-full shadow-md z-[999] w-max max-w-[100vw] flex-wrap')}
|
||||
<div
|
||||
className={clsx('z-50 transition-opacity duration-100', {
|
||||
'opacity-0 pointer-events-none': !visible,
|
||||
})}
|
||||
ref={setPopperElement}
|
||||
style={styles.popper}
|
||||
{...attributes.popper}
|
||||
>
|
||||
{Array.from(emojis).map((emoji, i) => (
|
||||
<EmojiButton
|
||||
key={i}
|
||||
emoji={emoji}
|
||||
onClick={handleReact(emoji)}
|
||||
tabIndex={(visible || focused) ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
<HStack
|
||||
className={clsx('z-[999] flex w-max max-w-[100vw] flex-wrap space-x-3 rounded-full bg-white px-3 py-2.5 shadow-lg focus:outline-none dark:bg-gray-900 dark:ring-2 dark:ring-primary-700')}
|
||||
>
|
||||
{Array.from(soapboxConfig.allowedEmoji).map((emoji, i) => (
|
||||
<EmojiButton
|
||||
key={i}
|
||||
emoji={emoji}
|
||||
onClick={onReact}
|
||||
tabIndex={visible ? 0 : -1}
|
||||
/>
|
||||
))}
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@ const FileInput = forwardRef<HTMLInputElement, IFileInput>((props, ref) => {
|
|||
{...props}
|
||||
ref={ref}
|
||||
type='file'
|
||||
className='block w-full text-sm text-gray-800 dark:text-gray-200 file:cursor-pointer file:mr-2 file:py-1.5 file:px-3 file:rounded-full file:text-xs file:leading-4 file:font-medium file:border-gray-200 file:border file:border-solid file:bg-white file:text-gray-700 hover:file:bg-gray-100 dark:file:border-gray-800 dark:file:bg-gray-900 dark:file:hover:bg-gray-800 dark:file:text-gray-500'
|
||||
className='block w-full text-sm text-gray-800 file:mr-2 file:cursor-pointer file:rounded-full file:border file:border-solid file:border-gray-200 file:bg-white file:py-1.5 file:px-3 file:text-xs file:font-medium file:leading-4 file:text-gray-700 hover:file:bg-gray-100 dark:text-gray-200 dark:file:border-gray-800 dark:file:bg-gray-900 dark:file:text-gray-500 dark:file:hover:bg-gray-800'
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -55,7 +55,7 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
|
|||
<div>
|
||||
<p
|
||||
data-testid='form-group-error'
|
||||
className='mt-0.5 text-xs text-danger-900 bg-danger-200 rounded-md inline-block px-2 py-1 relative form-error'
|
||||
className='form-error relative mt-0.5 inline-block rounded-md bg-danger-200 px-2 py-1 text-xs text-danger-900'
|
||||
>
|
||||
{errors.join(', ')}
|
||||
</p>
|
||||
|
@ -92,7 +92,7 @@ const FormGroup: React.FC<IFormGroup> = (props) => {
|
|||
{hasError && (
|
||||
<p
|
||||
data-testid='form-group-error'
|
||||
className='mt-0.5 text-xs text-danger-900 bg-danger-200 rounded-md inline-block px-2 py-1 relative form-error'
|
||||
className='form-error relative mt-0.5 inline-block rounded-md bg-danger-200 px-2 py-1 text-xs text-danger-900'
|
||||
>
|
||||
{errors.join(', ')}
|
||||
</p>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { forwardRef } from 'react';
|
||||
|
||||
const justifyContentOptions = {
|
||||
|
@ -61,14 +61,14 @@ const HStack = forwardRef<HTMLDivElement, IHStack>((props, ref) => {
|
|||
<Elem
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
className={classNames('flex rtl:space-x-reverse', {
|
||||
className={clsx('flex rtl:space-x-reverse', {
|
||||
// @ts-ignore
|
||||
[alignItemsOptions[alignItems]]: typeof alignItems !== 'undefined',
|
||||
// @ts-ignore
|
||||
[justifyContentOptions[justifyContent]]: typeof justifyContent !== 'undefined',
|
||||
// @ts-ignore
|
||||
[spaces[space]]: typeof space !== 'undefined',
|
||||
'flex-grow': grow,
|
||||
'grow': grow,
|
||||
'flex-wrap': wrap,
|
||||
}, className)}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import SvgIcon from '../icon/svg-icon';
|
||||
|
@ -15,6 +15,8 @@ interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
|||
transparent?: boolean,
|
||||
/** Predefined styles to display for the button. */
|
||||
theme?: 'seamless' | 'outlined',
|
||||
/** Override the data-testid */
|
||||
'data-testid'?: string
|
||||
}
|
||||
|
||||
/** A clickable icon. */
|
||||
|
@ -25,13 +27,13 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
|
|||
<button
|
||||
ref={ref}
|
||||
type='button'
|
||||
className={classNames('flex items-center space-x-2 p-1 rounded-full focus:outline-none focus:ring-2 focus:ring-offset-2 dark:ring-offset-0 focus:ring-primary-500', {
|
||||
className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', {
|
||||
'bg-white dark:bg-transparent': !transparent,
|
||||
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
|
||||
'opacity-50': filteredProps.disabled,
|
||||
}, className)}
|
||||
{...filteredProps}
|
||||
data-testid='icon-button'
|
||||
data-testid={filteredProps['data-testid'] || 'icon-button'}
|
||||
>
|
||||
<SvgIcon src={src} className={iconClassName} />
|
||||
|
||||
|
|
|
@ -21,9 +21,9 @@ interface IIcon extends Pick<React.SVGAttributes<SVGAElement>, 'strokeWidth'> {
|
|||
|
||||
/** Renders and SVG icon with optional counter. */
|
||||
const Icon: React.FC<IIcon> = ({ src, alt, count, size, countMax, ...filteredProps }): JSX.Element => (
|
||||
<div className='flex flex-col flex-shrink-0 relative' data-testid='icon'>
|
||||
<div className='relative flex shrink-0 flex-col' data-testid='icon'>
|
||||
{count ? (
|
||||
<span className='absolute -top-2 -right-3 min-w-[20px] h-5 flex-shrink-0 whitespace-nowrap flex items-center justify-center break-words'>
|
||||
<span className='absolute -top-2 -right-3 flex h-5 min-w-[20px] shrink-0 items-center justify-center whitespace-nowrap break-words'>
|
||||
<Counter count={count} countMax={countMax} />
|
||||
</span>
|
||||
) : null}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -59,7 +59,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
return (
|
||||
<div
|
||||
className={
|
||||
classNames('relative', {
|
||||
clsx('relative', {
|
||||
'rounded-md': theme !== 'search',
|
||||
'rounded-full': theme === 'search',
|
||||
'mt-1': !String(outerClassName).includes('mt-'),
|
||||
|
@ -68,7 +68,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
}
|
||||
>
|
||||
{icon ? (
|
||||
<div className='absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none'>
|
||||
<div className='pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3'>
|
||||
<Icon src={icon} className='h-4 w-4 text-gray-700 dark:text-gray-600' aria-hidden='true' />
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -83,7 +83,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
{...filteredProps}
|
||||
type={revealed ? 'text' : type}
|
||||
ref={ref}
|
||||
className={classNames('text-base placeholder:text-gray-600 dark:placeholder:text-gray-600', {
|
||||
className={clsx('text-base placeholder:text-gray-600 dark:placeholder:text-gray-600', {
|
||||
'text-gray-900 dark:text-gray-100 block w-full sm:text-sm dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||
['normal', 'search'].includes(theme),
|
||||
'rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800': theme === 'normal',
|
||||
|
@ -95,7 +95,7 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
/>
|
||||
|
||||
{append ? (
|
||||
<div className='absolute inset-y-0 right-0 rtl:left-0 rtl:right-auto flex items-center pr-3'>
|
||||
<div className='absolute inset-y-0 right-0 flex items-center pr-3 rtl:left-0 rtl:right-auto'>
|
||||
{append}
|
||||
</div>
|
||||
) : null}
|
||||
|
@ -108,12 +108,12 @@ const Input = React.forwardRef<HTMLInputElement, IInput>(
|
|||
intl.formatMessage(messages.showPassword)
|
||||
}
|
||||
>
|
||||
<div className='absolute inset-y-0 right-0 rtl:left-0 rtl:right-auto flex items-center'>
|
||||
<div className='absolute inset-y-0 right-0 flex items-center rtl:left-0 rtl:right-auto'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={togglePassword}
|
||||
tabIndex={-1}
|
||||
className='text-gray-700 dark:text-gray-600 hover:text-gray-500 dark:hover:text-gray-400 h-full px-2 focus:ring-primary-500 focus:ring-2'
|
||||
className='h-full px-2 text-gray-700 hover:text-gray-500 focus:ring-2 focus:ring-primary-500 dark:text-gray-600 dark:hover:text-gray-400'
|
||||
>
|
||||
<SvgIcon
|
||||
src={revealed ? require('@tabler/icons/eye-off.svg') : require('@tabler/icons/eye.svg')}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import StickyBox from 'react-sticky-box';
|
||||
|
||||
|
@ -21,8 +21,8 @@ interface LayoutComponent extends React.FC<ILayout> {
|
|||
|
||||
/** Layout container, to hold Sidebar, Main, and Aside. */
|
||||
const Layout: LayoutComponent = ({ children }) => (
|
||||
<div className='sm:pt-4 relative'>
|
||||
<div className='max-w-3xl mx-auto sm:px-6 md:max-w-7xl md:px-8 md:grid md:grid-cols-12 md:gap-8'>
|
||||
<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'>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@ const Layout: LayoutComponent = ({ children }) => (
|
|||
|
||||
/** Left sidebar container in the UI. */
|
||||
const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
||||
<div className='hidden lg:block lg:col-span-3'>
|
||||
<div className='hidden lg:col-span-3 lg:block'>
|
||||
<StickyBox offsetTop={80} className='pb-4'>
|
||||
{children}
|
||||
</StickyBox>
|
||||
|
@ -40,7 +40,7 @@ const Sidebar: React.FC<ISidebar> = ({ children }) => (
|
|||
/** Center column container in the UI. */
|
||||
const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, className }) => (
|
||||
<main
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'md:col-span-12 lg:col-span-9 xl:col-span-6 pb-36': true,
|
||||
}, className)}
|
||||
>
|
||||
|
@ -50,7 +50,7 @@ const Main: React.FC<React.HTMLAttributes<HTMLDivElement>> = ({ children, classN
|
|||
|
||||
/** Right sidebar container in the UI. */
|
||||
const Aside: React.FC<IAside> = ({ children }) => (
|
||||
<aside className='hidden xl:block xl:col-span-3'>
|
||||
<aside className='hidden xl:col-span-3 xl:block'>
|
||||
<StickyBox offsetTop={80} className='space-y-6 pb-12'>
|
||||
{children}
|
||||
</StickyBox>
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
MenuListProps,
|
||||
} from '@reach/menu-button';
|
||||
import { positionDefault, positionRight } from '@reach/popover';
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import './menu.css';
|
||||
|
@ -28,7 +28,7 @@ const MenuList: React.FC<IMenuList> = (props) => {
|
|||
<MenuItems
|
||||
onKeyDown={(event) => event.nativeEvent.stopImmediatePropagation()}
|
||||
className={
|
||||
classNames(className, 'py-1 bg-white dark:bg-primary-900 rounded-lg shadow-menu')
|
||||
clsx(className, 'shadow-menu rounded-lg bg-white py-1 dark:bg-primary-900')
|
||||
}
|
||||
{...filteredProps}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
|
@ -87,16 +87,16 @@ const Modal: React.FC<IModal> = ({
|
|||
}, [skipFocus, buttonRef]);
|
||||
|
||||
return (
|
||||
<div data-testid='modal' className={classNames('block w-full p-6 mx-auto text-start align-middle transition-all transform bg-white dark:bg-primary-900 text-gray-900 dark:text-gray-100 shadow-xl rounded-2xl pointer-events-auto', widths[width])}>
|
||||
<div className='sm:flex sm:items-start w-full justify-between'>
|
||||
<div data-testid='modal' className={clsx('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])}>
|
||||
<div className='w-full justify-between sm:flex sm:items-start'>
|
||||
<div className='w-full'>
|
||||
{title && (
|
||||
<div
|
||||
className={classNames('w-full flex items-center gap-2', {
|
||||
className={clsx('flex w-full items-center gap-2', {
|
||||
'flex-row-reverse': closePosition === 'left',
|
||||
})}
|
||||
>
|
||||
<h3 className='flex-grow text-lg leading-6 font-bold text-gray-900 dark:text-white'>
|
||||
<h3 className='grow text-lg font-bold leading-6 text-gray-900 dark:text-white'>
|
||||
{title}
|
||||
</h3>
|
||||
|
||||
|
@ -105,14 +105,14 @@ const Modal: React.FC<IModal> = ({
|
|||
src={closeIcon}
|
||||
title={intl.formatMessage(messages.close)}
|
||||
onClick={onClose}
|
||||
className='text-gray-500 hover:text-gray-700 dark:text-gray-300 dark:hover:text-gray-200 rtl:rotate-180'
|
||||
className='text-gray-500 hover:text-gray-700 rtl:rotate-180 dark:text-gray-300 dark:hover:text-gray-200'
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{title ? (
|
||||
<div className='w-full mt-2'>
|
||||
<div className='mt-2 w-full'>
|
||||
{children}
|
||||
</div>
|
||||
) : children}
|
||||
|
@ -121,7 +121,7 @@ const Modal: React.FC<IModal> = ({
|
|||
|
||||
{confirmationAction && (
|
||||
<HStack className='mt-5' justifyContent='between' data-testid='modal-actions'>
|
||||
<div className={classNames({ 'flex-grow': !confirmationFullWidth })}>
|
||||
<div className={clsx({ 'grow': !confirmationFullWidth })}>
|
||||
{cancelAction && (
|
||||
<Button
|
||||
theme='tertiary'
|
||||
|
@ -132,7 +132,7 @@ const Modal: React.FC<IModal> = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<HStack space={2} className={classNames({ 'flex-grow': confirmationFullWidth })}>
|
||||
<HStack space={2} className={clsx({ 'grow': confirmationFullWidth })}>
|
||||
{secondaryAction && (
|
||||
<Button
|
||||
theme='secondary'
|
||||
|
|
|
@ -12,7 +12,7 @@ const CountryCodeDropdown: React.FC<ICountryCodeDropdown> = ({ countryCode, onCh
|
|||
return (
|
||||
<select
|
||||
value={countryCode}
|
||||
className='h-full py-0 pl-3 pr-7 text-base bg-transparent border-transparent focus:outline-none focus:ring-primary-500 dark:text-white sm:text-sm rounded-md'
|
||||
className='h-full rounded-md border-transparent bg-transparent py-0 pl-3 pr-7 text-base focus:outline-none focus:ring-primary-500 dark:text-white sm:text-sm'
|
||||
onChange={(event) => onChange(event.target.value as any)}
|
||||
>
|
||||
{COUNTRY_CODES.map((code) => (
|
||||
|
|
|
@ -1,13 +1,32 @@
|
|||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
||||
|
||||
interface IProgressBar {
|
||||
progress: number,
|
||||
/** Number between 0 and 1 to represent the percentage complete. */
|
||||
progress: number
|
||||
/** Height of the progress bar. */
|
||||
size?: 'sm' | 'md'
|
||||
}
|
||||
|
||||
/** A horizontal meter filled to the given percentage. */
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress }) => (
|
||||
<div className='h-2.5 w-full rounded-full bg-gray-300 dark:bg-primary-800 overflow-hidden'>
|
||||
<div className='h-full bg-secondary-500' style={{ width: `${Math.floor(progress * 100)}%` }} />
|
||||
const ProgressBar: React.FC<IProgressBar> = ({ progress, size = 'md' }) => (
|
||||
<div
|
||||
className={clsx('h-2.5 w-full overflow-hidden rounded-lg bg-gray-300 dark:bg-primary-800', {
|
||||
'h-2.5': size === 'md',
|
||||
'h-[6px]': size === 'sm',
|
||||
})}
|
||||
>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress * 100) }}>
|
||||
{({ width }) => (
|
||||
<div
|
||||
className='h-full bg-secondary-500'
|
||||
style={{ width: `${width}%` }}
|
||||
/>
|
||||
)}
|
||||
</Motion>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ const Select = React.forwardRef<HTMLSelectElement, ISelect>((props, ref) => {
|
|||
return (
|
||||
<select
|
||||
ref={ref}
|
||||
className={`w-full pl-3 pr-10 py-2 text-base truncate border-gray-300 dark:border-gray-800 focus:outline-none focus:ring-primary-500 focus:border-primary-500 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:focus:ring-primary-500 dark:focus:border-primary-500 sm:text-sm rounded-md disabled:opacity-50 ${className}`}
|
||||
className={`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 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 sm:text-sm ${className}`}
|
||||
{...filteredProps}
|
||||
>
|
||||
{children}
|
||||
|
|
|
@ -53,14 +53,14 @@ const Slider: React.FC<ISlider> = ({ value, onChange }) => {
|
|||
|
||||
return (
|
||||
<div
|
||||
className='inline-flex cursor-pointer h-6 relative transition'
|
||||
className='relative inline-flex h-6 cursor-pointer transition'
|
||||
onMouseDown={handleMouseDown}
|
||||
ref={node}
|
||||
>
|
||||
<div className='w-full h-1 bg-primary-200 dark:bg-primary-700 absolute top-1/2 -translate-y-1/2 rounded-full' />
|
||||
<div className='h-1 bg-accent-500 absolute top-1/2 -translate-y-1/2 rounded-full' style={{ width: `${value * 100}%` }} />
|
||||
<div className='absolute top-1/2 h-1 w-full -translate-y-1/2 rounded-full bg-primary-200 dark:bg-primary-700' />
|
||||
<div className='absolute top-1/2 h-1 -translate-y-1/2 rounded-full bg-accent-500' style={{ width: `${value * 100}%` }} />
|
||||
<span
|
||||
className='bg-accent-500 absolute rounded-full w-3 h-3 -ml-1.5 top-1/2 -translate-y-1/2 z-10 shadow'
|
||||
className='absolute top-1/2 z-10 -ml-1.5 h-3 w-3 -translate-y-1/2 rounded-full bg-accent-500 shadow'
|
||||
tabIndex={0}
|
||||
style={{ left: `${value * 100}%` }}
|
||||
/>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
const spaces = {
|
||||
|
@ -52,14 +52,14 @@ const Stack = React.forwardRef<HTMLDivElement, IStack>((props, ref: React.Legacy
|
|||
<Elem
|
||||
{...filteredProps}
|
||||
ref={ref}
|
||||
className={classNames('flex flex-col', {
|
||||
className={clsx('flex flex-col', {
|
||||
// @ts-ignore
|
||||
[spaces[space]]: typeof space !== 'undefined',
|
||||
// @ts-ignore
|
||||
[alignItemsOptions[alignItems]]: typeof alignItems !== 'undefined',
|
||||
// @ts-ignore
|
||||
[justifyContentOptions[justifyContent]]: typeof justifyContent !== 'undefined',
|
||||
'flex-grow': grow,
|
||||
'grow': grow,
|
||||
}, className)}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
Tab as ReachTab,
|
||||
useTabsContext,
|
||||
} from '@reach/tabs';
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -46,11 +46,11 @@ const AnimatedTabs: React.FC<IAnimatedInterface> = ({ children, ...rest }) => {
|
|||
ref={ref}
|
||||
>
|
||||
<div
|
||||
className='w-full h-[3px] bg-primary-200 dark:bg-primary-700 absolute'
|
||||
className='absolute h-[3px] w-full bg-primary-200 dark:bg-gray-800'
|
||||
style={{ top }}
|
||||
/>
|
||||
<div
|
||||
className={classNames('absolute h-[3px] bg-primary-500 transition-all duration-200', {
|
||||
className={clsx('absolute h-[3px] bg-primary-500 transition-all duration-200', {
|
||||
'hidden': top <= 0,
|
||||
})}
|
||||
style={{ left, top, width }}
|
||||
|
|
|
@ -43,9 +43,9 @@ const TagInput: React.FC<ITagInput> = ({ tags, onChange, placeholder }) => {
|
|||
};
|
||||
|
||||
return (
|
||||
<div className='mt-1 relative shadow-sm flex-grow'>
|
||||
<div className='relative mt-1 grow shadow-sm'>
|
||||
<HStack
|
||||
className='p-2 pb-0 text-gray-900 dark:text-gray-100 placeholder:text-gray-600 dark:placeholder:text-gray-600 block w-full sm:text-sm dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500 rounded-md bg-white dark:bg-gray-900 border-gray-400 dark:border-gray-800'
|
||||
className='block w-full rounded-md border-gray-400 bg-white p-2 pb-0 text-gray-900 placeholder:text-gray-600 focus:border-primary-500 focus:ring-primary-500 dark:border-gray-800 dark:bg-gray-900 dark:text-gray-100 dark:ring-1 dark:ring-gray-800 dark:placeholder:text-gray-600 dark:focus:border-primary-500 dark:focus:ring-primary-500 sm:text-sm'
|
||||
space={2}
|
||||
wrap
|
||||
>
|
||||
|
@ -56,7 +56,7 @@ const TagInput: React.FC<ITagInput> = ({ tags, onChange, placeholder }) => {
|
|||
))}
|
||||
|
||||
<input
|
||||
className='p-1 mb-2 w-32 h-8 flex-grow bg-transparent outline-none'
|
||||
className='mb-2 h-8 w-32 grow bg-transparent p-1 outline-none'
|
||||
value={input}
|
||||
placeholder={placeholder}
|
||||
onChange={e => setInput(e.target.value)}
|
||||
|
|
|
@ -13,7 +13,7 @@ interface ITag {
|
|||
/** A single editable Tag (used by TagInput). */
|
||||
const Tag: React.FC<ITag> = ({ tag, onDelete }) => {
|
||||
return (
|
||||
<div className='inline-flex p-1 rounded bg-primary-500 items-center whitespace-nowrap'>
|
||||
<div className='inline-flex items-center whitespace-nowrap rounded bg-primary-500 p-1'>
|
||||
<Text theme='white'>{tag}</Text>
|
||||
|
||||
<IconButton
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
const themes = {
|
||||
|
@ -115,7 +115,7 @@ const Text = React.forwardRef<any, IText>(
|
|||
textDecoration: tag === 'abbr' ? 'underline dotted' : undefined,
|
||||
direction,
|
||||
}}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'cursor-default': tag === 'abbr',
|
||||
truncate: truncate,
|
||||
[sizes[size]]: true,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'onKeyDown' | 'onPaste' | 'required' | 'disabled' | 'rows' | 'readOnly'> {
|
||||
|
@ -26,6 +26,8 @@ interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElemen
|
|||
hasError?: boolean,
|
||||
/** Whether or not you can resize the teztarea */
|
||||
isResizeable?: boolean,
|
||||
/** Textarea theme. */
|
||||
theme?: 'default' | 'transparent',
|
||||
}
|
||||
|
||||
/** Textarea with custom styles. */
|
||||
|
@ -37,6 +39,7 @@ const Textarea = React.forwardRef(({
|
|||
autoGrow = false,
|
||||
maxRows = 10,
|
||||
minRows = 1,
|
||||
theme = 'default',
|
||||
...props
|
||||
}: ITextarea, ref: React.ForwardedRef<HTMLTextAreaElement>) => {
|
||||
const [rows, setRows] = useState<number>(autoGrow ? 1 : 4);
|
||||
|
@ -72,9 +75,10 @@ const Textarea = React.forwardRef(({
|
|||
ref={ref}
|
||||
rows={rows}
|
||||
onChange={handleChange}
|
||||
className={classNames({
|
||||
'bg-white dark:bg-transparent shadow-sm block w-full sm:text-sm rounded-md text-gray-900 dark:text-gray-100 placeholder:text-gray-600 dark:placeholder:text-gray-600 border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||
true,
|
||||
className={clsx('block w-full rounded-md text-gray-900 placeholder:text-gray-600 dark:text-gray-100 dark:placeholder:text-gray-600 sm:text-sm', {
|
||||
'bg-white dark:bg-transparent shadow-sm border-gray-400 dark:border-gray-800 dark:ring-1 dark:ring-gray-800 focus:ring-primary-500 focus:border-primary-500 dark:focus:ring-primary-500 dark:focus:border-primary-500':
|
||||
theme === 'default',
|
||||
'bg-transparent border-0 focus:border-0 focus:ring-0': theme === 'transparent',
|
||||
'font-mono': isCodeEditor,
|
||||
'text-red-600 border-red-600': hasError,
|
||||
'resize-none': !isResizeable,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import toast, { Toast as RHToast } from 'react-hot-toast';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
@ -40,7 +40,7 @@ const Toast = (props: IToast) => {
|
|||
return (
|
||||
<Icon
|
||||
src={require('@tabler/icons/circle-check.svg')}
|
||||
className='w-6 h-6 text-success-500 dark:text-success-400'
|
||||
className='h-6 w-6 text-success-500 dark:text-success-400'
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
|
@ -48,7 +48,7 @@ const Toast = (props: IToast) => {
|
|||
return (
|
||||
<Icon
|
||||
src={require('@tabler/icons/info-circle.svg')}
|
||||
className='w-6 h-6 text-primary-600 dark:text-accent-blue'
|
||||
className='h-6 w-6 text-primary-600 dark:text-accent-blue'
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
|
@ -56,7 +56,7 @@ const Toast = (props: IToast) => {
|
|||
return (
|
||||
<Icon
|
||||
src={require('@tabler/icons/alert-circle.svg')}
|
||||
className='w-6 h-6 text-danger-600'
|
||||
className='h-6 w-6 text-danger-600'
|
||||
aria-hidden
|
||||
/>
|
||||
);
|
||||
|
@ -102,7 +102,7 @@ const Toast = (props: IToast) => {
|
|||
<div
|
||||
data-testid='toast'
|
||||
className={
|
||||
classNames({
|
||||
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,
|
||||
'animate-enter': t.visible,
|
||||
'animate-leave': !t.visible,
|
||||
|
@ -112,7 +112,7 @@ const Toast = (props: IToast) => {
|
|||
<HStack space={4} alignItems='start'>
|
||||
<HStack space={3} justifyContent='between' alignItems='start' className='w-0 flex-1'>
|
||||
<HStack space={3} alignItems='start' className='w-0 flex-1'>
|
||||
<div className='flex-shrink-0'>
|
||||
<div className='shrink-0'>
|
||||
{renderIcon()}
|
||||
</div>
|
||||
|
||||
|
@ -126,15 +126,15 @@ const Toast = (props: IToast) => {
|
|||
</HStack>
|
||||
|
||||
{/* Dismiss Button */}
|
||||
<div className='flex flex-shrink-0 pt-0.5'>
|
||||
<div className='flex shrink-0 pt-0.5'>
|
||||
<button
|
||||
type='button'
|
||||
className='inline-flex rounded-md text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2'
|
||||
className='inline-flex rounded-md text-gray-600 hover:text-gray-700 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:text-gray-600 dark:hover:text-gray-500'
|
||||
onClick={dismissToast}
|
||||
data-testid='toast-dismiss'
|
||||
>
|
||||
<span className='sr-only'>Close</span>
|
||||
<Icon src={require('@tabler/icons/x.svg')} className='w-5 h-5' />
|
||||
<Icon src={require('@tabler/icons/x.svg')} className='h-5 w-5' />
|
||||
</button>
|
||||
</div>
|
||||
</HStack>
|
||||
|
|
|
@ -49,7 +49,7 @@ const Widget: React.FC<IWidget> = ({
|
|||
<WidgetTitle title={title} />
|
||||
{action || (onActionClick && (
|
||||
<IconButton
|
||||
className='w-6 h-6 ml-2 text-black dark:text-white rtl:rotate-180'
|
||||
className='ml-2 h-6 w-6 text-black rtl:rotate-180 dark:text-white'
|
||||
src={actionIcon}
|
||||
onClick={onActionClick}
|
||||
title={actionTitle}
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { spring } from 'react-motion';
|
||||
|
||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||
import Motion from 'soapbox/features/ui/util/optional-motion';
|
||||
import { HStack, Icon, ProgressBar, Stack, Text } from 'soapbox/components/ui';
|
||||
|
||||
interface IUploadProgress {
|
||||
/** Number between 0 and 1 to represent the percentage complete. */
|
||||
progress: number,
|
||||
/** Number between 0 and 100 to represent the percentage complete. */
|
||||
progress: number
|
||||
}
|
||||
|
||||
/** Displays a progress bar for uploading files. */
|
||||
|
@ -16,7 +14,7 @@ const UploadProgress: React.FC<IUploadProgress> = ({ progress }) => {
|
|||
<HStack alignItems='center' space={2}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/cloud-upload.svg')}
|
||||
className='w-7 h-7 text-gray-500'
|
||||
className='h-7 w-7 text-gray-500'
|
||||
/>
|
||||
|
||||
<Stack space={1}>
|
||||
|
@ -24,16 +22,7 @@ const UploadProgress: React.FC<IUploadProgress> = ({ progress }) => {
|
|||
<FormattedMessage id='upload_progress.label' defaultMessage='Uploading…' />
|
||||
</Text>
|
||||
|
||||
<div className='w-full h-1.5 rounded-lg bg-gray-200 relative'>
|
||||
<Motion defaultStyle={{ width: 0 }} style={{ width: spring(progress) }}>
|
||||
{({ width }) =>
|
||||
(<div
|
||||
className='absolute left-0 top-0 h-1.5 bg-primary-600 rounded-lg'
|
||||
style={{ width: `${width}%` }}
|
||||
/>)
|
||||
}
|
||||
</Motion>
|
||||
</div>
|
||||
<ProgressBar progress={progress / 100} size='sm' />
|
||||
</Stack>
|
||||
</HStack>
|
||||
);
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
|
||||
|
@ -141,7 +141,7 @@ const Upload: React.FC<IUpload> = ({
|
|||
|
||||
const uploadIcon = mediaType === 'unknown' && (
|
||||
<Icon
|
||||
className='h-16 w-16 mx-auto my-12 text-gray-800 dark:text-gray-200'
|
||||
className='mx-auto my-12 h-16 w-16 text-gray-800 dark:text-gray-200'
|
||||
src={MIMETYPE_ICONS[mimeType || ''] || defaultIcon}
|
||||
/>
|
||||
);
|
||||
|
@ -152,13 +152,13 @@ const Upload: React.FC<IUpload> = ({
|
|||
<Motion defaultStyle={{ scale: 0.8 }} style={{ scale: spring(1, { stiffness: 180, damping: 12 }) }}>
|
||||
{({ scale }) => (
|
||||
<div
|
||||
className={classNames('compose-form__upload-thumbnail', mediaType)}
|
||||
className={clsx('compose-form__upload-thumbnail', mediaType)}
|
||||
style={{
|
||||
transform: `scale(${scale})`,
|
||||
backgroundImage: mediaType === 'image' ? `url(${media.preview_url})` : undefined,
|
||||
backgroundPosition: typeof x === 'number' && typeof y === 'number' ? `${x}% ${y}%` : undefined }}
|
||||
>
|
||||
<div className={classNames('compose-form__upload__actions', { active })}>
|
||||
<div className={clsx('compose-form__upload__actions', { active })}>
|
||||
{onDelete && (
|
||||
<IconButton
|
||||
onClick={handleUndoClick}
|
||||
|
@ -178,7 +178,7 @@ const Upload: React.FC<IUpload> = ({
|
|||
</div>
|
||||
|
||||
{onDescriptionChange && (
|
||||
<div className={classNames('compose-form__upload-description', { active })}>
|
||||
<div className={clsx('compose-form__upload-description', { active })}>
|
||||
<label>
|
||||
<span style={{ display: 'none' }}>{intl.formatMessage(messages.description)}</span>
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
|
||||
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||
|
@ -13,7 +13,7 @@ const ValidationCheckmark = ({ isValid, text }: IValidationCheckmark) => {
|
|||
<HStack alignItems='center' space={2} data-testid='validation-checkmark'>
|
||||
<Icon
|
||||
src={isValid ? require('@tabler/icons/check.svg') : require('@tabler/icons/point.svg')}
|
||||
className={classNames({
|
||||
className={clsx({
|
||||
'w-4 h-4': true,
|
||||
'text-gray-400 dark:text-gray-600 dark:fill-gray-600 fill-gray-400': !isValid,
|
||||
'text-success-500': isValid,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React from 'react';
|
||||
import { useIntl, defineMessages } from 'react-intl';
|
||||
|
||||
|
@ -25,7 +25,7 @@ const VerificationBadge: React.FC<IVerificationBadge> = ({ className }) => {
|
|||
|
||||
return (
|
||||
<span className='verified-icon' data-testid='verified-badge'>
|
||||
<Element className={classNames('w-4 text-accent-500', className)} src={icon} alt={intl.formatMessage(messages.verified)} />
|
||||
<Element className={clsx('w-4 text-accent-500', className)} src={icon} alt={intl.formatMessage(messages.verified)} />
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -16,7 +16,7 @@ const mapStateToProps = (state: RootState) => ({
|
|||
openedViaKeyboard: state.dropdown_menu.keyboard,
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch: Dispatch, { status, items }: Partial<IDropdown>) => ({
|
||||
const mapDispatchToProps = (dispatch: Dispatch, { status, items, ...filteredProps }: Partial<IDropdown>) => ({
|
||||
onOpen(
|
||||
id: number,
|
||||
onItemClick: React.EventHandler<React.MouseEvent | React.KeyboardEvent>,
|
||||
|
@ -28,10 +28,18 @@ const mapDispatchToProps = (dispatch: Dispatch, { status, items }: Partial<IDrop
|
|||
actions: items,
|
||||
onClick: onItemClick,
|
||||
}) : openDropdownMenu(id, dropdownPlacement, keyboard));
|
||||
|
||||
if (filteredProps.onOpen) {
|
||||
filteredProps.onOpen(id, onItemClick, dropdownPlacement, keyboard);
|
||||
}
|
||||
},
|
||||
onClose(id: number) {
|
||||
dispatch(closeModal('ACTIONS'));
|
||||
dispatch(closeDropdownMenu(id));
|
||||
|
||||
if (filteredProps.onClose) {
|
||||
filteredProps.onClose(id);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use strict';
|
||||
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import classNames from 'clsx';
|
||||
import clsx from 'clsx';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Toaster } from 'react-hot-toast';
|
||||
import { IntlProvider } from 'react-intl';
|
||||
|
@ -271,7 +271,7 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
|||
const darkMode = useTheme() === 'dark';
|
||||
const themeCss = generateThemeCss(demo ? normalizeSoapboxConfig({ brandColor: '#0482d8' }) : soapboxConfig);
|
||||
|
||||
const bodyClass = classNames('bg-white dark:bg-gray-800 text-base h-full', {
|
||||
const bodyClass = clsx('h-full bg-white text-base dark:bg-gray-800', {
|
||||
'no-reduce-motion': !settings.get('reduceMotion'),
|
||||
'underline-links': settings.get('underlineLinks'),
|
||||
'demetricator': settings.get('demetricator'),
|
||||
|
@ -280,7 +280,7 @@ const SoapboxHead: React.FC<ISoapboxHead> = ({ children }) => {
|
|||
return (
|
||||
<>
|
||||
<Helmet>
|
||||
<html lang={locale} className={classNames('h-full', { dark: darkMode })} />
|
||||
<html lang={locale} className={clsx('h-full', { dark: darkMode })} />
|
||||
<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>}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Ładowanie…
Reference in New Issue