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

Storybook: живая игровая площадка для собственных компонентов ​

Storybook — это браузер компонентов, встроенный в sus-core: укажите ему на UIDocument, и он выведет список всех объявленных стори, смонтирует реальный экземпляр компонента на сцене и сформирует полную панель управления прямо из публичных полей Prop<T> вашего компонента — без необходимости вручную писать и синхронизировать список контролов.

Он поставляется как собственная сборка com.sharq-it.sus.core.storybook (Runtime/Storybook/com.sharq-it.sus.core.storybook.asmdef), которая ссылается только на com.sharq-it.sus.core. Он не зависит от sus-router — навигация между стори — это малюсенький самостоятельный слой (ниже), а не маршрутизированное приложение. Ничто другое в ядре не ссылается на эту сборку (autoReferenced: false), так что сборка игрока, которая никогда не импортирует сэмпл стори, не содержит её: ни .dll, ни стилей, 0 байт. Сборка появляется в билде в тот момент, когда стори какого-нибудь пакета импортированы и на них ссылаются — цена уплачивается по выбору, за проект, а не за покупателя.

Как открыть ​

  1. Добавьте UIDocument на сцену и поместите SusStorybookBehaviour на тот же GameObject (Add Component → SUS → Storybook Host). Нажмите Play (или используйте Editor's UI Toolkit preview) — оболочка смонтируется и выведет список всех пакетов, у которых объявлены стори.
  2. Downstreamные UI-пакеты поставляют собственные стори компонентов тем же способом: папка Samples~, чья собственная asmdef ссылается на com.sharq-it.sus.core.storybook плюс этот пакет. Импортируйте сэмпл Stories из записи Package Manager пакета и откройте сцену, которую он добавит (UIDocument + SusStorybookBehaviour) — в доках пакета указано имя сэмпла и имя файла сцены. Сцена никогда сама не выводит список стори, поэтому она не растёт и не становится устаревшей вместе с каталогом.
  3. Стори каждого такого пакета поступают тем же способом — сэмпл поставляется по выбору за пакет, и проект платит только за те, которые он импортирует.

Маркер SusStoryAssembly пакета также говорит о том, для чего предназначена сборка (ниже), и вкладка следует из этого:

  • Product, нет объявленных стори — пакет всё равно получает вкладку с табличкой, говорящей об этом, вместо того чтобы исчезнуть молча, так что «я импортировал неправильный сэмпл» и «в этом пакете нет стори» читаются как два разных события. Вот где табличка заслуживает своё место: стори, которые приходят через ручной импорт папки Samples~ (kit, game, пакет скина). Проект может также запросить эту же вкладку и табличку, прежде чем пакет вообще поставляет какую-либо стори, без маркера, через SusStoryRegistry.DeclarePackage(key, packageId, version).
  • Тестовый стенд (Kind = SusStoryPackageKind.Fixture) по умолчанию не получает вкладку и табличку — его стори существуют, чтобы быть доступными по прямому адресу из сьюта. Скрыто по умолчанию не то же, что исчезло: меню Window/SUS/Storybook/Show Engine Fixtures выводит все пакеты-стенды на этой машине, и открытие адреса с ?fixtures=1 тоже их выводит, так что ссылка, поделённая с кем-то, чьё меню переключено, всё равно открывает вкладку стенда.
  • Нет маркера, и нет вызова DeclarePackage для этого ключа — нет вкладки.

Писать свою стори ​

csharp
using Sharq.Core.Storybook;

[SusStory("myapp/atoms/price-tag", Name = "Price Tag", Purpose = "shows a price with currency")]
public sealed class PriceTagStory : ISusStory
{
    public SusComponent Create() => new PriceTag();

    public void Configure(SusStoryContext ctx)
    {
        var tag = (PriceTag)ctx.Component;
        tag.Amount.Value = 19.99f;
        // Пропс, который стори намеренно оставляет без контрола, ДОЛЖЕН объяснить почему —
        // исключение без причины — это дыра, а не решение:
        ctx.Exclude(nameof(PriceTag.InternalSku), "debug-only, not buyer-facing");
    }
}

Вот весь контракт: класс, реализующий ISusStory, помеченный [SusStory], в сборке, которая согласится с [assembly: SusStoryAssembly] (один раз на сборку, в AssemblyInfo.cs):

csharp
[assembly: Sharq.Core.Storybook.SusStoryAssembly(Package = "myapp")]

SusStoryAssembly также имеет Kind, по умолчанию SusStoryPackageKind.Product. Оставьте его как есть для пакета, который импортирует покупатель — этот стандарт — это то, что дарует сборке её вкладку (и её табличку пустого пакета, выше). Тестовый стенд, который существует только, чтобы дать сьюту адресуемые стори, вместо этого устанавливает Kind = SusStoryPackageKind.Fixture: он выпадает из списка вкладок, дерева и развёртки по умолчанию, и его стори всегда разрешаются по прямому адресу независимо — но «по умолчанию» не означает «никогда»: то же самое переключение меню и адрес ?fixtures=1, описанные выше, возвращают его в список.

Адрес <package>/<group>/<slug> — постоянный ID стори — это то, на что ориентируются глубокие ссылки и снимки экрана, поэтому переименование обращайте как переименование публичного API. Реестр находит стори, сканируя только сборки с SusStoryAssembly (не все сборки проекта), и строит только тип, который уже есть как System.Type из этого сканирования — никогда из строки — поэтому ничего не зависит от того, как stripped/IL2CPP-билд разрешает имена.

Если ваши стори генерируются, а не по принципу один класс на стори (пресеты скинов, управляемые данными композиции), вместо этого реализуйте ISusStoryProvider.Enumerate() — она обнаруживается тем же способом.

Как выводится панель управления ​

Панель не знает тип вашего компонента. Она задаёт три вопроса, на которые отвечает каждый SusComponent — DescribeProps(), DescribeAllowed(), DescribeEvents() — и строит контрол для каждого пропса на основе его объявленного типа:

Форма пропсаКонтрол
Prop<bool>переключатель
Prop<string> с допустимым набором (UseAllowed)сегментированные кнопки (≤ 5 значений) или выпадающее меню
Prop<string> с именем, заканчивающимся на Iconпалитра иконок (сетка глифов, с поиском)
Prop<string>, всё остальноетекстовое поле
Prop<int> / Prop<float>ползунок + числовое поле, диапазон из [SusRange] (0…100 по умолчанию)
Prop<Color>цветовой образец, сначала токены скина, потом произвольный цвет
Prop<List<T>>редактор количества строк с разворотом
что угодно read-only / обычный объектвывод только значения, обновляется в реальном времени из событий
поле события On*строка в логе событий (зона E) при его срабатывании, с аргументом

Нижний колонтитул всегда показывает пропсы N · контролы M. M ниже N без соответствующей причины в ctx.Exclude(...) — дефект стори, а не косметический пробел — панель это называет (dead props:, uncovered:) вместо молчания.

Диапазоны и зависимости ​

csharp
[SusRange(0, 5, 0.5)]                      public Prop<float> Rating = new(0);
[SusRange(0, 100, "%")]                    public Prop<int>   Progress = new(0);
[SusDependsOn(nameof(ContentMode), "icon")] public Prop<string> Icon = new("");
[SusDependsOn(nameof(Clearable))]           public Prop<string> ClearIcon = new("x");

[SusRange] даёт ползунку реальные границы вместо угаданных 0…100. [SusDependsOn] отключает контрол и называет причину («видно когда ContentMode = icon») вместо рендеринга контрола, который молча ничего не делает — несколько атрибутов на одном поле комбинируются с AND.

Окружение (зона B) ​

Ряд чипсов позволяет переключать брейкпоинт, плотность, тему, масштаб и устройство ввода не покидая стори — переключение оси никогда не сбрасывает контролы, которыми вы управляли. Пакет скина может зарегистрировать свою собственную ось (например skin) через SusStoryEnvAxisRegistry; ядро само по себе не поставляет ось скина, поэтому чип для неё появляется только когда такой пакет её регистрирует. То же верно для locale — её провайдер — это какой бы то ни было пакет локализации, который присутствует.

Публикация стори: адресная строка ​

Адрес стори — это #/<package>/<group>/<slug>?Prop=val&env.axis=val — запрос несёт только то, чем отличается от собственных значений по умолчанию в стори, поэтому неизменённая стори имеет короткую стабильную ссылку, а поделённая говорит ровно то, что изменилось. Share копирует этот текст; back / forward (также Alt+← / Alt+→) перемещаются по истории в памяти — ничто из этого не зависит от сетевого запроса, поэтому это работает так же в Editor как и в браузерном билде. Открытие сайта со ссылкой уже в адресной строке подсвечивает правильную запись в боковой панели и монтирует правильную стори без перезагрузки страницы.

Матрица и зонд (зоны C и E) ​

Ниже панели управления матрица Variant × State рендерит один реальный экземпляр на ячейку (не рисунок) для каждого значения оси Variant компонента против шести колонок: rest, hover, active, focused, disabled, error. Только компонент, поставляющий класс-двойник sus-state-hover / sus-state-active, получает колонки hover и active — Unity UI Toolkit не имеет способа навязать pseudo-class из кода, поэтому колонка без двойника выпадает, а не показывается неправильно. Выше 24 ячеек, или на стори, помеченной Weight = SusStoryWeight.Heavy (таблица данных, инвентарь, боевая сетка), матрица начинает свёрнутой и ничего не строит, пока вы об этом не попросите.

Полоса зонда под реальным экземпляром выводит каждое событие, которое компонент вызвал в этой сессии (с его аргументом), и счётчик здоровья из того же детектора аномалий, что использует MCP-зонд. Сравнение фреймов выводит —, а не ложный зелёный: диффинг скриншотов canon-vs-live — это конвейер захвата, который подключается к этому же контракту, но приземлился на момент написания — зонд честно говорит «не знаю» вместо того чтобы сообщать о сравнении, которое он не может выполнить.

Чем это не является, на сегодня ​

  • Старые per-package storybook оболочки (Samples~/Storybook в kit и game) всё ещё существуют рядом с этим движком, пока стори мигрируют на него по одной на пакет — сэмпл с именем Storybook и один с именем Stories пока не одно и то же.
  • Полный набор иконок Phosphor не поставляется. Палитра иконок читает то, что зарегистрировано в SusIconRegistry — собственные ~127 иконок ядра всегда есть; 9000+ глифов Phosphor появляются только после того, как проект импортирует отдельный сэмпл PhosphorIcons из sus-core.