diff --git a/VERSION.properties b/VERSION.properties index 6b9c2f32..20b9c72a 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.7.2 +client.version=1.7.3 server.version=1.6.1 diff --git a/docs/Personal_Messages/Протокол_DM_v1.md b/docs/Personal_Messages/Протокол_DM_v1.md index 9aa8d277..9b457dcb 100644 --- a/docs/Personal_Messages/Протокол_DM_v1.md +++ b/docs/Personal_Messages/Протокол_DM_v1.md @@ -682,3 +682,15 @@ Legacy-таблица `dm_sync_peer_state` после миграции v12 фи - ротация `clientKey`; - финальная конкретная UI-реализация массовой перешифровки; - межсерверная авторизация DM-синхронизации и доставки. + +## 15. UI списка диалогов и загрузки истории + +Актуальные правила клиентского интерфейса: + +- превью диалога выбирается по самому новому контентному сообщению независимо от направления `in/out`; +- если локально уже есть более новое расшифрованное сообщение, чем в серверной сводке `ListContacts`, клиент использует локальный текст и время до следующего согласования с сервером; +- локальное сообщение сравнивается с `lastMessageTimeMs`, поэтому входящие и исходящие сообщения участвуют в выборе на равных; +- индикатор загрузки истории всегда является компактным элементом фиксированной высоты сверху списка и не растягивается на высоту пустого чата; +- клик по аватару или логину собеседника открывает меню перехода в профиль или связи этого пользователя. + +Эти правила относятся только к UI и не изменяют сетевой или байтовый формат DM v1. diff --git a/docs/Personal_Messages/Формат_DM_v1.md b/docs/Personal_Messages/Формат_DM_v1.md index 14d6ddb3..dcb51ef0 100644 --- a/docs/Personal_Messages/Формат_DM_v1.md +++ b/docs/Personal_Messages/Формат_DM_v1.md @@ -341,3 +341,9 @@ ReadReceiptBody_v1_0 В версии DM v1 все типы `1..8` используют единый контейнер `SHiNE_DM`. Межсерверные операции `ReceiveOutcomingMessage`, `ReceiveIncomingMessage` и `DmSyncBatch` не вводят новый байтовый формат DM. Они передают уже сохранённые raw-контейнеры `SHiNE_DM` в Base64. `ackSyncIds` подтверждает только факт сохранения синхронизированной ревизии; delivery-state между серверами не передаётся. Принимающий сервер заново проверяет подпись и применяет контейнер по его `messageType`. + +## 14. Превью в списке диалогов + +Выбор последнего сообщения для клиентского превью не является частью байтового формата. Клиент сравнивает время сообщений обоих направлений и показывает текст самого нового входящего или исходящего сообщения. Локальная расшифрованная копия может временно иметь приоритет над более старым `lastMessageBlobB64` из серверной сводки. + +Контейнер `SHiNE_DM`, подписи, `messageType`, порядок полей и правила шифрования этой UI-доработкой не изменяются. diff --git a/shine-UI/assets/profile-icon-settings.svg b/shine-UI/assets/profile-icon-settings.svg index c3bd6471..160fcc17 100644 --- a/shine-UI/assets/profile-icon-settings.svg +++ b/shine-UI/assets/profile-icon-settings.svg @@ -1,5 +1,4 @@ - - - - + + + diff --git a/shine-UI/index.html b/shine-UI/index.html index 6a0bf3a9..b785dcf4 100644 --- a/shine-UI/index.html +++ b/shine-UI/index.html @@ -12,7 +12,7 @@ СИЯНИЕ ${item.label}`; btn.addEventListener('click', () => { closeTopChannelsMenu(listState); item.action?.(); @@ -1054,8 +1067,11 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi actionBtn.type = 'button'; actionBtn.className = `channel-menu-item ${channel.isSubscribed ? 'destructive' : ''}`.trim(); + const actionLabel = document.createElement('span'); + actionBtn.append(document.createRange().createContextualFragment(channelMenuIcon('subscribe')), actionLabel); + if (canToggleSubscription) { - actionBtn.textContent = channel.pending + actionLabel.textContent = channel.pending ? 'Выполняется...' : channel.isSubscribed ? 'Отписаться' @@ -1075,7 +1091,7 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi channel.pending = true; actionBtn.disabled = true; - actionBtn.textContent = 'Выполняется...'; + actionLabel.textContent = 'Выполняется...'; const nextSubscribed = !channel.isSubscribed; try { @@ -1097,13 +1113,13 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi } catch (error) { channel.pending = false; actionBtn.disabled = false; - actionBtn.textContent = channel.isSubscribed ? 'Отписаться' : 'Подписаться'; + actionLabel.textContent = channel.isSubscribed ? 'Отписаться' : 'Подписаться'; showToast(toUserMessage(error, 'Не удалось изменить подписку.'), { kind: 'error' }); rerenderList(); } }); } else { - actionBtn.textContent = 'Собственный канал'; + actionLabel.textContent = 'Собственный канал'; actionBtn.disabled = true; } @@ -1111,7 +1127,8 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi toggleWrap.className = 'channel-menu-toggle'; const toggleLabel = document.createElement('span'); - toggleLabel.textContent = 'Уведомления'; + toggleLabel.className = 'channel-menu-toggle-label'; + toggleLabel.innerHTML = `${channelMenuIcon('notifications')}Уведомления`; const toggleBtn = document.createElement('button'); toggleBtn.type = 'button'; diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index bb196e21..5ca75ded 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -1,7 +1,6 @@ import { renderHeader } from '../components/header.js'; import { renderUserAvatar } from '../components/avatar-image.js'; import { createOverflowDots } from '../components/overflow-dots.js'; -import { createShineConnectionsLogo } from '../components/shine-logo.js'; import { directMessages } from '../mock-data.js'; import { addAppLogEntry, @@ -36,6 +35,72 @@ import { makeProfileLinksRoute, makeProfileRoute } from '../services/shine-route export const pageMeta = { id: 'chat-view', title: 'Чат' }; const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места'; +function menuIconSvg(name) { + const paths = { + reply: '', + copy: '', + read: '', + edit: '', + delete: '', + call: '', + video: '', + clear: '', + }; + return ``; +} + +function openUserIdentityMenu({ anchorEl, login, navigate }) { + const root = document.getElementById('modal-root'); + if (!root || !anchorEl) return; + const cleanLogin = String(login || '').trim(); + root.innerHTML = ` +
+ +
+ `; + + const layer = root.querySelector('#chat-user-menu-layer'); + const menu = root.querySelector('.dm-user-identity-menu'); + const close = () => { + document.removeEventListener('keydown', onKeydown); + root.innerHTML = ''; + }; + const onKeydown = (event) => { + if (event.key === 'Escape') close(); + }; + const rect = anchorEl.getBoundingClientRect(); + + window.requestAnimationFrame(() => { + if (!menu) return; + const width = menu.offsetWidth || 190; + const left = Math.max(10, Math.min(window.innerWidth - width - 10, rect.left)); + menu.style.left = `${Math.round(left)}px`; + menu.style.top = `${Math.round(rect.bottom + 7)}px`; + }); + + layer?.addEventListener('pointerdown', (event) => { + if (event.target === layer) close(); + }); + document.addEventListener('keydown', onKeydown); + root.querySelector('[data-user-action="connections"]')?.addEventListener('click', () => { + close(); + navigate(makeProfileLinksRoute(cleanLogin)); + }); + root.querySelector('[data-user-action="profile"]')?.addEventListener('click', () => { + close(); + navigate(makeProfileRoute(cleanLogin)); + }); +} + function createChatHeaderParts(login, navigate) { const cleanLogin = String(login || '').trim() || 'unknown'; @@ -52,12 +117,13 @@ function createChatHeaderParts(login, navigate) { const avatarButton = document.createElement('button'); avatarButton.type = 'button'; avatarButton.className = 'chat-header-avatar-btn'; - avatarButton.title = `Профиль ${cleanLogin}`; - avatarButton.setAttribute('aria-label', `Открыть профиль ${cleanLogin}`); + avatarButton.title = `Меню ${cleanLogin}`; + avatarButton.setAttribute('aria-label', `Открыть меню пользователя ${cleanLogin}`); avatarButton.append(avatarSlot); - const loginEl = document.createElement('span'); - loginEl.className = 'chat-header-login'; + const loginEl = document.createElement('button'); + loginEl.type = 'button'; + loginEl.className = 'chat-header-login chat-header-login-btn'; loginEl.setAttribute('role', 'heading'); loginEl.setAttribute('aria-level', '1'); loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`); @@ -84,23 +150,14 @@ function createChatHeaderParts(login, navigate) { }) .catch(() => {}); - const connectionsButton = document.createElement('button'); - connectionsButton.type = 'button'; - connectionsButton.className = 'icon-btn chat-header-icon-btn chat-header-connections-btn'; - connectionsButton.title = `Связи ${cleanLogin}`; - connectionsButton.setAttribute('aria-label', `Открыть связи ${cleanLogin}`); - connectionsButton.append(createShineConnectionsLogo({ className: 'chat-header-connections-logo' })); - connectionsButton.addEventListener('click', () => { + const openMenu = (event) => { if (!cleanLogin || cleanLogin === 'unknown') return; - navigate(makeProfileLinksRoute(cleanLogin)); - }); + openUserIdentityMenu({ anchorEl: event.currentTarget, login: cleanLogin, navigate }); + }; + avatarButton.addEventListener('click', openMenu); + loginEl.addEventListener('click', openMenu); - avatarButton.addEventListener('click', () => { - if (!cleanLogin || cleanLogin === 'unknown') return; - navigate(makeProfileRoute(cleanLogin)); - }); - - return { centerNode: loginEl, avatarButton, connectionsButton }; + return { centerNode: loginEl, avatarButton }; } function truncatePreviewText(value, maxLen = 72) { @@ -232,11 +289,11 @@ function openMessageActionsMenu({ root.innerHTML = `
@@ -328,10 +385,10 @@ function openChatActionsMenu({ root.innerHTML = `
`; @@ -1083,7 +1140,7 @@ export function render({ navigate, route, chrome }) { ], }); const chatHeaderLeft = chatHeader.querySelector('.header-left'); - chatHeaderLeft?.append(chatHeaderParts.connectionsButton, chatHeaderParts.avatarButton); + chatHeaderLeft?.append(chatHeaderParts.avatarButton); chrome?.setTopbar(chatHeader); if (!isKnownContact) { diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js index b03ade88..ba436d76 100644 --- a/shine-UI/js/pages/messages-list.js +++ b/shine-UI/js/pages/messages-list.js @@ -1,5 +1,6 @@ import { authService, + getChatMessages, isSessionInvalidError, normalizeDmChatId, setContacts, @@ -106,6 +107,8 @@ function clipPreviewText(text, maxLen = PREVIEW_MAX_LEN) { } async function resolveDialogPreview(dialog) { + const localText = clipPreviewText(String(dialog?.lastMessageText || '').trim()); + if (localText) return localText; const blobB64 = String(dialog?.lastMessageBlobB64 || '').trim(); if (!blobB64) return 'Диалог пока пуст.'; @@ -145,6 +148,29 @@ async function resolveDialogPreview(dialog) { return pending; } +function resolveLocalMessageTimeMs(message) { + const keys = [message?.baseKey, message?.messageKey]; + for (const key of keys) { + const parts = String(key || '').split('|'); + const timeMs = Number(parts[2] || 0); + if (parts.length >= 4 && Number.isFinite(timeMs) && timeMs > 0) return timeMs; + } + const tempParts = String(message?.tempId || '').split('-'); + const tempTimeMs = Number(tempParts[1] || 0); + if (tempParts[0] === 'tmp' && Number.isFinite(tempTimeMs) && tempTimeMs > 0) return tempTimeMs; + const createdAtMs = Number(message?.createdAtMs || message?.ts || 0); + return Number.isFinite(createdAtMs) && createdAtMs > 0 ? createdAtMs : 0; +} + +function latestLocalDialogMessage(peerLogin) { + const messages = getChatMessages(peerLogin); + const latest = [...messages].sort((a, b) => resolveLocalMessageTimeMs(b) - resolveLocalMessageTimeMs(a))[0]; + if (!latest) return null; + const parsed = parseDmTechBlocks(String(latest?.text || '')); + const text = clipPreviewText(String(parsed.displayText || parsed.visibleText || '').trim()) || 'Сообщение'; + return { text, timeMs: resolveLocalMessageTimeMs(latest) }; +} + function formatChatRowTime(ts) { const value = Number(ts || 0); if (!Number.isFinite(value) || value <= 0) return ''; @@ -350,16 +376,23 @@ function renderRow(item) { unreadCount: Number(dialog?.unreadCount || 0), hasDialog: Boolean(dialog?.hasDialog), }; + const localLatest = latestLocalDialogMessage(peerLogin); + if (localLatest && localLatest.timeMs >= next.lastMessageTimeMs) { + next.lastMessageText = localLatest.text; + next.lastMessageTimeMs = localLatest.timeMs; + } const current = byPeer.get(key); if (!current) { byPeer.set(key, next); return; } - const currentRank = relationOrder(current.relationFlag); - const nextRank = relationOrder(relationFlag); - if (nextRank < currentRank || (nextRank === currentRank && next.lastMessageTimeMs > current.lastMessageTimeMs)) { - byPeer.set(key, next); - } + const newest = next.lastMessageTimeMs > current.lastMessageTimeMs ? next : current; + newest.relationFlag = relationOrder(relationFlag) < relationOrder(current.relationFlag) + ? relationFlag + : current.relationFlag; + newest.unreadCount = Math.max(Number(current.unreadCount || 0), Number(next.unreadCount || 0)); + newest.hasDialog = Boolean(current.hasDialog || next.hasDialog); + byPeer.set(key, newest); }); const rows = Array.from(byPeer.values()).sort((a, b) => { diff --git a/shine-UI/styles/buttons-white.css b/shine-UI/styles/buttons-white.css index 85f3b4f8..a7e0749c 100644 --- a/shine-UI/styles/buttons-white.css +++ b/shine-UI/styles/buttons-white.css @@ -221,3 +221,98 @@ 0 0 12px rgba(72, 145, 255, 0.34) !important; filter: drop-shadow(0 0 4px rgba(92, 190, 255, 0.46)) !important; } + +/* Второй общий тип кнопок. Цвет меняется одной переменной --shine-action-blue + * в main.css. Экран настройки сервера намеренно не получает этот класс. */ +:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn), +:root .screen-content.filled-action-buttons a.primary-btn, +:root .screen-content.filled-action-buttons a.secondary-btn, +:root .screen-content.filled-action-buttons a.ghost-btn, +:root .screen-content.filled-action-buttons a.text-btn { + color: #ffffff !important; + background: var(--shine-action-blue) !important; + background-image: linear-gradient(180deg, rgba(255,255,255,.14), rgba(255,255,255,0)) !important; + border: 0 !important; + box-shadow: + inset 0 1px 0 rgba(255,255,255,.24), + 0 8px 22px rgba(var(--shine-action-blue-rgb), .24) !important; + text-shadow: 0 1px 2px rgba(0,0,0,.18) !important; + border-radius: 14px; +} + +:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn):hover, +:root .screen-content.filled-action-buttons a.primary-btn:hover, +:root .screen-content.filled-action-buttons a.secondary-btn:hover, +:root .screen-content.filled-action-buttons a.ghost-btn:hover, +:root .screen-content.filled-action-buttons a.text-btn:hover { + background: var(--shine-action-blue-hover) !important; + box-shadow: + inset 0 1px 0 rgba(255,255,255,.28), + 0 10px 26px rgba(var(--shine-action-blue-rgb), .32) !important; +} + +:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn):active, +:root .screen-content.filled-action-buttons a.primary-btn:active, +:root .screen-content.filled-action-buttons a.secondary-btn:active, +:root .screen-content.filled-action-buttons a.ghost-btn:active, +:root .screen-content.filled-action-buttons a.text-btn:active { + background: var(--shine-action-blue-pressed) !important; + box-shadow: inset 0 3px 8px rgba(0,0,0,.26) !important; +} + +:root .screen-content.filled-action-buttons button:not(.icon-btn):not(.shine-local-demo-btn):disabled { + color: rgba(255,255,255,.58) !important; + background: rgba(var(--shine-action-blue-rgb), .42) !important; + box-shadow: none !important; +} + +/* Специальная системная кнопка должна сохранять синюю круглую подложку, + * несмотря на глобальный borderless-reset выше. */ +:root button.scroll-to-bottom-btn, +:root button.scroll-to-bottom-btn:hover, +:root button.scroll-to-bottom-btn:focus, +:root button.scroll-to-bottom-btn:focus-visible { + color: #ffffff !important; + background: var(--shine-action-blue) !important; + border: 0 !important; + box-shadow: + inset 0 1px 0 rgba(255,255,255,.25), + 0 10px 26px rgba(var(--shine-action-blue-rgb), .34) !important; + filter: none !important; +} + +:root button.scroll-to-bottom-btn:hover { + background: var(--shine-action-blue-hover) !important; +} + +:root button.scroll-to-bottom-btn:active { + background: var(--shine-action-blue-pressed) !important; + box-shadow: inset 0 3px 9px rgba(0,0,0,.28) !important; +} + +/* Пункты унифицированных выпадающих меню остаются без постоянной заливки, + * но получают общую синюю реакцию при наведении/фокусе. */ +:root button.dm-head-menu-item, +:root button.channel-menu-item, +:root button.dm-message-action-btn { + justify-content: flex-start !important; + color: #f4f8ff !important; + background: transparent !important; + text-align: left !important; +} + +:root button.dm-head-menu-item:hover, +:root button.dm-head-menu-item:focus-visible, +:root button.channel-menu-item:hover, +:root button.channel-menu-item:focus-visible, +:root button.dm-message-action-btn:hover, +:root button.dm-message-action-btn:focus-visible { + background: rgba(var(--shine-action-blue-rgb), .12) !important; + box-shadow: none !important; + transform: none !important; +} + +:root button.channel-menu-item.destructive, +:root button.dm-message-action-btn--danger { + color: #ffb7c5 !important; +} diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index f8830858..1a12ac35 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -9695,3 +9695,243 @@ body.chat-topbar-overlay .composer-slot > * { backdrop-filter: none !important; -webkit-backdrop-filter: none !important; } + +/* ===== Единая полировка меню, DM и управляющих кнопок (2026-08-26) ===== */ + +/* Центральная вкладка остаётся на прежней оси иконок, но имеет большую + * интерактивную область; подпись опущена ниже и больше не заходит на логотип. */ +.toolbar-btn-network { + width: calc(100% + 6px); + min-height: 58px; + margin-inline: -3px; +} + +.toolbar-btn-network > span:last-child { + display: block; + position: relative; + transform: translateY(7px); +} + +/* Общая круглая кнопка прокрутки. Её цвет задаётся --shine-action-blue. */ +.scroll-to-bottom-btn { + width: 54px; + height: 54px; + right: 16px; + border-radius: 50%; +} + +.scroll-to-bottom-btn__icon { + font-size: 28px; + font-weight: 700; + transform: translateY(-2px); +} + +/* Один визуальный язык для меню профиля, чатов и каналов. */ +.dm-head-menu, +.channel-menu-wrap, +.dm-message-actions-menu { + min-width: 196px; + padding: 6px; + gap: 2px; + border: 1px solid rgba(92, 190, 255, 0.22); + border-radius: 16px; + background: linear-gradient(155deg, rgba(19, 27, 42, 0.97), rgba(7, 12, 22, 0.98)); + box-shadow: + 0 18px 42px rgba(0, 0, 0, 0.48), + 0 0 24px rgba(39, 141, 255, 0.10), + inset 0 1px 0 rgba(255, 255, 255, 0.06); + backdrop-filter: blur(20px) saturate(125%); + -webkit-backdrop-filter: blur(20px) saturate(125%); +} + +/* У меню больше нет «облачного» треугольного хвостика. */ +.dm-head-menu::before { + display: none !important; + content: none !important; +} + +.dm-head-menu-item, +.channel-menu-item, +.dm-message-action-btn { + width: 100%; + min-height: 42px; + padding: 0 11px; + display: flex; + align-items: center; + justify-content: flex-start; + gap: 10px; + border: 0; + border-radius: 10px; + background: transparent; + color: #f4f8ff; + font: inherit; + font-size: 14px; + font-weight: 600; + line-height: 1.2; + text-align: left; + white-space: nowrap; +} + +.dm-head-menu-item:hover, +.dm-head-menu-item:focus-visible, +.channel-menu-item:hover, +.channel-menu-item:focus-visible, +.dm-message-action-btn:hover, +.dm-message-action-btn:focus-visible { + outline: none; + background: rgba(39, 141, 255, 0.12); +} + +.dm-head-menu-item svg, +.dm-head-menu-item img, +.channel-menu-icon, +.dm-menu-icon, +.dm-menu-image-icon { + width: 18px; + height: 18px; + min-width: 18px; + flex: 0 0 18px; + object-fit: contain; + color: #65b7ff; + filter: drop-shadow(0 0 5px rgba(39, 141, 255, 0.28)); +} + +.profile-head-menu .profile-head-menu-item img { + width: 17px; + height: 17px; + min-width: 17px; + flex-basis: 17px; +} + +.channel-menu-wrap--portal { + position: fixed; +} + +.channel-menu-divider { + margin: 4px 8px !important; + background: rgba(255, 255, 255, 0.08) !important; +} + +.channel-menu-item.destructive, +.dm-message-action-btn--danger { + color: #ffb7c5; + background: transparent; +} + +.channel-menu-toggle { + min-height: 42px; + padding: 0 11px; + border-radius: 10px; +} + +.channel-menu-toggle-label { + display: inline-flex; + align-items: center; + gap: 10px; +} + +.dm-message-actions-menu { + width: min(78vw, 240px); +} + +.dm-user-identity-menu { + width: 196px; +} + +.dm-user-menu-layer { + pointer-events: auto; +} + +.chat-header-login-btn { + padding: 4px 7px; + min-height: 34px; + border: 0; + border-radius: 10px; + background: transparent; + cursor: pointer; +} + +/* Поле сообщения занимает почти всю ширину; действия прижаты к правому краю. */ +body.chat-topbar-overlay .composer-slot { + padding-left: 0; + padding-right: 4px; +} + +.dm-chat-input { + gap: 4px; + padding-left: 0; + padding-right: 0; +} + +.dm-actions-col { + grid-template-columns: repeat(2, 43px); + grid-template-rows: 46px; + gap: 2px; +} + +.dm-chat-input--multiline .dm-actions-col { + grid-template-columns: 43px; +} + +.dm-emoji-btn, +.dm-send-icon-btn { + min-width: 43px; + width: 43px; +} + +/* Loader не имеет права участвовать в растяжении пустого flex-контейнера. */ +.dm-history-loader { + flex: 0 0 34px; + align-self: center; + width: max-content; + max-width: calc(100% - 24px); + height: 34px; + min-height: 34px; + max-height: 34px; + top: calc(var(--topbar-height, 64px) + 8px); + margin: 0 auto -4px; + overflow: visible; +} + +.dm-history-loader__pill { + flex: 0 0 auto; + width: max-content; + max-width: 100%; + height: 34px; +} + +/* «Личные»: широкие, низкие и более плотные плитки. */ +.dm-list-screen .dm-list { + width: calc(100% + 16px); + margin-inline: -8px; + gap: 4px; +} + +.dm-list-screen .dm-dialog-card { + grid-template-columns: 50px minmax(0, 1fr) auto; + min-height: 54px; + margin: 0; + padding: 5px 6px 5px 4px; + gap: 8px; + border-radius: 15px; +} + +.dm-list-screen .dm-av, +.dm-list-screen .dm-av .avatar, +.dm-list-screen .list-item .avatar { + width: 48px; + height: 48px; + min-width: 48px; + min-height: 48px; +} + +.dm-list-screen .dm-row-main, +.dm-list-screen .dm-row-meta-col { + min-height: 40px; +} + +@media (max-width: 380px) { + .dm-list-screen .dm-dialog-card { + grid-template-columns: 50px minmax(0, 1fr) auto; + } +} diff --git a/shine-UI/styles/main.css b/shine-UI/styles/main.css index 024dbf06..1c7a7b9c 100644 --- a/shine-UI/styles/main.css +++ b/shine-UI/styles/main.css @@ -11,6 +11,11 @@ --accent-soft: rgba(217, 181, 111, 0.18); --danger: #ff718f; --ok: #84f4a1; + /* Единый цвет основных действий: стартовые/регистрационные кнопки и scroll-down. */ + --shine-action-blue: #278dff; + --shine-action-blue-hover: #43a2ff; + --shine-action-blue-pressed: #1973db; + --shine-action-blue-rgb: 39, 141, 255; --radius-lg: 18px; --radius-md: 12px; --radius-sm: 9px;