跳转到内容

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,并在同一 GameObject 上放置 SusStorybookBehaviour (Add Component → SUS → Storybook Host)。进入播放模式(或 Editor 的 UI Toolkit 预览)— 外壳会挂载自己并列出每个声明了故事的包。
  2. 下游 UI 包以相同方式提供自己的组件故事:一个 Samples~ 文件夹,其自己的 asmdef 引用 com.sharq-it.sus.core.storybook 加上该包。从包的 Package Manager 条目导入 Stories 样本,并打开它放置的那个场景(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>>行数编辑器,带展开
任何只读 / 普通对象仅值显示,从事件实时更新
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) ​

一排芯片让你在不离开故事的情况下翻转断点、密度、主题、缩放和输入设备 — 切换一个轴永远不会重置你正在操作的控制。皮肤包可以 通过 SusStoryEnvAxisRegistry 注册自己的轴(例如 skin); 核心本身不提供皮肤轴,所以只有当这样的包注册了一个时,它的芯片才会出现。 对于 locale 也是如此 — 其提供者是任何存在的本地化包。

共享故事:地址栏 ​

故事的地址是 #/<package>/<group>/<slug>?Prop=val&env.axis=val — 查询只携带与故事自己的默认值不同的内容,所以未触及的故事有一个短的稳定链接, 共享的链接说出确切改变了什么。共享复制此文本;返回 / 前进(也是 Alt+← / Alt+→)通过内存中的历史移动 — 没有任何内容取决于网络往返, 所以它在编辑器中的工作方式与在浏览器构建中相同。用地址栏中已有的链接打开网站 会突出显示侧边栏中的正确条目并挂载正确的故事,不重新加载页面。

矩阵和探针(区域 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 屏幕截图 diffing 是一个捕获管道, 连接到这个相同的契约,但在撰写本文时还未落地 — 探针诚实地说"不知道" 而不是报告它无法进行的比较。

这不是什么,今天 ​

  • 旧的 per-package storybook 外壳(Samples~/Storybook 在 kit 和 game 中) 仍然与此引擎并存,故事逐个包迁移到它上面 — 名为 Storybook 的样本 和一个名为 Stories 的样本还不是同一回事。
  • Phosphor 的完整图标集未捆绑。 图标选择器读取在 SusIconRegistry 中 注册的内容 — 核心自己的 ~127 个图标始终存在;9000+ Phosphor 字形仅在 项目从 sus-core 导入单独的 PhosphorIcons 样本后出现。