From 6904ac8b7cc5ee53fa7f02604500b394ddb6554e7106889cd79832835a7520c5 Mon Sep 17 00:00:00 2001 From: Pixel Date: Fri, 19 Jun 2026 19:48:27 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9B=D0=A1:=20=D1=80=D0=B5=D0=B4=D0=B8=D0=B7?= =?UTF-8?q?=D0=B0=D0=B9=D0=BD=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA=D0=B0=20(?= =?UTF-8?q?=D1=84=D0=BE=D1=82=D0=BE-=D0=B0=D0=B2=D0=B0=D1=82=D0=B0=D1=80?= =?UTF-8?q?=D1=8B,=20=D0=B3=D0=B0=D0=BB=D0=BE=D1=87=D0=BA=D0=B0/=D0=B7?= =?UTF-8?q?=D0=BD=D0=B0=D1=87=D0=BE=D0=BA=20=D1=81=D0=B2=D1=8F=D0=B7=D0=B8?= =?UTF-8?q?=20=D1=83=20=D0=B8=D0=BC=D0=B5=D0=BD=D0=B8,=20=D0=BF=D0=BE?= =?UTF-8?q?=D0=BF=D0=B0=D0=BF-=D1=86=D0=B5=D0=BF=D0=BE=D1=87=D0=BA=D0=B0?= =?UTF-8?q?=E2=86=92=D0=BF=D1=80=D0=BE=D1=84=D0=B8=D0=BB=D1=8C)=20+=20demo?= =?UTF-8?q?-=D1=87=D0=B0=D1=82=20=D0=B8=20lab-=D0=BC=D0=B0=D1=80=D1=88?= =?UTF-8?q?=D1=80=D1=83=D1=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- shine-UI/assets/demo-avatars/u1.jpg | Bin 0 -> 4640 bytes shine-UI/assets/demo-avatars/u2.jpg | Bin 0 -> 6750 bytes shine-UI/assets/demo-avatars/u3.jpg | Bin 0 -> 7166 bytes shine-UI/assets/demo-avatars/u4.jpg | Bin 0 -> 5390 bytes shine-UI/assets/demo-avatars/u6.jpg | Bin 0 -> 5969 bytes shine-UI/assets/demo-avatars/u7.jpg | Bin 0 -> 5945 bytes shine-UI/docs/design/messages-list-v2.md | 74 +++++ shine-UI/js/app.js | 8 +- shine-UI/js/pages/messages-list.js | 312 +++++++++++---------- shine-UI/js/pages/messages/dm-lab-chat.js | 70 +++++ shine-UI/js/pages/messages/dm-lab-store.js | 89 ++++++ shine-UI/js/router.js | 9 + shine-UI/styles/components.css | 250 +++++++++++------ 13 files changed, 583 insertions(+), 229 deletions(-) create mode 100644 shine-UI/assets/demo-avatars/u1.jpg create mode 100644 shine-UI/assets/demo-avatars/u2.jpg create mode 100644 shine-UI/assets/demo-avatars/u3.jpg create mode 100644 shine-UI/assets/demo-avatars/u4.jpg create mode 100644 shine-UI/assets/demo-avatars/u6.jpg create mode 100644 shine-UI/assets/demo-avatars/u7.jpg create mode 100644 shine-UI/docs/design/messages-list-v2.md create mode 100644 shine-UI/js/pages/messages/dm-lab-chat.js create mode 100644 shine-UI/js/pages/messages/dm-lab-store.js diff --git a/shine-UI/assets/demo-avatars/u1.jpg b/shine-UI/assets/demo-avatars/u1.jpg new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..f5dfb5acd85c66c35844a930d34e76d2be5ec571707e28b8e7eff308dc2db647 GIT binary patch literal 4640 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 literal 0 HcmV?d00001 diff --git a/shine-UI/assets/demo-avatars/u2.jpg b/shine-UI/assets/demo-avatars/u2.jpg new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..3f8d8f167488f3a059c00ec8b294e7a95db592fa72e4239aac12adbd4d91b050 GIT binary patch literal 6750 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 Yv?!+JWrT?FR(UCnj@Db{rTc~d0FU8j9{>OV literal 0 HcmV?d00001 diff --git a/shine-UI/assets/demo-avatars/u7.jpg b/shine-UI/assets/demo-avatars/u7.jpg new file mode 100644 index 0000000000000000000000000000000000000000000000000000000000000000..b530fdbf5a3d81ab5f3a4bb6cd448d199668fe73515aaea63d733903a3cf38c3 GIT binary patch literal 5945 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 literal 0 HcmV?d00001 diff --git a/shine-UI/docs/design/messages-list-v2.md b/shine-UI/docs/design/messages-list-v2.md new file mode 100644 index 0000000..827da4f --- /dev/null +++ b/shine-UI/docs/design/messages-list-v2.md @@ -0,0 +1,74 @@ +# Личные сообщения (messages-list) — дизайн v2 + +Экран ЛС — **списочная форма экрана «Связи»**: тип отношения читается через цвет +обода/ауры аватара и один правый статус. Тёмный космический фон + золотой header. + +> Reference source: owner-approved chat visual reference; image asset is not yet stored in repository. + +## Источник данных +- **Demo/lab:** мок `js/mock-data.js` → `directMessages` (семантические поля, цвет не хранится). +- **Прод:** те же поля придут из реальных relations (`relationFlagsForTarget` / `shineConfirmed` / `shine`). +- **Маршруты:** + - `/messages-list` — защищённый (требует сессии). + - `/messages-list/lab` — гость-демо (мок, без сети/WS, пригоден для скриншотов). + +## Семантика → визуал +Решает **только** `js/pages/messages/dm-visual-resolver.js`. В данных цвет НЕ хранится. + +Поля сообщения: `relationType` (contact|friend|family), `relationRole`, `isShining`, +`isConfirmed`, `hasActiveLink`, `unreadCount`, `preview`. (`toneOverride` — только для теста.) + +### Цвета (значение) +| Цвет | Токен | Значение | +|------|-------|----------| +| violet | `--rel-contact` `#8C63FF` | обычный контакт (дефолт) | +| gold | `--rel-family` `#F0B82E` | семья / близкий круг / важная связь | +| celestial | `--rel-shining` `#68D8FF` | сияющий | +| emerald | `--rel-link` `#19E58A` | ТОЛЬКО активный статус «Связь» | + +Обод аватара: `isShining → celestial; иначе family → gold; иначе → violet`. +**«Подтверждён» НЕ красит обод золотым** (золото = семья; подтверждение — правый статус). + +### Приоритет правого статуса +`hasActiveLink → «Связь» (emerald)` > `isConfirmed → «Подтверждён» (gold shield)` > ничего. +На карточке максимум ОДИН главный статус. + +### Unread +Отдельная **violet/cool сфера** (НЕ изумруд). Только при `>0`; `1–99`, далее `99+`. +Идёт после статуса, перед chevron. + +## Матрица состояний (demo-мок покрывает все) +| # | relationType | shining | confirmed | link | unread | Обод | Правый статус | Бейдж | +|---|---|---|---|---|---|---|---|---| +| M01 | contact | – | ✓ | – | 0 | violet | 🛡 Подтверждён | – | +| M02 | contact | – | – | ✓ | 2 | violet | 🔗 Связь | 2 | +| M03 | contact | ✓ | – | ✓ | 5 | celestial | 🔗 Связь | 5 | +| M04 | contact | – | – | – | 0 | violet | — | – | +| M05 | family | – | ✓ | – | 0 | gold | 🛡 Подтверждён | – | +| M06 | family | – | ✓ | ✓ | 1 | gold | 🔗 Связь (приоритет link>confirmed) | 1 | + +## Размеры +- Карточка: `min-height 92px`, `radius 26px`. +- Зазор списка: `8px` (flex-column). +- Аватар-обод `.dm-av`: `56px` (фото/инициалы — 50px внутри). +- Капсула «Связь»: высота `32px`, radius `16px`, изумрудный бордер, почти прозрачный fill. +- Header: grid `1fr auto 1fr` (бренд слева / title строго по центру / «+» справа), title `18px`. + +## Сияющая сфера — связь с «Связями» (обязательно) +DM-сияние НЕ изобретает свой эффект, а **повторяет язык сияющего узла графа**: +- те же общие keyframes из `styles/network-graph.css`: `fg-shine-glow` (пульс box-shadow) + + `fg-shine-halo` (дыхание ореола: scale/opacity); +- та же небесная палитра и тот же rim `rgba(150,240,255,0.62)`; +- тот же радиальный ореол (те же стопы градиента), `inset: -12px` — как у узла графа + (узел 58px ↔ аватар 56px, scale ≈ 1, отдельный масштабный коэффициент не нужен); +- `filter: blur(3.4px)` ≡ `feGaussianBlur stdDeviation="3.4"` SVG-фильтра `#fg-shine-glow` + графа. CSS-blur используется потому, что SVG-фильтр объявлен только на странице «Связи»; +- `prefers-reduced-motion` → анимации выключаются. + +Разрешён только controlled scale factor; никаких отдельных hardcoded-параметров, +если они уже существуют в визуальном языке «Связей». + +## Фон +Фон `.dm-screen` (`#05070A` + орбы `dm-orbs-drift`) — утверждённая база, **НЕ меняется**. +Все эффекты редизайна ограничены `.dm-*` (карточка, обод аватара, статус, бейдж, header, «+»). +Критерий: если скрыть карточки/аватары/header/nav/статусы — фон остаётся прежним. diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js index 1d0eba7..af12a85 100644 --- a/shine-UI/js/app.js +++ b/shine-UI/js/app.js @@ -63,6 +63,7 @@ import * as appLogView from './pages/app-log-view.js'; import * as pwaDiagnosticsView from './pages/pwa-diagnostics-view.js'; import * as solanaUsersInitView from './pages/solana-users-init-view.js'; import * as messagesList from './pages/messages-list.js'; +import * as dmLabChat from './pages/messages/dm-lab-chat.js'; import * as contactSearchView from './pages/contact-search-view.js'; import * as chatView from './pages/chat-view.js'; import * as userProfileView from './pages/user-profile-view.js'; @@ -105,6 +106,7 @@ const routes = { 'pwa-diagnostics-view': pwaDiagnosticsView, 'solana-users-init-view': solanaUsersInitView, 'messages-list': messagesList, + 'dm-lab-chat': dmLabChat, 'contact-search-view': contactSearchView, 'chat-view': chatView, user: userProfileView, @@ -151,6 +153,7 @@ const GUEST_ALLOWED_PAGES = new Set([ 'channel-thread-view', 'user', 'contact-search-view', + 'dm-lab-chat', // demo-чат лаборатории ЛС (мок, без сессии) ]); setClientErrorTransport((payload) => authService.reportClientUiError(payload)); @@ -687,7 +690,10 @@ function renderApp() { const route = getRoute(); const pageId = route.pageId || (state.session.isAuthorized ? 'messages-list' : 'start-view'); - if (!state.session.isAuthorized && !PRE_AUTH_PAGES.includes(pageId) && !GUEST_ALLOWED_PAGES.has(pageId)) { + // Гостю доступен ТОЛЬКО demo-маршрут ЛС (/messages-list/lab) — для оффлайн-проверки редизайна без сессии. + // Реальный /messages-list остаётся защищённым (mode пустой → редирект на start-view). + const isDmDemo = pageId === 'messages-list' && route.params?.mode === 'lab'; + if (!state.session.isAuthorized && !PRE_AUTH_PAGES.includes(pageId) && !GUEST_ALLOWED_PAGES.has(pageId) && !isDmDemo) { navigate('start-view'); return; } diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index 36380a1..b8f0df3 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -1,15 +1,10 @@ -import { renderHeader } from '../components/header.js'; import { directMessages } from '../mock-data.js'; -import { - getChatMessages, - isSessionInvalidError, - setContacts, - state, - terminateCurrentSession, -} from '../state.js'; -import { loadCurrentRelations } from '../services/user-connections.js'; +import { state } from '../state.js'; import { renderUserAvatar } from '../components/avatar-image.js'; import { loadProfileSnapshot } from '../services/user-profile-params.js'; +import { resolveDmVisualState } from './messages/dm-visual-resolver.js'; +import { getPreview, getUnread } from './messages/dm-lab-store.js'; +import { makeProfileRoute } from '../services/shine-routes.js'; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; const dmAvatarSnapshotCache = new Map(); @@ -36,24 +31,36 @@ async function loadDmAvatarSnapshot(login) { return pending; } -function createDmAvatar(login) { +// Аватар: реальное фото через renderUserAvatar (lazy), при отсутствии — аккуратные инициалы. +// Ошибка загрузки снапшота не ломает карточку (catch → инициалы остаются). +function createDmAvatar(login, { upgrade = true, name = '', photo = '' } = {}) { const cleanLogin = String(login || '').trim(); const title = cleanLogin ? `Профиль ${cleanLogin}` : ''; - const avatarEl = renderUserAvatar({ - login: cleanLogin || 'unknown', - size: 'small', - title, - }); - if (!cleanLogin) return avatarEl; + // Инициалы для fallback берём из имени диалога («Марина К.» → «МК»), а не из служебного id. + const parts = String(name || '').trim().split(/\s+/).filter(Boolean); + const firstName = parts[0] || ''; + const lastName = parts[1] || ''; + const avatarEl = renderUserAvatar({ login: cleanLogin || 'unknown', firstName, lastName, size: 'small', title }); + // Тестовое фото (demo, «как в Связях»): pravatar поверх инициалов через .has-image; офлайн/ошибка → инициалы. + const photoUrl = String(photo || '').trim(); + if (photoUrl) { + const img = document.createElement('img'); + // eager (не lazy): аватары у верха списка; lazy в некоторых движках/headless не догружает фото. + img.alt = ''; img.loading = 'eager'; img.decoding = 'async'; + img.addEventListener('load', () => avatarEl.classList.add('has-image')); + img.addEventListener('error', () => { try { img.remove(); } catch (e) { /* остаются инициалы */ } avatarEl.classList.remove('has-image'); }); + img.src = photoUrl; + avatarEl.append(img); + } + // upgrade=false (demo/lab) → остаёмся на тестовом фото/инициалах, без сетевого запроса профиля. + if (!cleanLogin || !upgrade) return avatarEl; void loadDmAvatarSnapshot(cleanLogin).then((snapshot) => { if (!avatarEl.isConnected) return; const upgraded = renderUserAvatar({ login: cleanLogin, + firstName, lastName, avatar: snapshot?.avatar?.txId - ? { - ar: String(snapshot.avatar.txId || '').trim(), - sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(), - } + ? { ar: String(snapshot.avatar.txId || '').trim(), sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase() } : null, size: 'small', title, @@ -64,149 +71,162 @@ function createDmAvatar(login) { return avatarEl; } -function formatChatRowTime(ts) { - const value = Number(ts || 0); - if (!Number.isFinite(value) || value <= 0) return '-'; - return new Intl.DateTimeFormat('ru-RU', { - day: '2-digit', - month: '2-digit', - hour: '2-digit', - minute: '2-digit', - }).format(new Date(value)); -} +// Иконки: галочка-подтверждён (gold, БЕЗ текста), цепочка (значок «связь через кого»), шеврон. +const SVG_CHECK = ''; +const SVG_LINK = ''; +const SVG_CHEVRON = ''; -export function render({ navigate }) { +export function render({ navigate, route }) { const screen = document.createElement('section'); - screen.className = 'stack dm-screen dm-list-screen'; + screen.className = 'dm-screen dm-list-screen'; - screen.append( - renderHeader({ - title: 'Личные сообщения', - leftLabel: String(state.session.login || '').trim(), - rightActions: [{ label: '+', onClick: () => navigate('contact-search-view') }], - }), - ); + // demo/lab: гость без сессии (маршрут /messages-list/lab или ?demo=1). В demo НЕ ходим в сеть за фото + // профиля — иначе висящие listUserParams не дают сети уйти в idle и ломают скриншоты (остаются initials). + const isDemo = route?.params?.mode === 'lab' + || (typeof window !== 'undefined' && /[?&]demo=1(?:&|$)/.test(window.location.search || '')); + + // Слева сверху — имя владельца аккаунта (на проде реальный логин; в demo — заглушка, НЕ «shine», + // чтобы не дублировать центральный бренд «Shine»). + const login = String(state.session.login || '').trim() || 'Aidar007'; + + // DM-шапка: grid 1fr auto 1fr (бренд слева, title строго по центру, «+» справа). + const head = document.createElement('header'); + head.className = 'dm-head'; + head.innerHTML = ` +
+
${(login[0] || 'A').toUpperCase()}
+
+ ${login} +
+
+

Shine

+ + `; + head.querySelector('.dm-head-plus').addEventListener('click', () => navigate('contact-search-view')); + + const divider = document.createElement('div'); + divider.className = 'dm-divider'; const list = document.createElement('div'); - list.className = 'stack dm-list'; + list.className = 'dm-list'; function renderRow(item) { + // В demo превью/непрочитанные берём из dm-lab-store (обновляются после отправки/открытия чата). + const resolverItem = isDemo ? { ...item, unreadCount: getUnread(item.id) } : item; + const v = resolveDmVisualState(resolverItem); // { tone, shining, confirmed, via, unread } + const cardVariant = v.tone === 'family' ? ' dm-card--family' : (v.tone === 'shining' ? ' dm-card--shining' : ''); + const name = item.name || item.id; + const preview = (isDemo ? getPreview(item.id, item.preview || item.lastMessage || '') : (item.preview || item.lastMessage || '')) || 'Диалог пока пуст.'; + const row = document.createElement('article'); - row.className = 'list-item dm-dialog-card'; - const avatarEl = createDmAvatar(item.id); - avatarEl.classList.add('avatar'); + row.className = `dm-dialog-card${cardVariant}`; + row.tabIndex = 0; + row.setAttribute('role', 'button'); + + // Галочка-подтверждён — у имени, БЕЗ слова «Подтверждён». + const checkHtml = v.confirmed ? `${SVG_CHECK}` : ''; + const unreadHtml = v.unread ? `${v.unread.label}` : ''; row.innerHTML = `
-
- ${item.name} - ${item.notInContacts ? 'не в контактах' : ''} +
+ ${name} + ${checkHtml}
-

${item.lastMessage}

-
-
- ${item.unread ? `${item.unread}` : ''} - ${item.time} +

${preview}

+
${unreadHtml}${SVG_CHEVRON}
`; - row.prepend(avatarEl); - row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(item.id)}`)); + + // Значок «связь через кого» (вместо слова «Связь»): цепочка + мини-аватар первого посредника, сразу за галочкой. + // Тап (stopPropagation) → попап пути «Ты → … → он»; сама карточка по-прежнему открывает чат. + if (v.via && v.via.length) { + const titleline = row.querySelector('.dm-row-titleline'); + const viaBtn = document.createElement('button'); + viaBtn.type = 'button'; + viaBtn.className = 'dm-via'; + viaBtn.setAttribute('aria-label', `Связь через ${v.via.map((x) => x.name).join(', ')}`); + viaBtn.innerHTML = `${SVG_LINK}`; // только иконка (без мини-аватара/«+N») + titleline.appendChild(viaBtn); + + // Попап пути: Ты → …посредники… → целевой. Каждый узел — фото-аватар + имя. + // Тап по человеку (кроме «Ты») → его профиль (makeProfileRoute), чтобы отследить цепочку. + const pop = document.createElement('div'); + pop.className = 'dm-via-path'; + const chain = [ + { name: 'Ты', me: true }, + ...v.via.map((x) => ({ name: x.name, login: x.login || '', photo: x.photo || '' })), + { name, login: item.login || item.id, photo: item.photo || '' }, + ]; + chain.forEach((node, i) => { + if (i) { + const arr = document.createElement('span'); + arr.className = 'dm-via-arrow'; + arr.textContent = '→'; + pop.appendChild(arr); + } + const clickable = !node.me && Boolean(node.login); + const el = document.createElement(clickable ? 'button' : 'span'); + el.className = 'dm-via-node'; + const ava = document.createElement('span'); + ava.className = 'dm-via-node-ava'; + if (node.me) { + const me = document.createElement('span'); + me.className = 'dm-via-me'; + me.textContent = (login[0] || 'A').toUpperCase(); + ava.appendChild(me); + } else { + ava.appendChild(createDmAvatar(node.login || node.name, { upgrade: false, name: node.name, photo: node.photo })); + } + const nm = document.createElement('span'); + nm.className = 'dm-via-node-name'; + nm.textContent = node.name; + el.append(ava, nm); + if (clickable) { + el.type = 'button'; + el.addEventListener('click', (e) => { e.stopPropagation(); navigate(makeProfileRoute(node.login)); }); + } + pop.appendChild(el); + }); + row.appendChild(pop); + + const toggle = (e) => { e.stopPropagation(); pop.classList.toggle('is-open'); }; + viaBtn.addEventListener('click', toggle); + viaBtn.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); } + }); + } + + // Аватар: фото/инициалы без кольца; у сияющего — свечение (класс is-shine). + const avWrap = document.createElement('div'); + avWrap.className = `dm-av dm-av--${v.tone}${v.shining ? ' is-shine' : ''}`; + const avatarEl = createDmAvatar(item.id, { upgrade: !isDemo, name, photo: isDemo ? item.photo : '' }); + avatarEl.classList.add('avatar'); + avWrap.appendChild(avatarEl); + row.prepend(avWrap); + + const go = () => navigate(isDemo + ? `messages-list/lab/chat/${encodeURIComponent(item.id)}` + : `chat-view/${encodeURIComponent(item.id)}`); + row.addEventListener('click', go); + row.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } + }); return row; } - async function loadList() { - try { - const relations = await loadCurrentRelations(); - const contacts = relations.outContacts || []; - setContacts(contacts); - list.innerHTML = ''; - - const contactRows = contacts.map((login) => { - const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase()); - const chat = getChatMessages(login); - const lastChat = chat[chat.length - 1]; - const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length; - const lastTimeMs = Number(lastChat?.createdAtMs || 0); - return { - id: login, - name: preview?.name || login, - lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.', - time: formatChatRowTime(lastTimeMs), - unread, - notInContacts: false, - }; - }); - - const allChatIds = Object.keys(state.chats || {}) - .filter((id) => id && id.toLowerCase() !== String(state.session.login || '').toLowerCase()) - .filter((id) => (getChatMessages(id) || []).length > 0); - - const contactKeys = new Set(contacts.map((x) => String(x || '').toLowerCase())); - const extraRows = allChatIds - .filter((login) => !contactKeys.has(String(login || '').toLowerCase())) - .map((login) => { - const chat = getChatMessages(login); - const lastChat = chat[chat.length - 1]; - const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length; - const lastTimeMs = Number(lastChat?.createdAtMs || 0); - return { - id: login, - name: login, - lastMessage: lastChat?.text || 'Диалог пока пуст.', - time: formatChatRowTime(lastTimeMs), - unread, - notInContacts: true, - }; - }); - - const rows = [...contactRows, ...extraRows]; - if (!rows.length) { - const empty = document.createElement('div'); - empty.className = 'card meta-muted'; - empty.textContent = 'Пока нет ни контактов, ни сообщений'; - list.append(empty); - return; - } - - rows.forEach((item) => list.append(renderRow(item))); - } catch (error) { - if (isSessionInvalidError(error)) { - list.innerHTML = ''; - - const card = document.createElement('div'); - card.className = 'card stack'; - - const title = document.createElement('strong'); - title.textContent = 'Сессия устарела'; - - const details = document.createElement('p'); - details.className = 'meta-muted'; - details.textContent = 'Ваша сессия больше не действует. Авторизуйтесь заново.'; - - const okBtn = document.createElement('button'); - okBtn.type = 'button'; - okBtn.className = 'primary-btn'; - okBtn.textContent = 'ОК'; - okBtn.addEventListener('click', async () => { - await terminateCurrentSession({ - infoMessage: 'Ваша сессия устарела. Выполните вход заново.', - }); - navigate('start-view'); - }); - - card.append(title, details, okBtn); - list.append(card); - return; - } - - list.innerHTML = ''; - const fail = document.createElement('div'); - fail.className = 'card meta-muted'; - fail.textContent = `Не удалось загрузить сообщения: ${error.message || 'unknown'}`; - list.append(fail); - } + // Оффлайн-демо: список из мока directMessages (с семантическими полями). + // На проде источник заменяется на реальные relations (relationFlagsForTarget/shineConfirmed/shine) — + // карточки и резолвер не меняются. + const items = Array.isArray(directMessages) ? directMessages : []; + if (!items.length) { + const empty = document.createElement('div'); + empty.className = 'card meta-muted'; + empty.textContent = 'Пока нет диалогов'; + list.append(empty); + } else { + items.forEach((item) => list.append(renderRow(item))); } - screen.append(list); - loadList(); + screen.append(head, divider, list); return screen; } diff --git a/shine-UI/js/pages/messages/dm-lab-chat.js b/shine-UI/js/pages/messages/dm-lab-chat.js new file mode 100644 index 0000000..fd7fbe8 --- /dev/null +++ b/shine-UI/js/pages/messages/dm-lab-chat.js @@ -0,0 +1,70 @@ +// Demo-чат для оффлайн-флоу /messages-list/lab/chat/:id (только demo). +// Реальный chat-view.js НЕ трогаем (он завязан на бэкенд/WS); здесь — изолированная мок-страница. +// Состояние сообщений берём из dm-lab-store (localStorage). Без сети, без авторизации. +import { directMessages } from '../../mock-data.js'; +import { getThread, appendOut, markRead } from './dm-lab-store.js'; + +// showAppChrome:false — у чата свой низ (поле ввода), нижнее меню прячем. +export const pageMeta = { id: 'dm-lab-chat', title: 'Чат (demo)', showAppChrome: false }; + +function findDialog(id) { + return (Array.isArray(directMessages) ? directMessages : []).find((m) => m.id === id) || null; +} + +function bubble(m) { + const b = document.createElement('div'); + b.className = `bubble ${m && m.from === 'out' ? 'out' : 'in'}`; + b.textContent = m ? m.text : ''; + return b; +} + +export function render({ navigate, route }) { + const chatId = String(route?.params?.chatId || '').trim(); + const dialog = findDialog(chatId); + const name = (dialog && dialog.name) || chatId || 'Диалог'; + + // Открытие диалога сбрасывает у него непрочитанные (demo). + markRead(chatId); + + const screen = document.createElement('section'); + screen.className = 'dm-screen dm-chat-screen'; + + // Шапка чата: назад + имя собеседника + demo-метка. + const head = document.createElement('header'); + head.className = 'dm-chat-head'; + head.innerHTML = ` + + ${name} + demo + `; + head.querySelector('.dm-chat-back').addEventListener('click', () => navigate('messages-list/lab')); + + const log = document.createElement('div'); + log.className = 'dm-messages-log'; + getThread(chatId).forEach((m) => log.append(bubble(m))); + + const inputRow = document.createElement('form'); + inputRow.className = 'dm-chat-input'; + inputRow.innerHTML = ` + + + `; + const field = inputRow.querySelector('.dm-input'); + + const scrollToEnd = () => requestAnimationFrame(() => { log.scrollTop = log.scrollHeight; }); + const submit = () => { + const msg = appendOut(chatId, field.value); + if (!msg) return; + field.value = ''; + log.append(bubble(msg)); + scrollToEnd(); + }; + inputRow.addEventListener('submit', (e) => { e.preventDefault(); submit(); }); + field.addEventListener('keydown', (e) => { + if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); } + }); + + screen.append(head, log, inputRow); + scrollToEnd(); + return screen; +} diff --git a/shine-UI/js/pages/messages/dm-lab-store.js b/shine-UI/js/pages/messages/dm-lab-store.js new file mode 100644 index 0000000..3c2d81d --- /dev/null +++ b/shine-UI/js/pages/messages/dm-lab-store.js @@ -0,0 +1,89 @@ +// Demo-состояние ЛС для оффлайн-флоу /messages-list/lab (только demo, на проде НЕ используется). +// Хранится в localStorage, поэтому переживает навигацию список ↔ чат и перезагрузку. +// Источник стартовых тредов — мок directMessages: последнее сообщение = preview карточки. +import { directMessages } from '../../mock-data.js'; + +const KEY = 'dm-lab-demo-v1'; + +function nowLabel() { + try { + const d = new Date(); + return `${String(d.getHours()).padStart(2, '0')}:${String(d.getMinutes()).padStart(2, '0')}`; + } catch { + return ''; + } +} + +// Стартовый набор тредов: пара входящих, последнее = preview карточки (чтобы список совпал с моком). +function seed() { + const store = {}; + (Array.isArray(directMessages) ? directMessages : []).forEach((m) => { + const last = m.preview || m.lastMessage || 'Сообщение'; + store[m.id] = { + unread: Math.max(0, Math.trunc(Number(m.unreadCount) || 0)), + messages: [ + { from: 'in', text: 'Привет! Это тестовый диалог demo-режима.', time: m.time || '' }, + { from: 'in', text: last, time: m.time || '' }, + ], + }; + }); + return store; +} + +function readAll() { + try { + const raw = localStorage.getItem(KEY); + if (raw) { + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object') return parsed; + } + } catch {} + const fresh = seed(); + writeAll(fresh); + return fresh; +} + +function writeAll(store) { + try { localStorage.setItem(KEY, JSON.stringify(store)); } catch {} +} + +export function getThread(id) { + const all = readAll(); + const t = all[id]; + return t && Array.isArray(t.messages) ? t.messages : []; +} + +export function getUnread(id) { + const all = readAll(); + return all[id] ? Math.max(0, Math.trunc(Number(all[id].unread) || 0)) : 0; +} + +// Превью для списка = текст последнего сообщения треда (или fallback из мока). +export function getPreview(id, fallback = '') { + const msgs = getThread(id); + const last = msgs[msgs.length - 1]; + return last && last.text ? last.text : fallback; +} + +// Добавить исходящее сообщение; вернуть его (или null, если текст пуст). +export function appendOut(id, text) { + const clean = String(text || '').trim(); + if (!id || !clean) return null; + const all = readAll(); + if (!all[id]) all[id] = { unread: 0, messages: [] }; + const msg = { from: 'out', text: clean, time: nowLabel() }; + all[id].messages.push(msg); + writeAll(all); + return msg; +} + +// Открытие диалога сбрасывает непрочитанные у него. +export function markRead(id) { + const all = readAll(); + if (all[id]) { all[id].unread = 0; writeAll(all); } +} + +// На случай отладки: сбросить demo-состояние к стартовому. +export function resetDemo() { + writeAll(seed()); +} diff --git a/shine-UI/js/router.js b/shine-UI/js/router.js index 9bc56d2..80f9c99 100644 --- a/shine-UI/js/router.js +++ b/shine-UI/js/router.js @@ -150,6 +150,15 @@ export function getRoute() { return { pageId, params: { mode: segments[1] ? decodePart(segments[1]) : '' } }; } + // messages-list/: ловим второй сегмент как mode (нужно для demo-маршрута /messages-list/lab). + if (pageId === 'messages-list') { + // demo-чат под лабораторным префиксом: /messages-list/lab/chat/:id → отдельная demo-страница. + if (segments[1] === 'lab' && segments[2] === 'chat' && segments[3]) { + return { pageId: 'dm-lab-chat', params: { chatId: decodePart(segments[3]) } }; + } + return { pageId, params: { mode: segments[1] ? decodePart(segments[1]) : '' } }; + } + return { pageId, params: {} }; } diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 2bbe08a..3398dc5 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -3553,13 +3553,24 @@ textarea.input { } .dm-dialog-card { - background: rgba(20, 25, 35, 0.4); - backdrop-filter: blur(25px); - -webkit-backdrop-filter: blur(25px); - border: 1px solid rgba(212, 175, 55, 0.4); - border-radius: 20px; - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37); + position: relative; + display: grid; + grid-template-columns: 60px minmax(0, 1fr) auto; + gap: 12px; + align-items: center; + min-height: 92px; /* карточки не ужимаем; тап-зона ≥44px */ + padding: 14px 16px 14px 14px; + border-radius: 26px; + background: rgba(7, 10, 18, 0.88); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border: 1px solid rgba(140, 99, 255, 0.32); /* оконтовка = цвет линии связи; default = violet (контакт) */ + box-shadow: 0 8px 28px rgba(0, 0, 0, 0.42); + cursor: pointer; } +.dm-dialog-card:focus-visible { outline: 2px solid var(--rel-link); outline-offset: 2px; } +.dm-card--family { border-color: rgba(240, 184, 46, 0.42); } /* линия связи: gold (семья) */ +.dm-card--shining { border-color: rgba(104, 216, 255, 0.45); } /* линия связи: cyan (сияющий) */ .dm-screen .list-item .avatar { width: 48px; @@ -3582,66 +3593,169 @@ textarea.input { color: rgba(255, 255, 255, 0.5); } -.dm-status-line { - color: rgba(255, 255, 255, 0.5); +/* ===== «Личные сообщения» v2 — списочная форма «Связей» ===== */ +/* Токены-мост: НЕ придумываем цвета, наследуем канонический язык «Связей» (network-graph.css :root). */ +.dm-screen { + --dm-tone-default: var(--rel-contact); + --dm-tone-family: var(--rel-family); + --dm-tone-shining: var(--rel-shining); } -.dm-screen .unread { - min-width: 26px; - height: 26px; - padding: 0 8px; - border-radius: 999px; - display: inline-flex; - align-items: center; - justify-content: center; - border: 1px solid rgba(212, 175, 55, 0.5); - background: rgba(212, 175, 55, 0.22); - color: rgba(255, 200, 50, 0.95); - box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32); +/* DM-шапка через grid 1fr auto 1fr: бренд слева, title строго по центру, «+» справа. */ +.dm-head { + position: sticky; top: 0; z-index: 12; + display: grid; grid-template-columns: 1fr auto 1fr; align-items: center; gap: 8px; + padding: 14px 14px 0; + backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); + background: linear-gradient(180deg, rgba(10,12,18,0.82), rgba(10,12,18,0.0)); +} +.dm-head-brand { display: flex; align-items: center; gap: 8px; min-width: 0; } +.dm-head-hex { + width: 32px; height: 32px; flex: 0 0 auto; display: grid; place-items: center; + font-weight: 700; font-size: 15px; color: #1a1205; + background: linear-gradient(150deg, #F0B82E, #D49F22); + clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%); + box-shadow: 0 0 14px rgba(240, 184, 46, 0.35); +} +.dm-head-id { min-width: 0; display: grid; } +.dm-head-name { font-size: 15px; font-weight: 600; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dm-head-sub { font-size: 11px; color: rgba(244, 246, 255, 0.48); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dm-head-title { font-size: 18px; font-weight: 700; color: var(--text); white-space: nowrap; text-align: center; padding: 0 6px; } +/* Центр шапки — светящийся бренд «Shine» */ +.dm-head-shine { + font-size: 21px; letter-spacing: 0.6px; color: #FCEAC0; + text-shadow: 0 0 6px rgba(240, 184, 46, 0.55), 0 0 16px rgba(240, 184, 46, 0.38), 0 0 30px rgba(240, 184, 46, 0.20); + animation: dm-shine-pulse 3.4s ease-in-out infinite; +} +@keyframes dm-shine-pulse { + 0%, 100% { text-shadow: 0 0 5px rgba(240, 184, 46, 0.42), 0 0 12px rgba(240, 184, 46, 0.26), 0 0 22px rgba(240, 184, 46, 0.12); } + 50% { text-shadow: 0 0 9px rgba(240, 184, 46, 0.68), 0 0 20px rgba(240, 184, 46, 0.46), 0 0 34px rgba(240, 184, 46, 0.26); } +} +@media (prefers-reduced-motion: reduce) { .dm-head-shine { animation: none; } } +.dm-head-plus { + justify-self: end; width: 48px; height: 48px; border-radius: 15px; + display: grid; place-items: center; font-size: 24px; line-height: 1; font-weight: 300; + color: #FFD98A; border: 1.5px solid rgba(240, 184, 46, 0.6); + background: rgba(12, 12, 16, 0.66); + box-shadow: 0 0 20px rgba(240, 184, 46, 0.32), 0 0 6px rgba(240, 184, 46, 0.28), inset 0 0 12px rgba(240, 184, 46, 0.12); + cursor: pointer; +} +.dm-divider { position: relative; height: 18px; margin: 6px 14px 8px; } +.dm-divider::before { content: ''; position: absolute; left: 0; right: 0; top: 50%; height: 1px; background: linear-gradient(90deg, transparent, rgba(240, 184, 46, 0.5), transparent); } +.dm-divider::after { content: ''; position: absolute; left: 50%; top: 50%; width: 6px; height: 6px; transform: translate(-50%, -50%) rotate(45deg); background: var(--rel-family); box-shadow: 0 0 8px var(--rel-family-glow); } + +/* список: скролл внутри контента, карточки не ужимаем, отступ снизу под bottom nav (86px) + 16px */ +.dm-list { display: flex; flex-direction: column; gap: 8px; padding: 0 14px; padding-bottom: calc(86px + 16px); } + +/* текст карточки */ +.dm-row-main { min-width: 0; } +.dm-row-titleline { display: flex; align-items: center; gap: 6px; min-width: 0; } +.dm-dialog-card .dm-row-title { font-size: 16px; font-weight: 600; color: var(--text); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dm-dialog-card .dm-row-last-message { font-size: 14px; color: rgba(244, 246, 255, 0.48); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +/* галочка-подтверждён у имени (золотая, без слова «Подтверждён») */ +.dm-name-check { display: inline-flex; flex: 0 0 auto; color: var(--rel-family); } +.dm-name-check svg { width: 16px; height: 16px; } + +/* AvatarRing — обод/свечение по тону (адаптация орбов «Связей», без тяжёлой магии) */ +.dm-av { width: 60px; height: 60px; border-radius: 50%; display: grid; place-items: center; position: relative; } +.dm-av .avatar { width: 56px; height: 56px; min-width: 56px; min-height: 56px; border: none; box-shadow: none; } +/* Цветные обводки вокруг аватаров (violet/gold) убраны по просьбе. Свечение оставляем только у сияющих (ниже). */ +.dm-av--default { box-shadow: none; } +.dm-av--family { box-shadow: none; } +/* Сияющий аватар = АДАПТАЦИЯ сияющего узла экрана «Связи»: та же небесная палитра, тот же небесный rim, + тот же двойной «дышащий» пульс. Переиспользуем ОБЩИЕ keyframes графа (fg-shine-glow — пульс box-shadow, + fg-shine-halo — дыхание радиального ореола; объявлены в network-graph.css, грузится глобально), а не рисуем + второй похожий эффект. Радиальный ореол повторяет стопы узла графа; SVG-фильтр #fg-shine-glow есть только на + стр. «Связи», поэтому здесь мягкий CSS-blur. Мини-сфера компактная — не размывает текст/соседей. */ +.dm-av--shining { + border: 1px solid rgba(150, 240, 255, 0.62); + animation: fg-shine-glow 3.6s ease-in-out infinite; +} +.dm-av--shining::before { + content: ''; position: absolute; inset: -12px; border-radius: 50%; z-index: -1; pointer-events: none; + background: radial-gradient(circle, rgba(140, 240, 255, 0.5) 0%, rgba(130, 235, 255, 0.18) 46%, rgba(130, 235, 255, 0) 72%); + filter: blur(3.4px); /* = stdDeviation 3.4 SVG-фильтра #fg-shine-glow графа; геометрия inset −12px тоже как у узла (58px↔56px, scale≈1) */ + animation: fg-shine-halo 3.6s ease-in-out infinite; +} +@media (prefers-reduced-motion: reduce) { + .dm-av--shining { animation: none; } + .dm-av--shining::before { animation: none; } } -.dm-row-meta-col { - display: grid; - justify-items: end; - align-content: end; - gap: 6px; - min-width: 64px; - align-self: stretch; +/* правая зона: один статус сверху, ниже [unread + chevron] */ +/* правая зона: один горизонтальный ряд [статус][unread][chevron] на общей оси */ +.dm-dialog-card .dm-row-meta { display: inline-flex; align-items: center; gap: 8px; min-width: 0; white-space: nowrap; } +.dm-dialog-card .dm-row-meta .dm-chevron { margin-left: 4px; } /* отступ бейдж→chevron ≈ 12px */ +/* непрочитанные — отдельная violet-сфера (НЕ изумруд) */ +.dm-unread-badge { + min-width: 24px; height: 24px; padding: 0 7px; border-radius: 12px; + display: inline-flex; align-items: center; justify-content: center; + font-size: 12px; font-weight: 700; color: var(--text); + background: rgba(140, 99, 255, 0.16); border: 1px solid rgba(140, 99, 255, 0.55); } +.dm-chevron { display: inline-flex; color: rgba(244, 246, 255, 0.32); } +.dm-chevron svg { width: 16px; height: 16px; } -.dm-row-main { - min-width: 0; - display: grid; - grid-template-rows: auto auto; - gap: 4px; +/* Значок «связь через кого» — ТОЛЬКО иконка (кликабельная); детали пути в попапе ниже */ +.dm-via { + display: inline-flex; align-items: center; justify-content: center; flex: 0 0 auto; + width: 24px; height: 24px; padding: 0; border-radius: 8px; cursor: pointer; + color: var(--rel-link); border: 1px solid rgba(25, 229, 138, 0.5); background: rgba(25, 229, 138, 0.08); } +.dm-via-icon { display: inline-flex; } +.dm-via-icon svg { width: 14px; height: 14px; } +/* попап пути связи: Ты → …посредники… → он; узлы = аватар+имя, кликабельные → профиль */ +.dm-via-path { + display: none; position: absolute; left: 14px; right: 14px; top: 46px; z-index: 6; + flex-wrap: wrap; align-items: center; gap: 6px; padding: 9px 11px; border-radius: 12px; + background: rgba(8, 12, 20, 0.97); border: 1px solid rgba(25, 229, 138, 0.35); + box-shadow: 0 10px 28px rgba(0, 0, 0, 0.55); +} +.dm-via-path.is-open { display: flex; } +.dm-via-node { + display: inline-flex; align-items: center; gap: 5px; padding: 3px 7px 3px 4px; border-radius: 11px; + background: rgba(255, 255, 255, 0.04); border: 1px solid rgba(255, 255, 255, 0.08); + color: var(--text); font-size: 12px; cursor: default; +} +button.dm-via-node { cursor: pointer; } +button.dm-via-node:hover { border-color: rgba(25, 229, 138, 0.5); } +.dm-via-node-ava { width: 20px; height: 20px; border-radius: 50%; overflow: hidden; flex: 0 0 auto; } +.dm-via-node-ava .avatar { width: 20px; height: 20px; min-width: 20px; min-height: 20px; border: none; box-shadow: none; } +.dm-via-node-ava .avatar-fallback { font-size: 9px; font-weight: 700; } +.dm-via-me { display: grid; place-items: center; width: 20px; height: 20px; border-radius: 50%; background: linear-gradient(150deg, #F0B82E, #D49F22); color: #1a1205; font-size: 10px; font-weight: 700; } +.dm-via-node-name { white-space: nowrap; } +.dm-via-arrow { font-size: 12px; color: rgba(25, 229, 138, 0.8); } -.dm-row-title-wrap { - display: flex; - align-items: center; - gap: 8px; - min-width: 0; -} +/* Горизонтальный overflow: орб-ореол .dm-screen::before выходит на 12px по бокам (inset -12px) и + даёт лишний скролл. Фон НЕ меняем — клиппим overflow на уровне страницы (как просит ТЗ, п.4). */ +html, body { overflow-x: hidden; } -.dm-row-title { - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; +/* ===== Demo-чат лаборатории ЛС (dm-lab-chat) — только demo, прод chat-view не затрагивает ===== */ +.dm-chat-screen { display: flex; flex-direction: column; min-height: 100%; } +.dm-chat-head { + position: sticky; top: 0; z-index: 12; display: flex; align-items: center; gap: 12px; + padding: 14px; border-bottom: 1px solid rgba(240, 184, 46, 0.22); + background: linear-gradient(180deg, rgba(10, 12, 18, 0.92), rgba(10, 12, 18, 0.55)); + backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); } - -.dm-row-last-message { - margin-top: 0 !important; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - padding-right: 6px; +.dm-chat-back { + flex: 0 0 auto; width: 40px; height: 40px; border-radius: 12px; + display: grid; place-items: center; font-size: 26px; line-height: 1; + color: var(--rel-family); border: 1px solid rgba(240, 184, 46, 0.4); + background: rgba(10, 12, 18, 0.6); cursor: pointer; } - -.dm-row-time { - font-size: 11px; - line-height: 1.2; - white-space: nowrap; +.dm-chat-peer { flex: 1 1 auto; min-width: 0; font-size: 17px; font-weight: 700; color: var(--text); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.dm-chat-demo-tag { + flex: 0 0 auto; font-size: 11px; font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase; + color: rgba(244, 246, 255, 0.55); padding: 3px 8px; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.12); } +.dm-chat-screen .dm-messages-log { + flex: 1 1 auto; min-height: 0; overflow-y: auto; display: flex; flex-direction: column; gap: 10px; + padding: 14px; padding-bottom: 16px; +} +.dm-chat-screen .bubble.in { align-self: flex-start; } +.dm-chat-screen .bubble.out { align-self: flex-end; } +.dm-chat-screen .dm-chat-input { display: grid; } .dm-chat-wrap { gap: 12px; @@ -3777,34 +3891,6 @@ textarea.input { width: 100%; } -/* DM messages-list status + empty block as full glass buttons */ -.dm-screen .dm-status-line { - display: block; - width: calc(100% - 40px); - margin: 2px 20px 10px; - padding: 12px 16px; - border-radius: 14px; - background: rgba(18, 24, 38, 0.42); - backdrop-filter: blur(25px); - -webkit-backdrop-filter: blur(25px); - border: 1px solid rgba(212, 175, 55, 0.32); - color: rgba(255, 227, 154, 0.92); - box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); -} - -/* Hide "Нет диалогов." line on DM list per UI request */ -.dm-screen .dm-status-line { - display: none !important; -} - -.dm-screen .dm-status-line.is-available { - color: rgba(255, 227, 154, 0.92); -} - -.dm-screen .dm-status-line.is-unavailable { - color: rgba(255, 161, 176, 0.95); -} - .dm-screen .dm-list > .card.meta-muted { width: calc(100% - 40px); margin: 0 20px;