added Docs/UIKIT.md uikit_clear.md tz.md tz_clear.md
Build Unsigned iOS ipa / Build Unsigned iOS IPA (push) Canceled after 0s
Build Unsigned macOS App / Build macOS ZIP (push) Canceled after 0s
iOS CI / Build and Test SwiftUI App (push) Canceled after 0s

This commit is contained in:
wt committed 2026-10-14 17:56:31 +07:00
1 parent 6969863d1e
commit e018be346a
5 files changed
+4179

No files matched your search

+614
View File
@@ -0,0 +1,614 @@
# Luma UI Kit — стандарт интерфейса
Версия документа: 1.0. Дата фиксации: 14 октября 2026 года.
Документ фиксирует существующий дизайн Luma для повторного использования в новых экранах и при сопровождении. Здесь UI Kit означает библиотеку визуальных правил и компонентов продукта; приложение написано преимущественно на SwiftUI, а не на одноимённом UIKit framework.
Основание: текущие `Sources/App`, `Sources/Shared/UI`, UI policies в `Sources/Shared/Models`, `Sources/Watch/WatchViews.swift`, ресурсы цвета и `project.yml`. Исходный коммит: `6969863d1e4db434004f8e5e032ada270a4b948f`. Документ составлен по коду; screenshots и фактический рендеринг на устройствах в этом сеансе не получены. Системные материалы, SF Symbols и размеры semantic fonts зависят от платформы, версии ОС, настроек и accessibility.
Функциональное поведение подробнее описано в [обратном ТЗ](../tz.md). При расхождении старого описания с этим документом для текущего дизайна приоритет имеет исполняемый компонент, на который дана ссылка.
## Содержание
- [1. Правила применения](#1-правила-применения)
- [2. Визуальный характер](#2-визуальный-характер)
- [3. Палитра](#3-палитра)
- [4. Типографика](#4-типографика)
- [5. Геометрия и поверхности](#5-геометрия-и-поверхности)
- [6. Иконки](#6-иконки)
- [7. Каркас экранов](#7-каркас-экранов)
- [8. Avatar](#8-avatar)
- [9. ConversationRow и контакт](#9-conversationrow-и-контакт)
- [10. Поиск](#10-поиск)
- [11. Лента чата](#11-лента-чата)
- [12. Message bubble](#12-message-bubble)
- [13. Replies и reactions](#13-replies-и-reactions)
- [14. Composer](#14-composer)
- [15. Медиа](#15-медиа)
- [16. Call UI](#16-call-ui)
- [17. Формы и системные листы](#17-формы-и-системные-листы)
- [18. Feedback и overlays](#18-feedback-и-overlays)
- [19. Анимации и жесты](#19-анимации-и-жесты)
- [20. Watch UI](#20-watch-ui)
- [21. Состояния компонента](#21-состояния-компонента)
- [22. Текст и доступность](#22-текст-и-доступность)
- [23. Известные неоднородности](#23-известные-неоднородности)
- [24. Порядок добавления нового UI](#24-порядок-добавления-нового-ui)
- [25. Каталог компонентов и источников](#25-каталог-компонентов-и-источников)
## 1. Правила применения
- Новые экраны продолжают существующий визуальный язык: системные навигация и формы, синий accent, полупрозрачные поверхности, скруглённые элементы, SF Symbols.
- Сначала переиспользуется имеющийся компонент, затем существующий вариант, и только затем создаётся новый.
- Числа в таблицах — значения SwiftUI в points, если не указана другая единица. RGB — нормализованные значения 0…1.
- Semantic colors и font styles сохраняются как semantic, без подмены случайными HEX и фиксированным размером.
- Имена токенов в этом документе — документальные обозначения. Единого исполняемого `LumaTheme`/`DesignTokens` в проекте сейчас нет.
- «Стандарт» — правило для будущих экранов на основе текущих компонентов. «Факт/исключение» — существующая неоднородность, которая не должна автоматически распространяться на новые компоненты.
- Изменение документа само по себе не изменяет UI. Унификация разных текущих значений требует отдельного изменения приложения.
## 2. Визуальный характер
Luma сочетает нативную Apple-навигацию, Telegram-подобные списки/компоновку чата и Messages-подобную подачу ответов/анимаций. Основной контент спокойный: имя, сообщение и действие важнее декоративной рамки.
| Принцип | Применение |
| --- | --- |
| Нативность | TabView, NavigationStack, NavigationSplitView, Form, List, системные sheets/alerts/pickers |
| Акцент | Синий для действий и выбора; сине-голубой градиент исходящих |
| Полупрозрачность | Glass/material для управления и вторичных поверхностей |
| Иерархия | Primary → secondary → caption; текст не дублируется крупными декоративными заголовками |
| Разделение | Отступы и скруглённые поверхности; divider там, где меняется зона работы |
| Состояние | Иконка, текст, progress и доступность действия; цвет дополняет их |
| Контекст | Действия над сообщением — context menu, массовые — selection mode |
| Адаптация | Общие роли компонентов при разной навигации iOS/Mac/Watch |
## 3. Палитра
### 3.1. Основные цвета
| Документальный токен | Значение | Источник / использование |
| --- | --- | --- |
| `accent.root` | RGB 0.14, 0.56, 0.96 ≈ #248FF5 | `LumaApp`: `.tint(...)` |
| `accent.asset` | RGB 0.145, 0.565, 0.965 ≈ #2590F6 | `Resources/Shared.xcassets/AccentColor.colorset` |
| `text.primary` | `.primary` | Основной текст на адаптивных светлых поверхностях |
| `text.secondary` | `.secondary` | Подписи, время, вторичная информация |
| `text.onOutgoing` | `.white` | Текст исходящего bubble |
| `surface.incoming` | `.secondary.opacity(0.13)` | Входящий bubble |
| `surface.timeline` | `.secondary.opacity(0.035)` | Фон ChatView |
| `surface.photoPlaceholder` | `.secondary.opacity(0.12)` | Фотоостровок/placeholder |
| `border.media` | `.secondary.opacity(0.18)` | Рамка фото/видео, 1 pt |
| `state.success` | `.green` | Online, verified, completed call |
| `state.warning` | `.orange` | Plaintext и предупреждение |
| `state.error` | `.red` | Ошибка, destructive, неудачный звонок |
| `overlay.dark` | `.black` с локальной opacity | Viewer, подписи на media, звонок |
**Различие accent:** `.tint` на уровне корня и `Color.accentColor` из assets не являются одним числом. В новых компонентах переиспользовать `.tint`/`Color.accentColor` по роли, не добавлять третий близкий синий. Не считать таблицу результатом уже выполненной унификации.
### 3.2. Градиенты
Все перечисленные линейные градиенты направлены topLeading → bottomTrailing, кроме явно заданного источником другого направления.
| Название | RGB stops | Применение |
| --- | --- | --- |
| `gradient.auth` | (0.08, 0.38, 0.78) → (0.12, 0.62, 0.96) → (0.44, 0.84, 0.96) | LoginView, AppLockView |
| `gradient.outgoing` | (0.13, 0.51, 0.94) → (0.12, 0.63, 0.94) | Исходящее сообщение |
| `gradient.videoPlaceholder` | (0.06, 0.17, 0.29) → (0.09, 0.43, 0.64) | Видеоостровок |
| `gradient.videoNote` | (0.08, 0.30, 0.56) → (0.07, 0.64, 0.83) | Круглое видеосообщение |
| `gradient.call` | (0.11, 0.17, 0.24) → black | Экран звонка без remote video |
| `gradient.avatar` | baseColor → baseColor.opacity(0.68) | Fallback аватара |
### 3.3. Детерминированные цвета участников
Avatar fallback: hue = `colorSeed % 360 / 360`, saturation 0.68, brightness 0.88. Sender name в группе: отдельный hash JID, saturation 0.72, brightness 0.78. Цвет участника стабилен для идентичности, но avatar color и sender color не обязаны совпадать.
### 3.4. Тёмная тема
Большинство основных поверхностей использует semantic colors/materials и наследует системный color scheme. Отдельного переключателя темы или самостоятельной полной dark palette в текущей реализации нет. Auth gradients, белый текст исходящих и тёмный call/viewer сохраняют собственные цвета.
Для новых компонентов нужны light/dark проверки; документ не утверждает уже проведённый аудит контраста.
## 4. Типографика
Основной шрифт — системный SwiftUI. Название конкретного установленного семейства не закрепляется как файл шрифта.
| Роль | Стиль | Пример |
| --- | --- | --- |
| Имя чата/контакта | `.headline` | ConversationRow |
| Превью последнего сообщения | `.subheadline` | ConversationRow |
| Текст сообщения | унаследованный стандартный Text font | MessageBubble |
| Название call card | `.subheadline.weight(.semibold)` | MessageBubble |
| Подпись call card | `.caption`, opacity 0.78 | Длительность/результат |
| Время списка | `.caption`, secondary | ConversationRow |
| Метаданные bubble | `.caption2`, secondary | Время, delivery, «изм.» |
| Имя автора MUC | `.caption.weight(.semibold)` | Цветной sender name |
| Badge unread | `.caption2.bold()` | Белый текст capsule |
| Compact reply | `.callout.weight(.medium)` | Однострочная цитата |
| Пояснение формы | `.caption` / `.footnote` по экрану | Login, настройки, группа |
| Fingerprint | `.system(.caption, design: .monospaced)` | Шифрование |
| Таймер/длительность | `.monospacedDigit()` | Запись, player, звонок |
| Бренд login | system 42, bold, rounded | «Luma» |
| Имя в active call | system 30, semibold, rounded | До двух строк |
| Lock heading | `.title2.weight(.bold)` | «Luma заблокирован» |
Не превращать `.headline` в произвольные «17 px» в переносимом стандарте: semantic style может меняться платформой и Dynamic Type. Фиксированные sizes из таблицы применяются именно к указанным special components.
Обрезка: имя/preview списка — одна строка; имя звонящего — две; toast — две; compact reply — одна. Основной message body не обрезается правилом preview.
## 5. Геометрия и поверхности
### 5.1. Повторяющиеся интервалы
В реализации встречаются интервалы 3/4/5/6/7/8/10/12/13/14/16/18/22/24/28. Это не строго реализованная сетка кратности четырём. При повторении существующего компонента использовать его реальные значения.
| Роль | Значение |
| --- | --- |
| Между avatar и текстом списка | 12 |
| Между строками текста списка | 5 |
| Horizontal row insets списка чатов | 12 |
| Vertical listRowInsets чатов | 6 |
| Дополнительный vertical padding ConversationRow | 2 |
| Horizontal padding message row | 12 |
| Text/file bubble внутренний padding | horizontal 13, vertical 9 |
| Audio/voice bubble padding | horizontal 13, vertical 10 |
| Composer основной gap | 8 |
| Composer horizontal padding | 10 |
| Composer bottom / top | 8 / 6 |
| Input leading / trailing / vertical | 14 / 12 / 9 |
| Auth screen horizontal / vertical padding | 24 / 48 |
### 5.2. Формы
| Роль | Форма |
| --- | --- |
| Avatar, online dot, call control | Circle |
| Unread badge, media caption, toast, reactions | Capsule |
| Auth/lock card | RoundedRectangle continuous, radius 28 |
| Composer input | Glass rect, radius 21 |
| Photo/video island | RoundedRectangle continuous, radius 20 |
| Compact reply | RoundedRectangle continuous, radius 17 |
| Reply thumbnail | RoundedRectangle continuous, radius 6 |
| Attachment caption field | RoundedRectangle, radius 16 |
| Watch message | RoundedRectangle, radius 11 |
| Call local-video preview | RoundedRectangle continuous, radius 22 |
| Основной message bubble | Собственный ChatBubbleShape |
**Фактическая форма bubble:** `MessageBubble.messageBubbleShape` возвращает `ChatBubbleShape(isOutgoing:)`. Его path содержит нижний хвостик/асимметрию на стороне сообщения и опорные скругления около 20 pt. Не заменять его обычным RoundedRectangle при реализации «такого же» текстового bubble. Старые формулировки README/tz об отсутствии хвостиков расходятся с этим исполняемым shape. Фото/видео имеют независимую островковую рамку.
### 5.3. Материалы
- `.glassEffect()` — search field общего компонента, login inputs, composer controls, часть новых/платформенных действий.
- `.glassEffect(in: .rect(cornerRadius: 21))` — composer input.
- `.regularMaterial` — login/lock card и toast.
- `.ultraThinMaterial` — selection panel, нижняя панель предпросмотра вложений и другие явно заданные контекстом поверхности.
- `.buttonStyle(.glassProminent)` — основное действие login и ряд Mac forms.
- `.buttonStyle(.borderedProminent)` — часть системных forms, lock, attachment send, Watch.
- `.buttonStyle(.plain)` — управление внутри списка/bubble/player, где оформление рисует компонент.
Это несколько существующих контекстных вариантов, не требование заменить каждую кнопку на glass. Внешний вид материала не закрепляется статическим цветом.
### 5.4. Тени и границы
| Компонент | Значение |
| --- | --- |
| Login card | black opacity 0.14, radius 24, y 12 |
| Login logo tile | black opacity 0.16, radius 22, y 10 |
| Toast | black opacity 0.18, radius 12, y 4; border primary opacity 0.06 |
| Avatar звонящего | black opacity 0.32, radius 28, y 14; white border opacity 0.14, 1 pt |
| Local video | black opacity 0.38, radius 16, y 8; white border opacity 0.24, 1 pt |
| Media island | secondary border opacity 0.18, 1 pt |
| Reaction | 0.8 pt border; цвет зависит от own state |
На обычную строку чата не добавляется карточная тень. Использовать тень как отделение overlay/крупного элемента, а не универсальный декор.
## 6. Иконки
Основной набор — SF Symbols. Размер и weight задаются по компоненту; пользовательское действие имеет accessibility label, если состоит только из icon.
| Роль | Symbol |
| --- | --- |
| Контакты / звонки / чаты / настройки | `person.circle.fill` / `phone.fill` / `bubble.left.and.bubble.right.fill` / `gearshape.fill` |
| Поиск / очистить | `magnifyingglass` / `xmark.circle.fill` |
| Новый личный / групповой | `square.and.pencil` / `person.3.fill` |
| Вложение | Знак `+` в composer; набор файлов — `doc.on.doc` |
| Отправка | `arrow.up`; в preview — `arrow.up.circle.fill` |
| Emoji / voice / videoNote | `face.smiling` / `mic.fill` / `video.circle` |
| Location | `location.fill` |
| Ответ / пересылка | `arrowshape.turn.up.left` / `arrowshape.turn.up.right` |
| Редактирование / сохранить файл | `pencil` / `square.and.arrow.down` |
| Удалить / retract | `trash` / `trash.slash` |
| OMEMO / plaintext / decrypt error | `lock.fill` или `lock.shield.fill` по контексту / `lock.open` / `exclamationmark.shield` |
| Play / pause | `play.fill` / `pause.fill` |
| Раскрыть media | `arrow.up.left.and.arrow.down.right` |
| Звонок завершить | `phone.down.fill` |
| Switch camera | `camera.rotate.fill` |
| Toast success | `checkmark.circle.fill` |
Не заменять SF Symbols emoji в функциональных icon buttons. Emoji допустимы как пользовательские реакции и текстовый preview типов сообщений.
## 7. Каркас экранов
### 7.1. iOS/iPadOS
Native TabView: **Контакты → Звонки → Чаты → Настройки**. Начальный выбор — Чаты. Иконки selected/unselected сейчас одинаковые по SF name; выделение даёт системный tab UI.
Каждый сценарий использует NavigationStack соответствующего контекста. В pushed ChatView tab bar скрывается и восстанавливается при возврате. Верхний поиск списка остаётся над строками; `.searchable` и общий SearchField — существующие разные реализации, их итоговое расположение проверяется UI tests.
iPhone: portrait и обе landscape. iPad: все четыре orientation. Отдельная фиксированная desktop-sidebar ширина не переносится автоматически на iPad.
### 7.2. macOS
NavigationSplitView: sidebar + detail. Sidebar width min 240, ideal 290, max 380. Минимальный общий размер 840 × 560, default app window 1080 × 720.
Слева переключение режимов, поиск и список. Справа выбранный чат/контакт/звонок либо соответствующий empty state, настройки в своём контексте. Режимы: Чаты, Контакты, Звонки, Настройки. Новые chat/group actions — toolbar с `.help`.
Используются нативные SavePanel, QuickLook и AppKit интеграции; iPhone fullscreen не следует буквально имитировать отдельным окном Mac.
### 7.3. Watch
NavigationStack → List чатов → List истории/ответов. Нет нижней панели четырёх вкладок. Компактные semantic fonts и системные кнопки, самостоятельные упрощённые message bubbles. Не переносить размеры 268 pt media cards на экран часов.
## 8. Avatar
Источник: `Sources/Shared/UI/Components/AvatarView.swift`.
| Параметр | Значение |
| --- | --- |
| Default size | 50 × 50 |
| В профиле settings | 64 × 64 |
| В active call | 132 × 132 |
| Image | scaledToFill, clip Circle |
| Инициалы | size ×0.35, semibold, rounded, white |
| Group fallback icon | `person.3.fill`, size ×0.31, semibold, white |
| Online/joined dot | size ×0.25, green, bottomTrailing |
| Dot border | background, 2.5 pt |
Для direct dot означает online, для group — joined. Он не обозначает новое сообщение. Fallback color детерминирован по JID. Accessibility label включает имя и статус.
## 9. ConversationRow и контакт
Источник: `ConversationRow.swift`, `MainTabView.swift`.
Композиция: avatar слева, две строки текста, справа время сверху и unread снизу. Верхняя строка — имя headline и время caption secondary. Нижняя — group/encryption indicator, preview subheadline secondary, optional group count и unread badge.
- Preview заменяется JID, если lastMessage пуст.
- Время не показывается для distantPast.
- Direct lock icon показывается при непустом lastMessage; plaintext icon orange.
- Unread badge только при count >0; текст `min(count,99)`, именно «99», без «99+».
- Badge horizontal padding 7, vertical 3, white text, tint capsule.
- Имя и preview lineLimit 1.
- При нескольких аккаунтах подпись account JID caption2 secondary, leading padding 52.
- Contact rows используют имя, JID/статус и group subtitle; не вся строка contact обязана содержать preview последнего сообщения.
Пустые списки используют ContentUnavailableView с русским заголовком, SF Symbol и пояснением. Search-empty и data-empty различаются текстом.
## 10. Поиск
Общий SearchField:
- HStack gap 6;
- magnifyingglass и clear icon — 13 pt;
- input plain, autocorrection off, iOS autocapitalization never;
- clear button только при непустом вводе;
- внутренние отступы horizontal 10, vertical 7;
- glassEffect;
- внешние horizontal 12, vertical 6.
Существующие `.searchable` поля сохраняют нативную геометрию. Документ не предписывает эти padding нативному SearchField ОС. Главное правило размещения — над списком и в его контексте.
## 11. Лента чата
Лента занимает доступную ширину; incoming слева, outgoing справа. Message row HStack gap 8, horizontal padding 12. Противоположный spacer: минимум 54 для немедийных сообщений и 16 для media.
Внутри message stack gap 4. Group incoming имеет имя автора над содержимым с horizontal padding 8. Метаданные и reactions располагаются рядом с сообщением в его выравнивании, не в отдельной колонке списка.
Outer vertical padding MessageBubble: 7 для photo/video, 2 для остальных. Photo/video компонент добавляет собственные 5 vertical; не заменять эти два уровня одним случайным gap.
Даты показаны разделителями с локализованной датой. Фон timeline — secondary opacity 0.035. Подгрузка сверху должна сохранять позицию чтения. Кнопка вниз — 46 ×46, trailing 14, bottom 12.
## 12. Message bubble
### 12.1. Текст и файл
Incoming: semantic primary foreground + secondary opacity 0.13 background. Outgoing: white foreground + синий градиент. Text/file padding 13×9; shape — ChatBubbleShape. Audio/voice используют тот же background/shape с padding 13×10.
Обычный body — Text, на Mac доступно textSelection. Файл — кнопка с mediaContent и системной иконкой. System не означает всегда одинаковый серый центрированный label: call card и tombstone имеют свои варианты.
### 12.2. Metadata
HStack gap 4, font caption2, secondary, horizontal padding 4. Содержит security warning при необходимости, «изм.», время hour/minute и для outgoing delivery icon/text. У call card delivery indicator не показывается.
| Delivery | Icon | Текущий текст |
| --- | --- | --- |
| sending | `clock` | `Sending...` |
| sent | `checkmark` | `Sended` |
| delivered | `checkmark.circle.dotted` | `Delivered` |
| failed | `exclamationmark.circle.fill`, red | `Error` |
| read | `checkmark.circle.fill` | `Read` |
Это зафиксированные исключения русской локализации, включая опечатку `Sended`. Для новых строк стандарт — русский; автоматическая замена перечисленных строк этим документом не выполнена.
### 12.3. Удалённое сообщение
`trash.slash` + «Сообщение удалено», subheadline italic, gap 7, padding 13×9, прежнее directional background/shape. Outgoing foreground white opacity 0.86, incoming secondary. Реакции, edit и прежние вложения не должны оставаться видимыми.
### 12.4. Context menu и selection
Меню включает только доступные для конкретного сообщения действия: реакция, ответ, пересылка, копирование, сохранение, повтор, редактирование, выбор, удаление у меня/у всех. Destructive actions отмечены role destructive и требуют предусмотренного сценарием подтверждения.
Selection mode показывает count и панель массовых действий, отключает swipe-reply и обычное context menu. Accessibility сообщает selected/not selected. Нельзя рисовать активную кнопку действия, которое запрещено модельным predicate.
## 13. Replies и reactions
### 13.1. Compact reply
- весь reference opacity 0.56;
- ширина max 230, высота min 34;
- callout medium, lineLimit 1;
- radius 17, vertical padding 7;
- leading/trailing 10/12 по направлению;
- background primary opacity 0.018;
- border replyColor opacity 0.75, width 1.25;
- optional thumbnail 27×27, radius 6;
- дуга 58×34, stroke 2.4, round caps, offset ±18;
- направление отражает исходное сообщение, а не только новый ответ.
При наличии target нажатие открывает фокус ветки; legacy preview без id выглядит цитатой, но не притворяется существующей ссылкой. ReplyThreadOverlay — отдельный затемнённый focus context с root/ответами и bracket; не является новым экраном полноценного канала.
### 13.2. Reaction chip
Strip gap 5. Chip: emoji + count, внутренний gap 3, padding horizontal 8/vertical 4, Capsule. Count caption2 semibold.
| Состояние | Background | Border |
| --- | --- | --- |
| Есть own reaction | accent opacity 0.20 | accent opacity 0.55, 0.8 pt |
| Только чужие | secondary opacity 0.12 | secondary opacity 0.16, 0.8 pt |
Тап по chip переключает собственную реакцию. Quick choices: 👍 ❤️ 😂 😮 😢 🔥.
### 13.3. EmojiPicker
Заголовок «Выберите эмодзи», кнопка «Готово», grid и горизонтальные category icons снизу. Adaptive grid min 44, column gap 4, row gap 6; emoji font 28, cell 46×46. Selected category — accent background opacity 0.15, radius 10, icon 18 medium, button 44×44. Minimum height 360, macOS min width 380. Текстового поиска сейчас нет.
## 14. Composer
Основная горизонталь: attachment button → input → send/record. HStack выровнен bottom. Composer прозрачен; input/control имеют собственную glass surface.
Input: min height 42, radius 21, padding leading 14/trailing 12/vertical 9. TextField vertical axis, 1…6 строк. Placeholder «Сообщение» либо «Изменить сообщение». Emoji icon 20 regular, secondary.
Send: 42×42, arrow.up 18 bold, accent при доступности, secondary при disabled, glassEffect. При пустом draft вместо send — record control. Attachment control также 42×42.
Reply/edit banner располагается внутри input над полем, с цветной вертикалью и явной отменой. Selection mode заменяет обычную зону ввода панелью выбранных сообщений. Во время capture вместо поля показываются recording strip/timer/cancel/lock hints.
Для нового composer-действия: сохранять текущую высоту соседних controls, учитывать multiline expansion и не перекрывать safe area/клавиатуру.
## 15. Медиа
### 15.1. Фото/видеоостровок
| Параметр | Значение |
| --- | --- |
| Width | 268 |
| Photo height | natural по ratio, clamp 164…328 |
| Photo ratio clamp | 0.45…2.2, fallback 4:3 |
| Внутренний inset thumbnail | 6 |
| Radius / border | 20 / 1 pt secondary opacity 0.18 |
| Thumbnail mode | scaledToFit |
| Верхний expand icon | caption bold, padding 7, black opacity 0.42 circle |
| Нижняя подпись | caption2 semibold, white, padding 8×5, black opacity 0.48 capsule |
| Overlay inset | 9 |
Видео использует аналогичную рамку и свой расчёт islandHeight по thumbnail. Центральный play 22 bold, padding 15, black opacity 0.38. Loading заменяет/сопровождает соответствующий control. Не растягивать bitmap с нарушением ratio и не заменять fit на crop для обычного media island.
### 15.2. Voice/audio
Player content width 252. Play/pause 44×44, symbol 18 bold. Voice waveform height 30; seek track — 3 pt с knob 9×9 в соответствующем компоненте. Duration caption2 monospacedDigit. Speed chip caption bold, padding 6×3, accent opacity 0.14. Скорости 1×/1.5×/2×. Audio filename subheadline semibold.
Цвета управления учитывают incoming/outgoing: на синем bubble белые/полупрозрачные элементы, на incoming semantic/accent. Player должен показывать loading, paused, playing и progress.
### 15.3. VideoNote
Circle diameter 144 в покое и 176 при playback. Play symbol 24 bold с padding 17 и black opacity 0.34. Duration badge caption2 monospacedDigit semibold, padding 7×3, black opacity 0.48; container inset 12. Border white opacity 0.18, 1 pt.
Увеличение — spring response 0.32, damping 0.82. После завершения/исчезновения/запуска другого exclusive media возвращается к stopped presentation.
### 15.4. Attachment preview перед отправкой
NavigationStack, title «Перед отправкой», scroll grid, divider, нижняя material panel. Grid adaptive min 128/max 190, column gap 18, row gap 22, outer padding 16. Нижняя панель gap 10/padding 14, ultraThinMaterial.
Caption 1…4 строки, padding 13×10, secondary opacity 0.10, radius 16. Кнопка «Отправить»/«Отправка…», borderedProminent, disabled при пустом наборе/отправке. Неудача — красная caption с retry icon; failed drafts остаются видимыми. На Mac minimum 560×560.
### 15.5. Fullscreen viewer
MediaViewer поверх всего основного окна. Тёмный фон, исходное фото с zoom/pan либо системное видео. Свайп вертикально закрывает; увеличенное фото продолжает pan. Отдельный обязательный крестик не является текущим паттерном. Для обычных файлов используется системный QuickLook, не imitation media viewer.
## 16. Call UI
Full-window тёмная сцена, white foreground, крупное имя, статус и controls снизу. Remote video занимает фон; локальное — верхняя правая карточка 118×168, radius 22, top 92/trailing 18.
Без remote video: тёмный градиент, декоративный accent circle 440, opacity 0.24. Avatar 132, имя 30 semibold rounded, два ряда максимум. Подпись типа звонка subheadline white opacity 0.66; статус subheadline monospacedDigit white opacity 0.72.
Call control: круг 58×58, symbol 24 semibold, caption ниже. Incoming: decline red и answer green, horizontal gap 52. Active controls: полупрозрачные белые круги opacity 0.18/0.34 по состоянию, mic/camera/speaker/rotate; hangup red отдельно. Speaker control платформенно iOS.
History card в чате: icon circle 42, направление badge, content gap 12, title subheadline semibold, subtitle caption opacity 0.78, padding 14×10, max width 300. Completed accent green; declined/missed/unanswered/failed red; cancelled/answeredElsewhere secondary. Outgoing foreground white.
## 17. Формы и системные листы
### 17.1. Login
Полноэкранный auth gradient; контент max width 460, horizontal padding 24, vertical 48. Brand block: белая tile 88×88, radius 26, symbol 39 semibold, название 42 bold rounded.
Карточка regularMaterial, radius 28, padding 26, main spacing 18. Поля и дополнительный DisclosureGroup. Main action glassProminent, large control size, inner content height 34; spinner при подключении. Кнопка видимости пароля имеет label и не заменяет SecureField молча.
### 17.2. Settings / new chat / group / encryption
Переиспользуются Form/Section, Toggle switch, NavigationLink и нативная toolbar. Заголовки и объяснения по-русски. Main/cancel actions в стандартных toolbar placements на iOS; на Mac формы могут иметь нижнюю action row.
Ключевые settings sections: аккаунты, профиль, аккаунт, соединение, шифрование, уведомления, блокировка, приватность чата. Profile avatar 64; fingerprint monospaced caption selectable. Device trust: verified green, trusted blue, undecided orange, compromised red; protocol badge различает OMEMO 2 и legacy.
### 17.3. App lock
Auth gradient + material card radius 28/padding 28, outer padding 24; vertical spacing 22. Lock symbol 44 semibold, title2 bold white, password field max width 260, centered, roundedBorder. Кнопка «Разблокировать» borderedProminent, inner content max width 260/height 34. Error «Неверный пароль» caption medium white opacity 0.95. Биометрия — отдельная подписанная кнопка.
Lock должен скрывать underlying chat/media/call content, а не только накладывать полупрозрачный scrim на читаемую переписку.
## 18. Feedback и overlays
| Вид | Паттерн |
| --- | --- |
| Ошибка | System alert «Luma», сообщение, OK |
| Подтверждение destructive | Alert/confirmation по контексту, destructive button, Отмена |
| Короткий success | Toast capsule regularMaterial |
| Долгая операция | ProgressView и контекстный label, disabled controls |
| Empty state | ContentUnavailableView с SF Symbol и пояснением |
| Connection status | ConnectionBanner в контексте заголовка |
Toast: gap 8, subheadline medium, horizontal padding 16/vertical 10, top 8, максимум две строки, auto dismiss 2.5 с, checkmark.circle.fill accent.
ConnectionBanner фактически использует caption white и mini white ProgressView в состояниях «Подключение», «Ожидание сети», «Обновление». В обычном ready state — переданный заголовок headline. Контраст этого white banner на любой возможной светлой поверхности не подтверждён; не считать его универсальной готовой плашкой для любого фона.
Root layering: main → media viewer z100 → call z200 → toast z300. Lock выбирается вместо основного набора слоёв. QuickLook — отдельная системная презентация.
## 19. Анимации и жесты
| Эффект | Значение |
| --- | --- |
| Открытие timeline | Последние 12 rows; stagger 35 мс; rise 16; scale 0.94 |
| Lifetime entrance | 0.75 с |
| Outgoing launch | scale 0.2, rise 12 |
| Incoming launch | scale 0.4, rise 10 |
| Append spring | response 0.44, damping 0.68 |
| History burst | Более 4 appended rows не анимируются как отдельные fresh sends |
| Root account change | easeInOut 0.22 с |
| Root media/call | easeInOut 0.2 с |
| Toast | spring response 0.3, damping 0.85 |
| Composer capture switch | easeInOut 0.18 с |
| Reply swipe return | spring response 0.24, damping 0.82 |
Reply swipe: горизонтальный trigger 64; activation 24; maximum visual offset 76; lock dominance 1.6/follow 1.05. Gesture starts на bubble, не на пустом пространстве/metadata. При достижении порога — light haptic на iOS. Вертикальная прокрутка должна оставаться приоритетной.
Media dismiss: vertical минимум 8, dominance 1.15; distance clamp 88…140 в зависимости от 14% height либо predicted endpoint 1.35×threshold. Не смешивать этот gesture с pan zoomed photo.
Record: long press около 0.22–0.24 с, cancel left 92, lock up 78. UI отражает recording/finalizing и не обещает отправку до завершения recorder.
Анимационные значения отражают код; независимый тест Reduce Motion не выполнен. Для новых анимаций требуется проверить accessibility settings и отсутствие повторной анимации при archive updates.
## 20. Watch UI
| Компонент | Стандарт |
| --- | --- |
| Chat list | Native List, имя headline, preview caption secondary, one line |
| Unread | caption2 bold, padding 5, blue Circle, исходное число |
| Message | padding 7, radius 11, outgoing blue, incoming secondary opacity 0.25 |
| Time | caption2, opacity 0.7 |
| Message spacing | inner VStack 3, opposite spacer min 16 |
| Reply | TextField «Ответ», кнопка «Отправить», disabled при пустом тексте |
| Quick replies | «Хорошо», «Спасибо!», «Отвечу позже» |
| Voice idle | prominent red «Записать голосовое» |
| Voice recording | red timer/progress, «Отмена» и «Готово» |
| Voice preview | play/pause, duration, «Отправить голосовое», destructive «Удалить запись» |
| Transfer status | caption2 secondary label с iPhone symbol |
Watch outgoing использует Color.blue, не gradient.outgoing телефона. Это намеренно зафиксированный платформенный вариант. На Watch unread сейчас не ограничивается 99, в отличие от ConversationRow.
## 21. Состояния компонента
Для каждого нового интерактивного компонента определять применимые состояния:
| Состояние | Отображение / действие |
| --- | --- |
| Default | Обычная поверхность и основное содержимое |
| Pressed | Нативный button style либо существующая feedback-анимация |
| Disabled | `.disabled`, secondary/системное приглушение; действие недоступно |
| Loading | ProgressView вместо/рядом с control, без ложного success |
| Empty | Объяснение и применимое next action |
| Error | Контекстный текст/alert, явная возможность retry где предусмотрено |
| Selected | Существующий accent/outline/check и accessibility state |
| Incoming/outgoing | Выравнивание, цвет, metadata и shape по направлению |
| Light/dark | Semantic foreground/background либо explicit inverse scene |
| Focused | Системный keyboard/focus behavior; поле не скрыто overlay |
Не всем компонентам нужны все состояния. Сеть/OMEMO predicates определяют доступность; UI Kit не отменяет функциональные ограничения `tz.md`.
## 22. Текст и доступность
- Основной язык новых UI строк — русский, короткие глаголы: «Отправить», «Сохранить», «Отмена», «Готово».
- Progress label обозначает действие: «Подключение…», «Отправка…», «Соединение…».
- Ошибка объясняет доступное пользователю действие, а не только внутренний enum.
- Destructive действия должны различать «Удалить у меня» и «Удалить у всех».
- Иконки без текста имеют accessibilityLabel; медиа — label/hint; selected state сообщается текстом.
- Время и меняющиеся числа используют monospacedDigit в соответствующих компактных controls.
- Существуют identifiers `chat-timeline`, `bubble-<id>`, `reply-banner`, `info-toast`; сохранять их при визуальном рефакторинге.
- Не заявлять аудит VoiceOver/Dynamic Type/contrast только по наличию modifiers. Это отдельная проверка на устройствах.
- Контрольная рамка composer 42×42 — факт текущего кода, а не гарантия минимального 44×44 hit area у каждой кнопки. При расширении доступности проверять effective hit target отдельно.
## 23. Известные неоднородности
| Область | Факт | Как использовать дальше |
| --- | --- | --- |
| Accent | root и asset отличаются | Переиспользовать semantic accent; унификация отдельно |
| Bubble | Custom shape с хвостиком, старые тексты говорят обратное | Текущий shape — эталон существующего bubble |
| Delivery labels | Английские строки и `Sended` | Не распространять на новые тексты; исправление отдельно |
| Search | Shared custom и native searchable | Сохранять верхнее расположение, не считать padding общим для native |
| Buttons | Glass и borderedProminent | Выбирать по существующему контексту |
| Spacing | Нет единой 4-pt системы | Копировать конкретный pattern, не выдумывать новую сетку |
| Watch accent/badge | Color.blue, число без clamp99 | Платформенный вариант |
| Connection banner | White foreground на прозрачной поверхности | Проверить контраст в месте использования |
| Responsive media | Фиксированная ширина 268 | Проверять узкую ширину и большой Dynamic Type |
| Dark/accessibility | Semantic база, без полного аудита | Проверять каждое изменение на Apple runtimes |
## 24. Порядок добавления нового UI
1. Выбрать ближайший существующий экран и компонент из каталога ниже.
2. Описать цель, primary action и применимые состояния.
3. Сохранить navigation pattern платформы.
4. Использовать semantic text/colors; специальные значения взять из соответствующей таблицы.
5. Переиспользовать AvatarView, player, media preview, toast, форму или bubble вместо копирования логики.
6. Проверить loading/error/disabled/empty, keyboard, selection и доступность действия.
7. Проверить light/dark, маленькое/большое окно, iPhone landscape, Dynamic Type и VoiceOver, если затронуты.
8. При изменении существующего pattern обновить этот документ и связанные previews/UI tests; при изменении функциональной политики — также tz.md/tests/verify guard.
### Чек-лист визуальной приёмки
- [ ] Экран выглядит частью Luma и использует подходящий platform shell.
- [ ] Accent, typography и material роли совпадают с соседними экранами.
- [ ] Нет нового близкого синего или случайного radius вместо имеющегося компонента.
- [ ] Long names/body, empty state и ошибки не ломают layout.
- [ ] Media не обрезается неожиданно и не выходит за рамку.
- [ ] Incoming/outgoing/selected/disabled различимы.
- [ ] Scroll/reply swipe/media pan не конфликтуют.
- [ ] Loading не оставляет вечный блокирующий spinner.
- [ ] Controls доступны клавиатуре/VoiceOver по применимости.
- [ ] Системные safe areas, toolbar/tab bar и overlays не перекрывают важный контент.
- [ ] Проверены платформы, затронутые изменением.
## 25. Каталог компонентов и источников
Пути относительно корня репозитория. Исходник компонента содержит окончательные значения локальной геометрии и платформенные ветви.
| Область | Эталон |
| --- | --- |
| App tint / window | `Sources/App/LumaApp.swift` |
| Layering / toast / alert | `Sources/Shared/UI/RootView.swift` |
| Navigation / search / lists | `Sources/Shared/UI/MainTabView.swift` |
| List row / avatar / connection | `Sources/Shared/UI/Components/ConversationRow.swift`, `AvatarView.swift`, `ConnectionBanner.swift` |
| Timeline / composer / selection | `Sources/Shared/UI/ChatView.swift` |
| Message / reaction / metadata | `Sources/Shared/UI/Components/MessageBubble.swift` |
| Bubble shape | `Sources/Shared/UI/Components/ChatBubbleShape.swift` |
| Reply curves / focus | `Sources/Shared/UI/Components/BubbleShapes.swift`, `ReplyThreadOverlay.swift` |
| Photo / video / videoNote | `Sources/Shared/UI/Components/PhotoAttachmentPreview.swift`, `VideoAttachmentPreview.swift`, `VideoNotePreview.swift` |
| Voice / audio | `Sources/Shared/UI/Components/AudioMessagePlayer.swift` |
| Viewer | `Sources/Shared/UI/Components/MediaViewer.swift` |
| Emoji | `Sources/Shared/UI/Components/EmojiPickerView.swift` |
| Attachment preparation UI | `Sources/Shared/UI/AttachmentPreviewView.swift` |
| Calls | `Sources/Shared/UI/CallView.swift`, `Components/RTCVideoRendererView.swift` |
| Login / lock | `Sources/Shared/UI/LoginView.swift`, `AppLockView.swift` |
| Settings / encryption | `Sources/Shared/UI/SettingsView.swift`, `EncryptionDevicesView.swift` |
| New chat/group / group info | `Sources/Shared/UI/NewChatView.swift`, `NewGroupView.swift`, `GroupInfoView.swift` |
| Forward / location | `Sources/Shared/UI/ForwardMessageView.swift`, `LocationPickerView.swift`, `Components/LocationMessagePreview.swift` |
| Camera capture | `Sources/Shared/UI/SystemPhotoCameraView.swift`, `VideoNoteCaptureView.swift` |
| Server diagnostics | `Sources/Shared/UI/ServerInfoView.swift` |
| Preview fixtures | `Sources/Shared/UI/PreviewSupport.swift` и локальные `#Preview` |
| Animation / gestures | `Sources/Shared/Models/ChatMessageEntrancePolicy.swift`, `MessageReplySwipePolicy.swift`, `ChatScrollPositionPolicy.swift` |
| Watch | `Sources/Watch/WatchViews.swift` |
| Visual regression tests | `UITests/LayoutCheckUITests.swift`, `UITests/TimelineUITests.swift` |
Документ является baseline текущего UI. Он не объявляет новый дизайн уже реализованным и не подменяет screenshots живого приложения иллюстрациями.
+8
View File
@@ -179,3 +179,11 @@ OMEMO, и для XEP-0084 аватаров. Для медиа и вложени
Архитектура описана в [`Docs/ARCHITECTURE.md`](Docs/ARCHITECTURE.md), модель
угроз и ограничения хранения — в [`Docs/SECURITY.md`](Docs/SECURITY.md).
## Стандарт интерфейса
Текущий дизайн, компоненты, размеры, цвета, состояния и платформенные правила зафиксированы в [Luma UI Kit](Docs/UIKIT.md). Используйте его как основу для новых экранов и визуальных изменений.
## Требования для Web, Desktop и Android
Для разработки новых клиентов используйте [чистое техническое задание](tz_clear.md) и [единый UI Kit](uikit_clear.md). Эти документы задают целевой продукт и не зависят от реализации Apple-клиента.
+2151
View File
File diff suppressed because it is too large. Load diff
+793
View File
@@ -0,0 +1,793 @@
# Luma — техническое задание для Web, Desktop и Android
Версия: 1.0. Дата: 14 октября 2026 года. Статус: целевой стандарт разработки.
Связанный документ: [UI Kit](uikit_clear.md).
## Содержание
- [1. Назначение документа](#1-назначение-документа)
- [2. Продукт и пользователи](#2-продукт-и-пользователи)
- [3. Платформы и объём](#3-платформы-и-объём)
- [4. Общие требования к реализации](#4-общие-требования-к-реализации)
- [5. Предметная модель](#5-предметная-модель)
- [6. Аккаунты и вход](#6-аккаунты-и-вход)
- [7. Соединение и аутентификация](#7-соединение-и-аутентификация)
- [8. Навигация и основные экраны](#8-навигация-и-основные-экраны)
- [9. Контакты, presence и аватары](#9-контакты-presence-и-аватары)
- [10. Текстовые сообщения и доставка](#10-текстовые-сообщения-и-доставка)
- [11. Действия над сообщениями](#11-действия-над-сообщениями)
- [12. Реакции и typing](#12-реакции-и-typing)
- [13. Группы MUC](#13-группы-muc)
- [14. Шифрование и устройства](#14-шифрование-и-устройства)
- [15. Файлы и вложения](#15-файлы-и-вложения)
- [16. Просмотр и сохранение медиа](#16-просмотр-и-сохранение-медиа)
- [17. Запись аудио, видео и геопозиция](#17-запись-аудио-видео-и-геопозиция)
- [18. Локальная история и MAM](#18-локальная-история-и-mam)
- [19. Звонки](#19-звонки)
- [20. Уведомления, push и фоновая работа](#20-уведомления-push-и-фоновая-работа)
- [21. Локальная защита и хранение](#21-локальная-защита-и-хранение)
- [22. Приватность и безопасность](#22-приватность-и-безопасность)
- [23. Совместимость протоколов](#23-совместимость-протоколов)
- [24. Нефункциональные требования](#24-нефункциональные-требования)
- [25. Критерии приёмки](#25-критерии-приёмки)
- [26. Состав результата разработки](#26-состав-результата-разработки)
- [27. Паспорт конкретного приложения](#27-паспорт-конкретного-приложения)
## 1. Назначение документа
Разработать семейство XMPP-приложений Luma с общими пользовательскими сценариями, совместимыми сообщениями и единым визуальным языком для Web, Desktop и Android.
Документ является самостоятельным техническим заданием. Он определяет результат, поведение и критерии приёмки без привязки к языку программирования, UI framework, структуре исходников и конкретной библиотеке XMPP.
«Обязательно» означает требование базового продукта. «При наличии возможности» означает управляемую зависимость от разрешений, оборудования, сервера или платформы, а не необязательную реализацию обработки отказа. Даже при отсутствии функции приложение обязано корректно объяснить ограничение и сохранить доступность остальных сценариев.
Требования с идентификаторами используются в тестах и задачах. Выбранные технологии, поддерживаемые версии ОС/браузеров и инфраструктурные адреса фиксируются отдельно в паспорте конкретного приложения до его выпуска.
## 2. Продукт и пользователи
Luma — мессенджер для пользователей существующих XMPP-аккаунтов. Пользователь входит по JID и паролю, выбирает совместимый сервер, общается лично и в группах, отправляет файлы и звонит другим пользователям.
Основные ценности:
- независимость от одного XMPP-провайдера;
- совместимость с другими XMPP-клиентами;
- конфиденциальность содержимого через OMEMO;
- локальная история и восстановление с сервера;
- одинаковая семантика сообщений и действий на разных устройствах;
- понятные ошибки и отсутствие молчаливого отключения защиты.
Интерфейс первого выпуска — русский. Вся пользовательская строка должна допускать последующее добавление локализаций. Идентификаторы протокола и адреса пользователей не переводятся.
## 3. Платформы и объём
| Платформа | Форма | Обязательный сценарий |
| --- | --- | --- |
| Web | Приложение в браузере, адаптивное для телефона и компьютера | Работа в активной сессии, локальная история, сообщения, медиа и звонки при доступных разрешениях |
| Desktop | Устанавливаемое приложение для утверждённого набора настольных ОС | Оконный интерфейс, системные уведомления, работа при скрытом окне в пределах жизненного цикла процесса |
| Android | Устанавливаемое мобильное приложение | Сенсорный интерфейс, системный lifecycle, уведомления и входящие вызовы через поддержанный фоновый механизм |
### 3.1. Базовый объём
- несколько сохранённых аккаунтов, один активный XMPP-контекст;
- вход/выход/переключение, настройки подключения;
- контакты, presence, личные чаты и чат с собой;
- MUC: создание, вход, приглашение, выход, локальное удаление;
- текст, replies, reactions, correction, retraction и локальное удаление;
- множественный выбор и пересылка;
- OMEMO legacy и OMEMO 2, доверие устройствам;
- файлы, фото, видео, музыка, voice, video notes и геоточки;
- локальная история, MAM, Carbons, receipts и read state;
- аудио-/видеозвонки один на один и история звонков;
- аватары;
- настройка уведомлений, app lock и биометрия при доступности;
- серверная диагностика;
- push и фоновые входящие в пределах утверждённой платформенной интеграции.
### 3.2. За пределами базового объёма
Apple Watch и другие companion-устройства, регистрация новых XMPP-аккаунтов, восстановление серверного пароля, групповые звонки, каналы/MIX, боты, платежи, истории, live location, полнотекстовый поиск всех сообщений, обязательные одновременные соединения всех сохранённых аккаунтов.
Развёртывание XMPP-провайдера не является функцией пользовательского клиента. Инфраструктура push/relay, если нужна для заявленной доставки, входит в состав решения отдельным компонентом и должна быть поставлена либо явно подключена к утверждённому внешнему сервису.
## 4. Общие требования к реализации
**GEN-001.** Одинаковые действия на разных платформах должны иметь одинаковый смысл: «Отправлено», «Доставлено», «Прочитано», локальное удаление и отзыв нельзя интерпретировать по-разному.
**GEN-002.** UI, предметная логика, протоколы, хранение и системные интеграции разделяются устойчивыми интерфейсами. UI не владеет криптографической сессией или активным медиасоединением.
**GEN-003.** Криптография, обработка больших файлов, индексирование и архивная загрузка не должны монопольно занимать поток отображения интерфейса.
**GEN-004.** Все долгие операции имеют состояние выполнения, завершения, ошибки и отмены там, где отмена безопасна. Ошибка не оставляет интерфейс навсегда заблокированным.
**GEN-005.** Недоступная возможность сервера отключает соответствующую функцию с объяснением, сохраняя остальные функции. Поддержка, объявленная сервером, не отображается как уже настроенная интеграция приложения.
**GEN-006.** Приложение не обещает гарантированную доставку, прочтение, удаление чужих копий, работу закрытого процесса или конфиденциальность метаданных, если механизм этого не обеспечивает.
**GEN-007.** Исходные документы, тесты и поставляемая версия приложения должны соответствовать друг другу. Релиз имеет номер версии и журнал изменений.
## 5. Предметная модель
Ниже приведён логический контракт. Физическая схема БД и способы сериализации выбираются реализацией.
### 5.1. Аккаунт
| Атрибут | Содержание |
| --- | --- |
| accountId | Нормализованный bare JID, устойчивый ключ локального контекста |
| jid | Адрес пользователя |
| displayName | Отображаемое имя |
| deviceResource | Уникальный для установки ресурс соединения |
| connectionSettings | Endpoint/host/port, транспортный режим, настройки discovery |
| encryptionDefault | По умолчанию включён |
| typingEnabled | По умолчанию включён |
| notificationSettings | Разрешение/предпочтения, показ preview |
| credentialReference | Ссылка на защищённое хранилище, не пароль в общедоступном объекте |
Пароли, refresh tokens и приватные ключи не входят в открытый экспорт настроек.
### 5.2. Диалог
Идентичность диалога — accountId + conversationJid, а не один JID без контекста аккаунта.
Атрибуты: JID, direct/group, имя, avatar reference, lastActivity, lastMessagePreview, unreadCount, presence, optional pinned flag, encryptionOverride, group nickname, joined, autojoin, occupantCount и invitation metadata.
Значение joined после перезапуска определяется реальным соединением, а не только записью в БД. Режим шифрования: наследовать аккаунт / всегда включено / выключено.
### 5.3. Сообщение
| Группа | Атрибуты |
| --- | --- |
| Идентичность | accountId, conversationId, clientId, optional originId, server stanzaId, archive UID |
| Автор | sender JID, optional nickname/display name, incoming/outgoing |
| Содержимое | kind, body, timestamp |
| Доставка | sending / sent / delivered / read / failed |
| Защита | encrypted / plaintext / decryptionFailed, optional device/fingerprint reference |
| Медиа | remote URI, filename, MIME, byte count, duration, local asset reference |
| Изменения | editedAt, retractedAt |
| Reply | targetId, author JID, preview |
| Пересылка | Атрибуция источника, без ложного сохранения исходного авторства нового сообщения |
| Реакции | Автор → текущий набор emoji и timestamp |
| Звонок | Направление, audio/video, исход, начало, duration |
Kinds: text, attachment, photo, video, audio, voice, videoNote, location, system. Исторический звонок — системная запись с типизированными metadata.
### 5.4. Служебные сущности
- archive checkpoint: account/room archive key, opaque cursor, timestamp;
- local deletion suppression: account + conversation + идентификаторы удалённого сообщения;
- device: owner JID, id, protocol support, identity fingerprint, active/trust;
- attachment draft: staged asset, kind, filename, MIME, size, duration, thumbnail;
- call session: id, endpoint full JID, direction, media, phase, tracks/control state;
- pending operation: тип, target, автор, timestamp и ограниченный срок/объём удержания.
## 6. Аккаунты и вход
**ACC-001.** Входная форма содержит JID, пароль, показать/скрыть пароль и дополнительные настройки. Пустой JID/пароль не принимаются.
**ACC-002.** JID валидируется по выбранному XMPP address preparation; пробелы по краям ввода обрабатываются, resource не смешивается с bare account identity. Пароль не обрезается молча.
**ACC-003.** Native-клиенты поддерживают discovery и ручной host/port с STARTTLS/direct TLS. Стандартные порты: 5222 для STARTTLS, 5223 для direct TLS. Порт проверяется как число 1…65535; нечисловой ввод показывает ошибку, а не молча превращается в default.
**ACC-004.** Web-клиент использует защищённый WebSocket XMPP endpoint. Форма допускает ввод/выбор WSS endpoint и не обещает прямое использование произвольного TCP host/port браузером. BOSH не обязателен; если включён отдельно, допускается только HTTPS.
**ACC-005.** Endpoint discovery не должен незаметно отправлять пароль произвольному стороннему шлюзу. Пользователь видит домен выбранного endpoint; для неизвестной конфигурации требуется явный выбор сервера.
**ACC-006.** На время входа показывается progress и блокируется повторная отправка той же формы. После успешного входа открываются локальная история и основной интерфейс. Ошибка оставляет возможность исправить credentials.
**ACC-007.** Можно сохранить несколько аккаунтов. Добавление аккаунта делает его активным после успешного входа. Одновременно обязательно только одно активное соединение; неактивные аккаунты не представляются как постоянно online.
**ACC-008.** Переключение закрывает/отделяет старый контекст и открывает данные нового. Завершившаяся поздно операция прошлого аккаунта не может записать результат в новый аккаунт или отправить сообщение от его имени.
**ACC-009.** При доступных credentials запуск восстанавливает активный аккаунт и локальную историю, затем соединение. Если secrets недоступны/истекли, приложение запрашивает повторный вход без потери сохранённой истории.
**ACC-010.** Для каждого устройства создаётся устойчивый уникальный ресурс; одновременный запуск разных устройств одного аккаунта не должен создавать конфликт одинаковых default resources.
### 6.1. Выход и удаление данных
**ACC-011.** «Выйти» прекращает соединение, unregister/disable применимую push-привязку, удаляет сохранённые credentials текущего аккаунта и очищает его чувствительное session state. Локальную историю можно сохранить.
**ACC-012.** Отдельный вариант «Выйти и удалить локальные данные» удаляет историю, suppression, drafts, медиа cache и данные аккаунта в локальном профиле. Удаление OMEMO identity — явно обозначенный вариант со предупреждением о потере доступа к старым encrypted сообщениям; без согласия identity не уничтожается как побочный эффект обычного выхода.
**ACC-013.** Удаление локальных данных не называется secure erase и не обещает удаление server archives, exports, backup или чужих копий.
**ACC-014.** Перед уходом из аккаунта незавершённые записи/загрузки имеют определённое поведение: отмена либо сохранение безопасного локального результата; нет продолжения отправки от случайного другого аккаунта.
## 7. Соединение и аутентификация
**NET-001.** Подключение защищено TLS; проверяются certificate chain и ожидаемое имя сервиса/endpoint согласно используемому транспорту. Insecure HTTP/WS и обход ошибок сертификата не включены в пользовательский стандарт.
**NET-002.** Нативный транспорт поддерживает DNS discovery и ручные параметры. В Web используется WSS endpoint, утверждённый для выбранного сервера.
**NET-003.** Аутентификация выбирает наиболее сильный совместимый объявленный механизм. Приоритет SCRAM-PLUS с доступным channel binding, затем SCRAM-SHA-512/256/1, затем разрешённый защищённым транспортом fallback.
**NET-004.** PLUS не имитируется при отсутствии данных реального TLS-канала. В частности, реализации, не имеющие доступа к exporter/binding, выбирают совместимый non-PLUS механизм и корректно отражают это в диагностике.
**NET-005.** Проверяются nonce, server proof/signature, параметры challenge и применимые downgrade-protection данные. Для limits на challenge/iterations определяется защитная конфигурация, предотвращающая блокировку клиента недоверенным вводом.
**NET-006.** Состояния: отключено, подключение, подключено, восстановление, ошибка/требуется вход. Причина ошибки локализована. Нельзя показывать «Подключено» только по наличию сохранённого аккаунта.
**NET-007.** Поддерживаются stream management, carbons, ping и client-state indication при поддержке транспорта/сервера.
**NET-008.** Повторное соединение использует ограниченный backoff с jitter, прекращается при явном logout и не создаёт параллельных конкурирующих клиентов одного контекста. Возврат в foreground инициирует проверку/восстановление.
**NET-009.** Смена сети, sleep/wake, закрытие вкладки и потеря credentials обрабатываются без повреждения локального состояния. Возобновление stream не должно повторно создавать bubbles уже известных сообщений.
## 8. Навигация и основные экраны
**NAV-001.** Основные разделы: Контакты, Звонки, Чаты, Настройки. Начальный раздел — Чаты. Compact/wide варианты заданы UI Kit.
**NAV-002.** Поиск расположен над соответствующим списком. Поиск чатов — по имени/JID/аккаунту, контактов — по имени/JID, звонков — по собеседнику. Это фильтр списков; полнотекстовый поиск истории отдельно не обещается.
**NAV-003.** Чаты сортируются pinned-first при наличии pinned-функции, затем по последней активности, затем по имени для стабильности. Изменение unread не должно произвольно менять порядок при одинаковой активности.
**NAV-004.** В списке видны avatar, имя, preview, время, unread и применимый status. Аккаунт отображается при необходимости различать одинаковые JID.
**NAV-005.** Пустые данные и отсутствие совпадений поиска имеют разные объяснения. Создание чата/комнаты остаётся доступным в empty state.
**NAV-006.** Back возвращает в предыдущий логический контекст. На широком экране смена selection не должна ломать sidebar; клавиатурная навигация обязательна для Web/Desktop.
## 9. Контакты, presence и аватары
**CON-001.** Контакты синхронизируются через roster: initial state и последующие изменения. Удаление контакта из roster не удаляет историю.
**CON-002.** Новый direct chat открывается по валидному JID с optional name и выбором добавления в roster. Отсутствие roster membership не запрещает сообщение само по себе.
**CON-003.** Presence показывает реальное известное состояние; нет объявления online только из-за cached avatar/history.
**CON-004.** MUC-комнаты показываются отдельно от людей. Список сохранённых комнат не выдаётся за полный серверный список членства.
**CON-005.** Аватары публикуются/получаются через совместимые XMPP PEP mechanisms; legacy fallback допустим. Новый avatar нормализуется до разумного размера, baseline 512 px по длинной стороне.
**CON-006.** При отсутствии изображения используется стабильный цвет и инициалы, для группы — group symbol. Обновление cache не требует перезапуска клиента.
**CON-007.** Пользователю не обещается OMEMO для аватаров; доступ определяется серверными правами.
## 10. Текстовые сообщения и доставка
**MSG-001.** Пустое после trim сообщение не отправляется. Каждая новая отправка имеет устойчивый уникальный client/origin id и зафиксированный аккаунт/получателя.
**MSG-002.** До сетевого результата создаётся optimistic запись sending. Success transport send → sent, receipt → delivered, displayed → read. Ошибка → failed с объяснением.
**MSG-003.** Sent означает передачу по клиентскому протокольному пути, delivered — полученный receipt, read — подтверждённый marker. UI не выводит read только из времени или online собеседника.
**MSG-004.** Объединение live, Carbons и MAM не создаёт duplicate. Проверяются контекст аккаунта/чата и origin/stanza/client identities; archive UID не используется глобально для разных архивов.
**MSG-005.** Доставка повышается монотонно sending → sent → delivered → read. Failed не «исправляется» молча архивной копией; явная повторная отправка — отдельное действие с контролем дубликатов.
**MSG-006.** Порядок ленты — timestamp и устойчивый id при равных датах. Архивная вставка сверху сохраняет viewport.
**MSG-007.** Служебные receipts, reactions, markers и пустые protocol messages не становятся обычными bubbles. Реальная ошибка дешифрования отображается как отдельное безопасное состояние.
**MSG-008.** Чат с собой поддерживает текст/медиа и sync. Успешное собственное сообщение допускается помечать read по определённому self-chat правилу.
**MSG-009.** У исходящего failed text доступно явное «Повторить», если payload сохранён. Повтор не должен незаметно менять encryption policy уже подготовленного сообщения; изменение режима требует нового явного решения.
### 10.1. Непрочитанные
**READ-001.** Новое входящее увеличивает локальный unread, если чат не просматривается активно. Архивный replay не раздувает счётчик повторно.
**READ-002.** При активном просмотре last-visible/latest incoming direct message отправляется displayed по протоколу; одно и то же состояние не рассылается без необходимости.
**READ-003.** Outgoing direct messages помечаются markable. Для MUC приложение не рассылает direct displayed markers в комнату как универсальный механизм прочтения всеми участниками.
**READ-004.** Поддерживается own-device sync read state через совместимое служебное расширение. Маркер, пришедший раньше сообщения, применяется после получения target. Пользователь не видит служебную запись в self-chat.
**READ-005.** Read marker — metadata; нельзя заявлять его E2EE только по encrypted body исходного сообщения.
## 11. Действия над сообщениями
### 11.1. Доступность
| Действие | Требование |
| --- | --- |
| Ответить | Не удалённое, не служебное; в MUC нужен server room stanza id |
| Переслать | Не удалённое и не служебное |
| Копировать | Есть пользовательский текст/подпись, не tombstone |
| Редактировать | Собственный отправленный direct text |
| Удалить у всех | Собственное отправленное direct сообщение, поддерживаемый retraction |
| Удалить у меня | Локальная запись текущего аккаунта |
| Реакция | Доступное пользовательское сообщение с устойчивым reference id |
| Сохранить | Доступное/получаемое медиа по явному действию |
| Повторить | Failed собственная отправка, payload ещё доступен |
Для group edit/retraction базовый UI не обещает поддержку. Состояние sending ограничивает действия, требующие завершённой идентичности.
### 11.2. Редактирование
**EDIT-001.** Используется совместимая message correction. Оригинал сохраняет свою идентичность и положение; заменяется body, устанавливается editedAt.
**EDIT-002.** Применение проверяет чат, автора, допустимый тип, security context и timestamp. Сторонний отправитель не может исправить чужое сообщение.
**EDIT-003.** Correction до оригинала удерживается в ограниченной pending очереди. Более старая correction не заменяет новую.
**EDIT-004.** Шифрование исправления соответствует исходному сообщению; не допускается plaintext раскрытие исправляемого encrypted body из-за текущего глобального переключателя.
### 11.3. Ответы
**REPLY-001.** Поддерживаются стандартная reply reference и fallback для совместимых старых клиентов. Unicode offsets считаются согласно протоколу, без смешения байтов и символов.
**REPLY-002.** Отображается компактный preview источника, а fallback quote не дублируется в основном body.
**REPLY-003.** Нажатие открывает источник/ветку, если reference известен. При недоступном оригинале показывается preview и объяснение без crash.
**REPLY-004.** Legacy `>` quote допускается отображать без ложного target id. MUC replies ссылаются на room-assigned stanza id и корректный occupant identity.
**REPLY-005.** Reference нельзя применить к другому чату с совпавшим id. Визуальная ветка — группировка reply, не отдельная серверная комната.
### 11.4. Пересылка
**FWD-001.** Можно выбрать одно/несколько сообщений и переслать в существующий чат либо на валидный JID. Порядок: timestamp, устойчивый id.
**FWD-002.** Создаются новые сообщения с новой identity и понятной атрибуцией. Пересылка не подделывает исходного sender.
**FWD-003.** Media скачивается/расшифровывается и заново загружается для нового recipient и его encryption policy. Старый encrypted URI/key не переиспользуется автоматически.
**FWD-004.** При частичной ошибке показывается количество успешно пересланного; уже доставленные элементы не откатываются и не повторяются молча.
### 11.5. Удаление
**DEL-001.** «Удалить у меня» удаляет local row и сохраняет suppression всех известных identities, чтобы MAM/live echo не восстановили удалённое сообщение.
**DEL-002.** «Удалить у всех» отправляет retraction, проверяет авторство при применении и заменяет target на tombstone. Очищаются body/media/reply/reaction sensitive payload и связанные доступные cache.
**DEL-003.** Retract до оригинала удерживается pending. Generic fallback не повторяет удаляемое содержимое.
**DEL-004.** UI явно сообщает разницу локального удаления и отзыва. Ни одно не обещает стирание screenshots, backups, чужих exports или неподдерживающего клиента.
**DEL-005.** Массовое действие фильтрует/объясняет недоступные элементы; подтверждение показывает область действия и число сообщений.
## 12. Реакции и typing
**REACT-001.** Реакции совместимы с XMPP reactions: target id и полный актуальный набор автора; пустой набор снимает реакцию.
**REACT-002.** Emoji валидируются, дубликаты удаляются. Один reaction value — один grapheme, ограниченный безопасной длиной; baseline до 64 UTF-8 bytes.
**REACT-003.** Показываются emoji, count и own state. Повторный tap переключает свой вклад. Более старое событие не отменяет новое.
**REACT-004.** Direct авторы сравниваются по подготовленному bare JID, group — по корректной occupant identity. Нельзя объединять разных участников из-за неверной нормализации resource.
**REACT-005.** Реакции не шифруются автоматически только из-за OMEMO текста. Базовая совместимая stanza является видимой серверу metadata; отдельное encrypted extension требует самостоятельного согласования и interoperability tests.
**TYPE-001.** Typing имеет active/composing/paused/inactive/gone. Baseline пауза после 5 с, удалённый composing истекает через 8 с без обновления.
**TYPE-002.** Настройка typing per-account; выключение прекращает отправку/отображение и очищает pending indicators. Для direct учитывается поддержка peer.
**TYPE-003.** В группе показываются имена для одного/двух участников, затем обобщённый статус. Переход между чатами/уход из foreground не оставляет вечное «печатает».
## 13. Группы MUC
**MUC-001.** Пользователь создаёт/открывает room JID, задаёт nickname и optional name; для защищённых комнат предусмотрен запрос password при требовании сервера.
**MUC-002.** Поддерживаются invitations, accept/join, occupant count, autojoin и информация о комнате. Invitees валидируются; некорректные адреса явно обозначаются.
**MUC-003.** Новая encrypted-friendly комната конфигурируется persistent, members-only, nonanonymous и с доступными участникам affiliation lists, если сервер даёт эти права.
**MUC-004.** Для encrypted send проверяются real JID всех необходимых online/offline recipients через occupants и member/admin/owner lists. Недоступный/неполный набор получателей блокирует безопасную отправку.
**MUC-005.** Скрытые JID, отсутствие устройств или невозможность построить session не вызывают fallback к plaintext. Ошибка объясняет, что может изменить владелец/участник.
**MUC-006.** Own echo дополняет room stanza identity даже при повторном encrypted payload. Replies/reactions становятся доступны после устойчивой серверной identity.
**MUC-007.** «Выйти» сохраняет локальный чат/историю и снимает joined. «Удалить чат» удаляет локальную историю/сохранённую комнату после выхода; это не уничтожение комнаты сервера.
**MUC-008.** После локального удаления поздние события текущей room session не создают чат заново без явного основания. Повторное приглашение/вход имеют определённую семантику.
## 14. Шифрование и устройства
**ENC-001.** OMEMO включён по умолчанию для аккаунта. Per-chat override: наследовать / включено / выключено. UI показывает итоговый режим до отправки.
**ENC-002.** Ошибка encryption никогда не приводит к автоматической plaintext отправке. Plaintext возможен только при явно выбранной политике.
**ENC-003.** Поддерживаются legacy OMEMO и OMEMO 2 с совместимыми wire formats, device lists/bundles и согласованной lifecycle политикой identity/session.
**ENC-004.** Для direct предпочтителен OMEMO 2 при готовности и поддержке peer; иначе совместимый legacy. Для group OMEMO 2 используется только при поддержке всех recipients; legacy остаётся encrypted fallback.
**ENC-005.** Encrypt-to-self охватывает собственные другие устройства, чтобы Carbons/MAM могли быть прочитаны. Active own device не включается как чужой recipient с некорректной обязательной self-session.
**ENC-006.** Private identity, prekeys и sessions сохраняются устойчиво и раздельно по аккаунтам. Обновление приложения не создаёт новую identity без необходимости.
**ENC-007.** TOFU и verified различаются. Первое доверие отмечается как автоматическое; changed identity переводится в undecided и требует понятного решения. Compromised key не используется как trusted.
**ENC-008.** Пользователь видит список устройств, protocol support, fingerprint, active state и trust, может отметить проверенный fingerprint и снять verification.
**ENC-009.** OMEMO encryption/decryption выполняются в клиентском endpoint. Web gateway может передавать encrypted stanza, но не получает password/crypto state для скрытой серверной расшифровки. Применяются проверенные криптографические реализации и совместимые test vectors. Собственный новый алгоритм, замена примитивов или трактовка общего session storage как доказанной interoperability не допускаются.
**ENC-010.** Зашифрованный text не означает шифрование JID, времени, stanza ids, replies metadata, reactions, typing, avatars или custom sync. Матрица раскрываемых metadata является частью security документации каждого релиза.
**ENC-011.** Криптографический формат OMEMO 2 реализуется по утверждённой совместимой редакции с SCE и необходимыми payload/key/MAC rules; он не заменяется media AES-GCM только из-за удобства библиотеки.
## 15. Файлы и вложения
**FILE-001.** Поддерживаются файл, фото, видео, музыка, voice и videoNote. Тип определяется безопасно по metadata/MIME/доступному анализу; расширение не даёт права автоматически выполнять содержимое.
**FILE-002.** Можно выбрать до 20 элементов за пакет, просмотреть, удалить отдельные и добавить общую подпись. Перевышение лимита объясняется; элементы не теряются молча.
**FILE-003.** Источник копируется/удерживается в staged draft с достаточным сроком доступа. Не предполагается, что временный URL picker остаётся доступным после его закрытия.
**FILE-004.** При encrypted policy blob шифруется до upload случайным file key; ключ передаётся внутри защищённого сообщения, не upload service. Для plaintext это явно обозначено.
**FILE-005.** Используется HTTP File Upload discovery/slot. Выбирается подходящий component по реально загружаемому размеру. Ограничение сервера и памяти отображается до бессмысленного повторения операции.
**FILE-006.** Upload/download допускаются по HTTPS. Проверка распространяется на redirect chain; переход к небезопасной схеме не допускается. HTTP headers/credentials slot не отправляются произвольному другому origin после redirect.
**FILE-007.** Сохраняется исходное безопасное расширение filename. Encrypted media URI использует совместимую aesgcm scheme/key fragment, а не вымышленное обязательное расширение файла.
**FILE-008.** Body media-сообщения остаётся распознаваемой ссылкой; plaintext дополнительно передаёт совместимый OOB. Подпись пакета — отдельный text/reply к первому подходящему вложению.
**FILE-009.** Пакет отправляется с контролем порядка. Successful drafts удаляются из pending, failed остаются для retry; caption отправляется после успешных вложений. Ошибка caption не откатывает уже отправленные файлы.
**FILE-010.** Retry сохраняет account/recipient/message identity и encryption context. Сбой после PUT, но до XMPP send может оставить orphan blob; это не обозначается как гарантированно удалённый файл.
**FILE-011.** Загрузки поддерживают progress по доступным данным, cancellation и bounded retry. Недостаток диска/памяти обрабатывается как ошибка без повреждения остальных сообщений.
**FILE-012.** В Web endpoint обязан поддерживать необходимые browser access/CORS политики. Proxy не получает file key или decrypted blob для обхода ограничений незаметно для пользователя.
### 15.1. Совместимые transport metadata
Созданные Luma voice/videoNote сохраняют распознаваемое имя при загрузке:
| Тип | Формат transport filename |
| --- | --- |
| Voice | `luma-voice-<durationMilliseconds>-<filename.ext>` |
| VideoNote | `luma-videonote-<durationMilliseconds>-<filename.ext>` |
| Другой файл | Безопасное исходное имя с расширением |
Duration — положительное конечное значение, округлённое до миллисекунд. При отсутствии валидной duration имя не должно содержать неверную/отрицательную длительность. Получатель распознаёт специальные prefixes, обычный MIME/расширение и прежние совместимые encrypted URI. Transport filename и размер доступны upload service; они не являются секретными metadata.
`byteCount` сообщения относится к исходному файлу; лимит slot сравнивается с размером реально загружаемого encrypted/plain blob.
### 15.2. Baseline сетевых параметров
| Параметр | Значение по умолчанию |
| --- | --- |
| Upload request timeout | 120 с |
| Upload total resource timeout | 300 с |
| Automatic attempts | 2 суммарно |
| Задержка перед второй попыткой | 600 мс, допустим небольшой jitter |
| Retryable HTTP | 408, 429 с учётом Retry-After, 5xx |
| Неповторяемые без изменения условий | Insecure URL, invalid input/key, неподходящий размер, постоянный permission error |
Повтор discovery/slot/transport ограничен. Таймауты конфигурируемы и не являются обещанием скорости сети.
## 16. Просмотр и сохранение медиа
**MEDIA-001.** Photo/video имеют inline preview с сохранением aspect ratio без неожиданного crop. Loading/error/placeholder различаются.
**MEDIA-002.** Фото открывается в viewer с zoom/pan. Видео имеет play/pause/seek/fullscreen и доступные системные controls. Voice/audio — inline player, duration, seek, progress; voice speeds 1×/1.5×/2×.
**MEDIA-003.** В пределах приложения активный media playback согласован: начало другого audio/video не создаёт неконтролируемое одновременное звучание. Системный audio focus обрабатывается по платформе.
**MEDIA-004.** VideoNote показан кругом, воспроизводится inline и увеличивается при playback. Изменение размера не вызывает потерю позиции чтения.
**MEDIA-005.** Preview cache не скачивает один файл многократно из-за повторного появления row. Decrypted cache ограничивается размером/сроком и очищается согласно privacy/account lifecycle.
**MEDIA-006.** Экспорт/скачивание/копирование выполняются явно. Пользователь выбирает доступное платформенное место сохранения; отмена не показывает success.
**MEDIA-007.** Viewer закрывается доступной кнопкой/keyboard action и gesture по UI Kit. Zoomed pan не конфликтует с dismiss.
**MEDIA-008.** Неподдерживаемый codec/file открывается безопасным fallback/download с объяснением; не имитируется успешное воспроизведение.
## 17. Запись аудио, видео и геопозиция
**REC-001.** Разрешения microphone/camera запрашиваются по действию пользователя. Отказ не ломает текстовый composer.
**REC-002.** Voice: start, elapsed, cancel, finish; запись короче 0.4 с не отправляется. Длительность голоса не ограничивается автоматически 60 секундами: ограничения файла/сервера действуют отдельно.
**REC-003.** VideoNote: до 60 с, корректная ориентация, переключение камеры при доступном hardware. Минимальный валидный media asset — 0.8 с; early stop может дождаться достаточного capture, baseline 1.2 с.
**REC-004.** Отпускание/отмена до асинхронного старта capture не оставляет скрытую запись. Finalization имеет timeout и завершает UI в определённое состояние.
**REC-005.** При interruption пригодный partial asset может быть предложен для восстановления/предпросмотра, но не отправляется автоматически как подтверждённое действие.
**REC-006.** На touch поддерживаются hold/slide cancel/lock. На mouse/keyboard всегда есть явные start/stop/cancel; жест не является единственным способом записи.
**GEO-001.** Отправка текущей или выбранной точки доступна через меню вложения. Разрешение location запрашивается только в этом сценарии.
**GEO-002.** При denied/отсутствии GPS доступен ручной выбор координат на карте либо ввод точки. Live tracking в scope не входит.
**GEO-003.** Передаётся совместимый geo URI: finite latitude −90…90, longitude −180…180, optional nonnegative uncertainty. UI не выдаёт число знаков после запятой за точность датчика.
**GEO-004.** Карта/tiles provider и передача ему coordinates отражены в privacy конфигурации решения. OMEMO чата не скрывает запрос карты от её провайдера.
## 18. Локальная история и MAM
**HIST-001.** История хранится локально с изоляцией аккаунтов. Существующие сообщения доступны при временном отсутствии сети, если устройство/профиль допускает persistent storage.
**HIST-002.** На первом входе загружается ограниченное недавнее окно, baseline 40 сообщений. Полная многолетняя история не блокирует запуск; старое догружается по запросу/прокрутке.
**HIST-003.** Account archive и каждая MUC имеют собственные checkpoint. Cursor непрозрачен, не вычисляется из local id и не переносится между архивами.
**HIST-004.** Incremental запрос использует stored cursor. При его истечении допустим однократный timestamp fallback с overlap 60 с. Цикл cursor↔timestamp запрещён.
**HIST-005.** Проверяются query id, допустимый источник, порядок завершения и размер страницы. Чужой результат/overflow не считается успешной историей.
**HIST-006.** Missing/repeating cursor у incomplete page прекращает проход с recoverable error. Не допускаются вечный spinner и пропуск остатка как якобы complete.
**HIST-007.** Архивные mutations применяются согласованными пачками. Durable checkpoint продвигается только после успешной локальной транзакции соответствующих данных. Ошибка диска сохраняет предыдущую точку восстановления.
**HIST-008.** Backward pagination берёт следующий before из server RSM first даже если страница содержала только reactions/retractions/дубликаты.
**HIST-009.** Pending correction/retraction/reaction/read marker применяются после target. Очереди ограничены и очищаются по политике; overflow не создаёт crash.
**HIST-010.** Расшифрование и большие batches уступают время UI. Background/media capture/upload могут временно снижать архивную активность; возобновление не теряет checkpoint.
**HIST-011.** Автоматические retries ограничены; после исчерпания показывается recoverable state и доступно повторить. Незавершённый проход не объявляется «История полностью загружена».
**HIST-012.** Локальная миграция выполняется транзакционно; исходный формат удаляется после успешной записи. Upgrade не должен уничтожать единственную durable копию данных.
### 18.1. Baseline ограничений
| Параметр | Значение |
| --- | --- |
| MAM page/bootstrap | 40 / 40 |
| Максимум buffered stanza одной query | 96 |
| Decode slice | 8, с уступкой UI между срезами |
| Ориентир порции применения | До 2 страниц или 2 с перед yield/commit |
| Query/apply timeout | 20 / 20 с |
| Interactive backward query timeout | 15 с |
| Межстраничная пауза | 100 мс |
| Page retry | 1 дополнительный |
| Automatic catch-up recovery | До 3 повторов, пауза 20 с |
| Pending corrections/retractions | До 100 каждого вида |
| Pending reactions | До 200 |
Порция работы не ограничивает всю историю двумя страницами: после yield допустимо продолжение. Реализация может уменьшать slice по нагрузке и увеличивать configurable server-compatible limits после измерений, но не отменять контроль объёма.
## 19. Звонки
**CALL-001.** Поддерживаются аудио-/видеозвонки один на один, один активный звонок на клиентский контекст. Второй входящий/исходящий получает определённый busy/reject сценарий.
**CALL-002.** До запуска запрашиваются microphone/camera permissions. Есть incoming accept/decline, connecting, connected, elapsed и завершение.
**CALL-003.** Доступны mute, camera enable/disable, switch camera при нескольких камерах и speaker/audio output выбор в пределах платформенных возможностей.
**CALL-004.** Используется совместимый Jingle/JMI signaling и WebRTC media с ICE/DTLS-SRTP. Endpoint выбирается как подходящий full JID; последующие сообщения согласованы с тем же ресурсом.
**CALL-005.** SDP media sections сохраняют порядок offer. ICE candidates удерживаются до готового remote description, затем передаются/применяются без вечного ожидания bare/full JID.
**CALL-006.** STUN/TURN обнаруживаются через extdisco или утверждённую конфигурацию. Проверяются scheme, host/port, transport, expiration и credentials. Невалидная запись не приводит к crash медиадвижка.
**CALL-007.** TURN relay необходим для утверждённой тестовой матрицы сложных сетей. Отсутствие relay показывается диагностически; STUN не описывается как гарантия соединения за любым NAT.
**CALL-008.** Внешний fallback STUN допускается только как задокументированный конфигурационный выбор. Сервис-получатель сетевых адресов отражается в privacy документации.
**CALL-009.** Смена сети/краткая ICE failure имеют ограниченное recovery окно. После него звонок завершается корректно, освобождает tracks/camera/microphone и снимает active state.
**CALL-010.** Закрытие экрана звонка не может оставить скрыто включённый microphone/camera вопреки показанному состоянию. Background behavior задаётся интеграцией платформы, не визуальным состоянием окна.
### 19.1. История
**CALL-011.** Каждый звонок создаёт одну history card: направление, media type, начало, duration и outcome.
Outcomes: completed, declined, missed, cancelled, unanswered, failed, answeredElsewhere. Если media было connected, последующая transport error не переименовывает состоявшийся разговор в неудачную попытку; duration сохраняется.
Классификация до connected: отказ стороны → declined; собственная отмена → cancelled; timeout incoming ringing → missed; timeout outgoing ringing → unanswered; сбой negotiation → failed; подтверждённый ответ другим собственным устройством → answeredElsewhere.
**CALL-012.** Missed incoming увеличивает unread. Дубликаты completion callbacks не создают несколько карточек.
**CALL-013.** Call history синхронизируется с собственными устройствами через совместимое расширение и MAM/Carbons, если сервер хранит такие события. Metadata не выдаётся за OMEMO body.
### 19.2. Baseline таймеров
| Фаза | Значение |
| --- | --- |
| Ringing | 60 с |
| Negotiation | 90 с |
| ICE failed grace | 8 с |
| ICE disconnected grace | 12 с |
Таймеры должны согласовываться с сервером и remote signaling; они не гарантируют фонового пробуждения.
## 20. Уведомления, push и фоновая работа
**PUSH-001.** Поддерживаются локальные уведомления для новых событий активного клиента и отдельная push-интеграция для разрешённых платформенных состояний.
**PUSH-002.** Notification создаётся для нового incoming, не для outgoing/повторного MAM. В активно просматриваемом чате дублирующий banner подавляется.
**PUSH-003.** Уведомление содержит безопасную идентичность account/conversation/event для перехода в правильный контекст. Повторная доставка push и network event дедуплицируется.
**PUSH-004.** Default privacy: push payload не содержит plaintext body, file key или credentials. Preview после получения сообщения определяется пользовательской настройкой; при app lock default preview скрыт.
**PUSH-005.** Gateway/registration lifecycle включает register/update token, enable по XMPP, disable/logout/revoke и обработку expired token. Наличие server push feature без registration не отображается как «Push работает».
**PUSH-006.** Push — сигнал получения/пробуждения, а не источник истины истории. После него клиент синхронизируется с сервером; утерянный push не должен навсегда потерять сообщение.
**PUSH-007.** Базовый single-active-account режим регистрирует доставку для активного аккаунта; переключение согласует старую и новую registration. Уведомления неактивных аккаунтов не обещаются без отдельно реализованного multiaccount background режима. Поздний push сохраняет исходную account identity и не открывает переписку другого аккаунта.
### 20.1. Web
**WEB-001.** Для уведомлений вне открытой страницы поставляется поддержанный Web Push/service worker путь и gateway, если браузер/пользователь предоставили возможность. При отсутствии поддержки/permission приложение честно показывает статус «Только при открытом приложении».
**WEB-002.** Закрытая страница не считается постоянно подключённой XMPP-сессией. Service worker notification не гарантирует непрерывный microphone, медиазвонок или автоматический показ полноэкранного incoming UI.
**WEB-003.** Incoming push может уведомить о вызове и открыть приложение по действию. После открытия проверяются актуальность и состояние вызова; истёкший вызов не «принимается» из старого notification.
**WEB-004.** Несколько вкладок координируют активное соединение/device state либо используют изолированные ресурсы с безопасным доступом к crypto state. Не допускаются конкурентные повреждающие записи ratchet sessions.
### 20.2. Desktop
**DESK-001.** Пользователь понимает разницу «закрыть окно», «скрыть/свернуть» и «выйти из приложения». Работа в tray/фоновой сессии и автозапуск — явно управляемые настройки.
**DESK-002.** При живом фоновом процессе поддерживаются сообщения и incoming calls. Полностью завершённый процесс не получает обещание гарантированного пробуждения без отдельного системного механизма.
**DESK-003.** Sleep/wake, network change и logout восстанавливают/отключают соединение и push registration корректно. Tray icon не показывает active call после завершения.
### 20.3. Android
**AND-001.** Поставляется интеграция системной доставки уведомлений и входящих вызовов для утверждённого режима устройства. Выбор push provider и доступность устройств без соответствующего сервиса фиксируются в паспорте релиза.
**AND-002.** Фоновая работа соответствует lifecycle/permissions ОС. Постоянный socket в обычном скрытом UI не используется как единственная гарантия доставки.
**AND-003.** Для ongoing call используется разрешённый системный foreground/call механизм с видимым статусом; control из notification согласован с app state.
**AND-004.** Force stop, отозванные разрешения, ограничения батареи и отключённая сеть имеют задокументированную деградацию. Приложение не обещает обход пользовательского запрета ОС.
### 20.4. Приёмка интеграции
Push и фоновые звонки нельзя принять только по работающему foreground demo. Для каждой платформы проверяются foreground, скрытый UI, закрытый процесс/вкладка, denied permission, sleep, token rotation, logout и переход по старому notification. Недоступные платформой режимы обозначаются как ограничения, а не как выполненная гарантия доставки.
## 21. Локальная защита и хранение
**STORE-001.** История, crypto state и настройки раздельны по accountId; операции сохранения транзакционны либо имеют проверяемый equivalent recovery механизм.
**STORE-002.** Native credentials защищены системным secure storage; private key material защищается подходящим устройству key wrapping/access control. Пароль не хранится в открытом preferences файле.
**STORE-003.** В Web пароль не хранится plaintext в localStorage, URLs, analytics или общедоступных configuration objects. Persistent login и разблокировка crypto state используют отдельное задокументированное защищённое решение; отсутствие такого решения означает повторный вход, а не незаметное ослабление.
**STORE-004.** Web storage может иметь квоты/очистку. Приложение обрабатывает quota/exhaustion и показывает, когда история не сохранена устойчиво. Browser storage не представляется эквивалентом hardware-backed native vault.
**STORE-005.** Media cache имеет лимит и eviction. Draft/failed payload не исчезает без объяснения; при невозможном retry действие отключено с причиной.
**STORE-006.** Автосохранение не теряет введённый текст при переходах/обычном восстановлении. Draft принадлежит account + conversation; он не переносится в другой чат из-за reuse UI.
**STORE-007.** Миграции versioned и тестируются на старых данных/сбоях. Ошибка открытия persistent store показывает ограниченный режим, а не ложное подтверждение durable записи.
**LOCK-001.** App lock скрывает всю sensitive UI: чат, media, call details и previews. Поддерживаются пользовательский passcode и доступная системная биометрия.
**LOCK-002.** Минимальная длина passcode 4 символа, предусмотрены ограничение частоты ошибочных попыток и корректное хранение verifier/секрета. App lock не объявляется шифрованием базы без отдельного encryption механизма.
**LOCK-003.** После запуска и ухода в background lock применяется по настройке. Краткая потеря focus для системного picker/permission dialog не прерывает незавершённую операцию.
**LOCK-004.** Web не обещает native biometric unlock на любом браузере; при отсутствии защищённой интеграции остаётся passcode либо повторная авторизация. Lock settings показывают фактическую возможность.
## 22. Приватность и безопасность
**SEC-001.** Документируется разделение transport TLS, OMEMO payload, file encryption, DTLS-SRTP media, local protection и UI lock. Термины не подменяют друг друга.
**SEC-002.** Server/upload/STUN/TURN/maps/push providers получают только необходимые данные. Перечень providers и metadata доступен пользователю.
**SEC-003.** Логи исключают passwords, tokens, private/session keys, decrypted bodies и file key fragments. Сетевые трассы по умолчанию обезличены; диагностический экспорт требует явного действия и предупреждения о metadata.
**SEC-004.** Полученные text/filenames/URLs/XML не выполняются как HTML/script/команда. Web rendering безопасен для XSS; переход по внешним URL не передаёт секретный контекст.
**SEC-005.** У ограничений входных данных есть предельный размер/depth/count. Недоверенные stanza, challenges, media metadata и custom sync parsers не должны создавать бесконечную работу/выделение памяти.
**SEC-006.** Copy/export явно выводит plaintext за границы клиента; локальная очистка не обещает удалить clipboard или пользовательский файл.
**SEC-007.** «Удалить у всех» — протокольный отзыв; «Удалить локальные данные» — удаление собственного профиля. В UI нет обещания абсолютного стирания всех копий.
**SEC-008.** Для push/служебной own-device синхронизации проверяется источник и принадлежность аккаунту. Remote peer не может подделать собственную call/read history только прислав похожий XML element.
## 23. Совместимость протоколов
Обязательный профиль включает:
| Область | Протокол/семантика |
| --- | --- |
| XMPP | Базовые stream/session, roster, presence; XEP-0030 discovery |
| Web transport | Защищённый XMPP WebSocket endpoint |
| Устойчивость | XEP-0198 Stream Management, XEP-0199 Ping, XEP-0280 Carbons, XEP-0352 CSI |
| Группы | XEP-0045 MUC, invitations/room configuration и MUC-MAM |
| История | XEP-0313 MAM, XEP-0059 RSM, XEP-0359 origin-id/stanza-id |
| Доставка | XEP-0184 Receipts и XEP-0333 Chat Markers |
| Действия | XEP-0308 Correction, XEP-0461 Reply, XEP-0428 Fallback, XEP-0444 Reactions, XEP-0424 Retraction |
| Медиа | XEP-0363 HTTP Upload, XEP-0066 OOB, XEP-0454 encrypted file URI |
| Защита | XEP-0384: совместимые legacy/OMEMO 2; XEP-0420 SCE для нового формата |
| Аватары | XEP-0084 User Avatar PEP, optional legacy compatibility |
| Звонки | XEP-0166/0167 Jingle/RTP, XEP-0353 JMI, ICE/DTLS-SRTP, XEP-0215 extdisco |
| Push | XEP-0357 registration и платформенная доставка |
### 23.1. Own-device расширения
Для совместимости устройств Luma используются согласованные пространства имён:
- `https://luma.chat/read-marker`: conversation JID, target origin/client id, optional stanza id и timestamp; без видимого body;
- `https://luma.chat/call-history`: peer JID, direction, outcome, audio/video, startedAt, duration и устойчивый event id.
Wire names read-marker: `conversation`, `id`, optional `stanza`, `when`; call-history: `with`, `direction`, `status`, `video`, `start`, `duration`. Даты сериализуются ISO 8601 с timezone, duration — неотрицательные секунды, ids — непрозрачные строки. Origin-id внешней stanza служит устойчивой идентичностью события. Отсутствие неизвестной новой версии не должно ломать ранее поддержанный payload.
Схемы versioned при изменении; неизвестные поля игнорируются безопасно, обязательные валидируются. Повторная доставка идемпотентна. Совместимость с существующими payload не ломается без миграции. Эти расширения не считаются универсально поддерживаемыми всеми XMPP-клиентами.
### 23.2. Матрица испытаний
Релиз подтверждает обмен с минимум двумя независимыми XMPP-клиентами из утверждённого набора и минимум одним поддерживаемым сервером. Отдельно проверяются legacy/OMEMO 2, direct/MUC, media и calls. Поддержка XEP по имени без сценариев interoperability недостаточна.
## 24. Нефункциональные требования
**NFR-001.** UI Kit является обязательным стандартом visual/interaction behavior. Small/wide layout определяется доступной шириной, не строкой User-Agent.
**NFR-002.** Списки/лента виртуализируются или используют equivalent bounded rendering. Большой архив не создаёт все визуальные rows одновременно.
**NFR-003.** Проект задаёт измеряемые бюджеты startup, RAM, cache и frame responsiveness для утверждённых reference devices. До утверждения devices не заявляется произвольный универсальный SLA.
**NFR-004.** Encryption/decoding/uploads уступают ресурсы active input/capture. Scroll и text entry остаются доступными во время catch-up.
**NFR-005.** Системные permissions запрашиваются по контексту. Denied, hardware missing и revoked during session входят в тестовую матрицу.
**NFR-006.** Web/Desktop полностью управляются клавиатурой; Android поддерживает системную accessibility. Focus, text scale, reduced motion и contrast проверяются по UI Kit.
**NFR-007.** Сборки воспроизводимы: pinned/locked dependencies, migrations, build instructions, CI, license notices и release artifacts. Выбор framework не включён в пользовательские требования.
**NFR-008.** CI запускает unit/integration checks и smoke UI tests для поставляемых targets. Подпись/публикация и secrets отделены от публичного репозитория.
**NFR-009.** Crash/error reports не включают sensitive content по умолчанию. Телеметрия имеет утверждённую privacy-конфигурацию.
## 25. Критерии приёмки
Каждый сценарий выполняется на каждой заявленной платформе либо имеет заранее согласованную аппаратную/системную деградацию. «Не удалось проверить» не равнозначно «Выполнено».
| Сценарий | Ожидаемый результат | Требования |
| --- | --- | --- |
| Первый вход/ошибка/перезапуск | Верный lifecycle, история сохранена, secrets не раскрыты | ACC-001…010 |
| Нечисловой port / WSS endpoint | Понятная validation и правильный транспорт | ACC-003…005 |
| Переключение во время upload | Нет отправки/записи от другого аккаунта | ACC-008,014 |
| Logout с удалением/сохранением | Объяснённая область очистки и push disable | ACC-011…013 |
| TLS/auth отказ | Нет ослабления доверия или подделанного PLUS | NET-001…005 |
| Смена сети/sleep/reconnect | Один согласованный клиент, без duplicate | NET-006…009 |
| Text/receipts/displayed | sending/sent/delivered/read имеют разные основания | MSG-001…005 |
| Replay live/Carbons/MAM | Одна запись, правильные ids/delivery | MSG-004,005 |
| Draft переход между чатами | Текст остаётся у правильного recipient/account | STORE-006 |
| Edit/retract до оригинала | Отложенное корректное применение | EDIT-003, DEL-003 |
| Чужая correction/retraction | Не изменяет чужое сообщение | EDIT-002, SEC-008 |
| Reply XML/legacy/MUC | Корректный preview/reference без вымышленного id | REPLY-001…005 |
| Multi-forward и ошибка | Порядок, новая encryption policy, partial result | FWD-001…004 |
| Local delete + MAM | Удалённое не возвращается | DEL-001 |
| Реакция/снятие/старое событие | Count/own state и порядок актуальны | REACT-001…005 |
| Typing timeout/отключение | Нет вечного composing | TYPE-001…003 |
| Group hidden recipients | Fail closed, без plaintext | MUC-003…006 |
| OMEMO2/legacy/changed identity | Совместимый формат, правильное доверие | ENC-003…009 |
| Upload HTTPS/redirect/error | Secret-safe transport и bounded retry | FILE-004…012 |
| 20 media, partial failure, caption | Ошибки не уничтожают успешное/оставшееся | FILE-002,009 |
| Viewer/player/export cancel | Correct ratio/control и отсутствие ложного success | MEDIA-001…008 |
| Permissions/short record/early release | Нет скрытой capture и autosend partial | REC-001…006 |
| Location denied/manual | Точка отправляется без фонового tracking | GEO-001…004 |
| Fresh MAM/repeating cursor/disk failure | Checkpoint не теряется/не перескакивает | HIST-002…012 |
| JMI/full JID/SDP/late ICE | Correct endpoint и ограниченный recovery | CALL-004…009 |
| Call outcome/sync/duplicate callback | Одна правильная history card | CALL-011…013 |
| Push и network event вместе | Одна notification/event, правильный чат | PUSH-001…006 |
| Web вкладка закрыта / stale call | Notification/open по доступной платформе, без ложного active call | WEB-001…004 |
| Desktop скрыт/завершён/sleep | Поведение соответствует заявленному lifecycle | DESK-001…003 |
| Android background/permission/force stop | Проверенная доставка и честная деградация | AND-001…004 |
| Lock / preview / export | Sensitive UI скрыта, границы защиты объяснены | LOCK-001…004, SEC-006 |
| Quota/migration/cache eviction | Нет ложного durable success/повреждения | STORE-001…007 |
| Keyboard/text scale/dark/reduced motion | Доступный интерфейс | NFR-006, UI Kit |
## 26. Состав результата разработки
1. Устанавливаемое/развёрнутое приложение каждого согласованного target.
2. Исходники, locked dependencies, reproducible build и CI.
3. Реализованный набор UI components/tokens по `uikit_clear.md`.
4. Версионированные data schemas/migrations и протокольные adapters.
5. Push/gateway/TURN/WSS конфигурация в пределах выбранной инфраструктуры, без secrets в репозитории.
6. Unit, integration, interoperability и UI/accessibility tests с результатами.
7. Пользовательская справка, privacy/security описание и известные ограничения.
8. Паспорт релиза и checklist поддерживаемых систем.
## 27. Паспорт конкретного приложения
До начала platform-specific реализации фиксируются решения, не изменяющие пользовательский контракт:
| Решение | Что должно быть указано |
| --- | --- |
| Поддерживаемые targets | Desktop ОС, Android min/target, Web браузеры и режим PWA |
| Технологии | UI/runtime, XMPP/crypto/media libraries и стратегия shared code |
| Транспорт | Native discovery и Web endpoint configuration |
| Backend | WSS, push gateway/provider, TURN, upload и maps |
| Secrets | Native vault, Web persistent-login/crypto unlock модель |
| Lifecycle | Что означает закрыть окно/вкладку, background и explicit quit |
| Лимиты | Размеры файлов/cache, pending limits, performance reference devices |
| Совместимость | Серверы/клиенты и редакции encrypted wire formats |
| Дистрибуция | Signing, обновления, hosting и каналы поставки |
Паспорт заполняется до выпуска. Отсутствующее решение не заменяется непроверенным обещанием общей поддержки всех ОС, браузеров или фоновых состояний.
+613
View File
@@ -0,0 +1,613 @@
# Luma UI Kit — единый стандарт Web, Desktop и Android
Версия: 1.0. Дата: 14 октября 2026 года. Статус: целевой стандарт интерфейса.
Связанный документ: [техническое задание](tz_clear.md).
## Содержание
- [1. Назначение](#1-назначение)
- [2. Дизайн-принципы](#2-дизайн-принципы)
- [3. Единицы и токены](#3-единицы-и-токены)
- [4. Палитра](#4-палитра)
- [5. Типографика](#5-типографика)
- [6. Отступы, размеры и формы](#6-отступы-размеры-и-формы)
- [7. Поверхности, границы и elevation](#7-поверхности-границы-и-elevation)
- [8. Адаптивная компоновка](#8-адаптивная-компоновка)
- [9. Иконки](#9-иконки)
- [10. Компонент Avatar](#10-компонент-avatar)
- [11. Компонент ConversationRow](#11-компонент-conversationrow)
- [12. SearchField](#12-searchfield)
- [13. MessageRow и Bubble](#13-messagerow-и-bubble)
- [14. CompactReply и ThreadFocus](#14-compactreply-и-threadfocus)
- [15. ReactionChip и EmojiPicker](#15-reactionchip-и-emojipicker)
- [16. Composer](#16-composer)
- [17. MediaIsland и viewer](#17-mediaisland-и-viewer)
- [18. AttachmentPreview](#18-attachmentpreview)
- [19. LocationPicker](#19-locationpicker)
- [20. CallScene и CallHistoryCard](#20-callscene-и-callhistorycard)
- [21. Auth, Settings, Encryption и Lock](#21-auth-settings-encryption-и-lock)
- [22. Feedback, dialogs и слои](#22-feedback-dialogs-и-слои)
- [23. Анимации и жесты](#23-анимации-и-жесты)
- [24. Accessibility и ввод](#24-accessibility-и-ввод)
- [25. Каталог состояний и приёмка UI](#25-каталог-состояний-и-приёмка-ui)
- [26. Развитие стандарта](#26-развитие-стандарта)
## 1. Назначение
UI Kit определяет визуальный язык, токены, компоненты, состояния и адаптацию Luma для Web, Desktop и Android. Документ применяется независимо от UI framework и не требует конкретной реализации нативных материалов, шрифтов или системных иконок.
Цель — узнаваемый продукт с одинаковой иерархией и понятным поведением на разных устройствах. Совпадение физического пикселя между ОС не требуется; соответствие ролям, пропорциям и состояниям обязательно.
Документ нормативный: значения токенов реализуются централизованно, компоненты показываются в отдельном каталоге состояний. Примеры токенов и размеров не обозначают уже написанную библиотеку.
## 2. Дизайн-принципы
**UI-001.** Контент и общение находятся на первом плане. Декор не конкурирует с текстом, avatar и основным действием.
**UI-002.** Основной accent — синий. Outgoing сообщения синие, incoming нейтральные. Направление различается также выравниванием, а состояние — иконкой/подписью.
**UI-003.** Элементы округлые, поверхности спокойные, navigation и forms следуют привычкам платформы. Нет обязательного копирования внешнего вида одной ОС на другую.
**UI-004.** Полупрозрачность поддерживает иерархию, но не ухудшает чтение. При отсутствии blur/пониженной прозрачности используется определённый opaque fallback.
**UI-005.** Состояния loading/empty/error/disabled/selected являются частью компонента, а не отдельной импровизацией каждого экрана.
**UI-006.** Жест ускоряет действие, но для значимого действия есть видимый/доступный альтернативный control.
**UI-007.** Пользовательские строки — русские, короткие и точные. «Отправлено», «Доставлено» и «Прочитано» не заменяют друг друга.
**UI-008.** Маленькое окно, клавиатура, увеличение текста и reduced motion поддерживаются системно, а не исправляются после завершения дизайна.
## 3. Единицы и токены
| Величина | Web | Android | Desktop |
| --- | --- | --- | --- |
| Layout unit `u` | CSS px при стандартном масштабе | dp | Логическая единица UI, independent от device pixels |
| Размер текста | rem/em с role mapping | sp с font scale | Системный scaling / logical font size |
| Border | 1 CSS px или адаптированный hairline | 1 dp по роли | Логическая линия с корректным pixel snapping |
| Анимация | ms | ms | ms |
| Цвет | sRGB HEX + alpha | Эквивалент sRGB | Эквивалент sRGB |
При изменении device pixel ratio компонент не должен физически уменьшаться. Растровые assets поставляются в нужных масштабах; logo и функциональные icons предпочтительно в vector.
### 3.1. Обязательные группы токенов
`color.*`, `type.*`, `space.*`, `radius.*`, `size.*`, `border.*`, `shadow.*`, `motion.*`, `layer.*`, `breakpoint.*`.
Каждый токен имеет light/dark значение или единое значение, назначение и допустимое применение. Локальная константа не заменяет существующий токен тем же смыслом.
## 4. Палитра
### 4.1. Semantic colors
| Токен | Light | Dark | Назначение |
| --- | --- | --- | --- |
| `color.brand.accent` | #248FF5 | #68B5FF | Выбор, active icon, ссылка по проверенному контрасту |
| `color.action.fill` | #0969C8 | #0969C8 | Filled primary button с белым текстом |
| `color.action.onFill` | #FFFFFF | #FFFFFF | Текст primary action |
| `color.text.primary` | #172B40 | #F3F6FA | Основной текст |
| `color.text.secondary` | #526174 | #B8C3CF | Вторичный текст |
| `color.text.muted` | #5E6C7E | #A7B4C4 | Метаданные при достаточном контрасте |
| `color.surface.page` | #F4F7FA | #101821 | Фон приложения |
| `color.surface.base` | #FFFFFF | #18232F | Основные панели |
| `color.surface.raised` | #FFFFFF | #213040 | Dialog/card/popover |
| `color.surface.incoming` | #E7ECF2 | #293847 | Incoming bubble |
| `color.border.subtle` | #D7DFE8 | #3C4B5D | Divider/декоративная рамка |
| `color.border.control` | #7A899A | #8794A3 | Значимая граница control |
| `color.state.success` | #16713B | #6DDA91 | Успех/online/verified |
| `color.state.successFill` | #16713B | #16713B | Filled accept/success control, белый текст |
| `color.state.warning` | #8A4B00 | #FFC46B | Plaintext/предупреждение |
| `color.state.error` | #B4232F | #FF9199 | Ошибка/destructive text |
| `color.state.errorFill` | #B4232F | #B4232F | Destructive filled control |
| `color.state.onErrorFill` | #FFFFFF | #FFFFFF | Текст destructive filled control |
| `color.focus.ring` | #0969C8 | #8BC6FF | Keyboard focus |
| `color.overlay.scrim` | #000000 / 0.52 | #000000 / 0.64 | Модальная подложка |
Brand accent используется как цвет бренда; это не разрешение печатать мелкий белый текст на любой голубой поверхности. Filled actions используют `action.fill`. Для ссылок на светлом фоне применяется достаточно тёмный `action.fill` либо другой проверенный role token.
### 4.2. Сообщения и градиенты
| Токен | Значение | Назначение |
| --- | --- | --- |
| `gradient.message.outgoing` | #0969C8 → #0874BA | Outgoing text/audio/file bubble |
| `color.message.onOutgoing` | #FFFFFF | Основной текст outgoing |
| `color.message.outgoingMetadata` | Основной secondary вне bubble | Дата/status за пределами синей поверхности |
| `gradient.brand.hero` | #1461C7 → #1F9EF5 → #70D6F5 | Login/lock background, декоративная область |
| `gradient.media.video` | #0F2B4A → #176EA3 | Video placeholder |
| `gradient.media.videoNote` | #144D8F → #12A3D4 | VideoNote placeholder |
| `gradient.call` | #1C2B3D → #000000 | Call scene |
Направление градиентов — от верхнего левого угла к нижнему правому. Для текстовых bubbles используется тёмный сине-голубой градиент, чтобы белый body оставался читаемым; более яркий hero gradient остаётся декоративным.
Для обычного белого текста расчётный контраст с концами outgoing gradient — около 5.43:1 и 4.98:1. Контраст промежуточных цветов и всех фактических состояний проверяется при реализации. Полупрозрачный secondary text внутри bubble не должен снижать контраст ниже целевого порога.
### 4.3. Аватары и имена участников
Fallback avatar получает стабильный цвет по нормализованной identity. Цвет не меняется при rerender/перезапуске. Palette должна обеспечивать читаемый белый или тёмный foreground; случайный hue не используется без выбора контрастного текста.
Имя автора MUC может иметь устойчивый цвет, но не является единственным признаком identity. Имя остаётся читаемым в обеих темах. Цвет для имени и фон аватара могут отличаться по насыщенности.
### 4.4. Запрещённые применения
- Новый близкий синий для той же роли без добавления token.
- Белая мелкая подпись на ярком accent без проверки контраста.
- Ошибка/online/selected, различимые только цветом.
- Hardcoded light background с унаследованным dark foreground из другой темы.
- Прозрачный control без определённого фона/границы/focus state.
## 5. Типографика
Используется системный sans-serif шрифт платформы. Единая роль важнее одинакового имени font family. Fallback поддерживает кириллицу, emoji и символы JID. Mono нужен для fingerprint, не для всего интерфейса.
| Токен | Размер / line height | Weight | Применение |
| --- | --- | --- | --- |
| `type.brand` | 42 / 50 | 700 | Название на login |
| `type.hero` | 30 / 38 | 600 | Имя звонящего |
| `type.title` | 22 / 28 | 700 | Крупный заголовок формы/dialog |
| `type.heading` | 17 / 24 | 600 | Имя списка, toolbar title |
| `type.body` | 16 / 24 | 400 | Сообщение, поле, основной текст |
| `type.bodyStrong` | 16 / 24 | 600 | Выделенный текст/primary label |
| `type.subbody` | 14 / 20 | 400 | Preview списка, вторичные поля |
| `type.caption` | 12 / 16 | 400/500 | Метаданные, пояснения |
| `type.badge` | 12 / 16 | 700 | Unread/count |
| `type.mono` | 12 / 18 | 400 | Fingerprint/техническое значение |
Размеры масштабируются пользовательскими настройками; фиксированная высота row не должна обрезать увеличенный текст. Desktop compact density может уменьшать вторичный текст до 13/18, но основной message body не становится мелкой служебной подписью.
Таймеры/duration используют tabular numerals. Link/упоминания различимы, доступны клавиатуре и не ломают line wrapping.
### 5.1. Ограничение строк
- Имя и preview списка: 1 строка при обычном масштабе; при большом тексте допустим адаптивный рост.
- Имя звонящего: до 2 строк.
- Toast: до 2 строк, дальнейшее сообщение — dialog/inline error.
- Compact reply: 1 строка и ellipsis.
- Message body: без произвольного line clamp; длинный контент может иметь доступное «Показать полностью» только при отдельном согласованном лимите.
- JID/filename/fingerprint: безопасный wrap/ellipsis с возможностью прочитать/скопировать полное значение.
## 6. Отступы, размеры и формы
### 6.1. Spacing scale
| Токен | u |
| --- | --- |
| `space.1` | 4 |
| `space.2` | 8 |
| `space.3` | 12 |
| `space.4` | 16 |
| `space.5` | 20 |
| `space.6` | 24 |
| `space.7` | 32 |
| `space.8` | 40 |
| `space.9` | 48 |
Компонентные исключения допустимы для оптического выравнивания: bubble padding 13×9, input 14/12/9, metadata gap 4. Они именуются внутри компонента, не создают бессистемный глобальный scale.
### 6.2. Радиусы
| Токен | u | Применение |
| --- | --- | --- |
| `radius.small` | 6 | Reply thumbnail |
| `radius.control` | 12 | Компактная карточка/системный fallback |
| `radius.field` | 16 | Caption field |
| `radius.reply` | 17 | Compact reply |
| `radius.media` | 20 | Photo/video island |
| `radius.composer` | 21 | Поле composer |
| `radius.dialog` | 28 | Auth/dialog card |
| `radius.pill` | Половина высоты | Badge/chip/toast |
| `radius.circle` | 50% | Avatar/round control |
Message bubble имеет отдельную directional geometry: основное скругление 20, нижняя асимметрия/небольшой хвост на стороне отправителя с выступом около 4 u. Он не должен уменьшать внутреннюю область текста. Vector/path реализации обеих направлений являются зеркальными вариантами одного компонента.
Фото/видео не получают текстовый хвостик: у них самостоятельная островковая рамка.
### 6.3. Контролы
| Роль | Visual size | Hit area |
| --- | --- | --- |
| Composer send/attach/record | 42×42 | Touch min 48×48; pointer min 44×44 |
| Audio play | 44×44 | Touch min 48×48 |
| Standard icon button | 24 icon в 40/44 visual | Touch min 48×48 |
| Primary form action | Height min 48 | Вся кнопка |
| Call control | 58×58 circle | Не меньше visual size |
| Unread badge | min height 22 | Не отдельная обязательная target |
| Input | min height 44, composer visual min 42 | Не обрезается при scale |
Увеличение hit area не должно перекрывать target соседней кнопки. При узкой ширине gap и общий layout перераспределяются.
## 7. Поверхности, границы и elevation
| Уровень | Поверхность | Применение |
| --- | --- | --- |
| Page | page | Фон, timeline |
| Base | base | Sidebar, forms, основной контент |
| Raised | raised + border/elevation | Popover, dialog, floating panel |
| Glass | Полупрозрачная base surface + blur | Search/composer/toolbars |
| Inverse | Тёмная сцена + контрастный текст | Media viewer/call |
Glass light fallback: base opaque. Glass dark fallback: base opaque. При blur фон имеет достаточно непрозрачный слой для текста; blur не единственный источник читаемости. Reduced transparency отключает blur и сохраняет border.
Shadows:
- `shadow.floating`: black alpha 0.14, blur 24, offsetY 12 в light; более мягкий/ограниченный вариант в dark;
- `shadow.toast`: black alpha 0.18, blur 12, offsetY 4;
- `shadow.callPreview`: black alpha 0.38, blur 16, offsetY 8.
List row не получает отдельную тяжёлую тень. Border subtle декоративный; значимые focus/control boundaries используют более контрастный role token.
## 8. Адаптивная компоновка
### 8.1. Breakpoints
Это продуктовые диапазоны доступной ширины, а не определение типа устройства.
| Диапазон | Ширина | Layout |
| --- | --- | --- |
| Compact | <720 u | Одна основная панель, переход список → чат |
| Medium | 720…1099 u | Sidebar + detail при достаточной полезной ширине |
| Wide | ≥1100 u | Sidebar + detail, расширенная зона контента |
В medium split включается только если после sidebar остаётся минимум 400 u для detail; иначе compact. Приложение работает при viewport 320 u через reflow; специальные фиксированные размеры уменьшаются до доступной ширины.
### 8.2. Sidebar и detail
Sidebar min 240, preferred 290, max 380. Default desktop window ориентир 1080×720, рекомендуемый minimum 840×560; при фактически меньшем окне допускается compact layout, а не обрезанный недоступный контент.
Detail растёт с окном. Максимальная читаемая ширина обычного text bubble — 640 u и не более 78% доступной message row. Очень короткий текст не растягивается до максимума. Timeline может иметь центрированный content column с max 1000 u.
### 8.3. Platform shell
| Платформа/режим | Navigation |
| --- | --- |
| Android compact | Нижняя навигация: Контакты / Звонки / Чаты / Настройки |
| Mobile Web | Аналогичная compact bottom navigation, с учётом browser safe areas |
| Desktop/Web wide | Sidebar navigation/list + выбранный detail |
| Android large screen | Adaptive split/rail по доступной ширине |
Внутри compact чата нижняя глобальная навигация скрыта, Back возвращает к списку. Search остаётся сверху соответствующего списка. Keyboard/IME не перекрывает composer; изменение viewport не сбрасывает draft.
## 9. Иконки
Единый набор outline/filled vector icons выбирается для каждой платформы с согласованной семантикой. Использование конкретного proprietary symbol set не требуется. Толщина, оптический размер и baseline согласованы в каталоге.
| Роль | Графическая семантика |
| --- | --- |
| Контакты | Человек/контакты |
| Звонки | Телефон |
| Чаты | Два сообщения |
| Настройки | Шестерёнка |
| Новый чат | Карандаш/новое сообщение |
| Новая группа | Несколько людей |
| Поиск/очистить | Лупа/крестик в круге |
| Вложения | Плюс |
| Отправить | Стрелка вверх |
| Ответить/переслать | Стрелка влево/вправо с поворотом |
| OMEMO/plaintext | Закрытый/открытый замок |
| Decryption error | Щит с предупреждением |
| Retry | Круговая стрелка |
| Сохранить | Стрелка вниз/скачивание |
| Завершить звонок | Опущенная трубка |
| Turn camera | Камера с круговыми стрелками |
Обычный icon size 20/24 u; metadata 12/14. Иконка без текста имеет accessible name и tooltip на pointer, если смысл не очевиден. Emoji не заменяет функциональную пиктограмму кнопки.
## 10. Компонент Avatar
**UI-AVATAR.** Circle, photo fill с crop, fallback — initials/group icon и стабильный фон. Не смешивать photo fit с avatar fill: фотографии сообщений не обрезаются как avatar.
| Вариант | Размер |
| --- | --- |
| List | 50×50 |
| Profile | 64×64 |
| Call hero | 132×132 |
| Compact inline | 32×32 при необходимом контексте |
Initials около 35% diameter, semibold; group icon около 31%. Status dot bottom-end, diameter 25% avatar, border 2.5 u цветом окружающей поверхности. Direct dot означает online, group — joined. Accessible label включает имя и статус. Offline не рисуется ложным зелёным dot.
Состояния: photo, fallback, loading cached image, online/joined, offline, image error. Broken image не создаёт пустой невидимый avatar.
## 11. Компонент ConversationRow
Композиция: avatar → текстовая колонка → trailing time/unread. Gap avatar/text 12. Внешние insets horizontal 12, vertical 8; высота определяется содержимым, ориентир 72 u.
- Имя — heading.
- Preview — subbody secondary.
- Время — caption secondary.
- Межстрочный gap 4/5.
- Иконка group/encryption рядом с preview, не вместо имени.
- Badge count >0, white on action.fill, padding 7×3, min width 22.
- В основном списке значение выше 99 показывается «99+»; accessible name сообщает фактическое число.
- Multiaccount подпись — caption secondary, только когда нужно различать контекст.
Состояния: default, hover на pointer, pressed, keyboard focus, selected wide-panel, unread, online, muted при наличии функции, loading/empty отдельно от row.
Selected row: accent-tinted surface и ненавязчивый маркер/фон; hover слабее selected. Focus ring не исчезает на selected. Baseline focus outline 2 u с offset 2 u; при недостаточном контрасте используется двойная граница/контрастный separating layer.
## 12. SearchField
Поле располагается над фильтруемым списком. Height min 40 visual/48 touch target, radius pill либо control по platform shell. Horizontal inset 12, vertical 8, icon/text gap 8, outer margin 12.
Leading search icon, placeholder «Поиск чатов»/«Поиск контактов»/«Поиск звонков». Clear button появляется при непустом запросе. Native input behaviors сохраняются: selection, keyboard focus, paste и Escape очистки/закрытия по контексту.
Состояния: empty, filled, focused, disabled только при реальной невозможности, no results через отдельный empty state. Search field не обещает поиск message body, если реализована фильтрация списка.
## 13. MessageRow и Bubble
**UI-MESSAGE.** Incoming слева, outgoing справа. Outer horizontal inset 12, row gap 8. Opposite minimum space 54 для текста и 16 для media, с уменьшением в узком viewport. Body wrapping не расширяет страницу горизонтально.
Message stack:
1. optional group sender name;
2. optional compact source reply;
3. body/media;
4. reaction chips;
5. metadata.
### 13.1. Body
Text/file padding horizontal 13, vertical 9. Audio/voice — 13×10. Outgoing gradient + white; incoming neutral surface + primary. Body type.body.
Plaintext/decryption warning расположен в metadata и доступен screen reader. Не добавлять замок каждому слову или watermark поверх читаемого текста.
Text bubble использует общую directional rounded shape; media островок имеет независимый radius 20. Long URL/word безопасно переносится; copy возвращает исходный текст, а не строку с декоративными разрывами.
### 13.2. Metadata
Caption, gap 4, horizontal inset 4, вне основного colored body. Время hour:minute, marker «изм.», security warning и outgoing status.
| Состояние | Icon/подпись |
| --- | --- |
| Sending | Часы / «Отправка…» |
| Sent | Галочка / «Отправлено» |
| Delivered | Отличимая delivery-галочка / «Доставлено» |
| Read | Заполненный/двойной read marker / «Прочитано» |
| Failed | Warning red / «Не отправлено», доступен retry по ТЗ |
На compact можно оставить icon и доступный подробный label, на focus/hover показывать расшифровку. Статусы не различаются только tint.
### 13.3. Tombstone
Иконка удаления + «Сообщение удалено», subdued text/italic по доступности. Положение исходного сообщения сохраняется, прежние media/reaction/reply content не видны. Tombstone не приглашает к запрещённым edit/forward.
### 13.4. Selection и context menu
Touch: long press открывает actions; pointer: context click/явное menu; keyboard: доступная menu action. В selection mode видны checkmarks/count и панель массовых действий. Swipe reply отключён.
Список actions определяется ТЗ. Недоступное действие скрывается либо disabled с причиной, но не выполняет частично другую операцию под тем же названием.
## 14. CompactReply и ThreadFocus
Compact source bubble: max width 230, min height 34, radius 17, vertical padding 7, horizontal 10…12. Text subbody medium, one line, optional thumbnail 27×27/radius 6. Тонкий border 1.25 u; фон приглушённый.
Отступ/дуга отражают направление исходного сообщения. Curve width 58/height 34, stroke 2.4 round caps, horizontal offset около 18. Нельзя снижать opacity всего текста ниже readable contrast; muted effect достигается background/border, а не нечитаемой надписью.
При известном target компонент interactive и имеет label «Открыть ветку ответа». При preview без target он статичен и не показывает ложный hover-link.
ThreadFocus: затемнённый/raised отдельный слой, источник, число ответов, хронологическая ветка, selected reply metadata. Есть Close/Escape/Back и корректный focus restore. Bracket декоративный, не отдельный target. Это фокус replies в существующем чате.
## 15. ReactionChip и EmojiPicker
Chip: emoji + count, gap 3/4, padding 8×4, pill. Strip gap 5/8, wrap в узкой row.
| Вариант | Представление |
| --- | --- |
| Чужие реакции | Neutral subtle background + readable border |
| Есть own reaction | Accent-tinted background + accent border + accessible own state |
| Hover/focus | Нативная/общая interactive обратная связь |
| Pending mutation | Ненавязчивый progress, без ложного подтверждения failure |
Quick reactions: 👍 ❤️ 😂 😮 😢 🔥. Полный picker: header «Выберите эмодзи», Close/«Готово», grid, category navigation. Emoji visual 28, cell 46×46 с touch hit area минимум 48. Grid адаптивный; focused item виден и управляется keyboard.
Text search в emoji picker не обязателен. Если добавлен, он следует общему SearchField и не меняет базовую семантику reactions.
## 16. Composer
Horizontal structure: attach → multiline input → send/record. Gap 8. Outer horizontal 10/12, top 6/8, bottom 8 плюс safe area. Control visuals 42×42, hit targets по разделу размеров.
Input: min height 42 visual, radius 21, leading 14/trailing 12/vertical 9. Body font, от 1 до 6 строк до внутренней прокрутки. Placeholder «Сообщение»; edit mode «Изменить сообщение». Emoji control справа input, secondary, icon 20.
- Непустой draft: send arrow accent/action.
- Пустой draft: record action с доступным выбором audio/video.
- Reply/edit banner: над input body, автор/preview и Cancel.
- Upload preparation: progress и доступный cancel, текст не теряется.
- Recording: elapsed, waveform/indicator, cancel, finish/lock.
- Selection: separate action bar, обычная отправка не конфликтует с массовыми actions.
Keyboard contract Web/Desktop: Enter отправляет при настройке «Enter для отправки», Shift+Enter — новая строка; можно переключить поведение. IME composition не отправляет сообщение по промежуточному Enter. Esc закрывает локальный popup/отменяет reply/edit с сохранением исходного draft по сценарию.
Touch recording gestures не являются единственным способом: есть доступные «Начать», «Остановить», «Отменить». Back во время capture спрашивает о потере записи либо явно отменяет по утверждённому сценарию.
## 17. MediaIsland и viewer
### 17.1. Фото/видео
Default width 268, max available width минус row insets. Radius 20, border 1, thumbnail inset 6. Photo natural height по ratio, baseline clamp 164…328, с proportional уменьшением на узком экране. Object-fit/scale mode — contain.
Filename/size badge снизу: caption, white on достаточно тёмной scrim, padding 8×5, pill; inset 9. Expand icon сверху справа с hit area. Video имеет центральный play и duration.
Состояния: placeholder, loading, thumbnail, ready, playing для inline применимого типа, failed/retry. Error не замещается бесконечным spinner.
### 17.2. Audio/voice
Content default width 252 с clamp доступной ширины. Play circle 44, waveform около 30 high, progress/seek, duration и speed chip. Voice speed 1×/1.5×/2×; audio filename visible.
Seek имеет keyboard arrows/accessible slider semantics и touch target выше визуальной линии. Loading, paused, playing, ended и failed различимы.
### 17.3. VideoNote
Circle 144 idle, 176 playing при достаточном месте; при узком viewport размер уменьшается. Play icon 24, duration pill снизу, border 1. Увеличение не должно резко двигать viewport или перекрывать соседние controls.
### 17.4. Fullscreen viewer
Root overlay, тёмный фон, zoom/pan для фото, controls для видео. Обязательно доступны Close button, Escape на keyboard и Back на Android; vertical dismiss — дополнительный touch gesture. Close target располагается в safe area и не закрывает важное media.
Zoomed photo pan не запускает dismiss. После закрытия focus возвращается к исходному элементу, viewport остаётся на том же сообщении. Download/save — отдельное явное действие.
## 18. AttachmentPreview
Modal/panel с title «Перед отправкой», grid, подписью и action row. Grid cell min 128/max 190, column gap 18/20, row gap 22/24, container padding 16. В compact grid адаптируется до одной/двух колонок.
Caption field radius 16/padding 13×10, 1…4 строки. Нижняя panel raised/glass с opaque fallback, padding 14/16. Summary слева, primary send справа; mobile может переносить action row без обрезки.
На sending отмена/закрытие имеют определённую доступность; failed items остаются с clear status и retry. Caption не показывается как доставленная, если её отдельная отправка завершилась ошибкой.
## 19. LocationPicker
Header «Отправить геопозицию», карта/marker, текущая выбранная точка, «Моя геопозиция» и primary «Отправить». При denied permission — inline explanation и ручной выбор.
Map не поглощает все keyboard/Back actions. Выбор координат доступен через альтернативный ввод, если map недоступна. Preview location message — компактная карта/иконка и координаты/label; tap открывает просмотр без автоматического фонового tracking.
## 20. CallScene и CallHistoryCard
CallScene — inverse fullscreen/large overlay. Remote video занимает доступный фон, контрастная scrim защищает labels. Local video 118×168/radius 22, top-end с safe area; на compact масштабируется и не перекрывает controls.
Без video: call gradient, avatar 132, name hero до 2 строк, media type subbody, status/duration tabular. Основные controls снизу, visual circles 58 и подписанные labels.
- Incoming: decline red и accept green/dark-success fill с readable white text.
- Connecting: status и доступный cancel.
- Connected: mute, audio route при поддержке, camera/switch при наличии, hangup red.
- Permission/error: понятное пояснение и возврат без скрытой активной записи.
Control selected state передаётся icon + label/accessible state, а не только прозрачностью. Caption labels имеют достаточный контраст на scrim.
HistoryCard: max width 300, icon circle 42, content gap 12, title bodyStrong/subbodyStrong, subtitle caption, padding 14×10. Outcome icon/text: completed success, missed/failed/declined error, cancelled/answeredElsewhere neutral. Направление определяется также arrow/текстом.
## 21. Auth, Settings, Encryption и Lock
### 21.1. AuthCard
Hero gradient background; centered column max 460, outer horizontal 24/vertical 48, адаптация к keyboard. Logo tile 88/radius 26, brand 42, card radius 28/padding 24…28.
Form: JID, password с show/hide, advanced connection options, primary «Войти». Loading «Подключение…» и spinner. Error у поля/общая область без потери ввода. Native host/port и Web WSS endpoint представлены соответствующими полями, а не общим неработающим transport selector.
### 21.2. Settings
Grouped sections, стандартные switches/list rows/links. Profile avatar 64. Основные sections: аккаунты, профиль, соединение, шифрование, уведомления, блокировка, приватность.
Long values читаются/копируются; destructive logout отделён от обычных preferences. Multiaccount selection имеет checkmark и account identity.
### 21.3. EncryptionPanel
Итоговый режим чата, global/override control, список устройств, protocol badge, fingerprint mono, trust. Verified/trusted/undecided/compromised различаются текстом и символом. «Готово» не означает автоматической verification.
Fingerprint selectable/copyable, разбит на читаемые группы; label устройства не заменяет fingerprint. Empty devices объясняет следующий шаг, не показывает фиктивный secure success.
### 21.4. LockScreen
Hero/нейтральный фон и полностью скрытое sensitive content. Card radius 28, lock icon 44, title 22, field/action max 260 при обычной ширине, spacing около 22/24. Passcode input masked, explicit unlock, error inline и biometric action только при реальной доступности.
Lock не имитируется прозрачным blur поверх читаемого чата. Не существует автоматически перенесённой native-biometric кнопки в неподдержанном браузере.
## 22. Feedback, dialogs и слои
| Компонент | Контракт |
| --- | --- |
| Toast | Краткий результат, max 2 строки, закрываемый/доступный screen reader |
| Inline error | Ошибка поля/операции рядом с источником |
| Alert/dialog | Важная ошибка/подтверждение с ясными действиями |
| Loading | Spinner/progress + конкретная операция |
| Empty state | Название, пояснение, доступное next action |
| Connection status | Компактный читаемый статус в заголовке/панели |
Toast: padding 16×10, gap 8, pill, raised/glass fallback, shadow.toast. Default display 2.5 с; при accessibility/длинном тексте длительность увеличивается или доступно persistent alternative. Toast не единственное место критической ошибки.
Layers:
| Токен | Значение |
| --- | --- |
| `layer.base` | 0 |
| `layer.sticky` | 10 |
| `layer.popover` | 40 |
| `layer.dialog` | 80 |
| `layer.media` | 100 |
| `layer.call` | 200 |
| `layer.toast` | 300 |
| `layer.lock` | 400 |
Это роли stacking, не обязательные literal z-index в каждой технологии. При открытии слоя проверяются активные dialogs: неподходящий старый modal закрывается/уступает по сценарию. App lock имеет приоритет sensitive content. Error dialog при lock не раскрывает текст переписки.
Modal traps focus, имеет label и close action. После close focus восстанавливается. Browser/Android Back сначала закрывает верхний modal, затем navigates.
## 23. Анимации и жесты
| Токен/эффект | Значение |
| --- | --- |
| `motion.fast` | 120 ms |
| `motion.base` | 180…220 ms |
| `motion.panel` | 240…300 ms |
| Timeline entrance | До12 rows, stagger 35 ms, rise 16, scale 0.94 |
| Outgoing append | Spring около 440 ms; start scale 0.2/rise 12 при допустимом layout |
| Incoming append | Start scale 0.4/rise 10 |
| VideoNote resize | Spring около 320 ms |
| Reply swipe return | Spring около 240 ms |
Burst >4 appended rows/архив не анимируется как несколько свежих сообщений. Recycled rows не играют entrance повторно при каждом scroll.
Reduced motion: убрать scale/slide/stagger, оставить короткое fade или мгновенную смену. Важное состояние не зависит от завершения анимации. Animation cancellation не оставляет invisible interactive control.
Touch reply: horizontal activation 24, trigger 64, max offset 76; раннее доминирование горизонтали 1.6, после lock 1.05. Вертикальный scroll приоритетен. Haptic при пороге там, где поддержан и разрешён.
Record: hold около 240 ms, cancel left 92, lock up 78; размеры адаптируются к ограниченному пространству, доступные кнопки остаются.
Viewer swipe: vertical dominance 1.15, distance около 14% height clamp 88…140; fast predicted endpoint может завершать жест. На pointer Close/Escape остаются основным способом.
## 24. Accessibility и ввод
**UI-A11Y-001.** Целевой контраст обычного текста минимум 4.5:1, крупного 3:1; значимых controls/focus минимум 3:1 относительно соседних поверхностей. Проверяются фактические composite colors, не только исходные HEX.
**UI-A11Y-002.** Все действия доступны screen reader и имеют names/states. Icon-only button не называется именем файла icon.
**UI-A11Y-003.** Web/Desktop полностью поддерживают Tab/Shift+Tab, Enter/Space для действий, Escape для закрытия и предсказуемый focus order. Hover-only actions доступны также по focus/tap.
**UI-A11Y-004.** При text scale/zoom до 200% интерфейс reflow без потери основных действий. Fingerprint/URI можно прочитать полностью; text body не скрывается за fixed height.
**UI-A11Y-005.** Touch targets минимум 48×48 u для действий; визуально меньшая icon сохраняет просторный hit region. На pointer target не меньше 44×44 для основных icon actions.
**UI-A11Y-006.** Errors связаны с input, loading не бесконечно повторяется screen reader, live messages объявляются умеренно и не перехватывают focus.
**UI-A11Y-007.** Reduced motion/transparency и high contrast имеют определённые варианты. Цвет не единственный способ различения unread/failed/verified.
**UI-A11Y-008.** Record/call/media controls отражают фактическую активность camera/microphone. Для destructive есть явное подтверждение области действия.
## 25. Каталог состояний и приёмка UI
Поставляемая библиотека компонентов имеет отдельный доступный каталог/preview, не требующий реального XMPP login.
Обязательные fixtures:
- light/dark и reduced motion/transparency;
- compact 320/390, medium 840, wide 1280/1440;
- обычный и 200% текст;
- короткие/длинные имена/JID/URL/filename;
- чат без сообщений, search без результатов, offline и syncing;
- все delivery/security states;
- incoming/outgoing/group/reply/tombstone/reactions;
- media loading/ready/error и partial batch;
- recording start/locked/finalizing/error;
- incoming/connecting/active/failed call;
- keyboard focus, selection и menus;
- lock, denied permissions и уведомления без preview.
### 25.1. Чек-лист
- [ ] Все цвета/шрифты/spacing берутся из токенов по роли.
- [ ] Navigation соответствует ширине и платформе.
- [ ] Любая важная action доступна без жеста/hover.
- [ ] Focus не теряется в modal/viewer и возвращается после close.
- [ ] Long content и text scale не ломают layout.
- [ ] Incoming/outgoing/delivery/security определяются без одной лишь окраски.
- [ ] Media не обрезается неожиданно и не растягивается.
- [ ] Keyboard/IME не отправляет незавершённый ввод.
- [ ] Loading/ошибка/cancel завершаются корректным состоянием.
- [ ] Contrast проверен для фактических цветов и прозрачностей.
- [ ] Motion/transparency preferences работают.
- [ ] UI соответствует functional predicates `tz_clear.md`.
## 26. Развитие стандарта
Новый компонент сначала определяется как набор ролей/состояний, затем добавляется в tokens/catalog и после этого используется в экранах. Новый вариант существующего компонента получает имя и назначение; нельзя создавать похожую локальную копию с немного другим цветом/radius без причины.
Изменение токена проверяется на всех затронутых компонентах, light/dark и платформах. Несовместимые изменения версии UI Kit документируются и мигрируются согласованно.
Стандарт сохраняет общий характер Luma, при этом concrete native widget и способ реализации blur/vector/animation выбираются платформой. Ни одна из библиотек Web/Desktop/Android не считается реализованной только по существованию этого документа.