UI: общий верхний и нижний тулбар

This commit is contained in:
AidarKC
2026-08-11 15:22:10 +04:00
parent 628a970e6f
commit 801a4697ea
12 changed files with 218 additions and 36 deletions
+100 -2
View File
@@ -151,6 +151,8 @@ const routes = {
};
const screenEl = document.getElementById('app-screen');
const topbarEl = document.getElementById('topbar-slot');
const composerEl = document.getElementById('composer-slot');
const toolbarEl = document.getElementById('toolbar-slot');
const appShellEl = document.querySelector('.app-shell');
const initialSplashEl = document.getElementById('initial-splash');
@@ -181,6 +183,7 @@ let hiddenDmAudioContext = null;
let hiddenDmAudioUnlocked = false;
let initialConnectionCompleted = false;
let orientationLockInFlight = false;
let currentChromeCleanup = null;
const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1';
const GUEST_ALLOWED_PAGES = new Set([
'start-view',
@@ -204,6 +207,86 @@ initPwaInstallPromptHandling();
initCallUiOverlay();
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
function setShellMetricVar(name, valuePx) {
if (!appShellEl) return;
appShellEl.style.setProperty(name, `${Math.max(0, Math.ceil(Number(valuePx || 0)))}px`);
}
function attachSlotHeightObserver(slotEl, cssVarName) {
if (!slotEl || typeof ResizeObserver !== 'function') return null;
const sync = () => {
const visible = !slotEl.hidden && slotEl.childElementCount > 0;
setShellMetricVar(cssVarName, visible ? slotEl.offsetHeight : 0);
};
const observer = new ResizeObserver(sync);
observer.observe(slotEl);
sync();
return { observer, sync };
}
const topbarHeightObserver = attachSlotHeightObserver(topbarEl, '--topbar-height');
const composerHeightObserver = attachSlotHeightObserver(composerEl, '--composer-height');
const toolbarHeightObserver = attachSlotHeightObserver(toolbarEl, '--toolbar-height');
function clearSlot(slotEl, cssVarName) {
if (!slotEl) return;
slotEl.innerHTML = '';
slotEl.hidden = true;
setShellMetricVar(cssVarName, 0);
}
function mountSlot(slotEl, cssVarName, node) {
if (!slotEl) return;
slotEl.innerHTML = '';
if (node instanceof Node) {
slotEl.append(node);
slotEl.hidden = false;
} else {
slotEl.hidden = true;
}
setShellMetricVar(cssVarName, !slotEl.hidden ? slotEl.offsetHeight : 0);
}
function createChromeController(showAppChrome) {
let topbarNode = null;
let composerNode = null;
let disposed = false;
const apply = () => {
if (disposed) return;
if (!showAppChrome) {
clearSlot(topbarEl, '--topbar-height');
clearSlot(composerEl, '--composer-height');
return;
}
mountSlot(topbarEl, '--topbar-height', topbarNode);
mountSlot(composerEl, '--composer-height', composerNode);
topbarHeightObserver?.sync?.();
composerHeightObserver?.sync?.();
};
return {
setTopbar(node = null) {
topbarNode = node instanceof Node ? node : null;
apply();
},
setComposer(node = null) {
composerNode = node instanceof Node ? node : null;
apply();
},
clear() {
topbarNode = null;
composerNode = null;
apply();
},
dispose() {
disposed = true;
clearSlot(topbarEl, '--topbar-height');
clearSlot(composerEl, '--composer-height');
},
};
}
async function unlockHiddenDmAudio() {
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
@@ -908,7 +991,14 @@ function renderPageFailureFallback(pageId, error) {
screenEl.append(wrap);
screenEl.classList.toggle('no-app-chrome', false);
if (typeof currentChromeCleanup === 'function') {
currentChromeCleanup();
currentChromeCleanup = null;
}
clearSlot(topbarEl, '--topbar-height');
clearSlot(composerEl, '--composer-height');
toolbarEl.innerHTML = '';
toolbarHeightObserver?.sync?.();
refreshConnectionUi();
}
@@ -932,10 +1022,17 @@ function renderApp() {
currentCleanup();
currentCleanup = null;
}
if (typeof currentChromeCleanup === 'function') {
currentChromeCleanup();
currentChromeCleanup = null;
}
try {
screenEl.innerHTML = '';
const screen = page.render({ route, navigate });
const showAppChrome = page.pageMeta?.showAppChrome !== false;
const chrome = createChromeController(showAppChrome);
currentChromeCleanup = () => chrome.dispose();
const screen = page.render({ route, navigate, chrome });
if (!(screen instanceof Node)) {
throw new Error('Page render returned invalid node');
}
@@ -943,7 +1040,6 @@ function renderApp() {
screenEl.append(screen);
currentCleanup = typeof screen.cleanup === 'function' ? screen.cleanup : null;
const showAppChrome = page.pageMeta?.showAppChrome !== false;
screenEl.classList.toggle('no-app-chrome', !showAppChrome);
screenEl.classList.toggle('preauth-flow', PRE_AUTH_PAGES.includes(pageId));
@@ -951,6 +1047,7 @@ function renderApp() {
if (showAppChrome) {
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
}
toolbarHeightObserver?.sync?.();
refreshConnectionUi();
} catch (error) {
console.error('[renderApp] controlled fallback', error);
@@ -968,6 +1065,7 @@ function refreshToolbarOnly() {
if (showAppChrome) {
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
}
toolbarHeightObserver?.sync?.();
refreshConnectionUi();
}