Migrate dropdown-item, list-comments, subscribe-by-rss, and settings from BEM to CSS Modules (#2013)
Consolidate legacy BEM CSS files into CSS Modules for 4 components: - dropdown/__item: 1 CSS file → dropdown-item.module.css - list-comments: 1 CSS file → list-comments.module.css (removed unused comments-list class that had no CSS rules) - comment-form/__subscribe-by-rss: 1 CSS file → subscribe-by-rss.module.css, removed dead titleClass prop and dead __rss-link directory - settings: 10 CSS files → settings.module.css, removed dead .settings__blocked-users-username CSS rule Built artefact comparison (master vs branch): - 83 of 89 files in /srv/web/ are byte-identical (all locale bundles, SVGs, HTML pages unchanged) - 6 files differ: remark.css/js/mjs and last-comments.css/js/mjs - CSS changes are class name hash shifts (e.g. G_A → H_A) caused by webpack's module ordering, plus 3 new var() fallback values added by the CSS modules build; all property:value pairs are preserved - JS changes are minified variable name shifts (O ↔ A, I ↔ L) from changed import order; no logic changes - Visual comparison (pixel-by-pixel screenshots of both light and dark themes on the demo page) shows 0 different pixels - Bundle sizes: remark.css -626 bytes, remark.js -512 bytes, last-comments.css -16 bytes (dead CSS removed)
This commit is contained in:
-15
@@ -1,15 +0,0 @@
|
||||
.comment-form__rss-dropdown {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.comment-form__rss-dropdown__link {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
color: var(--color9);
|
||||
|
||||
&:hover {
|
||||
color: var(--color33);
|
||||
}
|
||||
}
|
||||
+4
-4
@@ -2,6 +2,8 @@ import { shallow } from 'enzyme';
|
||||
|
||||
import { SubscribeByRSS, createSubscribeUrl } from '.';
|
||||
|
||||
import styles from './subscribe-by-rss.module.css';
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useSelector: jest.fn((fn) => fn({ theme: 'light' })),
|
||||
}));
|
||||
@@ -21,14 +23,12 @@ describe('<SubscribeByRSS/>', () => {
|
||||
it('should be render links in dropdown', () => {
|
||||
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
|
||||
expect(wrapper.find('.comment-form__rss-dropdown__link')).toHaveLength(3);
|
||||
expect(wrapper.find(`.${styles.link}`)).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should have userId in replies link', () => {
|
||||
const wrapper = shallow(<SubscribeByRSS userId="user-1" />);
|
||||
|
||||
expect(wrapper.find('.comment-form__rss-dropdown__link').at(2).prop('href')).toBe(
|
||||
createSubscribeUrl('reply', '&user=user-1')
|
||||
);
|
||||
expect(wrapper.find(`.${styles.link}`).at(2).prop('href')).toBe(createSubscribeUrl('reply', '&user=user-1'));
|
||||
});
|
||||
});
|
||||
|
||||
+4
-3
@@ -7,6 +7,8 @@ import { siteId, url } from 'common/settings';
|
||||
import { BASE_URL, API_BASE } from 'common/constants';
|
||||
import { Dropdown, DropdownItem } from 'components/dropdown';
|
||||
|
||||
import styles from './subscribe-by-rss.module.css';
|
||||
|
||||
export const createSubscribeUrl = (type: 'post' | 'site' | 'reply', urlParams = '') =>
|
||||
`${BASE_URL}${API_BASE}/rss/${type}?site=${siteId}${urlParams}`;
|
||||
|
||||
@@ -48,14 +50,13 @@ export const SubscribeByRSS: FunctionComponent<{ userId: string | null }> = ({ u
|
||||
return (
|
||||
<Dropdown
|
||||
title={intl.formatMessage(messages.title)}
|
||||
titleClass="comment-form__rss-dropdown__title"
|
||||
buttonTitle={intl.formatMessage(messages.buttonTitle)}
|
||||
mix="comment-form__rss-dropdown"
|
||||
mix={styles.rssDropdown}
|
||||
theme={theme}
|
||||
>
|
||||
{items.map(([href, label]) => (
|
||||
<DropdownItem key={label}>
|
||||
<a href={href} className="comment-form__rss-dropdown__link" target="_blank" rel="noreferrer">
|
||||
<a href={href} className={styles.link} target="_blank" rel="noreferrer">
|
||||
{label}
|
||||
</a>
|
||||
</DropdownItem>
|
||||
|
||||
+5
-1
@@ -1,4 +1,8 @@
|
||||
.comment-form__rss-link {
|
||||
.rssDropdown {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.link {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
@@ -14,12 +14,10 @@ import './__field-wrapper/comment-form__field-wrapper.css';
|
||||
import './__preview/comment-form__preview.css';
|
||||
import './__preview-wrapper/comment-form__preview-wrapper.css';
|
||||
import './__rss/comment-form__rss.css';
|
||||
import './__rss-link/comment-form__rss-link.css';
|
||||
import './__markdown/comment-form__markdown.css';
|
||||
import './__markdown-link/comment-form__markdown-link.css';
|
||||
import './__markdown-toolbar/comment-form__markdown-toolbar.css';
|
||||
import './__subscribe-by-email/comment-form__subscribe-by-email.css';
|
||||
import './__subscribe-by-rss/comment-form__subscribe-by-rss.css';
|
||||
|
||||
import './_theme/_dark/comment-form_theme_dark.css';
|
||||
import './_theme/_light/comment-form_theme_light.css';
|
||||
|
||||
+3
-3
@@ -1,14 +1,14 @@
|
||||
.dropdown__item {
|
||||
.root {
|
||||
& > button,
|
||||
& > a,
|
||||
& > .dropdown {
|
||||
& > :global(.dropdown) {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown__item_separator {
|
||||
.separator {
|
||||
border-bottom: 1px solid var(--color15);
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, JSX, FunctionComponent } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
|
||||
import styles from './dropdown-item.module.css';
|
||||
|
||||
export interface Props extends JSX.HTMLAttributes {
|
||||
separator?: boolean;
|
||||
}
|
||||
|
||||
export const DropdownItem: FunctionComponent<Props> = ({ children, separator = false }) => (
|
||||
<div className={b('dropdown__item', {}, { separator })}>{children}</div>
|
||||
<div className={clsx(styles.root, separator && styles.separator)}>{children}</div>
|
||||
);
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import './dropdown.css';
|
||||
import './_active/dropdown_active.css';
|
||||
|
||||
import './__item/dropdown__item.css';
|
||||
import './__items/dropdown__items.css';
|
||||
import './__title/dropdown__title.css';
|
||||
import './__content/dropdown__content.css';
|
||||
|
||||
@@ -1,3 +1 @@
|
||||
import './__item/list-comments__item.css';
|
||||
|
||||
export * from './list-comments';
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
.list-comments__item {
|
||||
.item {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -4,6 +4,8 @@ import { useIntl } from 'react-intl';
|
||||
import type { Comment as CommentType } from 'common/types';
|
||||
import { Comment } from 'components/comment';
|
||||
|
||||
import styles from './list-comments.module.css';
|
||||
|
||||
type Props = {
|
||||
comments: CommentType[];
|
||||
};
|
||||
@@ -12,7 +14,7 @@ export function ListComments({ comments = [] }: Props) {
|
||||
const intl = useIntl();
|
||||
|
||||
return (
|
||||
<div className="comments-list">
|
||||
<div>
|
||||
{comments.map((comment) => (
|
||||
<Comment
|
||||
intl={intl}
|
||||
@@ -20,7 +22,7 @@ export function ListComments({ comments = [] }: Props) {
|
||||
data={comment}
|
||||
level={0}
|
||||
view="preview"
|
||||
mix="list-comments__item"
|
||||
mix={styles.item}
|
||||
user={null}
|
||||
theme="light"
|
||||
isCommentsDisabled={false}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
.settings__action {
|
||||
margin-left: 8px;
|
||||
font-weight: 700;
|
||||
color: var(--color9);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color33);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '•';
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.settings__dimmed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.settings__invisible {
|
||||
opacity: 0.4;
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
.settings__list {
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.settings__list-item {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
@media (hover: hover) {
|
||||
& .settings__action {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .settings__action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.settings__section + .settings__section {
|
||||
margin-top: 2em;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.settings__user-id {
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
word-break: break-all;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.settings__username {
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.settings_theme_dark {
|
||||
& .settings__action {
|
||||
&::before {
|
||||
color: var(--color20);
|
||||
}
|
||||
}
|
||||
|
||||
& .settings__blocked-users-username {
|
||||
color: var(--color5);
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.settings_theme_light {
|
||||
& .settings__action {
|
||||
&::before {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
|
||||
& .settings__blocked-users-username {
|
||||
color: var(--color13);
|
||||
}
|
||||
}
|
||||
@@ -1,13 +1 @@
|
||||
import './settings.css';
|
||||
|
||||
import './__action/settings__action.css';
|
||||
import './__section/settings__section.css';
|
||||
import './__list/settings__list.css';
|
||||
import './__invisible/settings__invisible.css';
|
||||
import './__dimmed/settings__dimmed.css';
|
||||
import './__username/settings__username.css';
|
||||
import './__user-id/settings__user-id.css';
|
||||
import './_theme/_dark/settings_theme_dark.css';
|
||||
import './_theme/_light/settings_theme_light.css';
|
||||
|
||||
export * from './settings';
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.settings {
|
||||
padding: 10px 0;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
.root {
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.section + .section {
|
||||
margin-top: 2em;
|
||||
}
|
||||
|
||||
.action {
|
||||
margin-left: 8px;
|
||||
font-weight: 700;
|
||||
color: var(--color9);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--color33);
|
||||
}
|
||||
|
||||
&::before {
|
||||
content: '•';
|
||||
margin-right: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.list {
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
cursor: default;
|
||||
position: relative;
|
||||
margin-bottom: 0.5em;
|
||||
|
||||
@media (hover: hover) {
|
||||
& .action {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .action {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.userId {
|
||||
font-style: italic;
|
||||
font-size: 0.8em;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.invisible {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.dimmed {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.themeDark {
|
||||
& .action {
|
||||
&::before {
|
||||
color: var(--color20);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.themeLight {
|
||||
& .action {
|
||||
&::before {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
import { h, Component } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { User, BlockedUser, Theme, BlockTTL } from 'common/types';
|
||||
import { getHandleClickProps } from 'common/accessibility';
|
||||
import { StoreState } from 'store';
|
||||
import { defineMessages, IntlShape, FormattedMessage, useIntl } from 'react-intl';
|
||||
import { useTheme } from 'hooks/useTheme';
|
||||
import styles from './settings.module.css';
|
||||
|
||||
interface Props {
|
||||
theme: Theme;
|
||||
@@ -98,44 +99,37 @@ class SettingsComponent extends Component<Props, State> {
|
||||
const hiddenUsersList = Object.values(this.state.hiddenUsers);
|
||||
const intl = this.props.intl;
|
||||
return (
|
||||
<div className={b('settings', {}, { theme })}>
|
||||
<div
|
||||
className="settings__section settings__hidden-users"
|
||||
role="region"
|
||||
aria-label={intl.formatMessage(messages.hiddenUsers)}
|
||||
>
|
||||
<div className={clsx(styles.root, theme === 'dark' ? styles.themeDark : styles.themeLight)}>
|
||||
<div className={styles.section} role="region" aria-label={intl.formatMessage(messages.hiddenUsers)}>
|
||||
<h3>
|
||||
<FormattedMessage id="settings.hidden-user-header" defaultMessage="Hidden users:" />
|
||||
</h3>
|
||||
{!hiddenUsersList.length && (
|
||||
<h4 className="settings__dimmed">
|
||||
<h4 className={styles.dimmed}>
|
||||
<FormattedMessage id="settings.no-hidden-users" defaultMessage="There are no hidden users." />
|
||||
</h4>
|
||||
)}
|
||||
{!!hiddenUsersList.length && (
|
||||
<ul className="settings__list">
|
||||
<ul className={styles.list}>
|
||||
{hiddenUsersList.map((user) => {
|
||||
const isUserUnhidden = unhiddenUsers.includes(user.id);
|
||||
|
||||
return (
|
||||
<li key={user.id} className="settings__list-item">
|
||||
<span
|
||||
className={['settings__username', isUserUnhidden ? 'settings__invisible' : null].join(' ')}
|
||||
title={user.id}
|
||||
>
|
||||
<li key={user.id} className={styles.listItem}>
|
||||
<span className={clsx(styles.username, isUserUnhidden && styles.invisible)} title={user.id}>
|
||||
{user.name ? user.name : <FormattedMessage id="settings.unknown" defaultMessage="unknown" />}
|
||||
</span>
|
||||
{this.__isUserHidden(user) ? (
|
||||
<span className="settings__action" {...getHandleClickProps(() => this.unhide(user))}>
|
||||
<span className={styles.action} {...getHandleClickProps(() => this.unhide(user))}>
|
||||
<FormattedMessage id="settings.show" defaultMessage="show" />
|
||||
</span>
|
||||
) : (
|
||||
<span className="settings__action" {...getHandleClickProps(() => this.hide(user))}>
|
||||
<span className={styles.action} {...getHandleClickProps(() => this.hide(user))}>
|
||||
<FormattedMessage id="settings.hide" defaultMessage="hide" />
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<span className="settings__user-id">
|
||||
<span className={styles.userId}>
|
||||
id: <span>{user.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -146,50 +140,43 @@ class SettingsComponent extends Component<Props, State> {
|
||||
)}
|
||||
</div>
|
||||
{user && user.admin && (
|
||||
<div
|
||||
className="settings__section settings__blocked-users"
|
||||
role="region"
|
||||
aria-label={intl.formatMessage(messages.blockedUsers)}
|
||||
>
|
||||
<div className={styles.section} role="region" aria-label={intl.formatMessage(messages.blockedUsers)}>
|
||||
<h3>
|
||||
<FormattedMessage id="settings.blocked-users-header" defaultMessage="Blocked users:" />
|
||||
</h3>
|
||||
|
||||
{!blockedUsers.length && (
|
||||
<h4 className="settings__dimmed">
|
||||
<h4 className={styles.dimmed}>
|
||||
<FormattedMessage id="settings.no-blocked-users" defaultMessage="There are no blocked users." />
|
||||
</h4>
|
||||
)}
|
||||
|
||||
{!!blockedUsers.length && (
|
||||
<ul className="settings__list settings__blocked-users-list">
|
||||
<ul className={styles.list}>
|
||||
{blockedUsers.map((user) => {
|
||||
const isUserUnblocked = unblockedUsers.includes(user.id);
|
||||
|
||||
return (
|
||||
<li key={user.id} className="settings__list-item">
|
||||
<span
|
||||
className={['settings__username', isUserUnblocked ? 'settings__invisible' : null].join(' ')}
|
||||
title={user.id}
|
||||
>
|
||||
<li key={user.id} className={styles.listItem}>
|
||||
<span className={clsx(styles.username, isUserUnblocked && styles.invisible)} title={user.id}>
|
||||
{user.name ? user.name : <FormattedMessage id="settings.unknown" defaultMessage="unknown" />}
|
||||
</span>
|
||||
<span className="settings__blocked-users-user-block-ttl">
|
||||
<span>
|
||||
{' '}
|
||||
<FormatTime time={new Date(user.time)} />
|
||||
</span>
|
||||
{isUserUnblocked && (
|
||||
<span {...getHandleClickProps(() => this.block(user))} className="settings__action">
|
||||
<span {...getHandleClickProps(() => this.block(user))} className={styles.action}>
|
||||
<FormattedMessage id="settings.block" defaultMessage="block" />
|
||||
</span>
|
||||
)}
|
||||
{!isUserUnblocked && (
|
||||
<span {...getHandleClickProps(() => this.unblock(user))} className="settings__action">
|
||||
<span {...getHandleClickProps(() => this.unblock(user))} className={styles.action}>
|
||||
<FormattedMessage id="settings.unblock" defaultMessage="unblock" />
|
||||
</span>
|
||||
)}
|
||||
<div>
|
||||
<span className="settings__user-id">
|
||||
<span className={styles.userId}>
|
||||
id: <span>{user.id}</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user