Files
bare/docs/ui.md
T
mayatnikovandClaude Opus 5 f9ac83bef8 Развести номера ADR: 064/065/066 этой ветки → 073/074/075
Ветка отведена от 522ba89, где последним был ADR-063, и заняла
064–072. Тем временем в main через #3 и #4 пришли свои 064
(строка ввода — div вместо form), 065 (стороны сообщений)
и 066 (серверного перца нет).

Переезжают три ADR этой ветки, 067–072 остаются на месте:
их номера свободны, а внутренние ссылки менять незачем.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QR8uS3zGkybzWRW4GEY1oz
2026-08-24 11:23:12 +03:00

23 KiB
Raw Blame History

Интерфейс

Визуальная система — docs/identity/brief.md, эталон экрана чата — docs/identity/screens.html. Здесь — состав экранов, поведение и тексты. Все тексты — русские, строчными, как в моке; заглавная только в начале предложений из нескольких слов.

Каркас

Одна страница index.html, роутинг по hash: #/ — список (на десктопе — первый чат), #/dm/<nick>, #/room/<id>, #/room/<id>/members, #/contact/<nick>, #/settings, #/new. Десктоп (≥ 760 px): сайдбар 224 px + чат. Мобильный: один экран за раз, «назад» — в шапке слева.

Без inline-стилей и inline-скриптов (CSP). Рендер — document.createElement и textContent; innerHTML не используется нигде: сообщения — пользовательские данные.

Страница ведёт себя как приложение, а не как документ (ADR-075): масштабирования нет, резинового отскока нет, выделение снято с шапки, сайдбара, кнопок и разделителей — но не с текста сообщений, отпечатков, полей ввода и версии в подвале сайдбара: их копируют. Установленное на «Домой» приложение объявлено мета-строками, имя на экране — bare, системная полоса остаётся над страницей. Подвал сайдбара и строка ввода отступают от нижней системной полосы iPhone.

Вход и регистрация

Одна страница, два режима переключателем «вход / регистрация». Логотип-знак и bare сверху.

Поля: ник, пароль. В регистрации дополнительно инвайт-код, если config.inviteRequired, и текст под паролем:

пароль — это ключ шифрования, а не запись в базе. восстановления нет. не короче 12 символов; лучше — фраза из нескольких слов.

Кнопка одна, в стиле строки ввода. Пока идёт PBKDF2 — состояние «вычисляем ключ…», кнопка заблокирована. Ошибки — строкой под формой цветом mark: «неверный ник или пароль», «ник занят», «ник: 2–32 символа, a–z, 0–9, _», «нужен инвайт-код», «инвайт-код не подходит». Форму ника и длину пароля клиент проверяет сам, до PBKDF2, в обоих режимах. Остальные состояния — «Тексты состояний».

Если на устройстве лежат ключи другого ника, до вычисления ключа — подтверждение «на этом устройстве история @nick. вход под другим ником удалит её.» с кнопками «экспортировать», «удалить» и «отмена» (ADR-029, ADR-051). «экспортировать» скачивает .bare прежнего аккаунта и подтверждение не закрывает; сессия для этого не нужна. База стирается после успешного входа или регистрации; отказ сервера её не трогает.

Список чатов (сайдбар)

Секции «каналы» и «личные», как в моке. Активный чат — инверсия (ink на bone). Непрочитанные — число цветом mark справа. Порядок — по lastId по убыванию. Внизу — «ты: @nick», по нажатию — настройки. Над секциями — строка + новый чат.

В подвале рядом с «ты: @nick» — версия и время коммита из GET /api/config: версия · дд.мм чч:мм, время в местной зоне, цветом stone, размером подписи. Не кнопка, но выделяется: её копируют. Времени нет — остаётся версия; конфигурации ещё нет — строки нет. В сайдбаре 224 px рядом они не помещаются: версия занимает вторую строку и стоит справа; на мобильном, где сайдбар во всю ширину, обе стоят в одной строке. Ник, которому не хватило места, обрезается многоточием (ADR-067).

Версия — серверная. Оболочка в браузере бывает старше: она приезжает из кэша, а обновление применяется отдельно и ждёт пустых полей (ADR-068).

Новый чат (#/new)

Две строки ввода: @ник → открыть личный чат; #имя комнаты → создать комнату. Ошибки: «такого ника нет», «нельзя писать себе».

Чат

Шапка: имя (#general / @marta), по нажатию — участники или карточка контакта. Без темы и «N онлайн».

Лента: десктоп — сетка «автор 132 px + текст», подряд идущие сообщения одного автора — без повтора автора; мобильный — автор над группой. Свой ник в колонке автора — цветом mark. Разделители дат — линия с датой; «новые» — линия цветом mark перед первым непрочитанным, исчезает при следующем открытии чата. Pending — текст цветом stone; failed — с пометкой «не отправлено · повторить». Нерасшифрованное — курсивом: «не удалось расшифровать: ключ изменился» / «…: нет ключа комнаты». Время — ts в локальной зоне, ЧЧ:ММ.

Лента открывается последними 50 сообщениями и стоит в конце. Прокрутка к верхнему краю подгружает следующие 50; то, что человек читает, при этом не двигается. Загруженное остаётся в разметке целиком — виртуализации нет (ADR-053).

Ввод: рамка 1 px ink, слева > цветом mark, подсказка «сообщение в #general» / «сообщение»; имя комнаты в подсказке обрезается до 12 символов многоточием — «сообщение в #длинноеимя…» (ADR-061), в шапке оно остаётся полным. Enter — отправить, Shift+Enter — перенос; на мобильном Enter — перенос, отправка — кнопка > справа. Подсказка «enter — отправить» только на десктопе. Лимит 4000 — счётчик появляется после 3500; лишнее обрезается и при наборе, и при вставке: режется приходящее, набранное остаётся на месте (ADR-071).

Строка сообщения — редактируемый блок, а не поле формы: над клавиатурой iOS рисует полосу помощника форм, и бывает она только у input и textarea (ADR-069). Блок принимает и отдаёт только текст: вставка приходит текстом, значение читается и пишется textContent, innerHTML не появляется. Подсказку рисует правило CSS для пустого блока. Растёт под содержимое, дальше семи строк — прокрутка. Заблокированный ввод перестаёт быть редактируемым. Остальные строки ввода — вход, регистрация, смена пароля, «новый чат», участники — остаются полями формы: там нужны менеджеры паролей, а полоса на разовом экране не мешает.

Предупреждение о ключе — полоса над вводом цветом mark: «ключ @marta изменился. сверьте отпечаток лично. [доверять новому ключу]». Ввод заблокирован до подтверждения. Полоса одна: предупреждение о ключе перебивает отказ отправки и «нет соединения» (ADR-038).

Комната, из состава которой нас больше нет (вышли сами, убрал владелец, комната удалена), — та же полоса цветом mark: «вы больше не участник комнаты». Ввод заблокирован, лента остаётся. Эта полоса перебивает и предупреждение о ключе (ADR-044).

Отказ отправки — та же полоса над вводом цветом mark с текстом из поля error последнего неотправленного сообщения (ADR-033): «проверьте часы на устройстве: расхождение больше 5 минут», «слишком часто, попробуйте позже», «сервер не справился, попробуйте позже». Полоса исчезает при следующей попытке. Ввод не блокируется.

Первое отправленное сообщение за всю историю устройства → запрос разрешения на уведомления (см. «Уведомления»).

Карточка контакта (#/contact/<nick>)

@nick, отпечаток 64 hex группами по 4 в две строки, строка «сверьте с собеседником голосом или лично». Если есть pending — оба отпечатка с пометками «старый» и «новый» и кнопка «доверять новому ключу». Кнопка «убрать из списка».

pending снимает и сервер, снова отдавший доверенный ключ: смены ключа не случилось, состояние закрывается само и молча (ADR-040).

Участники (#/room/<id>/members)

Список ников; у владельца — пометка «владелец». Владельцу: строка ввода @ник + «добавить», у каждого участника «убрать». Всем: «выйти из комнаты»; владельцу — «удалить комнату» с подтверждением «комната будет удалена у всех участников.» и кнопками «удалить» и «отмена». Если клиент-владелец получил needsRekey и не может выполнить rekey из-за неподтверждённого ключа — полоса: «нужен новый ключ комнаты: подтвердите ключ @x». Тот же текст — строкой состояния формы, когда неподтверждённый ключ обрывает добавление или удаление участника; ников в нём бывает несколько, через запятую (ADR-038).

Настройки (#/settings)

  • «ты: @nick», свой отпечаток.
  • «уведомления»: состояние (включены / выключены / запрещены в браузере), кнопка «включить» или «выключить». запрещены в браузере — разрешение отклонено или уведомлений в браузере нет вовсе; кнопки в этом состоянии нет (ADR-046). На iOS вне PWA — состояние выключены и вместо кнопки текст про установку, тот же, что в баннере.
  • «установить приложение»: кнопка «установить», если есть beforeinstallprompt; на iOS вне PWA — инструкция «поделиться → на экран «домой»». Устанавливать нечего — раздела нет.
  • «устройства»: список id (первые 8 символов), дата появления, «удалить». У своей строки кнопки нет: вместо неё пометка «это устройство». Своё устройство отсюда не отцепляется — это делает «выйти», где спрашивают про историю (ADR-052).
  • «история»: «занято N МБ» — оценка браузера, до десятых, пока меньше десяти, дальше целые; браузер, который её не даёт, строки не показывает (ADR-052). «экспорт» → скачивание .bare; «импорт» → выбор файла → «добавлено N сообщений» / «архив создан другим аккаунтом» / «файл повреждён». Число согласуется со словом: «добавлено 1 сообщение», «добавлено 2 сообщения», «добавлено 5 сообщений» (ADR-051).
  • «сменить пароль»: старый, новый, повтор; чекбокс «выйти на других устройствах». Ответ — «пароль изменён».
  • «выйти»: подтверждение «история на этом устройстве будет удалена. экспортировать сначала?» с кнопками «экспортировать», «выйти», «отмена». «экспортировать» — то же скачивание, что и в разделе «история»; подтверждение оно не закрывает (ADR-051).
  • «удалить аккаунт»: пароль + подтверждение «аккаунт и вся история будут удалены навсегда.» с кнопками «удалить» и «отмена».

Баннер установки (iOS)

Показывается при iPhone|iPad и navigator.standalone !== true, над списком чатов: «уведомления на iOS работают только у установленного приложения: поделиться → на экран «домой»». Крестик — «×» с подписью «закрыть» для экранного диктора — ставит installBannerDismissed, повтор не показывается.

Уведомления

Запрос разрешения — после первого успешно отправленного сообщения, один раз (notificationsAsked). После grantedpushManager.subscribe с vapidPublicKey и PUT /api/devices/{id}/push. Отказ — молча; включить можно в настройках.

Выключенные кнопкой уведомления сами не включаются: ни первым сообщением, ни запуском приложения. Обратно их включает только кнопка (ADR-049).

Уведомление: заголовок — @nick отправителя или #имя комнаты, текст — «новое сообщение», нажатие открывает этот чат. Содержимого сообщения в уведомлении нет: сервер его не знает (ADR-011). Пуш о собственном сообщении не приходит (ADR-045).

Сеть и состояния

  • SSE переподключается браузером; после ready клиент перечитывает комнаты и контакты и повторяет pending.
  • Вкладок одного профиля бывает несколько; поток событий держит одна из них, остальные получают изменения от неё и выглядят так же (ADR-035).
  • Без сети: полоса «нет соединения» цветом stone над вводом; ввод не блокируется — сообщения уходят в pending.
  • clock_skew — «проверьте часы на устройстве: расхождение больше 5 минут».
  • Приложение обновляется само: клиент спрашивает сервер о новой оболочке при запуске и при возвращении в приложение, не чаще раза в минуту, включает установленную версию и перезагружает страницу. Перезагрузка вкладки ждёт, пока пусты все её поля и строка сообщения: набранное она бы унесла. Опустевшее поле она замечает и там, где о нём не сказало ни одно событие — отправленное сообщение, уход с экрана: к отложенному вкладка возвращается по таймеру (ADR-072). Вкладок бывает несколько (ADR-035), и каждая ждёт своих полей: соседняя может включить новую оболочку раньше, но перезагрузиться под набранным текстом не заставит. Без сети проверка проходит молча (ADR-068).
  • Перезагружается вкладка только тогда, когда версия оболочки и правда сменилась. Тот же файл воркера, отданный сервером заново, её не трогает: перезагрузка вернула бы ту же страницу. Между перезагрузками — не меньше 30 секунд; сколько бы релизов ни вышло за жизнь вкладки, доезжают все (ADR-070).
  • 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.

Строка сообщения — редактируемый блок (ADR-069), и семантику ему задают руками: role="textbox", aria-multiline="true", подпись aria-label тем же текстом, что подсказка. Заблокированный ввод перестаёт быть редактируемым и говорит об этом aria-disabled.

Масштабирование запрещено — и щипок, и двойной тап (ADR-075). Это сознательный размен: интерфейс ведёт себя как приложение, а человек, которому нужно увеличить мелкое, средства лишается. Размеры шрифтов от запрета не меняются: поля ввода остаются 14 px, как в docs/identity/brief.md. Браузер вправе запрет проигнорировать.