SHA256
Обновить UI-ветку
This commit is contained in:
@@ -1,3 +1,5 @@
|
|||||||
|
import org.bouncycastle.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id 'java'
|
id 'java'
|
||||||
id 'application'
|
id 'application'
|
||||||
|
|||||||
+1
-1
@@ -12,7 +12,7 @@
|
|||||||
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
||||||
<title>СИЯНИЕ</title>
|
<title>СИЯНИЕ</title>
|
||||||
<script>
|
<script>
|
||||||
window.__SHINE_BUILD_HASH__ = '20260822140000';
|
window.__SHINE_BUILD_HASH__ = '20260823123601';
|
||||||
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
||||||
</script>
|
</script>
|
||||||
<script>
|
<script>
|
||||||
|
|||||||
@@ -938,6 +938,9 @@ function closeTopChannelsMenu(listState) {
|
|||||||
listState.topMenuCleanup();
|
listState.topMenuCleanup();
|
||||||
}
|
}
|
||||||
listState.topMenuCleanup = null;
|
listState.topMenuCleanup = null;
|
||||||
|
listState.topMenuAnchor?.setAttribute('aria-expanded', 'false');
|
||||||
|
listState.topMenuAnchor?.classList.remove('menu-open-pressed');
|
||||||
|
listState.topMenuAnchor = null;
|
||||||
const root = document.getElementById('modal-root');
|
const root = document.getElementById('modal-root');
|
||||||
if (root) {
|
if (root) {
|
||||||
const overlay = root.querySelector(`#${TOP_MENU_OVERLAY_ID}`);
|
const overlay = root.querySelector(`#${TOP_MENU_OVERLAY_ID}`);
|
||||||
@@ -989,9 +992,6 @@ function openTopChannelsMenu({
|
|||||||
if (item.divider) {
|
if (item.divider) {
|
||||||
const divider = document.createElement('div');
|
const divider = document.createElement('div');
|
||||||
divider.className = 'channel-menu-divider';
|
divider.className = 'channel-menu-divider';
|
||||||
divider.style.height = '1px';
|
|
||||||
divider.style.background = 'rgba(255,255,255,0.08)';
|
|
||||||
divider.style.margin = '6px 0';
|
|
||||||
menu.append(divider);
|
menu.append(divider);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1008,6 +1008,9 @@ function openTopChannelsMenu({
|
|||||||
|
|
||||||
overlay.append(menu);
|
overlay.append(menu);
|
||||||
root.append(overlay);
|
root.append(overlay);
|
||||||
|
anchorEl.setAttribute('aria-expanded', 'true');
|
||||||
|
anchorEl.classList.add('menu-open-pressed');
|
||||||
|
listState.topMenuAnchor = anchorEl;
|
||||||
|
|
||||||
const onOverlayClick = (event) => {
|
const onOverlayClick = (event) => {
|
||||||
if (event.target === overlay) closeTopChannelsMenu(listState);
|
if (event.target === overlay) closeTopChannelsMenu(listState);
|
||||||
@@ -1355,6 +1358,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const listState = {
|
const listState = {
|
||||||
openMenuId: null,
|
openMenuId: null,
|
||||||
topMenuCleanup: null,
|
topMenuCleanup: null,
|
||||||
|
topMenuAnchor: null,
|
||||||
notificationsState,
|
notificationsState,
|
||||||
revealedCounters: new Set(),
|
revealedCounters: new Set(),
|
||||||
channels: [],
|
channels: [],
|
||||||
@@ -1381,11 +1385,16 @@ export function render({ navigate, route, chrome }) {
|
|||||||
topMenuBtn.type = 'button';
|
topMenuBtn.type = 'button';
|
||||||
topMenuBtn.className = 'icon-btn channels-top-more-btn';
|
topMenuBtn.className = 'icon-btn channels-top-more-btn';
|
||||||
topMenuBtn.setAttribute('aria-label', 'Ещё действия');
|
topMenuBtn.setAttribute('aria-label', 'Ещё действия');
|
||||||
|
topMenuBtn.setAttribute('aria-haspopup', 'menu');
|
||||||
|
topMenuBtn.setAttribute('aria-expanded', 'false');
|
||||||
topMenuBtn.title = 'Ещё действия';
|
topMenuBtn.title = 'Ещё действия';
|
||||||
topMenuBtn.append(createOverflowDots());
|
topMenuBtn.append(createOverflowDots());
|
||||||
topMenuBtn.addEventListener('click', (event) => {
|
topMenuBtn.addEventListener('click', (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
animatePress(topMenuBtn);
|
if (listState.topMenuAnchor === topMenuBtn) {
|
||||||
|
closeTopChannelsMenu(listState);
|
||||||
|
return;
|
||||||
|
}
|
||||||
openTopChannelsMenu({
|
openTopChannelsMenu({
|
||||||
listState,
|
listState,
|
||||||
anchorEl: topMenuBtn,
|
anchorEl: topMenuBtn,
|
||||||
|
|||||||
@@ -1,22 +1,34 @@
|
|||||||
import { directMessages } from '../mock-data.js';
|
|
||||||
import {
|
import {
|
||||||
getChatMessages,
|
authService,
|
||||||
isSessionInvalidError,
|
isSessionInvalidError,
|
||||||
normalizeDmChatId,
|
normalizeDmChatId,
|
||||||
setContacts,
|
setContacts,
|
||||||
state,
|
state,
|
||||||
terminateCurrentSession,
|
terminateCurrentSession,
|
||||||
} from '../state.js';
|
} from '../state.js';
|
||||||
import { loadCurrentRelations } from '../services/user-connections.js';
|
|
||||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||||
import { createOverflowDots } from '../components/overflow-dots.js';
|
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||||
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
|
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
|
||||||
|
const PREVIEW_MAX_LEN = 200;
|
||||||
|
const SVG_CHEVRON = `
|
||||||
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
|
<path d="M9 6l6 6-6 6"></path>
|
||||||
|
</svg>
|
||||||
|
`;
|
||||||
|
const DM_BLOB_PREVIEW_CACHE = new Map();
|
||||||
|
const DM_BLOB_PREVIEW_PENDING = new Map();
|
||||||
const dmAvatarSnapshotCache = new Map();
|
const dmAvatarSnapshotCache = new Map();
|
||||||
const dmAvatarPendingByLogin = new Map();
|
const dmAvatarPendingByLogin = new Map();
|
||||||
|
|
||||||
|
const RELATION_ORDER = new Map([
|
||||||
|
['close_friend', 0],
|
||||||
|
['contact', 1],
|
||||||
|
['none', 2],
|
||||||
|
]);
|
||||||
|
|
||||||
async function loadDmAvatarSnapshot(login) {
|
async function loadDmAvatarSnapshot(login) {
|
||||||
const cleanLogin = String(login || '').trim();
|
const cleanLogin = String(login || '').trim();
|
||||||
if (!cleanLogin) return null;
|
if (!cleanLogin) return null;
|
||||||
@@ -68,10 +80,72 @@ function createDmAvatar(login, { className = '' } = {}) {
|
|||||||
return avatarEl;
|
return avatarEl;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveLastMessagePreview(text = '') {
|
function normalizeRelationFlag(value) {
|
||||||
const parsed = parseDmTechBlocks(String(text || ''));
|
const clean = String(value || '').trim().toLowerCase();
|
||||||
const display = String(parsed.displayText || '').trim();
|
if (clean === 'close_friend' || clean === 'contact') return clean;
|
||||||
return display || '';
|
return 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
function relationOrder(flag) {
|
||||||
|
return RELATION_ORDER.get(normalizeRelationFlag(flag)) ?? 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
function relationLabel(flag) {
|
||||||
|
switch (normalizeRelationFlag(flag)) {
|
||||||
|
case 'close_friend':
|
||||||
|
return 'близкий друг';
|
||||||
|
case 'contact':
|
||||||
|
return 'контакт';
|
||||||
|
default:
|
||||||
|
return 'не в контактах';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clipPreviewText(text, maxLen = PREVIEW_MAX_LEN) {
|
||||||
|
const normalized = String(text || '').replace(/\s+/g, ' ').trim();
|
||||||
|
if (!normalized) return '';
|
||||||
|
if (normalized.length <= maxLen) return normalized;
|
||||||
|
return `${normalized.slice(0, maxLen - 1)}…`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function resolveDialogPreview(dialog) {
|
||||||
|
const blobB64 = String(dialog?.lastMessageBlobB64 || '').trim();
|
||||||
|
if (!blobB64) return 'Диалог пока пуст.';
|
||||||
|
|
||||||
|
const cacheKey = [
|
||||||
|
blobB64,
|
||||||
|
String(state.session.login || '').trim().toLowerCase(),
|
||||||
|
String(state.session.storagePwdInMemory || '').trim(),
|
||||||
|
].join('|');
|
||||||
|
|
||||||
|
if (DM_BLOB_PREVIEW_CACHE.has(cacheKey)) return DM_BLOB_PREVIEW_CACHE.get(cacheKey);
|
||||||
|
if (DM_BLOB_PREVIEW_PENDING.has(cacheKey)) return DM_BLOB_PREVIEW_PENDING.get(cacheKey);
|
||||||
|
|
||||||
|
const pending = (async () => {
|
||||||
|
try {
|
||||||
|
const parsed = authService.parseSignedMessageBlob(blobB64);
|
||||||
|
const decrypted = await authService.decryptSignedMessageContent({
|
||||||
|
parsed,
|
||||||
|
blobB64,
|
||||||
|
login: state.session.login,
|
||||||
|
storagePwd: state.session.storagePwdInMemory,
|
||||||
|
});
|
||||||
|
const parsedText = parseDmTechBlocks(String(decrypted?.text || ''));
|
||||||
|
const display = clipPreviewText(String(parsedText.displayText || '').trim());
|
||||||
|
const result = display || 'Сообщение';
|
||||||
|
DM_BLOB_PREVIEW_CACHE.set(cacheKey, result);
|
||||||
|
return result;
|
||||||
|
} catch {
|
||||||
|
const fallback = 'Сообщение недоступно';
|
||||||
|
DM_BLOB_PREVIEW_CACHE.set(cacheKey, fallback);
|
||||||
|
return fallback;
|
||||||
|
} finally {
|
||||||
|
DM_BLOB_PREVIEW_PENDING.delete(cacheKey);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
DM_BLOB_PREVIEW_PENDING.set(cacheKey, pending);
|
||||||
|
return pending;
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatChatRowTime(ts) {
|
function formatChatRowTime(ts) {
|
||||||
@@ -86,11 +160,11 @@ function formatChatRowTime(ts) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compareChatRows(a, b) {
|
function compareChatRows(a, b) {
|
||||||
const timeA = Number(a?.lastTimeMs || 0);
|
const timeA = Number(a?.lastMessageTimeMs || 0);
|
||||||
const timeB = Number(b?.lastTimeMs || 0);
|
const timeB = Number(b?.lastMessageTimeMs || 0);
|
||||||
if (timeA !== timeB) return timeB - timeA;
|
if (timeA !== timeB) return timeB - timeA;
|
||||||
const nameA = String(a?.name || '').toLowerCase();
|
const nameA = String(a?.peerLogin || '').toLowerCase();
|
||||||
const nameB = String(b?.name || '').toLowerCase();
|
const nameB = String(b?.peerLogin || '').toLowerCase();
|
||||||
return nameA.localeCompare(nameB, 'ru');
|
return nameA.localeCompare(nameB, 'ru');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,17 +175,16 @@ export function render({ navigate, chrome }) {
|
|||||||
head.className = 'dm-head';
|
head.className = 'dm-head';
|
||||||
head.innerHTML = `
|
head.innerHTML = `
|
||||||
<div class="dm-head-brand" aria-hidden="true"></div>
|
<div class="dm-head-brand" aria-hidden="true"></div>
|
||||||
<h1 class="dm-head-title">Контакты</h1>
|
<h1 class="dm-head-title">Чаты</h1>
|
||||||
<div class="dm-head-menu-wrap">
|
<div class="dm-head-menu-wrap">
|
||||||
<button type="button" class="dm-head-menu-btn" aria-label="Меню контактов" aria-haspopup="menu" aria-expanded="false">
|
<button type="button" class="dm-head-menu-btn" aria-label="Меню чатов" aria-haspopup="menu" aria-expanded="false"></button>
|
||||||
</button>
|
|
||||||
<div class="dm-head-menu" role="menu" hidden>
|
<div class="dm-head-menu" role="menu" hidden>
|
||||||
<button type="button" class="dm-head-menu-item" role="menuitem" data-action="search-contacts">
|
<button type="button" class="dm-head-menu-item" role="menuitem" data-action="search-contacts">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
||||||
<circle cx="11" cy="11" r="6.5"></circle>
|
<circle cx="11" cy="11" r="6.5"></circle>
|
||||||
<path d="M16 16l4 4"></path>
|
<path d="M16 16l4 4"></path>
|
||||||
</svg>
|
</svg>
|
||||||
<span>Поиск контактов</span>
|
<span>Поиск пользователей</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,11 +200,15 @@ export function render({ navigate, chrome }) {
|
|||||||
menuTemplate?.remove();
|
menuTemplate?.remove();
|
||||||
|
|
||||||
let menuPortal = null;
|
let menuPortal = null;
|
||||||
|
let menuBackdrop = null;
|
||||||
|
|
||||||
const closeHeadMenu = () => {
|
const closeHeadMenu = () => {
|
||||||
menuPortal?.remove();
|
menuPortal?.remove();
|
||||||
|
menuBackdrop?.remove();
|
||||||
menuPortal = null;
|
menuPortal = null;
|
||||||
|
menuBackdrop = null;
|
||||||
menuButton?.setAttribute('aria-expanded', 'false');
|
menuButton?.setAttribute('aria-expanded', 'false');
|
||||||
|
menuButton?.classList.remove('menu-open-pressed');
|
||||||
menuWrap?.classList.remove('is-open');
|
menuWrap?.classList.remove('is-open');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,10 +216,16 @@ export function render({ navigate, chrome }) {
|
|||||||
if (!menuPortal || !menuButton) return;
|
if (!menuPortal || !menuButton) return;
|
||||||
const rect = menuButton.getBoundingClientRect();
|
const rect = menuButton.getBoundingClientRect();
|
||||||
const margin = 10;
|
const margin = 10;
|
||||||
const menuWidth = menuPortal.offsetWidth || 206;
|
const menuRect = menuPortal.getBoundingClientRect();
|
||||||
|
const menuWidth = menuRect.width || menuPortal.offsetWidth || 190;
|
||||||
|
const menuHeight = menuRect.height || menuPortal.offsetHeight || 56;
|
||||||
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
||||||
|
const below = rect.bottom + 7;
|
||||||
|
const top = below + menuHeight <= window.innerHeight - margin
|
||||||
|
? below
|
||||||
|
: Math.max(margin, rect.top - menuHeight - 7);
|
||||||
menuPortal.style.left = `${Math.round(left)}px`;
|
menuPortal.style.left = `${Math.round(left)}px`;
|
||||||
menuPortal.style.top = `${Math.round(rect.bottom + 7)}px`;
|
menuPortal.style.top = `${Math.round(top)}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openHeadMenu = () => {
|
const openHeadMenu = () => {
|
||||||
@@ -152,11 +235,7 @@ export function render({ navigate, chrome }) {
|
|||||||
portal.setAttribute('role', 'menu');
|
portal.setAttribute('role', 'menu');
|
||||||
portal.innerHTML = `
|
portal.innerHTML = `
|
||||||
<button type="button" class="dm-head-menu-item" role="menuitem" data-action="search-contacts">
|
<button type="button" class="dm-head-menu-item" role="menuitem" data-action="search-contacts">
|
||||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
<span>Поиск пользователей</span>
|
||||||
<circle cx="11" cy="11" r="6.5"></circle>
|
|
||||||
<path d="M16 16l4 4"></path>
|
|
||||||
</svg>
|
|
||||||
<span>Поиск контактов</span>
|
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@@ -168,11 +247,17 @@ export function render({ navigate, chrome }) {
|
|||||||
});
|
});
|
||||||
portal.addEventListener('click', (event) => event.stopPropagation());
|
portal.addEventListener('click', (event) => event.stopPropagation());
|
||||||
|
|
||||||
document.body.append(portal);
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'ui-menu-dim-layer';
|
||||||
|
backdrop.addEventListener('click', closeHeadMenu);
|
||||||
|
document.body.append(backdrop, portal);
|
||||||
|
menuBackdrop = backdrop;
|
||||||
menuPortal = portal;
|
menuPortal = portal;
|
||||||
menuButton.setAttribute('aria-expanded', 'true');
|
menuButton.setAttribute('aria-expanded', 'true');
|
||||||
|
menuButton.classList.add('menu-open-pressed');
|
||||||
menuWrap?.classList.add('is-open');
|
menuWrap?.classList.add('is-open');
|
||||||
positionHeadMenu();
|
positionHeadMenu();
|
||||||
|
requestAnimationFrame(positionHeadMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
menuButton?.addEventListener('click', (event) => {
|
menuButton?.addEventListener('click', (event) => {
|
||||||
@@ -204,7 +289,11 @@ export function render({ navigate, chrome }) {
|
|||||||
function renderRow(item) {
|
function renderRow(item) {
|
||||||
const row = document.createElement('article');
|
const row = document.createElement('article');
|
||||||
row.className = 'list-item dm-dialog-card';
|
row.className = 'list-item dm-dialog-card';
|
||||||
const avatarEl = createDmAvatar(item.id);
|
const relationFlag = normalizeRelationFlag(item.relationFlag);
|
||||||
|
const relationBadge = relationFlag === 'none'
|
||||||
|
? 'не в контактах'
|
||||||
|
: relationLabel(relationFlag);
|
||||||
|
const avatarEl = createDmAvatar(item.peerLogin);
|
||||||
avatarEl.classList.add('avatar');
|
avatarEl.classList.add('avatar');
|
||||||
const avatarWrap = document.createElement('div');
|
const avatarWrap = document.createElement('div');
|
||||||
avatarWrap.className = 'dm-av dm-av--default';
|
avatarWrap.className = 'dm-av dm-av--default';
|
||||||
@@ -213,25 +302,30 @@ export function render({ navigate, chrome }) {
|
|||||||
<div class="dm-row-main">
|
<div class="dm-row-main">
|
||||||
<div class="dm-row-titleline dm-row-titlewrap">
|
<div class="dm-row-titleline dm-row-titlewrap">
|
||||||
<strong class="dm-row-title"></strong>
|
<strong class="dm-row-title"></strong>
|
||||||
${item.notInContacts ? '<span class="dm-contact-note">не в контактах</span>' : ''}
|
<span class="dm-contact-note">${relationBadge}</span>
|
||||||
</div>
|
</div>
|
||||||
<p class="dm-row-last-message"></p>
|
<p class="dm-row-last-message"></p>
|
||||||
</div>
|
</div>
|
||||||
<div class="dm-row-meta-col">
|
<div class="dm-row-meta-col">
|
||||||
${item.unread ? `<span class="dm-unread-badge">${item.unread > 99 ? '99+' : item.unread}</span>` : '<span class="dm-row-meta-spacer" aria-hidden="true"></span>'}
|
${item.unreadCount ? `<span class="dm-unread-badge">${item.unreadCount > 99 ? '99+' : item.unreadCount}</span>` : '<span class="dm-row-meta-spacer" aria-hidden="true"></span>'}
|
||||||
<div class="dm-row-meta-line">
|
<div class="dm-row-meta-line">
|
||||||
${item.time ? '<span class="dm-row-time"></span>' : '<span class="dm-row-time dm-row-time--empty"></span>'}
|
${item.lastMessageTimeMs ? '<span class="dm-row-time"></span>' : '<span class="dm-row-time dm-row-time--empty"></span>'}
|
||||||
|
<span class="dm-chevron">${SVG_CHEVRON}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
const titleEl = row.querySelector('.dm-row-title');
|
const titleEl = row.querySelector('.dm-row-title');
|
||||||
const previewEl = row.querySelector('.dm-row-last-message');
|
const previewEl = row.querySelector('.dm-row-last-message');
|
||||||
const timeEl = row.querySelector('.dm-row-time');
|
const timeEl = row.querySelector('.dm-row-time');
|
||||||
if (titleEl) titleEl.textContent = String(item.name || '');
|
if (titleEl) titleEl.textContent = String(item.peerLogin || '');
|
||||||
if (previewEl) previewEl.textContent = resolveLastMessagePreview(item.lastMessage) || 'Диалог пока пуст.';
|
if (previewEl) previewEl.textContent = 'Загрузка…';
|
||||||
if (timeEl) timeEl.textContent = String(item.time || '');
|
if (timeEl) timeEl.textContent = formatChatRowTime(item.lastMessageTimeMs);
|
||||||
row.prepend(avatarWrap);
|
row.prepend(avatarWrap);
|
||||||
row.addEventListener('click', () => navigate(`chat/${encodeURIComponent(normalizeDmChatId(item.id))}`));
|
void resolveDialogPreview(item).then((text) => {
|
||||||
|
if (!previewEl?.isConnected) return;
|
||||||
|
previewEl.textContent = String(text || '').trim() || 'Диалог пока пуст.';
|
||||||
|
});
|
||||||
|
row.addEventListener('click', () => navigate(`chat/${encodeURIComponent(normalizeDmChatId(item.peerLogin))}`));
|
||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,62 +339,64 @@ export function render({ navigate, chrome }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const relations = await loadCurrentRelations();
|
const payload = await authService.listContacts();
|
||||||
const contacts = relations.outContacts || [];
|
const dialogs = Array.isArray(payload?.dialogs) ? payload.dialogs : [];
|
||||||
|
const contacts = Array.isArray(payload?.contacts) ? payload.contacts : [];
|
||||||
setContacts(contacts);
|
setContacts(contacts);
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|
||||||
const contactRows = contacts.map((login) => {
|
const byPeer = new Map();
|
||||||
const preview = directMessages.find((item) => item.id.toLowerCase() === login.toLowerCase());
|
dialogs.forEach((dialog) => {
|
||||||
const canonicalLogin = normalizeDmChatId(login);
|
const peerLogin = String(dialog?.peerLogin || '').trim();
|
||||||
const chat = getChatMessages(canonicalLogin);
|
if (!peerLogin) return;
|
||||||
const lastChat = chat[chat.length - 1];
|
const key = peerLogin.toLowerCase();
|
||||||
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
|
const relationFlag = normalizeRelationFlag(dialog?.relationFlag);
|
||||||
const lastTimeMs = Number(lastChat?.createdAtMs || 0);
|
const next = {
|
||||||
return {
|
id: peerLogin,
|
||||||
id: canonicalLogin,
|
peerLogin,
|
||||||
name: preview?.name || login,
|
relationFlag,
|
||||||
lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.',
|
lastMessageBlobB64: String(dialog?.lastMessageBlobB64 || ''),
|
||||||
time: formatChatRowTime(lastTimeMs),
|
lastMessageTimeMs: Number(dialog?.lastMessageTimeMs || 0),
|
||||||
unread,
|
unreadCount: Number(dialog?.unreadCount || 0),
|
||||||
notInContacts: false,
|
hasDialog: Boolean(dialog?.hasDialog),
|
||||||
lastTimeMs,
|
|
||||||
};
|
};
|
||||||
|
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 allChatIds = Object.keys(state.chats || {})
|
const rows = Array.from(byPeer.values()).sort((a, b) => {
|
||||||
.filter((id) => id && id.toLowerCase() !== String(state.session.login || '').toLowerCase())
|
const orderA = relationOrder(a.relationFlag);
|
||||||
.filter((id) => (getChatMessages(id) || []).length > 0);
|
const orderB = relationOrder(b.relationFlag);
|
||||||
|
if (orderA !== orderB) return orderA - orderB;
|
||||||
const contactKeys = new Set(contacts.map((x) => String(x || '').toLowerCase()));
|
return compareChatRows(a, b);
|
||||||
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,
|
|
||||||
lastTimeMs,
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const rows = [...contactRows, ...extraRows].sort(compareChatRows);
|
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'card meta-muted';
|
empty.className = 'card meta-muted';
|
||||||
empty.textContent = 'Пока нет ни контактов, ни сообщений';
|
empty.textContent = 'Пока нет диалогов';
|
||||||
list.append(empty);
|
list.append(empty);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
rows.forEach((item) => list.append(renderRow(item)));
|
let dividerInserted = false;
|
||||||
|
rows.forEach((item) => {
|
||||||
|
if (!dividerInserted && normalizeRelationFlag(item.relationFlag) === 'none' && list.childNodes.length > 0) {
|
||||||
|
const divider = document.createElement('div');
|
||||||
|
divider.className = 'dm-divider';
|
||||||
|
list.append(divider);
|
||||||
|
dividerInserted = true;
|
||||||
|
}
|
||||||
|
list.append(renderRow(item));
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (isSessionInvalidError(error)) {
|
if (isSessionInvalidError(error)) {
|
||||||
list.innerHTML = '';
|
list.innerHTML = '';
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { renderHeader } from '../components/header.js';
|
import { renderHeader } from '../components/header.js';
|
||||||
|
import { createOverflowDots } from '../components/overflow-dots.js';
|
||||||
import { authService, state } from '../state.js';
|
import { authService, state } from '../state.js';
|
||||||
import { makeProfileRoute } from '../services/shine-routes.js';
|
import { makeProfileRoute } from '../services/shine-routes.js';
|
||||||
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
import { makeProfileLinksRoute } from '../services/shine-routes.js';
|
||||||
@@ -493,18 +494,113 @@ export function render({ navigate, route, chrome } = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let topMenuPortal = null;
|
||||||
|
let topMenuBackdrop = null;
|
||||||
|
let topMenuButton = null;
|
||||||
|
|
||||||
|
const closeTopMenu = () => {
|
||||||
|
topMenuPortal?.remove();
|
||||||
|
topMenuBackdrop?.remove();
|
||||||
|
topMenuPortal = null;
|
||||||
|
topMenuBackdrop = null;
|
||||||
|
topMenuButton?.setAttribute('aria-expanded', 'false');
|
||||||
|
topMenuButton?.classList.remove('menu-open-pressed');
|
||||||
|
};
|
||||||
|
|
||||||
|
const positionTopMenu = () => {
|
||||||
|
if (!topMenuPortal || !topMenuButton) return;
|
||||||
|
const rect = topMenuButton.getBoundingClientRect();
|
||||||
|
const margin = 10;
|
||||||
|
const menuRect = topMenuPortal.getBoundingClientRect();
|
||||||
|
const menuWidth = menuRect.width || topMenuPortal.offsetWidth || 190;
|
||||||
|
const menuHeight = menuRect.height || topMenuPortal.offsetHeight || 56;
|
||||||
|
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
||||||
|
const below = rect.bottom + 7;
|
||||||
|
const top = below + menuHeight <= window.innerHeight - margin
|
||||||
|
? below
|
||||||
|
: Math.max(margin, rect.top - menuHeight - 7);
|
||||||
|
topMenuPortal.style.left = `${Math.round(left)}px`;
|
||||||
|
topMenuPortal.style.top = `${Math.round(top)}px`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const openTopMenu = () => {
|
||||||
|
if (!topMenuButton || topMenuPortal) return;
|
||||||
|
const portal = document.createElement('div');
|
||||||
|
portal.className = 'dm-head-menu dm-head-menu--portal network-head-menu';
|
||||||
|
portal.setAttribute('role', 'menu');
|
||||||
|
portal.innerHTML = `
|
||||||
|
<button type="button" class="dm-head-menu-item network-head-menu-item" role="menuitem" data-action="find-person">
|
||||||
|
<span>Найти человека</span>
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
portal.querySelector('[data-action="find-person"]')?.addEventListener('click', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
event.stopPropagation();
|
||||||
|
closeTopMenu();
|
||||||
|
openSearchModal();
|
||||||
|
});
|
||||||
|
portal.addEventListener('click', (event) => event.stopPropagation());
|
||||||
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'ui-menu-dim-layer';
|
||||||
|
backdrop.addEventListener('click', closeTopMenu);
|
||||||
|
document.body.append(backdrop, portal);
|
||||||
|
topMenuBackdrop = backdrop;
|
||||||
|
topMenuPortal = portal;
|
||||||
|
topMenuButton.setAttribute('aria-expanded', 'true');
|
||||||
|
topMenuButton.classList.add('menu-open-pressed');
|
||||||
|
positionTopMenu();
|
||||||
|
requestAnimationFrame(positionTopMenu);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleTopMenu = (event) => {
|
||||||
|
event?.preventDefault?.();
|
||||||
|
event?.stopPropagation?.();
|
||||||
|
if (topMenuPortal) closeTopMenu();
|
||||||
|
else openTopMenu();
|
||||||
|
};
|
||||||
|
|
||||||
const header = renderHeader({
|
const header = renderHeader({
|
||||||
title: 'Связи',
|
title: 'Связи',
|
||||||
rightActions: [
|
rightActions: [
|
||||||
{ label: 'Найти', onClick: openSearchModal },
|
{
|
||||||
|
iconNode: createOverflowDots(),
|
||||||
|
className: 'network-top-more-btn',
|
||||||
|
title: 'Ещё действия',
|
||||||
|
ariaLabel: 'Меню связей',
|
||||||
|
onClick: toggleTopMenu,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
// «Связи» используют тот же общий topbar, что и остальные страницы.
|
// «Связи» используют тот же общий topbar, что и остальные страницы.
|
||||||
// Отдельный класс нужен только для page-specific fade графа, не для геометрии header.
|
// Отдельный класс нужен только для page-specific fade графа, не для геометрии header.
|
||||||
header.classList.add('network-topbar');
|
header.classList.add('network-topbar');
|
||||||
|
topMenuButton = header.querySelector('.network-top-more-btn');
|
||||||
|
topMenuButton?.setAttribute('aria-haspopup', 'menu');
|
||||||
|
topMenuButton?.setAttribute('aria-expanded', 'false');
|
||||||
|
|
||||||
|
const onTopMenuOutsideClick = (event) => {
|
||||||
|
if (!topMenuPortal) return;
|
||||||
|
if (topMenuPortal.contains(event.target) || topMenuButton?.contains(event.target)) return;
|
||||||
|
closeTopMenu();
|
||||||
|
};
|
||||||
|
const onTopMenuKeydown = (event) => {
|
||||||
|
if (event.key !== 'Escape' || !topMenuPortal) return;
|
||||||
|
closeTopMenu();
|
||||||
|
topMenuButton?.focus();
|
||||||
|
};
|
||||||
|
const onTopMenuViewportChange = () => positionTopMenu();
|
||||||
|
document.addEventListener('click', onTopMenuOutsideClick);
|
||||||
|
document.addEventListener('keydown', onTopMenuKeydown);
|
||||||
|
window.addEventListener('resize', onTopMenuViewportChange, { passive: true });
|
||||||
|
window.addEventListener('scroll', onTopMenuViewportChange, { passive: true, capture: true });
|
||||||
|
|
||||||
// Ресайз и перерисовку рёбер движок обрабатывает сам (window resize + ResizeObserver внутри).
|
// Ресайз и перерисовку рёбер движок обрабатывает сам (window resize + ResizeObserver внутри).
|
||||||
screen.cleanup = () => {
|
screen.cleanup = () => {
|
||||||
|
closeTopMenu();
|
||||||
|
document.removeEventListener('click', onTopMenuOutsideClick);
|
||||||
|
document.removeEventListener('keydown', onTopMenuKeydown);
|
||||||
|
window.removeEventListener('resize', onTopMenuViewportChange);
|
||||||
|
window.removeEventListener('scroll', onTopMenuViewportChange, true);
|
||||||
if (engine) engine.destroy();
|
if (engine) engine.destroy();
|
||||||
engine = null;
|
engine = null;
|
||||||
appScreenEl?.classList.remove('network-scroll-lock');
|
appScreenEl?.classList.remove('network-scroll-lock');
|
||||||
|
|||||||
@@ -114,11 +114,15 @@ export function render({ navigate, chrome }) {
|
|||||||
const profileMenuButton = topbar.querySelector('.profile-head-menu-btn');
|
const profileMenuButton = topbar.querySelector('.profile-head-menu-btn');
|
||||||
profileMenuButton?.append(createOverflowDots());
|
profileMenuButton?.append(createOverflowDots());
|
||||||
let profileMenuPortal = null;
|
let profileMenuPortal = null;
|
||||||
|
let profileMenuBackdrop = null;
|
||||||
|
|
||||||
const closeProfileMenu = () => {
|
const closeProfileMenu = () => {
|
||||||
profileMenuPortal?.remove();
|
profileMenuPortal?.remove();
|
||||||
|
profileMenuBackdrop?.remove();
|
||||||
profileMenuPortal = null;
|
profileMenuPortal = null;
|
||||||
|
profileMenuBackdrop = null;
|
||||||
profileMenuButton?.setAttribute('aria-expanded', 'false');
|
profileMenuButton?.setAttribute('aria-expanded', 'false');
|
||||||
|
profileMenuButton?.classList.remove('menu-open-pressed');
|
||||||
profileMenuWrap?.classList.remove('is-open');
|
profileMenuWrap?.classList.remove('is-open');
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -126,10 +130,16 @@ export function render({ navigate, chrome }) {
|
|||||||
if (!profileMenuPortal || !profileMenuButton) return;
|
if (!profileMenuPortal || !profileMenuButton) return;
|
||||||
const rect = profileMenuButton.getBoundingClientRect();
|
const rect = profileMenuButton.getBoundingClientRect();
|
||||||
const margin = 10;
|
const margin = 10;
|
||||||
const menuWidth = profileMenuPortal.offsetWidth || 224;
|
const menuRect = profileMenuPortal.getBoundingClientRect();
|
||||||
|
const menuWidth = menuRect.width || profileMenuPortal.offsetWidth || 210;
|
||||||
|
const menuHeight = menuRect.height || profileMenuPortal.offsetHeight || 144;
|
||||||
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
const left = Math.max(margin, Math.min(window.innerWidth - menuWidth - margin, rect.right - menuWidth));
|
||||||
|
const below = rect.bottom + 7;
|
||||||
|
const top = below + menuHeight <= window.innerHeight - margin
|
||||||
|
? below
|
||||||
|
: Math.max(margin, rect.top - menuHeight - 7);
|
||||||
profileMenuPortal.style.left = `${Math.round(left)}px`;
|
profileMenuPortal.style.left = `${Math.round(left)}px`;
|
||||||
profileMenuPortal.style.top = `${Math.round(rect.bottom + 7)}px`;
|
profileMenuPortal.style.top = `${Math.round(top)}px`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const openProfileMenu = () => {
|
const openProfileMenu = () => {
|
||||||
@@ -139,15 +149,12 @@ export function render({ navigate, chrome }) {
|
|||||||
portal.setAttribute('role', 'menu');
|
portal.setAttribute('role', 'menu');
|
||||||
portal.innerHTML = `
|
portal.innerHTML = `
|
||||||
<button type="button" class="dm-head-menu-item profile-head-menu-item" role="menuitem" data-top-action="profile">
|
<button type="button" class="dm-head-menu-item profile-head-menu-item" role="menuitem" data-top-action="profile">
|
||||||
<img src="/assets/profile-icon-profile.svg" alt="" aria-hidden="true" />
|
|
||||||
<span>Редактировать профиль</span>
|
<span>Редактировать профиль</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="dm-head-menu-item profile-head-menu-item" role="menuitem" data-top-action="wallet">
|
<button type="button" class="dm-head-menu-item profile-head-menu-item" role="menuitem" data-top-action="wallet">
|
||||||
<img src="/assets/profile-icon-wallet.svg" alt="" aria-hidden="true" />
|
|
||||||
<span>Кошелёк</span>
|
<span>Кошелёк</span>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="dm-head-menu-item profile-head-menu-item" role="menuitem" data-top-action="settings">
|
<button type="button" class="dm-head-menu-item profile-head-menu-item" role="menuitem" data-top-action="settings">
|
||||||
<img src="/assets/profile-icon-settings.svg" alt="" aria-hidden="true" />
|
|
||||||
<span>Настройки</span>
|
<span>Настройки</span>
|
||||||
</button>
|
</button>
|
||||||
`;
|
`;
|
||||||
@@ -161,11 +168,17 @@ export function render({ navigate, chrome }) {
|
|||||||
portal.querySelector('[data-top-action="settings"]')?.addEventListener('click', () => goTo('settings-view'));
|
portal.querySelector('[data-top-action="settings"]')?.addEventListener('click', () => goTo('settings-view'));
|
||||||
portal.addEventListener('click', (event) => event.stopPropagation());
|
portal.addEventListener('click', (event) => event.stopPropagation());
|
||||||
|
|
||||||
document.body.append(portal);
|
const backdrop = document.createElement('div');
|
||||||
|
backdrop.className = 'ui-menu-dim-layer';
|
||||||
|
backdrop.addEventListener('click', closeProfileMenu);
|
||||||
|
document.body.append(backdrop, portal);
|
||||||
|
profileMenuBackdrop = backdrop;
|
||||||
profileMenuPortal = portal;
|
profileMenuPortal = portal;
|
||||||
profileMenuButton.setAttribute('aria-expanded', 'true');
|
profileMenuButton.setAttribute('aria-expanded', 'true');
|
||||||
|
profileMenuButton.classList.add('menu-open-pressed');
|
||||||
profileMenuWrap?.classList.add('is-open');
|
profileMenuWrap?.classList.add('is-open');
|
||||||
positionProfileMenu();
|
positionProfileMenu();
|
||||||
|
requestAnimationFrame(positionProfileMenu);
|
||||||
};
|
};
|
||||||
|
|
||||||
profileMenuButton?.addEventListener('click', (event) => {
|
profileMenuButton?.addEventListener('click', (event) => {
|
||||||
|
|||||||
@@ -221,3 +221,289 @@
|
|||||||
0 0 12px rgba(72, 145, 255, 0.34) !important;
|
0 0 12px rgba(72, 145, 255, 0.34) !important;
|
||||||
filter: drop-shadow(0 0 4px rgba(92, 190, 255, 0.46)) !important;
|
filter: drop-shadow(0 0 4px rgba(92, 190, 255, 0.46)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ===== Dropdown menu polish: persistent overflow press + text-only glowing rows ===== */
|
||||||
|
/* Menu rows stay visually transparent in every interaction state. The menu panel itself
|
||||||
|
* remains translucent, while each action is represented only by its glowing label. */
|
||||||
|
:root .dm-head-menu-item,
|
||||||
|
:root .dm-head-menu-item:hover,
|
||||||
|
:root .dm-head-menu-item:focus,
|
||||||
|
:root .dm-head-menu-item:focus-visible,
|
||||||
|
:root .dm-head-menu-item:active,
|
||||||
|
:root .channel-menu-item,
|
||||||
|
:root .channel-menu-item:hover,
|
||||||
|
:root .channel-menu-item:focus,
|
||||||
|
:root .channel-menu-item:focus-visible,
|
||||||
|
:root .channel-menu-item:active,
|
||||||
|
:root .channel-menu-item.destructive,
|
||||||
|
:root .channel-menu-item.destructive:hover,
|
||||||
|
:root .channel-menu-item.destructive:focus-visible,
|
||||||
|
:root .channel-menu-item.destructive:active {
|
||||||
|
color: #F7FBFF !important;
|
||||||
|
background: transparent !important;
|
||||||
|
background-image: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transform: none !important;
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px rgba(92, 190, 255, 0.72),
|
||||||
|
0 0 12px rgba(72, 145, 255, 0.34) !important;
|
||||||
|
filter: drop-shadow(0 0 4px rgba(92, 190, 255, 0.34)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hover/focus is expressed only by a slightly stronger glyph glow, never a row plate. */
|
||||||
|
:root .dm-head-menu-item:hover,
|
||||||
|
:root .dm-head-menu-item:focus-visible,
|
||||||
|
:root .channel-menu-item:hover,
|
||||||
|
:root .channel-menu-item:focus-visible {
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
text-shadow:
|
||||||
|
0 0 6px rgba(110, 205, 255, 0.86),
|
||||||
|
0 0 15px rgba(72, 145, 255, 0.46) !important;
|
||||||
|
filter: drop-shadow(0 0 5px rgba(92, 190, 255, 0.46)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the overflow trigger visibly pressed for exactly as long as its menu is open. */
|
||||||
|
:root .topbar-slot .dm-head-menu-btn[aria-expanded='true'],
|
||||||
|
:root .topbar-slot .channels-top-more-btn[aria-expanded='true'],
|
||||||
|
:root .topbar-slot .network-top-more-btn[aria-expanded='true'],
|
||||||
|
:root .network-header-overlay .network-top-more-btn[aria-expanded='true'] {
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
background: rgba(0, 0, 0, 0.12) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 3px 8px rgba(0, 0, 0, 0.58),
|
||||||
|
inset 0 -1px 2px rgba(255, 255, 255, 0.08) !important;
|
||||||
|
transform: translateY(1px) scale(0.97) !important;
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px rgba(92, 190, 255, 0.72),
|
||||||
|
0 0 12px rgba(72, 145, 255, 0.34) !important;
|
||||||
|
filter: brightness(0.9) drop-shadow(0 0 4px rgba(92, 190, 255, 0.56)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== Authoritative open-menu trigger state (2026-08-23) =====
|
||||||
|
* A dedicated JS class, not :active/:focus, owns the pressed state while a menu is open.
|
||||||
|
* Kept at the very end of the stylesheet so older compatibility rules cannot cancel it. */
|
||||||
|
:root .dm-head-menu-btn.menu-open-pressed,
|
||||||
|
:root .channels-top-more-btn.menu-open-pressed,
|
||||||
|
:root .network-top-more-btn.menu-open-pressed,
|
||||||
|
:root .topbar-slot .dm-head-menu-btn.menu-open-pressed,
|
||||||
|
:root .topbar-slot .channels-top-more-btn.menu-open-pressed,
|
||||||
|
:root .topbar-slot .network-top-more-btn.menu-open-pressed,
|
||||||
|
:root .network-header-overlay .network-top-more-btn.menu-open-pressed {
|
||||||
|
color: #FFFFFF !important;
|
||||||
|
background: rgba(0, 0, 0, 0.14) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 3px 8px rgba(0, 0, 0, 0.62),
|
||||||
|
inset 0 -1px 2px rgba(255, 255, 255, 0.09) !important;
|
||||||
|
transform: translateY(1px) scale(0.97) !important;
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px rgba(92, 190, 255, 0.76),
|
||||||
|
0 0 12px rgba(72, 145, 255, 0.38) !important;
|
||||||
|
filter: brightness(0.9) drop-shadow(0 0 4px rgba(92, 190, 255, 0.58)) !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Final dropdown behavior: truly transparent menu + persistent pressed trigger =====
|
||||||
|
* The dropdown itself has no visual surface. Only the global dim layer affects content
|
||||||
|
* behind it. The overflow trigger gets its own inset pseudo-layer so the pressed state
|
||||||
|
* cannot be cancelled by the generic button reset after pointer release. */
|
||||||
|
:root .dm-head-menu,
|
||||||
|
:root .dm-head-menu--portal,
|
||||||
|
:root .channel-menu-wrap,
|
||||||
|
:root .channel-menu-wrap--portal {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
background-image: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
-webkit-backdrop-filter: none !important;
|
||||||
|
backdrop-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .dm-head-menu::before,
|
||||||
|
:root .dm-head-menu::after,
|
||||||
|
:root .channel-menu-wrap::before,
|
||||||
|
:root .channel-menu-wrap::after,
|
||||||
|
:root .channel-menu-wrap--portal::before,
|
||||||
|
:root .channel-menu-wrap--portal::after {
|
||||||
|
content: none !important;
|
||||||
|
display: none !important;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* No separator or hover plate: the dropdown is literally labels over faded content. */
|
||||||
|
:root .channel-menu-divider {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .dm-head-menu-item,
|
||||||
|
:root .dm-head-menu-item:hover,
|
||||||
|
:root .dm-head-menu-item:focus,
|
||||||
|
:root .dm-head-menu-item:focus-visible,
|
||||||
|
:root .dm-head-menu-item:active,
|
||||||
|
:root .channel-menu-item,
|
||||||
|
:root .channel-menu-item:hover,
|
||||||
|
:root .channel-menu-item:focus,
|
||||||
|
:root .channel-menu-item:focus-visible,
|
||||||
|
:root .channel-menu-item:active,
|
||||||
|
:root .channel-menu-item.destructive,
|
||||||
|
:root .channel-menu-item.destructive:hover,
|
||||||
|
:root .channel-menu-item.destructive:focus-visible,
|
||||||
|
:root .channel-menu-item.destructive:active {
|
||||||
|
background: transparent !important;
|
||||||
|
background-color: transparent !important;
|
||||||
|
background-image: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Keep the channels backdrop out of the toolbar itself, matching the other pages. */
|
||||||
|
:root .channels-menu-overlay {
|
||||||
|
top: calc(var(--topbar-height, 64px) + var(--call-minimized-bar-height, 0px)) !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
left: 0 !important;
|
||||||
|
right: 0 !important;
|
||||||
|
background: rgba(0, 0, 0, 0.49) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Authoritative pressed well. It is independent from :active/:focus and survives until
|
||||||
|
* JS removes menu-open-pressed / aria-expanded=true on actual menu close. */
|
||||||
|
:root .dm-head-menu-btn,
|
||||||
|
:root .channels-top-more-btn,
|
||||||
|
:root .network-top-more-btn {
|
||||||
|
position: relative !important;
|
||||||
|
isolation: isolate;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .dm-head-menu-btn.menu-open-pressed::before,
|
||||||
|
:root .channels-top-more-btn.menu-open-pressed::before,
|
||||||
|
:root .network-top-more-btn.menu-open-pressed::before,
|
||||||
|
:root .dm-head-menu-btn[aria-expanded='true']::before,
|
||||||
|
:root .channels-top-more-btn[aria-expanded='true']::before,
|
||||||
|
:root .network-top-more-btn[aria-expanded='true']::before {
|
||||||
|
content: '' !important;
|
||||||
|
display: block !important;
|
||||||
|
position: absolute !important;
|
||||||
|
inset: 2px !important;
|
||||||
|
z-index: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
background: rgba(0, 0, 0, 0.18) !important;
|
||||||
|
background-image: none !important;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 4px 10px rgba(0, 0, 0, 0.72),
|
||||||
|
inset 0 -1px 2px rgba(255, 255, 255, 0.10) !important;
|
||||||
|
opacity: 1 !important;
|
||||||
|
transform: none !important;
|
||||||
|
filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .dm-head-menu-btn.menu-open-pressed,
|
||||||
|
:root .channels-top-more-btn.menu-open-pressed,
|
||||||
|
:root .network-top-more-btn.menu-open-pressed,
|
||||||
|
:root .dm-head-menu-btn[aria-expanded='true'],
|
||||||
|
:root .channels-top-more-btn[aria-expanded='true'],
|
||||||
|
:root .network-top-more-btn[aria-expanded='true'] {
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
transform: translateY(1px) scale(0.97) !important;
|
||||||
|
filter: brightness(0.90) drop-shadow(0 0 4px rgba(92, 190, 255, 0.58)) !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root .dm-head-menu-btn.menu-open-pressed > *,
|
||||||
|
:root .channels-top-more-btn.menu-open-pressed > *,
|
||||||
|
:root .network-top-more-btn.menu-open-pressed > *,
|
||||||
|
:root .dm-head-menu-btn[aria-expanded='true'] > *,
|
||||||
|
:root .channels-top-more-btn[aria-expanded='true'] > *,
|
||||||
|
:root .network-top-more-btn[aria-expanded='true'] > * {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Definitive open overflow state + lighter menu dim (2026-08-23) =====
|
||||||
|
* The generic button reset above has specificity 0,4,1, so the persistent
|
||||||
|
* menu state must beat it directly on the real button (not on ::before).
|
||||||
|
* Keep this visually identical to the physical :active press until JS closes the menu. */
|
||||||
|
:root body .topbar-slot button.dm-head-menu-btn.menu-open-pressed,
|
||||||
|
:root body .topbar-slot button.dm-head-menu-btn[aria-expanded='true'],
|
||||||
|
:root body .topbar-slot button.channels-top-more-btn.menu-open-pressed,
|
||||||
|
:root body .topbar-slot button.channels-top-more-btn[aria-expanded='true'],
|
||||||
|
:root body .topbar-slot button.network-top-more-btn.menu-open-pressed,
|
||||||
|
:root body .topbar-slot button.network-top-more-btn[aria-expanded='true'],
|
||||||
|
:root body .network-header-overlay button.network-top-more-btn.menu-open-pressed,
|
||||||
|
:root body .network-header-overlay button.network-top-more-btn[aria-expanded='true'] {
|
||||||
|
color: #ffffff !important;
|
||||||
|
background: rgba(0, 0, 0, 0.12) !important;
|
||||||
|
background-image: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
box-shadow:
|
||||||
|
inset 0 3px 8px rgba(0, 0, 0, 0.58),
|
||||||
|
inset 0 -1px 2px rgba(255, 255, 255, 0.08) !important;
|
||||||
|
transform: translateY(1px) scale(0.97) !important;
|
||||||
|
filter: brightness(0.9) drop-shadow(0 0 4px rgba(92, 190, 255, 0.56)) !important;
|
||||||
|
animation: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The menu itself is still fully transparent; only the content below is dimmed.
|
||||||
|
* Previous alpha was ~0.49. Reduce it by about 20% relatively -> ~0.39. */
|
||||||
|
:root body .ui-menu-dim-layer,
|
||||||
|
:root body .dm-head-menu-backdrop,
|
||||||
|
:root body .channels-menu-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.39) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Round persistent overflow press + subtle background depth (2026-08-23) =====
|
||||||
|
* The open overflow control is a circular recessed well. While a topbar menu is
|
||||||
|
* present, only the content plane below the toolbar recedes slightly; the toolbar
|
||||||
|
* and the fully transparent menu stay at full size. */
|
||||||
|
:root body .topbar-slot button.dm-head-menu-btn.menu-open-pressed,
|
||||||
|
:root body .topbar-slot button.dm-head-menu-btn[aria-expanded='true'],
|
||||||
|
:root body .topbar-slot button.channels-top-more-btn.menu-open-pressed,
|
||||||
|
:root body .topbar-slot button.channels-top-more-btn[aria-expanded='true'],
|
||||||
|
:root body .topbar-slot button.network-top-more-btn.menu-open-pressed,
|
||||||
|
:root body .topbar-slot button.network-top-more-btn[aria-expanded='true'],
|
||||||
|
:root body .network-header-overlay button.network-top-more-btn.menu-open-pressed,
|
||||||
|
:root body .network-header-overlay button.network-top-more-btn[aria-expanded='true'] {
|
||||||
|
border-radius: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root body .topbar-slot button.dm-head-menu-btn.menu-open-pressed::before,
|
||||||
|
:root body .topbar-slot button.dm-head-menu-btn[aria-expanded='true']::before,
|
||||||
|
:root body .topbar-slot button.channels-top-more-btn.menu-open-pressed::before,
|
||||||
|
:root body .topbar-slot button.channels-top-more-btn[aria-expanded='true']::before,
|
||||||
|
:root body .topbar-slot button.network-top-more-btn.menu-open-pressed::before,
|
||||||
|
:root body .topbar-slot button.network-top-more-btn[aria-expanded='true']::before,
|
||||||
|
:root body .network-header-overlay button.network-top-more-btn.menu-open-pressed::before,
|
||||||
|
:root body .network-header-overlay button.network-top-more-btn[aria-expanded='true']::before {
|
||||||
|
border-radius: 50% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Depth cue: the feed/graph plane gently recedes while a topbar overflow menu is open.
|
||||||
|
* Keep the scale small enough that opening the menu does not feel like a page jump. */
|
||||||
|
@supports selector(body:has(*)) {
|
||||||
|
:root body .app-shell > .screen-content {
|
||||||
|
transform-origin: 50% 50%;
|
||||||
|
transition: transform 180ms cubic-bezier(0.22, 0.61, 0.36, 1), filter 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root body:has(> .ui-menu-dim-layer) .app-shell > .screen-content,
|
||||||
|
:root body:has(> #modal-root #channels-top-menu-overlay) .app-shell > .screen-content {
|
||||||
|
transform: scale(0.975);
|
||||||
|
filter: brightness(0.97) saturate(0.96);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
:root body .app-shell > .screen-content {
|
||||||
|
transition: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+330
-26
@@ -9476,19 +9476,16 @@ body.chat-topbar-overlay .composer-slot > * {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ===== Connections: restore canonical shared topbar (2026-08-22 14:00) =====
|
/* ===== Connections: shared topbar fade + edge fade (2026-08-23) =====
|
||||||
* The toolbar itself is the standard renderHeader() mounted in #topbar-slot.
|
* Header geometry stays canonical, but the graph itself now continues under the
|
||||||
* Network-specific positioning must never move .screen-content under it.
|
* shared topbar exactly like scrollable message content. The generic topbar
|
||||||
* The graph fade lives inside .network-stage instead, so canvas layout cannot
|
* overlay is therefore the source of the upper fade. */
|
||||||
* change toolbar height/position or overlap its controls. */
|
|
||||||
.topbar-slot:has(> .network-topbar)::before {
|
.topbar-slot:has(> .network-topbar)::before {
|
||||||
/* Keep the generic toolbar background/fade inside the toolbar bounds only. */
|
height: calc(100% + 48px) !important;
|
||||||
height: 100% !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Undo the previous Connections-only full-bleed-under-toolbar geometry. */
|
|
||||||
.screen-content.network-scroll-lock {
|
.screen-content.network-scroll-lock {
|
||||||
top: calc(var(--call-minimized-bar-height, 0px) + var(--topbar-height, 0px)) !important;
|
top: var(--call-minimized-bar-height, 0px) !important;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: calc(var(--toolbar-height, 78px) + var(--composer-height, 0px));
|
bottom: calc(var(--toolbar-height, 78px) + var(--composer-height, 0px));
|
||||||
@@ -9503,34 +9500,37 @@ body.chat-topbar-overlay .composer-slot > * {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Filters return to the same content offset they had before the toolbar-overlay experiment. */
|
/* Filters stay below the visible topbar while the movable graph runs beneath it. */
|
||||||
.network-stage > .fg-filter-bar.app-top-tabs {
|
.network-stage > .fg-filter-bar.app-top-tabs {
|
||||||
top: var(--app-primary-tabs-top-gap, 14px) !important;
|
top: calc(var(--topbar-height, 64px) + var(--app-primary-tabs-top-gap, 14px)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fade belongs to the graph surface, not to the toolbar. It sits above graph
|
/* The graph can be panned in every direction, so its side and bottom edges fade
|
||||||
* nodes/edges but below filter chips (filter bar z-index is 11). */
|
* into the app canvas as well. The top edge is intentionally omitted here: it
|
||||||
|
* is handled by .topbar-slot::before, matching messages/chats. */
|
||||||
.network-stage::before {
|
.network-stage::before {
|
||||||
content: '';
|
content: '';
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
inset: 0;
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
height: 58px;
|
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: linear-gradient(
|
background:
|
||||||
to bottom,
|
linear-gradient(to right,
|
||||||
|
rgba(5, 7, 10, 0.98) 0%,
|
||||||
|
rgba(5, 7, 10, 0.58) 3%,
|
||||||
|
rgba(5, 7, 10, 0) 10%) left / 50% 100% no-repeat,
|
||||||
|
linear-gradient(to left,
|
||||||
|
rgba(5, 7, 10, 0.98) 0%,
|
||||||
|
rgba(5, 7, 10, 0.58) 3%,
|
||||||
|
rgba(5, 7, 10, 0) 10%) right / 50% 100% no-repeat,
|
||||||
|
linear-gradient(to top,
|
||||||
rgba(5, 7, 10, 1) 0%,
|
rgba(5, 7, 10, 1) 0%,
|
||||||
rgba(5, 7, 10, 0.86) 28%,
|
rgba(5, 7, 10, 0.72) 4%,
|
||||||
rgba(5, 7, 10, 0.48) 58%,
|
rgba(5, 7, 10, 0.28) 8%,
|
||||||
rgba(5, 7, 10, 0.16) 82%,
|
rgba(5, 7, 10, 0) 14%) bottom / 100% 50% no-repeat;
|
||||||
rgba(5, 7, 10, 0) 100%
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Explicitly keep the Connections header on the canonical topbar contract.
|
/* Explicitly keep the Connections header on the canonical shared topbar contract. */
|
||||||
* No network-only absolute positioning, dimensions or padding. */
|
|
||||||
.topbar-slot > .page-header.app-topbar-shell.network-topbar {
|
.topbar-slot > .page-header.app-topbar-shell.network-topbar {
|
||||||
position: relative !important;
|
position: relative !important;
|
||||||
inset: auto !important;
|
inset: auto !important;
|
||||||
@@ -9551,3 +9551,307 @@ body.chat-topbar-overlay .composer-slot > * {
|
|||||||
backdrop-filter: none !important;
|
backdrop-filter: none !important;
|
||||||
-webkit-backdrop-filter: none !important;
|
-webkit-backdrop-filter: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Уведомления: без рамки должны быть не только реальные Ответы/События, но и
|
||||||
|
* пустые/ошибочные состояния, потому что все они являются прямыми card-детьми списка. */
|
||||||
|
.notifications-screen .notifications-list > .card,
|
||||||
|
.notifications-screen .notifications-list > .card:hover,
|
||||||
|
.notifications-screen .notifications-list > .card:focus,
|
||||||
|
.notifications-screen .notifications-list > .card:focus-visible,
|
||||||
|
.notifications-screen .notifications-list > .card:active {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===== Connections: unified graph fade without overlay seams (2026-08-23) =====
|
||||||
|
* Side/bottom fade is applied to the graph itself (including its nebula glow),
|
||||||
|
* instead of painting dark gradients above it. This keeps the center glow and
|
||||||
|
* the edge attenuation in one composited image, so no visible fade stripe can
|
||||||
|
* cut through the nebula while the graph is panned. */
|
||||||
|
.network-stage::before {
|
||||||
|
content: none !important;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.screen-content.network-scroll-lock .network-board--full.fg-stage {
|
||||||
|
-webkit-mask-image: radial-gradient(
|
||||||
|
ellipse 62% 118% at 50% -4%,
|
||||||
|
#000 0%,
|
||||||
|
#000 68%,
|
||||||
|
rgba(0, 0, 0, 0.96) 75%,
|
||||||
|
rgba(0, 0, 0, 0.78) 82%,
|
||||||
|
rgba(0, 0, 0, 0.48) 89%,
|
||||||
|
rgba(0, 0, 0, 0.18) 95%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
mask-image: radial-gradient(
|
||||||
|
ellipse 62% 118% at 50% -4%,
|
||||||
|
#000 0%,
|
||||||
|
#000 68%,
|
||||||
|
rgba(0, 0, 0, 0.96) 75%,
|
||||||
|
rgba(0, 0, 0, 0.78) 82%,
|
||||||
|
rgba(0, 0, 0, 0.48) 89%,
|
||||||
|
rgba(0, 0, 0, 0.18) 95%,
|
||||||
|
transparent 100%
|
||||||
|
);
|
||||||
|
-webkit-mask-repeat: no-repeat;
|
||||||
|
mask-repeat: no-repeat;
|
||||||
|
-webkit-mask-size: 100% 100%;
|
||||||
|
mask-size: 100% 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Connections uses the same compact overflow trigger as Contacts, Channels and Profile. */
|
||||||
|
.topbar-slot .network-top-more-btn {
|
||||||
|
width: 40px;
|
||||||
|
min-width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 0;
|
||||||
|
display: inline-grid;
|
||||||
|
place-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== Unified top/dropdown menus (2026-08-23) ===== */
|
||||||
|
.ui-menu-dim-layer {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc(var(--topbar-height, 64px) + var(--call-minimized-bar-height, 0px));
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9998;
|
||||||
|
background: rgba(0, 0, 0, 0.51);
|
||||||
|
border: 0;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu,
|
||||||
|
.channel-menu-wrap,
|
||||||
|
.channel-menu-wrap--portal {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu {
|
||||||
|
min-width: 0;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: rgba(5, 7, 10, 0.92);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu::before { content: none !important; display: none !important; }
|
||||||
|
|
||||||
|
.dm-head-menu--portal { z-index: 10000; }
|
||||||
|
|
||||||
|
.dm-head-menu-item,
|
||||||
|
.channel-menu-item {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 40px;
|
||||||
|
padding: 7px 10px;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
outline: 0;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
color: rgba(255, 255, 255, 0.95) !important;
|
||||||
|
font-family: var(--font-main);
|
||||||
|
font-size: var(--app-topbar-title-size, 18px);
|
||||||
|
line-height: 1.2;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0;
|
||||||
|
text-align: left;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-item svg,
|
||||||
|
.dm-head-menu-item img { display: none !important; }
|
||||||
|
|
||||||
|
.dm-head-menu-item:hover,
|
||||||
|
.dm-head-menu-item:focus-visible,
|
||||||
|
.dm-head-menu-item:active,
|
||||||
|
.channel-menu-item:hover,
|
||||||
|
.channel-menu-item:focus-visible,
|
||||||
|
.channel-menu-item:active {
|
||||||
|
background: transparent !important;
|
||||||
|
color: #fff !important;
|
||||||
|
text-shadow: 0 0 10px rgba(255, 255, 255, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-menu-wrap {
|
||||||
|
width: max-content;
|
||||||
|
min-width: 180px;
|
||||||
|
max-width: calc(100vw - 24px);
|
||||||
|
padding: 8px 10px;
|
||||||
|
gap: 0;
|
||||||
|
background: rgba(5, 7, 10, 0.92);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
-webkit-backdrop-filter: blur(16px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channels-menu-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.51) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-menu-divider {
|
||||||
|
border: 0 !important;
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== Unified topbar dropdown menus cleanup (2026-08-23) =====
|
||||||
|
* - no menu outline/contour
|
||||||
|
* - dim lower layers ~51% while menu is open
|
||||||
|
* - menu rows are text-only using the same core type rhythm as the topbar
|
||||||
|
* - shared positioning helpers clamp menus to the viewport */
|
||||||
|
.dm-head-menu-backdrop {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
top: calc(var(--topbar-height, 64px) + var(--call-minimized-bar-height, 0px));
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 9998;
|
||||||
|
background: rgba(5, 7, 10, 0.51);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channels-menu-overlay {
|
||||||
|
background: rgba(5, 7, 10, 0.51);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu,
|
||||||
|
.channel-menu-wrap {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: none !important;
|
||||||
|
box-shadow: 0 18px 42px rgba(0, 0, 0, 0.34) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu {
|
||||||
|
min-width: 164px;
|
||||||
|
max-width: min(240px, calc(100vw - 20px));
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: linear-gradient(180deg, rgba(12, 16, 24, 0.96), rgba(8, 11, 18, 0.96));
|
||||||
|
backdrop-filter: blur(18px);
|
||||||
|
-webkit-backdrop-filter: blur(18px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu::before {
|
||||||
|
content: none !important;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-item,
|
||||||
|
.channel-menu-item {
|
||||||
|
width: 100%;
|
||||||
|
min-height: 42px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border: 0 !important;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: transparent !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
gap: 0;
|
||||||
|
font-family: var(--font-main);
|
||||||
|
font-size: 18px;
|
||||||
|
line-height: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--app-topbar-gold);
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px var(--app-topbar-blue-glow),
|
||||||
|
0 0 12px var(--app-topbar-blue-glow-soft);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-item > svg,
|
||||||
|
.dm-head-menu-item > img {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-item > span {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-item:hover,
|
||||||
|
.dm-head-menu-item:focus-visible,
|
||||||
|
.dm-head-menu-item:active,
|
||||||
|
.channel-menu-item:hover,
|
||||||
|
.channel-menu-item:focus-visible,
|
||||||
|
.channel-menu-item:active {
|
||||||
|
outline: none;
|
||||||
|
background: rgba(255, 255, 255, 0.05) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-menu-item.destructive {
|
||||||
|
color: var(--app-topbar-gold);
|
||||||
|
text-shadow:
|
||||||
|
0 0 5px rgba(255, 124, 156, 0.35),
|
||||||
|
0 0 12px rgba(255, 124, 156, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.channel-menu-divider {
|
||||||
|
background: rgba(255, 255, 255, 0.08) !important;
|
||||||
|
margin: 4px 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-wrap.is-open .dm-head-menu-btn,
|
||||||
|
.dm-head-menu-btn:focus-visible,
|
||||||
|
.topbar-slot .channels-top-more-btn:focus-visible,
|
||||||
|
.topbar-slot .channels-top-more-btn[aria-expanded="true"] {
|
||||||
|
background: rgba(255, 255, 255, 0.04);
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* ===== Transparent topbar-like dropdown surface (2026-08-23) =====
|
||||||
|
* The lower UI stays visible through the menu, just dimmed by the backdrop.
|
||||||
|
* No opaque card/contour: labels float over the same faded content language as the top toolbar. */
|
||||||
|
.ui-menu-dim-layer,
|
||||||
|
.dm-head-menu-backdrop {
|
||||||
|
background: rgba(0, 0, 0, 0.39) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.channels-menu-overlay {
|
||||||
|
background: rgba(0, 0, 0, 0.39) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu,
|
||||||
|
.dm-head-menu--portal,
|
||||||
|
.channel-menu-wrap,
|
||||||
|
.channel-menu-wrap--portal {
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
background: rgba(5, 7, 10, 0.10) !important;
|
||||||
|
background-image: none !important;
|
||||||
|
-webkit-backdrop-filter: blur(8px) brightness(0.82) !important;
|
||||||
|
backdrop-filter: blur(8px) brightness(0.82) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* The panel itself must never become an opaque plate through legacy pseudo-elements. */
|
||||||
|
.dm-head-menu::before,
|
||||||
|
.channel-menu-wrap::before,
|
||||||
|
.channel-menu-wrap--portal::before {
|
||||||
|
content: none !important;
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dm-head-menu-item,
|
||||||
|
.channel-menu-item,
|
||||||
|
.dm-head-menu-item:hover,
|
||||||
|
.dm-head-menu-item:focus,
|
||||||
|
.dm-head-menu-item:focus-visible,
|
||||||
|
.dm-head-menu-item:active,
|
||||||
|
.channel-menu-item:hover,
|
||||||
|
.channel-menu-item:focus,
|
||||||
|
.channel-menu-item:focus-visible,
|
||||||
|
.channel-menu-item:active {
|
||||||
|
background: transparent !important;
|
||||||
|
background-image: none !important;
|
||||||
|
border: 0 !important;
|
||||||
|
outline: 0 !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ body::before {
|
|||||||
--toolbar-height: 78px;
|
--toolbar-height: 78px;
|
||||||
--keyboard-offset: 0px;
|
--keyboard-offset: 0px;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
border-left: 1px solid rgba(211, 170, 86, 0.2);
|
border-left: 0;
|
||||||
border-right: 1px solid rgba(211, 170, 86, 0.2);
|
border-right: 0;
|
||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user