SHA256
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:
@@ -1,4 +1,5 @@
|
||||
import { createTopBar } from '../components/topbar.js';
|
||||
import { iconHtml } from '../components/ui-icon.js';
|
||||
import { authService, clearAuthMessages, resetRegistrationFlow, state } from '../state.js';
|
||||
import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
import {
|
||||
@@ -31,19 +32,8 @@ function normalizeLoginForTemporaryUiGuard(login) {
|
||||
|
||||
function makePasswordToggleIcons() {
|
||||
return {
|
||||
eye: `
|
||||
<svg class="key-toggle-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M2.4 12s3.6-6.5 9.6-6.5S21.6 12 21.6 12s-3.6 6.5-9.6 6.5S2.4 12 2.4 12Z" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<circle cx="12" cy="12" r="2.9" fill="none" stroke="currentColor" stroke-width="1.8"/>
|
||||
</svg>
|
||||
`,
|
||||
eyeOff: `
|
||||
<svg class="key-toggle-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
|
||||
<path d="M3 4l18 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round"/>
|
||||
<path d="M2.4 12s3.6-6.5 9.6-6.5c2.4 0 4.5.8 6.1 1.9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
<path d="M21.6 12s-3.6 6.5-9.6 6.5c-2.4 0-4.5-.8-6.1-1.9" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,
|
||||
eye: iconHtml('eye', false, 'key-toggle-icon'),
|
||||
eyeOff: iconHtml('eyeOff', false, 'key-toggle-icon'),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -97,9 +87,10 @@ export function render({ navigate }) {
|
||||
passwordInputRow.className = 'inline-input-row';
|
||||
passwordInputRow.append(passwordInput, passwordToggleButton);
|
||||
|
||||
const statusText = document.createElement('p');
|
||||
// Строка под логином всегда занимает место — сообщения не сдвигают форму.
|
||||
const statusText = document.createElement('span');
|
||||
statusText.className = 'status-line registration-login-status';
|
||||
statusText.style.display = 'none';
|
||||
statusText.setAttribute('aria-live', 'polite');
|
||||
|
||||
const formError = document.createElement('p');
|
||||
formError.className = 'status-line is-unavailable';
|
||||
@@ -135,10 +126,24 @@ export function render({ navigate }) {
|
||||
passwordLengthText.textContent = `Символов в пароле: ${getCurrentPassword().length}`;
|
||||
}
|
||||
|
||||
function setStatusMessage(message, kind = '') {
|
||||
function setStatusMessage(message, kind = '', details = '') {
|
||||
statusText.textContent = message;
|
||||
statusText.title = details || message;
|
||||
statusText.className = kind ? `status-line registration-login-status ${kind}` : 'status-line registration-login-status';
|
||||
statusText.style.display = message ? '' : 'none';
|
||||
}
|
||||
|
||||
function showCheckedLoginStatus(isFree, className) {
|
||||
if (!isFree) {
|
||||
setStatusMessage('Логин уже занят', 'is-unavailable');
|
||||
} else if (className === 'free') {
|
||||
setStatusMessage('Логин свободен', 'is-available');
|
||||
} else if (className === 'premium') {
|
||||
setStatusMessage('Премиум-логин – покупка через DAO', 'is-unavailable');
|
||||
} else if (className === 'company') {
|
||||
setStatusMessage('Логин бренда – нужно согласование', 'is-unavailable');
|
||||
} else {
|
||||
setStatusMessage('Не подходит для обычной регистрации', 'is-unavailable');
|
||||
}
|
||||
}
|
||||
|
||||
function resetLoginCheckState() {
|
||||
@@ -186,34 +191,24 @@ export function render({ navigate }) {
|
||||
|
||||
const normalizedLogin = normalizeLoginForTemporaryUiGuard(login);
|
||||
if (!normalizedLogin) {
|
||||
setStatusMessage('Логин содержит недопустимые символы или имеет неверную длину ❌', 'is-unavailable');
|
||||
setStatusMessage('Только латиница, цифры и _, до 20 символов', 'is-unavailable');
|
||||
formError.style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (normalizedLogin.length < MIN_REGISTRATION_LOGIN_LENGTH) {
|
||||
setStatusMessage(`Логин должен быть не короче ${MIN_REGISTRATION_LOGIN_LENGTH} символов ❌`, 'is-unavailable');
|
||||
setStatusMessage(`Не короче ${MIN_REGISTRATION_LOGIN_LENGTH} символов`, 'is-unavailable');
|
||||
formError.style.display = 'none';
|
||||
return false;
|
||||
}
|
||||
|
||||
if (login === lastCheckedLogin) {
|
||||
if (!lastCheckedFree) {
|
||||
setStatusMessage('Логин уже занят ❌', 'is-unavailable');
|
||||
} else if (lastCheckedClassName === 'free') {
|
||||
setStatusMessage('Логин свободен ✅', 'is-available');
|
||||
} else if (lastCheckedClassName === 'premium') {
|
||||
setStatusMessage('Логин свободен, но это премиум-логин (покупка через DAO) ❌', 'is-unavailable');
|
||||
} else if (lastCheckedClassName === 'company') {
|
||||
setStatusMessage('Логин свободен, но относится к компании/бренду (отдельное согласование) ❌', 'is-unavailable');
|
||||
} else {
|
||||
setStatusMessage('Логин нельзя использовать для обычной регистрации ❌', 'is-unavailable');
|
||||
}
|
||||
showCheckedLoginStatus(lastCheckedFree, lastCheckedClassName);
|
||||
formError.style.display = 'none';
|
||||
return lastCheckedFree && lastCheckedClassName === 'free';
|
||||
}
|
||||
|
||||
setStatusMessage('Проверяем логин...');
|
||||
setStatusMessage('Проверяем логин…', 'is-checking');
|
||||
try {
|
||||
const check = await checkLoginExistsOnSolana({
|
||||
login,
|
||||
@@ -237,28 +232,18 @@ export function render({ navigate }) {
|
||||
lastCheckedLogin = login;
|
||||
lastCheckedFree = isFree;
|
||||
lastCheckedClassName = className;
|
||||
if (!isFree) {
|
||||
setStatusMessage('Логин уже занят ❌', 'is-unavailable');
|
||||
} else if (className === 'free') {
|
||||
setStatusMessage('Логин свободен ✅', 'is-available');
|
||||
} else if (className === 'premium') {
|
||||
setStatusMessage('Логин свободен, но это премиум-логин (покупка через DAO) ❌', 'is-unavailable');
|
||||
} else if (className === 'company') {
|
||||
setStatusMessage('Логин свободен, но относится к компании/бренду (отдельное согласование) ❌', 'is-unavailable');
|
||||
} else {
|
||||
setStatusMessage('Логин нельзя использовать для обычной регистрации ❌', 'is-unavailable');
|
||||
}
|
||||
showCheckedLoginStatus(isFree, className);
|
||||
formError.style.display = 'none';
|
||||
return isFree && className === 'free';
|
||||
} catch (error) {
|
||||
if (runId !== loginCheckRunId) return false;
|
||||
if (isSolanaRpcUnavailableError(error)) {
|
||||
setStatusMessage('Нет связи с сервером Solana. Попробуйте ещё раз позже.', 'is-unavailable');
|
||||
setStatusMessage('Нет связи с Solana – попробуйте позже', 'is-unavailable');
|
||||
return false;
|
||||
}
|
||||
const base = toUserMessage(error, 'Не удалось проверить логин');
|
||||
const details = formatSolanaErrorDetails(error);
|
||||
setStatusMessage(`${base}. Детали: ${details}`, 'is-unavailable');
|
||||
setStatusMessage(base, 'is-unavailable', `${base}. Детали: ${details}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -313,8 +298,8 @@ export function render({ navigate }) {
|
||||
const passwordLabel = document.createElement('label');
|
||||
passwordLabel.className = 'stack registration-password-single';
|
||||
passwordLabel.innerHTML = '<span class="field-label">Пароль</span>';
|
||||
form.append(loginField, statusText, passwordLabel);
|
||||
loginField.append(loginInput);
|
||||
form.append(loginField, passwordLabel);
|
||||
loginField.append(loginInput, statusText);
|
||||
passwordLabel.append(passwordInputRow);
|
||||
form.append(passwordLengthText, formError, faqButton);
|
||||
actions.innerHTML = '';
|
||||
|
||||
Reference in New Issue
Block a user