К содержимому

6. Адаптивная раскладка (breakpoints) ​

Выгода: один .sharq-экран работает на desktop и мобильных без отдельной сборки. Направьте экран на ось breakpoint ниже — его токены перестроят раскладку для узкой панели вместо ручной пересборки под мобильные.

Адаптация под размер экрана использует одну ось: SusBreakpointService.

Нет отдельного сервиса High/Low resolution (SusResolutionService удалён) и нет автоматического масштабирования панели, привязанного к размеру монитора. Density (.density-compact / .density-comfortable) — это ручной пресет продукта, а не адаптация под размер экрана.

PanelSettings у сэмплов используют ConstantPixelSize, поэтому ширина для breakpoint отслеживает ширину панели / Game view (без авто-масштабирования Unity ScaleWithScreenSize).

csharp
// Inside SusComponent (injected automatically):
BreakpointService.Current.Value  // Prop<Breakpoint>
BreakpointService.IsMobile       // Computed<bool> — width ≤ 1024
BreakpointService.IsTablet       // Computed<bool> — Md | Lg
BreakpointService.IsDesktop      // Computed<bool> — width ≥ 1920

Как измеряется ширина ​

Классы (.breakpoint-*) всегда находятся на корне token cascade (SusBootstrap.TokenCascadeRoot — тот же элемент, что хранит theme/density).

Источник ширины (тот же путь, что использовал старый resolution-сервис):

  1. Основной: cascadeRoot.resolvedStyle.width (и layout.width при необходимости)
  2. Fallback: размер visualTree панели
  3. Последний resort: размер Editor Game view / Screen.width (только когда root ещё не выложен)

Обновления запускаются:

  • SusBreakpointService.Attach(root) из LoadTokenCascade / Mount
  • Каждый GeometryChangedEvent от SusComponent → проталкивает ширину cascade-root (явный Attach(cascadeRoot).Update(width), тот же паттерн, что у удалённого SusResolutionService.Update(cascadeRoot, width))
  • Лёгкий поллинг + geometry-хук visualTree панели (перетаскивание Editor Game view часто пропускает geometry только на content root)

Не используйте собственную ширину дочернего компонента для выбора класса на root — это некорректно переключит токены для всего дерева.

Совет для Editor ​

Если Game view зафиксирован на конкретном разрешении (например, 1920×1080), ширина панели может не меняться, даже если вы просто ресайзите закреплённое окно. Используйте Free Aspect и перетаскивайте ширину через пороги ниже, либо принудительно задайте breakpoint в Storybook (select Breakpoint → sm / md / …).

Breakpoints ​

ИмяШиринаКласс на root
Sm≤ 640px.breakpoint-sm
Md≤ 1024px.breakpoint-md
Lg≤ 1440px.breakpoint-lg
Xl≤ 1920px.breakpoint-xl
Xxl> 1920px.breakpoint-2xl

Токены (--sk-*) ​

UI-пакеты, построенные на core (kit, game), переопределяют spacing / heights / fonts через .breakpoint-* в собственной таблице токенов. Компоненты уже используют var(--sk-button-height), var(--sk-space-16), var(--sk-font-body) и т.д. — они реагируют автоматически при смене класса на root.

Не все --sk-* токены перемещаются вместе с breakpoint. Размеры разбиты по семействам — spacing, control height, размер шрифта и несколько других переопределяются; border width, radius (кроме pill, который следует за собственной height), letter spacing, 9-slice границы и небольшие оптические коррекции остаются неизменными на каждом breakpoint специально. Полный список семейств и обоснование для каждого инвариантного — см. Design tokens §6.1.

Breakpoint и density (.density-compact / .density-comfortable) оба отвечают на вопрос "сколько здесь места", поэтому проект должен уметь их комбинировать и получать один предсказуемый результат, а не молчаливое переопределение одного другим.

Статус: объединение breakpoint и density предсказуемым образом, и сбор всех комбинаций breakpoint × density в одну сгенерированную таблицу токенов — часть предстоящего выпуска kit — см. changelog kit. Сегодня эти две оси ещё могут конкурировать на некоторых breakpoint; эта заметка будет заменена на простое описание нового поведения как только выпуск выйдет.

Использование в компоненте ​

csharp
public class ResponsivePanel : SusComponent
{
    public Prop<float> PanelWidth = new(300f);

    protected override void Created()
    {
        Watch(BreakpointService.Current, (old, bp) =>
        {
            PanelWidth.Value = bp >= Breakpoint.Xl ? 400f : 300f;
        });
    }
}

USS: переопределение через токен, а не селектор breakpoint ​

Предпочитайте токен вместо ручного написания селектора .breakpoint-* на проектном классе — токен уже несёт правильное значение для каждого breakpoint, и продолжит работать после разрешения breakpoint и density вместе (см. заметку о статусе выше):

css
/* Предпочтительно — токен уже варьируется вместе с breakpoint */
.responsive-panel { width: var(--sk-tooltip-max-width, 300px); }
css
/* всё ещё парсится, но переопределение проектного класса такой формы может перестать выигрывать
   как только breakpoint и density объединятся с большей специфичностью — переопределите токен вместо этого */
.responsive-panel { width: 300px; }
.breakpoint-xl .responsive-panel { width: 400px; }

API ​

csharp
public class SusBreakpointService
{
    public Prop<Breakpoint> Current { get; }
    public Computed<bool> IsMobile { get; }   // ≤ 1024
    public Computed<bool> IsTablet { get; }   // Md | Lg
    public Computed<bool> IsDesktop { get; }  // ≥ 1920

    /// <summary>When set, width polling is ignored (Storybook / QA).</summary>
    public Breakpoint? Override { get; }

    public static SusBreakpointService Attach(VisualElement root);
    public static SusBreakpointService For(VisualElement root);
    public static SusBreakpointService For(SusComponent component);

    public void Update(float logicalWidth);
    public void UpdateFromElement(VisualElement el);
    public void SetOverride(Breakpoint? breakpoint); // null = resume auto
}

Attach идемпотентен и является правильной точкой входа (у SusBreakpointService нет .Instance).