From 24cca1f1c6e08a57201e4734bed7eab08a027f3cf49f9f09fd97fafa1d55ec79 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 15 Jul 2026 16:16:24 +0400 Subject: [PATCH] =?UTF-8?q?UI:=20=D0=B4=D0=BE=D1=80=D0=B0=D0=B1=D0=BE?= =?UTF-8?q?=D1=82=D0=BA=D0=B0=20=D1=80=D0=B5=D0=B3=D0=B8=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B8=20=D0=B8=20=D0=B4=D0=BE=D0=BA=D1=83?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D1=82=D0=B0=D1=86=D0=B8=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 2 +- Deploy/PRODUCTION_SERVERS.md | 6 +- Deploy/README.md | 2 +- VERSION.properties | 4 +- docs/Blockchain/CHANGELOG.md | 7 + ...-15_2115_регистрация_глаз_логин_12_слов.md | 15 ++ docs/deploy/README.md | 2 +- docs/deploy/servers/shineup.me_main.md | 3 +- docs/ИТХ/Спецификация_ИТХ_v1.md | 6 +- shine-UI/js/pages/register-view.js | 238 +++++++++++------- shine-UI/js/pages/registration-keys-view.js | 1 + .../js/pages/registration-payment-view.js | 1 + shine-UI/js/state.js | 1 + shine-UI/styles/components.css | 8 + 14 files changed, 192 insertions(+), 104 deletions(-) create mode 100644 docs/Pending_Features/2026-07-15_2115_регистрация_глаз_логин_12_слов.md diff --git a/AGENTS.md b/AGENTS.md index 5ba967b..5e9fb0e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,7 +87,7 @@ ## Deploy - Все документы и заметки по деплою хранить в папке `Dev_Docs/deploy/`. -- Production-хост SHiNE: `player@shineup.me` (`185.229.109.118`). +- Production-хост SHiNE: `player@shineup.me` (`178.208.64.62`). - Основной test-хост SHiNE: `player@193.8.215.70` (`t.shineup.me`). - Резервный test-хост SHiNE: `player@93.170.12.154` (`test.shineup.me`). - Базовый путь на сервере для SHiNE: `/home/player` (проекты SHiNE размещать в `/home/player/SHiNE/...`). diff --git a/Deploy/PRODUCTION_SERVERS.md b/Deploy/PRODUCTION_SERVERS.md index a752eed..fb08996 100644 --- a/Deploy/PRODUCTION_SERVERS.md +++ b/Deploy/PRODUCTION_SERVERS.md @@ -6,7 +6,7 @@ - `player@shineup.me` - домен `shineup.me` -- IP `185.229.109.118` +- IP `178.208.64.62` и @@ -20,7 +20,7 @@ - SSH: `player@shineup.me` - домен: `shineup.me` -- IP: `185.229.109.118` +- IP: `178.208.64.62` - пользователь: `player` - базовый путь: `/home/player` @@ -39,7 +39,7 @@ Caddy: -- активный конфиг: `/home/player/SHiNE/caddy/Caddyfile` +- активный конфиг: `/etc/caddy/Caddyfile` - UI root: `/home/player/SHiNE/shine-ui` - `/ws` проксируется на `127.0.0.1:7070` diff --git a/Deploy/README.md b/Deploy/README.md index daaad5a..1f6d973 100644 --- a/Deploy/README.md +++ b/Deploy/README.md @@ -14,7 +14,7 @@ - Production SHiNE: - `player@shineup.me` - домен `shineup.me` - - IP `185.229.109.118` + - IP `178.208.64.62` - Второй production SHiNE: - `player@193.8.215.70` - домен `t.shineup.me` diff --git a/VERSION.properties b/VERSION.properties index 5ded630..0e03320 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.312 -server.version=1.2.291 +client.version=1.2.313 +server.version=1.2.292 diff --git a/docs/Blockchain/CHANGELOG.md b/docs/Blockchain/CHANGELOG.md index 221e699..49f6946 100644 --- a/docs/Blockchain/CHANGELOG.md +++ b/docs/Blockchain/CHANGELOG.md @@ -1,5 +1,12 @@ # История изменений документации блокчейна +## 2026-07-14 20:43:43 +0400 +- Базовый коммит-ориентир: `118be41`. +- В `docs/ИТХ/Спецификация_ИТХ_v1.md` изменён магик файла чекпоинта: + вместо `SHNCKPT1` (8 байт) — читаемый `SHiNE-CHECKPOINT` (16 байт ASCII, + по образцу `SQLite format 3\0`). Версия формата в магик не входит — + она в поле `v` заголовка и в первой строке preimage. + ## 2026-07-14 19:56:50 +0400 - Базовый коммит-ориентир: `172f007`. - В `docs/ИТХ/Спецификация_ИТХ_v1.md` из TODO убран «вход в график по депозиту»: diff --git a/docs/Pending_Features/2026-07-15_2115_регистрация_глаз_логин_12_слов.md b/docs/Pending_Features/2026-07-15_2115_регистрация_глаз_логин_12_слов.md new file mode 100644 index 0000000..ed586be --- /dev/null +++ b/docs/Pending_Features/2026-07-15_2115_регистрация_глаз_логин_12_слов.md @@ -0,0 +1,15 @@ +# Регистрация: глаз у пароля, автопроверка логина и сохранение связки с 12 словами + +- краткое описание: + На экране регистрации добавлены кнопка-глаз у поля пароля, автопроверка логина с задержкой около 1.5 секунды и сохранение связки между обычным полем пароля и режимом `12 слов`, пока пользователь вручную не изменит пароль. +- что проверять: + Открыть экран регистрации и убедиться, что справа от поля пароля есть квадратная кнопка-глаз, которая переключает скрытый и открытый показ пароля. + Включить режим `Представить пароль в виде 12 слов`, заполнить несколько или все слова, затем выключить режим и проверить, что в поле пароля появился склеенный пароль. + Снова включить режим `12 слов` без ручного редактирования пароля и проверить, что ранее введённые слова восстановились. + После этого вручную изменить пароль в обычном поле и снова включить режим `12 слов` — набор слов должен быть очищен. + Ввести логин и перестать печатать примерно на 1.5 секунды — под полем логина должен автоматически появиться актуальный статус доступности. + Проверить, что кнопка `Проверить логин` находится ниже статуса, а текст про частые вопросы снизу страницы отсутствует. +- ожидаемый результат: + Регистрация ведёт себя предсказуемо: пароль можно показать или скрыть, слова не теряются при простом скрытии режима `12 слов`, а статус логина обновляется автоматически и вручную. +- статус: + pending diff --git a/docs/deploy/README.md b/docs/deploy/README.md index cf6d69d..fc1a9c6 100644 --- a/docs/deploy/README.md +++ b/docs/deploy/README.md @@ -18,7 +18,7 @@ - Production: - SSH: `player@shineup.me` - Домен: `shineup.me` - - IP: `185.229.109.118` + - IP: `178.208.64.62` - Second production: - SSH: `player@193.8.215.70` - Домен: `t.shineup.me` diff --git a/docs/deploy/servers/shineup.me_main.md b/docs/deploy/servers/shineup.me_main.md index 66700ba..3e94738 100644 --- a/docs/deploy/servers/shineup.me_main.md +++ b/docs/deploy/servers/shineup.me_main.md @@ -2,6 +2,7 @@ - SSH: `player@shineup.me` - Определение IP: через DNS-резолв домена `shineup.me` на момент подключения +- Текущий IP на момент последнего обновления документа: `178.208.64.62` - Пользователь: `player` - Базовый путь: `/home/player` - Каталог SHiNE: `/home/player/SHiNE` @@ -17,7 +18,7 @@ ## Caddy -- Активный конфиг (через systemd `ExecStart`): `/home/player/SHiNE/caddy/Caddyfile` +- Активный конфиг (через systemd `ExecStart`): `/etc/caddy/Caddyfile` - Для UI: - `root * /home/player/SHiNE/shine-ui` - `try_files {path} /index.html` (SPA fallback) diff --git a/docs/ИТХ/Спецификация_ИТХ_v1.md b/docs/ИТХ/Спецификация_ИТХ_v1.md index eba6ef6..746a8ec 100644 --- a/docs/ИТХ/Спецификация_ИТХ_v1.md +++ b/docs/ИТХ/Спецификация_ИТХ_v1.md @@ -84,12 +84,16 @@ RotaV1 ### 5.1. Контейнер ``` -magic: 8 байт ASCII "SHNCKPT1" +magic: 16 байт ASCII "SHiNE-CHECKPOINT" (без завершающего нуля) headerLen: u32 BE — длина headerJson в байтах headerJson: UTF-8 JSON (раздел 5.2) bundle: записи бандла подряд (раздел 5.4), суммарной длиной до конца файла ``` +Магик читаемый — открыв файл в hex-редакторе, видно, что это (по образцу +`"SQLite format 3\0"`). Версия формата в магик не входит: она в поле `v` +заголовка и в первой строке preimage (`SHiNE-CHECKPOINT-V1`). + Рекомендуемые Arweave-теги: `App-Name: SHiNE`, `Type: checkpoint`, `Day-Index: ` (теги информационные, доверия к ним нет — истина в подписи). diff --git a/shine-UI/js/pages/register-view.js b/shine-UI/js/pages/register-view.js index 00bfcf0..a0b08fc 100644 --- a/shine-UI/js/pages/register-view.js +++ b/shine-UI/js/pages/register-view.js @@ -14,7 +14,6 @@ import { PASSWORD_WORDS_COUNT, } from '../services/password-words.js'; import { sha256Text } from '../services/crypto-utils.js'; -import { openRegistrationFaq } from './registration-faq-view.js'; import { defaultServerHttp } from '../deploy-config.js'; export const pageMeta = { id: 'register-view', title: 'Зарегистрироваться', showAppChrome: false }; @@ -89,6 +88,24 @@ function createWordsLayout({ words, onInput }) { return { section, inputs, preview }; } +function makePasswordToggleIcons() { + return { + eye: ` + + `, + eyeOff: ` + + `, + }; +} + export function render({ navigate }) { const screen = document.createElement('section'); screen.className = 'stack'; @@ -100,7 +117,10 @@ export function render({ navigate }) { let passwordMode = String(state.registrationDraft.passwordMode || 'single') === 'words' ? 'words' : 'single'; let passwordWords = normalizePasswordWords(state.registrationDraft.passwordWords); + let passwordWordsLinked = Boolean(state.registrationDraft.passwordWordsLinked); let usePromoCode = Boolean(state.registrationDraft.usePromoCode); + let loginCheckTimer = 0; + let loginCheckRunId = 0; const loginInput = document.createElement('input'); loginInput.className = 'input'; @@ -119,9 +139,22 @@ export function render({ navigate }) { passwordInput.autocapitalize = 'off'; passwordInput.spellcheck = false; passwordInput.maxLength = PASSWORD_MAX_LENGTH; - passwordInput.value = passwordMode === 'single' ? state.registrationDraft.password : ''; + passwordInput.value = String(state.registrationDraft.password || ''); passwordInput.placeholder = 'Введите пароль'; + const passwordIcons = makePasswordToggleIcons(); + + const passwordToggleButton = document.createElement('button'); + passwordToggleButton.className = 'icon-btn key-toggle-btn registration-password-toggle'; + passwordToggleButton.type = 'button'; + passwordToggleButton.setAttribute('aria-label', 'Показать пароль'); + passwordToggleButton.setAttribute('title', 'Показать пароль'); + passwordToggleButton.innerHTML = passwordIcons.eyeOff; + + const passwordInputRow = document.createElement('div'); + passwordInputRow.className = 'inline-input-row'; + passwordInputRow.append(passwordInput, passwordToggleButton); + const { section: wordsSection, inputs: wordInputs, @@ -130,6 +163,7 @@ export function render({ navigate }) { words: passwordWords, onInput: (index, value) => { passwordWords[index] = value; + passwordWordsLinked = true; syncDraftState(); updateWordsPreview(); }, @@ -182,8 +216,8 @@ export function render({ navigate }) { promoField.append(promoFieldLabel, promoInput, promoHint); const statusText = document.createElement('p'); - statusText.className = 'meta-muted'; - statusText.textContent = 'Проверка логина: не выполнена'; + statusText.className = 'status-line'; + statusText.style.display = 'none'; const serverNotice = document.createElement('div'); serverNotice.className = 'card stack'; @@ -192,25 +226,6 @@ export function render({ navigate }) {

При регистрации адресом вашего первого сервера будет: ${state.entrySettings.shineServerHttp || defaultServerHttp}.

`; - const faqCard = document.createElement('div'); - faqCard.className = 'card stack registration-faq-card'; - - const faqTitle = document.createElement('p'); - faqTitle.className = 'field-label'; - faqTitle.textContent = 'Частые вопросы перед регистрацией'; - - const faqText = document.createElement('p'); - faqText.className = 'meta-muted'; - faqText.textContent = 'Если хотите подробнее понять схему деривации, ключи, первый сервер и формат 12 слов, откройте отдельный экран с вопросами.'; - - const faqButton = document.createElement('button'); - faqButton.className = 'ghost-btn'; - faqButton.type = 'button'; - faqButton.textContent = 'Частые вопросы'; - faqButton.addEventListener('click', () => openRegistrationFaq(navigate, 'key-derivation')); - - faqCard.append(faqTitle, faqText, faqButton); - const formError = document.createElement('p'); formError.className = 'status-line is-unavailable'; formError.style.display = 'none'; @@ -253,11 +268,46 @@ export function render({ navigate }) { passwordLengthText.textContent = text; } + function setStatusMessage(message, kind = '') { + statusText.textContent = message; + statusText.className = kind ? `status-line ${kind}` : 'status-line'; + statusText.style.display = message ? '' : 'none'; + } + + function resetLoginCheckState() { + lastCheckedLogin = ''; + lastCheckedFree = false; + lastCheckedClassName = ''; + loginCheckRunId += 1; + if (loginCheckTimer) { + window.clearTimeout(loginCheckTimer); + loginCheckTimer = 0; + } + } + + function scheduleAvailabilityCheck() { + resetLoginCheckState(); + setStatusMessage(''); + if (!loginInput.value.trim()) return; + loginCheckTimer = window.setTimeout(() => { + loginCheckTimer = 0; + runAvailabilityCheck({ automatic: true }).catch(() => {}); + }, 1500); + } + + function togglePasswordVisibility() { + const reveal = passwordInput.type === 'password'; + passwordInput.type = reveal ? 'text' : 'password'; + passwordToggleButton.innerHTML = reveal ? passwordIcons.eye : passwordIcons.eyeOff; + passwordToggleButton.setAttribute('aria-label', reveal ? 'Скрыть пароль' : 'Показать пароль'); + passwordToggleButton.setAttribute('title', reveal ? 'Скрыть пароль' : 'Показать пароль'); + } + function updatePasswordModeVisibility() { const wordsMode = passwordMode === 'words'; wordsSection.style.display = wordsMode ? 'grid' : 'none'; if (passwordField) passwordField.style.display = wordsMode ? 'none' : 'grid'; - passwordInput.style.display = wordsMode ? 'none' : ''; + passwordInputRow.style.display = wordsMode ? 'grid' : 'none'; updateWordsPreview(); } @@ -265,6 +315,7 @@ export function render({ navigate }) { state.registrationDraft.login = String(loginInput.value.trim()); state.registrationDraft.passwordMode = passwordMode; state.registrationDraft.passwordWords = normalizePasswordWords(passwordWords); + state.registrationDraft.passwordWordsLinked = passwordWordsLinked; state.registrationDraft.password = getCurrentPassword(); state.registrationDraft.usePromoCode = usePromoCode; state.registrationDraft.promoCode = String(promoInput.value || '').trim(); @@ -274,26 +325,25 @@ export function render({ navigate }) { promoField.style.display = usePromoCode ? 'grid' : 'none'; } - async function runAvailabilityCheck() { + async function runAvailabilityCheck({ automatic = false } = {}) { + const runId = ++loginCheckRunId; const login = loginInput.value.trim(); const promoCode = String(promoInput.value || '').trim(); if (!login) { - statusText.textContent = 'Введите логин'; + setStatusMessage(automatic ? '' : 'Введите логин'); formError.style.display = 'none'; return false; } const normalizedLogin = normalizeLoginForTemporaryUiGuard(login); if (!normalizedLogin) { - statusText.textContent = 'Логин содержит недопустимые символы или имеет неверную длину ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин содержит недопустимые символы или имеет неверную длину ❌', 'is-unavailable'); formError.style.display = 'none'; return false; } if (normalizedLogin.length < TEMP_ABSOLUTE_MIN_LOGIN_LEN) { - statusText.textContent = `Логин должен быть не короче ${TEMP_ABSOLUTE_MIN_LOGIN_LEN} символов ❌`; - statusText.className = 'status-line is-unavailable'; + setStatusMessage(`Логин должен быть не короче ${TEMP_ABSOLUTE_MIN_LOGIN_LEN} символов ❌`, 'is-unavailable'); formError.style.display = 'none'; return false; } @@ -302,44 +352,35 @@ export function render({ navigate }) { let promoAccepted = false; if (usePromoCode) { if (!promoCode) { - statusText.textContent = 'Введите промокод ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Введите промокод ❌', 'is-unavailable'); formError.style.display = 'none'; return false; } promoAccepted = await isTemporaryPromoAccepted(promoCode); if (!promoAccepted) { - statusText.textContent = 'Неверный промокод ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Неверный промокод ❌', 'is-unavailable'); formError.style.display = 'none'; return false; } } else if (wantsShortLogin) { - statusText.textContent = `Логины короче ${TEMP_MIN_LOGIN_WITHOUT_PROMO} символов временно доступны только по специальному коду ❌`; - statusText.className = 'status-line is-unavailable'; + setStatusMessage(`Логины короче ${TEMP_MIN_LOGIN_WITHOUT_PROMO} символов временно доступны только по специальному коду ❌`, 'is-unavailable'); formError.style.display = 'none'; return false; } if (login === lastCheckedLogin) { if (!lastCheckedFree) { - statusText.textContent = 'Логин уже занят ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин уже занят ❌', 'is-unavailable'); } else if (lastCheckedClassName === 'promo') { - statusText.textContent = 'Логин свободен ✅ Временный код принят'; - statusText.className = 'status-line is-available'; + setStatusMessage('Логин свободен ✅ Временный код принят', 'is-available'); } else if (lastCheckedClassName === 'free') { - statusText.textContent = 'Логин свободен ✅'; - statusText.className = 'status-line is-available'; + setStatusMessage('Логин свободен ✅', 'is-available'); } else if (lastCheckedClassName === 'premium') { - statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин свободен, но это премиум-логин (покупка через DAO) ❌', 'is-unavailable'); } else if (lastCheckedClassName === 'company') { - statusText.textContent = 'Логин свободен, но относится к компании/бренду (отдельное согласование) ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин свободен, но относится к компании/бренду (отдельное согласование) ❌', 'is-unavailable'); } else { - statusText.textContent = 'Логин нельзя использовать для обычной регистрации ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин нельзя использовать для обычной регистрации ❌', 'is-unavailable'); } formError.style.display = 'none'; return lastCheckedFree && (lastCheckedClassName === 'free' || lastCheckedClassName === 'promo'); @@ -347,11 +388,13 @@ export function render({ navigate }) { checkButton.disabled = true; checkButton.textContent = 'Проверка...'; + if (automatic) setStatusMessage('Проверяем логин...'); try { const check = await checkLoginExistsOnSolana({ login, solanaEndpoint: state.entrySettings.solanaServer, }); + if (runId !== loginCheckRunId) return false; const isFree = !check.exists; let className = ''; let precheckWarning = ''; @@ -375,48 +418,55 @@ export function render({ navigate }) { lastCheckedFree = isFree; lastCheckedClassName = className; if (!isFree) { - statusText.textContent = 'Логин уже занят ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин уже занят ❌', 'is-unavailable'); } else if (className === 'promo') { - statusText.textContent = 'Логин свободен ✅ Временный код принят'; - statusText.className = 'status-line is-available'; + setStatusMessage('Логин свободен ✅ Временный код принят', 'is-available'); } else if (className === 'free') { - statusText.textContent = precheckWarning - ? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})` - : 'Логин свободен ✅'; - statusText.className = 'status-line is-available'; + setStatusMessage( + precheckWarning + ? `Логин свободен ✅ (предпроверка Solana недоступна: ${precheckWarning})` + : 'Логин свободен ✅', + 'is-available', + ); } else if (className === 'premium') { - statusText.textContent = 'Логин свободен, но это премиум-логин (покупка через DAO) ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин свободен, но это премиум-логин (покупка через DAO) ❌', 'is-unavailable'); } else if (className === 'company') { - statusText.textContent = 'Логин свободен, но относится к компании/бренду (отдельное согласование) ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин свободен, но относится к компании/бренду (отдельное согласование) ❌', 'is-unavailable'); } else { - statusText.textContent = 'Логин нельзя использовать для обычной регистрации ❌'; - statusText.className = 'status-line is-unavailable'; + setStatusMessage('Логин нельзя использовать для обычной регистрации ❌', 'is-unavailable'); } formError.style.display = 'none'; return isFree && (className === 'free' || className === 'promo'); } catch (error) { + if (runId !== loginCheckRunId) return false; const base = toUserMessage(error, 'Не удалось проверить логин'); const details = formatSolanaErrorDetails(error); - statusText.textContent = `${base}. Детали: ${details}`; - statusText.className = 'status-line is-unavailable'; + setStatusMessage(`${base}. Детали: ${details}`, 'is-unavailable'); return false; } finally { - checkButton.disabled = false; - checkButton.textContent = 'Проверить логин'; + if (runId === loginCheckRunId) { + checkButton.disabled = false; + checkButton.textContent = 'Проверить логин'; + } } } - checkButton.addEventListener('click', runAvailabilityCheck); + checkButton.addEventListener('click', () => runAvailabilityCheck()); + passwordToggleButton.addEventListener('click', togglePasswordVisibility); loginInput.addEventListener('input', () => { syncDraftState(); - lastCheckedLogin = ''; + scheduleAvailabilityCheck(); }); passwordInput.addEventListener('input', () => { + if (passwordWordsLinked && String(passwordInput.value || '') !== composePasswordFromWords(passwordWords)) { + passwordWords = emptyPasswordWords(); + passwordWordsLinked = false; + wordInputs.forEach((input) => { + input.value = ''; + }); + } syncDraftState(); updateWordsPreview(); }); @@ -425,13 +475,16 @@ export function render({ navigate }) { const nextMode = passwordModeCheckbox.checked ? 'words' : 'single'; if (nextMode === passwordMode) return; if (nextMode === 'words') { - passwordWords = emptyPasswordWords(); - wordInputs.forEach((input) => { - input.value = ''; - }); - passwordInput.value = ''; + if (!passwordWordsLinked) { + passwordWords = emptyPasswordWords(); + wordInputs.forEach((input) => { + input.value = ''; + }); + passwordInput.value = ''; + } } else { passwordInput.value = composePasswordFromWords(passwordWords); + passwordWordsLinked = true; } passwordMode = nextMode; updatePasswordModeVisibility(); @@ -441,25 +494,21 @@ export function render({ navigate }) { promoCheckbox.addEventListener('change', () => { usePromoCode = promoCheckbox.checked; - lastCheckedLogin = ''; - lastCheckedFree = false; - lastCheckedClassName = ''; + resetLoginCheckState(); updatePromoVisibility(); syncDraftState(); if (usePromoCode) { - statusText.textContent = 'Временный код включён: для логинов 5-7 символов будет локальная проверка'; - statusText.className = 'status-line is-available'; + setStatusMessage('Временный код включён: для логинов 5-7 символов будет локальная проверка', 'is-available'); } else { - statusText.textContent = 'Проверка логина: не выполнена'; - statusText.className = 'meta-muted'; + setStatusMessage(''); + scheduleAvailabilityCheck(); } }); promoInput.addEventListener('input', () => { - lastCheckedLogin = ''; - lastCheckedFree = false; - lastCheckedClassName = ''; + resetLoginCheckState(); syncDraftState(); + if (usePromoCode) scheduleAvailabilityCheck(); }); nextButton.addEventListener('click', async () => { @@ -493,6 +542,7 @@ export function render({ navigate }) { state.registrationDraft.password = nextPassword; state.registrationDraft.passwordMode = passwordMode; state.registrationDraft.passwordWords = normalizePasswordWords(passwordWords); + state.registrationDraft.passwordWordsLinked = passwordWordsLinked; state.registrationDraft.usePromoCode = usePromoCode; state.registrationDraft.promoCode = promoCode; if (credsChanged) { @@ -504,16 +554,18 @@ export function render({ navigate }) { function renderInputStage() { serverNotice.style.display = ''; - faqCard.style.display = ''; - form.innerHTML = ` - - - `; - const loginField = form.children[0]; - passwordField = form.children[1]; + form.innerHTML = ''; + const loginField = document.createElement('label'); + loginField.className = 'stack'; + loginField.innerHTML = 'Логин'; + const passwordLabel = document.createElement('label'); + passwordLabel.className = 'stack registration-password-single'; + passwordLabel.innerHTML = 'Пароль'; + form.append(loginField, statusText, checkButton, passwordLabel); + passwordField = passwordLabel; loginField.append(loginInput); - passwordField.append(passwordInput); - form.append(passwordModeToggle, promoToggle, promoField, passwordLengthText, wordsSection, statusText, checkButton, formError); + passwordField.append(passwordInputRow); + form.append(passwordModeToggle, promoToggle, promoField, passwordLengthText, wordsSection, formError); actions.innerHTML = ''; actions.append(backButton, nextButton); updatePasswordModeVisibility(); @@ -523,7 +575,6 @@ export function render({ navigate }) { async function startGenerationStage() { serverNotice.style.display = 'none'; - faqCard.style.display = 'none'; const runId = ++generationRunId; form.innerHTML = ''; @@ -630,7 +681,6 @@ export function render({ navigate }) { }), form, serverNotice, - faqCard, actions, ); diff --git a/shine-UI/js/pages/registration-keys-view.js b/shine-UI/js/pages/registration-keys-view.js index 481431f..f66f697 100644 --- a/shine-UI/js/pages/registration-keys-view.js +++ b/shine-UI/js/pages/registration-keys-view.js @@ -155,6 +155,7 @@ export function render({ navigate }) { state.registrationDraft.password = ''; state.registrationDraft.passwordMode = 'single'; state.registrationDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice(); + state.registrationDraft.passwordWordsLinked = false; state.registrationDraft.usePromoCode = false; state.registrationDraft.promoCode = ''; state.registrationDraft.storagePwd = ''; diff --git a/shine-UI/js/pages/registration-payment-view.js b/shine-UI/js/pages/registration-payment-view.js index a5731c7..639b753 100644 --- a/shine-UI/js/pages/registration-payment-view.js +++ b/shine-UI/js/pages/registration-payment-view.js @@ -82,6 +82,7 @@ async function completeRegistrationLogin({ navigate, keyBundle }) { state.registrationDraft.password = ''; state.registrationDraft.passwordMode = 'single'; state.registrationDraft.passwordWords = EMPTY_PASSWORD_WORDS.slice(); + state.registrationDraft.passwordWordsLinked = false; state.registrationDraft.usePromoCode = false; state.registrationDraft.promoCode = ''; state.registrationDraft.storagePwd = ''; diff --git a/shine-UI/js/state.js b/shine-UI/js/state.js index 3a55c60..f5e4cb9 100644 --- a/shine-UI/js/state.js +++ b/shine-UI/js/state.js @@ -286,6 +286,7 @@ function createInitialState({ withStoredSession = true } = {}) { password: '', passwordMode: 'single', passwordWords: emptyPasswordWords(), + passwordWordsLinked: false, usePromoCode: false, promoCode: '', sessionId: '', diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index aa4ab4b..8caa4e9 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -4417,6 +4417,14 @@ html, body { overflow-x: hidden; } color: #f6deb0; } +.registration-password-toggle { + width: 44px; + height: 44px; + min-width: 44px; + min-height: 44px; + border-radius: 14px; +} + .registration-progress { width: 100%; height: 10px;