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 字节。当一个包的故事被导入并被引用时,该程序集才会出现 在构建中 — 成本是按选择计费的,按项目,而不是按购买者。
打开它
- 向场景添加
UIDocument,并在同一 GameObject 上放置SusStorybookBehaviour(Add Component → SUS → Storybook Host)。进入播放模式(或 Editor 的 UI Toolkit 预览)— 外壳会挂载自己并列出每个声明了故事的包。 - 下游 UI 包以相同方式提供自己的组件故事:一个
Samples~文件夹,其自己的asmdef引用com.sharq-it.sus.core.storybook加上该包。从包的 Package Manager 条目导入Stories样本,并打开它放置的那个场景(UIDocument+SusStorybookBehaviour)— 该包的文档会说明样本的名称和场景文件。该场景本身从不列出故事,因此不会随着 目录增长或变得陈旧。 - 每个这样的包的故事以相同的方式到达 — 样本是按包的选择提供的,项目 只需为它导入的那些支付费用。
包的 SusStoryAssembly 标记也说明了程序集的用途(如下),选项卡从中遵循:
- Product,没有声明的故事 — 包仍然会得到一个选项卡,带有说明的牌子,而不是默默消失, 所以"我导入了错误的样本"和"此包没有故事"被理解为两种不同的情况。这是牌子发挥作用的地方:通过 手工导入
Samples~文件夹(kit、game、皮肤包)到达的故事。项目还可以在包根本不提供任何故事之前 请求相同的选项卡和牌子,不需要任何标记,通过SusStoryRegistry.DeclarePackage(key, packageId, version)。 - 测试工作台(
Kind = SusStoryPackageKind.Fixture)默认不获取选项卡和牌子 — 其故事存在于 通过来自套件的直接地址到达。隐藏默认不是同意消失:Window/SUS/Storybook/Show Engine Fixtures菜单 列出该机器上的每个测试工作台包,打开带有?fixtures=1的地址也会列出它们,所以与菜单切换关闭的人 分享的链接仍然会打开工作台选项卡。 - 没有标记,该键也没有
DeclarePackage调用 — 没有选项卡。
编写自己的故事
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 中):
[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:)而不是保持沉默。
范围和依赖关系
[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样本后出现。