UI v4: новый стиль по макету V3/V4 и доводка экранов

Палитра «Сияние» по умолчанию, новые роли цветов. Лента канала: плоские записи, ряд действий
с равным шагом иконок, дата у записи. Обсуждение со строкой ввода, личный чат, шторки.
Вход и регистрация: стартовый экран (надпись «СИЯНИЕ», сияние мандалы), настройки входа
со статусами серверов, статус логина в одну строку, вход с другого устройства (код, таймер, шаги),
сохранение ключей с переключателями. Корневые разделы – заголовок по центру; фильтр чатов –
переключатель; уведомления – список как в чатах; фото – просмотр на весь экран.
Общее: один зазор под шапкой (--page-top-gap), переключатели высотой с поле ввода,
второстепенные кнопки по одной роли (в том числе на экранах устройств и серверов),
иконки только из ui-icon.js, полоса прокрутки скрыта, среднее тире в текстах интерфейса.

Что нужно от сервера и разработчика – docs/UI-Design/UI-v4-for-dev.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
q
2026-09-29 01:36:52 +03:00
co-authored by Claude Opus 5.5
parent 13693a0a53
commit 2e62616b46
84 changed files with 2507 additions and 1583 deletions
+64 -45
View File
@@ -10,14 +10,14 @@ import { checkServerAvailabilityByKey, resolveAndCheckShineServerLogin } from '.
export const pageMeta = { id: 'entry-settings-view', title: 'Настройки входа', showAppChrome: false };
const SERVER_FIELDS = [
{ key: 'solanaServer', label: 'Адрес Solana сервера' },
{ key: 'solanaServer', label: 'Сервер Solana' },
{ key: 'shineServerLogin', label: 'Сервер Сияния' },
{ key: 'arweaveServer', label: 'Адрес сервера Arweave' },
{ key: 'arweaveServer', label: 'Сервер Arweave' },
];
export function render({ navigate }) {
const screen = document.createElement('section');
screen.className = 'stack';
screen.className = 'stack entry-settings';
const draft = {
language: state.entrySettings.language,
@@ -30,62 +30,79 @@ export function render({ navigate }) {
const timers = new Map();
let disposed = false;
const checks = [];
const body = document.createElement('div');
body.className = 'card stack';
body.className = 'entry-servers';
SERVER_FIELDS.forEach((field) => {
const block = document.createElement('div');
block.className = 'stack';
block.className = 'entry-server';
const head = document.createElement('div');
head.className = 'entry-server__head';
const title = document.createElement('label');
title.className = 'field-label';
title.htmlFor = `entry-${field.key}`;
title.textContent = field.label;
// Статус — он же кнопка повторной проверки.
const checkButton = document.createElement('button');
checkButton.className = 'text-btn entry-status';
checkButton.type = 'button';
const input = document.createElement('input');
input.className = 'input';
input.id = `entry-${field.key}`;
input.type = 'text';
input.autocapitalize = 'off';
input.spellcheck = false;
input.value = draft[field.key];
const controls = document.createElement('div');
controls.className = 'row wrap-row';
const checkButton = document.createElement('button');
checkButton.className = 'secondary-btn server-check-btn';
checkButton.type = 'button';
checkButton.textContent = 'Проверить';
const status = document.createElement('span');
status.className = 'status-line';
const hint = document.createElement('p');
hint.className = 'entry-server__hint';
const applyStatus = (value, exactAddress = '') => {
draft.statuses[field.key] = value;
checkButton.classList.remove('is-available', 'is-unavailable');
status.classList.remove('is-available', 'is-unavailable');
if (disposed) return;
if (value !== 'checking') draft.statuses[field.key] = value;
checkButton.classList.remove('is-available', 'is-unavailable', 'is-checking');
checkButton.disabled = false;
if (value === 'available') {
status.textContent = field.key === 'shineServerLogin' && exactAddress
? `Доступен: ${exactAddress}`
: 'Доступен';
checkButton.textContent = 'Доступен';
checkButton.classList.add('is-available');
status.classList.add('is-available');
} else if (value === 'unavailable') {
status.textContent = 'Недоступен';
checkButton.textContent = 'Недоступен';
checkButton.classList.add('is-unavailable');
status.classList.add('is-unavailable');
} else if (value === 'checking') {
checkButton.textContent = 'Проверка…';
checkButton.classList.add('is-checking');
checkButton.disabled = true;
} else {
status.textContent = field.key === 'shineServerLogin' && draft.shineServerHttp
? `Текущий адрес: ${draft.shineServerHttp}`
: 'Статус не проверен';
checkButton.textContent = 'Проверить';
}
checkButton.setAttribute('aria-label', `${field.label}: ${checkButton.textContent}. Проверить ещё раз`);
if (field.key === 'shineServerLogin') {
const address = exactAddress || draft.shineServerHttp;
hint.textContent = address || '';
hint.hidden = !address;
}
};
let checkSeq = 0;
const runCheck = async () => {
draft[field.key] = input.value.trim();
checkButton.disabled = true;
checkButton.textContent = 'Проверка...';
window.clearTimeout(timers.get(field.key));
// Результат устаревшей проверки (адрес успели поменять) не должен перезаписать новый.
const seq = ++checkSeq;
applyStatus('checking');
try {
if (field.key === 'shineServerLogin') {
const resolved = await resolveAndCheckShineServerLogin(input.value, draft.solanaServer);
if (seq !== checkSeq) return;
draft.shineServerHttp = resolved.httpBase;
draft.shineServer = resolved.wsUrl;
applyStatus(resolved.status, resolved.httpBase);
@@ -93,26 +110,29 @@ export function render({ navigate }) {
const next = await checkServerAvailabilityByKey(field.key, input.value, {
solanaEndpoint: draft.solanaServer,
});
if (seq !== checkSeq) return;
applyStatus(next);
}
} finally {
checkButton.disabled = false;
checkButton.textContent = 'Проверить';
} catch {
if (seq === checkSeq) applyStatus('unavailable');
}
};
applyStatus(draft.statuses[field.key]);
hint.hidden = field.key !== 'shineServerLogin' || !hint.textContent;
checks.push(runCheck);
checkButton.addEventListener('click', runCheck);
input.addEventListener('input', () => {
draft[field.key] = input.value;
checkSeq += 1;
applyStatus('idle');
window.clearTimeout(timers.get(field.key));
timers.set(field.key, window.setTimeout(() => {
void runCheck();
}, 3000));
});
input.addEventListener('blur', () => {
input.addEventListener('change', () => {
void runCheck();
});
input.addEventListener('keydown', (event) => {
@@ -122,17 +142,17 @@ export function render({ navigate }) {
}
});
controls.append(checkButton, status);
block.append(title, input, controls);
head.append(title, checkButton);
block.append(head, input, hint);
body.append(block);
});
const actions = document.createElement('div');
actions.className = 'auth-footer-actions';
actions.className = 'entry-settings__actions';
if (isLocalDemoAvailable()) {
const localDemoButton = document.createElement('button');
localDemoButton.className = 'secondary-btn preauth-local-demo-btn';
localDemoButton.className = 'secondary-btn';
localDemoButton.type = 'button';
localDemoButton.textContent = 'Открыть локальный тестовый режим';
localDemoButton.addEventListener('click', () => {
@@ -145,18 +165,12 @@ export function render({ navigate }) {
const serverUiButton = document.createElement('button');
serverUiButton.className = 'secondary-btn';
serverUiButton.type = 'button';
serverUiButton.textContent = 'Настроить свой сервер';
serverUiButton.textContent = 'Настроить свой сервер ↗';
serverUiButton.addEventListener('click', () => {
const url = new URL('server-ui.html', window.location.href);
window.open(url.toString(), '_blank', 'noopener');
});
const cancelButton = document.createElement('button');
cancelButton.className = 'secondary-btn';
cancelButton.type = 'button';
cancelButton.textContent = 'Отмена';
cancelButton.addEventListener('click', () => navigate('start-view'));
const saveButton = document.createElement('button');
saveButton.className = 'primary-btn';
saveButton.type = 'button';
@@ -170,7 +184,8 @@ export function render({ navigate }) {
}
});
actions.append(serverUiButton, cancelButton, saveButton);
actions.prepend(saveButton);
actions.append(serverUiButton);
screen.append(
createTopBar({
@@ -182,8 +197,12 @@ export function render({ navigate }) {
);
screen.cleanup = () => {
disposed = true;
timers.forEach((timerId) => window.clearTimeout(timerId));
};
// Серверы проверяются сразу — зелёный «Доступен» видно без лишних нажатий.
checks.forEach((check) => { void check(); });
return screen;
}