Files
bare/web/js/ui/shell.js
T
mayatnikovandClaude Opus 5 383e374f0f 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
2026-08-23 15:11:13 +03:00

98 lines
4.0 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Каркас: сайдбар со списком чатов и место под экран — docs/ui.md, «Каркас»
// и «Список чатов».
import * as pwa from "../pwa.js";
import { INSTALL_IOS, clear, el, mark } from "./dom.js";
import { mount } from "./chats.js";
// Время коммита — в местной зоне и коротко: день с месяцем и часы
// с минутами (ADR-067). Год не показывается: версия отвечает на вопрос
// «что сейчас работает», а не ведёт летопись.
const BUILD_DAY = new Intl.DateTimeFormat("ru-RU", { day: "2-digit", month: "2-digit" });
const BUILD_TIME = new Intl.DateTimeFormat("ru-RU", { hour: "2-digit", minute: "2-digit" });
// frame отдаёт корень, место под экран и отписку списка чатов.
// screen — что показывать на мобильном, где виден один экран за раз:
// "list" или "screen". active — чат, который сейчас открыт.
export function frame(ctx, screen, active = null) {
const root = el("div", "shell");
root.dataset.screen = screen;
const main = el("main", "main");
const { nav, dispose } = side(ctx, active);
root.append(nav, main);
return { root, main, dispose };
}
function side(ctx, active) {
const nav = el("nav", "side");
const brand = el("div", "brand");
brand.append(mark(), el("span", null, "bare"));
nav.append(brand);
// Баннер установки — над списком чатов (docs/ui.md, «Баннер установки»).
// Место под него занимается сразу, содержимое приезжает из meta.
const place = el("div", "banner-slot");
nav.append(place);
banner(place);
const list = el("div", "list");
const add = el("button", "item item--new", "+ новый чат");
add.type = "button";
add.addEventListener("click", () => ctx.go("#/new"));
const items = el("div");
list.append(add, items);
nav.append(list);
const foot = el("div", "foot");
const me = el("button", "me");
me.type = "button";
const dot = el("i");
dot.setAttribute("aria-hidden", "true");
me.append(dot, el("span", "me__nick", `ты: @${ctx.me.nick}`));
me.addEventListener("click", () => ctx.go("#/settings"));
foot.append(me);
const version = build(ctx.config);
if (version !== null) {
foot.append(el("span", "build", version));
}
nav.append(foot);
return { nav, dispose: mount(items, ctx, active) };
}
// build — версия и время коммита из GET /api/config (ADR-065, ADR-067).
// Конфигурации нет — офлайн-старт до первого ответа сервера — значит,
// и строки нет: выдумывать версию не из чего. Время без версии не бывает:
// её сервер отдаёт всегда, хотя бы как «unknown».
function build(config) {
const version = config?.version;
if (typeof version !== "string" || version === "") {
return null;
}
const at = config?.commitAt;
if (!Number.isFinite(at) || at <= 0) {
return version;
}
return `${version} · ${BUILD_DAY.format(at)} ${BUILD_TIME.format(at)}`;
}
// banner — баннер установки на iOS: пуши там работают только
// у установленного приложения (ADR-011). Крестик закрывает его насовсем.
async function banner(place) {
if (!pwa.iosBrowser() || await pwa.bannerHidden()) {
return;
}
const box = el("div", "banner");
box.append(el("p", "banner__text", INSTALL_IOS));
const close = el("button", "banner__close", "×");
close.type = "button";
close.setAttribute("aria-label", "закрыть");
close.addEventListener("click", () => {
clear(place);
pwa.hideBanner();
});
box.append(close);
place.append(box);
}