# Веб-UI Конвенция: *как* мы пишем код приложения. Это правила оформления кода (How), а не спецификация поведения — что именно приложение показывает и какие действия обязано поддерживать, живёт в спеке OpenSpec. Фреймворк выбран 2026-08-11 разведкой `spa-framework-choice`: [ADR](../adr/ADR-2026-08-11-spa-on-vue.md), сравнение кандидатов в [research/spa-framework.md](../research/spa-framework.md). Прежняя редакция описывала htmx с прямым запретом на шаг сборки и реактивные фреймворки; она снята целиком вместе со сменой решения на SPA 2026-08-10. Правила ниже применены каркасом приложения (`spa-skeleton`, 2026-08-15): до него они были выведены из выбора и из замера на пробном экране, а не из написанного кода. Место, где правило разойдётся с тем, что окажется удобным, — повод править эту запись, а не обходить её молча. Логирование запросов — [logging.md](logging.md). Трансляция доменных ошибок наружу — [errors.md](errors.md). **Механизировано:** типы разметки и кода проверяет `vue-tsc`, и он входит в команду сборки, а не стоит отдельным шагом. Форматирование и статический анализ держит **Biome**, поведение экранов — **юнит-тесты Vue**; оба шага входят в набор проверок наравне со сборкой. Инструменты зовутся контейнером, а не из `PATH`: требованием к машине разработчика остаётся docker, а не установленный Node. ## Что решено про само приложение - **Приложение — SPA**, а не страницы, отрисованные сервером. Сервер отдаёт контракт данных, разметку собирает клиент. - **Приложение ставится на телефон** и запускается с ярлыка: манифест, иконки, service worker. - **Статика вшивается в бинарник** через `go:embed` и раздаётся им же. Внешнего веб-сервера под статику не заводим, бинарник остаётся самодостаточным. - **Шрифты и скрипты — со своего хоста**, без внешних. Внешних ресурсов времени выполнения нет. - **Офлайн-чтения расшифровок и очереди отправки без сети не делаем** — граница из [паспорта](../passport.md). Без сети приложение показывает состояние, а не пустой экран. - **Web Push не делаем**: уведомления идут через apprise и ntfy — решение живёт в [architecture.md](../architecture.md), «Уведомления», делает его [ntfy-delivery](../../tasks/items/ntfy-delivery.md). - **Записи звука в приложении не делаем** — граница из [паспорта](../passport.md), «Диктофон»; файл выбирают системным диалогом. ## Фреймворк и сборка - **Vue 3, TypeScript, сборка Vite.** Серверной отрисовки нет, надстройки над фреймворком (Nuxt) нет: она ждёт рядом процесс Node, а у нас статика в бинарнике. - **Компонент — однофайловый, `