# Интерфейс Визуальная система — `docs/identity/brief.md`, эталон экрана чата — `docs/identity/screens.html`. Здесь — состав экранов, поведение и тексты. Все тексты — русские, строчными, как в моке; заглавная только в начале предложений из нескольких слов. ## Каркас Одна страница `index.html`, роутинг по hash: `#/` — список (на десктопе — первый чат), `#/dm/`, `#/room/`, `#/room//members`, `#/contact/`, `#/settings`, `#/new`. Десктоп (≥ 760 px): сайдбар 224 px + чат. Мобильный: один экран за раз, «назад» — в шапке слева. Без inline-стилей и inline-скриптов (CSP). Рендер — `document.createElement` и `textContent`; `innerHTML` не используется нигде: сообщения — пользовательские данные. ## Вход и регистрация Одна страница, два режима переключателем «вход / регистрация». Логотип-знак и `bare` сверху. Поля: `ник`, `пароль`. В регистрации дополнительно `инвайт-код`, если `config.inviteRequired`, и текст под паролем: > пароль — это ключ шифрования, а не запись в базе. восстановления нет. не короче 12 символов; лучше — фраза из нескольких слов. Кнопка одна, в стиле строки ввода. Пока идёт PBKDF2 — состояние «вычисляем ключ…», кнопка заблокирована. Ошибки — строкой под формой цветом `mark`: «неверный ник или пароль», «ник занят», «ник: 2–32 символа, a–z, 0–9, _», «нужен инвайт-код», «инвайт-код не подходит». Форму ника и длину пароля клиент проверяет сам, до PBKDF2, в обоих режимах. Остальные состояния — «Тексты состояний». Если на устройстве лежат ключи другого ника, до вычисления ключа — подтверждение «на этом устройстве история @nick. вход под другим ником удалит её.» с кнопками «удалить» и «отмена» (ADR-029). База стирается после успешного входа или регистрации; отказ сервера её не трогает. ## Список чатов (сайдбар) Секции «каналы» и «личные», как в моке. Активный чат — инверсия (ink на bone). Непрочитанные — число цветом `mark` справа. Порядок — по `lastId` по убыванию. Внизу — «ты: @nick», по нажатию — настройки. Над секциями — строка `+ новый чат`. ## Новый чат (`#/new`) Две строки ввода: `@ник` → открыть личный чат; `#имя комнаты` → создать комнату. Ошибки: «такого ника нет», «нельзя писать себе». ## Чат Шапка: имя (`#general` / `@marta`), по нажатию — участники или карточка контакта. Без темы и «N онлайн». Лента: десктоп — сетка «автор 132 px + текст», подряд идущие сообщения одного автора — без повтора автора; мобильный — автор над группой. Свой ник в колонке автора — цветом `mark`. Разделители дат — линия с датой; «новые» — линия цветом `mark` перед первым непрочитанным, исчезает при следующем открытии чата. Pending — текст цветом `stone`; failed — с пометкой «не отправлено · повторить». Нерасшифрованное — курсивом: «не удалось расшифровать: ключ изменился» / «…: нет ключа комнаты». Время — `ts` в локальной зоне, `ЧЧ:ММ`. Ввод: рамка 1 px ink, слева `>` цветом `mark`, placeholder «сообщение в #general» / «сообщение». Enter — отправить, Shift+Enter — перенос; на мобильном Enter — перенос, отправка — кнопка `>` справа. Подсказка «enter — отправить» только на десктопе. Лимит 4000 — счётчик появляется после 3500. Предупреждение о ключе — полоса над вводом цветом `mark`: «ключ @marta изменился. сверьте отпечаток лично. [доверять новому ключу]». Ввод заблокирован до подтверждения. Полоса одна: предупреждение о ключе перебивает отказ отправки и «нет соединения» (ADR-038). Комната, из состава которой нас больше нет (вышли сами, убрал владелец, комната удалена), — та же полоса цветом `mark`: «вы больше не участник комнаты». Ввод заблокирован, лента остаётся. Эта полоса перебивает и предупреждение о ключе (ADR-044). Отказ отправки — та же полоса над вводом цветом `mark` с текстом из поля `error` последнего неотправленного сообщения (ADR-033): «проверьте часы на устройстве: расхождение больше 5 минут», «слишком часто, попробуйте позже», «сервер не справился, попробуйте позже». Полоса исчезает при следующей попытке. Ввод не блокируется. Первое отправленное сообщение за всю историю устройства → запрос разрешения на уведомления (см. «Уведомления»). ## Карточка контакта (`#/contact/`) `@nick`, отпечаток 64 hex группами по 4 в две строки, строка «сверьте с собеседником голосом или лично». Если есть `pending` — оба отпечатка с пометками «старый» и «новый» и кнопка «доверять новому ключу». Кнопка «убрать из списка». `pending` снимает и сервер, снова отдавший доверенный ключ: смены ключа не случилось, состояние закрывается само и молча (ADR-040). ## Участники (`#/room//members`) Список ников; у владельца — пометка «владелец». Владельцу: строка ввода `@ник` + «добавить», у каждого участника «убрать». Всем: «выйти из комнаты»; владельцу — «удалить комнату» с подтверждением «комната будет удалена у всех участников.» и кнопками «удалить» и «отмена». Если клиент-владелец получил `needsRekey` и не может выполнить rekey из-за неподтверждённого ключа — полоса: «нужен новый ключ комнаты: подтвердите ключ @x». Тот же текст — строкой состояния формы, когда неподтверждённый ключ обрывает добавление или удаление участника; ников в нём бывает несколько, через запятую (ADR-038). ## Настройки (`#/settings`) - «ты: @nick», свой отпечаток. - «уведомления»: состояние (`включены` / `выключены` / `запрещены в браузере`), кнопка включить/выключить. На iOS вне PWA — текст про установку. - «установить приложение»: кнопка, если есть `beforeinstallprompt`; на iOS — инструкция «поделиться → на экран «домой»». - «устройства»: список `id` (первые 8 символов), дата, «это устройство», «удалить». - «история»: «занято N МБ»; «экспорт» → скачивание `.bare`; «импорт» → выбор файла → «добавлено N сообщений» / «архив создан другим аккаунтом» / «файл повреждён». - «сменить пароль»: старый, новый, повтор; чекбокс «выйти на других устройствах». Ответ — «пароль изменён». - «выйти»: подтверждение «история на этом устройстве будет удалена. экспортировать сначала?» с кнопками «экспортировать», «выйти», «отмена». - «удалить аккаунт»: пароль + подтверждение «аккаунт и вся история будут удалены навсегда.» с кнопками «удалить» и «отмена». ## Баннер установки (iOS) Показывается при `iPhone|iPad` и `navigator.standalone !== true`, над списком чатов: «уведомления на iOS работают только у установленного приложения: поделиться → на экран «домой»». Крестик — `installBannerDismissed`, повтор не показывается. ## Уведомления Запрос разрешения — после первого успешно отправленного сообщения, один раз (`notificationsAsked`). После `granted` — `pushManager.subscribe` с `vapidPublicKey` и `PUT /api/devices/{id}/push`. Отказ — молча; включить можно в настройках. ## Сеть и состояния - SSE переподключается браузером; после `ready` клиент перечитывает комнаты и контакты и повторяет `pending`. - Вкладок одного профиля бывает несколько; поток событий держит одна из них, остальные получают изменения от неё и выглядят так же (ADR-035). - Без сети: полоса «нет соединения» цветом `stone` над вводом; ввод не блокируется — сообщения уходят в `pending`. - `clock_skew` — «проверьте часы на устройстве: расхождение больше 5 минут». - `401 unauthenticated` на любом запросе — выход на экран входа с сохранением IndexedDB (сессия истекла, история остаётся). Исключение одно: служебный выход перед повторным входом при смене пароля и удалении аккаунта (ADR-031) — там этот ответ означает, что сессии и так нет. ## Тексты состояний Общие для всех форм строки (ADR-028). Ошибка — цветом `mark`, ответ об успехе — цветом `mute`, место одно. | состояние | текст | |---|---| | запрос не дошёл | «нет соединения» | | код ответа, на который нет сценария (`internal`, `too_large`, прочее) | «сервер не справился, попробуйте позже» | | `429 rate_limited` | «слишком часто, попробуйте позже» | | пароль короче 12 символов | «пароль: не короче 12 символов» | | новый пароль и повтор различаются | «пароли не совпадают» | | ключевой блоб не разобран, не расшифрован или не соответствует публичному ключу | «ключ аккаунта повреждён» | | `iter` блоба не равен ответу `GET /api/kdf` | «параметры ключа не совпали» | | `401 invalid_credentials` в настройках | «неверный пароль» | ## Доступность Семантика: `nav`, `main`, `form`, `button`, `ul/li` для списков; `aria-live="polite"` на ленте; фокус в строку ввода при открытии чата на десктопе; контраст ink/bone и mark/bone не ниже 4.5:1; цели нажатия на мобильном не меньше 44 px.