SHA256
Пересобрать верхний UI-бар
This commit is contained in:
+1
-1
@@ -12,7 +12,7 @@
|
||||
<link rel="apple-touch-icon" href="./img/logo.jpg" />
|
||||
<title>СИЯНИЕ</title>
|
||||
<script>
|
||||
window.__SHINE_BUILD_HASH__ = '20260819190000';
|
||||
window.__SHINE_BUILD_HASH__ = '20260822122600';
|
||||
window.__SHINE_CLIENT_VERSION__ = '1.2.10';
|
||||
</script>
|
||||
<script>
|
||||
|
||||
+5
-5
@@ -81,17 +81,17 @@ 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 solanaRpcCheckView from './pages/solana-rpc-check-view.js';
|
||||
import * as messagesList from './pages/messages-list.js';
|
||||
import * as messagesList from './pages/messages-list.js?v=202608221218';
|
||||
import * as contactSearchView from './pages/contact-search-view.js';
|
||||
import * as chatView from './pages/chat-view.js?v=202608191738';
|
||||
import * as chatView from './pages/chat-view.js?v=202608221218';
|
||||
import * as userProfileView from './pages/user-profile-view.js';
|
||||
import * as channelsList from './pages/channels-list.js';
|
||||
import * as channelsList from './pages/channels-list.js?v=202608221218';
|
||||
import * as channelView from './pages/channel-view.js';
|
||||
import * as channelThreadView from './pages/channel-thread-view.js';
|
||||
import * as addChannelView from './pages/add-channel-view.js';
|
||||
import * as addPersonalPublicChatView from './pages/add-personal-public-chat-view.js';
|
||||
import * as networkView from './pages/network-view.js';
|
||||
import * as notificationsView from './pages/notifications-view.js';
|
||||
import * as networkView from './pages/network-view.js?v=202608221226';
|
||||
import * as notificationsView from './pages/notifications-view.js?v=202608221226';
|
||||
|
||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||
const DM_UNSUPPORTED_FORMAT_TEXT = 'Формат сообщения не поддерживается';
|
||||
|
||||
@@ -1,14 +1,20 @@
|
||||
export function renderHeader({ title, leftAction, leftLabel = '', rightActions = [] }) {
|
||||
export function renderHeader({ title = '', centerNode = null, leftAction, leftLabel = '', rightActions = [] }) {
|
||||
const wrap = document.createElement('header');
|
||||
wrap.className = 'page-header';
|
||||
wrap.className = 'page-header app-topbar-shell';
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'header-left';
|
||||
if (leftAction) {
|
||||
const btn = document.createElement('button');
|
||||
btn.type = 'button';
|
||||
btn.className = 'icon-btn';
|
||||
btn.textContent = leftAction.label;
|
||||
const rawLabel = String(leftAction.label || '').trim();
|
||||
const isBackAction = rawLabel === '←' || rawLabel === '<' || rawLabel === '‹';
|
||||
btn.className = `icon-btn${isBackAction ? ' header-back-btn' : ''}`;
|
||||
btn.textContent = isBackAction ? '←' : rawLabel;
|
||||
if (isBackAction) {
|
||||
btn.setAttribute('aria-label', leftAction.ariaLabel || 'Назад');
|
||||
btn.title = leftAction.title || 'Назад';
|
||||
}
|
||||
btn.addEventListener('click', leftAction.onClick);
|
||||
left.append(btn);
|
||||
}
|
||||
@@ -19,9 +25,16 @@ export function renderHeader({ title, leftAction, leftLabel = '', rightActions =
|
||||
left.append(label);
|
||||
}
|
||||
|
||||
const h1 = document.createElement('h1');
|
||||
h1.className = 'page-title';
|
||||
h1.textContent = title;
|
||||
const center = document.createElement('div');
|
||||
center.className = 'header-center';
|
||||
if (centerNode instanceof Node) {
|
||||
center.append(centerNode);
|
||||
} else {
|
||||
const h1 = document.createElement('h1');
|
||||
h1.className = 'page-title';
|
||||
h1.textContent = title;
|
||||
center.append(h1);
|
||||
}
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.className = 'header-actions';
|
||||
@@ -40,6 +53,6 @@ export function renderHeader({ title, leftAction, leftLabel = '', rightActions =
|
||||
right.append(btn);
|
||||
});
|
||||
|
||||
wrap.append(left, h1, right);
|
||||
wrap.append(left, center, right);
|
||||
return wrap;
|
||||
}
|
||||
|
||||
@@ -1144,31 +1144,26 @@ export function render({ navigate, route, chrome }) {
|
||||
const appScreen = document.getElementById('app-screen');
|
||||
appScreen?.classList.add('channels-scroll-clean');
|
||||
|
||||
const threadHeaderButton = document.createElement('button');
|
||||
threadHeaderButton.type = 'button';
|
||||
threadHeaderButton.className = 'icon-btn channel-header-route-btn app-topbar-title-action';
|
||||
threadHeaderButton.textContent = 'Тред в канале: ...';
|
||||
threadHeaderButton.disabled = true;
|
||||
|
||||
const header = renderHeader({
|
||||
title: '',
|
||||
centerNode: threadHeaderButton,
|
||||
leftAction: { label: '<', onClick: () => navigate(resolveThreadPreviousInChannels(selector, activeResolvedChannelLabel)) },
|
||||
rightActions: [],
|
||||
rightActions: [
|
||||
{
|
||||
label: '↑',
|
||||
title: 'К списку каналов',
|
||||
ariaLabel: 'К списку каналов',
|
||||
className: 'channel-thread-list-btn',
|
||||
onClick: () => navigate('channels-list'),
|
||||
},
|
||||
],
|
||||
});
|
||||
header.classList.add('channel-thread-topbar');
|
||||
const headerLeft = header.querySelector('.header-left');
|
||||
let threadHeaderButton = null;
|
||||
if (headerLeft) {
|
||||
const channelsListButton = document.createElement('button');
|
||||
channelsListButton.type = 'button';
|
||||
channelsListButton.className = 'icon-btn';
|
||||
channelsListButton.textContent = '↑';
|
||||
channelsListButton.title = 'К списку каналов';
|
||||
channelsListButton.setAttribute('aria-label', 'К списку каналов');
|
||||
channelsListButton.addEventListener('click', () => navigate('channels-list'));
|
||||
headerLeft.append(channelsListButton);
|
||||
|
||||
threadHeaderButton = document.createElement('button');
|
||||
threadHeaderButton.type = 'button';
|
||||
threadHeaderButton.className = 'icon-btn channel-header-route-btn';
|
||||
threadHeaderButton.textContent = 'Тред в канале: ...';
|
||||
threadHeaderButton.disabled = true;
|
||||
headerLeft.append(threadHeaderButton);
|
||||
}
|
||||
chrome?.setTopbar(header);
|
||||
|
||||
const statusBox = document.createElement('div');
|
||||
|
||||
@@ -2056,19 +2056,20 @@ export function render({ navigate, route, chrome }) {
|
||||
statusBox.style.display = '';
|
||||
};
|
||||
|
||||
const channelHeaderButton = document.createElement('button');
|
||||
channelHeaderButton.type = 'button';
|
||||
channelHeaderButton.className = 'icon-btn channel-header-route-btn app-topbar-title-action';
|
||||
channelHeaderButton.textContent = 'Канал: ...';
|
||||
channelHeaderButton.disabled = true;
|
||||
|
||||
const header = renderHeader({
|
||||
title: '',
|
||||
centerNode: channelHeaderButton,
|
||||
leftAction: { label: '<', onClick: () => navigate('channels-list') },
|
||||
rightActions: [
|
||||
{ label: 'Канал: ...', className: 'channel-header-route-btn', onClick: () => {} },
|
||||
{ label: 'Оглавление', className: 'channel-header-entrypoint-btn', onClick: () => {} },
|
||||
],
|
||||
});
|
||||
const channelHeaderButton = header.querySelector('.header-actions .channel-header-route-btn');
|
||||
const channelEntrypointButton = header.querySelector('.header-actions .channel-header-entrypoint-btn');
|
||||
if (channelHeaderButton) {
|
||||
channelHeaderButton.disabled = true;
|
||||
}
|
||||
if (channelEntrypointButton) {
|
||||
channelEntrypointButton.disabled = true;
|
||||
channelEntrypointButton.hidden = true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { channels as mockChannels } from '../mock-data.js';
|
||||
import { channels as mockChannels } from '../mock-data.js';
|
||||
import { authService, setChannelsFeed, state } from '../state.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
import { parseMessageAttachments } from '../services/attachment-format.js';
|
||||
@@ -939,7 +939,6 @@ function openTopChannelsMenu({
|
||||
listState,
|
||||
anchorEl,
|
||||
navigate,
|
||||
onSubscribeChannel,
|
||||
onFindChannel,
|
||||
}) {
|
||||
closeTopChannelsMenu(listState);
|
||||
@@ -951,7 +950,7 @@ function openTopChannelsMenu({
|
||||
let left = rect.right - menuWidth;
|
||||
left = Math.max(12, Math.min(left, window.innerWidth - menuWidth - 12));
|
||||
|
||||
const estimatedHeight = 320;
|
||||
const estimatedHeight = 250;
|
||||
let top = rect.bottom + 8;
|
||||
if (top + estimatedHeight > window.innerHeight - 10) {
|
||||
top = Math.max(12, rect.top - estimatedHeight - 8);
|
||||
@@ -968,14 +967,12 @@ function openTopChannelsMenu({
|
||||
menu.style.width = `${Math.round(menuWidth)}px`;
|
||||
|
||||
const items = [
|
||||
{ label: 'Поиск', action: () => onFindChannel?.() },
|
||||
{ label: 'Новый канал', action: () => navigate('add-channel-view') },
|
||||
{ divider: true },
|
||||
{ label: 'Все каналы', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_ALL)) },
|
||||
{ label: 'Мои каналы', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_OWNED)) },
|
||||
{ label: 'Подписки', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_FOLLOWING)) },
|
||||
{ divider: true },
|
||||
{ label: 'Новый канал', action: () => navigate('add-channel-view') },
|
||||
{ divider: true },
|
||||
{ label: 'Добавить канал', action: () => onSubscribeChannel?.() },
|
||||
{ label: 'Просмотреть канал', action: () => onFindChannel?.() },
|
||||
];
|
||||
|
||||
items.forEach((item) => {
|
||||
@@ -1375,7 +1372,7 @@ export function render({ navigate, route, chrome }) {
|
||||
|
||||
const backBtn = document.createElement('button');
|
||||
backBtn.type = 'button';
|
||||
backBtn.className = 'icon-btn channels-top-back-btn';
|
||||
backBtn.className = 'icon-btn header-back-btn channels-top-back-btn';
|
||||
backBtn.textContent = '←';
|
||||
backBtn.setAttribute('aria-label', 'Назад');
|
||||
backBtn.addEventListener('click', () => navigateBack());
|
||||
@@ -1386,24 +1383,6 @@ export function render({ navigate, route, chrome }) {
|
||||
const topBarRight = document.createElement('div');
|
||||
topBarRight.className = 'channels-top-right';
|
||||
|
||||
const findChannelBtn = document.createElement('button');
|
||||
findChannelBtn.type = 'button';
|
||||
findChannelBtn.className = 'icon-btn channels-top-search-btn';
|
||||
findChannelBtn.setAttribute('aria-label', 'Найти канал');
|
||||
findChannelBtn.title = 'Найти канал';
|
||||
const findChannelIcon = document.createElement('span');
|
||||
findChannelIcon.className = 'channels-search-icon';
|
||||
findChannelIcon.setAttribute('aria-hidden', 'true');
|
||||
findChannelBtn.append(findChannelIcon);
|
||||
findChannelBtn.addEventListener('click', () => openChannelFinderModal({ navigate }));
|
||||
|
||||
const createInMyBtn = document.createElement('button');
|
||||
createInMyBtn.type = 'button';
|
||||
createInMyBtn.className = 'icon-btn channels-top-add-btn';
|
||||
createInMyBtn.textContent = '+';
|
||||
createInMyBtn.setAttribute('aria-label', 'Создать канал');
|
||||
createInMyBtn.addEventListener('click', () => navigate('add-channel-view'));
|
||||
|
||||
const topMenuBtn = document.createElement('button');
|
||||
topMenuBtn.type = 'button';
|
||||
topMenuBtn.className = 'icon-btn channels-top-more-btn';
|
||||
@@ -1418,18 +1397,12 @@ export function render({ navigate, route, chrome }) {
|
||||
anchorEl: topMenuBtn,
|
||||
navigate,
|
||||
onFindChannel: () => openChannelFinderModal({ navigate }),
|
||||
onSubscribeChannel: () => openSimpleSubscribeModal({
|
||||
kind: 'channel',
|
||||
kindLabel: 'Добавить канал',
|
||||
submitLabel: 'Добавить',
|
||||
onSuccess: async () => loadFeedAndRender({ screen, listState, contentEl, navigate }),
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
topBarLeft.append(backBtn, topTitle);
|
||||
topBarRight.append(findChannelBtn, createInMyBtn, topMenuBtn);
|
||||
topBarEl.append(topBarLeft, topBarRight);
|
||||
topBarLeft.append(backBtn);
|
||||
topBarRight.append(topMenuBtn);
|
||||
topBarEl.append(topBarLeft, topTitle, topBarRight);
|
||||
|
||||
const bottomCta = document.createElement('button');
|
||||
bottomCta.type = 'button';
|
||||
@@ -1450,10 +1423,8 @@ export function render({ navigate, route, chrome }) {
|
||||
});
|
||||
|
||||
topTitle.textContent = channelsViewTitle(listState.viewMode);
|
||||
findChannelBtn.style.display = '';
|
||||
createInMyBtn.style.display = '';
|
||||
topMenuBtn.style.display = '';
|
||||
if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle);
|
||||
if (topTitle.parentElement !== topBarEl) topBarEl.insertBefore(topTitle, topBarRight);
|
||||
|
||||
updateBottomCta({ button: bottomCta });
|
||||
};
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { renderHeader } from '../components/header.js';
|
||||
import { renderUserAvatar } from '../components/avatar-image.js';
|
||||
import { directMessages } from '../mock-data.js';
|
||||
import {
|
||||
addAppLogEntry,
|
||||
@@ -27,10 +28,55 @@ import {
|
||||
import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
|
||||
import { showToast } from '../services/channels-ux.js';
|
||||
import { buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js';
|
||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||
|
||||
export const pageMeta = { id: 'chat-view', title: 'Чат' };
|
||||
const CONVERSATION_CLEAR_NOTICE_TEXT = 'История переписки очищена с этого места';
|
||||
|
||||
function createChatHeaderParts(login) {
|
||||
const cleanLogin = String(login || '').trim() || 'unknown';
|
||||
|
||||
const avatarSlot = document.createElement('span');
|
||||
avatarSlot.className = 'chat-header-avatar-slot chat-header-avatar-slot--left';
|
||||
const initialAvatar = renderUserAvatar({
|
||||
login: cleanLogin,
|
||||
size: 'small',
|
||||
className: 'chat-header-avatar',
|
||||
title: cleanLogin,
|
||||
});
|
||||
avatarSlot.append(initialAvatar);
|
||||
|
||||
const loginEl = document.createElement('span');
|
||||
loginEl.className = 'chat-header-login';
|
||||
loginEl.setAttribute('role', 'heading');
|
||||
loginEl.setAttribute('aria-level', '1');
|
||||
loginEl.setAttribute('aria-label', `Чат с ${cleanLogin}`);
|
||||
loginEl.textContent = cleanLogin;
|
||||
|
||||
void loadProfileSnapshot(cleanLogin)
|
||||
.then((snapshot) => {
|
||||
if (!avatarSlot.isConnected) return;
|
||||
const upgradedAvatar = renderUserAvatar({
|
||||
login: cleanLogin,
|
||||
firstName: String(snapshot?.firstName || '').trim(),
|
||||
lastName: String(snapshot?.lastName || '').trim(),
|
||||
avatar: snapshot?.avatar?.txId
|
||||
? {
|
||||
ar: String(snapshot.avatar.txId || '').trim(),
|
||||
sha256Hex: String(snapshot?.avatar?.sha256Hex || '').trim().toLowerCase(),
|
||||
}
|
||||
: null,
|
||||
size: 'small',
|
||||
className: 'chat-header-avatar',
|
||||
title: cleanLogin,
|
||||
});
|
||||
avatarSlot.replaceChildren(upgradedAvatar);
|
||||
})
|
||||
.catch(() => {});
|
||||
|
||||
return { centerNode: loginEl, avatarSlot };
|
||||
}
|
||||
|
||||
function truncatePreviewText(value, maxLen = 72) {
|
||||
const normalized = String(value || '').replace(/\s+/g, ' ').trim();
|
||||
if (!normalized) return '';
|
||||
@@ -919,9 +965,9 @@ export function render({ navigate, route, chrome }) {
|
||||
const log = document.createElement('div');
|
||||
log.className = 'messages-log dm-messages-log';
|
||||
|
||||
chrome?.setTopbar(
|
||||
renderHeader({
|
||||
title: `Чат с ${contact.name}`,
|
||||
const chatHeaderParts = createChatHeaderParts(chatId);
|
||||
const chatHeader = renderHeader({
|
||||
centerNode: chatHeaderParts.centerNode,
|
||||
leftAction: { label: '←', onClick: () => navigate('messages-list') },
|
||||
rightActions: [
|
||||
{
|
||||
@@ -989,8 +1035,9 @@ export function render({ navigate, route, chrome }) {
|
||||
},
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
});
|
||||
chatHeader.querySelector('.header-left')?.append(chatHeaderParts.avatarSlot);
|
||||
chrome?.setTopbar(chatHeader);
|
||||
|
||||
if (!isKnownContact) {
|
||||
const card = document.createElement('div');
|
||||
|
||||
@@ -206,9 +206,6 @@ export function render({ navigate, chrome }) {
|
||||
window.addEventListener('resize', onMenuViewportChange, { passive: true });
|
||||
window.addEventListener('scroll', onMenuViewportChange, { passive: true, capture: true });
|
||||
|
||||
const divider = document.createElement('div');
|
||||
divider.className = 'dm-divider';
|
||||
|
||||
const list = document.createElement('div');
|
||||
list.className = 'stack dm-list';
|
||||
|
||||
@@ -352,7 +349,7 @@ export function render({ navigate, chrome }) {
|
||||
}
|
||||
|
||||
chrome?.setTopbar(head);
|
||||
screen.append(divider, list);
|
||||
screen.append(list);
|
||||
loadList();
|
||||
|
||||
screen.cleanup = () => {
|
||||
|
||||
@@ -215,7 +215,7 @@ function buildGraphModel(graph, centerLogin) {
|
||||
let persistedCenterLogin = '';
|
||||
let persistedCenterHistory = [];
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
export function render({ navigate, route, chrome } = {}) {
|
||||
const keepHistory = String(route?.params?.mode || '').trim().toLowerCase() === 'keep-history';
|
||||
const routeLogin = normalizeLogin(route?.params?.login || '');
|
||||
if (!keepHistory) {
|
||||
@@ -546,7 +546,7 @@ export function render({ navigate, route }) {
|
||||
|
||||
// Панель фильтров слоёв (оверлей под шапкой)
|
||||
const filterBar = document.createElement('div');
|
||||
filterBar.className = 'fg-filter-bar';
|
||||
filterBar.className = 'fg-filter-bar app-top-tabs';
|
||||
// Не даём нажатию на чип «провалиться» в сцену: иначе движок делает setPointerCapture на stage,
|
||||
// а захват указателя перенаправляет нативный click со сцены — и кнопка фильтра не срабатывает.
|
||||
filterBar.addEventListener('pointerdown', (e) => e.stopPropagation());
|
||||
@@ -560,8 +560,8 @@ export function render({ navigate, route }) {
|
||||
filterBar.append(chip);
|
||||
});
|
||||
|
||||
header.classList.add('network-header-overlay');
|
||||
stage.append(board, header, filterBar);
|
||||
chrome?.setTopbar(header);
|
||||
stage.append(board, filterBar);
|
||||
screen.append(stage);
|
||||
return screen;
|
||||
}
|
||||
|
||||
@@ -304,18 +304,18 @@ export function render({ navigate, chrome } = {}) {
|
||||
chrome?.setTopbar(renderHeader({ title: 'Уведомления' }));
|
||||
|
||||
const tabs = document.createElement('div');
|
||||
tabs.className = 'tabs';
|
||||
tabs.className = 'notification-feed-tabs app-top-tabs';
|
||||
tabs.innerHTML = `
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn notification-tab-btn ${state.notificationsTab === 'replies' ? 'active' : ''}"
|
||||
class="fg-filter-chip notification-tab-btn ${state.notificationsTab === 'replies' ? 'is-active' : ''}"
|
||||
data-tab="replies"
|
||||
data-selected="${state.notificationsTab === 'replies' ? 'true' : 'false'}"
|
||||
aria-selected="${state.notificationsTab === 'replies' ? 'true' : 'false'}"
|
||||
>Ответы</button>
|
||||
<button
|
||||
type="button"
|
||||
class="tab-btn notification-tab-btn ${state.notificationsTab === 'events' ? 'active' : ''}"
|
||||
class="fg-filter-chip notification-tab-btn ${state.notificationsTab === 'events' ? 'is-active' : ''}"
|
||||
data-tab="events"
|
||||
data-selected="${state.notificationsTab === 'events' ? 'true' : 'false'}"
|
||||
aria-selected="${state.notificationsTab === 'events' ? 'true' : 'false'}"
|
||||
@@ -365,7 +365,7 @@ export function render({ navigate, chrome } = {}) {
|
||||
|
||||
tabs.querySelectorAll('.notification-tab-btn').forEach((node) => {
|
||||
const selected = node.dataset.tab === normalizedTab;
|
||||
node.classList.toggle('active', selected);
|
||||
node.classList.toggle('is-active', selected);
|
||||
node.dataset.selected = selected ? 'true' : 'false';
|
||||
node.setAttribute('aria-selected', selected ? 'true' : 'false');
|
||||
});
|
||||
|
||||
@@ -101,6 +101,8 @@ export function render({ navigate, chrome }) {
|
||||
const topbar = document.createElement('header');
|
||||
topbar.className = 'page-header app-topbar-shell app-topbar-shell--profile';
|
||||
topbar.innerHTML = `
|
||||
<div class="header-left" aria-hidden="true"></div>
|
||||
<div class="header-center"><h1 class="page-title">Профиль</h1></div>
|
||||
<div class="header-actions profile-head-menu-wrap dm-head-menu-wrap">
|
||||
<button type="button" class="dm-head-menu-btn profile-head-menu-btn" aria-label="Меню профиля" aria-haspopup="menu" aria-expanded="false">
|
||||
<span class="dm-head-menu-dots" aria-hidden="true"><i></i><i></i><i></i></span>
|
||||
|
||||
@@ -163,3 +163,44 @@
|
||||
transform: translateY(1px) scale(0.97) !important;
|
||||
filter: brightness(0.9) !important;
|
||||
}
|
||||
|
||||
/* Верхний toolbar — отдельная цветовая роль: золотой текст и иконки.
|
||||
* Это правило намеренно расположено после глобального белого button-rule. */
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn),
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):hover,
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):focus,
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):focus-visible,
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):active,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn),
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):hover,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):focus,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):focus-visible,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):active {
|
||||
color: var(--app-topbar-gold) !important;
|
||||
}
|
||||
|
||||
/* Верхний toolbar: вместо золотого акцента — белые глифы с голубым ореолом.
|
||||
* Правило стоит последним, чтобы перекрыть общий белый button-reset и старую золотую роль. */
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn),
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):hover,
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):focus,
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):active,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn),
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):hover,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):focus,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):active {
|
||||
color: #F7FBFF !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.56)) !important;
|
||||
}
|
||||
|
||||
:root .topbar-slot button:not(.fg-filter-chip):not(.toolbar-btn):focus-visible,
|
||||
:root .network-header-overlay button:not(.fg-filter-chip):not(.toolbar-btn):focus-visible {
|
||||
color: #FFFFFF !important;
|
||||
outline: none !important;
|
||||
filter:
|
||||
drop-shadow(0 0 5px rgba(110, 205, 255, 0.82))
|
||||
drop-shadow(0 0 10px rgba(72, 145, 255, 0.42)) !important;
|
||||
}
|
||||
|
||||
@@ -8193,3 +8193,538 @@ html, body { overflow-x: hidden; }
|
||||
body.chat-topbar-overlay .app-shell.keyboard-open .scroll-to-bottom-btn {
|
||||
bottom: calc(var(--keyboard-offset, 0px) + var(--composer-height, 0px) + 18px);
|
||||
}
|
||||
|
||||
/* ===== Унифицированный верхний toolbar приложения =====
|
||||
* Единая геометрия для основных экранов и вложенных страниц:
|
||||
* слева действие/назад, по центру заголовок, справа действия.
|
||||
*/
|
||||
:root {
|
||||
--app-topbar-base-height: 64px;
|
||||
--app-topbar-title-size: 18px;
|
||||
--app-topbar-bg: #05070A;
|
||||
}
|
||||
|
||||
.page-header.app-topbar-shell {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(44px, 1fr) minmax(0, auto) minmax(44px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.page-header.app-topbar-shell .header-center {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.page-header.app-topbar-shell .header-left,
|
||||
.page-header.app-topbar-shell .header-actions {
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.page-header.app-topbar-shell .header-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.page-header.app-topbar-shell .header-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.page-header .page-title,
|
||||
.topbar-slot .dm-head-title,
|
||||
.topbar-slot .channels-top-title,
|
||||
.app-topbar-title-action {
|
||||
margin: 0;
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-main);
|
||||
font-size: var(--app-topbar-title-size);
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
text-shadow: none;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.topbar-slot {
|
||||
padding: 0 12px;
|
||||
background: var(--app-topbar-bg);
|
||||
}
|
||||
|
||||
.topbar-slot .page-header,
|
||||
.topbar-slot .dm-head,
|
||||
.topbar-slot .channels-top-bar {
|
||||
width: 100%;
|
||||
min-height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
margin: 0;
|
||||
padding: calc(10px + env(safe-area-inset-top)) 0 10px;
|
||||
box-sizing: border-box;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-top-bar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(44px, 1fr) minmax(0, auto) minmax(44px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-top-left,
|
||||
.topbar-slot .channels-top-right {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-top-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-top-right {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-top-title {
|
||||
justify-self: center;
|
||||
}
|
||||
|
||||
.app-topbar-shell--profile {
|
||||
justify-content: stretch;
|
||||
}
|
||||
|
||||
.app-topbar-shell--profile .header-left,
|
||||
.app-topbar-shell--profile .header-actions,
|
||||
.app-topbar-shell .header-left,
|
||||
.app-topbar-shell .header-actions {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Одинаковая стрелка назад на всех экранах. Визуально чуть крупнее текста,
|
||||
* чтобы сам глиф по воспринимаемому размеру совпадал с 18px заголовком. */
|
||||
.header-back-btn,
|
||||
.channels-top-back-btn.header-back-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-main);
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* В канале и треде название остаётся кликабельным, но визуально является
|
||||
* тем же центральным заголовком, а не отдельной кнопкой справа. */
|
||||
.topbar-slot .app-topbar-title-action {
|
||||
width: auto;
|
||||
min-height: 40px;
|
||||
padding: 0 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-screen--thread .app-topbar-title-action,
|
||||
.topbar-slot .channels-screen--channel .app-topbar-title-action {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.topbar-slot .page-header.channel-thread-topbar {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(44px, 1fr) minmax(0, auto) minmax(44px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.topbar-slot .page-header.channel-thread-topbar .header-left,
|
||||
.topbar-slot .page-header.channel-thread-topbar .header-actions {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
flex: initial;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.topbar-slot .page-header.channel-thread-topbar .header-left {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.topbar-slot .page-header.channel-thread-topbar .header-actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.topbar-slot .page-header.channel-thread-topbar .header-center {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Чат: в центре только компактная идентичность собеседника. */
|
||||
.chat-header-identity {
|
||||
min-width: 0;
|
||||
max-width: 132px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.chat-header-avatar-slot {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.chat-header-avatar.avatar,
|
||||
.chat-header-avatar.avatar-image {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
min-width: 28px;
|
||||
min-height: 28px;
|
||||
font-size: 11px;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.chat-header-login {
|
||||
min-width: 0;
|
||||
max-width: 96px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font-main);
|
||||
font-size: 13px;
|
||||
line-height: 1.15;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.78);
|
||||
}
|
||||
|
||||
/* «Связи» рисует шапку внутри сцены, поэтому повторяем тот же контракт там. */
|
||||
.network-header-overlay,
|
||||
.network-header-overlay.page-header {
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
min-height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
margin: 0;
|
||||
padding: calc(10px + env(safe-area-inset-top)) 12px 10px;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(44px, 1fr) minmax(0, auto) minmax(44px, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
background: var(--app-topbar-bg);
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.network-header-overlay .header-center {
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.network-header-overlay .page-title {
|
||||
font-family: var(--font-main);
|
||||
font-size: var(--app-topbar-title-size);
|
||||
line-height: 1.2;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0;
|
||||
color: rgba(255, 255, 255, 0.95);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
.network-header-overlay .header-left,
|
||||
.network-header-overlay .header-actions {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.network-header-overlay .icon-btn {
|
||||
min-height: 40px;
|
||||
padding: 0 8px;
|
||||
font-size: 14px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
-webkit-backdrop-filter: none;
|
||||
}
|
||||
|
||||
.network-header-overlay .header-back-btn {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
min-height: 40px;
|
||||
padding: 0;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
/* ===== Верхний toolbar: финальная унификация геометрии и типографики ===== */
|
||||
:root {
|
||||
--app-topbar-gold: #F0C56B;
|
||||
}
|
||||
|
||||
/* Фиксированная высота, а не только min-height: при смене основных вкладок
|
||||
* верхняя граница контента больше не меняется из-за локальной шапки страницы. */
|
||||
.topbar-slot .page-header,
|
||||
.topbar-slot .dm-head,
|
||||
.topbar-slot .channels-top-bar {
|
||||
height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
min-height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
max-height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
/* Один и тот же текстовый ритм на Контактах / Каналах / Связях /
|
||||
* Уведомлениях / Профиле и вложенных экранах. */
|
||||
.page-header .page-title,
|
||||
.topbar-slot .dm-head-title,
|
||||
.topbar-slot .channels-top-title,
|
||||
.app-topbar-title-action,
|
||||
.network-header-overlay .page-title {
|
||||
font-family: var(--font-main);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
font-style: normal;
|
||||
line-height: 22px;
|
||||
letter-spacing: 0;
|
||||
color: var(--app-topbar-gold);
|
||||
text-shadow: none;
|
||||
}
|
||||
|
||||
/* В списке контактов логин слева также остаётся в общей палитре шапки. */
|
||||
.topbar-slot .dm-head-name {
|
||||
font-family: var(--font-main);
|
||||
color: var(--app-topbar-gold);
|
||||
}
|
||||
|
||||
/* Все интерактивные элементы верхней шапки имеют одинаковую рабочую высоту. */
|
||||
.topbar-slot .icon-btn,
|
||||
.topbar-slot .dm-head-menu-btn,
|
||||
.topbar-slot .channels-top-more-btn {
|
||||
min-height: 40px;
|
||||
height: 40px;
|
||||
color: var(--app-topbar-gold);
|
||||
}
|
||||
|
||||
.topbar-slot .dm-head-menu-btn {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.topbar-slot .channels-top-more-btn {
|
||||
width: 40px;
|
||||
min-width: 40px;
|
||||
font-size: 24px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
/* Личный чат: аватар остаётся компактным, логин теперь равен общему 18px. */
|
||||
.chat-header-identity {
|
||||
max-width: 176px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.chat-header-login {
|
||||
max-width: 136px;
|
||||
font-size: 18px;
|
||||
line-height: 22px;
|
||||
font-weight: 700;
|
||||
color: var(--app-topbar-gold);
|
||||
}
|
||||
|
||||
/* Связи живут вне topbar-slot, поэтому фиксируем ту же фактическую высоту. */
|
||||
.network-header-overlay,
|
||||
.network-header-overlay.page-header {
|
||||
height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
min-height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
max-height: calc(var(--app-topbar-base-height) + env(safe-area-inset-top));
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.network-header-overlay .icon-btn,
|
||||
.network-header-overlay .header-back-btn {
|
||||
color: var(--app-topbar-gold);
|
||||
}
|
||||
|
||||
/* ===== Верхний toolbar: белый интерфейс с мягким синим свечением ===== */
|
||||
:root {
|
||||
--app-topbar-gold: #F7FBFF;
|
||||
--app-topbar-blue-glow: rgba(92, 190, 255, 0.72);
|
||||
--app-topbar-blue-glow-soft: rgba(72, 145, 255, 0.34);
|
||||
}
|
||||
|
||||
.page-header .page-title,
|
||||
.topbar-slot .dm-head-title,
|
||||
.topbar-slot .channels-top-title,
|
||||
.app-topbar-title-action,
|
||||
.network-header-overlay .page-title,
|
||||
.topbar-slot .dm-head-name,
|
||||
.chat-header-login {
|
||||
color: var(--app-topbar-gold);
|
||||
text-shadow:
|
||||
0 0 5px var(--app-topbar-blue-glow),
|
||||
0 0 14px var(--app-topbar-blue-glow-soft);
|
||||
}
|
||||
|
||||
.topbar-slot .icon-btn,
|
||||
.topbar-slot .dm-head-menu-btn,
|
||||
.topbar-slot .channels-top-more-btn,
|
||||
.network-header-overlay .icon-btn,
|
||||
.network-header-overlay .header-back-btn {
|
||||
color: var(--app-topbar-gold);
|
||||
text-shadow:
|
||||
0 0 5px var(--app-topbar-blue-glow),
|
||||
0 0 12px var(--app-topbar-blue-glow-soft);
|
||||
}
|
||||
|
||||
.topbar-slot .icon-btn svg,
|
||||
.network-header-overlay .icon-btn svg {
|
||||
filter:
|
||||
drop-shadow(0 0 3px var(--app-topbar-blue-glow))
|
||||
drop-shadow(0 0 7px var(--app-topbar-blue-glow-soft));
|
||||
}
|
||||
|
||||
.topbar-slot .dm-head-menu-dots i {
|
||||
background: var(--app-topbar-gold);
|
||||
box-shadow:
|
||||
0 0 5px var(--app-topbar-blue-glow),
|
||||
0 0 10px var(--app-topbar-blue-glow-soft);
|
||||
}
|
||||
|
||||
/* Уведомления: переключатели лент повторяют стеклянные чипы экрана «Связи». */
|
||||
.notifications-screen .notification-feed-tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
padding: 8px 12px 4px;
|
||||
margin: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.notifications-screen .notification-feed-tabs .notification-tab-btn {
|
||||
min-width: 92px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Личный чат: логин остаётся строго в центральной колонке,
|
||||
* аватар живёт отдельно в левой колонке рядом со стрелкой назад. */
|
||||
.page-header.app-topbar-shell .header-center {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-header-login {
|
||||
display: block;
|
||||
max-width: min(42vw, 180px);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.chat-header-avatar-slot--left {
|
||||
flex: 0 0 28px;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
|
||||
/* ===== Shared primary topbar + upper tabs geometry (2026-08-22) =====
|
||||
* «Связи» теперь монтируют header в общий topbar-slot, как остальные основные экраны.
|
||||
* Верхние переключатели используют один ритм и не меняют вертикальную позицию между страницами.
|
||||
*/
|
||||
:root {
|
||||
--app-primary-tabs-top-gap: 14px;
|
||||
--app-primary-tabs-gap: 8px;
|
||||
--app-primary-tab-min-height: 28px;
|
||||
}
|
||||
|
||||
/* Граф занимает ровно доступную область между общей шапкой и нижним toolbar. */
|
||||
.screen-content.network-scroll-lock .network-screen {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.screen-content.network-scroll-lock .network-stage {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* Общий контейнер верхних кнопок-вкладок. */
|
||||
.app-top-tabs {
|
||||
min-height: var(--app-primary-tab-min-height);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--app-primary-tabs-gap);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* На обычных экранах отступ создаёт стандартный padding screen-content (14px). */
|
||||
.notifications-screen .notification-feed-tabs.app-top-tabs {
|
||||
min-height: var(--app-primary-tab-min-height);
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* В «Связях» screen-content намеренно без padding, поэтому тот же отступ задаём явно. */
|
||||
.network-stage > .fg-filter-bar.app-top-tabs {
|
||||
position: absolute;
|
||||
top: var(--app-primary-tabs-top-gap);
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
z-index: 11;
|
||||
min-height: var(--app-primary-tab-min-height);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Одинаковая фактическая высота и типографика самих верхних чипов. */
|
||||
.app-top-tabs .fg-filter-chip {
|
||||
min-height: var(--app-primary-tab-min-height);
|
||||
height: var(--app-primary-tab-min-height);
|
||||
padding: 0 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-family: var(--font-main);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.notifications-screen .notification-feed-tabs .notification-tab-btn {
|
||||
min-width: 92px;
|
||||
}
|
||||
|
||||
@@ -422,7 +422,7 @@
|
||||
/* Панель фильтров слоёв (оверлей под шапкой) */
|
||||
.fg-filter-bar {
|
||||
position: absolute;
|
||||
top: max(54px, calc(env(safe-area-inset-top) + 50px));
|
||||
top: max(72px, calc(env(safe-area-inset-top) + 68px));
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 11;
|
||||
|
||||
Reference in New Issue
Block a user