Пересобрать верхний 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
+16 -21
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 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');
+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>