Живой документ. Фиксирует калибровки, ритм и договорённости по UI. Обновляется при каждом уточнении.
У Material-иконок из библиотеки androidx.compose.material.icons.outlined.* одинаковый спек-размер (24×24 grid), но визуально «нарисованная» часть у всех разная. Если просто поставить всем Modifier.size(24.dp), одни иконки будут казаться большими и тяжёлыми (Bolt, Delete), другие — мелкими (Contacts, VpnKey). Нужна ручная калибровка.
new_size = 24 × 52 / measured_px, клип в [18..28].52 px — референсный painted-глиф Fingerprint, на него ориентируемся как на «единичный».
| Иконка | Размер | Почему |
|---|---|---|
| Add, Apps, ArrowBack, Close | 28dp | тонкие линии, глиф 36–41 px |
| AutoMode, Bolt, DeleteOutline, Edit, List, Photo, QrCode, Tune, DriveFileRenameOutline | 26dp | среднеплотные, глиф ~47 px |
| LocationOn | 24dp | балун — не трогать, референс |
| LocationOff | 26dp | балун с перечёркиванием — не трогать |
| ContentCopy, MyLocation, PeopleAlt, Wifi, ExpandMore, ExpandLess | 22dp | широкие/высокие, визуально тяжёлые |
| Contacts, VpnKey | 20dp | перегруженные внутренним содержимым |
Используется везде, где раньше был Bolt (кнопка «Автоматизация» на карточке ключа, иконка раздела в настройках). Размер 26dp — как у Bolt.
IconsDebugSheet.kt — карта iconSizeByName. Отладочный экран вызывается из SettingsScreen кнопкой «Иконки UI (дебаг)» — на нём FlowRow с калиброванными иконками сверху и список с именами/размерами/контекстом ниже.
Используем стандартные роли Material3. Сознательно ограничились четырьмя:
| Роль | Размер/lineHeight | Где применяется |
|---|---|---|
titleLarge SemiBold | 22sp/28sp | Заголовок bottom-sheet («Для гостя · <объект>») |
titleMedium SemiBold | 16sp/24sp | Заголовки правил («Геолокация», «Wi-Fi», «Интервал», «Активация»); лейблы FieldSection («Координаты», «Радиус», «Режим», «Сохранённые сети») |
bodyMedium | 14sp/20sp | Подписи радио-кнопок, текст внутри pill-кнопок, текст сегментов |
bodySmall muted | 12sp/16sp | Статусы и подсказки («36540 м до точки», «74 м · максимум, гость может уменьшить», hint-тексты). Цвет: onSurfaceVariant |
Договорились работать кратно 2 / 4 / 8 / 10 / 14 / 16 / 20. Не «то густо то пусто» — одинаковые элементы одинаково.
spacedBy(12.dp). Плашки отделены 12dp.padding(horizontal=20.dp).padding(top=16.dp).verticalArrangement = spacedBy(16.dp) между крупными блоками.spacedBy(2.dp). Только 2dp между жирной меткой и статусом/подсказкой под ней — чтобы воспринимались как пара «заголовок + подпись».Spacer(10.dp). Единообразно в Geo, Wi-Fi connect, Wi-Fi list.Material3 Slider занимает ~48dp по лейауту (из-за минимального touch-target), из которых видимая дорожка — только ~4dp по центру, а над/под ней по ~22dp пустой touch-зоны. Из-за этого:
Modifier.offset(y=-16.dp) чтобы подтянуть тамб к тексту.Modifier.sliderTrim() (хелпер в GuestAutomationEditor.kt), он репортует родителю высоту 22dp вместо 48dp. Так padding(16.dp) плашки даёт одинаковый гэп от тамба до нижней границы, как у плашек, заканчивающихся pill-кнопкой.У каждого правила (Геолокация, Wi-Fi, Время, Авторизация, Подтверждение) и у блока «Активация» — выбирается один из трёх уровней. Кружок-иконка карточки красится в соответствующий цвет, когда свитч включён.
| Ключ | Ярлык | Цвет | Описание |
|---|---|---|---|
own |
Гость решает | #3BA55D (зелёный) | Настройку выбирает сам гость |
suggest |
Предустановка | #E5B409 (жёлтый) | Значение задано, гость может изменить |
force |
Закреплено | #CC3322 (красный) | Значение задано, гость изменить не может |
Выключенный свитч — кружок становится серым (#9AA0A6), независимо от выбранного уровня.
Единый «посадочный слот» 38dp по всему приложению:
CircleShape, background = цвет уровня.contentAlignment = Alignment.Center.Оборачиваем в Box(Modifier.size(38.dp), contentAlignment=Center) { RadioButton(...) } — чтобы центр круга радио совпадал с центром цветного кружка выше. Без этой обёртки радио съезжает по горизонтали, потому что сам RadioButton рендерится с отступами.
Между радио и текстом — Spacer(14.dp). Между радио-строками в группе — Column(spacedBy(4.dp)).
Отключён минимальный touch-target: CompositionLocalProvider(LocalMinimumInteractiveComponentEnforcement provides false) — иначе радио-строки набирают по 48dp и ломают ритм.
Стандартный Material3 Switch. Справа от заголовка карточки. Выключает всё правило. Цвета уровня при этом не показываются (кружок серый).
Кастомный контрол для переключения режимов внутри правила (сейчас — только в Wi-Fi: «По конкретной сети» / «По сетям вокруг»). Два пункта в Row, каждый на weight(1f):
colorScheme.surface, RoundedCornerShape(10.dp), padding 4dp.primaryContainer, RoundedCornerShape(8.dp), текст SemiBold.Обводка + иконка + текст, закруглённая. Используется для действий второго порядка («Использовать текущее положение», «Сделать снимок сетей вокруг»). В отключённом состоянии — обводка и текст alpha=0.4.
См. раздел 3 про offset(y=-16.dp) и sliderTrim(). Перед слайдером обязательно идёт StatusLine со статус-текстом (бодиСмолл, muted) — «74 м · максимум, гость может уменьшить».
Вызывается по клику на строку заголовка карточки (весь Row кроме Switch). Открывается прямо под шапкой, по всей ширине контента карточки. Для этого:
Box с onGloballyPositioned, меряем его ширину в dp.DropdownMenuItem с модификатором Modifier.width(anchorWidthDp).Пункты dropdown — воздушные, не плоские строки: цветной маркер слева, ярлык жирно, описание под ним мелко-muted. Выровнены вертикально по первой строке (ярлыку).
В отличие от обычных цветных кружков правил, у Активации в заголовке — пульсирующий.
drawBehind + Stroke(width=6dp).LinearEasing. Не FastOutSlowInEasing — иначе виден «пуф» на рестарте цикла.sin(π·p) × 0.6: при p=0 и p=1 alpha=0, середина — пик. Рестарт цикла невидим.Одно предложение в UI-строке — без точки в конце. Точки только когда предложений два и больше (для разделения). Касается всех описаний, подсказок, статусов, hint-текстов, заголовков, единичных фраз.
В статусах слайдеров в жёлтом (Предустановка) режиме — честно пишем про направление свободы:
В красном (Закреплено) — «74 м · точно» / «5 из 7 · точно».
«Гость решает / Предустановка / Закреплено» — прижились, не меняем.
Если у правила есть состояние «выключено» — делаем его через Switch, не через радио-опцию «Не требуется / Нет / Выкл». Иначе у пользователя два способа выключить одно и то же — путает.
Примеры применения:
В блоке «Координаты» иконки копирования/удаления стоят справа на одном вертикальном уровне с обеими строками (label + статус). Реализовано через Row { Column(weight=1f) { label; status }; CopyIcon; DeleteIcon }. Иконки centered по вертикали всей пары.
Тело карточки раскрывается только если:
Анимация: expandVertically(tween(220)) + fadeIn(tween(220)) на открытии, shrinkVertically(tween(180)) + fadeOut(tween(120)) на закрытии.
При получении гостевого ключа — только если приложение в foreground и это новый ключ (не дубль). Иначе молча сохраняем. (Подробнее — см. memory feedback_key_dialog.md.)
sin(π·p) × 0.6. Любое упрощение ломает визуал.reference_icons_calibration.md, feedback_punctuation.md, feedback_key_dialog.md.Последнее обновление: 18.04.2026 · Версия приложения: 2.07