本协议用于约束仓库内 单组件 HTML fragment 的结构、可配置方式与质量标准。
合并门禁:任意 PR 若不满足本协议的“必须/禁止”条款,将被 CI 拒绝合并。
为“器材/元件组件库”提供统一的最小标准,使:
- 单组件足够短小精悍(低负重、便于复用)
- 统一样式与可配置机制(避免每个组件自带一堆背景/字体/布局)
- 可被宿主系统组合为完整实验页面(宿主负责页面布局/背景/说明文案等)
组件文件只负责“器材长什么样 + 哪些参数可调 + 参数如何映射到视觉”,不负责页面布局、背景、字号、说明文案等。
- 组件文件:位于
components/**下的单个.html文件。 - HTML fragment:不包含
<!doctype html> / <html> / <head> / <body>的片段。 - Manifest:写在组件文件顶部的
<!-- @cmp-manifest ... -->JSON 元数据块,用于检索、渲染与参数注入。 - 宿主:最终承载组件的业务系统/编辑器/画布。
- 组件文件必须是 HTML fragment(仅输出组件本体)。
- 一个组件文件 只定义一个组件。
- 组件文件内必须存在且仅存在 一个根节点(组件根容器)。
- 禁止输出完整页面结构:
<!doctype html><html><head><body>等。 - 禁止引入外链资源:外部 CSS/JS、图片 URL、字体 URL(
http:///https://////@import等)。 - 禁止依赖宿主页面的全局样式(必须自洽)。
- Manifest 必须放在文件最顶部,使用注释块包裹:
<!-- @cmp-manifest- JSON
-->
- JSON 必须可被严格解析(不允许注释、尾逗号)。
Manifest 顶层字段(v1):
schema(必须):固定为"cmp-manifest/v1"id(必须):全局唯一,例如phy.resistor.axial.basicname(必须):中文名nameEn(建议):英文名,用于国际化显示category(必须):subject/domain形式,例如physics/circuitversion(必须):SemVer,例如1.0.0viewport(建议):{ "w": 64, "h": 64 },用于默认预览比例tags(建议):标签数组,用于搜索过滤props(建议):可配置项声明(用于宿主生成调参面板)cssVars(建议):该组件支持的 CSS 变量声明(用于宿主注入/文档展示)
props(建议但强烈推荐):每个条目包含:
key:参数名(例如size)type:如number(px)/number(0-1)/color/enum(...)default:默认值min/max:可选desc:说明
cssVars(建议):对象格式,键为 prop 的 key,值为对应的 CSS 变量名(包含 -- 前缀)。
例如:
{
"size": "--cmp-size",
"stroke": "--cmp-stroke",
"glow": "--cmp-glow"
}这种方式建立 props 到 CSS 变量的映射,方便宿主系统自动将 props 值注入到对应的 CSS 变量中。
<!-- @cmp-manifest
{
"schema": "cmp-manifest/v1",
"id": "phy.meter.voltage.draggable",
"name": "电压表(可拖拽)",
"category": "physics/circuit",
"version": "1.0.0",
"viewport": { "w": 120, "h": 120 },
"tags": ["voltmeter", "meter", "circuit", "draggable"],
"props": [
{ "key": "size", "type": "number(px)", "default": 120, "min": 48, "max": 360, "desc": "组件尺寸(正方形)" }
],
"cssVars": {
"size": "--cmp-size"
}
}
-->组件根节点必须满足:
class="cmp"data-cmp-id="..."与 Manifest.id 完全一致role="img"且aria-label="组件名称"
示例:
<div class="cmp" data-cmp-id="phy.resistor.axial.basic" role="img" aria-label="电阻(轴向)">
<!-- svg / div 结构 -->
</div>- 样式必须内联在组件文件中(
<style>...</style>),不得外链。 - 所有 CSS 选择器必须 以组件根作用域前缀 限定,避免污染宿主:
- 推荐:
.cmp[data-cmp-id="..."] ...
- 推荐:
- 视觉参数必须通过 CSS 变量 可被宿主覆盖(见 5.3)。
- 禁止任何全局选择器:
html/body/:root/*(除非被组件根前缀限定)。 - 禁止设置宿主背景、页面字体、全局字号、页面布局等与“组件本体”无关的内容。
通用视觉 token(建议每个组件尽量复用这些命名):
--cmp-size:组件尺寸(正方形)--cmp-stroke:描边颜色--cmp-stroke-width:描边宽度--cmp-accent:强调色--cmp-glow:强调/发光强度(0~1)--cmp-shadow:阴影强度(如需要)
写法要求:
- 必须使用
var(--xxx, fallback)提供 fallback,保证组件自洽。 - 宿主可通过根节点
style覆盖变量,例如:<div class="cmp" ... style="--cmp-size:96px;--cmp-accent:#ffcc00"></div>
- 能用 CSS/HTML 实现就不要 JS。
- 组件必须确有交互需求(拖拽、旋转、指针指示等)。
- JS 必须 完全自包含:IIFE 包裹,不向
window挂载任何变量/函数。 - JS 只能查询/操作自己的根节点内部 DOM。
- 禁止网络请求;禁止读取/写入宿主敏感数据;禁止永久运行的定时器。
交互组件应通过 CustomEvent 通知宿主状态变化,详见 docs/EVENT.md。
- 事件名:
cmp:change(过程中持续触发)、cmp:changeend(交互结束时触发一次) - 事件必须
bubbles: true, composed: true detail结构:{ id, type, values }- Manifest 中应声明
events字段描述组件支持的事件
建议模式:
<script>
(() => {
const root = document.currentScript?.previousElementSibling; // 或通过 data-cmp-id 查询
if (!root) return;
// 仅操作 root 内部
})();
</script>- 根节点
role="img"+aria-label - 重要可交互元素(如旋钮)应具备合理的
aria-*(可选)
提交 PR 前请确保:
- 组件是 HTML fragment(无 doctype/html/head/body)
- 仅一个根节点,且带
class="cmp"、data-cmp-id、role、aria-label - Manifest 存在且 JSON 可解析
- Manifest.id 与 data-cmp-id 一致
- 无外链资源(无 http/https/@import 等)
- CSS 作用域已隔离(无全局选择器污染)
- 关键可配置项已用 CSS 变量暴露,且提供 fallback
- 若有 JS:IIFE 自包含、无全局污染、无网络请求
- v1 的 schema 固定为
cmp-manifest/v1 - 不兼容变更必须升级 schema(例如 v2),并在仓库
docs/内提供迁移说明
cmp-manifest/v2 adds machine-readable locale metadata without changing the DOM contract.
- Language-neutral fields stay at the top level:
id,schema,category,version,viewport,props[].key,cssVars - Locale-specific fields move into
locales[locale]:name,tags,ariaLabel, optionaldescription, localized prop text, and localized event text - Default locale is
zh-CN - Hosts and tools must fall back field-by-field to
zh-CN - Legacy
cmp-manifest/v1remains valid during migration