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

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

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

Адаптация под размер экрана использует одну ось: 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.widthlayout.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 Breakpointsm / md / …).

Breakpoints

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

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

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

Блоки токенов breakpoint идут после density в таблице токенов kit, поэтому пересекающиеся ключи (heights, spacing) отдают предпочтение активному 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

css
.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).