---
url: https://nsh.dev/ru/docs/design-guides
description: "Рекомендации по продуктовому и интерактивному дизайну приложений, созданных с помощью Native Shell: иерархия, цвет и токены, отступы, плотность, компоненты, состояния взаимодействия, оверлеи, анимация, интерфейсы с большим объёмом данных, тексты интерфейса и соответствие платформе."
---

# Руководства по дизайну

Используйте это руководство, прежде чем выбирать компоненты или писать код раскладки для приложения на Native Shell. Оно фиксирует продуктовые решения, которые GPUI Kit накопил за годы работы с десктопом и которые перенесены в NSH: интерфейс должен ощущаться нативным, сдержанным, точным и понятным без догадок.

Это нормативное руководство. **Должно** обозначает требование корректности или совместимости с экосистемой, **следует** — вариант по умолчанию, отступить от которого можно только по конкретной причине, **можно** — необязательный приём. Страницы [Elements](https://nsh.dev/translations/ru/elements.md) и [API](https://nsh.dev/translations/ru/modules.md) остаются авторитетным источником по отдельным свойствам и методам.

Правила опираются на поведение в `nsh/base` (`nsh_base` в Rust), на тему и её токены, а также на взаимодействие, которое пользователи каждой платформы уже знают. NSH рисует интерфейс средствами UI самой платформы, а внешний вид оставляет приложению: компоненты `nsh/base` дают поведение, состояние, фокус и доступность, а каждый цвет, размер, радиус и отступ берётся из собственных компонентов приложения, поэтому эти правила направляют дизайн-систему приложения. <a href="https://ui.shadcn.com/docs" target="_blank" rel="noopener noreferrer">shadcn/ui</a> предлагает полезные методы (открытый код, композиция и надёжные значения по умолчанию), но не определяет, как должно выглядеть приложение на NSH. Когда влияния противоречат друг другу, сохраняйте поведение платформы и взаимодействие, которое людям уже понятно.

<a id="design-thesis"></a>

## Тезис дизайна

Создавайте интерфейсы, которые ощущаются нативными, тихими и точными. Пусть опыт определяют содержание, иерархия и взаимодействие; декор должен их поддерживать, а не конкурировать с ними.

1. **Ясность важнее индивидуальности.** Сначала сделайте понятными основную задачу и следующее действие, и только потом добавляйте выражение бренда.
2. **Композиция важнее изобретения.** Начинайте с устоявшихся компонентов и собирайте из них сценарии, специфичные для продукта. Новый примитив создавайте только тогда, когда его поведение действительно новое.
3. **Токены важнее значений.** Цвета, радиусы, типографика и отступы должны образовывать систему. Избегайте разрозненных литералов, которые не могут реагировать на темы. См. [Styling](https://nsh.dev/translations/ru/docs/styling.md), [Tailwind](https://nsh.dev/translations/ru/docs/tailwind.md) и [тему](https://nsh.dev/translations/ru/docs/rust.md#theme).
4. **Платформа важнее веб-конвенций.** Сохраняйте доступ с клавиатуры, оформление окон, меню, плотные представления данных, изменяемые по размеру области и постоянную навигацию на десктопах, а на телефонах и планшетах — системные заголовки, жест «назад», алерты и размеры целей касания, где задаче это полезно.
5. **Состояние должно быть видно.** Наведение, нажатие, фокус, выбор, отключённое состояние, загрузка, валидация и деструктивные состояния требуют различимого и единообразного оформления.
6. **Геометрия объясняет вложенность.** Радиусы, отступы и границы должны описывать одну связную иерархию, даже когда поверхности перекрываются.
7. **Обратная связь отражает реальность.** Загрузка, подсказки и анимация должны проясняют реальное состояние или переход, а не добавлять церемонности мгновенному действию.

<a id="learning-from-shadcn"></a>

## Чему научиться у Shadcn

Главный вклад Shadcn — не конкретный цвет границы, а способ построения системы:

- владейте верхним слоем интерфейса, а не боритесь с закрытой абстракцией;
- собирайте из небольших предсказуемых частей компоненты, специфичные для продукта;
- предоставляйте значения по умолчанию, которые уже образуют единый визуальный язык;
- сохраняйте код и композицию читаемыми и для людей, и для ИИ;
- отделяйте примитивы поведения от стилизованного слоя.

NSH применяет эти идеи через разделение на примитивы `nsh`, нестилизованные базовые компоненты `nsh/base` и стилизованный слой, который принадлежит приложению ([Three layers](https://nsh.dev/translations/ru/docs/architecture.md#three-layers)). Приложения строят свои компоненты на `nsh/base`, в TSX или в Rust-крейте приложения; повторно используемое поведение принадлежит `nsh/base`, а визуальная политика остаётся выше.

Не копируйте эти веб-допущения вслепую:

| Веб-привычка | Значение по умолчанию в NSH |
| --- | --- |
| Курсор-указатель на каждой кнопке | Курсор по умолчанию; указатель-рука (`cursor-pointer`) для ссылок |
| Постраничная навигация как основная структура | Постоянные окна, панели, боковые панели, вкладки и меню на десктопах; `NavStack` с системной кнопкой «назад» на телефонах |
| Фокус и прокрутка браузера как запасной вариант | Явное владение фокусом и прокрутка, принадлежащая области (`Scroll`, `List`) |
| Одна колонка в подходе mobile-first | Одна колонка на телефонах, изменяемая по размеру оболочка с заданным минимальным размером окна на десктопах |
| Критичные действия, появляющиеся при наведении | Действия, доступные с клавиатуры, касанием и указателем и не зависящие от наведения |
| Ряд иконок-кнопок, видимых только при наведении | Видимое основное действие плюс `Menu` или `ContextMenu` для второстепенных команд |
| Текст в стиле ссылки для команд приложения | `Button` в стиле приложения default, outline или ghost; стиль ссылки только для URL, веб-ресурсов или адресов электронной почты |
| Крупная плотность касания везде | Цели размера для касания на сенсорных экранах; средняя плотность с указателем, компактная только там, где она помогает работе с информацией |
| Нарисованный заголовок и карточка-алерт на каждой платформе | Системный заголовок и `AlertDialog` на iOS и Android; слот `header` страницы на десктопах и в вебе |
| Переопределения CSS для всех потомков | Компоненты приложения, их части и токены темы |

<a id="start-from-the-task"></a>

## Начинайте с задачи

Прежде чем рисовать экран, запишите:

- основную задачу пользователя;
- объект, который просматривается или изменяется;
- действия, которые должны оставаться доступными немедленно;
- информацию, необходимую для принятия решения;
- состояния: пустое, загрузки, ошибки, офлайн, только для чтения и отказа в доступе;
- путь по сценарию с клавиатуры, касанием и скринридером;
- где показывается приложение: само по себе или внутри хост-приложения.

Организуйте экран вокруг этих ответов. Не начинайте с сетки дашборда или каталога компонентов. Хороший интерфейс выражает ментальную модель пользователя: документы, учётные записи, проекты, сообщения, настройки или другой устойчивый объект, а не внутреннюю архитектуру сервисов.

Проектируйте основную задачу до распределения элементов управления. Её визуальный вес, расположение и глубина информации должны соответствовать её важности для продукта. Ключевой результат нельзя сводить к маленькому счётчику, иконке в углу или слабому действию в подвале, пока второстепенное содержимое занимает страницу. Когда набор результатов — главная ценность продукта, рассмотрите область сводки или карточку, показывающую количество, репрезентативные результаты, значимое состояние и понятное следующее действие.

Для каждого предлагаемого действия назовите его видимый объект, текущее состояние, область действия и результат. Если интерфейс не показывает или явно не подразумевает всё это, действие преждевременно. Не выставляйте возможность лишь потому, что она есть в бэкенде; сначала продумайте, как объект войдёт в ментальную модель пользователя.

<a id="visual-language"></a>

## Визуальный язык

<a id="hierarchy"></a>

### Иерархия

Предпочитайте небольшое число чётких уровней:

- **заголовок окна или страницы** обозначает текущий объект или рабочее пространство;
- **заголовок раздела** разделяет значимые области (`role="header"`, чтобы скринридеры могли переходить между ними);
- **основной текст** несёт работу;
- **приглушённый текст** содержит второстепенные метаданные и справку;
- **подписи** обозначают элементы управления и значения.

Сначала используйте размер, насыщенность, отступы и разделители, и только потом добавляйте цвет или контейнеры. Избегайте вложенных карточек: большинству областей достаточно фона, тонкой границы и продуманных отступов.

Оценивайте иерархию по всей функциональности, а не по отдельным компонентам. Скройте акцентный цвет и декор при проверке: основная задача, текущий выбор, сводка результата и следующее действие всё равно должны быть очевидны из структуры. Экран из элементов, каждый из которых по отдельности выглядит правдоподобно, всё равно может не работать, если они не образуют единый порядок чтения и единый путь принятия решения.

Относитесь к акцентам как к ограниченному бюджету. Локальной поверхности нужна одна ясная точка внимания, а не поле конкурирующих выделений. Если всё окрашено, снабжено бейджем, выделено жирным, заключено в рамку или повышено до алерта, ничто не читается как важное. Сначала задавайте приоритет структурой и близостью; более сильный цвет и компоненты тратьте только там, где различие меняет то, что пользователь замечает или делает.

<a id="color-and-themes"></a>

### Цвет и темы

Читайте цвета из темы (`theme()` в Rust, классы токенов в TSX) и используйте их по семантической роли:

- `background` и `foreground` для основной поверхности и текста;
- `card`, `popover`, `sidebar` и их токены foreground для соответствующих поверхностей;
- `muted` и `muted-foreground` для вспомогательной информации;
- `primary` для основного действия или акцента выбора, `secondary` и `accent` для более тихого акцента и строк при наведении или фокусе;
- `destructive` только для деструктивных действий и ошибок, а цвета статуса, которые приложение добавляет в свою тему (`success`, `warning`), только для их значений;
- от `chart-1` до `chart-5` для рядов данных;
- `border`, `input` и `ring` для структуры и взаимодействия.

```rust
use nsh_base::theme;

view()
    .rounded(move || theme().radius)
    .border_1()
    .border_color(move || theme().border)
    .bg(move || theme().background)
    .p_4()
    .gap_1()
    .child(text("Balance").text_color(move || theme().foreground))
    .child(text("Updated just now").text_sm().text_color(move || theme().muted_foreground))
```

```tsx
<View class="gap-1 rounded-(--radius) border border-border bg-background p-4">
  <Text class="text-foreground">Balance</Text>
  <Text class="text-sm text-muted-foreground">Updated just now</Text>
</View>
```

Не используйте семантический цвет статуса как декор. Не кодируйте смысл только цветом. Проверяйте каждую пользовательскую поверхность в светлой и тёмной темах и с пользовательскими значениями темы, включая тему хост-приложения, которое встраивает ваше; никогда не предполагайте, что foreground чёрный, а background белый.

Используйте бейдж для короткого состояния, счётчика или классификации, которым полезно быстрое сканирование, а не для каждой подписи, значения метаданных, фильтра или заголовка раздела. Делайте большинство бейджей нейтральными; семантические цвета оставляйте для состояний, которые действительно означают успех, предупреждение, деструктивность или информацию. Ряд разноцветных бейджей — это обычно недостающее решение об иерархии или группировке.

Интерфейс приложения не должен содержать сырые hex-цвета, цвета `rgb()`/`rgba()` или `hsl()`, а также палитровые классы вроде `bg-zinc-100`. Получайте цвета из темы по семантической роли. Если нужной роли нет, определите её в файле темы приложения ([Theme tokens](https://nsh.dev/translations/ru/docs/tailwind.md#theme-tokens): `"up": "#16a34a"` в его `light` и `dark` даёт `text-up`, `bg-up/30`, `token("up")` в TSX и `theme().color("up")` в Rust), а не встраивайте значение палитры в месте вызова. Сырые цвета допустимы только внутри определений темы или в проверенном data/raster-содержимом, где цвет сам является данными.

<a id="radius-spacing-and-density"></a>

### Радиус, отступы и плотность

Используйте конечную шкалу радиусов, как для типографики и размеров элементов управления. Выбирайте из `radius` темы (для элементов управления и поверхностей одинаково, `rounded-(--radius)`), радиусов, которые приложение добавляет в свою тему, и значений Tailwind от `rounded-sm` до `rounded-2xl` между ними), а не придумывайте радиус для каждого элемента. Крупные контейнеры могут использовать более мягкий уровень, чем вложенные в них элементы управления; компактные элементы и оверлеи используют более плотные уровни. Для обычного прямоугольника радиус должен оставаться заметно меньше половины его короткой стороны. Используйте `rounded-full` (`.rounded_full()`) только для намеренных кругов и таблеток; подсказка не должна случайно превращаться в капсулу.

Вложенные поверхности должны читаться как концентрические формы. Используйте `inner radius ≈ max(0, outer radius − gap)`, чтобы выбрать ближайший подходящий радиус; видимая полоса должна оставаться ровной на протяжении всего поворота. Добавьте именованный токен в тему (`"radius-inset": "0.375rem"`, `rounded-(--radius-inset)`), если повторяющейся композиции нужно промежуточное значение. Сегментированный элемент управления — это один силуэт: крайние сегменты следуют внутренним углам контейнера, средние остаются прямоугольными, а на каждой границе один разделитель постоянной толщины.

![Light theme chat composer: the inset surface, both buttons, and placeholder spacing change together; red guide circles show their shared corner center.](https://nsh.dev/diagrams/radius-hierarchy-light.svg)

Радиус должен управлять всей видимой поверхностью, а не только её границей. Открытые углы остаются прозрачными для поверхности за ними; фоны дочерних элементов, заливки выбора и прокручиваемое содержимое тоже должны следовать границе. Задайте внутренним поверхностям собственный радиус или обрежьте контейнер с помощью `overflow-hidden` (`.overflow_hidden()`), затем проверьте композицию на контрастном фоне в обеих темах. При выборе обрезки сохраняйте внешние кольца фокуса.

Используйте компактную шкалу отступов и повторяйте её. Связанные пары подпись/элемент управления должны быть ближе, чем отдельные группы; отдельные группы должны быть ближе, чем отдельные разделы. Предпочитайте размеры компонентов приложения (малый, средний по умолчанию, большой) разовым высотам. Используйте компактные варианты для панелей инструментов и экранов с плотными данными, а не для того, чтобы втиснуть неясную раскладку в меньшее пространство.

Общая семантическая шкала намеренно невелика: отступы идут примерно по ступеням 2, 4, 8, 12, 16, 24 и 32 пункта, а типографика держится около 12, 14, 16, 18 и 20 пунктов. Воспринимайте это как соотношения, а не как разрешение разбрасывать их текущие значения по коду функциональности. Шкала отступов Tailwind даёт их: один шаг равен 4 пунктам (`p-4` — это 16pt), как в классах, так и в типизированных методах Rust; `"spacing"` в `"tailwind"` для `nsh.json` меняет шаг классов, а типизированные методы сохраняют шкалу Tailwind по умолчанию. Приложение, которому нужна другая шкала, владеет ею в собственных компонентах или токенах темы (`p-(--gap-card)`) и использует её последовательно.
<a id="spatial-grammar"></a>

### Пространственная грамматика

Отступы выражают отношения. Выбирайте промежуток из семантической шкалы, спрашивая, что два элемента значат друг для друга:

| Отношение | Типичный класс | Текущая шкала | Примеры |
| --- | --- | --- | --- |
| Оптическая коррекция | `gap-0.5` | 2 pt | базовая линия значка, компактный разделитель |
| Части одного элемента управления | `gap-1` | 4 pt | значок и подпись в меню, заголовок и описание |
| Тесно связанные элементы управления | `gap-2` | 8 pt | значок и подпись кнопки, действия диалога |
| Одна группа содержимого | `gap-3` | 12 pt | столбцы уведомления, строки компактной формы |
| Отдельные группы в одном разделе | `gap-4` | 16 pt | внутренние отступы панели, группы формы |
| Отдельные разделы | `gap-6` | 24 pt | крупные блоки страницы или инспектора |
| Граница крупной области | `gap-8` | 32 pt | «воздух» пустого состояния, полосы страницы |

Эти значения описывают шкалу по умолчанию, а не литералы, которые нужно повторять. Используйте классы отступов и типизированные методы (`gap-2`,
`.gap_2()`, `p-4`, `.p_4()`) или токены темы. Собственная шкала продукта должна сохранять порядок и
отношения и передаваться через компоненты самого приложения, а не записываться в каждом месте вызова.

Применяйте эти правила при определении горизонтального и вертикального пространства:

1. **Сначала внутри, потом снаружи.** Внутренний отступ компонента принадлежит компоненту; промежуток между компонентами принадлежит их родителю.
2. **Вертикальный ритм показывает группировку.** Промежуток между заголовком и описанием меньше, чем промежуток от
   этого описания до следующего раздела. Одинаковые промежутки подразумевают одинаковые отношения.
3. **Горизонтальное пространство помогает сканированию.** В повторяющихся строках значки, подписи, значения, бейджи и завершающие действия остаются в стабильных столбцах.
4. **Начало и конец семантичны.** Думайте краями в порядке чтения и записывайте их как start и end (`ps-*`,
   `me-*`, `.ps_4()`), чтобы раскладка зеркально отражалась в режиме справа налево.
5. **Не удваивайте отступы.** Карточка, помещённая в уже снабжённую отступами панель, не должна автоматически добавлять ещё один полный отступ панели.
6. **Используйте оптическое выравнивание умеренно.** Коррекция в одну-две точки допустима для геометрии значка или глифа, но задокументируйте, почему она отличается от шкалы.

Типичные композиции иллюстрируют эти отношения:

- содержимое кнопки использует 4 pt при малых размерах и 8 pt при обычных;
- заголовки и нижние части диалогов используют внутренний промежуток 8 pt;
- компактные строки списков и меню используют вертикальный отступ 4 pt и горизонтальный 8–12 pt;
- заголовки листов (sheet) используют около 16 pt отступа в начале и 12 pt в конце, оставляя место для элемента закрытия, а нижние части используют 16 pt по горизонтали и 12 pt по вертикали;
- уведомления используют горизонтальный отступ 16 pt и промежуток между столбцами 12 pt, потому что значок, сообщение и действие — отдельные группы.

Не воспринимайте их как рецепты для копирования на каждую поверхность. Они раскрывают устройство системы: элементы управления плотнее внутри, строки оптимизированы для сканирования, а контейнеры тратят больше пространства на своей границе, чем между своим содержимым.

<a id="proportion-and-layer-hierarchy"></a>

### Пропорции и иерархия слоёв

Начинайте с требований к содержимому, затем задавайте пропорции. Избегайте произвольного деления пополам, когда у одной панели явно другая роль.

- Навигационная боковая панель должна быть достаточно широкой для стабильных подписей, но заметно подчинённой рабочей области. Задайте ей минимальную, предпочтительную и максимальную ширину (`min-w-*`, `w-*`, `max-w-*`), а не только процент.
- В раскладках «главная панель — детали» оставьте коллекцию удобной для сканирования, а панели деталей отдайте излишек. Отправной точкой часто удобно брать примерно одну треть и две трети, но ограничения содержимого имеют приоритет. На телефоне эти две панели становятся двумя страницами `NavStack`.
- Инспекторы и вспомогательные листы по умолчанию не должны перекрывать основной объект. Их размер должен изменяться, либо их должно быть можно закрыть, когда содержимое растёт; [`Sheet`](https://nsh.dev/translations/ru/elements/sheet.md) закрывается вместе с подложкой, по Escape и по «назад».
- Ширина диалога определяется решением: `AlertDialog` для короткого подтверждения, средняя форма или отдельное окно или страница для сложной работы. Не увеличивайте диалог только ради свободного пространства.
- Самую сильную elevation оставляйте для самого верхнего слоя принятия решения. Внутри слоя показывайте иерархию фоном и тонкими линиями, а не всё более крупными тенями.

Определите три ограничения размера для каждой крупной области: минимум, при котором её задача ещё решается, комфортное значение по умолчанию и способ поглощения излишка. Сохраняйте пользовательские разделения, если они отражают предпочтения рабочего процесса, и ограничивайте восстановленные значения относительно текущего окна.

<a id="alignment-details"></a>

### Детали выравнивания

Выравнивание — это структурная система, а не заключительная полировка. Установите для каждой поверхности небольшой набор осей выравнивания: общие передние и задние края, базовые линии текста, центральные линии и фиксированные функциональные полосы. Элементы одного уровня должны примыкать к одной оси сверху донизу или от начала к концу, даже если это компоненты разных типов.

![Alignment spines across a content region](https://nsh.dev/diagrams/alignment-spines-light.svg)

Вертикальные красные линии проходят рядом с общими краями или центрами элементов управления для содержимого, статуса, времени и завершающих действий.
Горизонтальные линии расположены под базовыми линиями текста или проходят через центр строки, показывая выравнивание по нижнему краю и по вертикальному центру.
Компактное сравнение выделяет сдвиг на один отрисованный пиксель, который нужно исправить у его структурного владельца.

- Задайте родственным областям общий отступ содержимого. Заголовок, панель инструментов, строка списка, пустое состояние и нижняя часть, относящиеся к одному уровню, не должны каждый изобретать немного свой передний край.
- Повторяйте геометрию столбцов во всей области. Заголовки, строки, итоги, состояния загрузки и встроенные редакторы должны резервировать одни и те же полосы для идентификации, метаданных, статуса, чисел и действий.
- Выравнивайте связанные элементы управления между строками и разделами. Подписи, поля, описания и сообщения проверки формы должны образовывать стабильную вертикальную сетку при просмотре страницы сверху вниз.
- Сохраняйте согласованность горизонтальных полос. Элементы, делящие панель инструментов, заголовок, строку состояния или строку, должны использовать одну базовую или центральную линию, а не индивидуально подобранные смещения.
- Вводите отступы только для настоящей иерархии, вложенности или раскрытия. Декоративные отступы заставляют равноправные элементы выглядеть подчинёнными и ломают линию чтения поверхности.
- Когда вложенный уровень заканчивается, возвращайтесь точно на ось родителя. Не позволяйте накопленным отступам смещаться во вложенных контейнерах.
- Сохраняйте ось при необязательном содержимом. Отсутствующие значки, бейджи, описания или завершающие действия не должны сдвигать остальные подписи; используйте намеренные слоты или полосы, если важно сравнение между строками.
- Выравнивайте крупные области друг с другом там, где совпадает их иерархия. Заголовки боковой панели, заголовки содержимого, разделённые панели, панели инструментов и нижние панели не обязаны совпадать по всем координатам, но совпадающие уровни должны образовывать видимые непрерывные линии.

Выравнивать нужно не каждый край. Дочерний элемент может иметь отступ, основное значение может предшествовать вспомогательным метаданным, а деструктивное решение может получить дополнительное отделение. Такие исключения должны передавать иерархию или смысл; они не должны возникать из-за несогласованных отступов компонентов. Начинайте с общей оси, затем делайте исключение явным.

Относитесь к точному выравниванию и повторяющимся промежуткам как к инвариантам качества. Когда два края или два промежутка должны быть равны, различие в один отрисованный пиксель — это дефект, а не допустимое оптическое приближение. Проверяйте итоговые границы с помощью инструмента измерения при репрезентативных размерах окна, размерах текста и коэффициентах масштабирования дисплея. Сравнивайте координаты и расстояния; не утверждайте выравнивание только по беглому взгляду на скриншот.

Допуск в отрисованный пиксель — это правило проверки, а не разрешение править код сырыми смещениями в пунктах. Равные отношения должны вычисляться из одного класса или метода отступа, токена, определения сетки или общего отступа компонента. Если они различаются, исправляйте общего владельца. Учитывайте дробную раскладку и округление устройства (телефоны 2× и 3×, дробные масштабы на десктопе), чтобы намеченные оси попадали на один физический пиксель, а не расходились при определённых масштабах.

- При смешении размеров в одной строке выравнивайте текст по базовым линиям, а не по центрам ограничивающих рамок.
- Центрируйте значки в фиксированном слоте, чтобы подписи не смещались, когда у значков разная собственная ширина.
- Выравнивайте сопоставимые числа по концу и используйте табличные цифры (`tabular-nums`); прозу и идентификаторы выравнивайте по началу, если этого не требует локаль.
- Держите завершающие действия строк и индикаторы раскрытия в полосах фиксированной ширины.
- Выравнивайте элементы управления формы по их интерактивной рамке, а не по справочному тексту под ними.
- Используйте `justify-between` (`.justify_between()`) только тогда, когда две стороны действительно владеют противоположными краями; это не должно маскировать отсутствующую структуру посередине.
- Тонкие линии принадлежат владельцу границы. Две соседние области не должны обе рисовать один и тот же разделитель.
- Полоса прокрутки принадлежит прокручиваемой области и располагается у заднего края этой панели, редактора или окна. Отступы содержимого могут сдвигать текст и строки, но не должны затягивать полосу прокрутки в середину поверхности. Добавляйте отступы к содержимому scroll view, а не к самому scroll view.

<a id="density-tiers"></a>

### Уровни плотности

По умолчанию используется medium. Меняйте плотность для всего локального контекста, а не для одного изолированного элемента управления:

- **комфортная / large:** онбординг, разреженные формы, важные решения;
- **стандартная / medium:** большинство элементов интерфейса приложения и рабочих процессов, а также любой сенсорный экран;
- **компактная / small:** панели инструментов, меню, таблицы и повторяющиеся профессиональные данные на десктопе;
- **сверхкомпактная / xsmall:** исключительные утилиты высокой плотности, но никогда не автоматический выбор для всего приложения.

Используйте ограниченную шкалу, а не произвольные размеры. На сенсорных экранах каждая нажимаемая цель не меньше 44 × 44 пунктов
([Целевые области касания](https://nsh.dev/translations/ru/docs/accessibility.md#touch-targets)). С указателем кнопки обычно проходят примерно через рамки 24, 28
и 32 pt, поля ввода при размере large растягиваются примерно до 40 pt, а строки таблиц используют примерно 28, 32 и 40 pt. Задайте компонентам приложения размер, который одновременно меняет типографику, значок, отступы и область нажатия; пользовательская высота, меняющая только внешнюю рамку, обычно неполна.

<a id="text-size-base-font-and-points"></a>

### Размер текста, базовый шрифт и пункты

Хорошо спроектированная шкала сохраняет иерархию при росте текста. Увеличенный текст удачен, когда отношения между заголовком и основным текстом, элементом управления и значком, внутренними и внешними отступами, основной и второстепенной областями остаются теми же при любом размере, а не просто когда все объекты становятся больше.

Длины в NSH измеряются в пунктах, а выбранный пользователем размер текста (Dynamic Type на iOS, размер шрифта на Android, масштабирование текста на десктопах, собственная настройка хост-приложения) масштабирует текст: растёт каждый текст и поле ввода, допускающие масштабирование шрифта, а отступы, радиусы, границы и изображения сохраняют свой размер ([Размер текста](https://nsh.dev/translations/ru/docs/device-and-environment.md#text-size)). Базовый размер текста приложения (`text-base` в Tailwind, 16 пунктов) — это размер основного текста, вокруг которого выстраивается типографическая шкала.

Проектируйте в отношениях:

- ступени типографики сохраняют ту же иерархию вокруг базового размера основного текста;
- ступени отступов сохраняют те же отношения группировки вокруг типографики;
- рамки элементов управления и области нажатия растут вместе с подписями: задавайте их размер по содержимому и минимуму (`min-h-11`), а не фиксированной высотой;
- минимумы панелей и комфортная ширина учитывают увеличенный текст;
- радиусы скругления и оформление фокуса остаются оптически согласованными с рамкой элемента управления.

Поскольку текст растёт сам по себе, вокруг него ломается фиксированная рамка: более крупная подпись внутри кнопки фиксированной высоты, более крупный документ внутри фиксированных минимумов панелей или более крупные строки внутри фиксированной высоты строки разрушают исходный ритм и могут обрезать содержимое. Позвольте строкам переноситься или переключаться на столбец, когда текст становится крупным (`fontScale()`, `font_scale()`), и ограничивайте масштаб текста, который уже крупный, например заголовка 34pt (`maxFontSizeMultiplier`).

Как правило, раскладка приложения не должна содержать литеральные значения в пунктах (`p-[13px]`, `px(13.0)`). Используйте шкалу отступов
(`p-2`, `gap-3`, `w-64`, `text-sm` и их типизированные методы) или размеры компонентов приложения. Литеральные пункты применяйте только тогда, когда значение представляет физическую или растровую границу: тонкую линию, платформенный отступ окна, размер bitmap, минимальный допуск hit-test или геометрию, которая должна совпадать с внешней поверхностью. Это проверяемые, документированные исключения. Отступы продукта, типографика, размер значков и геометрия обычных элементов управления остаются на шкале.

Тестируйте при нескольких размерах текста, а не только по умолчанию. Проверяйте иерархию, перенос, усечение, минимальный размер окна, изменение размера панелей, зазор вокруг кольца фокуса, расположение всплывающих элементов и измерение строк списка.
<a id="surfaces-and-elevation"></a>

### Поверхности и возвышение

Используйте возвышение, чтобы показать порядок наложения, а не важность. Базовая поверхность плоская; её области задаются разделителями и контрастом фона. Всплывающие окна (popover), меню, листы (sheet), диалоги и уведомления могут использовать всё более сильные тени, потому что располагаются над остальным содержимым. Не добавляйте тень каждой карточке.

Все поверхности одного вида должны иметь единое оформление. Дайте семействам всплывающих элементов одну тематическую поверхность (токены `popover`, границу, `radius` и одну тень), чтобы `Popover`, `Menu`, `ContextMenu` и `Tooltip` не расходились по виду. Когда приложение вводит ещё одну привязанную поверхность, повторно используйте это оформление, а не приближайте его не связанными между собой литералами границы и тени.

<a id="typography-and-icons"></a>

### Типографика и иконки

Используйте системный шрифт интерфейса для текста интерфейса, а моноширинный — только для кода, идентификаторов, сочетаний клавиш и выровненных числовых данных. Сохраняйте основной текст читаемым и избегайте избыточных заглавных букв и межбуквенного интервала, особенно для CJK-текста.

Используйте в продукте одно семейство иконок. Иконки дополняют подписи; они не должны заменять непонятные действия догадками. Кнопкам только с иконкой всегда нужно доступное имя (`accessibilityLabel`). Их визуальное пояснение подчиняется политике подсказок ниже. Заливку или цвет иконки применяйте, чтобы передать состояние, а не просто чтобы оживить панель инструментов.

<a id="layout-patterns"></a>

## Паттерны компоновки

<a id="choose-a-stable-shell"></a>

### Выберите стабильную оболочку

Большинству приложений подойдёт одна из этих оболочек:

- **одно рабочее пространство:** заголовок или панель инструментов над одним основным представлением;
- **стек навигации:** страницы добавляются и убираются (`NavStack`), на телефонах работает системный жест «назад»;
- **рабочее пространство с боковой панелью:** постоянная навигация рядом со сменяемым подробным представлением;
- **master–detail:** панели коллекции и подробностей с изменяемым размером, на телефоне — две страницы;
- **рабочее пространство документов:** вкладки для нескольких долгоживущих объектов;
- **служебное окно:** одна сфокусированная задача с коротким фиксированным путём действий.

Сохраняйте глобальную навигацию неизменной, пока меняется содержимое. Отдавайте основной рабочей области оставшееся место с помощью `flex-1` (`.flex_1()`) и `min-w-0` / `min-h-0` там, где переполняющиеся дочерние элементы должны сжиматься. Используйте `Scroll` и `List` по их прямому назначению, вместо того чтобы заново собирать прокрутку или виртуализацию из вложенных представлений.

На настольных платформах собственная строка заголовка приложения ([`nsh/desktop`](https://nsh.dev/translations/ru/modules/nsh-desktop.md)) — это прежде всего рамка окна. Сохраняйте её область перетаскивания (`.window_drag(true)`) и не привязывайте обычные клики по заголовку к редким командам редактирования. Переименование должно находиться за явной командой объекта, при необходимости — с доступом с клавиатуры.

<a id="responsive-windows"></a>

### Адаптивные окна

Окно может быть любого размера: его задаёт пользователь, разделённый вид или приложение-хост. Решите, что происходит при сужении окна:

1. сохраните основную задачу;
2. позвольте областям с изменяемым размером дойти до документированного минимума;
3. сверните второстепенные подписи или инспекторы;
4. перенесите редко используемые действия в меню;
5. прокручивайте только ту область, содержимое которой действительно переполняет её.

Не скрывайте действие, не предоставив другого пути к нему. Избегайте прокрутки всего окна, когда прокручиваться должен только список или тело документа. Контрольные точки (`md:`, `lg:`) и `windowSize()` (`window()` в Rust) следуют за поверхностью, на которую выводит NSH.

У flex-компоновки NSH то же давление собственных размеров, что и в других системах компоновки: дочерний элемент `flex-1` всё равно может отказаться сжиматься вокруг длинного содержимого. Дизайн и реализация должны согласованно определять, какие панели могут сжиматься, обрезаться, переносить текст или прокручиваться. Обрезанная область также обрезает внешнее кольцо фокуса; никогда не жертвуйте видимостью клавиатурного фокуса ради упрощения обработки переполнения.

<a id="forms-and-settings"></a>

### Формы и настройки

Используйте видимую подпись для каждого поля и размещайте подсказку или проверку рядом с полем, которое она описывает. Выравнивайте связанные поля, но не загоняйте длинные подписи в узкую фиксированную колонку. Выбирайте подходящий элемент управления: `Checkbox` для независимых вариантов, [`RadioGroup`](https://nsh.dev/translations/ru/elements/radio.md) из `Radio` для небольшого видимого набора, `Menu` с вариантами для более длинного набора и [`Switch`](https://nsh.dev/translations/ru/elements/switch.md) для настройки, вступающей в силу немедленно. Их части (дорожка и бегунок переключателя, индикатор флажка) стилизует приложение, используя классы `checked:`, `indeterminate:` и `disabled:`, которые следуют за элементом управления:

```rust
use nsh_base::{switch, theme};

fn setting_switch(label: &'static str, on: Signal<bool>) -> impl IntoElement {
    switch()
        .checked(on)
        .on_change(move |next: &bool| on.set(*next))
        .flex_row()
        .items_center()
        .gap_3()
        .track_style(|s| {
            s.w(px(44.0))
                .p(px(2.0))
                .rounded_full()
                .bg(move || theme().muted)
                .transition_colors()
                .checked(|s| s.bg(move || theme().primary))
        })
        .thumb_style(|s| {
            s.size(px(20.0))
                .rounded_full()
                .bg(move || theme().background)
                .shadow_sm()
                .transition_transform()
                .checked(|s| s.translate_x(px(20.0)))
        })
        .child(text(label).text_color(move || theme().foreground))
}
```

```tsx
import { Text } from "nsh";
import { Switch } from "nsh/base";

function SettingSwitch(props: { label: string; on: boolean; onChange: (e: { checked: boolean }) => void }) {
  return (
    <Switch
      checked={props.on}
      onChange={props.onChange}
      class="flex-row items-center gap-3"
      trackClass="w-11 rounded-full bg-muted p-0.5 transition-colors checked:bg-primary"
      thumbClass="size-5 rounded-full bg-background shadow-sm transition-transform checked:translate-x-5"
    >
      <Text class="text-foreground">{props.label}</Text>
    </Switch>
  );
}
```

Флажок «выбрать всё» показывает частичный выбор с помощью неопределённого состояния:

```rust
use nsh_base::{checkbox, theme};

fn select_all(selected: Signal<usize>, total: usize) -> impl IntoElement {
    checkbox()
        .checked(move || selected.get() == total)
        .indeterminate(move || selected.get() > 0 && selected.get() < total)
        .on_change(move |all: &bool| selected.set(if *all { total } else { 0 }))
        .flex_row()
        .items_center()
        .gap_2()
        .indicator_style(|s| {
            s.size(px(20.0))
                .items_center()
                .justify_center()
                .rounded(move || theme().radius)
                .border_2()
                .border_color(move || theme().border)
                .checked(|s| s.bg(move || theme().primary).border_color(move || theme().primary))
                .indeterminate(|s| s.bg(move || theme().primary).border_color(move || theme().primary))
        })
        .indicator(text(move || mark(selected.get(), total)).text_color(move || theme().primary_foreground))
        .child(text("Select all").text_color(move || theme().foreground))
}

fn mark(selected: usize, total: usize) -> &'static str {
    if selected == total { "✓" } else if selected > 0 { "–" } else { "" }
}
```

```tsx
import { Text } from "nsh";
import { Checkbox } from "nsh/base";

function SelectAll(props: { selected: number; total: number; onChange: (e: { checked: boolean }) => void }) {
  return (
    <Checkbox
      checked={props.selected === props.total}
      indeterminate={props.selected > 0 && props.selected < props.total}
      onChange={props.onChange}
      class="flex-row items-center gap-2"
      indicatorClass="size-5 items-center justify-center rounded-(--radius) border-2 border-border checked:border-primary checked:bg-primary indeterminate:border-primary indeterminate:bg-primary"
      indicator={
        <Text class="text-primary-foreground">
          {props.selected === props.total ? "✓" : props.selected > 0 ? "–" : ""}
        </Text>
      }
    >
      <Text class="text-foreground">Select all</Text>
    </Checkbox>
  );
}
```

Отключайте отправку, пока операция выполняется, сохраняйте ввод пользователя и показывайте результат рядом с действием. Держите поля ввода над экранной клавиатурой (`env(keyboard-inset-height)`). Диалоги оставляйте для коротких, сфокусированных решений; для сценариев, требующих изучения или многих полей, используйте полную страницу или лист.

Индикация загрузки должна соответствовать реальному ожиданию. Переключение между панелями настроек, содержимое которых уже доступно, должно происходить мгновенно; скелетон уместен только пока содержимое действительно загружается и его форма известна.

<a id="components-and-composition"></a>

## Компоненты и композиция

Следуйте принципу Shadcn: компоненты — это строительный материал, а не закрытая дизайн-система. `nsh/base` предоставляет поведение; приложение задаёт согласованные значения по умолчанию в собственных компонентах и отвечает за композицию и семантику продукта.

- Используйте варианты компонентов по смыслу. Primary зарезервирован за явным действием по умолчанию, завершающим область решения, — обычно это действие, вызываемое клавишей Return на настольных платформах. Единственное, частое или желательное действие не становится primary автоматически. Команда `Add` на панели управления обычно использует кнопку default; основное действие формы `Create` может использовать primary. Destructive применяйте для разрушительного подтверждения, а ghost — для тихих действий панели инструментов.
- Предпочитайте явные части и слоты стилизации произвольных потомков.
- Сохраняйте единообразие повторяющегося паттерна во всём продукте. Оборачивайте его в компонент приложения, если он несёт доменный язык или политику.
- Используйте стандартный компонент для его семантической роли. `Menu`, `ContextMenu`, `Popover`, `Sheet`, `Dialog` и `AlertDialog` — не взаимозаменяемые блоки; у каждого свои контракты выбора, фокуса, клавиатуры, закрытия и компоновки, и на каждом устройстве он выглядит по-своему.
- Сохраняйте геометрию семейства компонентов. Строки меню имеют общие вертикальные и горизонтальные отступы, высоту, слоты для иконки и галочки, разделители, радиус и оформление состояний. Не имитируйте одно меню пользовательским всплывающим окном, чьи отступы лишь приближаются к системным.
- Не оборачивайте компонент только ради переименования всех его свойств или заморозки всех его возможностей.
- Выносите повторно используемое поведение без стилей продукта в `nsh/base` или в крейт, общий для продуктов приложения; тематическое, выраженно авторское оформление оставляйте в компонентах приложения.

```rust
use nsh_base::{button, theme};

/// The footer of a form: a default Cancel and the primary commit.
fn form_actions(cancel: impl Fn() + 'static, create: impl Fn() + 'static) -> impl IntoElement {
    view()
        .flex_row()
        .justify_end()
        .gap_2()
        .child(
            button()
                .on_press(cancel)
                .rounded(move || theme().radius)
                .border_1()
                .border_color(move || theme().border)
                .px_4()
                .py_2()
                .active(|s| s.bg(move || theme().muted))
                .child(text("Cancel").text_color(move || theme().foreground)),
        )
        .child(
            button()
                .on_press(create)
                .rounded(move || theme().radius)
                .bg(move || theme().primary)
                .px_4()
                .py_2()
                .active(|s| s.bg(move || theme().primary.opacity(0.9)))
                .child(text("Create").text_color(move || theme().primary_foreground)),
        )
}
```

```tsx
import { Text, View } from "nsh";
import { Button } from "nsh/base";

// The footer of a form: a default Cancel and the primary commit.
function FormActions(props: { onCancel: () => void; onCreate: () => void }) {
  return (
    <View class="flex-row justify-end gap-2">
      <Button onPress={props.onCancel} class="rounded-(--radius) border border-border px-4 py-2 active:bg-muted">
        <Text class="text-foreground">Cancel</Text>
      </Button>
      <Button onPress={props.onCreate} class="rounded-(--radius) bg-primary px-4 py-2 active:bg-primary/90">
        <Text class="text-primary-foreground">Create</Text>
      </Button>
    </View>
  );
}
```

<a id="interaction-states"></a>

## Состояния взаимодействия

<a id="make-the-result-understandable-before-the-click"></a>

### Сделайте результат понятным до клика

Элемент управления должен предсказывать свой результат. Используйте привычные платформенные элементы и их расположение, чтобы люди могли действовать, не изучая интерфейс заранее. Его подпись называет действие и объект, его состояние показывает доступность, а обратная связь подтверждает тот же исход.

Не называйте кнопку Button `Save`, если она открывает сценарий настройки, или `Delete`, если она лишь убирает элемент из группы. Указывайте область действия, если контекст не делает её очевидной. Реагируйте на активацию немедленно, предотвращайте повторную отправку во время длительной работы и показывайте результат рядом с изменившимся объектом. Добавляйте сообщение об успехе только тогда, когда сам результат не виден.

Каждый интерактивный элемент управления должен быть продуман для следующих состояний:

| Состояние | NSH | Требование к дизайну |
| --- | --- | --- |
| Покой | | Ясная аффорданс без визуального шума |
| Наведение | `hover:` | Тонкая обратная связь для указателя, никогда не единственный сигнал |
| Нажатие | `active:` | Мгновенная обратная связь при нажатии, стилизуется приложением |
| Открыто / нажато | `open` всплывающего окна | Постоянная обратная связь, пока открыто связанное всплывающее окно |
| Видимый фокус | `focus-visible:` | Контрастное кольцо клавиатурного фокуса |
| Выбрано / отмечено | `selected:`, `checked:` | Постоянное состояние, отличное от наведения |
| Отключено | `disabled:` | Сниженная выразительность и никакой вводящей в заблуждение реакции на наведение или нажатие |
| Загрузка | | Сохраните контекст, предотвратите повторное действие, объясните долгое ожидание |
| Ошибка | | Сообщите, что произошло и как восстановиться |

Привязывайте команды, которые должны работать с клавиатуры, с помощью [`keymap`](https://nsh.dev/translations/ru/modules/nsh.md#keymap) на настольных платформах и в вебе. Соответствуйте привычным платформенным сочетаниям клавиш (`"mod+k"` — это Command в macOS и Control в остальных системах), показывайте сочетания в меню или подсказках и держите фокус в логичном месте после открытия или закрытия оверлея.

Выбор — часть информационной модели, а не необязательная полировка. Вкладки, сегментированные варианты, выбираемые строки, фильтры и пункты навигации должны показывать постоянное выбранное состояние (`selected`, которое озвучивают программы чтения с экрана). Кнопка, владеющая меню, должна оставаться видимо нажатой или открытой, пока всплывающее окно не закроется; одно наведение не может объяснить связь между триггером и поверхностью.

```rust
use nsh_base::{tab, theme};

fn section_tab(label: &'static str, id: Section, current: Signal<Section>) -> impl IntoElement {
    let selected = move || current.get() == id;
    tab()
        .selected(selected)
        .on_press(move || current.set(id))
        .rounded(move || theme().radius)
        .px_3()
        .py_2()
        .selected_style(|s| s.bg(move || theme().muted))
        .child(text(label).text_color(move || {
            if selected() { theme().foreground } else { theme().muted_foreground }
        }))
}
```

```tsx
import { Text } from "nsh";
import { Tab } from "nsh/base";

function SectionTab(props: { label: string; selected: boolean; onPress: () => void }) {
  return (
    <Tab
      selected={props.selected}
      onPress={props.onPress}
      class="group rounded-(--radius) px-3 py-2 selected:bg-muted"
    >
      <Text class="text-muted-foreground group-selected:text-foreground">{props.label}</Text>
    </Tab>
  );
}
```

В сегментированном элементе управления заливка выбранного сегмента и поверхность невыбранных должны разделять силуэт контейнера. Ни одна из них не должна делать угол на конце прямым или делать границу тяжелее, чем у соседних.

Показывайте выбранный пункт навигации, строку списка или вкладку через собственную поверхность элемента: заливку выбора, более сильный цвет переднего плана или больший вес шрифта. Не добавляйте полосу на ведущем краю или одностороннюю границу как маркер выбора. Это привычка веб-шаблонов, а не нативное соглашение; она ломает скруглённый силуэт элемента и добавляет второй, конкурирующий край в колонку, которая уже выровнена по тексту.

Для разрушительных действий различайте обратимую и необратимую работу. Для обратимых изменений предпочитайте отмену или временное уведомление. Используйте [`AlertDialog`](https://nsh.dev/translations/ru/elements/alert-dialog.md), когда последствия серьёзны и не могут быть отменены; называйте в тексте подтверждения конкретный объект и последствие.

<a id="pointer-conventions"></a>

### Соглашения об указателе

Используйте курсор по умолчанию для кнопок, флажков, пунктов меню, вкладок и других элементов управления. Используйте указывающую руку (`cursor-pointer`) для ссылок и содержимого, которое ведёт себя как ссылка. Курсоры текста, изменения размера, захвата и запрета применяйте только тогда, когда они описывают выполняемое манипулирование. Курсор подкрепляет аффорданс; он не заменяет видимое состояние элемента или его доступную роль.

Делайте эффекты наведения умеренными, потому что у сенсорного, клавиатурного и вспомогательного взаимодействия наведения нет. Не показывайте при наведении единственную копию разрушительного или важного действия. Контекстные действия строки могут становиться менее заметными в покое, если те же команды остаются доступны через выделение, клавиатуру или контекстное меню.

<a id="prefer-command-surfaces-over-hover-toolbars"></a>

### Предпочитайте поверхности команд панелям при наведении

Выбирайте место действия по частоте команды и её области:

- держите основное или частое действие видимым как кнопку с подписью или привычный элемент панели инструментов;
- помещайте второстепенные действия текущей области за видимым триггером [`Menu`](https://nsh.dev/translations/ru/elements/menu.md);
- помещайте команды, действующие на объект под указателем или пальцем, в `ContextMenu`: щелчок правой кнопкой на настольных платформах, удержание пальца на сенсорных экранах;
- предоставляйте ту же важную команду через сочетание клавиш ([`keymap`](https://nsh.dev/translations/ru/modules/nsh.md#keymap)), если у неё есть естественная клавиатурная форма, и в меню приложения (`Menu.setApplicationMenu` из [`nsh/desktop`](https://nsh.dev/translations/ru/modules/nsh-desktop.md));
- используйте иконку, появляющуюся при наведении, только как быстрый путь к команде, остающейся доступной в другом месте.

Это больше, чем визуальное предпочтение. Меню `nsh/base` уже обеспечивают размещение у триггера, каскадные подменю на настольных платформах и лист с подменю на месте на телефонах, закрытие выбором пункта, нажатием снаружи, Escape или «назад», а также роли menu и menu item для программ чтения с экрана. Пользовательской полосе кнопок при наведении придётся заново реализовывать это поведение, и она невидима для сенсорных, исключительно клавиатурных и многих вспомогательных сценариев.

```rust
use nsh_base::{menu, menu_item, menu_separator, theme};
use nsh_core::{color::black, element::StyleRefinement};

/// The app's popup surface, shared by its menus, popovers and tooltips.
fn popup(s: StyleRefinement) -> StyleRefinement {
    s.min_w(px(192.0))
        .p_1()
        .rounded(move || theme().radius)
        .border_1()
        .border_color(move || theme().border)
        .bg(move || theme().popover)
        .shadow_lg()
}

fn item(label: &'static str, on_select: impl Fn() + 'static) -> impl IntoElement {
    menu_item()
        .on_select(on_select)
        .px_3()
        .py_2()
        .rounded(move || theme().radius)
        .hover(|s| s.bg(move || theme().muted))
        .active(|s| s.bg(move || theme().muted))
        .child(text(label).text_color(move || theme().foreground))
}

fn more_menu(rename: impl Fn() + 'static, duplicate: impl Fn() + 'static, delete: impl Fn() + 'static) -> impl IntoElement {
    menu()
        .offset(px(4.0))
        .popup_style(popup)
        .backdrop_style(|s| s.bg(black().opacity(0.5)))
        .trigger(
            view()
                .label("More actions")
                .size(px(44.0))
                .items_center()
                .justify_center()
                .rounded_full()
                .active(|s| s.bg(move || theme().muted))
                .child(text("⋯").text_color(move || theme().foreground)),
        )
        .child(item("Rename…", rename))
        .child(item("Duplicate", duplicate))
        .child(menu_separator().h(px(1.0)).my_1().bg(move || theme().border))
        .child(item("Delete…", delete))
}
```

```tsx
import { Text, View } from "nsh";
import { Menu, MenuItem, MenuSeparator } from "nsh/base";

/** The app's popup surface, shared by its menus, popovers and tooltips. */
export const popup = "min-w-48 rounded-(--radius) border border-border bg-popover p-1 shadow-lg";
const item = "rounded-(--radius) px-3 py-2 hover:bg-muted active:bg-muted";

function MoreMenu(props: { onRename: () => void; onDuplicate: () => void; onDelete: () => void }) {
  return (
    <Menu
      offset={4}
      popupClass={popup}
      backdropClass="bg-black/50"
      trigger={
        <View accessibilityLabel="More actions" class="size-11 items-center justify-center rounded-full active:bg-muted">
          <Text class="text-foreground">⋯</Text>
        </View>
      }
    >
      <MenuItem onSelect={props.onRename} class={item}><Text class="text-foreground">Rename…</Text></MenuItem>
      <MenuItem onSelect={props.onDuplicate} class={item}><Text class="text-foreground">Duplicate</Text></MenuItem>
      <MenuSeparator class="my-1 h-px bg-border" />
      <MenuItem onSelect={props.onDelete} class={item}><Text class="text-foreground">Delete…</Text></MenuItem>
    </Menu>
  );
}
```

Выбирайте `Menu`, когда пользователям нужен видимый признак существования других команд, например в переполнении панели инструментов, действиях с документом или меню учётной записи. Выбирайте `ContextMenu` для команд, относящихся к выделению или объекту, таких как переименование, дублирование, показ или удаление. Контекстное меню не должно быть единственным способом выполнить важную команду; предоставьте путь через строку меню, панель инструментов, клавиатуру или подробное представление в зависимости от случая.

Не помещайте каждое действие в меню, чтобы экран выглядел минималистично. Важны обнаруживаемость и скорость: главное действие остаётся видимым, опасные пункты остаются чётко подписанными и отделёнными, а пункт меню должен использовать один и тот же глагол, иконку, сочетание клавиш, состояние доступности и результат везде, где он появляется.
<a id="button-means-application-action-link-means-external-resource"></a>

### Button означает действие приложения; Link означает внешний ресурс

Используйте `Button`, когда активация меняет состояние приложения, подтверждает решение, открывает инструмент, отправляет данные или выполняет команду. Вид кнопки выбирается по локальной иерархии:

- primary button — для единственного выделенного действия-обязательства в области принятия решения;
- default button — для обычных видимых действий;
- outline button — когда действию нужна чёткая граница при меньшем акценте;
- ghost button — для привычных малозаметных действий на панелях инструментов и внутри текста;
- icon button — только для общеизвестного символа, с доступным именем и подсказкой (tooltip), если значение или область действия неочевидны.

Не назначайте primary только потому, что кнопка — единственное действие на экране, расположена в правом верхнем углу или команда хочет больше кликов. Primary сообщает о действии по умолчанию и определяет поведение клавиатуры. Если активация — всего лишь обычная команда, например добавление элемента, открытие инструмента или обновление представления, используйте default, outline или ghost button в соответствии с её местом в иерархии.

Подчёркнутую ссылку (`role="link"`) используйте только для внешнего ресурса: URL, веб-страницы, онлайн-документации или адреса электронной почты. Она показывает курсор-указатель в виде руки, потому что её контракт — выход из текущего контекста приложения к этому ресурсу. Не используйте стиль ссылки, чтобы сделать функциональную команду менее заметной. Действия Delete, Save, Refresh, Add, Open-menu или навигация внутри приложения, оформленные как ссылка, скрывают возможности элемента и дают неверную доступную роль.

Слово «View» не превращает назначение внутри приложения в ссылку. Полный отчёт, анализ, панель сведений или локальная запись по-прежнему открываются через кнопку, строку, карточку, вкладку или элемент раскрытия. Используйте краткие подписи, учитывающие контекст, например `Full analysis`, когда содержащая карточка уже указывает, что откроется; подчёркивание оставьте для ресурса, который действительно открывается в браузере или почтовом клиенте.

Вся внутренняя навигация — строки боковой панели, вкладки, «хлебные крошки», элементы списков, открытие локального представления или переключение рабочих пространств — должна использовать стек навигации, `Button` или соответствующий компонент. Визуальный акцент выбирается через вариант кнопки или выбранное состояние компонента навигации, но никогда не за счёт искажения семантики.

<a id="feedback-and-overlays"></a>

## Обратная связь и оверлеи

Выбирайте наименьшую поверхность, подходящую для решения:

- tooltip: короткое пояснение или сочетание клавиш ([`Tooltip`](https://nsh.dev/translations/ru/elements/tooltip.md) или `tooltip` любого элемента);
- popover: контекстные элементы управления, не прерывающие задачу ([`Popover`](https://nsh.dev/translations/ru/elements/popover.md));
- menu: компактный список действий ([`Menu`](https://nsh.dev/translations/ru/elements/menu.md));
- notification: асинхронный статус, не требующий решения;
- dialog: сфокусированное решение или короткая форма ([`Dialog`](https://nsh.dev/translations/ru/elements/dialog.md));
- alert dialog: явное подтверждение действия с серьёзными последствиями ([`AlertDialog`](https://nsh.dev/translations/ru/elements/alert-dialog.md));
- sheet: дополнительная работа, которой нужно более постоянное пространство ([`Sheet`](https://nsh.dev/translations/ru/elements/sheet.md)).

Каждый из них принимает форму, ожидаемую на устройстве, из одной структуры, которую стилизует приложение (`popupClass`, `backdropClass`):

| | Настольные компьютеры | Телефоны и планшеты |
| --- | --- | --- |
| `Tooltip` | после того как указатель задержится на элементе | только когда его открывает приложение |
| `Popover` | привязан к элементу; нажатие снаружи закрывает его и обрабатывается дальше | привязан к элементу; нажатие снаружи только закрывает его |
| `Menu` | всплывающее окно у источника вызова, каскадные подменю | панель (sheet) снизу; подменю занимает место своих элементов |
| `ContextMenu` | вторичная кнопка, в позиции указателя | удержание пальцем открывает панель (sheet) |
| `Sheet` | боковая панель поверх затемнённой подложки | то же самое |
| `AlertDialog` | предупреждение в стиле диалогов платформы | системное предупреждение или панель действий |

Tooltip добавляет недостающий смысл и не пересказывает очевидное действие. Используйте его для незнакомого символа, неоднозначной области действия или полезного сочетания клавиш, которое иначе скрыто. Повторяющиеся узнаваемые команды, такие как Copy и Download в нижней части сообщения, требуют доступных имён и доступа с клавиатуры, но подписи при наведении добавляют лишние помехи, а не ясность. Телефоны показывают tooltip только когда его открывает приложение, поэтому tooltip не должен содержать то, что нужно пользователю сенсорного устройства.

Alert нарушает визуальную иерархию, даже если не открывает модальное окно. Используйте его для важной, исключительной информации, требующей внимания в текущей задаче, а не как декоративный контейнер для обычных описаний, советов или пустого места. Если содержимому не нужно немедленное внимание или действие, предпочитайте встроенную справку, приглушённый текст или обычный раздел.

Избегайте наложения оверлеев друг на друга. Escape и кнопка «Назад» в Android закрывают самый верхний закрываемый слой; синхронизируйте состояние приложения через `onOpenChange` и возвращайте фокус на источник вызова или следующую логичную цель.

Действие в оверлее должно относиться к объекту или состоянию, которое оверлей действительно показывает. Например, показывайте `Clear history` только когда виден отдельный раздел недавней истории и в нём есть записи. Результаты поиска, недавние элементы и избранное — разные коллекции; подписывайте и разделяйте их, а не сливайте в один необъяснённый список. Скройте неприменимое действие или отключите его с полезным объяснением — не размещайте в нижней части двусмысленную иконку корзины.

Нижняя часть — не свалка для возможностей, которым не нашлось места в дизайне. В ней могут быть сочетания клавиш, статус или действия, относящиеся ко всей поверхности, но каждый элемент должен отвечать на вопросы: что его объект, почему он доступен сейчас, на что он влияет и какое видимое состояние изменится после активации?

<a id="motion"></a>

## Анимация

Анимация объясняет изменение, а не служит фоновым украшением. Используйте короткие переходы для появления, закрытия, раскрытия и пространственной непрерывности. Избегайте анимации крупных изменений макета, когда ту же связь передаёт прозрачность или трансформация. Учитывайте настройки уменьшения движения (`motion-reduce:`, `reduceMotion()`), никогда не требуйте анимации для понимания состояния и не добавляйте анимацию по умолчанию к каждому компоненту.

```rust
view()
    .on_press(open)
    .transition_transform()
    .duration(150.0)
    .when(!reduce_motion(), |v| v.active(|s| s.scale(0.97)))
```

```tsx
<View onPress={open} class="transition-transform duration-150 active:scale-[0.97] motion-reduce:active:scale-100" />
```

Политика анимации относится к уровню приложения. `nsh/base` отвечает за механизм жизненного цикла или геометрию, необходимые для перехода, но не решает, что в каждом продукте элементы должны плавно проявляться или скользить. Задавайте независимо анимируемым значениям стабильную идентичность и делайте так, чтобы прерывание плавно разворачивалось от текущего измеренного значения, а не перезапускалось от прежней конечной точки.

Анимация следует непрерывности взаимодействия. Пока указатель или позиция прокрутки перемещается между связанными целями, сохраняйте поверхность предпросмотра стабильной и обновляйте её содержимое или позицию на месте. Закрывайте её только после завершения взаимодействия; повторяющееся затухание и повторное появление при каждом обновлении воспринимается как мерцание.

<a id="designing-data-heavy-interfaces"></a>

## Проектирование интерфейсов с большим объёмом данных

Плотность не означает тесноту. В таблицах, деревьях, длинных списках и редакторах:

- сохраняйте заголовки и основную идентичность строк визуально стабильными;
- выравнивайте сопоставимые значения и используйте табличные цифры там, где это уместно;
- различайте фокус, наведение, активную строку и множественное выделение;
- держите сортировку и фильтрацию видимыми и обратимыми;
- сохраняйте выделение по идентичности в предметной области при фильтрации и изменении порядка;
- виртуализируйте большие коллекции (`List`), не меняя семантику клавиатуры;
- используйте постепенное раскрытие для второстепенных столбцов и инспекторов;
- предоставляйте полезное пустое состояние, объясняющее следующее действие.

Выбирайте таблицу для сравнения по единообразным полям, список — для просмотра разнородных элементов, дерево — для настоящей иерархии. Не используйте сложный макет данных как визуальный стиль.

<a id="interface-language"></a>

## Язык интерфейса

Слова — часть архитектуры интерфейса. Составляйте словарь функции как систему — назначения, объекты, команды, состояния и результаты, — а не как изолированные переводы деталей реализации. Предпочитайте самую короткую формулировку, которая остаётся точной в реальном контексте.

<a id="let-context-carry-context"></a>

### Пусть контекст несёт контекст

Не повторяйте информацию, которую уже задаёт окружающая поверхность. Пункт боковой панели обычно сам является объектом или областью: используйте `Users`, а не `User Management`; `Shortcuts`, а не `Shortcut Configuration Management`. Столбец, строки которого уже содержат действия, может обойтись без общего заголовка `Operation`. Диалогу с заголовком `Delete “Roadmap”?` не нужен основной текст, повторно задающий тот же вопрос.

Это экономия контекста, а не удаление ради удаления. Добавляйте текст, когда он меняет решение: указывает затрагиваемую область, необратимое последствие, необычное предварительное условие или способ восстановления. Каждое лишнее слово должно отвечать на вопрос, на который текущий макет ещё не отвечает.

Используйте существительные для назначений и объектов (`Users`, `Appearance`, `Orders`), глаголы для команд (`Save`, `Duplicate`, `Export`), а прилагательные или короткие фразы — для состояний (`Offline`, `Up to date`, `Pending review`). Избегайте оберток вроде `Management`, `Module`, `Page`, `Function`, `Operation` и `System`, если слово не отличает реальное понятие предметной области.

<a id="write-each-language-do-not-translate-its-shape"></a>

### Пишите на каждом языке, а не переводите его форму

Начинайте с общего замысла, иерархии и терминологии, затем составляйте текст для каждой локали как естественный язык интерфейса. Не сохраняйте порядок слов исходного языка, количество слов, вежливые слова-наполнители или грамматическую категорию. Английское `Users` может выражать китайское понятие функции, которое буквально развернулось бы в «управление пользователями»; верность означает сохранение цели, а не сохранение лексем.

Убирайте слова, которые предоставляет окружающая информационная архитектура. Внутри поверхности `Settings` назначение часто называется просто `Account`, а не `Account Settings` и никогда не неестественным единственным числом `Account Setting`. Правильная подпись выбирается по роли и соседним элементам, а не по отдельной исходной фразе.

Ведите небольшой словарь продукта для повторяющихся объектов, команд и состояний. Используйте один и тот же термин на панели инструментов, в меню, контекстном меню, диалоге, поиске сочетаний клавиш и документации, если только контекст действительно не меняет его значение. Проверяйте текст на отрисованной поверхности: соседние подписи часто выявляют повторы или непоследовательную область действия, которые не видны в файле локализации.

В локализованных технических текстах сохраняйте устоявшийся термин фреймворка, когда перевод был бы менее точным. Сохраняйте идентификаторы API в исходной форме и оформляйте их как код. Не оставляйте обычные иностранные слова только ради технического звучания. При необходимости объясняйте сохранённый термин при первом употреблении, затем используйте одну и ту же форму во всём интерфейсе, документации и примерах API.

В документации используйте точное английское название именованного API, компонента, исходного файла или руководства в тексте ссылки, в том числе на локализованных страницах. Например, пишите `View`, `Button`, `Menu` и `Text`, а не переводите их названия и не добавляйте лишний префикс `NSH`. Окружающие пояснения оставляйте на языке страницы. Ссылкой делайте только именованную цель; слова вроде «исходный код» размещайте вне ссылки, когда они объясняют, почему источник приведён.

В технической сравнительной таблице, общей для разных локалей, сохраняйте названия возможностей, фреймворков, API и моделей в их устоявшейся английской форме. Переводите пояснения вокруг таблицы, а не её технические подписи. Общие слова интерфейса с устоявшимся местным названием по-прежнему можно локализовать вне контекста такой сравнительной таблицы.
<a id="buttons-and-confirmation-dialogs"></a>

### Кнопки и диалоги подтверждения

По умолчанию подписи кнопок короткие, обычно из одного или двух слов, и описывают результат, а не жест или компонент.
Предпочитайте `Save`, `Move` или `Delete` вариантам `Click to save`, `Perform move` или `Confirm deletion`. Используйте `Cancel` последовательно
для действия, которое закрывает диалог без применения изменений. Оставляйте `OK` только для подтверждения прочтения сугубо информационного содержимого.

Краткость — это значение по умолчанию, а не ограничение по числу символов. Намеренно более длинная подпись лучше, если её слова раскрывают последствие
или различают варианты, которые пользователи могли бы перепутать, например `Delete from this group` и `Delete everywhere`, or `Restart without saving`. Длина должна давать информацию, необходимую для решения, и не должна повторять
заголовок или текст диалога.

Когда возможно, используйте в качестве подписи подтверждения самый конкретный и краткий результат:

| Контекст | Слабый вариант | Предпочтительный вариант |
| --- | --- | --- |
| Диалог удаления | `Yes`, `Sure`, `Confirm deletion` | `Delete` |
| Несохранённые изменения | `Confirm`, `Yes` | `Discard changes` |
| Простое подтверждение прочтения | `Confirm operation` | `OK` или `Done` |
| Сложное согласие, результат которого нельзя выразить одним глаголом | `Yes` | `Confirm` |

`Confirm` — полезный запасной вариант, когда окружающий диалог полностью описывает сложное обязательство и ни один более короткий глагол результата
не подходит точно. Он не должен заменять понятную команду. `Sure` — разговорная форма, а не устойчивая английская команда, и
она слишком неоднозначна для стандартного словаря.

Диалог подтверждения должен представлять собой одно компактное решение:

- заголовок: решение или условие, например `Delete “Roadmap”?`;
- текст: только новая информация об области действия, последствиях или восстановлении;
- действия: `Cancel` и результат, например `Delete`;
- деструктивное оформление: применяется к деструктивному результату (действие `AlertDialog` с ролью `destructive`) и не
  заменяет точные формулировки.

Избегайте общих заголовков, таких как `Notice`, `Warning`, `Error` и `Confirmation`, если можно назвать реальное условие.
Избегайте ритуальных фраз вроде «Вы уверены, что хотите…», «Не хотели бы вы…», «Обратите внимание, что…» и
«успешно», если структура или состояние и так это сообщают. Вежливость должна выражаться спокойным, уважительным
тоном, а не повторяющимся `please`.

<a id="capitalization-punctuation-and-symbols"></a>

### Регистр, пунктуация и символы

По умолчанию используйте регистр предложения для английского UI: `Reset layout`, а не `Reset Layout` или `RESET LAYOUT`. Сохраняйте имена
собственные и устоявшиеся аббревиатуры. Следуйте платформенному соглашению, например заглавным буквам в каждом слове для нативных команд меню, только когда
интеграция с платформой от этого выигрывает, и применяйте это соглашение последовательно внутри класса компонентов.

ЗАГЛАВНЫЕ БУКВЫ могут давать сдержанный типографский акцент для очень коротких заголовков разделов, надзаголовков, статусов,
устоявшихся аббревиатур и кодоподобных идентификаторов. Их компактная форма и выверенный трекинг могут создавать уровень, схожий с жирным шрифтом, но
им не место на кнопках, в длинных заголовках, предложениях и плотных списках. Не сочетайте в одной области заглавные буквы, насыщенный цвет и
жирное начертание и не преобразуйте автоматически каждую строку: названия продуктов, аббревиатуры и
локализованное содержимое должны сохранять предполагаемый регистр.

Подписи, кнопки, пункты меню, вкладки, заголовки, плейсхолдеры и короткие состояния не заканчиваются точкой. Законченные
поясняющие предложения, предупреждения и сообщения об ошибках заканчиваются. Избегайте восклицательных знаков в обычных сообщениях об успехе и неудаче.
В китайском используйте полноширинную пунктуацию в предложениях и опускайте завершающие знаки в коротких подписях элементов управления по тому же
смысловому правилу.

Используйте один символ многоточия (`…`), а не три точки. Добавляйте его к каждой кнопке или пункту меню, которые открывают диалог,
лист или отдельное окно, а также к команде, которой для завершения нужны дополнительный ввод или выбор, например
`Settings…` или `Export…`. Команда, выполняемая немедленно, многоточия не получает. Для обозначения выполняющейся работы используйте неопределённый индикатор
прогресса, а не декоративные точки.

Ошибки должны сообщать, что произошло, и при необходимости следующее действие для восстановления. Обратная связь об успехе должна называть получившееся
состояние, только если оно ещё не видно. Предпочитайте `Couldn’t save. Check your connection and try again.` техническому коду или длинному извинению; опускайте тост `Saved successfully`, когда документ
визуально становится сохранённым.

<a id="internationalization-and-platform-fit"></a>

## Интернационализация и соответствие платформе

Имена и подписи для доступности (A11y) могут оставаться на английском; локализация этих строк не требуется. Сохраняйте
собственную подпись элемента, называя управляющий им элемент, даже если эта подпись локализована. Это исключение не
распространяется на видимый текст интерфейса.

Текст должен выдерживать расширение, типографику CJK, письмо справа налево и различную нотацию сочетаний клавиш. Не задавайте размер
элемента управления по одной английской подписи. Не размещайте текст в растровых ресурсах, избегайте склейки переведённых фрагментов и позволяйте
подписям переноситься или обрезаться только там, где продукт определяет путь восстановления, например подсказку.

Учитывайте различия платформ, несущие смысл: Command и Control, нативные оформления окон, системный внешний вид, поведение полос прокрутки, меню, возможности уведомлений, а на телефонах — системный заголовок, жест «назад» и
оповещения. Сохраняйте информационную архитектуру продукта одинаковой на всех платформах, но не уничтожайте привычное поведение платформы
ради поверхностного попиксельного совпадения.

Приложения NSH работают на телефонах, планшетах, десктопах и в вебе, часто внутри хост-приложения. Проектируйте для каждого случая:

- **Системный UI сохраняет вид платформы.** На iOS и Android заголовок страницы системный (`NavPage` и его `title`, а также
  параметры заголовка); на десктопах и в вебе показывается слот страницы `header`, который рисует приложение. Задавайте оба для каждой страницы
  ([Navigation](https://nsh.dev/translations/ru/docs/navigation.md#navpage-and-the-header)). Подтверждения используют `AlertDialog`; строка состояния следует
  странице или диалогу сверху (`statusBar`).
- **Тема хоста.** Встроенному приложению можно передать тему его хоста; классы и привязки Rust, читающие токены,
  следуют ей нативно ([Theme tokens](https://nsh.dev/translations/ru/docs/tailwind.md#theme-tokens)).
- **Безопасные области и клавиатура.** Держите содержимое внутри безопасной области (`env(safe-area-inset-*)`, `safeAreaInsets()`), а поля ввода —
  над экранной клавиатурой (`env(keyboard-inset-height)`) ([Device and environment](https://nsh.dev/translations/ru/docs/device-and-environment.md)).
- **Внешний вид, размер текста и анимация.** Каждая поверхность работает в светлой и тёмной темах (`dark:`, `colorScheme()`), при
  пользовательском размере текста и с уменьшенной анимацией (`motion-reduce:`, `reduceMotion()`).
- **Письмо справа налево.** Направление раскладки следует локали, а стиль `direction` задаёт его для представления и его
  поддерева; свойства начала и конца зеркалируются ([Right to left](https://nsh.dev/translations/ru/docs/layout.md#right-to-left)).
- **Касание и указатель.** Цели касания не меньше 44 × 44 пунктов; наведение и подсказки существуют только при наличии указателя.

```rust
use nsh_base::nav_page;

nav_page()
    .title("Inbox")
    .header(page_header("Inbox"))
    .child(inbox())
```

```tsx
import { NavPage } from "nsh/base";

<NavPage title="Inbox" header={<PageHeader title="Inbox" />}>
  <Inbox />
</NavPage>
```

<a id="guidance-for-ai-generated-interfaces"></a>

## Рекомендации для интерфейсов, созданных ИИ

ИИ, изменяющий интерфейс NSH, должен сначала изучить ближайшую функцию, компоненты приложения и токены темы, а также
документацию и объявления компонентов (`.nsh/types`). Перед генерацией кода он должен сформулировать основную задачу, владельца состояния, композицию
компонентов, а также пути работы с клавиатуры и касанием. Он не должен выводить API из примеров React, React Native
или Shadcn и придумывать свойство или метод NSH только потому, что имя кажется правдоподобным; `nsh check` и проверка классов при сборке
сообщают о том, чего не существует.

Результат ИИ неполон, пока человек не сможет объяснить, почему иерархия, плотность, выбор компонентов и исключительные
буквальные значения уместны в этом продукте. Правдоподобный на вид скриншот не является доказательством: поведение клавиатуры и касания,
фокус, динамическое содержимое, темы, изменение размера, размер текста и состояния сбоя — часть дизайна.

<a id="accessibility-checklist"></a>

## Чек-лист доступности

Прежде чем считать экран завершённым, убедитесь, что:

- каждое действие доступно и выполнимо с клавиатуры, касанием и через средство чтения с экрана;
- порядок фокуса соответствует визуальному порядку и порядку задач;
- фокус остаётся видимым и восстанавливается после оверлеев;
- у элементов управления есть доступные имена; элементы только с иконкой используют подсказки, когда их значение, область действия или сочетание клавиш
  требуют пояснения;
- у текста и значимых границ достаточный контраст;
- статус не передаётся только цветом;
- отключённое состояние и состояние «только для чтения» различимы;
- подписи, ошибки и описания остаются рядом со своими элементами управления;
- содержимое остаётся удобным при более длинных переводах, письме справа налево и увеличенном тексте;
- цели касания не меньше 44 × 44 пунктов, а цели для указателя имеют комфортный размер даже в плотной раскладке.

<a id="design-review-checklist"></a>

## Чек-лист дизайн-ревью

Ревью не перечисляет компоненты, а оценивает, принял ли интерфейс верные решения. Задавайте вопросы по порядку:

1. **Понятна ли задача?** Может ли новый пользователь распознать назначение, основное действие и следующий шаг, не обучаясь,
   не гадая и не экспериментируя?
2. **Выполняет ли каждое действие своё обещание?** Описывают ли подпись, элемент управления, состояние, область действия, обратная связь и результат
   один согласованный исход?
3. **Иерархия решительная и сдержанная?** Получает ли ключевая функция заслуженное пространство, в то время как насыщенный цвет,
   жирный шрифт, бейджи, оповещения и основные кнопки остаются редкими?
4. **Может ли интерфейс делать меньше, но лучше?** Можно ли убрать, объединить или отложить точку входа, параметр или состояние,
   не ослабляя задачу целиком?
5. **Точна ли структура?** Есть ли у равноправных элементов общие оси выравнивания, равны ли равные промежутки до пикселя отрисовки и
   находятся ли полосы прокрутки на краю реальной прокручиваемой области?
6. **Следует ли он системе компонентов?** Сохраняют ли стандартные элементы управления геометрию, состояния, поведение клавиатуры и
   модель закрытия, а внешний вид задаётся токенами темы и масштаба?
7. **Остаётся ли он удобным в каждом состоянии и при каждом ограничении?** Проверьте поведение клавиатуры, касания и фокуса,
   состояния пустоты, загрузки, сбоя и разрешений, более длинные переводы, увеличенный текст, письмо справа налево, обе темы оформления,
   минимальный размер окна и уменьшенную анимацию.
8. **Проверен ли он на реальных платформах?** Выполните задачу с реальными компонентами, текстом и репрезентативным содержимым
   на каждой платформе, для которой он поставляется (`nsh run --target …`, `nsh test --target …`), а не только на идеальном скриншоте.
