SHA256
Палитра «Сияние» по умолчанию, новые роли цветов. Лента канала: плоские записи, ряд действий с равным шагом иконок, дата у записи. Обсуждение со строкой ввода, личный чат, шторки. Вход и регистрация: стартовый экран (надпись «СИЯНИЕ», сияние мандалы), настройки входа со статусами серверов, статус логина в одну строку, вход с другого устройства (код, таймер, шаги), сохранение ключей с переключателями. Корневые разделы – заголовок по центру; фильтр чатов – переключатель; уведомления – список как в чатах; фото – просмотр на весь экран. Общее: один зазор под шапкой (--page-top-gap), переключатели высотой с поле ввода, второстепенные кнопки по одной роли (в том числе на экранах устройств и серверов), иконки только из ui-icon.js, полоса прокрутки скрыта, среднее тире в текстах интерфейса. Что нужно от сервера и разработчика – docs/UI-Design/UI-v4-for-dev.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
119 lines
3.6 KiB
JavaScript
119 lines
3.6 KiB
JavaScript
import { iconHtml } from './ui-icon.js';
|
|
import { createDropdownMenu } from './dropdown-menu.js';
|
|
|
|
function appendNode(target, node) {
|
|
if (!target || !(node instanceof Node)) return;
|
|
target.append(node);
|
|
}
|
|
|
|
function createActionButton(action = {}, cleanupFns) {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = `icon-btn topbar__action${action.className ? ` ${action.className}` : ''}`;
|
|
if (action.title) button.title = action.title;
|
|
if (action.ariaLabel) button.setAttribute('aria-label', action.ariaLabel);
|
|
if (action.id) button.dataset.action = action.id;
|
|
|
|
if (action.iconNode instanceof Node) {
|
|
button.append(action.iconNode);
|
|
} else if (action.icon) {
|
|
button.innerHTML = iconHtml(action.icon);
|
|
} else if (action.label === '⋮' || action.label === '⋯') {
|
|
// Меню экрана везде рисуется одной иконкой «⋯»
|
|
button.innerHTML = iconHtml('more');
|
|
} else {
|
|
button.textContent = String(action.label ?? '');
|
|
}
|
|
|
|
if (action.menu) {
|
|
const menu = createDropdownMenu({
|
|
anchorEl: button,
|
|
transparent: true,
|
|
dimBackground: true,
|
|
keepAnchorPressed: true,
|
|
...(typeof action.menu === 'object' ? action.menu : {}),
|
|
});
|
|
cleanupFns.add(() => menu.destroy());
|
|
} else if (typeof action.onClick === 'function') {
|
|
button.addEventListener('click', action.onClick);
|
|
}
|
|
|
|
return button;
|
|
}
|
|
|
|
export function createTopBar({
|
|
title = '',
|
|
className = '',
|
|
left = null,
|
|
center = null,
|
|
back = null,
|
|
leftLabel = '',
|
|
actions = [],
|
|
} = {}) {
|
|
const cleanupFns = new Set();
|
|
const topbar = document.createElement('header');
|
|
topbar.className = `topbar${className ? ` ${className}` : ''}`;
|
|
|
|
const leftSlot = document.createElement('div');
|
|
leftSlot.className = 'topbar__left';
|
|
|
|
const backAction = back;
|
|
if (backAction?.visible !== false && typeof backAction?.onClick === 'function') {
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.className = `icon-btn topbar__back${backAction.className ? ` ${backAction.className}` : ''}`;
|
|
button.innerHTML = iconHtml('back');
|
|
button.setAttribute('aria-label', backAction.ariaLabel || 'Назад');
|
|
button.title = backAction.title || 'Назад';
|
|
button.addEventListener('click', backAction.onClick);
|
|
leftSlot.append(button);
|
|
}
|
|
|
|
appendNode(leftSlot, left);
|
|
|
|
if (leftLabel) {
|
|
const label = document.createElement('span');
|
|
label.className = 'topbar__left-label';
|
|
label.textContent = leftLabel;
|
|
leftSlot.append(label);
|
|
}
|
|
|
|
const centerSlot = document.createElement('div');
|
|
centerSlot.className = 'topbar__center';
|
|
const resolvedCenter = center;
|
|
if (resolvedCenter instanceof Node) {
|
|
centerSlot.append(resolvedCenter);
|
|
} else {
|
|
const heading = document.createElement('h1');
|
|
heading.className = 'topbar__title';
|
|
heading.textContent = String(title || '');
|
|
centerSlot.append(heading);
|
|
}
|
|
|
|
const rightSlot = document.createElement('div');
|
|
rightSlot.className = 'topbar__right';
|
|
const normalizedActions = actions;
|
|
normalizedActions.forEach((action) => {
|
|
rightSlot.append(createActionButton(action, cleanupFns));
|
|
});
|
|
|
|
topbar.append(leftSlot, centerSlot, rightSlot);
|
|
|
|
topbar.addCleanup = (cleanup) => {
|
|
if (typeof cleanup === 'function') cleanupFns.add(cleanup);
|
|
return cleanup;
|
|
};
|
|
topbar.cleanup = () => {
|
|
for (const cleanup of cleanupFns) {
|
|
try {
|
|
cleanup();
|
|
} catch (error) {
|
|
console.warn('[TopBar] cleanup failed', error);
|
|
}
|
|
}
|
|
cleanupFns.clear();
|
|
};
|
|
|
|
return topbar;
|
|
}
|