Чат v1.1.1: упростить ввод и стороны по устройству

This commit is contained in:
2026-08-24 12:17:13 +03:00
parent d7624dfade
commit 1a56d2f8a1
17 changed files with 146 additions and 318 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
# ADR-024: Айдентика «Скобы», интерфейс и язык
Уточнён [ADR-075](075-no-zoom-app-feel.md) (масштабирование запрещено, размеры шрифтов прежние), [ADR-067](067-version-in-sidebar-foot.md) (версия и время коммита в подвале сайдбара) и [ADR-069](069-message-input-is-editable-block.md) (строка сообщения — редактируемый блок).
Уточнён [ADR-075](075-no-zoom-app-feel.md) (масштабирование запрещено, размеры шрифтов прежние), [ADR-067](067-version-in-sidebar-foot.md) (версия и время коммита в подвале сайдбара), [ADR-076](076-ios-input-accessory-is-system.md) (строка сообщения — `textarea`) и [ADR-077](077-message-alignment-by-pointer.md) (стороны сообщений зависят от способа ввода).
## Контекст
+3 -1
View File
@@ -1,5 +1,7 @@
# ADR-064: строка ввода — div, а не form
Пересмотрен [ADR-076](076-ios-input-accessory-is-system.md): ни контейнер, ни вид редактируемого элемента не дают веб-странице управления системной панелью iOS; поле снова стало `textarea`, контейнер `div` оставлен как безвредная деталь реализации.
Уточняет `docs/ui.md`, «Доступность»: `form` в списке семантики держит `role="form"`, а не элемент `<form>`, — для строки ввода чата.
Пересмотрен [ADR-069](069-message-input-is-editable-block.md). Диагноз здесь неверен: полосу помощника форм iOS рисует не из-за `<form>`, а для любых `input` и `textarea`, — вынос поля из формы её не убрал. Контейнер `div` с `role="form"` и кнопка `type="button"` остаются, `textarea` заменён редактируемым блоком — тем самым «следующим шагом», который записан ниже в «Следствиях».
@@ -19,7 +21,7 @@
- `<form class="compose">``<div class="compose" role="form">`. `role="form"` — тот же ориентир для скринридера, что и элемент `<form>`, без вызывающих панель heuristics WebKit.
- Кнопка отправки — `type="button"` с явным `click`, вместо `type="submit"` и события `submit` на форме.
- `autocomplete="off"` на самом поле — на случай, если панель зависит ещё и от него, а не только от `<form>`.
- `textarea`, `maxLength`, `field-sizing: content`, placeholder без изменений.
- На момент решения `textarea`, `maxLength`, `field-sizing: content` и placeholder оставались без изменений; актуальное состояние строки ввода задаёт пересмотревший это решение ADR-076.
Остальные формы (вход, `#/new`, участники, настройки) не тронуты: там несколько полей в форме, панель навигации между ними — по крайней мере, не чистый минус, и незачем менять то, что не жаловались.
@@ -1,5 +1,7 @@
# ADR-065: сообщения выравниваются по стороне — свои вправо, чужие влево
Уточнён [ADR-077](077-message-alignment-by-pointer.md): стороны остаются на телефоне и планшете, на десктопе все сообщения выравниваются слева.
Пересматривает часть [ADR-024](024-identity-and-ui.md): десктопную сетку ленты «автор 132 px + текст» из `docs/identity/brief.md`, «Компоновка». Уточняет `docs/ui.md`, «Чат».
## Контекст
@@ -1,5 +1,7 @@
# ADR-069: Строка сообщения — редактируемый блок, а не поле формы
Пересмотрен [ADR-076](076-ios-input-accessory-is-system.md): проверка на реальном iPhone опровергла главное следствие — системная панель остаётся и у `contenteditable`; строка снова стала `textarea`.
Уточняет [ADR-024](024-identity-and-ui.md) («Чат» и «Доступность» в `docs/ui.md`) и [ADR-075](075-no-zoom-app-feel.md): в перечне элементов управления, по которым второй тап не гасится, прибавился редактируемый блок.
Уточнён [ADR-071](071-input-limit-cuts-what-arrives.md): предел держится до вставки и режет приходящее, а не хвост блока.
@@ -1,5 +1,7 @@
# ADR-071: Предел строки сообщения держится до вставки
Пересмотрен [ADR-076](076-ios-input-accessory-is-system.md): после возврата к `textarea` предел снова держит нативный `maxLength`; ручная обработка вставки больше не нужна.
Уточняет [ADR-069](069-message-input-is-editable-block.md): предел 4000 символов держит по-прежнему сам блок, но режет он приходящее, а не набранное.
## Контекст
+1 -1
View File
@@ -2,7 +2,7 @@
Уточняет [ADR-024](024-identity-and-ui.md): типографика из `docs/identity/brief.md` остаётся как записана, а страница перестаёт масштабироваться.
Уточнён [ADR-069](069-message-input-is-editable-block.md): строка сообщения стала редактируемым блоком, и она в перечне элементов управления, по которым второй тап не гасится.
Уточнён [ADR-076](076-ios-input-accessory-is-system.md): строка сообщения снова стала `textarea`; она уже входит в перечень элементов управления, по которым второй тап не гасится.
## Контекст
@@ -0,0 +1,26 @@
# ADR-076: Панель ввода iOS системная, строка сообщения — `textarea`
Пересматривает [ADR-064](064-composer-not-form.md), [ADR-069](069-message-input-is-editable-block.md) и [ADR-071](071-input-limit-cuts-what-arrives.md). Уточняет `docs/ui.md`, «Чат» и «Доступность».
## Контекст
В v1.1 строка сообщения прошла два варианта: `textarea` вынесли из `<form>`, затем заменили на `contenteditable="plaintext-only"`. Оба решения исходили из предположения, что системная полоса iOS со стрелками и «готово» появляется только у поля формы.
Проверка v1.1 на реальном iPhone опровергла предположение: полоса осталась и у `contenteditable`. Это часть клавиатурного интерфейса WebKit, а не узел страницы. Web API для её отключения нет. В нативной оболочке внешний вид клавиатуры может менять хост iOS; установленная PWA остаётся веб-страницей и такого доступа не получает.
`contenteditable` при этом стоил клиенту ручной реализации подсказки, предела, вставки без разметки, переноса строки, `disabled`, счётчика, работы IME и запасного пути для браузеров без `plaintext-only`. Главное следствие ADR-069 не наступило, а цена осталась.
## Решение
- Строка сообщения снова становится `textarea` с нативными `placeholder`, `maxLength`, `disabled`, выделением, вставкой и отменой. Число `rows` считается по фактической высоте текста от одной до семи: это сохраняет автовысоту в Safari без поддержки `field-sizing`. Доступное имя повторяет подсказку через `aria-label`, кнопка `>` объявляется как «отправить»; `autocapitalize`, `autocorrect` и `enterkeyhint` остаются прежними.
- Контейнер `div role="form"` и кнопка `type="button"` остаются. Они не убирают системную панель, но и не создают отдельного поведения; менять их ещё раз без причины незачем.
- Системная панель iOS принимается как ограничение веб-клиента. `inputmode="none"` и собственная экранная клавиатура не используются: убрать вместе с панелью системную клавиатуру означает потерять обычный набор, раскладки, диктовку и доступность.
- Нативная iOS-оболочка не добавляется в Bare. Если абсолютное управление панелью станет требованием, это отдельный продуктовый и архитектурный выбор.
- Тап по неинтерактивному месту истории вызывает `blur()` строки сообщения и закрывает клавиатуру. Кнопки и ссылки в ленте сохраняют своё действие.
## Следствия
- Строка сообщения снова опирается на браузерное текстовое поле и стала заметно проще. Лишнее не проходит через `maxLength`; ручная обработка `beforeinput`, вставки, IME и текстовых узлов удалена.
- Полоса со стрелками и «готово» на iPhone остаётся. Код и документация больше не обещают веб-странице контроль над системным UI.
- Тап по истории возвращает место ленте без нажатия системной кнопки «готово».
- Проверка самой панели по-прежнему требует реального iPhone; поведение `textarea`, отправки и снятия фокуса проверяется в обычном браузере.
@@ -0,0 +1,21 @@
# ADR-077: На десктопе все сообщения слева, на touch свои справа
Уточняет [ADR-065](065-message-side-alignment.md) и `docs/ui.md`, «Чат».
## Контекст
ADR-065 развёл свои и чужие сообщения по сторонам на всех экранах. На телефоне и планшете сторона ускоряет чтение короткой узкой ленты. На десктопе широкая лента даёт обратный эффект: взгляд ходит между краями, хотя цвет собственного ника уже отличает автора.
Порог ширины не отличает тип устройства. Планшет бывает шире небольшого окна ноутбука и не должен менять привычную мобильную сторону только из-за поворота экрана.
## Решение
- На устройстве с точным основным указателем и наведением (`hover: hover` и `pointer: fine`) все блоки сообщений выравниваются по левому краю.
- В остальных случаях сохраняется ADR-065: свои сообщения справа, чужие слева. Это телефоны, планшеты и другие touch-интерфейсы.
- Поток, ограничение ширины блока, автор над текстом, цвет своего ника и полноширинные разделители не меняются.
## Следствия
- Десктопная лента снова читается по одному левому краю, но не возвращается к старой сетке `132px 1fr`.
- Планшет не считается десктопом только из-за ширины экрана.
- Гибридное устройство следует своему основному указателю, который сообщает браузер. Подключение дополнительной мыши само по себе правило не меняет.
+1 -1
View File
@@ -43,7 +43,7 @@ font-family: ui-monospace, "SF Mono", Menlo, Consolas, "DejaVu Sans Mono", monos
- Десктоп: сайдбар 224 px с правой границей `line`, шапка 64 px, отступы контента 32 px.
- Мобильный: шапка 56 px, отступы 20 px, ввод с min-height 44 px. Нижнее поле подвала и строки ввода считается с `env(safe-area-inset-bottom)`: под системную полосу iPhone они не заходят (ADR-075).
- Сообщения — поток на обеих ширинах, автор и время над текстом; свои — у правого края ленты, чужие — у левого, ширина блока не больше `min(640px, 85%)` (ADR-065).
- Сообщения — поток, автор и время над текстом; на телефоне и планшете свои — у правого края, чужие — у левого, на десктопе все — у левого; ширина блока не больше `min(640px, 85%)` (ADR-077).
- Ввод — рамка 1 px ink, без скруглений, `>` цветом mark слева.
- Активный элемент списка — инверсия: фон ink, текст bone.
- Разделители — 1 px `line`; разделитель «новые» — 1 px mark.
+2 -1
View File
@@ -30,7 +30,7 @@
.author { color:var(--mute); font-size:12px; }
.author.me { color:var(--mark); }
.author .t { color:var(--stone); }
/* поток сообщений: свои — у правого края, чужие — у левого (ADR-065) */
/* поток сообщений: на touch своё справа, на десктопе всё слева (ADR-077) */
.msg { display:flex; flex-direction:column; gap:4px; max-width:min(640px, 85%); }
.msg.me { align-self:flex-end; align-items:flex-end; }
.msg p { margin:0; }
@@ -42,6 +42,7 @@
/* 2a — десктоп */
.desktop { width:1120px; height:700px; display:grid; grid-template-columns:224px 1fr; }
.desktop .msg.me { align-self:flex-start; align-items:flex-start; }
.side { border-right:1px solid var(--line); display:flex; flex-direction:column; }
.brand { height:64px; display:flex; align-items:center; gap:10px; padding:0 20px; border-bottom:1px solid var(--line); font-size:15px; letter-spacing:-0.02em; }
.list { padding:20px; display:flex; flex-direction:column; gap:2px; }
+1 -1
View File
@@ -5,4 +5,4 @@
- Блокировка собеседника и персональные инвайты — если общего инвайт-кода и лимитов (ADR-019, ADR-021) окажется мало.
- Подписи сообщений вторым ключом — если потребуется защита от сговора участника комнаты с сервером (ADR-016).
Закрыто ADR-015…024: регистрация, контакты, лимиты, смена пароля, идентификация устройств, язык интерфейса, айдентика, доверие к ключам, протокол, схема базы, деплой, правила пушей. ADR-064 и ADR-069 — полоса помощника форм над клавиатурой iOS, ADR-065 — сторона сообщений в ленте, ADR-066 — серверный «перец», ADR-067…072 и ADR-073…075 — версия в подвале, самообновление PWA, строка ввода, запрет масштабирования, код причины отказа пушей.
Закрыто ADR-015…024: регистрация, контакты, лимиты, смена пароля, идентификация устройств, язык интерфейса, айдентика, доверие к ключам, протокол, схема базы, деплой, правила пушей. ADR-064 и ADR-069 — попытки убрать системную панель ввода iOS, ADR-065 — сторона сообщений в ленте, ADR-066 — серверный «перец», ADR-067…075 — версия в подвале, самообновление PWA, строка ввода, запрет масштабирования, код причины отказа пушей. ADR-076 фиксирует системное ограничение панели iOS и возврат к `textarea`, ADR-077 — стороны сообщений отдельно для touch и десктопа.
+4 -4
View File
@@ -38,13 +38,13 @@
Шапка: имя (`#general` / `@marta`), по нажатию — участники или карточка контакта. Без темы и «N онлайн».
Лента: поток блоков сверху вниз, одинаковый на десктопе и мобильном; подряд идущие сообщения одного автора — без повтора автора над группой. Свои сообщения выровнены по правому краю ленты, чужие — по левому (ADR-065); свой ник в колонке автора при этом остаётся цветом `mark`. Ширина блока сообщения ограничена — иначе выравнивать нечего, — текст внутри блока выровнен по левому краю независимо от стороны. Разделители дат и «новые» — во всю ширину ленты: линия с датой; «новые» — линия цветом `mark` перед первым непрочитанным, исчезает при следующем открытии чата. Pending — текст цветом `stone`; failed — с пометкой «не отправлено · повторить». Нерасшифрованное — курсивом: «не удалось расшифровать: ключ изменился» / «…: нет ключа комнаты». Время — `ts` в локальной зоне, `ЧЧ:ММ`.
Лента: поток блоков сверху вниз; подряд идущие сообщения одного автора — без повтора автора над группой. На телефоне и планшете свои сообщения выровнены по правому краю, чужие — по левому; на десктопе все сообщения стоят слева (ADR-077). Десктоп определяется точным указателем с наведением, а не шириной окна: широкий планшет остаётся планшетом. Свой ник остаётся цветом `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).
Ввод: рамка 1 px ink, слева `>` цветом `mark`, подсказка «сообщение в #general» / «сообщение»; имя комнаты в подсказке обрезается до 12 символов многоточием — «сообщение в #длинноеимя…» (ADR-061), в шапке оно остаётся полным. Enter — отправить, Shift+Enter — перенос; на мобильном Enter — перенос, отправка — кнопка `>` справа. Подсказка «enter — отправить» только на десктопе. Лимит 4000 — счётчик появляется после 3500; нативный `maxLength` не пускает лишнее. Тап по неинтерактивному месту истории снимает фокус со строки и закрывает клавиатуру (ADR-076).
Строка сообщения — редактируемый блок, а не поле формы: над клавиатурой iOS рисует полосу помощника форм, и бывает она только у `input` и `textarea` (ADR-069). Блок принимает и отдаёт только текст: вставка приходит текстом, значение читается и пишется `textContent`, `innerHTML` не появляется. Подсказку рисует правило CSS для пустого блока. Растёт под содержимое, дальше семи строк — прокрутка. Заблокированный ввод перестаёт быть редактируемым. Остальные строки ввода — вход, регистрация, смена пароля, «новый чат», участники — остаются полями формы: там нужны менеджеры паролей, а полоса на разовом экране не мешает.
Строка сообщения — `textarea` (ADR-076): она принимает только текст, сама держит вставку, выделение, отмену, подсказку, предел и состояние `disabled`. Растёт под содержимое, дальше семи строк — прокрутка. Системную панель со стрелками и «готово» iOS рисует поверх клавиатуры и для `textarea`, и для `contenteditable`; у веб-страницы нет поддерживаемого API, чтобы её скрыть. Полное управление этой панелью потребовало бы отдельной нативной iOS-оболочки; собственная экранная клавиатура в веб-клиенте не делается.
Предупреждение о ключе — полоса над вводом цветом `mark`: «ключ @marta изменился. сверьте отпечаток лично. [доверять новому ключу]». Ввод заблокирован до подтверждения. Полоса одна: предупреждение о ключе перебивает отказ отправки и «нет соединения» (ADR-038).
@@ -118,6 +118,6 @@
Семантика: `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`.
Строка сообщения — нативная `textarea` (ADR-076): роль многострочного поля и состояние `disabled` сообщает сам браузер; доступное имя повторяет текст видимой подсказки через `aria-label`, кнопка `>` объявляется как «отправить».
Масштабирование запрещено — и щипок, и двойной тап (ADR-075). Это сознательный размен: интерфейс ведёт себя как приложение, а человек, которому нужно увеличить мелкое, средства лишается. Размеры шрифтов от запрета не меняются: поля ввода остаются 14 px, как в `docs/identity/brief.md`. Браузер вправе запрет проигнорировать.