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>
61 lines
2.5 KiB
JavaScript
61 lines
2.5 KiB
JavaScript
import { iconHtml } from './ui-icon.js';
|
|
|
|
// Шторка снизу (на широком экране — окно по центру) с заголовком и «✕».
|
|
// Закрывается по «✕», Escape и тапу мимо; фокус возвращается туда, откуда открыли.
|
|
// Возвращает { root, body, footer, close }. content — Node или функция (sheet) => Node.
|
|
export function openSheet({ title = '', content = null, className = '', onClose = null } = {}) {
|
|
const opener = document.activeElement;
|
|
const modal = document.createElement('div');
|
|
modal.className = `modal sheet${className ? ` ${className}` : ''}`;
|
|
const card = document.createElement('div');
|
|
card.className = 'modal-card sheet__card';
|
|
card.setAttribute('role', 'dialog');
|
|
card.setAttribute('aria-modal', 'true');
|
|
|
|
const head = document.createElement('div');
|
|
head.className = 'sheet__head';
|
|
const heading = document.createElement('h2');
|
|
heading.className = 'modal-title sheet__title';
|
|
heading.textContent = title;
|
|
heading.id = `sheet-title-${Math.random().toString(36).slice(2, 8)}`;
|
|
card.setAttribute('aria-labelledby', heading.id);
|
|
const closeBtn = document.createElement('button');
|
|
closeBtn.type = 'button';
|
|
closeBtn.className = 'icon-btn sheet__close';
|
|
closeBtn.setAttribute('aria-label', 'Закрыть');
|
|
closeBtn.innerHTML = iconHtml('close');
|
|
head.append(heading, closeBtn);
|
|
|
|
const body = document.createElement('div');
|
|
body.className = 'sheet__body';
|
|
const footer = document.createElement('div');
|
|
footer.className = 'sheet__footer';
|
|
footer.hidden = true;
|
|
card.append(head, body, footer);
|
|
modal.append(card);
|
|
|
|
let closed = false;
|
|
const close = () => {
|
|
if (closed) return;
|
|
closed = true;
|
|
document.removeEventListener('keydown', onKeydown, true);
|
|
modal.remove();
|
|
opener?.focus?.();
|
|
onClose?.();
|
|
};
|
|
const onKeydown = (event) => {
|
|
if (event.key === 'Escape') { event.stopPropagation(); close(); }
|
|
};
|
|
modal.addEventListener('click', (event) => { if (event.target === modal) close(); });
|
|
closeBtn.addEventListener('click', close);
|
|
document.addEventListener('keydown', onKeydown, true);
|
|
|
|
const sheet = { root: modal, body, footer, close };
|
|
const node = typeof content === 'function' ? content(sheet) : content;
|
|
if (node instanceof Node) body.append(node);
|
|
if (footer.childElementCount) footer.hidden = false;
|
|
document.body.append(modal);
|
|
(card.querySelector('[autofocus]') || closeBtn).focus();
|
|
return sheet;
|
|
}
|