615 lines
51 KiB
Markdown
615 lines
51 KiB
Markdown
# 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 живого приложения иллюстрациями.
|