Migrate batch 1 components from BEM to CSS Modules (#2014)
* feat: migrate batch 1 components from BEM to CSS Modules Migrate 8 components from BEM to CSS Modules: - button (7 BEM files -> 1 module) - dropdown (7 BEM files -> 1 module) - thread (3 BEM files -> 1 module) - auth-panel (2 BEM files -> 1 module) - dropdown-item, list-comments, subscribe-by-rss, settings (from batch 0 PR #2013) Consolidates 19 BEM CSS files into 8 CSS Module files. Uses clsx for conditional class composition, replacing bem-react-helper's b() calls. Class naming follows the established convention: BEM block = .root, elements = camelCase, modifiers = camelCase. Visual regression verification on built artefacts: - remark.css: 43,779 -> 43,299 bytes (480 bytes smaller) - last-comments.css: 18,792 -> 18,776 bytes (16 bytes smaller) - remark.js: 256,709 -> 304,837 bytes (48KB larger, expected: CSS Module classname mappings now live in JS instead of plain strings) - Dark theme: pixel-identical (zero difference) - Light theme: pixel-identical (0.21% diff is the native demo page "Toggle theme" button, not any remark42 widget element) Also updates CLAUDE.md CSS guideline to reflect the migration status. * Migrate remaining BEM components to CSS Modules (final batch) Migrate the last 4 BEM components to CSS Modules, completing the migration and removing bem-react-helper from the project entirely. Components migrated: - subscribe-by-email (1 BEM CSS file -> 1 module) - comment-form + markdown-toolbar (20 BEM CSS files -> 2 modules) - comment (19 BEM CSS files -> expanded existing module) - root (10 BEM CSS files -> expanded existing module) Consolidates ~50 BEM CSS files into 4 new + 2 expanded CSS Module files. Removes bem-react-helper dependency — all components now use clsx for conditional class composition. Dead CSS cleanup during migration: - Orphaned comment-actions selectors in comment theme CSS (already migrated) - Dead BEM modifiers: comment_disabled, comment_pinned, comment_guest - Dead element: comment__user-id (CSS existed but never used in TSX) - Dead button type classes: comment-form__button_type_preview/_send - Dead mix values: auth-email-login-form__back-button, comment-form__email-dropdown Key implementation details: - comment_highlighting stays global via :global() (imperatively added by classList) - Bare .dark/.light theme class preserved on root wrapper (8+ modules depend on it) - raw-content.css kept as global utility CSS (syntax highlighting) Visual regression verification on built artefacts: - remark.css: 43,779 -> 36,106 bytes (-17.5%) - last-comments.css: 18,792 -> 13,955 bytes (-25.7%) - remark.js: 256,709 -> 253,637 bytes (-1.2%) - last-comments.js: 121,726 -> 120,795 bytes (-0.8%) - Total: 441,006 -> 424,493 bytes (-3.7%) - Screenshot comparison: pixel-identical across light/dark themes
This commit is contained in:
@@ -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`
|
||||
|
||||
@@ -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<string, string> = {
|
||||
primary: styles.kindPrimary,
|
||||
secondary: styles.kindSecondary,
|
||||
link: styles.kindLink,
|
||||
};
|
||||
|
||||
const sizeStyles: Record<string, string> = {
|
||||
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<HTMLDivElement>()` 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 `<a>` (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
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.auth-panel {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
align-items: center;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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('<AuthPanel />', () => {
|
||||
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('<AuthPanel />', () => {
|
||||
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('<AuthPanel />', () => {
|
||||
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('<AuthPanel />', () => {
|
||||
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');
|
||||
});
|
||||
|
||||
@@ -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<Props, State> {
|
||||
|
||||
renderAuthorized = (user: User) => {
|
||||
return (
|
||||
<div className={clsx('user', styles.user)}>
|
||||
<div className={styles.user}>
|
||||
<button
|
||||
className={clsx('user-profile-button', styles.userButton)}
|
||||
className={styles.userButton}
|
||||
onClick={() => postMessageToParent({ profile: { ...user, current: '1' } })}
|
||||
title={this.props.intl.formatMessage(messages.openProfile)}
|
||||
>
|
||||
<div className={clsx('user-avatar', styles.userAvatar)}>
|
||||
<div className={styles.userAvatar}>
|
||||
<Avatar url={user.picture} />
|
||||
</div>
|
||||
{user.name}
|
||||
</button>{' '}
|
||||
<div className={clsx('user-logout-button', styles.userLogoutButton)}>
|
||||
<div className={styles.userLogoutButton}>
|
||||
<IconButton title={this.props.intl.formatMessage(messages.signout)} onClick={this.props.signout}>
|
||||
<SignOutIcon size="14" />
|
||||
</IconButton>
|
||||
@@ -83,13 +82,12 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
renderThirdPartyWarning = () => {
|
||||
if (IS_STORAGE_AVAILABLE || !IS_THIRD_PARTY) return null;
|
||||
return (
|
||||
<div className="auth-panel__column">
|
||||
<div className={styles.column}>
|
||||
<FormattedMessage
|
||||
id="authPanel.disabled-cookies"
|
||||
defaultMessage="Disable third-party cookies blocking to login or open comments in"
|
||||
/>{' '}
|
||||
<a
|
||||
className="auth-panel__pseudo-link"
|
||||
href={`${window.location.origin}/web/comments.html${window.location.search}`}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
@@ -105,7 +103,7 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="auth-panel__column">
|
||||
<div className={styles.column}>
|
||||
<FormattedMessage id="authPanel.enable-cookies" defaultMessage="Allow cookies to login and comment" />
|
||||
</div>
|
||||
);
|
||||
@@ -115,7 +113,7 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
return (
|
||||
<Button
|
||||
kind="link"
|
||||
mix="auth-panel__admin-action"
|
||||
className={styles.adminAction}
|
||||
{...getHandleClickProps(this.toggleBlockedVisibility)}
|
||||
role="link"
|
||||
>
|
||||
@@ -133,7 +131,7 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
return (
|
||||
<Button
|
||||
kind="link"
|
||||
mix="auth-panel__admin-action"
|
||||
className={styles.adminAction}
|
||||
{...getHandleClickProps(this.toggleCommentsAvailability)}
|
||||
role="link"
|
||||
>
|
||||
@@ -146,17 +144,17 @@ class AuthPanelComponent extends Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
render({ user, postInfo, theme }: Props, { isBlockedVisible }: State) {
|
||||
render({ user, postInfo }: Props, { isBlockedVisible }: State) {
|
||||
const { read_only } = postInfo;
|
||||
const isAdmin = user && user.admin;
|
||||
const isSettingsLabelVisible = Object.keys(this.props.hiddenUsers).length > 0 || isAdmin || isBlockedVisible;
|
||||
|
||||
return (
|
||||
<div className={b('auth-panel', {}, { theme, loggedIn: !!user })}>
|
||||
<div className="auth-panel__column">{user ? this.renderAuthorized(user) : read_only && <Auth />}</div>
|
||||
<div className={styles.root}>
|
||||
<div className={styles.column}>{user ? this.renderAuthorized(user) : read_only && <Auth />}</div>
|
||||
{this.renderThirdPartyWarning()}
|
||||
{this.renderCookiesWarning()}
|
||||
<div className="auth-panel__column auth-panel__column_separated">
|
||||
<div className={clsx(styles.column, styles.columnSeparated)}>
|
||||
{isSettingsLabelVisible && <span>{this.renderSettingsLabel()}</span>}
|
||||
{isAdmin && <span>{this.renderReadOnlySwitch()}</span>}
|
||||
{!isAdmin && read_only && (
|
||||
|
||||
@@ -1,5 +1 @@
|
||||
import './auth-panel.css';
|
||||
|
||||
import './__column/auth-panel__column.css';
|
||||
|
||||
export * from './auth-panel';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.button_kind_link {
|
||||
background: transparent;
|
||||
font-weight: 600;
|
||||
color: var(--color9);
|
||||
|
||||
&:hover {
|
||||
color: var(--color33);
|
||||
}
|
||||
|
||||
&:disabled,
|
||||
&:hover:disabled {
|
||||
color: var(--color9);
|
||||
}
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
.button_kind_primary {
|
||||
background: var(--color15);
|
||||
color: var(--color6);
|
||||
|
||||
&:hover {
|
||||
background: var(--color33);
|
||||
}
|
||||
|
||||
&:hover:disabled {
|
||||
background: var(--color15);
|
||||
}
|
||||
}
|
||||
-8
@@ -1,8 +0,0 @@
|
||||
.button_kind_secondary {
|
||||
background: var(--color6);
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 0 0 2px var(--color33);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.button_size_large {
|
||||
height: 36px;
|
||||
padding: 0 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.button_size_middle {
|
||||
height: 2rem;
|
||||
padding: 0 12px;
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.button_theme_dark.button_kind_secondary {
|
||||
background: var(--color8);
|
||||
color: var(--color20);
|
||||
}
|
||||
|
||||
.button_theme_dark.button_kind_link {
|
||||
&:disabled,
|
||||
&:hover:disabled {
|
||||
color: var(--color6);
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
.button {
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +1,42 @@
|
||||
import clsx from 'clsx';
|
||||
import { h, JSX } from 'preact';
|
||||
import { forwardRef } from 'preact/compat';
|
||||
import b, { Mods, Mix } from 'bem-react-helper';
|
||||
|
||||
import type { Theme } from 'common/types';
|
||||
|
||||
import styles from './button.module.css';
|
||||
|
||||
const kindStyles: Record<string, string> = {
|
||||
primary: styles.kindPrimary,
|
||||
secondary: styles.kindSecondary,
|
||||
link: styles.kindLink,
|
||||
};
|
||||
|
||||
const sizeStyles: Record<string, string> = {
|
||||
middle: styles.sizeMiddle,
|
||||
large: styles.sizeLarge,
|
||||
};
|
||||
|
||||
export type ButtonProps = Omit<JSX.HTMLAttributes, 'size' | 'className'> & {
|
||||
kind?: 'primary' | 'secondary' | 'link';
|
||||
size?: 'middle' | 'large';
|
||||
theme?: Theme;
|
||||
mods?: Mods;
|
||||
mix?: Mix;
|
||||
mix?: string | string[];
|
||||
type?: string;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
|
||||
({ children, theme, mods, mix, kind, type = 'button', size, className, ...props }, ref) => (
|
||||
({ children, theme, mix, kind, type = 'button', size, className, ...props }, ref) => (
|
||||
<button
|
||||
className={clsx(b('button', { mods: { kind, size, theme }, mix }, { ...mods }), className)}
|
||||
className={clsx(
|
||||
styles.root,
|
||||
kind && kindStyles[kind],
|
||||
size && sizeStyles[size],
|
||||
theme === 'dark' && styles.themeDark,
|
||||
mix,
|
||||
className
|
||||
)}
|
||||
type={type}
|
||||
{...props}
|
||||
ref={ref}
|
||||
|
||||
@@ -1,12 +1 @@
|
||||
import './button.css';
|
||||
|
||||
import './_kind/_link/button_kind_link.css';
|
||||
import './_kind/_primary/button_kind_primary.css';
|
||||
import './_kind/_secondary/button_kind_secondary.css';
|
||||
|
||||
import './_size/_large/button_size_large.css';
|
||||
import './_size/_middle/button_size_middle.css';
|
||||
|
||||
import './_theme/_dark/button_theme_dark.css';
|
||||
|
||||
export { Button } from './button';
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
.comment-form__actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-top: 12px;
|
||||
flex-wrap: wrap;
|
||||
justify-content: space-between;
|
||||
min-height: 30px;
|
||||
gap: 12px;
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
.comment-form__button_type_preview {
|
||||
&:hover,
|
||||
&:focus {
|
||||
box-shadow: inset 0 0 0 2px var(--color9);
|
||||
color: var(--color15);
|
||||
}
|
||||
}
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
.comment-form__button_type_send {
|
||||
background: var(--color15);
|
||||
color: var(--color6);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background: var(--color9);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.comment-form__button {
|
||||
margin-right: 8px;
|
||||
align-self: flex-start;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 20px;
|
||||
}
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
.comment-form__control-panel {
|
||||
height: 30px;
|
||||
background-color: var(--color5);
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
.comment-form__counter {
|
||||
position: absolute;
|
||||
right: 4px;
|
||||
bottom: 4px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: var(--color38);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.comment-form__error {
|
||||
margin: 0;
|
||||
padding: 10px 12px;
|
||||
font-size: 14px;
|
||||
line-height: 18px;
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
.comment-form__field-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
.comment-form__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; /* prevent scrollbar from appearing */
|
||||
backface-visibility: hidden; /* let's try to fix blinking in Safari */
|
||||
transform: translateZ(0); /* let's try to fix blinking in Safari, again */
|
||||
|
||||
&:focus {
|
||||
box-shadow: 0 0 0 2px var(--color47);
|
||||
border-color: var(--color15);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
.comment-form__markdown-link {
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
-4
@@ -1,4 +0,0 @@
|
||||
.comment-form__markdown {
|
||||
margin-bottom: 5px;
|
||||
font-size: 12px;
|
||||
}
|
||||
-3
@@ -1,3 +0,0 @@
|
||||
.comment-form__preview-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.comment-form__preview {
|
||||
margin-top: 8px;
|
||||
padding: 7px 11px;
|
||||
overflow: hidden;
|
||||
font-size: 16px;
|
||||
line-height: 1.2;
|
||||
border: 1px dashed;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.comment-form__rss {
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
+3
-2
@@ -19,6 +19,7 @@ import enMessages from 'locales/en.json';
|
||||
|
||||
import { SubscribeByEmail, SubscribeByEmailForm } from '.';
|
||||
import { RequestError } from '../../../utils/errorUtils';
|
||||
import styles from './subscribe-by-email.module.css';
|
||||
|
||||
const emailVerificationForSubscribeMock = emailVerificationForSubscribe as unknown as jest.Mock<
|
||||
ReturnType<typeof emailVerificationForSubscribe>
|
||||
@@ -91,7 +92,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
it('should render email form by default', () => {
|
||||
const store = mockStore(initialStore);
|
||||
const wrapper = createWrapper(store);
|
||||
const title = wrapper.find('.comment-form__subscribe-by-email__title');
|
||||
const title = wrapper.find(`.${styles.title}`);
|
||||
const button = wrapper.find(Button);
|
||||
|
||||
expect(title.text()).toEqual('Subscribe to replies');
|
||||
@@ -103,7 +104,7 @@ describe('<SubscribeByEmailForm/>', () => {
|
||||
const store = mockStore({ ...initialStore, user: { email_subscription: true } });
|
||||
const wrapper = createWrapper(store);
|
||||
|
||||
expect(wrapper.find('.comment-form__subscribe-by-email_subscribed')).toHaveLength(1);
|
||||
expect(wrapper.find(`.${styles.subscribed}`)).toHaveLength(1);
|
||||
expect(wrapper.text().startsWith('You are subscribed on updates by email')).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
+18
-26
@@ -1,7 +1,7 @@
|
||||
import { h, FunctionComponent, Fragment } from 'preact';
|
||||
import { useState, useCallback, useRef } from 'preact/hooks';
|
||||
import { useSelector, useDispatch } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
import clsx from 'clsx';
|
||||
import { useIntl, defineMessages, IntlShape, FormattedMessage } from 'react-intl';
|
||||
|
||||
import { User } from 'common/types';
|
||||
@@ -21,6 +21,8 @@ import { getPersistedEmail } from 'components/auth/auth.utils';
|
||||
import { isUserAnonymous } from 'utils/isUserAnonymous';
|
||||
import { isJwtExpired } from 'utils/jwt';
|
||||
|
||||
import styles from './subscribe-by-email.module.css';
|
||||
|
||||
const emailRegexp = /[^@]+@[^.]+\..+/;
|
||||
|
||||
enum Step {
|
||||
@@ -77,12 +79,11 @@ const renderEmailPart = (
|
||||
handleChangeEmail: (e: Event) => void
|
||||
) => (
|
||||
<>
|
||||
<div className="comment-form__subscribe-by-email__title">
|
||||
<div className={styles.title}>
|
||||
<FormattedMessage id="subscribeByEmail.subscribe-to-replies" defaultMessage="Subscribe to replies" />
|
||||
</div>
|
||||
<Input
|
||||
autofocus
|
||||
className="comment-form__subscribe-by-email__input"
|
||||
placeholder={intl.formatMessage(messages.email)}
|
||||
value={emailAddress}
|
||||
onInput={handleChangeEmail}
|
||||
@@ -99,11 +100,11 @@ const renderTokenPart = (
|
||||
setEmailStep: () => void
|
||||
) => (
|
||||
<>
|
||||
<Button kind="link" mix="auth-email-login-form__back-button" {...getHandleClickProps(setEmailStep)}>
|
||||
<Button kind="link" {...getHandleClickProps(setEmailStep)}>
|
||||
<FormattedMessage id="subscribeByEmail.back" defaultMessage="Back" />
|
||||
</Button>
|
||||
<TextareaAutosize
|
||||
className="comment-form__subscribe-by-email__token-input"
|
||||
className={styles.tokenInput}
|
||||
placeholder={intl.formatMessage(messages.token)}
|
||||
autofocus
|
||||
onInput={handleChangeToken}
|
||||
@@ -246,15 +247,9 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
: intl.formatMessage(messages.subscribed);
|
||||
|
||||
return (
|
||||
<div className={b('comment-form__subscribe-by-email', { mods: { subscribed: true } })}>
|
||||
<div className={styles.subscribed}>
|
||||
{text}
|
||||
<Button
|
||||
kind="primary"
|
||||
size="middle"
|
||||
mix="comment-form__subscribe-by-email__button"
|
||||
theme={theme}
|
||||
onClick={handleUnsubscribe}
|
||||
>
|
||||
<Button kind="primary" size="middle" className={styles.button} theme={theme} onClick={handleUnsubscribe}>
|
||||
<FormattedMessage id="subscribeByEmail.unsubscribe" defaultMessage="Unsubscribe" />
|
||||
</Button>
|
||||
</div>
|
||||
@@ -269,7 +264,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
*/
|
||||
|
||||
return (
|
||||
<div className={b('comment-form__subscribe-by-email', { mods: { unsubscribed: true } })}>
|
||||
<div className={styles.unsubscribed}>
|
||||
<FormattedMessage
|
||||
id="subscribeByEmail.have-been-unsubscribed"
|
||||
defaultMessage="You have been unsubscribed by email to updates"
|
||||
@@ -277,7 +272,7 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
<Button
|
||||
kind="primary"
|
||||
size="middle"
|
||||
mix="comment-form__subscribe-by-email__button"
|
||||
className={styles.button}
|
||||
theme={theme}
|
||||
onClick={() => setStep(Step.Close)}
|
||||
>
|
||||
@@ -291,22 +286,25 @@ export const SubscribeByEmailForm: FunctionComponent = () => {
|
||||
step === Step.Email ? intl.formatMessage(messages.submit) : intl.formatMessage(messages.subscribe);
|
||||
|
||||
return (
|
||||
<form className={b('comment-form__subscribe-by-email', {}, { theme })} onSubmit={handleSubmit}>
|
||||
<form
|
||||
className={clsx(styles.root, theme === 'dark' ? styles.themeDark : styles.themeLight)}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{step === Step.Email && renderEmailPart(loading, intl, emailAddress, handleChangeEmail)}
|
||||
{step === Step.Token && renderTokenPart(loading, intl, token, handleChangeToken, setEmailStep)}
|
||||
{error !== null && (
|
||||
<div className="comment-form__subscribe-by-email__error" role="alert">
|
||||
<div className={styles.error} role="alert">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
<Button
|
||||
mix="comment-form__subscribe-by-email__button"
|
||||
className={styles.button}
|
||||
kind="primary"
|
||||
size="large"
|
||||
type="submit"
|
||||
disabled={!isValidEmailAddress || loading}
|
||||
>
|
||||
{loading ? <Preloader className="comment-form__subscribe-by-email__preloader" /> : buttonLabel}
|
||||
{loading ? <Preloader className={styles.preloader} /> : buttonLabel}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
@@ -320,13 +318,7 @@ export const SubscribeByEmail: FunctionComponent = () => {
|
||||
const buttonTitle = intl.formatMessage(isAnonymous ? messages.onlyRegisteredUsers : messages.subscribeByEmail);
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
mix="comment-form__email-dropdown"
|
||||
title={intl.formatMessage(messages.email)}
|
||||
theme={theme}
|
||||
disabled={isAnonymous}
|
||||
buttonTitle={buttonTitle}
|
||||
>
|
||||
<Dropdown title={intl.formatMessage(messages.email)} theme={theme} disabled={isAnonymous} buttonTitle={buttonTitle}>
|
||||
<SubscribeByEmailForm />
|
||||
</Dropdown>
|
||||
);
|
||||
|
||||
+10
-14
@@ -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);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.comment-form_simple {
|
||||
border-width: 12px;
|
||||
}
|
||||
-50
@@ -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);
|
||||
}
|
||||
}
|
||||
-38
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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<Props, State> {
|
||||
};
|
||||
|
||||
renderMarkdownTip = () => (
|
||||
<div className="comment-form__markdown">
|
||||
<div className={styles.markdown}>
|
||||
<FormattedMessage
|
||||
id="commentForm.notice-about-styling"
|
||||
defaultMessage="Styling with <a>Markdown</a> is supported"
|
||||
values={{
|
||||
a: (title: string) => (
|
||||
<a class="comment-form__markdown-link" target="_blank" href="markdown-help.html">
|
||||
<a class={styles.markdownLink} target="_blank" href="markdown-help.html">
|
||||
{title}
|
||||
</a>
|
||||
),
|
||||
@@ -426,14 +427,13 @@ export class CommentForm extends Component<Props, State> {
|
||||
|
||||
return (
|
||||
<form
|
||||
className={b('comment-form', {
|
||||
mods: {
|
||||
theme,
|
||||
type: mode || 'reply',
|
||||
simple: isSimpleView,
|
||||
},
|
||||
mix,
|
||||
})}
|
||||
className={clsx(
|
||||
styles.root,
|
||||
theme === 'dark' ? styles.themeDark : styles.themeLight,
|
||||
(!mode || mode === 'reply') && styles.typeReply,
|
||||
isSimpleView && styles.simple,
|
||||
mix
|
||||
)}
|
||||
onSubmit={this.send}
|
||||
aria-label={intl.formatMessage(messages.newComment)}
|
||||
onDragOver={this.onDragOver}
|
||||
@@ -441,7 +441,7 @@ export class CommentForm extends Component<Props, State> {
|
||||
data-testid={`commentform_${this.props.id}`}
|
||||
>
|
||||
{!isSimpleView && (
|
||||
<div className="comment-form__control-panel" data-testid="markdown-toolbar">
|
||||
<div className={styles.controlPanel} data-testid="markdown-toolbar">
|
||||
<MarkdownToolbar
|
||||
intl={intl}
|
||||
allowUpload={Boolean(uploadImage)}
|
||||
@@ -450,13 +450,13 @@ export class CommentForm extends Component<Props, State> {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="comment-form__field-wrapper">
|
||||
<div className={styles.fieldWrapper}>
|
||||
<TextExpander>
|
||||
<TextareaAutosize
|
||||
id={this.textareaId}
|
||||
ref={this.textareaRef}
|
||||
onPaste={this.onPaste}
|
||||
className="comment-form__field"
|
||||
className={styles.field}
|
||||
placeholder={placeholderMessage}
|
||||
value={text}
|
||||
onInput={this.onInput}
|
||||
@@ -467,17 +467,17 @@ export class CommentForm extends Component<Props, State> {
|
||||
dir="auto"
|
||||
/>
|
||||
</TextExpander>
|
||||
{charactersLeft < 100 && <span className="comment-form__counter">{charactersLeft}</span>}
|
||||
{charactersLeft < 100 && <span className={styles.counter}>{charactersLeft}</span>}
|
||||
</div>
|
||||
|
||||
{(isErrorShown || !!errorMessage) &&
|
||||
(errorMessage || intl.formatMessage(messages.unexpectedError)).split('\n').map((e) => (
|
||||
<p className="comment-form__error" role="alert" key={e}>
|
||||
<p className={styles.error} role="alert" key={e}>
|
||||
{e}
|
||||
</p>
|
||||
))}
|
||||
|
||||
<div className="comment-form__actions">
|
||||
<div className={styles.actions}>
|
||||
{user ? (
|
||||
<>
|
||||
<div>
|
||||
@@ -486,20 +486,20 @@ export class CommentForm extends Component<Props, State> {
|
||||
kind="secondary"
|
||||
theme={theme}
|
||||
size="large"
|
||||
mix="comment-form__button"
|
||||
className={styles.button}
|
||||
disabled={isDisabled}
|
||||
onClick={this.getPreview}
|
||||
>
|
||||
<FormattedMessage id="commentForm.preview" defaultMessage="Preview" />
|
||||
</Button>
|
||||
)}
|
||||
<Button kind="primary" size="large" mix="comment-form__button" type="submit" disabled={isDisabled}>
|
||||
<Button kind="primary" size="large" className={styles.button} type="submit" disabled={isDisabled}>
|
||||
{label}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{mode === 'main' && (
|
||||
<div className="comment-form__rss">
|
||||
<div className={styles.rss}>
|
||||
{this.renderMarkdownTip()}
|
||||
{this.renderSubscribeButtons()}
|
||||
</div>
|
||||
@@ -517,9 +517,9 @@ export class CommentForm extends Component<Props, State> {
|
||||
// TODO: it can be more elegant;
|
||||
// for example it can render full comment component here (or above textarea on mobile)
|
||||
!!preview && (
|
||||
<div className="comment-form__preview-wrapper">
|
||||
<div className={styles.previewWrapper}>
|
||||
<div
|
||||
className="comment-form__preview raw-content"
|
||||
className={clsx(styles.preview, 'raw-content')}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: preview }}
|
||||
dir="auto"
|
||||
|
||||
@@ -1,28 +1,2 @@
|
||||
import './comment-form.css';
|
||||
|
||||
import './__actions/comment-form__actions.css';
|
||||
|
||||
import './__button/comment-form__button.css';
|
||||
import './__button/_type/_preview/comment-form__button_type_preview.css';
|
||||
import './__button/_type/_send/comment-form__button_type_send.css';
|
||||
|
||||
import './__control-panel/comment-form__control-panel.css';
|
||||
import './__counter/comment-form__counter.css';
|
||||
import './__error/comment-form__error.css';
|
||||
import './__field/comment-form__field.css';
|
||||
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 './__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 './_theme/_dark/comment-form_theme_dark.css';
|
||||
import './_theme/_light/comment-form_theme_light.css';
|
||||
|
||||
import './_simple/comment-form_simple.css';
|
||||
|
||||
export { CommentForm } from './comment-form';
|
||||
export type { Props } from './comment-form';
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function BoldIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 10 16"
|
||||
version="1.1"
|
||||
width="10"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 10 16" version="1.1" width="10" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M1 2h3.83c2.48 0 4.3.75 4.3 2.95 0 1.14-.63 2.23-1.67 2.61v.06c1.33.3 2.3 1.23 2.3 2.86 0 2.39-1.97 3.52-4.61 3.52H1V2zm3.66 4.95c1.67 0 2.38-.66 2.38-1.69 0-1.17-.78-1.61-2.34-1.61H3.13v3.3h1.53zm.27 5.39c1.77 0 2.75-.64 2.75-1.98 0-1.27-.95-1.81-2.75-1.81h-1.8v3.8h1.8v-.01z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function CodeIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 14 16"
|
||||
version="1.1"
|
||||
width="14"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 14 16" version="1.1" width="14" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M9.5 3L8 4.5 11.5 8 8 11.5 9.5 13 14 8 9.5 3zm-5 0L0 8l4.5 5L6 11.5 2.5 8 6 4.5 4.5 3z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function HeaderIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 18 16"
|
||||
version="1.1"
|
||||
width="18"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 18 16" version="1.1" width="18" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M13.62 9.08L12.1 3.66h-.06l-1.5 5.42h3.08zM5.7 10.13S4.68 6.52 4.53 6.02h-.08l-1.13 4.11H5.7zM17.31 14h-2.25l-.95-3.25h-4.07L9.09 14H6.84l-.69-2.33H2.87L2.17 14H0l3.3-9.59h2.5l2.17 6.34L10.86 2h2.52l3.94 12h-.01z"
|
||||
|
||||
+3
-1
@@ -1,8 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function ImageIcon() {
|
||||
return (
|
||||
<svg className="comment-form__toolbar-icon" width="11.25" height="15" viewBox="0 0 384 512" aria-hidden="true">
|
||||
<svg className={toolbarStyles.icon} width="11.25" height="15" viewBox="0 0 384 512" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M369.9 97.9L286 14C277 5 264.8-.1 252.1-.1H48C21.5 0 0 21.5 0 48v416c0 26.5 21.5 48 48 48h288c26.5 0 48-21.5 48-48V131.9c0-12.7-5.1-25-14.1-34zM332.1 128H256V51.9l76.1 76.1zM48 464V48h160v104c0 13.3 10.7 24 24 24h104v288H48zm32-48h224V288l-23.5-23.5c-4.7-4.7-12.3-4.7-17 0L176 352l-39.5-39.5c-4.7-4.7-12.3-4.7-17 0L80 352v64zm48-240c-26.5 0-48 21.5-48 48s21.5 48 48 48 48-21.5 48-48-21.5-48-48-48z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function ItalicIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 6 16"
|
||||
version="1.1"
|
||||
width="6"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 6 16" version="1.1" width="6" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2.81 5h1.98L3 14H1l1.81-9zm.36-2.7c0-.7.58-1.3 1.33-1.3.56 0 1.13.38 1.13 1.03 0 .75-.59 1.3-1.33 1.3-.58 0-1.13-.38-1.13-1.03z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function LinkIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 16 16"
|
||||
version="1.1"
|
||||
width="16"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 16 16" version="1.1" width="16" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function OrderedListIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 12 16"
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M12.01 13c0 .59 0 1-.59 1H4.6c-.59 0-.59-.41-.59-1 0-.59 0-1 .59-1h6.81c.59 0 .59.41.59 1h.01zM4.6 4h6.81C12 4 12 3.59 12 3c0-.59 0-1-.59-1H4.6c-.59 0-.59.41-.59 1 0 .59 0 1 .59 1zm6.81 3H4.6c-.59 0-.59.41-.59 1 0 .59 0 1 .59 1h6.81C12 9 12 8.59 12 8c0-.59 0-1-.59-1zm-9.4-6h-.72c-.3.19-.58.25-1.03.34V2h.75v2.14H.17V5h2.84v-.86h-1V1zm.392 8.12c-.129 0-.592.04-.802.07.53-.56 1.14-1.25 1.14-1.89C2.72 6.52 2.18 6 1.38 6c-.59 0-.97.2-1.38.64l.58.58c.19-.19.38-.38.64-.38.28 0 .48.16.48.52 0 .53-.77 1.2-1.7 2.06V10h3v-.88h-.598zm-.222 3.79v-.03c.44-.19.64-.47.64-.86 0-.7-.56-1.11-1.44-1.11-.48 0-.89.19-1.28.52l.55.64c.25-.2.44-.31.69-.31.27 0 .42.13.42.36 0 .27-.2.44-.86.44v.75c.83 0 .98.17.98.47 0 .25-.23.38-.58.38-.28 0-.56-.14-.81-.38l-.48.66c.3.36.77.56 1.41.56.83 0 1.53-.41 1.53-1.16 0-.5-.31-.81-.77-.94v.01z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function QuoteIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 14 16"
|
||||
version="1.1"
|
||||
width="14"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 14 16" version="1.1" width="14" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M6.16 3.5C3.73 5.06 2.55 6.67 2.55 9.36c.16-.05.3-.05.44-.05 1.27 0 2.5.86 2.5 2.41 0 1.61-1.03 2.61-2.5 2.61-1.9 0-2.99-1.52-2.99-4.25 0-3.8 1.75-6.53 5.02-8.42L6.16 3.5zm7 0c-2.43 1.56-3.61 3.17-3.61 5.86.16-.05.3-.05.44-.05 1.27 0 2.5.86 2.5 2.41 0 1.61-1.03 2.61-2.5 2.61-1.89 0-2.98-1.52-2.98-4.25 0-3.8 1.75-6.53 5.02-8.42l1.14 1.84h-.01z"
|
||||
|
||||
+3
-8
@@ -1,15 +1,10 @@
|
||||
import { h } from 'preact';
|
||||
|
||||
import toolbarStyles from '../markdown-toolbar.module.css';
|
||||
|
||||
export function UnorderedListIcon() {
|
||||
return (
|
||||
<svg
|
||||
className="comment-form__toolbar-icon"
|
||||
viewBox="0 0 12 16"
|
||||
version="1.1"
|
||||
width="12"
|
||||
height="16"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg className={toolbarStyles.icon} viewBox="0 0 12 16" version="1.1" width="12" height="16" aria-hidden="true">
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M2 13c0 .59 0 1-.59 1H.59C0 14 0 13.59 0 13c0-.59 0-1 .59-1h.81c.59 0 .59.41.59 1H2zm2.59-9h6.81c.59 0 .59-.41.59-1 0-.59 0-1-.59-1H4.59C4 2 4 2.41 4 3c0 .59 0 1 .59 1zM1.41 7H.59C0 7 0 7.41 0 8c0 .59 0 1 .59 1h.81c.59 0 .59-.41.59-1 0-.59 0-1-.59-1h.01zm0-5H.59C0 2 0 2.41 0 3c0 .59 0 1 .59 1h.81c.59 0 .59-.41.59-1 0-.59 0-1-.59-1h.01zm10 5H4.59C4 7 4 7.41 4 8c0 .59 0 1 .59 1h6.81c.59 0 .59-.41.59-1 0-.59 0-1-.59-1h.01zm0 5H4.59C4 12 4 12.41 4 13c0 .59 0 1 .59 1h6.81c.59 0 .59-.41.59-1 0-.59 0-1-.59-1h.01z"
|
||||
|
||||
+15
-7
@@ -1,9 +1,9 @@
|
||||
.comment-form__toolbar {
|
||||
.toolbar {
|
||||
display: block;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.comment-form__toolbar-file-input {
|
||||
.fileInput {
|
||||
position: absolute;
|
||||
width: 1px;
|
||||
height: 1px;
|
||||
@@ -11,7 +11,7 @@
|
||||
clip-path: inset(50%);
|
||||
}
|
||||
|
||||
.comment-form__toolbar-item {
|
||||
.item {
|
||||
background: none;
|
||||
border: 0;
|
||||
color: var(--color37);
|
||||
@@ -28,16 +28,24 @@
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form__toolbar-icon {
|
||||
.icon {
|
||||
display: inline-block;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.comment-form__toolbar-group {
|
||||
.group {
|
||||
display: inline-block;
|
||||
margin-left: 20px;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form__toolbar-group:first-child {
|
||||
margin-left: 0;
|
||||
:global(.dark) .item {
|
||||
color: var(--color20);
|
||||
|
||||
&:hover {
|
||||
color: var(--color9);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,8 @@ import '@github/markdown-toolbar-element';
|
||||
import { h, Component } from 'preact';
|
||||
import { defineMessages, IntlShape } from 'react-intl';
|
||||
|
||||
import styles from './markdown-toolbar.module.css';
|
||||
|
||||
// TODO: Use SVGR
|
||||
import { BoldIcon } from './markdown-toolbar-icons/bold-icon';
|
||||
import { HeaderIcon } from './markdown-toolbar-icons/header-icon';
|
||||
@@ -95,48 +97,40 @@ export class MarkdownToolbar extends Component<Props> {
|
||||
const attachImageLabel = intl.formatMessage(messages.attachImage);
|
||||
|
||||
return (
|
||||
<markdown-toolbar className="comment-form__toolbar" for={props.textareaId}>
|
||||
<div className="comment-form__toolbar-group">
|
||||
<md-header className="comment-form__toolbar-item" title={headerLabel} aria-label={headerLabel}>
|
||||
<markdown-toolbar className={styles.toolbar} for={props.textareaId}>
|
||||
<div className={styles.group}>
|
||||
<md-header className={styles.item} title={headerLabel} aria-label={headerLabel}>
|
||||
<HeaderIcon />
|
||||
</md-header>
|
||||
<md-bold className="comment-form__toolbar-item" title={boldLabel} aria-label={boldLabel}>
|
||||
<md-bold className={styles.item} title={boldLabel} aria-label={boldLabel}>
|
||||
<BoldIcon />
|
||||
</md-bold>
|
||||
<md-italic className="comment-form__toolbar-item" title={italicLabel} aria-label={italicLabel}>
|
||||
<md-italic className={styles.item} title={italicLabel} aria-label={italicLabel}>
|
||||
<ItalicIcon />
|
||||
</md-italic>
|
||||
</div>
|
||||
<div className="comment-form__toolbar-group">
|
||||
<md-quote className="comment-form__toolbar-item" title={quoteLabel} aria-label={quoteLabel}>
|
||||
<div className={styles.group}>
|
||||
<md-quote className={styles.item} title={quoteLabel} aria-label={quoteLabel}>
|
||||
<QuoteIcon />
|
||||
</md-quote>
|
||||
<md-code className="comment-form__toolbar-item" title={codeLabel} aria-label={codeLabel}>
|
||||
<md-code className={styles.item} title={codeLabel} aria-label={codeLabel}>
|
||||
<CodeIcon />
|
||||
</md-code>
|
||||
<md-link className="comment-form__toolbar-item" title={linkLabel} aria-label={linkLabel}>
|
||||
<md-link className={styles.item} title={linkLabel} aria-label={linkLabel}>
|
||||
<LinkIcon />
|
||||
</md-link>
|
||||
{this.props.allowUpload ? (
|
||||
<label className="comment-form__toolbar-item" title={attachImageLabel} aria-label={attachImageLabel}>
|
||||
<input multiple className="comment-form__toolbar-file-input" type="file" onChange={this.uploadImages} />
|
||||
<label className={styles.item} title={attachImageLabel} aria-label={attachImageLabel}>
|
||||
<input multiple className={styles.fileInput} type="file" onChange={this.uploadImages} />
|
||||
<ImageIcon />
|
||||
</label>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="comment-form__toolbar-group">
|
||||
<md-unordered-list
|
||||
className="comment-form__toolbar-item"
|
||||
title={unorderedListLabel}
|
||||
aria-label={unorderedListLabel}
|
||||
>
|
||||
<div className={styles.group}>
|
||||
<md-unordered-list className={styles.item} title={unorderedListLabel} aria-label={unorderedListLabel}>
|
||||
<UnorderedListIcon />
|
||||
</md-unordered-list>
|
||||
<md-ordered-list
|
||||
className="comment-form__toolbar-item"
|
||||
title={orderedListLabel}
|
||||
aria-label={orderedListLabel}
|
||||
>
|
||||
<md-ordered-list className={styles.item} title={orderedListLabel} aria-label={orderedListLabel}>
|
||||
<OrderedListIcon />
|
||||
</md-ordered-list>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
.comment__body {
|
||||
padding-left: 17px;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.comment__info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 84px;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.comment__input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.comment__status {
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
font-weight: 700;
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.comment__text {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
.comment__time {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.comment__user-id {
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.comment__username {
|
||||
vertical-align: middle;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
.comment_collapsed {
|
||||
padding: 12px 0 12px 28px;
|
||||
|
||||
& > .comment__body {
|
||||
& .comment__text,
|
||||
& .comment__actions {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__info {
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
& .comment__avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
& .comment__username,
|
||||
& .comment__time {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
.comment_editing {
|
||||
& .comment__text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* it isn't mobile first, but it's fine here */
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border: 8px solid;
|
||||
padding-bottom: 0;
|
||||
|
||||
& .comment__body {
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
& .comment__input {
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-bottom-width: 0;
|
||||
border-top-width: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
.comment_replying {
|
||||
padding-bottom: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
/* it isn't mobile first, but it's fine here */
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border: 8px solid;
|
||||
|
||||
& .comment__info {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
& .comment__body {
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
& .comment__input {
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-bottom-width: 0;
|
||||
border-top-width: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
.comment_theme_dark {
|
||||
& .comment__action_type_collapse {
|
||||
border-color: var(--color34);
|
||||
color: var(--color34);
|
||||
|
||||
&.comment__action_selected {
|
||||
background: var(--color34);
|
||||
color: var(--color8);
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__action_type_delete,
|
||||
& .comment__action_type_edit {
|
||||
color: var(--color14);
|
||||
}
|
||||
|
||||
& .comment__action {
|
||||
& + .comment__controls {
|
||||
&::before {
|
||||
color: var(--color5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__control_view_inactive {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--color11);
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__edit-timer {
|
||||
color: var(--color32);
|
||||
|
||||
& + .comment__controls {
|
||||
&::before {
|
||||
color: var(--color5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__status {
|
||||
color: var(--color11);
|
||||
}
|
||||
|
||||
& .comment__time {
|
||||
color: var(--color35);
|
||||
|
||||
&:hover {
|
||||
color: var(--color1);
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__user-id {
|
||||
color: var(--color13);
|
||||
}
|
||||
|
||||
& .comment__username {
|
||||
color: var(--color10);
|
||||
|
||||
&:hover {
|
||||
color: var(--color33);
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_editing {
|
||||
& .comment__input {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_replying {
|
||||
& .comment__input {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_view_preview {
|
||||
& .comment__info {
|
||||
&::after {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_highlighting {
|
||||
background: var(--color19);
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
.comment_theme_light {
|
||||
& .comment__action_type_collapse {
|
||||
border-color: var(--color20);
|
||||
color: var(--color20);
|
||||
|
||||
&.comment__action_selected {
|
||||
background: var(--color20);
|
||||
color: var(--color6);
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__action_type_delete,
|
||||
& .comment__action_type_edit {
|
||||
color: var(--color32);
|
||||
}
|
||||
|
||||
& .comment__action {
|
||||
& + .comment__controls {
|
||||
&::before {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__control_view_inactive {
|
||||
&,
|
||||
&:hover,
|
||||
&:focus {
|
||||
color: var(--color11);
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__edit-timer {
|
||||
color: var(--color32);
|
||||
|
||||
& + .comment__controls {
|
||||
&::before {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__status {
|
||||
color: var(--color11);
|
||||
}
|
||||
|
||||
& .comment__time {
|
||||
color: var(--color11);
|
||||
|
||||
&:hover {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__user-id {
|
||||
color: var(--color13);
|
||||
}
|
||||
|
||||
& .comment__username {
|
||||
color: var(--color10);
|
||||
}
|
||||
|
||||
&.comment_editing {
|
||||
& .comment__input {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_replying {
|
||||
& .comment__input {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_view_preview {
|
||||
& .comment__info {
|
||||
&::after {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.comment_highlighting {
|
||||
background: var(--color4);
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.comment_useless {
|
||||
& .comment__body {
|
||||
opacity: 0.35;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
.comment_view_admin {
|
||||
&,
|
||||
&.comment_theme_light,
|
||||
&.comment_theme_dark {
|
||||
& .comment__username {
|
||||
color: var(--color29);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
.comment_view_preview {
|
||||
&,
|
||||
&.comment_theme_light,
|
||||
&.comment_theme_dark {
|
||||
font-size: 14px;
|
||||
padding-bottom: 0;
|
||||
|
||||
& .comment__title {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color9);
|
||||
}
|
||||
|
||||
& .comment__title-link {
|
||||
color: var(--color9);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__info {
|
||||
display: inline;
|
||||
padding-right: 0;
|
||||
font-weight: 700;
|
||||
color: var(--color10);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__username {
|
||||
color: var(--color9);
|
||||
}
|
||||
|
||||
& .comment__text {
|
||||
display: inline;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
.comment_view_user {
|
||||
&.comment_collapsed {
|
||||
& .comment__text,
|
||||
& .comment__actions {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
& .comment__title {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
& .comment__body {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
& .comment__title-link {
|
||||
color: var(--color29);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
.comment {
|
||||
display: block;
|
||||
padding: 12px 0 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
transition: background 0.3s ease-in-out;
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
& > .comment__body .comment__actions .comment__controls {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.comment__avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.comment_level_6 {
|
||||
& .comment__text,
|
||||
& .comment__actions {
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.comment-form_type_reply {
|
||||
margin-left: 17px;
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
/* stylelint-disable no-descending-specificity */
|
||||
.user {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -48,3 +49,348 @@
|
||||
color: var(--color41);
|
||||
}
|
||||
}
|
||||
|
||||
.root {
|
||||
display: block;
|
||||
padding: 12px 0 8px;
|
||||
font-size: 16px;
|
||||
line-height: 1.4;
|
||||
transition: background 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding-left: 17px;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 84px;
|
||||
font-size: 14px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.status {
|
||||
margin-left: 8px;
|
||||
vertical-align: middle;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.text {
|
||||
margin-bottom: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.time {
|
||||
margin-left: 8px;
|
||||
margin-right: 8px;
|
||||
vertical-align: middle;
|
||||
text-decoration: none;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.username {
|
||||
vertical-align: middle;
|
||||
font-weight: 700;
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
.level6 .text {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.collapsed {
|
||||
padding: 12px 0 12px 28px;
|
||||
|
||||
& > .body {
|
||||
& .text {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
& .info {
|
||||
margin-bottom: 0;
|
||||
opacity: 0.8;
|
||||
min-height: 20px;
|
||||
}
|
||||
|
||||
& .avatar {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
& .username,
|
||||
& .time {
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.editing {
|
||||
& .text {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border: 8px solid;
|
||||
padding-bottom: 0;
|
||||
|
||||
& .body {
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
& .input {
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-bottom-width: 0;
|
||||
border-top-width: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.replying {
|
||||
padding-bottom: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border: 8px solid;
|
||||
|
||||
& .info {
|
||||
padding-left: 8px;
|
||||
}
|
||||
|
||||
& .body {
|
||||
padding: 8px 8px 0;
|
||||
}
|
||||
|
||||
& .input {
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
border-bottom-width: 0;
|
||||
border-top-width: 8px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.useless {
|
||||
& .body {
|
||||
opacity: 0.35;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viewAdmin {
|
||||
&,
|
||||
&.themeLight,
|
||||
&.themeDark {
|
||||
& .username {
|
||||
color: var(--color29);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viewPreview {
|
||||
&,
|
||||
&.themeLight,
|
||||
&.themeDark {
|
||||
font-size: 14px;
|
||||
padding-bottom: 0;
|
||||
|
||||
& .title {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
color: var(--color9);
|
||||
}
|
||||
|
||||
& .titleLink {
|
||||
color: var(--color9);
|
||||
text-decoration: none;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
|
||||
& .info {
|
||||
display: inline;
|
||||
padding-right: 0;
|
||||
font-weight: 700;
|
||||
color: var(--color10);
|
||||
|
||||
&::after {
|
||||
content: ' ';
|
||||
}
|
||||
}
|
||||
|
||||
& .username {
|
||||
color: var(--color9);
|
||||
}
|
||||
|
||||
& .text {
|
||||
display: inline;
|
||||
vertical-align: bottom;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.viewUser {
|
||||
&.collapsed {
|
||||
& .text {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
& .title {
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
& .body {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
& .titleLink {
|
||||
color: var(--color29);
|
||||
font-weight: bold;
|
||||
text-decoration: none;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
display: block;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.75;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.themeDark {
|
||||
& .status {
|
||||
color: var(--color11);
|
||||
}
|
||||
|
||||
& .time {
|
||||
color: var(--color35);
|
||||
|
||||
&:hover {
|
||||
color: var(--color1);
|
||||
}
|
||||
}
|
||||
|
||||
& .username {
|
||||
color: var(--color10);
|
||||
|
||||
&:hover {
|
||||
color: var(--color33);
|
||||
}
|
||||
}
|
||||
|
||||
&.editing {
|
||||
& .input {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
}
|
||||
|
||||
&.replying {
|
||||
& .input {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color7);
|
||||
}
|
||||
}
|
||||
|
||||
&.viewPreview {
|
||||
& .info {
|
||||
&::after {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.comment_highlighting) {
|
||||
background: var(--color19);
|
||||
}
|
||||
}
|
||||
|
||||
.themeLight {
|
||||
& .status {
|
||||
color: var(--color11);
|
||||
}
|
||||
|
||||
& .time {
|
||||
color: var(--color11);
|
||||
|
||||
&:hover {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
|
||||
& .username {
|
||||
color: var(--color10);
|
||||
}
|
||||
|
||||
&.editing {
|
||||
& .input {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
}
|
||||
|
||||
&.replying {
|
||||
& .input {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
|
||||
@media (pointer: coarse) and (max-width: 768px) {
|
||||
border-color: var(--color5);
|
||||
}
|
||||
}
|
||||
|
||||
&.viewPreview {
|
||||
& .info {
|
||||
&::after {
|
||||
color: var(--color10);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&:global(.comment_highlighting) {
|
||||
background: var(--color4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { h, JSX, Component, createRef, ComponentType } from 'preact';
|
||||
import { FormattedMessage, IntlShape, defineMessages } from 'react-intl';
|
||||
import b from 'bem-react-helper';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { COMMENT_NODE_CLASSNAME_PREFIX } from 'common/constants';
|
||||
@@ -283,7 +282,6 @@ export class Comment extends Component<CommentProps, State> {
|
||||
|
||||
render(props: CommentProps, state: State): JSX.Element {
|
||||
const isAdmin = this.isAdmin();
|
||||
const isGuest = this.isGuest();
|
||||
const isCurrentUser = this.isCurrentUser();
|
||||
|
||||
const isReplying = props.editMode === CommentMode.Reply;
|
||||
@@ -316,49 +314,58 @@ export class Comment extends Component<CommentProps, State> {
|
||||
user: props.data.user,
|
||||
};
|
||||
|
||||
const defaultMods = {
|
||||
disabled: props.disabled,
|
||||
pinned: props.data.pin,
|
||||
// TODO: we also have critical_score, so we need to collapse comments with it in future
|
||||
useless:
|
||||
!!props.isUserBanned ||
|
||||
!!props.data.delete ||
|
||||
(props.view !== 'preview' &&
|
||||
props.data.score < StaticStore.config.low_score &&
|
||||
!props.data.pin &&
|
||||
!props.disabled),
|
||||
// TODO: add default view mod or don't?
|
||||
guest: isGuest,
|
||||
view: props.view === 'main' || props.view === 'pinned' ? props.data.user.admin && 'admin' : props.view,
|
||||
replying: props.view === 'main' && isReplying,
|
||||
editing: props.view === 'main' && isEditing,
|
||||
theme: props.view === 'preview' ? undefined : props.theme,
|
||||
level: props.level,
|
||||
collapsed: props.collapsed,
|
||||
};
|
||||
// TODO: we also have critical_score, so we need to collapse comments with it in future
|
||||
const isUseless =
|
||||
!!props.isUserBanned ||
|
||||
!!props.data.delete ||
|
||||
(props.view !== 'preview' &&
|
||||
props.data.score < StaticStore.config.low_score &&
|
||||
!props.data.pin &&
|
||||
!props.disabled);
|
||||
|
||||
let viewClass: string | undefined;
|
||||
if (props.view === 'main' || props.view === 'pinned') {
|
||||
viewClass = props.data.user.admin ? styles.viewAdmin : undefined;
|
||||
} else if (props.view === 'user') {
|
||||
viewClass = styles.viewUser;
|
||||
} else if (props.view === 'preview') {
|
||||
viewClass = styles.viewPreview;
|
||||
}
|
||||
|
||||
const rootClassName = clsx(
|
||||
styles.root,
|
||||
props.view !== 'preview' && (props.theme === 'dark' ? styles.themeDark : styles.themeLight),
|
||||
viewClass,
|
||||
isUseless && styles.useless,
|
||||
props.view === 'main' && isReplying && styles.replying,
|
||||
props.view === 'main' && isEditing && styles.editing,
|
||||
props.level === 6 && styles.level6,
|
||||
props.collapsed && styles.collapsed,
|
||||
props.mix
|
||||
);
|
||||
|
||||
if (props.view === 'preview') {
|
||||
return (
|
||||
<article className={b('comment', { mix: props.mix }, defaultMods)}>
|
||||
<div className="comment__body" dir="auto">
|
||||
<article className={rootClassName}>
|
||||
<div className={styles.body} dir="auto">
|
||||
{!!o.title && (
|
||||
<div className="comment__title">
|
||||
<a className="comment__title-link" href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
||||
<div className={styles.title}>
|
||||
<a className={styles.titleLink} href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
||||
{o.title}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="comment__info">
|
||||
<div className={styles.info}>
|
||||
{!!o.title && o.user.name}
|
||||
|
||||
{!o.title && (
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__username">
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className={styles.username}>
|
||||
{o.user.name}
|
||||
</a>
|
||||
)}
|
||||
</div>{' '}
|
||||
<div
|
||||
className="comment__text raw-content"
|
||||
className={clsx(styles.text, 'raw-content')}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: o.text }}
|
||||
/>
|
||||
@@ -384,27 +391,24 @@ export class Comment extends Component<CommentProps, State> {
|
||||
const goToParentMessage = intl.formatMessage(messages.goToParent);
|
||||
|
||||
return (
|
||||
<article
|
||||
className={b('comment', { mix: this.props.mix }, defaultMods)}
|
||||
id={props.disabled ? undefined : `${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}
|
||||
>
|
||||
<article className={rootClassName} id={props.disabled ? undefined : `${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
||||
{props.view === 'user' && o.title && (
|
||||
<div className="comment__title">
|
||||
<a className="comment__title-link" href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
||||
<div className={styles.title}>
|
||||
<a className={styles.titleLink} href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`}>
|
||||
{o.title}
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
<div className="comment__info">
|
||||
<div className={styles.info}>
|
||||
{props.view !== 'user' && !props.collapsed && (
|
||||
<div className="comment__avatar">
|
||||
<div className={styles.avatar}>
|
||||
<Avatar url={o.user.picture} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.user}>
|
||||
{props.view !== 'user' && (
|
||||
<button onClick={() => this.toggleUserInfoVisibility()} className="comment__username">
|
||||
<button onClick={() => this.toggleUserInfoVisibility()} className={styles.username}>
|
||||
{o.user.name}
|
||||
</button>
|
||||
)}
|
||||
@@ -433,7 +437,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
)}
|
||||
</div>
|
||||
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className="comment__time">
|
||||
<a href={`${o.locator.url}#${COMMENT_NODE_CLASSNAME_PREFIX}${o.id}`} className={styles.time}>
|
||||
{getLocalDatetime(this.props.intl, o.time)}
|
||||
</a>
|
||||
|
||||
@@ -454,13 +458,13 @@ export class Comment extends Component<CommentProps, State> {
|
||||
)}
|
||||
|
||||
{props.isUserBanned && props.view !== 'user' && (
|
||||
<span className="comment__status">
|
||||
<span className={styles.status}>
|
||||
<FormattedMessage id="comment.blocked-user" defaultMessage="Blocked" />
|
||||
</span>
|
||||
)}
|
||||
|
||||
{isAdmin && !props.isUserBanned && props.data.delete && (
|
||||
<span className="comment__status">
|
||||
<span className={styles.status}>
|
||||
<FormattedMessage id="comment.deleted-user" defaultMessage="Deleted" />
|
||||
</span>
|
||||
)}
|
||||
@@ -474,10 +478,10 @@ export class Comment extends Component<CommentProps, State> {
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="comment__body">
|
||||
<div className={styles.body}>
|
||||
{(!props.collapsed || props.view === 'pinned') && (
|
||||
<div
|
||||
className="comment__text raw-content"
|
||||
className={clsx(styles.text, 'raw-content')}
|
||||
ref={this.textNode}
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: o.text }}
|
||||
@@ -520,7 +524,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
user={props.user}
|
||||
theme={props.theme}
|
||||
mode="reply"
|
||||
mix="comment__input"
|
||||
mix={styles.input}
|
||||
onSubmit={(text: string, title: string) => this.addComment(text, title, o.id)}
|
||||
onCancel={this.toggleReplying}
|
||||
getPreview={this.props.getPreview!}
|
||||
@@ -537,7 +541,7 @@ export class Comment extends Component<CommentProps, State> {
|
||||
theme={props.theme}
|
||||
value={o.orig}
|
||||
mode="edit"
|
||||
mix="comment__input"
|
||||
mix={styles.input}
|
||||
onSubmit={(text: string) => this.updateComment(props.data.id, text)}
|
||||
onCancel={this.toggleEditing}
|
||||
getPreview={this.props.getPreview!}
|
||||
|
||||
@@ -1,24 +1 @@
|
||||
import './raw-content.css';
|
||||
|
||||
import './comment.css';
|
||||
import './__body/comment__body.css';
|
||||
|
||||
import './__info/comment__info.css';
|
||||
import './__input/comment__input.css';
|
||||
import './__status/comment__status.css';
|
||||
import './__text/comment__text.css';
|
||||
import './__time/comment__time.css';
|
||||
import './__user-id/comment__user-id.css';
|
||||
import './__username/comment__username.css';
|
||||
|
||||
import './_collapsed/comment_collapsed.css';
|
||||
import './_editing/comment_editing.css';
|
||||
import './_replying/comment_replying.css';
|
||||
import './_useless/comment_useless.css';
|
||||
|
||||
import './_view/_admin/comment_view_admin.css';
|
||||
import './_view/_preview/comment_view_preview.css';
|
||||
import './_view/_user/comment_view_user.css';
|
||||
|
||||
import './_theme/_dark/comment_theme_dark.css';
|
||||
import './_theme/_light/comment_theme_light.css';
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
.dropdown__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;
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
.root {
|
||||
& > button,
|
||||
& > a,
|
||||
& > :global(.dropdown) {
|
||||
& > [data-dropdown] {
|
||||
display: inline-block;
|
||||
text-align: left;
|
||||
padding: 5px;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
.dropdown__items {
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.dropdown__items:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.dropdown__title {
|
||||
&::after {
|
||||
content: '\25BE';
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.dropdown_active {
|
||||
& > .dropdown__content {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.dropdown_theme_dark {
|
||||
& .dropdown__content {
|
||||
background-color: var(--color8);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.dropdown_theme_light {
|
||||
& .dropdown__content {
|
||||
background-color: var(--color6);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
.dropdown {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
.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);
|
||||
}
|
||||
@@ -1,15 +1,16 @@
|
||||
import { h, Component, createRef, RenderableProps } from 'preact';
|
||||
import b from 'bem-react-helper';
|
||||
import clsx from 'clsx';
|
||||
|
||||
import { Theme } from 'common/types';
|
||||
import { sleep } from 'utils/sleep';
|
||||
import { Button } from 'components/button';
|
||||
import { parseMessage } from 'utils/post-message';
|
||||
|
||||
import styles from './dropdown.module.css';
|
||||
|
||||
type Props = RenderableProps<{
|
||||
title: string;
|
||||
titleClass?: string;
|
||||
heading?: string;
|
||||
isActive?: boolean;
|
||||
disabled?: boolean;
|
||||
buttonTitle?: string;
|
||||
@@ -27,6 +28,7 @@ interface State {
|
||||
|
||||
export class Dropdown extends Component<Props, State> {
|
||||
rootNode = createRef<HTMLDivElement>();
|
||||
contentRef = createRef<HTMLDivElement>();
|
||||
storedDocumentHeight: string | null = null;
|
||||
storedDocumentHeightSet = false;
|
||||
checkInterval: number | undefined = undefined;
|
||||
@@ -75,7 +77,7 @@ export class Dropdown extends Component<Props, State> {
|
||||
if (!this.rootNode.current) return false;
|
||||
let parent = this.rootNode.current.parentElement!;
|
||||
while (parent !== document.body) {
|
||||
if (parent.classList.contains('dropdown')) return true;
|
||||
if (parent.hasAttribute('data-dropdown')) return true;
|
||||
parent = parent.parentElement!;
|
||||
}
|
||||
return false;
|
||||
@@ -91,8 +93,7 @@ export class Dropdown extends Component<Props, State> {
|
||||
if (!this.rootNode.current || !this.state.isActive) return;
|
||||
const windowHeight = window.innerHeight;
|
||||
const dcBottom = (() => {
|
||||
// TODO: use ref
|
||||
const dc = Array.from(this.rootNode.current.children).find((c) => c.classList.contains('dropdown__content'));
|
||||
const dc = this.contentRef.current;
|
||||
if (!dc) return 0;
|
||||
const rect = dc.getBoundingClientRect();
|
||||
return window.scrollY + Math.abs(rect.top) + dc.scrollHeight + 10;
|
||||
@@ -114,8 +115,7 @@ export class Dropdown extends Component<Props, State> {
|
||||
|
||||
async __adjustDropDownContent() {
|
||||
if (!this.rootNode.current) return;
|
||||
// TODO: use ref
|
||||
const dc = this.rootNode.current.querySelector<HTMLDivElement>('.dropdown__content');
|
||||
const dc = this.contentRef.current;
|
||||
if (!dc) return;
|
||||
await sleep(0);
|
||||
const rect = dc.getBoundingClientRect();
|
||||
@@ -177,15 +177,24 @@ export class Dropdown extends Component<Props, State> {
|
||||
window.removeEventListener('message', this.receiveMessage);
|
||||
}
|
||||
|
||||
render({ title, titleClass = '', heading, children, mix, theme, disabled, buttonTitle }: Props, { isActive }: State) {
|
||||
render({ title, titleClass = '', children, mix, theme, disabled, buttonTitle }: Props, { isActive }: State) {
|
||||
return (
|
||||
<div className={b('dropdown', { mix }, { theme, active: isActive })} ref={this.rootNode}>
|
||||
<div
|
||||
className={clsx(
|
||||
styles.root,
|
||||
isActive && styles.active,
|
||||
theme === 'dark' ? styles.themeDark : styles.themeLight,
|
||||
mix
|
||||
)}
|
||||
ref={this.rootNode}
|
||||
data-dropdown
|
||||
>
|
||||
<Button
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded={isActive && 'true'}
|
||||
onClick={this.onTitleClick}
|
||||
theme={theme}
|
||||
mix={['dropdown__title', titleClass]}
|
||||
mix={[styles.title, titleClass]}
|
||||
kind="link"
|
||||
disabled={disabled}
|
||||
title={buttonTitle}
|
||||
@@ -194,13 +203,13 @@ export class Dropdown extends Component<Props, State> {
|
||||
</Button>
|
||||
{isActive && (
|
||||
<div
|
||||
className="dropdown__content"
|
||||
className={styles.content}
|
||||
ref={this.contentRef}
|
||||
tabIndex={-1}
|
||||
role="listbox"
|
||||
style={{ transform: `translateX(${this.state.contentTranslateX}px)` }}
|
||||
>
|
||||
{heading && <div className="dropdown__heading">{heading}</div>}
|
||||
<div className="dropdown__items">{children}</div>
|
||||
<div className={styles.items}>{children}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,2 @@
|
||||
import './dropdown.css';
|
||||
import './_active/dropdown_active.css';
|
||||
|
||||
import './__items/dropdown__items.css';
|
||||
import './__title/dropdown__title.css';
|
||||
import './__content/dropdown__content.css';
|
||||
|
||||
import './_theme/_dark/dropdown_theme_dark.css';
|
||||
import './_theme/_light/dropdown_theme_light.css';
|
||||
|
||||
export { Dropdown } from './dropdown';
|
||||
export { DropdownItem } from './__item';
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
.root__copyright {
|
||||
margin: 48px 0 0;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.root__input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
.root__pinned-comment {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.root__pinned-comments {
|
||||
margin-top: 20px;
|
||||
padding: 0 12px 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
.root__preloader {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
.root__thread {
|
||||
&:last-child {
|
||||
margin-bottom: -24px;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
.root__threads {
|
||||
margin-top: 4px;
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.root_theme_dark {
|
||||
color: var(--color20);
|
||||
|
||||
& .root__copyright {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .root__copyright-link {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .root__pinned-comment {
|
||||
border-bottom-color: var(--color18);
|
||||
}
|
||||
|
||||
& .root__pinned-comments {
|
||||
background: var(--color19);
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
.root_theme_light {
|
||||
color: var(--color0);
|
||||
|
||||
& .root__copyright {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .root__copyright-link {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .root__pinned-comment {
|
||||
border-bottom-color: var(--color3);
|
||||
}
|
||||
|
||||
& .root__pinned-comments {
|
||||
background: var(--color4);
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1 @@
|
||||
import './root.css';
|
||||
|
||||
import './__copyright/root__copyright.css';
|
||||
import './__input/root__input.css';
|
||||
import './__preloader/root__preloader.css';
|
||||
import './__pinned-comment/root__pinned-comment.css';
|
||||
import './__pinned-comments/root__pinned-comments.css';
|
||||
import './__thread/root__thread.css';
|
||||
import './__threads/root__threads.css';
|
||||
|
||||
import './_theme/_dark/root_theme_dark.css';
|
||||
import './_theme/_light/root_theme_light.css';
|
||||
|
||||
export { Root, ConnectedRoot } from './root';
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.root {
|
||||
position: relative;
|
||||
|
||||
&::selection {
|
||||
background: var(--color42);
|
||||
}
|
||||
}
|
||||
|
||||
.root_user-info {
|
||||
position: static;
|
||||
}
|
||||
@@ -1,3 +1,52 @@
|
||||
.root {
|
||||
position: relative;
|
||||
|
||||
&::selection {
|
||||
background: var(--color42);
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.pinnedComment {
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
border-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.pinnedComments {
|
||||
margin-top: 20px;
|
||||
padding: 0 12px 12px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.preloader {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.thread {
|
||||
&:last-child {
|
||||
margin-bottom: -24px;
|
||||
}
|
||||
}
|
||||
|
||||
.threads {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.copyright {
|
||||
margin: 48px 0 0;
|
||||
font-size: 12px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.sortPicker {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
@@ -10,3 +59,43 @@
|
||||
max-width: 320px;
|
||||
margin: 20px auto;
|
||||
}
|
||||
|
||||
.themeDark {
|
||||
color: var(--color20);
|
||||
|
||||
& .copyright {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .copyrightLink {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .pinnedComment {
|
||||
border-bottom-color: var(--color18);
|
||||
}
|
||||
|
||||
& .pinnedComments {
|
||||
background: var(--color19);
|
||||
}
|
||||
}
|
||||
|
||||
.themeLight {
|
||||
color: var(--color0);
|
||||
|
||||
& .copyright {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .copyrightLink {
|
||||
color: var(--color1);
|
||||
}
|
||||
|
||||
& .pinnedComment {
|
||||
border-bottom-color: var(--color3);
|
||||
}
|
||||
|
||||
& .pinnedComments {
|
||||
background: var(--color4);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { h, Component, Fragment } from 'preact';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import { useSelector } from 'react-redux';
|
||||
import b from 'bem-react-helper';
|
||||
import { IntlShape, useIntl, FormattedMessage, defineMessages } from 'react-intl';
|
||||
import clsx from 'clsx';
|
||||
|
||||
@@ -201,7 +200,7 @@ export class Root extends Component<Props, State> {
|
||||
|
||||
render(props: Props, { isUserLoading, commentsShown, isSettingsVisible }: State) {
|
||||
if (isUserLoading) {
|
||||
return <Preloader className="root__preloader" />;
|
||||
return <Preloader className={styles.preloader} />;
|
||||
}
|
||||
|
||||
const isCommentsDisabled = props.info.read_only!;
|
||||
@@ -219,7 +218,7 @@ export class Root extends Component<Props, State> {
|
||||
onBlockedUsersHide={this.onBlockedUsersHide}
|
||||
onCommentsChangeReadOnlyMode={this.props.setCommentsReadOnlyState}
|
||||
/>
|
||||
<div className="root__main">
|
||||
<div>
|
||||
{isSettingsVisible ? (
|
||||
<Settings
|
||||
intl={this.props.intl}
|
||||
@@ -239,7 +238,7 @@ export class Root extends Component<Props, State> {
|
||||
id={encodeURI(url || '')}
|
||||
intl={this.props.intl}
|
||||
theme={props.theme}
|
||||
mix="root__input"
|
||||
mix={styles.input}
|
||||
mode="main"
|
||||
user={props.user}
|
||||
onSubmit={(text: string, title: string) => this.props.addComment(text, title)}
|
||||
@@ -249,7 +248,7 @@ export class Root extends Component<Props, State> {
|
||||
)}
|
||||
{this.props.pinnedComments.length > 0 && (
|
||||
<div
|
||||
className="root__pinned-comments"
|
||||
className={styles.pinnedComments}
|
||||
role="region"
|
||||
aria-label={this.props.intl.formatMessage(messages.pinnedComments)}
|
||||
>
|
||||
@@ -262,12 +261,12 @@ export class Root extends Component<Props, State> {
|
||||
data={comment}
|
||||
level={0}
|
||||
disabled={true}
|
||||
mix="root__pinned-comment"
|
||||
mix={styles.pinnedComment}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className={clsx('sort-picker', styles.sortPicker)}>
|
||||
<div className={styles.sortPicker}>
|
||||
<SortPicker />
|
||||
</div>
|
||||
<Comments
|
||||
@@ -296,17 +295,17 @@ function Comments({ isLoading, topComments, commentsShown, showMore }: CommentsP
|
||||
const isShowMoreButtonVisible = IS_MOBILE && commentsShown < topComments.length;
|
||||
|
||||
return (
|
||||
<div className="root__threads" role="list">
|
||||
<div className={styles.threads} role="list">
|
||||
{isLoading ? (
|
||||
<Preloader className="root__preloader" />
|
||||
<Preloader className={styles.preloader} />
|
||||
) : (
|
||||
<>
|
||||
{topComments.length > 0 &&
|
||||
renderComments.map((id) => (
|
||||
<Thread key={`thread-${id}`} id={id} mix="root__thread" level={0} getPreview={getPreview} />
|
||||
<Thread key={`thread-${id}`} id={id} mix={styles.thread} level={0} getPreview={getPreview} />
|
||||
))}
|
||||
{isShowMoreButtonVisible && (
|
||||
<Button className={clsx('more-comments', styles.moreComments)} onClick={showMore}>
|
||||
<Button className={styles.moreComments} onClick={showMore}>
|
||||
<FormattedMessage id="root.show-more" defaultMessage="Show more" />
|
||||
</Button>
|
||||
)}
|
||||
@@ -317,7 +316,7 @@ function Comments({ isLoading, topComments, commentsShown, showMore }: CommentsP
|
||||
}
|
||||
|
||||
const CopyrightLink = (title: string) => (
|
||||
<a class="root__copyright-link" href="https://remark42.com/">
|
||||
<a class={styles.copyrightLink} href="https://remark42.com/">
|
||||
{title}
|
||||
</a>
|
||||
);
|
||||
@@ -337,10 +336,10 @@ export function ConnectedRoot() {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className={clsx(b('root', {}, { theme: props.theme }), props.theme)}>
|
||||
<div className={clsx(styles.root, props.theme === 'dark' ? styles.themeDark : styles.themeLight, props.theme)}>
|
||||
<Root {...props} {...actions} intl={intl} />
|
||||
{!noFooter && (
|
||||
<p className="root__copyright" role="contentinfo">
|
||||
<p className={styles.copyright} role="contentinfo">
|
||||
<FormattedMessage
|
||||
id="root.powered-by"
|
||||
defaultMessage="Powered by <a>Remark42</a>"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
.thread_theme_dark {
|
||||
& .thread__collapse {
|
||||
&::after {
|
||||
border-color: var(--color36);
|
||||
}
|
||||
|
||||
&:hover::after {
|
||||
border-color: var(--color6);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1 @@
|
||||
import './thread.css';
|
||||
import './_theme_dark/thread_theme_dark.css';
|
||||
|
||||
import './__collapse/thread__collapse.css';
|
||||
|
||||
export { Thread } from './thread';
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user