iOS: запрет масштабирования, версия в подвале, самообновление, строка ввода

Отзыв с iPhone после первого живого прогона.

Масштабирование запрещено (ADR-066): viewport как в соседнем sixlines плюс
viewport-fit=cover, мета-теги установленного приложения, touch-action против
двойного тапа и страховка на JS — гашение жестов iOS, потому что мета-строку
Safari вправе проигнорировать. Размеры шрифтов не тронуты: 14 px из брифа
остаются, поднимать поля до 16 px владелец отклонил. Цена — доступность
для тех, кому зум нужен — записана в ADR и в ui.md честно.

Версия в подвале (ADR-065, 067): GET /api/config отдаёт короткую ревизию
и время коммита. Именно коммита, а не сборки: штамп компиляции в бинаре
means каждая пересборка даёт новый хеш, и обещание ADR-022 «бинарь собирается
из тега байт в байт» перестаёт проверяться.

Самообновление PWA (ADR-068, 070): в режиме приложения на iPhone перезагрузки
нет, и старая версия жила бы вечно. Проверка при возврате в приложение,
включение ждущей оболочки, перезагрузка — но не тогда, когда в строке набран
текст. Замок от петли сравнивает версию оболочки, а не считает перезагрузки:
счётчик отменял саму фичу, долгоживущая вкладка переставала обновляться.
Замерено: было 30, 60, 120, 240 секунд между релизами, стало ровно 30.

Строка ввода — редактируемый блок (ADR-069): iOS рисует над клавиатурой
полосу со стрелками и «готово» для полей формы, и убрать её со страницы
нельзя. Для contenteditable не рисует. Режим plaintext-only с проверкой,
что он применился, и запасным путём с разбором вставки: невалидное значение
атрибута в части браузеров делает блок нередактируемым вовсе. Чтение
и запись только через textContent, innerHTML в клиенте по-прежнему нет.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015DbCjVfTFq4ZFG8juD45YJ
This commit is contained in:
2026-08-23 15:11:13 +03:00
co-authored by Claude Opus 5
parent cfd0ec0ce9
commit 383e374f0f
31 changed files with 1462 additions and 79 deletions
+126 -15
View File
@@ -25,16 +25,63 @@ body {
height: 100%;
}
/* масштабирования нет (ADR-066). manipulation снимает зум по двойному тапу
и задержку 300 мс, которую браузер держит, ожидая второго касания;
прокрутку и щипок правило не трогает — щипок запрещён в viewport.
Правило стоит на html: двойной тап — жест документа, и браузер берёт
пересечение значений всех предков */
html {
touch-action: manipulation;
overscroll-behavior: none;
}
body {
margin: 0;
/* виден вырез: viewport-fit=cover пускает страницу под скруглённые углы,
под боковой вырез в альбомной ориентации и под верхнюю системную
полосу. Полосу над страницей держит мета-строка о стиле полосы
(ADR-066), поэтому верхняя вставка обычно нулевая и не делает ничего;
там, где она не нулевая, шапка не уезжает под часы */
padding-top: env(safe-area-inset-top);
padding-left: env(safe-area-inset-left);
padding-right: env(safe-area-inset-right);
background: var(--bone);
color: var(--ink);
font-family: var(--mono);
font-size: 14px;
line-height: 1.55;
overscroll-behavior: none;
-webkit-text-size-adjust: 100%;
}
/* интерфейс не выделяется: долгое нажатие по шапке, списку и кнопкам
вызывает лупу и «копировать», а копировать там нечего. Текст сообщений,
отпечатки и поля ввода выделяются как обычно — их именно копируют
(ADR-066) */
.side,
.head,
.brand,
.divider,
.input .p,
.counter,
.enter,
button {
-webkit-user-select: none;
user-select: none;
}
/* прокрутка внутренних лент не уходит в страницу: отскок и «потянуть
для обновления» ощущения приложения не добавляют */
.list,
.body,
.feed,
.auth {
overscroll-behavior: contain;
}
/* #app — колонка ровно в высоту окна: от неё считают высоту экраны,
поэтому сайдбар с «ты: @nick» стоит на месте, а прокручивается
только содержимое */
@@ -360,16 +407,34 @@ input[type="password"] {
color: var(--mute);
}
/* подвал сайдбара: «ты: @nick» и версия с временем коммита (ADR-067).
Версия не кнопка: нажимать в ней нечего, поэтому она стоит рядом
с кнопкой, а не внутри неё. В сайдбаре 224 px они рядом не помещаются
никогда — версия занимает вторую строку и остаётся справа; на мобильном,
где сайдбар во всю ширину, обе стоят в одной строке */
.foot {
flex: none;
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 2px 8px;
margin-top: auto;
/* нижняя системная полоса iPhone проходит поверх страницы: подвал
отступает от неё, а не прячется под ней (ADR-066) */
padding: 6px 20px calc(6px + env(safe-area-inset-bottom));
border-top: 1px solid var(--line);
}
.me {
display: flex;
align-items: center;
gap: 8px;
width: 100%;
flex: 1 1 auto;
min-width: 0;
min-height: 44px;
margin-top: auto;
padding: 16px 20px;
padding: 0;
border: 0;
border-top: 1px solid var(--line);
background: none;
color: var(--mute);
font: inherit;
@@ -385,6 +450,30 @@ input[type="password"] {
background: var(--ink);
}
/* ник бывает в 32 символа (ADR-019): он обрезается, а версия остаётся
целой — читать половину хеша незачем */
.me__nick {
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* версия выделяется: с неё начинается разговор о поломке (ADR-067),
а на iPhone скопировать её иначе нечем — ни строки адреса, ни консоли
там нет. Общий запрет выделения снят с неё так же, как с текста
сообщений и отпечатков (ADR-066) */
.build {
flex: none;
margin-left: auto;
color: var(--stone);
font-size: 11px;
-webkit-user-select: text;
user-select: text;
}
.main {
flex: 1;
min-width: 0;
@@ -766,7 +855,8 @@ input[type="password"] {
.compose {
flex: none;
padding: 0 16px 20px;
/* то же, что у подвала: строка ввода не уезжает под системную полосу */
padding: 0 16px calc(20px + env(safe-area-inset-bottom));
}
.input {
@@ -799,7 +889,10 @@ input[type="password"] {
flex: 1;
min-width: 0;
min-height: 0;
padding: 12px 0;
/* отступ назван: высота блока считается вместе с ним (box-sizing —
border-box), и предел роста строки сообщения меряется от него */
--field-pad: 12px;
padding: var(--field-pad) 0;
border: 0;
border-radius: 0;
background: none;
@@ -809,15 +902,26 @@ input[type="password"] {
line-height: 1.5;
}
/* строка ввода перебивает общее правило для input: рамка здесь одна,
у всей строки */
/* строка сообщения — редактируемый блок, а не поле формы (ADR-069):
растёт под текст сама, дальше семи строк — прокрутка. Переносы строк
в ней настоящие символы, поэтому pre-wrap; длинное слово рвётся,
иначе строка ввода уезжает вбок */
.input textarea.input__field {
resize: none;
.input .input__field--text {
overflow-y: auto;
max-height: 7em;
/* растёт под текст там, где браузер это умеет; где нет — прокрутка */
field-sizing: content;
/* семь строк — это семь высот строки плюс вертикальные отступы:
без них предел вышел бы вдвое ниже обещанного */
max-height: calc(7 * 1.5em + 2 * var(--field-pad));
white-space: pre-wrap;
overflow-wrap: anywhere;
}
/* подсказка пустой строки: placeholder у блока не бывает, текст приходит
атрибутом data-hint (ADR-069) */
.input .input__field--text:empty::before {
content: attr(data-hint);
color: var(--stone);
}
.input .input__field::placeholder {
@@ -942,7 +1046,7 @@ input[type="password"] {
}
.compose {
padding: 0 32px 28px;
padding: 0 32px calc(28px + env(safe-area-inset-bottom));
}
.input {
@@ -951,7 +1055,7 @@ input[type="password"] {
}
.input .input__field {
padding: 13px 0;
--field-pad: 13px;
}
/* на десктопе отправляет enter — кнопка не нужна ни в чате, ни в
@@ -993,6 +1097,13 @@ input[type="password"] {
display: none;
}
/* строка сообщения — тоже цель нажатия не меньше 44 px (docs/ui.md,
«Доступность»): своей высоты у блока нет, её задаёт содержимое */
.input .input__field--text {
min-height: 44px;
}
/* автор стоит над группой, поэтому пустая ячейка автора не занимает
места; на десктопе она держит колонку сетки и остаётся */