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

SUS — Design Tokens: шрифты, цвета, иконки, темы

Архитектура дизайн-токенов (design tokens) для нового SUS.
Единственный источник правды для тем, шрифтов и иконок в SUS (каскад StyleSheet + Resources). Принцип: всё через CSS-каскад и var() — компонент не знает конкретных значений, только семантические токены.

Design tokens dark theme
Цвета, типографика и иконки — Dark
Design tokens light theme
Те же токены — Light

Содержание

  1. Общая архитектура (вкл. рестайл без C#)
  2. Шрифты
  3. Цвета: трёхслойная система токенов
  4. Иконки: Phosphor Icons
  5. Темы: переключение Light/Dark
  6. Адаптивные breakpoints
  7. Bootstrap: как всё собирается вместе
  8. Структура файлов
  9. Исторический чеклист
  10. Приложение A: сводная таблица токенов
  11. Приложение B: ключевые правила и паттерны

1. Общая архитектура

1.1 Принцип

The component writes: And receives:
  color: var(--sus-text-primary) → rgba(255,255,255,1) (in dark theme)
  font-size: var(--sus-font-size-body) → 14px (semantic size from _font.uss)
  -unity-font: var(--sus-font-body) → Montserrat (family bridge)

Токены резолвятся через CSS-каскад. Переменные задаются в :root USS-файлах, которые подключаются прямым вызовом root.styleSheets.Add().

1.2 Три слоя токенов

LAYER 1 — Raw values (ground truth)
  _palette.uss                     — --base-color-*, --base-space-*, --base-font-*, --base-radius-*
  _font.uss                        — --font-family-*, --font-heading, --font-body

LAYER 2 — Theme aliases (light/dark switch)
  _theme.uss (:root + .theme-dark)  — --thm-* (dark default)
  _theme.uss (.theme-light)         — --thm-* (light override)

LAYER 3 — Semantic UI tokens
  design-tokens.uss                 — --sus-* (delegates to --thm-*)
  _icon.uss                         — .sus-icon-bg utility class

Порядок загрузки (каскад контейнера через SusBootstrap.LoadTokenCascade / SusApp): _palette_font_themedesign-tokens_icon → зарегистрированные extras (L4/L5) → OverlayHost.

Panel-level TSS (SusDefault.tss / ApplyDefaultTSS) также включает _palette, _font и опциональный _global_global не входит в каскад контейнера.

Тема переключается добавлением/снятием .theme-dark / .theme-light на корне каскада (SusThemeService.Instance.SetTheme(root, theme)).

1.3 Как это работало в старой схеме

SusDataManager.LoadUIResolutionThemes() (legacy) создавал несколько пресетов темы. В SUS это упрощено — только Dark/Light через классы .theme-*; адаптация под размер экрана — только breakpoints (SusBreakpointService + переопределения токенов .breakpoint-*). См. 06-responsive.md.

1.4 Рестайл без правки C#

Каскад токенов — не только правило внутренней гигиены: это поддерживаемый способ сделать контролы похожими на вашу игру без правки C#:

  1. Токены (--sus-*) — переопределите семантические токены (или L1/L2-значения, на которые они делегируют) на корне каскада. Все потребители этих токенов перекрасятся вместе: одно изменение листа — весь UI.
  2. Классы — добавьте проектное USS-правило на класс компонента или элемента для разового вида (кнопка, панель, ряд) без форка скрипта компонента.
  3. Состояния как классы — hover / active / disabled / selected (и похожие) ведутся USS- классами или UITK pseudo-classes. C# только переключает членство (AddToClassList / RemoveFromClassList или аналог Sharq). Рестайл состояния — та же работа, что и рестайл базы: меняйте лист, не скрипт.

Политика vs долг. Look-and-feel, который buyer должен уметь переопределить, живёт в USS. Геометрия и motion, которые обязаны следовать runtime props, могут оставаться в C#. Inline-запись appearance из C# (.style.backgroundColor, .style.color, шрифты, radii и подобное) перебивает любой селектор — известный остаток таких call site ещё есть и мигрируется в USS/классы. Не считайте эти call site API темизации; предпочитайте токены и классы. Именованные исключения (motion tweens, font service, drag ghosts и подобное) намеренны и задокументированы в коде.


2. Шрифты

2.1 Текущее состояние

Уже сделано: Montserrat (6 начертаний) поставляется с sus-core:

sus-core/Runtime/Resources/SusRuntime/
├── _font.uss
└── Fonts/Montserrat/
    ├── Montserrat-Regular.ttf    (400)
    ├── Montserrat-Medium.ttf     (500)
    ├── Montserrat-Bold.ttf       (700)
    ├── Montserrat-Black.ttf      (900)
    ├── Montserrat-Italic.ttf
    └── Montserrat-Light.ttf      (300)

_font.uss задаёт :root { -unity-font: url("Fonts/Montserrat/Montserrat-Regular.ttf"); } и CSS-переменные --font-family-regular, --font-family-medium, --font-family-bold, --font-family-black, --font-family-italic, --font-family-light.

SusBootstrap.Mount<T>() автоматически загружает Resources/SusRuntime/_font.uss при каждом mount.

2.2 Семантические шрифты (поставляются)

_font.uss определяет:

  • Слоты семейств (Family slots) --font-family-* (по умолчанию Montserrat). Опциональные проектные override используют fallback-паттерн из заголовка _font.uss (var(…, url(...))) — эти override-слоты не входят в опубликованный инвентарь из 61 токена L3.
  • Семантические размеры (L3) — компоненты используют их (не сырые --base-font-N и не числовые px-алиасы с префиксом --sus-):
css
--sus-font-size-caption   /* ← --base-font-size-xs (10px) */
--sus-font-size-small     /* 12px */
--sus-font-size-body      /* 14px */
--sus-font-size-subtitle  /* 16px */
--sus-font-size-heading3  /* 20px */
--sus-font-size-heading2  /* 24px */
--sus-font-size-heading1  /* 32px */
--sus-font-size-hero      /* 48px */

design-tokens.uss также экспонирует мосты семейств (family bridges) для -unity-font:

--sus-font-body · --sus-font-label · --sus-font-heading · --sus-font-italic

2.2.1 Сырые размеры шрифта L1 (палитра)

В _palette.uss по-прежнему есть сырые --base-font-10--base-font-32. Считайте их внутренностями палитры. Компоненты и buyer-сэмплы должны использовать восемь токенов --sus-font-size-caption--sus-font-size-hero выше.

2.2.2 Пользовательский override

Создайте Assets/Resources/SusRuntime/_font.ussResources.Load предпочитает копию проекта. Код не нужен. Открытые пробелы по начертаниям (Thin / ExtraLight / …) ведутся во внутреннем roadmap и не входят в этот пакет.


3. Цвета: трёхслойная система токенов

3.1 Слой 1 — сырые значения (в _palette.uss)

Файл, который идёт с sus-core в Resources/. Базовые цвета без семантики:

css
/* sus-core/Runtime/Resources/SusRuntime/_palette.uss — Layer 1 */
:root {
    /* Greys */
    --base-color-Grey98:    rgb(250, 250, 250);
    --base-color-Grey94:    rgb(240, 240, 240);
    --base-color-Grey90:    rgb(229, 229, 229);
    --base-color-Grey80:    rgb(204, 204, 204);
    --base-color-Grey60:    rgb(153, 153, 153);
    --base-color-Grey40:    rgb(102, 102, 102);
    --base-color-Grey30:    rgb( 77,  77,  77);
    --base-color-Grey24:    rgb( 61,  61,  61);
    --base-color-Grey16:    rgb( 41,  41,  41);
    --base-color-Grey12:    rgb( 31,  31,  31);
    --base-color-Grey08:    rgb( 20,  20,  20);
    --base-color-Grey04:    rgb( 10,  10,  10);
    --base-color-Grey02:    rgb(  5,   5,   5);

    /* Primary (Blue) */
    --base-color-Primary90: rgb(205, 225, 255);
    --base-color-Primary70: rgb(102, 163, 255);
    --base-color-Primary50: rgb(  0, 102, 255);
    --base-color-Primary30: rgb(  0,  61, 153);
    --base-color-Primary10: rgb(  0,  20,  51);

    /* Accent / Success / Warning / Error */
    --base-color-Success:   rgb( 76, 175,  80);
    --base-color-Warning:   rgb(255, 193,   7);
    --base-color-Error:     rgb(244,  67,  54);
}

3.2 Слой 2 — тематические алиасы (Dark/Light в _theme.uss)

L2 определяет только --thm-* (без выдуманных --thm-bg / --thm-fail / --thm-text-inverse). Полный список — в USS-файле; ключевые группы ниже совпадают с as-is.

Тема Dark (по умолчанию):

css
/* sus-core/Runtime/Resources/SusRuntime/_theme.uss — Layer 2 Dark (excerpt) */
:root,
.theme-dark {
    /* Surface */
    --thm-bg-page:            var(--base-color-Grey04);
    --thm-bg-surface:         var(--base-color-Grey08);
    --thm-bg-surface-raised:  var(--base-color-Grey12);
    --thm-bg-surface-overlay: var(--base-color-Grey16);
    --thm-bg-surface-variant: var(--base-color-Grey18);
    --thm-bg-disabled:        var(--base-color-Grey40);

    /* Text */
    --thm-text-primary:       var(--base-color-Grey94);
    --thm-text-secondary:     var(--base-color-Grey80);
    --thm-text-disabled:      var(--base-color-Grey60);
    --thm-text-on-primary:    var(--base-color-Grey98);

    /* Border */
    --thm-border:             var(--base-color-Grey24);
    --thm-border-hover:       var(--base-color-Grey40);
    --thm-border-focus:       var(--base-color-Primary50);

    /* Brand / status */
    --thm-primary:            var(--base-color-Primary50);
    --thm-primary-hover:      var(--base-color-Primary70);
    --thm-primary-pressed:    var(--base-color-Primary30);
    --thm-secondary:          var(--base-color-Grey24);
    --thm-secondary-hover:    var(--base-color-Grey30);
    --thm-success:            var(--base-color-Success);
    --thm-success-hover:      var(--base-color-SuccessHover);
    --thm-warning:            var(--base-color-Warning);
    --thm-warning-hover:      var(--base-color-WarningHover);
    --thm-error:              var(--base-color-Error);
    --thm-error-hover:        var(--base-color-ErrorHover);
    --thm-info:               var(--base-color-Info50);
    --thm-info-hover:         var(--base-color-Info70);

    /* Scrim / overlays */
    --thm-scrim:              var(--base-color-BlackT30);
    --thm-hover-overlay:      var(--base-color-WhiteT5);
    --thm-selected-overlay:   var(--base-color-WhiteT10);
    --thm-disabled-overlay:   var(--base-color-WhiteT5);
}

Тема Light (.theme-light — инвертируемые шаги серого зеркалятся; brand/status в основном те же --base-*):

css
/* sus-core/Runtime/Resources/SusRuntime/_theme.uss — Layer 2 Light (excerpt) */
.theme-light {
    --thm-bg-page:            var(--base-color-Grey96);
    --thm-bg-surface:         var(--base-color-Grey92);
    --thm-bg-surface-raised:  var(--base-color-Grey88);
    --thm-bg-surface-overlay: var(--base-color-Grey84);
    --thm-bg-surface-variant: var(--base-color-Grey81);
    --thm-bg-disabled:        var(--base-color-Grey60);

    --thm-text-primary:       var(--base-color-Grey06);
    --thm-text-secondary:     var(--base-color-Grey20);
    --thm-text-disabled:      var(--base-color-Grey40);
    --thm-text-on-primary:    var(--base-color-Grey98);

    --thm-border:             var(--base-color-Grey76);
    --thm-border-hover:       var(--base-color-Grey60);
    --thm-border-focus:       var(--base-color-Primary50);

    --thm-secondary:          var(--base-color-Grey76);
    --thm-secondary-hover:    var(--base-color-Grey70);
    --thm-hover-overlay:      var(--base-color-BlackT10);
    --thm-selected-overlay:   var(--base-color-BlackT18);
    --thm-disabled-overlay:   var(--base-color-BlackT10);
    --thm-scrim:              var(--base-color-BlackT30);
    /* primary / success / warning / error / info — same --base-* as dark */
}

Полный блок — в _theme.uss (включая игровые utility --thm-rare / --thm-heal / … — только L2, без зеркала в --sus-*).

3.3 Слой 3 — семантические UI-токены (design-tokens.uss + размеры шрифта)

Публичный L3 API: 61 определение --sus-*53 в design-tokens.uss плюс 8 семантических font-size токенов в _font.uss. Копируйте из этих файлов; не выдумывайте компонентные цветовые алиасы (кнопки, inputs, tooltips, scrollbars) — собирайте из L3.

css
/* sus-core/Runtime/Resources/SusRuntime/design-tokens.uss — Layer 3 (as-is) */
:root {
    /* ── Surface ── */
    --sus-bg-page:            var(--thm-bg-page);
    --sus-bg-surface:         var(--thm-bg-surface);
    --sus-bg-surface-raised:  var(--thm-bg-surface-raised);
    --sus-bg-overlay:         var(--thm-bg-surface-overlay);

    /* ── Text ── */
    --sus-text-primary:       var(--thm-text-primary);
    --sus-text-secondary:     var(--thm-text-secondary);
    --sus-text-disabled:      var(--thm-text-disabled);
    --sus-text-on-primary:    var(--thm-text-on-primary);

    /* ── Border ── */
    --sus-border:             var(--thm-border);
    --sus-border-hover:       var(--thm-border-hover);
    --sus-border-focus:       var(--thm-border-focus);
    --sus-divider:            var(--thm-border);

    /* ── Scrim ── */
    --sus-scrim:              var(--thm-scrim);

    /* ── Surfaces ── */
    --sus-bg-surface-variant: var(--thm-bg-surface-variant);
    --sus-bg-disabled:        var(--thm-bg-disabled);

    /* ── Primary ── */
    --sus-primary:            var(--thm-primary);
    --sus-primary-hover:      var(--thm-primary-hover);
    --sus-primary-pressed:    var(--thm-primary-pressed);

    /* ── Secondary ── */
    --sus-secondary:          var(--thm-secondary);
    --sus-secondary-hover:    var(--thm-secondary-hover);

    /* ── Semantic colors ── */
    --sus-success:            var(--thm-success);
    --sus-success-hover:      var(--thm-success-hover);
    --sus-warning:            var(--thm-warning);
    --sus-warning-hover:      var(--thm-warning-hover);
    --sus-error:              var(--thm-error);
    --sus-error-hover:        var(--thm-error-hover);
    --sus-info:               var(--thm-info);
    --sus-info-hover:         var(--thm-info-hover);

    /* ── Overlay accents (hover/selected/disabled states) ── */
    --sus-hover-overlay:      var(--thm-hover-overlay);
    --sus-selected-overlay:   var(--thm-selected-overlay);
    --sus-disabled-overlay:   var(--thm-disabled-overlay);

    /* ── Opacity states ── */
    --sus-opacity-hover:      0.04;
    --sus-opacity-focus:      0.12;
    --sus-opacity-selected:   0.08;
    --sus-opacity-disabled:   0.38;

    /* ── Font family bridges (-unity-font) ── */
    --sus-font-body:         var(--base-font-body);
    --sus-font-label:        var(--base-font-label);
    --sus-font-heading:      var(--base-font-heading);
    --sus-font-italic:       var(--base-font-italic);

    /* ── Spacing ── */
    --sus-space-0:  0px;
    --sus-space-4:  var(--base-space-4);
    --sus-space-8:  var(--base-space-8);
    --sus-space-12: var(--base-space-12);
    --sus-space-16: var(--base-space-16);
    --sus-space-24: var(--base-space-24);
    --sus-space-32: var(--base-space-32);
    --sus-space-48: var(--base-space-48);
    --sus-space-64: var(--base-space-64);

    /* ── Radius ── */
    --sus-radius-sm:   var(--base-radius-sm);
    --sus-radius-md:   var(--base-radius-md);
    --sus-radius-lg:   var(--base-radius-lg);
    --sus-radius-xl:   var(--base-radius-xl);
    --sus-radius-full: var(--base-radius-full);
}

Размеры шрифта (тоже L3; задаются в _font.uss, не в design-tokens.uss):

css
/* sus-core/Runtime/Resources/SusRuntime/_font.uss — semantic sizes */
:root {
    --sus-font-size-caption:  var(--base-font-size-xs);
    --sus-font-size-small:    var(--base-font-size-sm);
    --sus-font-size-body:     var(--base-font-size-md);
    --sus-font-size-subtitle: var(--base-font-size-lg);
    --sus-font-size-heading3: var(--base-font-size-xl);
    --sus-font-size-heading2: var(--base-font-size-2xl);
    --sus-font-size-heading1: var(--base-font-size-3xl);
    --sus-font-size-hero:     var(--base-font-size-4xl);
}

Композиция (не токены L3): кнопки/inputs/tooltips/scrollbars собирают токены выше — например primary-кнопка background-color: var(--sus-primary); подпись color: var(--sus-text-on-primary); hover var(--sus-primary-hover); затемнение модального backdrop var(--sus-scrim); приподнятая поверхность панели var(--sus-bg-overlay) или var(--sus-bg-surface-raised).

Использование шкалы space в вёрстке: держите зазоры между sibling не меньше --sus-space-8, выравнивайте ряды, избегайте обрезанного контента и используйте только токены (без сырых px).

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

xml
<!-- Card.sharq — compose L3 tokens (no component-level color aliases) -->
<style scoped>
.card {
    background-color: var(--sus-bg-surface);
    border-width: 1px;
    border-color: var(--sus-border);
    border-radius: var(--sus-radius-md);
    color: var(--sus-text-primary);
    padding: var(--sus-space-16);
}
.card__title {
    font-size: var(--sus-font-size-heading3);
    -unity-font: var(--sus-font-heading);
    color: var(--sus-text-primary);
}
.card__body {
    font-size: var(--sus-font-size-body);
    color: var(--sus-text-secondary);
    margin-top: var(--sus-space-8);
}
.card__cta {
    background-color: var(--sus-primary);
    color: var(--sus-text-on-primary);
}
.card__cta:hover {
    background-color: var(--sus-primary-hover);
}
.card__link {
    color: var(--sus-primary); /* links: brand color, not a separate link token */
}
</style>

4. Иконки: Phosphor Icons

SUS поставляет Phosphor Icons (MIT) как VectorImage SVG — не Bootstrap Icons. Дерева Icons/bootstrap/ больше нет.

НаборПутьРазмерРоль
CoreResources/SusRuntime/Icons/core/{regular,fill}/~127 SVGВстроенный набор, всегда присутствует: всё, что компоненты и downstream UI-пакеты используют по умолчанию
PhosphorSamples~/PhosphorIcons/Resources/SusRuntime/Icons/phosphor/{thin,light,regular,bold,fill,duotone}/1512 имён × 6 weights ≈ 9000 SVGОпциональная полная библиотека, импортируется как сэмпл Phosphor Icon Set

Weights (SusIconWeight): Thin, Light, Regular (по умолчанию), Bold, Fill, Duotone.

Статус: ✅ runtime + editor автоматически регистрируют оба провайдера; downstream UI-пакеты используют тот же registry.

Полный набор намеренно вынесен за пределы пакета: папка Resources попадает в каждый player build и не вырезается, поэтому 9000 SVG стоили бы каждому потребителю ~19 MB независимо от того, нужны они или нет. PhosphorIconProvider читает из любой папки Assets/**/Resources/SusRuntime/Icons/phosphor/, поэтому и импорт сэмпла, и копирование только нужных иконок в свой Resources — оба варианта работают. Без него SusIconRegistry резолвит через core-подмножество и для «длинного хвоста» возвращает null вместо падения.

4.1 Раскладка на диске

sus-core/Runtime/Resources/SusRuntime/Icons/
└── core/
    ├── regular/     star.svg, gear.svg, …
    └── fill/        star-fill.svg, …          (weight folder + optional -fill suffix)

sus-core/Samples~/PhosphorIcons/Resources/SusRuntime/Icons/    (optional sample)
└── phosphor/
    ├── thin/
    ├── light/
    ├── regular/     {name}.svg
    ├── bold/        {name}-bold.svg
    ├── fill/        {name}-fill.svg
    └── duotone/     {name}-duotone.svg

Конвенция загрузчика (ResourcesFolderIconProvider): resources path
SusRuntime/Icons/{collection}/{weightFolder}/{fileName}
где fileName{name} для regular, иначе {name}-{weight}.

Два конструктора:

  • new ResourcesFolderIconProvider("app")project-local перегрузка (без package id). Отдаёт иконки из любой папки Assets/**/Resources/SusRuntime/Icons/app/… (включая wizard Customization/Icons/Resources/…). Используйте для собственных иконок приложения.
  • new ResourcesFolderIconProvider("com.my.game", "app")packaged перегрузка. Первый аргумент — UPM package id, в чьём Runtime/Resources/SusRuntime/Icons/app/… лежат иконки (editor scan + починка .meta резолвятся относительно этого пакета). Используйте только когда иконки поставляются внутри вашего пакета. Не передавайте "com.sharq-it.sus.core" для проектных иконок — сканирование уйдёт в пакет core.

4.2 Как работает поиск иконки

SusIcon / SusIconRegistry.Load(alias, weight)
  → resolve semantic aliases (e.g. "settings" → "gear")
  → optional name suffix "-fill" / "-bold" / … overrides weight
  → query ISusIconProvider list (first hit wins):
       1. Project providers (SusApp.UseIcons / RegisterProvider highest priority)
       2. CoreIconProvider          (Icons/core)
       3. PhosphorIconProvider      (any Assets/**/Resources/…/Icons/phosphor, auto via PhosphorIconBootstrap)

PhosphorIconBootstrap регистрирует Phosphor на SubsystemRegistration / загрузке editor с asHighestPriority: false, поэтому core + проектные иконки сохраняют приоритет при совпадающих именах.

4.3 API (текущий)

csharp
// Weight enum — not the old SusIconStyle Outline/Filled
public enum SusIconWeight { Thin, Light, Regular, Bold, Fill, Duotone }

// Core primitive (also used under the hood)
var icon = new SusIcon("gear");                      // Regular
var star = new SusIcon("star", SusIconWeight.Fill);
icon.Name.Value = "x";                               // reactive

VectorImage img = SusIconRegistry.Load("star", SusIconWeight.Fill);
SusIconRegistry.AddAlias("settings", "gear");
SusIconRegistry.RegisterProvider(myProvider, asHighestPriority: true);

// SusApp — project icons win over Phosphor
SusApp.Create(uiDocument)
    .UseIcons(new ResourcesFolderIconProvider("com.my.game", "app"))
    .Mount<HomeScreen>();

Downstream UI-пакеты могут экспонировать иконку более высокого уровня:

xml
<sus:SusIcon Icon="star" Weight="Fill" Size="md" />

Kind=phosphor|game|portrait|auto — game/portrait используют consumer media bridge; см. документацию вашего опционального пакета компонентов.

4.4 Настройки импорта SVG (ВАЖНО)

Каждый .svg.meta должен использовать UI Toolkit Vector Image + antialiased tessellation:

yaml
svgType: 3                          # UI Toolkit Vector Image
tessellationMode: 1                 # Antialiased Arc Encoding (NOT 0)
textureSize: 256
sampleCount: 4

Без tessellationMode: 1 кривые выглядят пиксельными.

4.5 Вспомогательный USS

_icon.uss — глобальный класс для обычного VisualElement / core SusIcon (.sus-icon-bg):

css
.sus-icon-bg {
    background-size: contain;
    background-repeat: no-repeat;
    background-position: center;
    -unity-background-image-tint-color: rgb(255, 255, 255);
    flex-shrink: 0;
}

Загружается вместе с каскадом токенов (SusBootstrap / SusApp).

4.6 USS vs C# для иконок

ЧтоГдеПочему
background-size/position/repeat, flex-shrinkUSS (.sus-icon-bg / companion)Статика
Default tintUSSТема может переопределить
backgroundImageC# через SusIconRegistry.LoadRuntime-ассет
Size / weight / nameProps / C#Динамика

4.7 Кастомные / проектные иконки

Не кладите файлы в вымышленный Icons/bootstrap/custom/. Предпочтительнее:

  1. ResourcesFolderIconProvider, указывающий на вашу коллекцию под
    Resources/SusRuntime/Icons/{yourCollection}/{weight}/, затем
    SusApp.UseIcons(...) или SusIconRegistry.RegisterProvider(...).
  2. Или SusIconSetAsset + SusApp.UseIcons(iconSet).
  3. Семантические алиасы: SusIconRegistry.AddAlias("my-settings", "gear").

Scaffolding Setup Project использует Customization/Icons/.../app/ + ResourcesFolderIconProvider("app") (project-local перегрузка) как слой проектного override.


5. Темы: переключение Light/Dark

5.1 Как это работало в предыдущей схеме

  • UIResolutionThemes — ScriptableObject с массивом из 6 StyleTheme (Dark/Light/CustomLight × High/Low)
  • panel.themeStyleSheet = ThemeStyleSheet (.tss), который через @import тянет цепочку USS
  • PollScreenWidth() в Update — переключает High/Low при пересечении 1600px
  • SetStyleTheme() — переключает Dark/Light/CustomLight

5.2 SUS — текущий API

Нет ThemeStyleSheet для переключения темы — USS-листы добавляются на контейнер через LoadTokenCascade / SusApp. Варианты темы — CSS-классы на корне каскада.

Файлы слоёв:

  • _palette.uss — L1 --base-* (сырые значения)
  • _theme.uss — L2 --thm-* для .theme-dark / .theme-light

Runtime API (SusThemereadonly struct; сервис — singleton):

csharp
// sus-core/Runtime/SusTheme.cs + Runtime/Services/SusThemeService.cs
namespace Sharq.Core
{
    public readonly struct SusTheme
    {
        public string Name { get; }
        public SusTheme(string name);
        public static SusTheme Dark => new("dark");
        public static SusTheme Light => new("light");
        public string CssClass => $"theme-{Name}";  // "theme-dark", "theme-light", …
    }

    public class SusThemeService
    {
        public static SusThemeService Instance { get; }
        public static Prop<SusTheme> Current { get; }

        // Applies .theme-{name} on the cascade root + OverlayHost
        public void SetTheme(VisualElement root, SusTheme theme);
    }
}

Использование:

csharp
SusThemeService.Instance.SetTheme(root, SusTheme.Dark);
SusThemeService.Instance.SetTheme(root, SusTheme.Light);
SusThemeService.Instance.SetTheme(root, new SusTheme("midnight"));  // custom → .theme-midnight

Watch(SusThemeService.Current, (_, next) => AdaptToTheme(next));

CSS-переключение через класс на корне — уже в _theme.uss (см. 3.2).

5.3 SusBootstrap / SusApp

SusApp.Create(...).UseTheme(...).Mount/Run применяет тему последней, чтобы OverlayHost получил theme-класс. Предпочитайте этот путь ручному styleSheets.Add.

SusBootstrap.Mount<T>() / LoadTokenCascade загружают полный каскад (_palette_font_themedesign-tokens_icon → extras + OverlayHost).


6. Адаптивные breakpoints

Адаптация под размер экрана — только SusBreakpointService (классы .breakpoint-sm.breakpoint-2xl на корне каскада). Нет оси High/Low resolution и нет автоматического масштаба UI, привязанного к размеру монитора.

Ширина читается из cascadeRoot.resolvedStyle.width на geometry changes (тот же путь подачи, что у удалённого SusResolutionService), с fallback на panel / Screen только когда root ещё не выложен. См. 06-responsive.md.

Token-листы downstream UI-пакетов могут переопределять пакетные токены под этими классами (высоты, spacing, шрифты). Компоненты, которые уже потребляют эти vars, подхватывают активный breakpoint без per-component C#.

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


7. Bootstrap: как всё собирается вместе

7.1 Порядок загрузки (предпочитайте SusApp)

SusApp.Create(uiDocument)
  ├─ ApplyDefaultTSS (panel: _palette + _font + _global via SusDefault.tss)
  ├─ EnsureEventSystem
  ├─ UseIcons → SusIconRegistry
  ├─ LoadTokenCascade (container):
  │     _palette → _font → _theme → design-tokens → _icon → extras
  │     + Breakpoint/Density/Scale services + OverlayHost
  ├─ EnsureWorldSpacePanel (if UseWorldSpace && playing)
  ├─ UseFonts / UseCustomStyles
  ├─ Configure callbacks (router / manual UI)
  ├─ Mount<T> (optional)
  └─ SusThemeService.Instance.SetTheme(root, theme)   ← last

Важно: каскад контейнера всегда включает _palette и _theme (не только design-tokens). Предпочитайте SusApp самодельному root.styleSheets.Add(...).

Оговорка про world-space panel. EnsureWorldSpacePanel даёт world-панели собственный каскад токенов (_palette → _font → _theme → design-tokens), но UseFonts, UseCustomStyles (брендинг) и SetTheme применяются только к screen root + OverlayHost — не к world panel. Поэтому world UI (healthbars/nameplates из downstream UI-пакета) рендерится с дефолтной темой Dark, дефолтными шрифтами и без branding override. Если нужны brand-цвета / light-тема / кастомные шрифты на world UI, примените их к app.WorldPanel / WorldSpaceService.Default.WorldSpacePanel сами (например SusThemeService.Instance.SetTheme(worldRoot, theme) и загрузите branding USS на него).

7.2 Что определяет каждый слой

_palette.uss     — L1: --base-color-*, --base-space-*, --base-font-*, --base-radius-*
_font.uss        — font families (--font-family-*) + optional family override slots + --sus-font-size-caption…hero
_theme.uss       — L2: --thm-* for .theme-dark / .theme-light
design-tokens.uss— L3: --sus-* semantic tokens (colors, space, radius, font family bridges)
_icon.uss        — .sus-icon-bg utility

7.3 Минимальный пример

csharp
public class AppEntry : MonoBehaviour
{
    public UIDocument uiDocument;

    void Start()
    {
        SusApp.Create(uiDocument)
            .UseTheme(SusTheme.Dark)
            .Mount<App>();
        // Fonts, colors, icons, OverlayHost, theme — all wired.
    }
}

Низкоуровневая альтернатива (ручной каскад):

csharp
void Start()
{
    SusBootstrap.ApplyDefaultTSS(uiDocument);
    var root = uiDocument.rootVisualElement;
    SusBootstrap.LoadTokenCascade(root);
    SusBootstrap.Mount<App>(root);
    SusThemeService.Instance.SetTheme(root, SusTheme.Dark);
}

8. Структура файлов

8.1 В sus-core (идёт с пакетом)

sus-core/
├── Docs/
│   └── DESIGN_TOKENS.md          ← this document

├── Runtime/
│   ├── Resources/SusRuntime/
│   │   ├── _palette.uss          ← L1 --base-*                  ✅
│   │   ├── _font.uss             ← Fonts (Montserrat)           ✅
│   │   ├── _theme.uss            ← L2 --thm-*                   ✅
│   │   ├── design-tokens.uss     ← L3 --sus-*                   ✅
│   │   ├── _icon.uss             ← .sus-icon-bg                 ✅
│   │   ├── Fonts/Montserrat/     ← 6 .ttf
│   │   └── Icons/
│   │       └── core/{regular,fill}/
│   │           (full phosphor/{thin,light,regular,bold,fill,duotone}/
│   │            ships as the optional Samples~/PhosphorIcons sample)
│   ├── SusTheme.cs / Services/SusThemeService.cs
│   ├── SusBreakpointService.cs
│   ├── SusIconRegistry.cs + icon providers
│   ├── SusIcon.cs
│   ├── SusApp.cs
│   └── SusBootstrap.cs

Замечание по версионированию: версия пакета ведётся в package.json. При каждом изменении .cs в sus-core поднимайте package.json (pre-push hook требует этого).

8.2 В проекте пользователя (опциональный override)

Assets/Resources/SusRuntime/
├── _font.uss      ← font override
├── _palette.uss   ← L1 palette override (optional)
├── _theme.uss     ← L2 theme override (optional)
└── Icons/
    └── {yourCollection}/{regular|fill|…}/  ← via ResourcesFolderIconProvider

Или используйте SusApp.UseIcons(...) / SusIconSetAsset — см. §4.7.

8.3 SUS Theme Editor (инструмент Editor)

Окно Editor для визуального редактирования _palette.uss вместо ручного набора rgb(): свотчи цвета на каждый токен (--base-color-*), полоса превью с живым contrast ratio и генератор hover/pressed-вариантов из исходного цвета. Сохраняет обратно в проектный override _palette.uss.

Окно SUS Theme Editor со свотчами цветовых токенов, полосой live-превью и генератором hover/pressed-вариантов

9. Исторический чеклист

Фазы B–F (palette/theme/L3-токены, breakpoints, Phosphor icons, каскад SusApp, доки) сделаны. Эта buyer-страница — не roadmap.

Открытая работа по начертаниям (Thin / ExtraLight / SemiBold / ExtraBold и связанные расширения _font.uss) ведётся во внутреннем roadmap и не входит в этот пакет.

ФазаРезультатСтатус
A — Шрифты (поставляемое подмножество)Montserrat 6 styles + семантические font-size токены + family bridges✅ (открытые faces → internal plan)
B — Цвета_palette / _theme / design-tokens + SusThemeService
C — Размеры / breakpointsspacing/radius L1; SusBreakpointService
D — ИконкиCore-подмножество + опциональный сэмпл Phosphor + registry
E — BootstrapSusApp / LoadTokenCascade
F — Документацияэта страница + проекция в пакет

Приложение A: сводная таблица токенов

Публичный инвентарь L3 (61 --sus-*). Источник: design-tokens.uss + _font.uss (font-size токены).

Surfaces / text / border / scrim

ТокенРольСлой
--sus-bg-pageФон страницыColor L3
--sus-bg-surfaceКарточка / панельColor L3
--sus-bg-surface-raisedПриподнятая поверхностьColor L3
--sus-bg-overlayOverlay-поверхность (--thm-bg-surface-overlay)Color L3
--sus-bg-surface-variantВариант поверхностиColor L3
--sus-bg-disabledЗаливка disabledColor L3
--sus-text-primaryОсновной текстColor L3
--sus-text-secondaryВторичный текстColor L3
--sus-text-disabledТекст disabledColor L3
--sus-text-on-primaryТекст на залитом primaryColor L3
--sus-borderРамка по умолчаниюColor L3
--sus-border-hoverРамка hoverColor L3
--sus-border-focusFocus ringColor L3
--sus-dividerРазделительColor L3
--sus-scrimЗатемнение modal / overlayColor L3

Brand / status / overlays / opacity

ТокенРольСлой
--sus-primary, --sus-primary-hover, --sus-primary-pressedBrandColor L3
--sus-secondary, --sus-secondary-hoverНейтральный акцентColor L3
--sus-success, --sus-success-hoverSuccessColor L3
--sus-warning, --sus-warning-hoverWarningColor L3
--sus-error, --sus-error-hoverErrorColor L3
--sus-info, --sus-info-hoverInfoColor L3
--sus-hover-overlayHover washColor L3
--sus-selected-overlaySelected washColor L3
--sus-disabled-overlayDisabled washColor L3
--sus-opacity-hover, --sus-opacity-focus, --sus-opacity-selected, --sus-opacity-disabledЧисловые opacityColor L3

Шрифты

ТокенРольСлой
--sus-font-body, --sus-font-label, --sus-font-heading, --sus-font-italicFamily bridges (-unity-font)Font L3
--sus-font-size-caption10pxSize L3
--sus-font-size-small12pxSize L3
--sus-font-size-body14pxSize L3
--sus-font-size-subtitle16pxSize L3
--sus-font-size-heading320pxSize L3
--sus-font-size-heading224pxSize L3
--sus-font-size-heading132pxSize L3
--sus-font-size-hero48pxSize L3

Spacing / radius

ТокенРольСлой
--sus-space-0--sus-space-64 (0,4,8,12,16,24,32,48,64)Шкала spacingSize L3
--sus-radius-sm--sus-radius-full (sm,md,lg,xl,full)Скругление угловSize L3

L1-хелперы, часто рядом с L3 (не входят в 61): --font-family-regular, --main-font-family, --base-space-*, --base-radius-*.


Приложение B: ключевые правила и паттерны (выявленные в процессе)

B.1 Статика в USS, динамика в C#

┌─────────────┬────────────────────────┬──────────────────────────┐
│ Where │ Examples │ Rule │
├─────────────┼────────────────────────┼──────────────────────────┤
│ USS (static)│ background-size, │ Do not change in runtime │
│ │ flex-shrink, │ from props - write in USS │
│             │ margin-left, color,    │                          │
│             │ font-size (layout)     │                          │
├─────────────┼────────────────────────┼──────────────────────────┤
│ C# (dynamics) │ width/height from Size, │ Change from props or │
│ │ tint from TintColor, │ runtime logic - in code │
│             │ backgroundImage        │                          │
│             │ (Resources.Load)       │                          │
└─────────────┴────────────────────────┴──────────────────────────┘

Мотивация: главный источник изменений — props. Меняя props, мы меняем в C# только динамический атрибут. Статика лежит в USS и не размазана по коду. Так проще читать и отлаживать.

Пример (хорошо):

csharp
// C# - only dynamic from props
el.style.width = size;                     // ← from prop Size
el.style.height = size;
el.AddToClassList("icon-row");             // ← statics in USS

/* USS:
.icon-row { flex-direction: row; align-items: center; margin-bottom: 6px; } */

Пример (плохо — антипаттерн):

csharp
el.style.flexDirection = FlexDirection.Row;   // ← static in C# (migrate to USS)
el.style.alignItems = Align.Center;
el.style.color = new StyleColor(Color.white);

B.2 Sharq bare <style> — как работают селекторы

Source (.sharq) Compiled (.uss) Does root match?
─────────────────────────────────────────────────────────────────────
<style> .sus-icon { ✅ YES
    flex-shrink: 0;            flex-shrink: 0;
</style>                   }

<style> .sus-icon .sus-icon { ❌ NO
.sus-icon { flex-shrink: 0;            (.sus-icon inside .sus-icon)
    flex-shrink: 0;        }
}
</style>

<style> .sus-icon .child { ✅ YES (if child is inside)
.child {                        ...                    }
    ...
}
</style>

Правило: если нужно стилизовать ROOT компонента, пишите свойства прямо в <style> без селектора. Если нужно стилизовать дочерние элементы — используйте их классы (без префикса компонента).

В особых случаях (self-target, container-target — см. правило sharq-css-scoping.mdc) — перенесите правило в companion .uss как unscoped.

B.3 Паттерн re-assert backgroundImage

UI Toolkit Known Issue: VectorImage иногда не рендерится на первом кадре. Проверенный паттерн (из старого SizedIcon):

csharp
el.style.backgroundImage = new StyleBackground(vec);
el.MarkDirtyRepaint();
el.schedule.Execute(() =>
{
    el.style.backgroundImage = new StyleBackground(vec);
    el.MarkDirtyRepaint();
}).ExecuteLater(0);
el.schedule.Execute(() =>
{
    el.style.backgroundImage = new StyleBackground(vec);
    el.MarkDirtyRepaint();
}).ExecuteLater(16);  // next frame

Переустановка backgroundImage трижды: сразу, через 0 кадров, через ~1 кадр. Так VectorImage не «потеряется» при пересборке дерева.

B.4 SVG Import: tessellationMode

ПараметрЗначениеЗачем
svgType3UI Toolkit Vector Image (не Texture2D)
tessellationMode1Antialiased Arc Encoding — гладкие края на дугах
textureSize256Достаточно для иконок до 48px
sampleCount4Multisampling для subpixel AA

Никогда не используйте tessellationMode: 0 (Basic Triangulation) — даёт пиксельные/зубчатые края на SVG-кривых.

B.5 Загрузка USS: предпочитайте SusApp / LoadTokenCascade

csharp
// ✅ Correct: SusApp (or LoadTokenCascade) on the document root
SusApp.Create(uiDocument)
    .UseTheme(SusTheme.Dark)
    .Mount<App>();
// Cascade order: _palette → _font → _theme → design-tokens → _icon → extras + OverlayHost
// Panel TSS (_global) via ApplyDefaultTSS / SusDefault.tss

// ✅ Lower-level
SusBootstrap.ApplyDefaultTSS(uiDocument);
SusBootstrap.LoadTokenCascade(uiDocument.rootVisualElement);

// ❌ Incorrect: hand-add only design-tokens without _palette/_theme
root.styleSheets.Add(designTokensSheet);

B.6 Версионирование sus-core

При любом изменении .cs в sus-core:

  1. Поднимите версию в sus-core/package.json (patch)
  2. Закоммитьте с version tag
  3. Push → кэш в Unity Package Manager
  4. Очистите Library/PackageCache/com.sharq-it.sus.core/ у клиента
  5. Удалите Packages/packages-lock.json
  6. Refresh Unity → пакет резолвится заново

Изменения скриптов (.uss, .svg, .meta) не требуют ручного сброса кэша.