diff --git a/CLAUDE.md b/CLAUDE.md index 431272d7..f032f95a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -22,7 +22,7 @@ - **Backend**: Formatting with golangci-lint, strict error handling - **Frontend**: TypeScript with ESLint, Stylelint and Prettier - **Imports**: Group stdlib, external packages, then internal packages -- **CSS**: CSS Modules for new components (`component.module.css`) +- **CSS**: All components use CSS Modules (`component.module.css`). Class naming: BEM block = `.root`, elements = camelCase, modifiers = camelCase. Use `clsx` for conditional class composition. `raw-content.css` is the only global CSS file (syntax highlighting utility). Root wrapper keeps bare `.dark`/`.light` theme class — 8+ module CSS files depend on `:global(.dark)` ancestor. `comment_highlighting` uses `:global()` for imperative `classList` usage in root.tsx ## Key Backend Packages - **Web/API**: `github.com/go-chi/chi/v5`, `github.com/go-pkgz/rest` diff --git a/docs/plans/completed/2026-02-24-bem-to-css-modules-batch1.md b/docs/plans/completed/2026-02-24-bem-to-css-modules-batch1.md new file mode 100644 index 00000000..3ca6d605 --- /dev/null +++ b/docs/plans/completed/2026-02-24-bem-to-css-modules-batch1.md @@ -0,0 +1,443 @@ +# BEM → CSS Modules migration, batch 1: leaf components + +## Overview +- Migrate 4 leaf BEM components to CSS Modules: **button**, **dropdown**, **thread**, **auth-panel** +- Consolidates 19 BEM CSS files into 4 CSS module files +- Cleans up dead CSS classes and unused props discovered during analysis +- Follows the pattern established in PR #2013 (batch 0: dropdown-item, list-comments, subscribe-by-rss, settings) + +## Context +- PR #2013 migrated 4 small components with zero visual regression (verified pixel-by-pixel on built artefacts) +- Key learning from batch 0: `mix` is pure string concatenation — parent and child can be migrated independently in any order +- These 4 "leaf" components are migrated first because they have no inward `mix` coupling with unmigrated parents (or the coupling is dead) +- Batch 2 (comment-form, subscribe-by-email, comment, root) follows after this lands + +## Development Approach +- **Testing approach**: Regular (run existing tests after each change; no new tests needed as these are CSS-only changes with no logic) +- Complete each task fully before moving to the next +- Make small, focused changes +- Run `cd frontend && pnpm lint` and `cd frontend && pnpm test` after each task + +## Progress Tracking +- Mark completed items with `[x]` immediately when done +- Add newly discovered tasks with ➕ prefix +- Document issues/blockers with ⚠️ prefix + +## Implementation Steps + +### Task 1: Migrate `button` to CSS Modules + +7 BEM CSS files → 1 `button.module.css`. Uses lookup objects for dynamic kind/size/theme props. + +**Files to modify:** +- `frontend/apps/remark42/app/components/button/button.tsx` +- `frontend/apps/remark42/app/components/button/index.ts` + +**Files to create:** +- `frontend/apps/remark42/app/components/button/button.module.css` + +**Files to delete (7):** +- `button/button.css` +- `button/_kind/_link/button_kind_link.css` + parent dirs +- `button/_kind/_primary/button_kind_primary.css` + parent dirs +- `button/_kind/_secondary/button_kind_secondary.css` + parent dirs +- `button/_size/_large/button_size_large.css` + parent dirs +- `button/_size/_middle/button_size_middle.css` + parent dirs +- `button/_theme/_dark/button_theme_dark.css` + parent dirs + +**Steps:** +- [x] Create `button.module.css` consolidating all 7 CSS files: + ```css + .root { + background: none; + border: 0; + padding: 0; + margin: 0; + border-radius: 4px; + font-family: inherit; + font-size: inherit; + cursor: pointer; + white-space: nowrap; + + &:focus { + box-shadow: 0 0 0 2px var(--color47); + outline: none; + } + + &:disabled { + opacity: 0.6; + cursor: default; + } + } + + .kindLink { + background: transparent; + font-weight: 600; + color: var(--color9); + + &:hover { color: var(--color33); } + &:disabled, &:hover:disabled { color: var(--color9); } + } + + .kindPrimary { + background: var(--color15); + color: var(--color6); + + &:hover { background: var(--color33); } + &:hover:disabled { background: var(--color15); } + } + + .kindSecondary { + background: var(--color6); + color: inherit; + + &:hover { box-shadow: inset 0 0 0 2px var(--color33); } + } + + .sizeMiddle { + height: 2rem; + padding: 0 12px; + } + + .sizeLarge { + height: 36px; + padding: 0 12px; + font-size: 16px; + } + + .themeDark { + &.kindSecondary { + background: var(--color8); + color: var(--color20); + } + + &.kindLink { + &:disabled, &:hover:disabled { color: var(--color6); } + } + } + ``` +- [x] Update `button.tsx`: + - Remove `import b, { Mods, Mix } from 'bem-react-helper'` + - Add `import styles from './button.module.css'` + - Add lookup objects: + ```tsx + const kindStyles: Record = { + primary: styles.kindPrimary, + secondary: styles.kindSecondary, + link: styles.kindLink, + }; + + const sizeStyles: Record = { + middle: styles.sizeMiddle, + large: styles.sizeLarge, + }; + ``` + - Remove `mods` from `ButtonProps` type and destructuring (never passed by any caller) + - Change `Mix` type import to plain `string | string[]` for `mix` prop + - Change className to: `clsx(styles.root, kind && kindStyles[kind], size && sizeStyles[size], theme === 'dark' && styles.themeDark, mix, className)` +- [x] Update `index.ts`: remove all 7 CSS imports, keep only `export { Button } from './button'` +- [x] Delete all 7 old CSS files and their BEM directories +- [x] Run `cd frontend && pnpm lint && pnpm test` — must pass before next task + +### Task 2: Migrate `dropdown` to CSS Modules + +7 BEM CSS files → 1 `dropdown.module.css`. Replaces 3 DOM class queries with refs and `data-dropdown` attribute. Removes dead `heading` prop (no callers) and dead `dropdown__heading` class (no CSS). Updates already-migrated `dropdown-item.module.css`. + +**Files to modify:** +- `frontend/apps/remark42/app/components/dropdown/dropdown.tsx` +- `frontend/apps/remark42/app/components/dropdown/index.ts` +- `frontend/apps/remark42/app/components/dropdown/__item/dropdown-item.module.css` (change `:global(.dropdown)` → `[data-dropdown]`) + +**Files to create:** +- `frontend/apps/remark42/app/components/dropdown/dropdown.module.css` + +**Files to delete (7):** +- `dropdown/dropdown.css` +- `dropdown/__content/dropdown__content.css` + dir +- `dropdown/__items/dropdown__items.css` + dir +- `dropdown/__title/dropdown__title.css` + dir +- `dropdown/_active/dropdown_active.css` + dir +- `dropdown/_theme/_dark/dropdown_theme_dark.css` + dirs +- `dropdown/_theme/_light/dropdown_theme_light.css` + dirs + +**Steps:** +- [x] Create `dropdown.module.css` consolidating all 7 CSS files: + ```css + .root { + display: inline-block; + position: relative; + } + + .content { + position: absolute; + z-index: 20; + outline-width: 0; + display: none; + top: 100%; + left: 0; + transform: translate(-0.5em, 5px); + min-width: 120px; + max-width: 260px; + border: 2px solid var(--color15); + border-radius: 3px; + padding: 0 0 5px; + } + + .items { + padding: 5px 0; + + &:last-child { padding-bottom: 0; } + } + + .title { + &::after { + content: '\25BE'; + margin-left: 2px; + } + } + + .active > .content { + display: block; + } + + .themeDark > .content { + background-color: var(--color8); + } + + .themeLight > .content { + background-color: var(--color6); + } + ``` +- [x] Update `dropdown.tsx`: + - Replace `import b from 'bem-react-helper'` with `import clsx from 'clsx'` and `import styles from './dropdown.module.css'` + - Add `contentRef = createRef()` alongside existing `rootNode` ref + - Add `data-dropdown` attribute to root div (for nested dropdown detection by parent traversal) + - Replace 3 DOM queries with ref: + - Line 78: `parent.classList.contains('dropdown')` → `parent.hasAttribute('data-dropdown')` + - Line 95: `Array.from(...).find(c => c.classList.contains('dropdown__content'))` → `this.contentRef.current` + - Line 118: `this.rootNode.current.querySelector('.dropdown__content')` → `this.contentRef.current` + - Root div: `className={clsx(styles.root, isActive && styles.active, theme === 'dark' ? styles.themeDark : styles.themeLight, mix)}` + - Content div: `className={styles.content}` with `ref={this.contentRef}` + - Items div: `className={styles.items}` + - Button mix: `mix={[styles.title, titleClass]}` (clsx in Button handles arrays) + - Remove dead `heading` prop from Props type and the heading div from JSX (prop is never passed by any caller, class has no CSS) +- [x] Update `dropdown-item.module.css`: change `& > :global(.dropdown)` to `& > [data-dropdown]` +- [x] Update `index.ts`: remove all 7 CSS imports, keep `export { Dropdown }` and `export { DropdownItem }` +- [x] Delete all 7 old CSS files and their BEM directories +- [x] Run `cd frontend && pnpm lint && pnpm test` — must pass before next task + +### Task 3: Migrate `thread` to CSS Modules + +3 BEM CSS files → 1 `thread.module.css`. Levels 0-5 had no CSS rules — only level 6 matters. The `mix` prop (receives `"root__thread"` from root component) is passed through as a plain class string. + +**Files to modify:** +- `frontend/apps/remark42/app/components/thread/thread.tsx` +- `frontend/apps/remark42/app/components/thread/index.ts` + +**Files to create:** +- `frontend/apps/remark42/app/components/thread/thread.module.css` + +**Files to delete (3):** +- `thread/thread.css` +- `thread/__collapse/thread__collapse.css` + dir +- `thread/_theme_dark/thread_theme_dark.css` + dir + +**Steps:** +- [x] Create `thread.module.css` consolidating all 3 CSS files: + ```css + .root { + position: relative; + } + + .indented { + margin-left: 17px; + } + + .level6 .level6 { + margin-left: 0; + } + + .collapse { + height: calc(100% - 50px); + width: 11px; + position: absolute; + top: 50px; + left: -4px; + cursor: pointer; + + &::after { + display: block; + content: ''; + position: absolute; + left: 5px; + top: 0; + border-left: 1px dotted var(--color35); + height: 100%; + } + + &:hover::after { + transform: translateX(-1px); + border-left: 3px solid var(--color10); + z-index: 10; + } + } + + .collapsed { + composes: collapse; + width: 18px; + height: 18px; + top: 12px; + left: 0; + display: flex; + text-align: center; + opacity: 0.8; + border-radius: 2px; + border: 1px solid; + + &::after { display: none; } + &:hover { opacity: 1; } + &:hover::after { transform: translateX(0); } + + & > div { + position: relative; + top: 6px; + left: 3px; + width: 12px; + height: 2px; + border-bottom: 2px solid; + + &::before, &::after { + content: ''; + width: 100%; + height: 2px; + border-bottom: 2px solid; + position: absolute; + top: -4px; + left: 0; + } + + &::after { top: 4px !important; } + } + } + + .themeDark { + & .collapse { + &::after { border-color: var(--color36); } + &:hover::after { border-color: var(--color6); } + } + } + ``` +- [x] Update `thread.tsx`: + - Replace `import b from 'bem-react-helper'` with `import clsx from 'clsx'` and `import styles from './thread.module.css'` + - Root div: `className={clsx(styles.root, indented && styles.indented, level === 6 && styles.level6, theme === 'dark' && styles.themeDark, mix)}` + - Collapse div: `className={collapsed ? styles.collapsed : styles.collapse}` +- [x] Update `index.ts`: remove all 3 CSS imports, keep only `export { Thread } from './thread'` +- [x] Delete all 3 old CSS files and their BEM directories +- [x] Run `cd frontend && pnpm lint && pnpm test` — must pass before next task + +### Task 4: Migrate `auth-panel` BEM remnants to CSS Modules + +2 BEM CSS files → merge into existing `auth-panel.module.css`. Removes dead global class strings alongside module classes. Has test file to update. + +**Dead code to clean up:** +- `auth-panel__pseudo-link` — no CSS rules, remove from JSX +- `auth-panel_loggedIn` / `auth-panel_theme_*` — dead mods from `b()`, no CSS rules +- `clsx('user', styles.user)` etc. — bare global strings (`'user'`, `'user-profile-button'`, `'user-avatar'`, `'user-logout-button'`) have no CSS; remove from `clsx()` + +**Files to modify:** +- `frontend/apps/remark42/app/components/auth-panel/auth-panel.tsx` +- `frontend/apps/remark42/app/components/auth-panel/auth-panel.module.css` +- `frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx` +- `frontend/apps/remark42/app/components/auth-panel/index.ts` + +**Files to delete (2):** +- `auth-panel/auth-panel.css` +- `auth-panel/__column/auth-panel__column.css` + dir + +**Steps:** +- [x] Merge BEM styles into existing `auth-panel.module.css` — add these classes after existing ones: + ```css + .root { + display: flex; + justify-content: space-between; + font-size: 14px; + line-height: 16px; + align-items: center; + } + + .column:last-child { + margin-left: 8px; + text-align: right; + } + + .columnSeparated > * + * { + position: relative; + display: inline-block; + margin-left: 20px; + + &::before { + position: absolute; + left: -15px; + display: inline-block; + width: 10px; + text-align: center; + content: '•'; + } + } + + .adminAction { } + ``` +- [x] Update `auth-panel.tsx`: + - Remove `import b from 'bem-react-helper'` + - Root div: `className={styles.root}` (drop dead `theme`/`loggedIn` mods) + - Column divs: `className={styles.column}` + - Separated column: `className={clsx(styles.column, styles.columnSeparated)}` + - Remove `className="auth-panel__pseudo-link"` from `` (dead class, no CSS) + - Clean up dual class patterns: `clsx('user', styles.user)` → `styles.user`, same for userButton/userAvatar/userLogoutButton + - Change `mix="auth-panel__admin-action"` → `className={styles.adminAction}` on both Button calls +- [x] Update `auth-panel.test.tsx`: + - Add `import styles from './auth-panel.module.css'` + - `.find('.auth-panel__admin-action')` → `` .find(`.${styles.adminAction}`) `` + - `.find('.auth-panel__column')` → `` .find(`.${styles.column}`) `` +- [x] Update `index.ts`: remove 2 CSS imports, keep `export * from './auth-panel'` +- [x] Delete `auth-panel.css` and `__column/` directory +- [x] Run `cd frontend && pnpm lint && pnpm test` — must pass before next task + +### Task 5: Verify acceptance criteria +- [x] Verify all 4 components use CSS Modules (no remaining `b()` calls in migrated files) +- [x] Run full frontend test suite: `cd frontend && pnpm test` +- [x] Run frontend linter: `cd frontend && pnpm lint` +- [x] Grep for old BEM class names to verify no remaining references to deleted CSS files +- [x] Verify `bem-react-helper` is no longer imported in any of the 4 migrated components + +## Technical Details + +### Class naming convention +- BEM block → `root` +- BEM element → camelCase of element name (`dropdown__content` → `content`, `auth-panel__column` → `column`) +- BEM modifier → camelCase (`button_kind_primary` → `kindPrimary`, `thread_theme_dark` → `themeDark`) +- Combined modifier → compound `&.` nesting (`button_theme_dark.button_kind_secondary` → `.themeDark { &.kindSecondary { ... } }`) + +### Button `mix` prop after migration +- Type changes from `Mix` (bem-react-helper) to `string | string[] | undefined` +- Passed directly to `clsx()` which handles all these types +- Callers in batch 2 (not yet migrated) continue passing BEM strings — works fine +- Already-migrated callers pass module hashes — works fine + +### Dropdown DOM query replacements +| Before | After | +|---|---| +| `classList.contains('dropdown')` | `hasAttribute('data-dropdown')` | +| `querySelector('.dropdown__content')` | `this.contentRef.current` | +| `Array.from(...).find(c => c.classList.contains('dropdown__content'))` | `this.contentRef.current` | + +### Files to delete (total: 19 CSS files + BEM directories) +- Button: 7 CSS files +- Dropdown: 7 CSS files +- Thread: 3 CSS files +- Auth-panel: 2 CSS files + +## Post-Completion +- Visual smoke test: `cd frontend && pnpm dev:app`, verify button variants, dropdown open/close, thread collapse/expand, auth-panel admin actions in both light and dark themes +- Built artefact comparison: build docker image from branch, compare against master (same method as PR #2013) +- Batch 2 (comment-form, subscribe-by-email, comment, root) as follow-up diff --git a/frontend/apps/remark42/app/components/auth-panel/__column/auth-panel__column.css b/frontend/apps/remark42/app/components/auth-panel/__column/auth-panel__column.css deleted file mode 100644 index 770215b2..00000000 --- a/frontend/apps/remark42/app/components/auth-panel/__column/auth-panel__column.css +++ /dev/null @@ -1,19 +0,0 @@ -.auth-panel__column_separated > * + * { - position: relative; - display: inline-block; - margin-left: 20px; - - &::before { - position: absolute; - left: -15px; - display: inline-block; - width: 10px; - text-align: center; - content: '•'; - } -} - -.auth-panel__column:last-child { - margin-left: 8px; - text-align: right; -} diff --git a/frontend/apps/remark42/app/components/auth-panel/auth-panel.css b/frontend/apps/remark42/app/components/auth-panel/auth-panel.css deleted file mode 100644 index 459d498b..00000000 --- a/frontend/apps/remark42/app/components/auth-panel/auth-panel.css +++ /dev/null @@ -1,7 +0,0 @@ -.auth-panel { - display: flex; - justify-content: space-between; - font-size: 14px; - line-height: 16px; - align-items: center; -} diff --git a/frontend/apps/remark42/app/components/auth-panel/auth-panel.module.css b/frontend/apps/remark42/app/components/auth-panel/auth-panel.module.css index 27f0a1e3..ada4c947 100644 --- a/frontend/apps/remark42/app/components/auth-panel/auth-panel.module.css +++ b/frontend/apps/remark42/app/components/auth-panel/auth-panel.module.css @@ -1,3 +1,35 @@ +.root { + display: flex; + justify-content: space-between; + font-size: 14px; + line-height: 16px; + align-items: center; +} + +.column:last-child { + margin-left: 8px; + text-align: right; +} + +.columnSeparated > * + * { + position: relative; + display: inline-block; + margin-left: 20px; + + &::before { + position: absolute; + left: -15px; + display: inline-block; + width: 10px; + text-align: center; + content: '•'; + } +} + +/* stylelint-disable-next-line block-no-empty -- selector-only class for test targeting */ +.adminAction { +} + .user { display: flex; align-items: center; diff --git a/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx b/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx index 5a38a644..6902508b 100644 --- a/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx +++ b/frontend/apps/remark42/app/components/auth-panel/auth-panel.test.tsx @@ -8,6 +8,7 @@ import type { User } from 'common/types'; import enMessages from 'locales/en.json'; import { AuthPanel, Props } from './auth-panel'; +import styles from './auth-panel.module.css'; const DefaultProps = { postInfo: { @@ -47,7 +48,7 @@ describe('', () => { postInfo: { ...DefaultProps.postInfo, read_only: true }, } as Props); - const adminAction = element.find('.auth-panel__admin-action'); + const adminAction = element.find(`.${styles.adminAction}`); expect(adminAction.exists()).toBe(false); }); @@ -60,7 +61,7 @@ describe('', () => { hiddenUsers: { hidden_joe: {} as User }, } as Props); - const adminAction = element.find('.auth-panel__admin-action'); + const adminAction = element.find(`.${styles.adminAction}`).first(); expect(adminAction.text()).toEqual('Show settings'); }); @@ -73,7 +74,7 @@ describe('', () => { user: { id: 'john', name: 'John' }, } as Props); - const authPanelColumn = element.find('.auth-panel__column'); + const authPanelColumn = element.find(`.${styles.column}`); expect(authPanelColumn.length).toEqual(2); @@ -89,7 +90,7 @@ describe('', () => { user: { id: 'test', admin: true, name: 'John' }, } as Props); - const adminAction = element.find('.auth-panel__admin-action').first(); + const adminAction = element.find(`.${styles.adminAction}`).first(); expect(adminAction.text()).toEqual('Show settings'); }); diff --git a/frontend/apps/remark42/app/components/auth-panel/auth-panel.tsx b/frontend/apps/remark42/app/components/auth-panel/auth-panel.tsx index 36dc1cf7..fa20e3a8 100644 --- a/frontend/apps/remark42/app/components/auth-panel/auth-panel.tsx +++ b/frontend/apps/remark42/app/components/auth-panel/auth-panel.tsx @@ -1,6 +1,5 @@ import { h, Component } from 'preact'; import { FormattedMessage, IntlShape, useIntl } from 'react-intl'; -import b from 'bem-react-helper'; import clsx from 'clsx'; import { User, Theme, PostInfo } from 'common/types'; @@ -60,18 +59,18 @@ class AuthPanelComponent extends Component { renderAuthorized = (user: User) => { return ( -
+
{' '} -
+
@@ -83,13 +82,12 @@ class AuthPanelComponent extends Component { renderThirdPartyWarning = () => { if (IS_STORAGE_AVAILABLE || !IS_THIRD_PARTY) return null; return ( -
+
{' '} { return null; } return ( -
+
); @@ -115,7 +113,7 @@ class AuthPanelComponent extends Component { return ( { : intl.formatMessage(messages.subscribed); return ( -
+
{text} -
@@ -269,7 +264,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => { */ return ( -
+
{ ); @@ -320,13 +318,7 @@ export const SubscribeByEmail: FunctionComponent = () => { const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByEmail); return ( - + ); diff --git a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.css b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/subscribe-by-email.module.css similarity index 56% rename from frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.css rename to frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/subscribe-by-email.module.css index 3a997e89..da2ea8ba 100644 --- a/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/comment-form__subscribe-by-email.css +++ b/frontend/apps/remark42/app/components/comment-form/__subscribe-by-email/subscribe-by-email.module.css @@ -1,4 +1,4 @@ -.comment-form__subscribe-by-email { +.root { display: flex; flex-wrap: wrap; flex-direction: column; @@ -9,31 +9,27 @@ text-align: left; } -.comment-form__subscribe-by-email_token { - padding-top: 0; -} - -.comment-form__subscribe-by-email_subscribed, -.comment-form__subscribe-by-email_unsubscribed { +.subscribed, +.unsubscribed { padding: 8px 12px; text-align: left; font-size: 14px; } -.comment-form__subscribe-by-email__title { +.title { margin-bottom: 12px; } -.comment-form__subscribe-by-email__button { +.button { margin-top: 10px; flex-grow: 1; } -.comment-form__subscribe-by-email__preloader { +.preloader { margin: 0 auto; } -.comment-form__subscribe-by-email__token-input { +.tokenInput { resize: vertical; border: 1px solid var(--color31); padding: 4px; @@ -49,18 +45,18 @@ } } -.comment-form__subscribe-by-email__error { +.error { margin-top: 8px; padding: 6px 8px; line-height: 1.2; } -.comment-form__subscribe-by-email_theme_dark .comment-form__subscribe-by-email__error { +.themeDark .error { background: var(--color28); color: var(--color27); } -.comment-form__subscribe-by-email_theme_light .comment-form__subscribe-by-email__error { +.themeLight .error { background: var(--color26); color: var(--color25); } diff --git a/frontend/apps/remark42/app/components/comment-form/_simple/comment-form_simple.css b/frontend/apps/remark42/app/components/comment-form/_simple/comment-form_simple.css deleted file mode 100644 index eb0b8b19..00000000 --- a/frontend/apps/remark42/app/components/comment-form/_simple/comment-form_simple.css +++ /dev/null @@ -1,3 +0,0 @@ -.comment-form_simple { - border-width: 12px; -} diff --git a/frontend/apps/remark42/app/components/comment-form/_theme/_dark/comment-form_theme_dark.css b/frontend/apps/remark42/app/components/comment-form/_theme/_dark/comment-form_theme_dark.css deleted file mode 100644 index 22ff7bc9..00000000 --- a/frontend/apps/remark42/app/components/comment-form/_theme/_dark/comment-form_theme_dark.css +++ /dev/null @@ -1,50 +0,0 @@ -.comment-form_theme_dark { - border-color: var(--color7); - background: var(--color8); /* try to fix textarea blinking in Safari */ - - & .comment-form__actions { - background: var(--color7); - } - - & .comment-form__button_type_preview { - background: var(--color8); - color: var(--color20); - } - - & .comment-form__button_type_send { - color: var(--color20); - } - - & .comment-form__error { - border-top: 8px solid var(--color7); - background: var(--color28); - color: var(--color27); - } - - & .comment-form__field { - background: var(--color8); - color: var(--color5); - } - - & .comment-form__preview { - border-color: var(--color8); - background: var(--color8); - color: var(--color20); - } - - & .comment-form__preview-wrapper { - background: var(--color7); - } - - & .comment-form__toolbar-item { - color: var(--color20); - - &:hover { - color: var(--color9); - } - } - - & .comment-form__control-panel { - background-color: var(--color7); - } -} diff --git a/frontend/apps/remark42/app/components/comment-form/_theme/_light/comment-form_theme_light.css b/frontend/apps/remark42/app/components/comment-form/_theme/_light/comment-form_theme_light.css deleted file mode 100644 index 6e373a66..00000000 --- a/frontend/apps/remark42/app/components/comment-form/_theme/_light/comment-form_theme_light.css +++ /dev/null @@ -1,38 +0,0 @@ -.comment-form_theme_light { - border-color: var(--color5); - background: var(--color6); /* try to fix textarea blinking in Safari */ - - & .comment-form__actions { - background: var(--color5); - } - - & .comment-form__button_type_preview { - background: var(--color6); - color: var(--color0); - } - - & .comment-form__button_type_send { - color: var(--color6); - } - - & .comment-form__error { - border-top: 8px solid var(--color5); - background: var(--color26); - color: var(--color25); - } - - & .comment-form__field { - background: var(--color6); - color: var(--color0); - } - - & .comment-form__preview { - border-color: var(--color5); - background: var(--color6); - color: var(--color7); - } - - & .comment-form__preview-wrapper { - background: var(--color5); - } -} diff --git a/frontend/apps/remark42/app/components/comment-form/comment-form.css b/frontend/apps/remark42/app/components/comment-form/comment-form.css deleted file mode 100644 index 13e2987c..00000000 --- a/frontend/apps/remark42/app/components/comment-form/comment-form.css +++ /dev/null @@ -1,15 +0,0 @@ -.comment-form { - position: relative; - display: block; - border-style: solid; - border-width: 6px 12px 12px 12px; - border-radius: 2px; -} - -.comment-form__dropdown_rss { - text-align: left; - - & .dropdown__content { - width: 8em; - } -} diff --git a/frontend/apps/remark42/app/components/comment-form/comment-form.module.css b/frontend/apps/remark42/app/components/comment-form/comment-form.module.css new file mode 100644 index 00000000..1974a89d --- /dev/null +++ b/frontend/apps/remark42/app/components/comment-form/comment-form.module.css @@ -0,0 +1,186 @@ +.root { + position: relative; + display: block; + border-style: solid; + border-width: 6px 12px 12px 12px; + border-radius: 2px; +} + +.simple { + border-width: 12px; +} + +.typeReply { + margin-left: 17px; + + @media (pointer: coarse) and (max-width: 768px) { + margin-left: 0; + } +} + +.controlPanel { + height: 30px; + background-color: var(--color5); +} + +.fieldWrapper { + position: relative; +} + +.field { + /* font-size * line-height * lines * vertical-padding */ + --height: calc(16px * 1.4 * 4 + 10px * 2); + + display: block; + box-sizing: border-box; + width: 100%; + height: var(--height); + min-height: var(--height); + padding: 10px 12px; + margin: 0; + font-size: 16px; + line-height: 1.4; + border: 0; + resize: none; + overflow: hidden; + backface-visibility: hidden; + transform: translateZ(0); + + &:focus { + box-shadow: 0 0 0 2px var(--color47); + border-color: var(--color15); + outline: none; + } + + &:disabled { + color: var(--color10); + } +} + +.counter { + position: absolute; + right: 4px; + bottom: 4px; + font-size: 10px; + font-weight: 700; + color: var(--color38); +} + +.error { + margin: 0; + padding: 10px 12px; + font-size: 14px; + line-height: 18px; +} + +.actions { + display: flex; + align-items: center; + padding-top: 12px; + flex-wrap: wrap; + justify-content: space-between; + min-height: 30px; + gap: 12px; +} + +.button { + margin-right: 8px; + align-self: flex-start; + + &:last-child { + margin-right: 20px; + } +} + +.rss { + font-size: 12px; + line-height: 1; +} + +.previewWrapper { + overflow: hidden; +} + +.preview { + margin-top: 8px; + padding: 7px 11px; + overflow: hidden; + font-size: 16px; + line-height: 1.2; + border: 1px dashed; + border-radius: 2px; +} + +.markdown { + margin-bottom: 5px; + font-size: 12px; +} + +.markdownLink { + font-weight: 700; + white-space: nowrap; +} + +.themeDark { + border-color: var(--color7); + background: var(--color8); + + & .actions { + background: var(--color7); + } + + & .error { + border-top: 8px solid var(--color7); + background: var(--color28); + color: var(--color27); + } + + & .field { + background: var(--color8); + color: var(--color5); + } + + & .preview { + border-color: var(--color8); + background: var(--color8); + color: var(--color20); + } + + & .previewWrapper { + background: var(--color7); + } + + & .controlPanel { + background-color: var(--color7); + } +} + +.themeLight { + border-color: var(--color5); + background: var(--color6); + + & .actions { + background: var(--color5); + } + + & .error { + border-top: 8px solid var(--color5); + background: var(--color26); + color: var(--color25); + } + + & .field { + background: var(--color6); + color: var(--color0); + } + + & .preview { + border-color: var(--color5); + background: var(--color6); + color: var(--color7); + } + + & .previewWrapper { + background: var(--color5); + } +} diff --git a/frontend/apps/remark42/app/components/comment-form/comment-form.tsx b/frontend/apps/remark42/app/components/comment-form/comment-form.tsx index 5e774032..22c6e481 100644 --- a/frontend/apps/remark42/app/components/comment-form/comment-form.tsx +++ b/frontend/apps/remark42/app/components/comment-form/comment-form.tsx @@ -1,6 +1,6 @@ import { h, Component, createRef, Fragment } from 'preact'; import { FormattedMessage, IntlShape, defineMessages } from 'react-intl'; -import b, { Mix } from 'bem-react-helper'; +import clsx from 'clsx'; import { User, Theme, Image } from 'common/types'; import { StaticStore } from 'common/static-store'; @@ -20,13 +20,14 @@ import { SubscribeByRSS } from './__subscribe-by-rss'; import { MarkdownToolbar } from './markdown-toolbar'; import { TextExpander } from './text-expander'; import { updatePersistedComments, getPersistedComment, removePersistedComment } from './comment-form.persist'; +import styles from './comment-form.module.css'; export type Props = { id: string; user: User | null; errorMessage?: string; value?: string; - mix?: Mix; + mix?: string | string[]; mode?: 'main' | 'edit' | 'reply'; theme: Theme; autofocus?: boolean; @@ -362,13 +363,13 @@ export class CommentForm extends Component { }; renderMarkdownTip = () => ( -
+
( - + {title} ), @@ -426,14 +427,13 @@ export class CommentForm extends Component { return (
{ data-testid={`commentform_${this.props.id}`} > {!isSimpleView && ( -
+
{ />
)} -
+
{ dir="auto" /> - {charactersLeft < 100 && {charactersLeft}} + {charactersLeft < 100 && {charactersLeft}}
{(isErrorShown || !!errorMessage) && (errorMessage || intl.formatMessage(messages.unexpectedError)).split('\n').map((e) => ( -

+

{e}

))} -
+
{user ? ( <>
@@ -486,20 +486,20 @@ export class CommentForm extends Component { kind="secondary" theme={theme} size="large" - mix="comment-form__button" + className={styles.button} disabled={isDisabled} onClick={this.getPreview} > )} -
{mode === 'main' && ( -
+
{this.renderMarkdownTip()} {this.renderSubscribeButtons()}
@@ -517,9 +517,9 @@ export class CommentForm extends Component { // TODO: it can be more elegant; // for example it can render full comment component here (or above textarea on mobile) !!preview && ( -
+