Пересобрать верхний UI-бар

This commit is contained in:
2026-08-22 12:34:07 +03:00
parent bc8c7f318b
commit f5e401cff8
14 changed files with 700 additions and 98 deletions
+1 -1
View File
@@ -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
View File
@@ -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 = 'Формат сообщения не поддерживается';
+18 -5
View File
@@ -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 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;
}
+17 -22
View File
@@ -1144,31 +1144,26 @@ export function render({ navigate, route, chrome }) {
const appScreen = document.getElementById('app-screen');
appScreen?.classList.add('channels-scroll-clean');
const header = renderHeader({
title: '',
leftAction: { label: '<', onClick: () => navigate(resolveThreadPreviousInChannels(selector, activeResolvedChannelLabel)) },
rightActions: [],
});
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');
const threadHeaderButton = document.createElement('button');
threadHeaderButton.type = 'button';
threadHeaderButton.className = 'icon-btn channel-header-route-btn';
threadHeaderButton.className = 'icon-btn channel-header-route-btn app-topbar-title-action';
threadHeaderButton.textContent = 'Тред в канале: ...';
threadHeaderButton.disabled = true;
headerLeft.append(threadHeaderButton);
}
const header = renderHeader({
centerNode: threadHeaderButton,
leftAction: { label: '<', onClick: () => navigate(resolveThreadPreviousInChannels(selector, activeResolvedChannelLabel)) },
rightActions: [
{
label: '↑',
title: 'К списку каналов',
ariaLabel: 'К списку каналов',
className: 'channel-thread-list-btn',
onClick: () => navigate('channels-list'),
},
],
});
header.classList.add('channel-thread-topbar');
chrome?.setTopbar(header);
const statusBox = document.createElement('div');
+7 -6
View File
@@ -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;
+10 -39
View File
@@ -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 });
};
+52 -5
View File
@@ -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');
+1 -4
View File
@@ -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 = () => {
+4 -4
View File
@@ -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;
}
+4 -4
View File
@@ -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');
});
+2
View File
@@ -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>
+41
View File
@@ -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;
}
+535
View File
@@ -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;
}
+1 -1
View File
@@ -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;