00 — 设计哲学与目标
核心命题
软件原型的根本问题是:时间轴上的状态在静态文档中不可见。一个页面在真实运行中会经历加载、空、错误、选中、展开、权限受限等数十种状态,但传统原型工具只截取其中一帧。
RPML 的答案是:把时间轴压平为空间。每一个本需要点击或等待才能看到的状态,都被直接铺陈在同一份文档里。
两个关键词
- 复杂(complex):覆盖真实生产页面的信息密度。不是 Demo,是可以交付给工程师和 QA 的规格。
- 完整(complete):没有一个状态、分支、权限或边界情况是隐含的。读完原型不应还有"那这种情况怎么处理"的疑问。
设计目标
- 零运行时依赖:一个
.rpml 文件 + 一个 dist/rpui.js 渲染器,无框架、无 CDN。 - Agent 友好:标记语言可由 LLM 直接生成和审查,
llms.txt 是权威组件参考。 - 可机器验证:
rpml validate CLI 检查结构约束与 pin/annotation 引用完整性。 - 语言与实现分离:
spec/ 描述语言规则,packages/renderer-web 提供浏览器实现,两者可独立演进。
不在目标内
- 运行时交互(点击、动画、API 调用)
- 样式覆盖(RPML 不暴露 CSS 变量给原型作者)
- 代码生成(属于独立工具层,非语言核心)
01 — 基础语法规则
文件格式
.rpml 文件是 类 HTML 的标记,UTF-8 编码,根元素为 <page>。不需要 <?xml?> 声明,不需要 <html>/<head>/<body> 包装。
渲染器按 HTML 规则解析(而非严格 XML),因此:
- 布尔属性可省略值:
required、has-action、has-clear-button 合法。 - 文本中的裸
& 不需要转义(但 &/</> 仍被识别)。 - 自闭合写法
<button /> 与显式闭合 <button></button> 均可。
<page title="页面标题" route="/route" description="说明">
...
</page>
标签命名
所有 RPML 元素使用简洁的裸标签名(kebab-case,小写),例如 page、view、button、table。单词标签直接使用名称本身;复合名称保留连字符,例如 list-item、table-row、enum-item。平台原语使用 ios- / macos- 前缀。
属性
属性值用双引号包裹,类型:
| 类型 | 示例 | 说明 |
|---|
| 字符串 | label="提交" | 普通文本 |
| 枚举 | state="filled" | 固定取值集合 |
| 数字 | count="12" | 整数或浮点数字符串 |
| CSV | items="A,B,C" | 逗号分隔列表 |
| 布尔 | has-clear-button | 属性存在即为 true |
| data-pin | data-pin="3" | 正整数,从 1 起,不跳号 |
嵌套规则
<page> 是唯一合法的根元素。<view> 必须是 <page> 的直接子元素,且恰好一个。- 快照原语(
viewport 及其内部元素)嵌套在 <view> 内。 <annotation id="N"> 是 <page> 的直接子元素(顶级注释)。- 嵌套注释写在另一个
<annotation> 内部。
文本内容
注释块内的文本内容(prose)直接写在元素体内,支持换行。快照原语不应有文本内容(通过属性传值)。
禁止事项
- 禁止
<script>、<style>、onclick 等任何 JS/CSS 注入 - 禁止
position:absolute / position:fixed 内联样式 - 禁止外部
src(图片 CDN、图标 CDN) - 禁止直接 HTML 元素(
<div>、<button> 等)表示产品 UI
02 — 文档结构
顶层结构
<page title="..." route="..." description="...">
<!-- 1. 主快照区(必须,且只有一个) -->
<view device="web" scale="0.65">
<viewport device="web">
<!-- RPML 快照原语 -->
</viewport>
</view>
<!-- 2. 顶级注释(零个或多个,与 data-pin 一一对应) -->
<annotation id="1" label="导航栏">...</annotation>
<annotation id="2" label="筛选栏">...</annotation>
</page>
运行时将主快照渲染在左侧,注释渲染在右侧可独立滚动的面板。
page 属性
| 属性 | 必须 | 说明 |
|---|
title | ✓ | 页面标题,显示在文档头部 |
route | 推荐 | 对应的路由路径,如 /projects/:id/tasks(快照模式;文档模式不使用) |
description | 推荐 | 一句话说明主快照捕获的是哪个代表性状态 |
mode | 可选 | snapshot(默认)\ | doc;doc 切换为单栏文档流,详见下文 |
view 属性
| 属性 | 说明 |
|---|
device | web(默认 1440px)\ | ipad(834px)\ | mobile(390px) |
scale | 缩放比例,如 0.65,用于在文档中完整展示宽屏快照 |
width | 覆盖设备预设宽度(px 数字字符串) |
height | 显式数字则启用固定高度裁剪;省略或 auto 则自适应高度 |
viewport
<viewport> 是快照内容的直接容器,device 属性应与 view 一致。
双栏布局
渲染器自动将文档拆成两栏:
- 左栏:标题头 + 主快照(
view) - 右栏:顶级
<annotation> 列表,独立滚动
顶级注释不需要也不应该手动放入任何布局容器,运行时负责移动它们。
文档模式(mode="doc")
当内容本质上是线性的说明文字(发布说明、规格说明、需求文档)时,给 <page> 加上 mode="doc",渲染器会切换成单栏、自上而下的文档流:
- 没有缩放的
<view> 主快照画布; - 没有 pin 水滴标记;
- 没有右侧独立滚动的注释栏;
- 没有
route 路由徽标(文档不对应某个具体屏幕)。
所有子元素按源码顺序排布,形态接近 Markdown,但每一块都是 RPML 原语:
<page title="发布说明" mode="doc">
<doc-heading level="1">RPUI 0.9 发布说明</doc-heading>
<doc-paragraph>文档模式把页面渲染成单栏可读文档。</doc-paragraph>
<doc-list type="bullet">
<doc-list-item>新增 <code>doc-*</code> 排版原语。</doc-list-item>
<doc-list-item>页面级 <code>mode="doc"</code> 切换文档流。</doc-list-item>
</doc-list>
<doc-quote cite="设计原则">文档模式用来排布说明,而非绘制 UI。</doc-quote>
</page>
文档排版原语
| 标签 | 属性 | 说明 |
|---|
doc-heading | level(1–6,默认 1) | 标题,level="2" 起带分隔线 |
doc-paragraph | — | 段落;可内嵌 strong / em / code / a |
doc-list | type(bullet \ | number,默认 bullet) | 列表容器 |
doc-list-item | — | 列表项 |
doc-quote | cite(可选来源) | 引用块,cite 渲染为「— 来源」 |
文档流中也可以直接放普通快照原语(如 <alert>、<table>、<chart>)来举例,它们同样按源码顺序排布。
文档模式下校验器跳过 view / pin / annotation 的结构要求,只保留 title 检查。仍需要展示交互状态、权限变体、隐藏 UI 时,请使用默认的快照模式。
03 — 元素模型
元素分类
| 分类 | 标签 | 说明 |
|---|
| 画布元素 | page / view / annotation / enum | 文档结构骨架 |
| 快照原语 | 其余裸标签(如 button、table、card) | 静态 UI 积木 |
标签命名
RPML 使用简洁的裸标签名,没有前缀。单词标签直接使用名称本身(button、table、logo);复合名称保留连字符(list-item、table-row、enum-item);平台原语使用 ios- / macos- 前缀。
属性驱动
RPML 元素通过 HTML 属性传递所有显示状态,不接受子文本作为数据(注释块除外)。
<!-- 正确 -->
<button label="提交" variant="primary" state="loading" />
<!-- 错误:不要用文本内容传 label -->
<button>提交</button>
幂等渲染
实现层在 connectedCallback 中设置 dataset.rpReady = 'true',防止重复 DOM 生成。外部代码不需要关心这个细节。
空元素写法
快照原语通常没有子元素,推荐自闭合写法:
<button label="确定" variant="primary" />
<avatar size="32" initials="LY" />
元素完整列表
见 spec/elements/ 目录,按分类组织,每个元素一份规格文档。
04 — 注释模型
Pin 系统
在 <view> 内的任意元素上添加 data-pin="N",运行时自动渲染水滴形编号标记:
<navigator data-pin="1">...</navigator>
<table data-pin="2">...</table>
Pin 编号规则:
- 从 1 开始,不跳号(1, 2, 3…)
- 每个
data-pin="N" 必须有对应的顶级 <annotation id="N">
顶级注释
<annotation id="1" label="顶部导航">
导航栏固定在顶部,高度 64px。包含 Logo、面包屑、全局搜索、通知入口和用户头像。
</annotation>
id 与 data-pin 的值对应,运行时将注释渲染在右侧面板,并在 pin 和注释标题间建立深链接。每个 data-pin="N" 与编号注释 <annotation id="N"> 严格一一对应:编号注释必须有对应 pin,反之亦然。注释数量没有目标值——页面有多少个有意义的区域就标多少个,不为凑数添加,也不为压数删减。
全局注释
跨区域、不属于任何单一快照区域的说明(如全局权限矩阵、术语表、统一的空/错/载策略、页面级约定)不应伪造成带编号的注释(那会破坏 pin 一一对应)。改用 <annotation-global>:它没有 id、没有 pin,运行时把它渲染在注释面板最顶部(第 0 个注释)。
<annotation-global label="角色权限矩阵">
三类角色能力差异,供研发实现 RBAC、QA 设计权限用例。
</annotation-global>
嵌套注释
在注释内部嵌套 <annotation> 表达子区域的规格:
<annotation id="3" label="筛选栏">
筛选栏固定在表格上方,展开态默认可见。
<annotation label="搜索框行为">
默认态:显示 placeholder。聚焦:展示历史搜索。
filled:显示清空按钮。
</annotation>
</annotation>
Section 地址
运行时自动生成 data-rp-section 路径(作者无需手写):
| 层级 | 路径示例 | 标记样式 |
|---|
| 顶级 | 3 | 蓝色水滴,显示 id |
| 嵌套 1 级 | 3-2 | 紫色圆圈,显示局部索引 2 |
| 嵌套 2 级 | 3-2-1 | 绿色三角,显示局部索引 1 |
局部索引 = 该节点在其父节点的注释兄弟中的第几个(从 1 起)。
深链接
- 点击 pin 或注释标题 → URL 更新为
?section=<path>,滚动并高亮目标注释 - 加载带
?section=3-2-1 的 URL → 直接聚焦该嵌套注释
嵌套深度建议
| 层级 | 对应内容 |
|---|
| L1 | 页面区域(导航、侧边栏、表格…) |
| L2 | 区域内的元素或关注点 |
| L3 | 状态族(用 enum 展开) |
| L4 | 单个状态规则 |
| L5 | 边界/异常 |
深度由复杂度决定,不是目标值。简单组件停在 L2–L3 即可;复杂数据表若有必要可深入到 L5。不必让每个区域都到同一深度。
05 — 状态机声明规范
RPML 不运行状态机,它把状态机展开为空间。每个状态都显式呈现,不依赖运行时跳转。
声明方式
用 <enum> + <enum-item> 表达互斥状态集合:
<annotation label="搜索框状态">
<enum>
<enum-item label="默认" description="显示 placeholder,无输入" />
<enum-item label="聚焦" description="高亮边框,历史搜索下拉展开" />
<enum-item label="filled" description="有输入值,显示清空按钮" />
<enum-item label="无结果" description="显示空状态插图 + 清空建议" />
</enum>
</annotation>
异步状态的标准集合
对于任何从服务端加载数据的区域,至少覆盖:
| 状态 | 触发条件 |
|---|
| loading | 请求发出,响应未返回 |
| loaded | 正常数据,信息密度高的代表帧 |
| empty | 数据为空(首次使用 / 筛选后无结果) |
| error | 网络错误 / 服务端 5xx,含重试入口 |
| partial | 部分数据加载失败(如表格某列数据源不可用) |
状态组合
当两个轴相交时,枚举乘积:
<!-- 行状态 × 选中状态 -->
<enum>
<enum-item label="默认·未选" description="..." />
<enum-item label="默认·已选" description="复选框勾选,行高亮" />
<enum-item label="已读·未选" description="字体色变浅" />
<enum-item label="逾期·未选" description="截止日期红色警示" />
<enum-item label="逾期·已选" description="红色警示 + 行高亮叠加" />
</enum>
删去不可能的组合,保留所有有差异呈现的组合。
在快照中选择代表态
主快照应选择信息密度最高的代表态:有数据、有选中、有展开的面板、激活的筛选。不选空态或 loading 态作为主快照(把它们放进 enum)。
06 — 权限模型
核心思路
RPML 通过注释文字 + permission-gate + enum 组合描述权限差异,不通过条件渲染实现。
permission-gate
<permission-gate role="admin" label="仅管理员可见">
<button label="删除项目" variant="danger" />
</permission-gate>
| 属性 | 说明 |
|---|
role | 允许访问的角色标识(字符串,可以是任意 domain 定义的值) |
label | 权限说明文字,渲染为视觉提示框 |
权限 × 状态矩阵
当权限与状态组合时,用 enum 穷举乘积:
<annotation label="操作按钮区权限矩阵">
<enum>
<enum-item label="管理员·进行中" description="显示编辑、转移、删除" />
<enum-item label="管理员·已完成" description="只显示归档" />
<enum-item label="成员·进行中" description="只显示编辑,无删除" />
<enum-item label="成员·已完成" description="只读,无操作按钮" />
<enum-item label="访客" description="全部按钮隐藏" />
</enum>
</annotation>
最佳实践
- 在注释中明确列出角色名称(不用"有权限用户"这类模糊描述)
- 对每个权限差异点单独建一个
enum - 主快照选取权限最高的角色视角(通常是管理员),其他角色差异在枚举中说明
07 — 枚举分支声明规范
基础用法
<enum> 是水平排列的状态卡片容器,每个 <enum-item> 表示一个互斥状态。
<enum>
<enum-item label="待审核" description="提交后默认态,高亮黄色标签" />
<enum-item label="审核中" description="审核员已认领,标签变蓝" />
<enum-item label="已通过" description="绿色标签,操作区只保留归档按钮" />
<enum-item label="已拒绝" description="红色标签,显示拒绝原因折叠块" />
</enum>
自动编号
运行时为每个 enum-item 添加黑色方块索引徽章(1、2、3…),注释文字可引用"状态 2"。
覆盖矩阵方法
对于多维度交叉场景,先建矩阵再删除不可能组合:
状态轴: 待办 / 进行中 / 已完成 / 逾期
选中轴: 未选 / 已选
→ 保留有视觉差异的组合(逾期+已选 vs 逾期+未选 不同),
删除无差异组合(已完成+已选 与 已完成+未选 若样式相同则合并)
description 属性
description 是 enum-item 的可选短注释,适合写触发条件或渲染规则,不适合长段落(长段落改用嵌套注释)。
嵌套位置
<enum> 可以放在:
- 顶级
<annotation> 体内(描述该区域的整体状态族) - 嵌套
<annotation> 体内(描述子元素的状态族)
不要把 <enum> 放在 <view> 内。
08 — 数据绑定与动态值声明
静态属性值
RPML 是静态文档,所有"数据"通过属性值硬编码,表达一个具体的代表帧:
<stat-card label="本月销售额" value="¥128,450" trend="up" change="+12% vs 上月" />
<table rows="8" columns="名称,负责人,状态,截止日期" />
<pagination total="156" current="2" page-size="20" />
选取代表值的原则
| 类型 | 推荐做法 |
|---|
| 数值 | 选中等规模的真实值(既不为零,也不超长截断) |
| 列表 | rows 用 5–10,让表格视觉完整 |
| 文本 | 中等长度,暴露截断行为时选偏长值 |
| 日期 | 用具体日期,不用 YYYY-MM-DD 占位 |
动态值声明(注释中描述)
当属性值在运行时会变化时,在注释中描述来源和刷新规则:
<annotation label="统计卡片数据来源">
value 来自 /api/dashboard/stats 接口,TTL 60s 轮询刷新。
trend 由本期与上期差值的正负决定。
change 格式:±N% vs 上月,前端计算,服务端不返回。
</annotation>
占位符 vs 真实值
- 快照主体用真实感数据(不写 "用户名",写 "李云飞")
- 注释中说明真实数据的来源、格式、长度约束
09 — 边界条件声明规范
边界条件是"生产环境一定会发生但原型常常遗漏"的状态。每个有意义的区域都应在 L4–L5 层覆盖边界。
标准边界检查清单
数量边界
| 边界 | 示例说明 |
|---|
| 0 条数据 | 表格/列表为空,显示空状态组件 |
| 1 条数据 | 分页是否显示?批量操作是否可用? |
| 最大值 | 超长文本截断规则,… 位置,tooltip 展示完整内容 |
| 超过阈值 | badge 超过 99 显示 99+;进度超过 100% 如何处理 |
异步边界
| 边界 | 示例说明 |
|---|
| 超时 | 请求超过 X 秒,显示重试 |
| 并发冲突 | 两人同时编辑同一条记录 |
| 部分失败 | 批量操作 10 条中 3 条失败 |
权限边界
| 边界 | 示例说明 |
|---|
| 无权限访问 | 跳转 403 页还是内联提示? |
| 权限降级 | 管理员被降为成员后,已打开的操作入口如何变化 |
输入边界
| 边界 | 示例说明 |
|---|
| 必填为空 | 红色边框 + 错误文字 |
| 格式错误 | 即时验证还是提交时验证? |
| 超长输入 | maxlength 截断,字符计数器 |
| XSS 注入样本 | 用 <script>alert(1)</script> 作为值,验证转义 |
声明方式
边界条件放在 L4–L5 层的 enum-item 或深层 annotation 中:
<annotation label="批量删除边界">
<enum>
<enum-item label="全选后删除" description="含当前页外的已选条目,弹窗确认数量" />
<enum-item label="含已锁定条目" description="跳过锁定项,完成后提示跳过数量" />
<enum-item label="网络中断" description="乐观更新回滚,toast 显示失败数量" />
</enum>
</annotation>
10 — 语言版本策略
当前版本
RPML 1.0 — 与 RPUI @21stware/rpui v0.x 对齐。
版本标识
.rpml 文件不需要显式声明版本(渲染器透明处理兼容性)。规格版本号 1.0 用于本规格文档与演进规则,文件本身不携带版本标识。
演进规则
| 变更类型 | 版本策略 |
|---|
| 新增元素或属性 | Patch(向后兼容,旧文件不受影响) |
| 修改属性语义(兼容) | Minor |
| 删除元素/属性、破坏性语义变更 | Major(新 spec 版本,旧文件显示兼容警告) |
语言与渲染器解耦
RPML 语言只使用简洁的裸标签名(page、view、button、navigator 等)。解析器在解析时把这些语言标签映射为渲染器实际注册的 Web Components 标签名(如 page-el、main-view)——后者是实现细节,不是受支持的书写语法。早期草案曾直接书写带 -el 后缀或 rp-/proto-/snap- 前缀的标签名;这些写法已不再是合法的 RPML,新文档一律使用裸标签名。
渲染器版本关系
RPML spec 1.x → @21stware/rpui 0.x / 1.x
RPML spec 2.0 → @21stware/rpui 2.x(破坏性版本独立发布)
React Integration
@21stware/rpui-react wraps the RPML Web Components runtime as a React component with incremental, scroll-preserving rendering.
Installation
npm install @21stware/rpui @21stware/rpui-react
# or
bun add @21stware/rpui @21stware/rpui-react
Both packages are required: @21stware/rpui provides the runtime and custom element definitions; @21stware/rpui-react is the React binding.
Basic usage
import { RpmlRenderer } from "@21stware/rpui-react";
function App() {
return (
<RpmlRenderer
rpml={`
<page title="Dashboard" route="/dashboard">
<view device="web" scale="0.7">
<viewport>
<navbar logo="Acme" />
</viewport>
</view>
</page>
`}
/>
);
}
Live editor with debounce
When the RPML text changes on every keystroke (e.g. a live code editor), use debounce to avoid re-parsing on each character. The component preserves scroll position across updates.
import { useState } from "react";
import { RpmlRenderer } from "@21stware/rpui-react";
function Editor() {
const [rpml, setRpml] = useState("");
const [error, setError] = useState<string | null>(null);
return (
<div style={{ display: "flex", gap: 16 }}>
<textarea value={rpml} onChange={(e) => setRpml(e.target.value)} />
<div style={{ flex: 1 }}>
{error && <p style={{ color: "red" }}>{error}</p>}
<RpmlRenderer rpml={rpml} debounce={150} onError={setError} />
</div>
</div>
);
}
Rendering modes
Pass a mode prop to control interaction behavior:
// view (default) — scrollable panes, pins and navigation active
<RpmlRenderer rpml={source} mode="view" />
// edit — full-height flat layout, no scrolling, no interactions (read-only)
<RpmlRenderer rpml={source} mode="edit" />
// pick — flat layout, hover/click highlights elements for selection
<RpmlRenderer
rpml={source}
mode="pick"
onPick={info => console.log(info.langTag, info.line, info.attrs)}
/>
In pick mode elements highlight on hover and show a solid outline when clicked. Pass selected to mark elements programmatically (accepts an array of CSS selectors):
<RpmlRenderer
mode="pick"
rpml={source}
selected={["navbar-el", '[data-pin="1"]']}
onPick={({ langTag, line, pin, attrs }) => revealLine(line)}
/>
Theming
Pass a theme object to override the default color palette. All fields are optional CSS color strings:
const darkTheme: RpuiTheme = {
bg: "#1e1e2e",
surface: "#2a2a3c",
border: "#3a3a4c",
text: "#cdd6f4",
textMuted: "#7f849c",
accent: "#89b4fa",
// pick-mode highlights
pickHoverBorder: "rgba(137,180,250,.8)",
pickHover: "rgba(137,180,250,.08)",
pickSelectedBorder: "rgba(166,227,161,1)",
pickSelected: "rgba(166,227,161,.12)",
};
<RpmlRenderer rpml={source} theme={darkTheme} />;
The theme keys map to CSS custom properties on the host element (--rp-bg, --rp-surface, etc.) which cascade into the prototype chrome (page background, annotation pane, header text, borders).
Props
| Prop | Type | Default | Description |
|---|
rpml | string | required | RPML source text |
debounce | number | 0 | Delay in ms before re-rendering after prop change |
mode | `"view" \ | "edit" \ | "pick"` | "view" | Interaction mode |
theme | RpuiTheme | — | Color overrides applied as CSS vars on the host |
selected | string[] | — | CSS selectors to mark as selected (pick mode) |
onPick | (info: PickInfo) => void | — | Called when user clicks an element in pick mode |
onLinkNavigate | (to: string, section?: string) => void | — | Called when an <anchor> link is clicked; prevents default URL navigation |
onElementSelectRender | (info: PickInfo) => ReactNode | — | Return a React node to portal-render inside the selected element (pick mode) |
onError | `(msg: string \ | null) => void` | — | Parse error callback; null signals recovery |
className | string | — | CSS class on the host <div> |
style | CSSProperties | — | Inline style on the host <div> |
PickInfo
interface PickInfo {
element: Element; // the DOM element
tag: string; // Web Component tag, e.g. "navbar-el"
langTag: string; // RPML language tag, e.g. "navigator"
pin?: string; // data-pin value if present
line?: number; // 1-based source line in the original .rpml file
attrs: Record<string, string>;
}
Link navigation override
By default, <anchor> clicks navigate by reloading the page with ?rpml=<to>. Pass onLinkNavigate to intercept and handle navigation in-app (e.g. with React Router):
<RpmlRenderer
rpml={source}
onLinkNavigate={(to, section) => {
router.push(`/${to}${section ? `?section=${section}` : ""}`);
}}
/>
When onLinkNavigate is provided, the default location.href reload is suppressed.
Element select render (pick mode)
In pick mode, onElementSelectRender lets you render a React node as a portal inside the selected element — useful for inline inspectors, action menus, or overlays:
<RpmlRenderer
rpml={source}
mode="pick"
onElementSelectRender={(info) => (
<div
style={{
position: "absolute",
top: 0,
right: 0,
background: "#fff",
border: "1px solid #ccc",
padding: 8,
}}
>
<p>
{info.langTag} (line {info.line})
</p>
<button onClick={() => console.log("edit", info)}>Edit</button>
</div>
)}
/>
The portal is cleared when the element is deselected or when the RPML source changes.
VS Code integration
The VS Code extension's preview panel runs in pick mode by default. Clicking any prototype element highlights it and jumps the editor cursor to its source line in the .rpml file.
The webview automatically inherits VS Code's active theme by mapping VS Code CSS variables to RPUI CSS vars:
--vscode-editor-background → --rp-bg
--vscode-editor-foreground → --rp-text
--vscode-panel-border → --rp-border
--vscode-button-background → --rp-primary
… and pick-highlight vars from VS Code selection tokens
No configuration is needed — the preview adapts to light and dark themes automatically.
How incremental rendering works
Each RpmlRenderer instance creates a single DocRenderer (from @21stware/rpui) tied to its host <div>. On each rpml change:
- The RPML source is re-parsed and the host is updated via
replaceChildren. - Custom elements that have already initialized (marked with
data-rp-ready) skip their expensive setup in connectedCallback, re-using previously established DOM structure. - Scroll position is saved before and restored after the update via
requestAnimationFrame.
This makes successive updates fast: the component-level paint is fresh, but the runtime overhead is amortized.
Build
The package is built with rolldown and ships as a single ESM file. React and @21stware/rpui are peer dependencies (externalized from the bundle).
bun run --cwd packages/renderer-react build
name: rapid-prototype-implement description: Generate static RPML UI prototypes from product requirements, screenshots, existing UI code, or design notes. Each .rpml file is one screen or functional region (root <page>) that lays out every interaction state, permission variant, and loading/empty/error/validation branch in a single annotated layout; a multi-screen product becomes a set of such files, browsed together as a gallery. The result is a product definition engineering can build from and QA can test against. Use when the user wants to prototype, spec, or visualize one or more product screens, turn requirements or a design into a reviewable UI artifact, or document a page's states and edge cases, even if they don't explicitly say "RPML" or "prototype".
RPUI Prototype Implementation Skill
Turn product requirements, screenshots, existing UI code, or design notes into a static RPML prototype. Each .rpml file is one screen or functional region — a single readable document that bakes every interaction state, permission variant, and loading/empty/error/validation branch into one spatial layout, at a depth engineering can implement from and QA can derive test cases from, without running the app. A multi-screen product is a set of these files (one per page/region), browsed together as a gallery via serve, the compiler, or the playground.
RPML does not simulate interaction; it replaces time with space. The two words that govern quality are complex (cover a real production page's information density) and complete (no state, branch, permission, or edge case left implicit). For the _why_, see spec/00-overview.md.
If a reviewer finishes reading the prototype and still has to ask "but what happens when…", it is not done.
Capabilities (generation-first)
Generation best practices (non-negotiable)
These are short and load-bearing — follow them even before opening the references.
Output contract (per file). Each .rpml covers exactly one screen/region. Emit a bare .rpml file — root element <page>, no HTML wrapper, no doctype. The document holds:
- one
<page> with title, route (the screen's URL path), and a description naming the representative state the snapshot captures — or <page title="..." mode="doc"> for linear reference documents (release notes, specs) with no canvas or route, - exactly one
<view device="web|ipad|mobile"> containing the main snapshot (usually inside a <viewport device="…">), - snapshot content built with RPML primitives only,
data-pin="N" on every meaningful region, numbered from 1 with no gaps,- a matching top-level
<annotation id="N" label="…"> for every pin (and a matching pin for every numbered annotation — strict 1:1), <annotation-global label="…"> for cross-cutting notes that don't belong to one region (permission matrix, glossary, global policy) — pin-less, rendered at the top of the pane,<enum> / <enum-item> for every conditional branch and state family.
To preview, host the .rpml (playground ?rpml=, npx @21stware/rpui serve ., or the compiler). Only as a secondary "embed in a page" option do you wrap it in HTML with a single <script type="module" src="dist/rpui.js"></script> — never the primary output.
Pin↔annotation parity. Every data-pin="N" has exactly one top-level <annotation id="N">, and every numbered <annotation id="N"> has exactly one data-pin="N". Pins are consecutive from 1. A numbered annotation with no pin is a defect — route genuinely cross-cutting notes to <annotation-global> instead.
Overlay trigger pattern. Overlays and transient feedback (modal, drawer, dropdown, popover, tooltip, toast) are interaction _results_, not page regions. Never place them in the main snapshot. Instead: pin the trigger (the button/row/menu entry that opens it), state the trigger condition + permission gate in the annotation body, and render the overlay inside the annotation as an <enum> of its variants.
<!-- main snapshot: only the trigger is pinned -->
<button label="批量关闭" variant="danger" data-pin="5"></button>
<!-- annotation: trigger condition + overlay rendered here -->
<annotation id="5" label="批量关闭">
触发条件:勾选 ≥1 行后点击「批量关闭」,仅主管/坐席可见。点击弹出二次确认。
<enum>
<enum-item label="确认弹窗" description="列出影响范围与可逆性。">
<modal title="批量关闭确认" has-footer>
<alert
type="warning"
title="将关闭 3 条工单"
message="客户 7 天内可重开。"
></alert>
</modal>
</enum-item>
<enum-item label="关闭成功" description="3s 自动消失,列表刷新。">
<toast type="success" title="已关闭 3 条工单"></toast>
</enum-item>
</enum>
</annotation>
A side panel that is a _permanently docked_ structural region may appear open in the snapshot — but document its open/close trigger anyway. When unsure, treat it as an overlay.
Forbidden. No div/button/input/table for product UI (use RPML primitives; plain text in annotations is fine). No onclick, event attributes, timers, API calls, runtime focus, or hover behavior. No external CSS, image CDNs, or icon CDNs (the runtime ships inline SVG icons). No position:absolute/fixed in snapshot content — RPUI owns pin positioning.
Bare tags. Single-word elements have no suffix (button, table); compound names keep their hyphen (list-item, table-row); platform primitives use ios- / macos-. Never write the underlying component tags (page-el, main-view).
References (single sources of truth)
Depth lives in these — do not re-derive it:
- Method — recursive decomposition L1–L5, coverage-matrix for combinatorial states, annotation-body dimensions, the what-NOT-to-do list:
references/practise.md. - Compressed spec — root structure, attributes, rules at a glance:
references/spec-summary.md. Full language rules: spec/. - Component reference — every element and its attributes:
llms.txt (authoritative). One-line element index: references/element-index.md. - Worked example (the complexity bar) — a complete, implementation-depth prototype to study before authoring:
references/example-reference.rpml (a service desk: every region pinned and annotated, deep where the domain warrants, every overlay modeled as trigger → result, cross-cutting concerns in <annotation-global>). Match your depth to the domain — don't over-build a simple page to this level. More graduated examples (entry → complex) live in examples/. - Visual catalog —
bun run dev, then the source-mode preview at /preview/.
Workflow
- Gather inputs (requirement → screenshot → conditional code → permission matrix → async states). Make every inferred state explicit in an annotation.
- Pick the device preset (
web desktop/admin, ipad, mobile) — prefer fixed-width, auto-height. - Choose the most information-dense representative state for the snapshot: loaded data, active selection, an open docked panel, role-specific controls, active validation. Never an empty shell.
- Build the snapshot inside
<view> with RPML primitives, adding data-pin="N" to every meaningful region. - Create one top-level
<annotation id="N"> per pin. - Apply recursive decomposition (L1→L5) and the coverage-matrix method to each region — see
references/practise.md. - Write annotation bodies at implementation depth; expand every hidden interaction result into an
<enum>. - Verify no forbidden patterns (HTML product UI, JS, external resources, absolute positioning).
- Validate:
bun run validate <file.rpml> — fix every reported error before delivering.
Multi-screen products. A prototype is rarely one file. Produce one .rpml per screen or functional region, named by route, and collect them in a directory the gallery can host (serve, the compiler, or playground folder-drop). Never cram multiple screens into one <page> — the one-<view> contract forbids it. The split signal is conceptual, not numeric: if a <view> is covering more than one screen or route, split it into separate files. Link the resulting screens with <anchor to="other.rpml" section="N"> and state the entry/exit routes in each description so the set reads as one connected flow.
Quality bar
Before finishing, confirm:
- pin numbers continuous; every pin has a matching top-level annotation,
- the snapshot shows the most information-dense useful state,
- decomposition reached implementation depth where the domain warranted it (state machines, permission gates, validation, boundaries covered),
- combinatorial states (permission × state, role × scale, step × validation) enumerated, not collapsed,
- every hidden interaction result expanded into an enum; overlays modeled as trigger → result,
- role/permission differences explicit,
- runtime limits noted where they affect fidelity (e.g.
table cell text is sampled from column names — describe exact data in the annotation), - no forbidden product-UI HTML, scripts, event handlers, or external resources.
RPML Generation Practices
The single reference for how to decompose a page into a complete RPML prototype. SKILL.md routes here for method depth; the runnable system prompt is prompts/generate-rpml.md.
Collect in priority order:
- Product requirement / user story — the feature, route, and user goal.
- Screenshot or design draft — identifies regions, layout, and density.
- Existing code with conditionals — read every
v-if, &&, ternary, and guard; each is a state to enumerate. - Permission matrix / role notes — which roles exist and what differs per role.
- Known async states — loading, empty, error, retry, partial-failure, timeout.
If any input is missing, infer common SaaS/product states and make every assumption explicit in an annotation. Never silently omit a plausible state.
2. Recursive decomposition (L1–L5)
Apply this top-down to every pinned region. Stop when further splitting adds no implementation value.
| Level | Element | Purpose |
|---|
| L1 | <annotation id="N"> (pinned) | Structural area of the page: navbar, sidebar, filter bar, table, drawer |
| L2 | Nested <annotation> | Distinct responsibility inside the region: one column, a form field group, the bulk-action bar |
| L3 | <enum> or nested annotation containing one | Mutually exclusive states for that element: default/focus/filled/error; collapsed/expanded |
| L4 | <enum-item> + description | What each state means: trigger, threshold, transition, permission gate |
| L5 | Deepest annotation/enum | Extremes and failure modes: 0/empty/overflow values, race conditions, permission denials |
A simple stat card may stop at L3. A data table with a detail drawer routinely reaches L5. Let the domain decide depth; let completeness decide breadth.
3. Coverage-matrix method
Completeness in complex apps is combinatorial, not a flat list. When two or more axes interact, enumerate the product, not each axis alone:
- permission × state — detail-drawer buttons differ by role and by ticket status.
- role × data-size — admin view of 5000 rows vs agent view of 7 rows.
- flow-step × validation — each wizard step × (valid / invalid / pending).
- read-state × SLA × selection — a table row's appearance is the product of all three.
Build the matrix mentally, drop impossible cells, and create one <enum-item> per surviving combination. If a cell is intentionally out of scope, say so in an annotation rather than leaving it blank.
4. Annotation body structure
L1/L2 bodies must read like a spec, not a caption. For a non-trivial region, cover the relevant subset in plain prose — one or two precise sentences each:
- Trigger / entry condition — what causes this to appear or activate.
- Data source & refresh — where values come from, polling/refresh cadence.
- State enumeration — which states exist (then expand them in
<enum>). - Permission gate — which roles see/use it, what changes per role.
- Validation rule — required fields, formats, cross-field constraints.
- Error / async handling — loading, empty, partial-failure, retry behavior.
- Boundary values — limits, overflow, truncation, zero/critical states.
"Compact" means no padding — it does not mean omitting a dimension that matters. Completeness wins over brevity; precision wins over length.
4.1 Cross-cutting concerns → <annotation-global>
Some notes don't belong to any single pinned region: a role/permission matrix that spans the whole page, a global empty/error/loading policy, a glossary of domain terms, page-wide conventions. Do not invent a numbered annotation for these — a numbered annotation must always have a matching pin. Put them in <annotation-global label="…">, which is pin-less by design and renders at the top of the annotation pane (the "0th" annotation):
<annotation-global label="角色权限矩阵">
三类角色能力差异,供研发实现 RBAC、QA 设计权限用例。
<enum>
<enum-item label="管理员" description="全量读写"></enum-item>
<enum-item label="成员" description="读写本人"></enum-item>
<enum-item label="只读" description="仅查看与导出"></enum-item>
</enum>
</annotation-global>
4.2 Cross-page links and diagrams
<anchor to="other.rpml" section="N" label="…"> — link to another screen in the file set; section optionally deep-links a specific annotation on the target. Use for flow transitions and drill-downs so the set reads as one connected product.<diagram> — render a Mermaid flow / state / sequence / ER diagram inside an annotation to specify a state machine or flow precisely. Put the diagram header (graph TD, stateDiagram-v2, …) on its own line.
5. Quality bar
A prototype meets the bar when a reviewer reading it has no remaining "but what happens when…" questions.
Concrete targets:
- One annotation per pinned region — no target count. Pin and annotate every meaningful region the page actually has. A dense admin page has many; a simple form has few. Never pad to a number, never drop a real region to stay under one. Completeness decides breadth; the page decides the count.
- Depth follows complexity. Nest as deep as the region warrants — a stat card stays shallow, a data table with a detail drawer goes deep. Don't force uniform depth.
- Strict pin↔annotation parity. Every
data-pin="N" ↔ exactly one numbered <annotation id="N">, both directions. A numbered annotation with no pin is a defect. Cross-cutting notes go in <annotation-global> (see §4.1), not an orphan numbered annotation. - Every conditional branch in
<enum> — states, permission variants, validation outcomes, async results. - Implementation-depth annotation bodies: trigger conditions, data source, state-machine transitions, permission gates, validation rules, error handling, boundary values.
Reference: example-reference.rpml (bundled with this skill) — implementation-level bodies, every overlay modeled as trigger → result, with cross-cutting concerns in <annotation-global>. Study it before authoring; it is the complexity bar.
6. What NOT to do
- Do not use
div, button, input, or table for product UI. Use RPML primitives only. - Do not add
onclick, hover behavior, runtime focus, timers, API calls, or framework state. - Do not import external CSS, image CDNs, or icon CDNs. The runtime provides inline SVG icons.
- Do not use
position:absolute or position:fixed in snapshot content. RPUI owns pin positioning. - Do not place overlays (
modal, drawer, dropdown, popover, tooltip, toast) in the main snapshot. Pin the trigger; render the overlay inside its annotation enum. - Do not stack mutually exclusive states (empty + loading + modal) side by side in the snapshot.
- Use bare RPML tags. Single-word elements have no suffix (
button, table); compound names keep their hyphen (list-item, table-row); platform primitives use ios- / macos-. - Do not omit a plausible state because the input didn't mention it; infer and annotate.
7. Validation
Run the validator after generating:
bun run validate <file.rpml>
The validator checks:
- Every
data-pin="N" has a matching top-level <annotation id="N">. - Pin numbers are continuous from 1 with no gaps.
- Structural constraints (page root, exactly one view, etc.).
Fix all reported errors before delivering the file.
RPML Spec Summary (Context Pack)
An RPML file is HTML-like markup, parsed as HTML (not strict XML). The root element is <page>. No HTML wrapper, no doctype required. Because it parses as HTML, boolean attributes may omit their value (required, has-action) and bare & in text needs no escaping. Import the renderer once:
<script type="module" src="./dist/rpui.js"></script>
Or load a standalone .rpml file at runtime via the playground (?rpml=), npx @21stware/rpui serve ., or the compiler.
Root structure
Snapshot mode (default) — one screen with a scaled canvas and annotation pane:
<page title="..." route="/route" description="Snapshot shows [representative state]">
<view device="web|ipad|mobile" scale="0.65">
<viewport device="web|ipad|mobile">
<!-- snapshot: RPML primitives only, data-pin="N" on meaningful regions -->
</viewport>
</view>
<annotation id="1" label="Region Name">
Spec prose.
<enum>
<enum-item label="State A" description="Trigger/condition."><!-- RPML primitives --></enum-item>
</enum>
<annotation label="Sub-region">Nested spec.</annotation>
</annotation>
<!-- one <annotation id="N"> per data-pin="N" -->
</page>
Document mode (mode="doc") — linear prose, no canvas, no route:
<page title="..." mode="doc">
<doc-heading level="1">Title</doc-heading>
<doc-paragraph>Body text with <strong>bold</strong> and <code>code</code>.</doc-paragraph>
<doc-list type="bullet">
<doc-list-item>Item one.</doc-list-item>
</doc-list>
<doc-quote cite="Source">Quoted text.</doc-quote>
</page>
Two-layer model
Canvas layer — document structure and specification:
page — root; title, route (snapshot mode), description, optional mode (snapshot default | doc for linear documents with no canvas/route/pins).view — scaled snapshot frame; device, scale, optional width/height.viewport — snapshot viewport; same device as view.annotation — specification block; top-level has id matching a pin, nested has no id.annotation-global — page-level, pin-less note for cross-cutting concerns; renders at the top of the pane. No id, no pin.enum — horizontal container for mutually exclusive states.enum-item — one state card; label required, description optional.anchor — cross-page link (to, optional section) to another screen in the file set.diagram — Mermaid text → inline SVG; place inside an annotation.
Primitive layer — static UI building blocks used inside view and inside annotation enum-item bodies. A broad library across layout, controls, navigation, data display, feedback, enterprise, iOS, macOS, and agent families. The full registered set is enumerated in element-index.md.
Pin system
- Add
data-pin="N" to any element inside <view>. Pins number from 1 with no gaps. Pin as many regions as the page has — no target count. - Strict bidirectional parity: every
data-pin="N" ↔ exactly one top-level <annotation id="N">. A numbered annotation with no pin is a defect — put cross-cutting notes in <annotation-global> instead. - The runtime renders water-drop pin markers automatically. Never write pin DOM manually.
Annotation nesting and section addressing
Annotations nest arbitrarily. The runtime auto-assigns data-rp-section paths (authors do not write them):
| Depth | Example path | Marker |
|---|
Top-level (has id) | 3 | Blue water-drop, shows id |
| Nested depth 1 | 3-2 | Purple circle, shows local index 2 |
| Nested depth ≥2 | 3-2-1 | Green triangle, shows local index 1 |
Local index = 1-based position among annotation siblings under the same parent. Sibling order is significant.
Clicking a pin or annotation title sets ?section=<path> in the URL. Loading a URL with ?section=3-2-1 focuses that annotation.
Decomposition levels (L1–L5)
| Level | What it describes |
|---|
| L1 | Page region (annotation with id) |
| L2 | Element or concern inside the region (nested annotation) |
| L3 | State family — mutually exclusive states (enum) |
| L4 | Per-state rule — trigger, threshold, transition (enum-item + description) |
| L5 | Boundary/exception — edge cases, overflow, permission denial |
Not every region reaches L5. Let domain complexity decide depth.
enum usage
Use <enum> for: state families (loaded/loading/empty/error), permission variants, validation branches, overlay results (open/closed, success/failure), and any conditional branch in code. Each enum-item gets an auto-numbered black square badge. Combinatorial states (permission × state) must be enumerated as products, not as separate flat lists.
Overlay pattern
modal, drawer, dropdown, popover, tooltip, toast are never placed in the main snapshot. Pin the trigger element; render the overlay inside the trigger's annotation (usually inside <enum>).
Exception: a permanently docked side panel may appear open in the snapshot as the representative state, but its trigger and conditions must still be documented.
Forbidden in RPML
- Raw
div, button, input, table for product UI. onclick, event attributes, timers, API calls, framework state.- External images (use
image-placeholder), external CSS, CDN icons. position:absolute or position:fixed in snapshot content.- Prefixed or aliased tags — use bare RPML tag names only.
- Interactive JS of any kind.
Validation
bun run validate <file.rpml>
Checks structural constraints (root is page, exactly one view, page has a title, annotation-global carries no id), pin↔annotation parity, and consecutive pin numbering from 1.
RPML Element Index
All elements registered by the RPUI runtime. RPML authoring uses the bare language tag names listed below; the runtime maps each to its Web Component tag. This table is the authoritative roster — packages/parser/src/vocabulary.ts is the single source it mirrors.
Canvas elements
| Element | Category | Description |
|---|
| page | Canvas | Root document shell; title, route, description, optional mode. Default (snapshot): main view left, annotations right. mode="doc": single-column document flow, no route badge, no view/pins/pane |
| view | Canvas | Scaled snapshot frame; device preset sets fixed width; scale attribute zooms the canvas |
| annotation | Canvas | Specification block; top-level (id=N) links to data-pin="N"; nested adds sub-region spec |
| annotation-global | Canvas | Page-level, pin-less note for cross-cutting concerns; renders at top of pane; no id/pin |
| enum | Canvas | Horizontal row of mutually exclusive state/variant cards |
| enum-item | Canvas | One state card with label and optional description; auto-numbered with a black square badge |
| anchor | Canvas | Cross-page link (to, optional section) to another screen in the file set |
| diagram | Canvas | Renders Mermaid text (flow/state/sequence/ER) to inline SVG inside an annotation |
Layout primitives
| Element | Category | Description |
|---|
| viewport | Layout | Fixed-width snapshot viewport matching a device preset |
| layout | Layout | CSS grid container with columns, rows, and gap attributes |
| panel | Layout | White panel/card shell with optional padding and elevation |
| navigator | Layout | Top navigation bar container |
| sidebar | Layout | Side navigation container; supports collapsed state |
| logo | Layout | Logo placeholder with size and label |
| split-pane | Layout | Two-column split layout |
| divider | Layout | Horizontal or vertical divider line |
| spacer | Layout | Empty space with explicit size |
| scroll-area | Layout | Custom-styled scrollable container with thin scrollbar; height attribute |
| collapsible | Layout | Expand/collapse section with label and expanded state; body slot for children |
| aspect-ratio | Layout | Container maintaining a width/height ratio; ratio attribute (e.g. 16/9) |
Control primitives
| Element | Category | Description |
|---|
| search | Controls | Search field with state (default/focus/filled/error/disabled) and optional clear button |
| input | Controls | Text input with label, state, value, optional leading icon, error, help |
| textarea | Controls | Multi-line text input with rows, label, state, error, help |
| select | Controls | Dropdown select; state collapsed/expanded/filled/error/disabled; options CSV; error |
| button | Controls | Action button with variant (primary/secondary/ghost/danger/link), state, icon, size |
| button-group | Controls | Container grouping related buttons |
| checkbox | Controls | Checkbox with state (unchecked/checked/indeterminate/disabled/error) |
| checkbox-group | Controls | Checkbox group with label, direction, and validation error |
| radio | Controls | Radio button with state (unchecked/checked/disabled/error) |
| radio-group | Controls | Radio group with label, direction, and validation error |
| toggle | Controls | Toggle switch with state (on/off/disabled/error) |
| password-input | Controls | Masked password field with optional eye toggle; full state matrix |
| tag-input | Controls | Chip multi-input; tags CSV, placeholder, label, state, error |
| form | Controls | Form container with layout (vertical/horizontal) |
| form-item | Controls | Labeled form field wrapper with required, error, help |
| form-field-description | Controls | Field-level remark rendered below a field; text attr or child text |
| radio-card | Controls | Selectable card with radio indicator; label, description, state unchecked/checked/disabled |
| date-picker | Controls | Date picker input with state, value, error, help |
| upload | Controls | File upload zone with state (empty/has-file/uploading/error) and progress |
| image-placeholder | Controls | Placeholder for images; use instead of external image URLs |
| progress | Controls | Progress bar or circle with value, kind, and status |
| slider | Controls | Single-thumb slider with value, min, max; state error/disabled |
| range | Controls | Dual-thumb range slider with low, high, min, max; state error/disabled |
| number-input | Controls | Numeric input with +/- steppers; state error/disabled |
| rating | Controls | Star rating display with value and max; state disabled |
| pin-input | Controls | OTP/PIN cell input with length and value; state disabled |
| color-swatch | Controls | Color swatch chip with hex value and label; state disabled |
| autocomplete | Controls | Autocomplete input; open shows list; label, state, error |
| combobox | Controls | Search-select combo box; options CSV, value, placeholder |
| input-group | Controls | Input with prefix/suffix addons; prefix, suffix, placeholder, value |
| toggle-group | Controls | Toggle button group; type single/multiple, options CSV, active index |
| toggle-group-item | Controls | Individual toggle group button with label and active state |
| field | Controls | Field wrapper; label, description, error attributes; control slot for children |
Navigation primitives
| Element | Category | Description |
|---|
| badge | Navigation | Numeric badge/count indicator with max cap |
| avatar | Navigation | User avatar circle with initials and size |
| list | Navigation | Generated list; auto-creates items when no children provided |
| list-item | Navigation | List row with label, icon, badge, and state |
| tabs | Navigation | Tabbed navigation container with active tab |
| tab | Navigation | Individual tab with label and optional badge |
| pagination | Navigation | Pagination control with total, current, and page-size |
| steps | Navigation | Step indicator for multi-step flows with active step |
| breadcrumb | Navigation | Breadcrumb trail from comma-separated items |
| segmented | Navigation | Segmented control (button group acting as radio) |
| command-palette | Navigation | Command palette overlay with query and results |
| context-menu | Navigation | Context menu with comma-separated items |
| menu | Navigation | Menu container |
| menu-item | Navigation | Menu row with label, icon, shortcut, and state |
| toc | Navigation | Table of contents from comma-separated items |
| kbd | Navigation | Keyboard shortcut chip(s) |
| menubar | Navigation | Horizontal menu bar container; contains menubar-item children |
| menubar-item | Navigation | Menu bar item with label and dropdown chevron |
| nav-menu | Navigation | Horizontal navigation menu with bottom-border active indicator; contains nav-menu-item children |
| nav-menu-item | Navigation | Navigation menu item with label and active state |
Data display primitives
| Element | Category | Description |
|---|
| table | Data display | Static table; columns CSV; has-checkbox and has-action add affordances; <table-row> children pin exact cell values, else sampled by rows |
| table-row | Data display | table child declaring one explicit row; content CSV, optional checked |
| table-list-row | Data display | Standalone row slice with content CSV and state (default/selected/unread/highlighted/disabled) |
| chart | Data display | Static inline-SVG data viz; kind bar/line/area/donut/sparkline; data CSV; labels; height; color |
| avatar-group | Data display | Overlapping avatar stack with +N overflow; items count or <avatar> children |
| comment | Data display | Comment thread entry; author/avatar/time; body slot; nest for replies |
| file-list | Data display | File attachment list container; items count or <file-item> children |
| file-item | Data display | One file row; name (sets icon), size, state uploaded/uploading/error, progress |
| bulk-action-bar | Data display | Bulk action bar shown when rows are selected; count and actions CSV |
| empty | Data display | Empty state with label, description, and optional action |
| loading | Data display | Loading placeholder; kind skeleton or spinner; rows count |
| alert | Data display | Inline alert banner (info/success/warning/error) with title and message |
| toast | Data display | Toast notification (info/success/warning/error) rendered in annotations |
| dropdown | Data display | Static opened dropdown panel |
| popover | Data display | Static opened popover panel |
| tooltip | Data display | Visible tooltip bubble with text and position |
| modal | Data display | Static opened modal dialog with title, width, optional footer |
| drawer | Data display | Static opened side drawer with side, width, title |
| card | Data display | Content card with title, subtitle, optional image and footer slots |
| stat-card | Data display | KPI card with label, value, trend, and change |
| tag | Data display | Colored label tag; closable variant |
| chip | Data display | Compact token chip with label, icon, closable |
| tree | Data display | Tree container |
| tree-item | Data display | Tree node with label, icon, level, expanded/collapsed, state |
| timeline | Data display | Timeline container |
| timeline-item | Data display | Timeline event with label, time, and state |
| calendar | Data display | Month grid calendar with selected date |
| kanban | Data display | Kanban board container |
| kanban-column | Data display | Kanban column with title and count |
| kanban-card | Data display | Kanban card with label and tag |
| code-block | Data display | Code placeholder with language and line count |
| diff | Data display | Diff view with added/removed/context lines |
| image-grid | Data display | Grid of image placeholders with count and columns |
| key-value | Data display | Description list container |
| kv-row | Data display | Key-value row with label and value |
| accordion | Data display | Accordion container |
| accordion-item | Data display | Expandable accordion section with label |
| banner | Data display | Full-width page-level banner (info/success/warning/error) |
| skeleton | Data display | Loading skeleton shape (line/block/card/list/avatar) |
| countdown | Data display | Time-remaining chip with value |
| result | Data display | Full-page result screen (success/error/empty) with title and optional action |
| carousel | Data display | Horizontal scrolling carousel with prev/next buttons; contains carousel-item children |
| carousel-item | Data display | One slide in a carousel; fixed 300px width |
| data-table | Data display | Enhanced table with sortable headers; columns CSV, rows semicolon-separated |
| hover-card | Data display | Inline trigger text that shows a card on hover; trigger, title, description |
| sonner | Data display | Toast notification card; title, description, type (info/success/warning/error) |
| permission-gate | Enterprise | Locked content wrapper with reason label |
| quota-bar | Enterprise | Usage bar that turns red at ≥90%; label, used, limit |
| api-key | Enterprise | Masked API key display with copy affordance |
| audit-row | Enterprise | Audit log row with actor, action, time |
| workflow-node | Enterprise | Workflow step node with label and state |
| Element | Category | Description |
|---|
| ios-navbar | iOS | iOS-style navigation bar with title, large variant, back, trailing |
| ios-tabbar | iOS | iOS tab bar with items, icons, active tab |
| ios-list | iOS | iOS grouped list with header |
| ios-list-item | iOS | iOS list row with label, detail, icon, chevron |
| ios-action-sheet | iOS | iOS action sheet with title, actions, destructive action |
| ios-alert | iOS | iOS alert dialog with title, message, actions |
| ios-switch | iOS | iOS-style toggle switch |
| ios-segmented | iOS | iOS segmented control |
| ios-button | iOS | iOS-style button (filled/tinted/plain) |
| ios-search | iOS | iOS search bar |
| ios-stepper | iOS | iOS stepper control |
| Element | Category | Description |
|---|
| macos-window | macOS | macOS window chrome with traffic light buttons |
| macos-toolbar | macOS | macOS toolbar strip |
| macos-menubar | macOS | macOS menu bar with items |
| macos-sidebar | macOS | macOS source list sidebar |
| macos-source-item | macOS | macOS sidebar source item with label, icon, group, state |
| macos-segmented | macOS | macOS segmented control |
| macos-popover | macOS | macOS-style popover |
| macos-sheet | macOS | macOS sheet dialog |
| macos-stepper | macOS | macOS stepper control |
| macos-disclosure | macOS | macOS disclosure triangle with label and expanded state |
| macos-table | macOS | macOS-style table with sortable header look |
Agent / conversational UI primitives
| Element | Category | Description |
|---|
| chat | Agent | Conversation container wrapping the message stream |
| user-message | Agent | Right-aligned user message bubble |
| agent-message | Agent | Left-aligned agent message (default role "Agent") with optional rich children |
| system-message | Agent | Centered system/context note |
| tool-call | Agent | Tool call; shows the tool name as headline + 工具 tag + status; args on their own line |
| agent-output | Agent | Command/code/tool output block (kind: text/code/terminal) |
| reasoning | Agent | Collapsible thinking/reasoning block |
| message-actions | Agent | Per-message action buttons (copy/retry/up/down/edit/share) |
| suggestions | Agent | Suggested reply/prompt chips |
| typing | Agent | Streaming typing indicator |
| composer | Agent | Prompt input bar; attachments (files), mode toggles (thinking/web/code), model pill, state idle/streaming/disabled |
| citation | Agent | Source reference chip with index and title |
| token-usage | Agent | Token/context usage meter with used and limit |
Document mode primitives (mode="doc" pages only)
| Element | Category | Description |
|---|
| doc-heading | Document | Heading level 1–6; level 2+ adds a bottom border |
| doc-paragraph | Document | Body paragraph; inline strong, em, code, a allowed |
| doc-list | Document | Ordered/unordered list; type bullet (default) or number |
| doc-list-item | Document | List item inside doc-list |
| doc-quote | Document | Block quote with optional cite attribution line |
Design system palette primitives
| Element | Category | Description |
|---|
| color-palette | Design System | Grid of color swatches; items="Name:#hex,…" CSV |
| font-palette | Design System | Typography sample table; items="Label:size/weight,…" CSV |
| space-palette | Design System | Proportional bar chart of spacing tokens; tokens="name:px,…" CSV |
| radius-palette | Design System | Corner-radius swatch grid; tokens="name:value,…" CSV |