Astryx 组件族契约(Family Contract)体系:跨组件共享行为的治理机制与实践指南
【免费下载链接】astryxAn open source design system that's fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx
导读
组件族契约(Family Contract)是 Astryx 开源设计系统中用于"跨组件共享行为治理"的核心机制:当一组兄弟组件需要共享输入框尺寸、末端操作位(end lane)、浮层关闭(overlay dismissal)或状态呈现等行为时,家族契约负责拥有这些共享规则,而单个组件规范(component spec)只需链接到所属家族,不必复制共享规则。本文将以docs/families/README.md为骨架,结合docs/templates/knowledge/family-contract.md模板、6 份已批准的家族契约记录以及packages/core中的真实源码与测试,完整讲解家族契约的定位、生命周期、记录结构与六大实际家族(按钮、输入字段、布局原语、布局区域、导航目标、浮层关闭),帮助读者掌握这套知识契约体系的设计意图、读写规范与验证方式。
一、什么是组件族契约
在 Astryx 仓库中,docs/families/README.md用三句话定义了家族契约的定位:
Family contracts own behavior shared by sibling components, such as input sizing, end lanes, overlay dismissal, or status presentation. A component spec links to its family instead of copying the shared rule.
即:家族契约拥有兄弟组件之间共享的行为,典型如:
- input sizing(输入框尺寸,如
sm/md/lg的统一行高契约); - end lanes(输入字段末端操作位,如清除按钮、加载 Spinner、状态控件、展开按钮占用的非重叠空间);
- overlay dismissal(浮层关闭,如 Escape 只关最上层浮层的"单一栈"规则);
- status presentation(状态呈现,如校验状态的
attached/detached/tooltip三种放置方式)。
这一设计直接服务于"单一事实、单一属主"(one fact, one owner)的仓库知识治理原则(见 knowledge-contracts.md 中的 INV2):组件规范只描述"这个组件独立承诺的聚合行为",凡是被多个兄弟组件共享的规则,一律收敛到家族契约,避免同一规则在多份组件文档中被复制、改写以致漂移。
家族契约的生命周期非常明确:
- 新记录以
draft(草稿)起步,仅供评审参考,不构成规则; - 只有经过显式的属主(owner)批准后才升级为
current(当前生效),评审与实现才可依赖; - 被取代的记录转为
archived(归档),保留上下文并说明为何不再治理。
新记录的撰写必须使用 docs/templates/knowledge/family-contract.md 模板,模板字段由 docs/schemas/knowledge/v3.json 定义,并由 scripts/check-knowledge.mjs 校验模板、记录与审批元数据。
二、家族契约记录的标准结构
每一份家族契约都遵循统一的模板骨架,理解这套骨架是读懂任何一份家族契约的前提。以 family-contract.md 为例,记录由frontmatter 元数据与正文小节两部分组成。
2.1 frontmatter:记录的身份与权威信息
| 字段 | 含义 |
|---|---|
schema_version/template_version | 当前采用的 schema 与模板版本 |
kind | 固定为family,与 component/module/design/theme/system 等知识记录种类区分 |
id | 规范 ID,格式为family:<family-name>,作为其他记录链接的规范标识 |
authority | draft/current/archived三态之一 |
archive_reason/superseded_by | 归档原因与取代它的记录 ID |
approved_by/approved_at | 批准人(owner)与批准时间,current的前提 |
owners | 记录属主列表,负责该家族后续评审与变更 |
review_triggers | 触发评审的维度,如behavior, layout, theming, accessibility, public-api |
verified_by | 代表性验证锚点(测试或检查),如packages/core/src/Button/Button.test.tsx |
members | 家族成员组件列表,格式为component:<Name> |
architecture/contributing | 关联的架构记录与贡献记录 |
deciding_specs | 做出关键决策的规范引用,如spec:AST-002/DEC-1 |
2.2 正文小节:契约的九大组成部分
模板正文规定了 9 个必备小节,每份家族契约都必须覆盖:
- Intent(意图):一段话说明用户(构建者)在使用这一族组件时应获得的一致体验;
- Membership rule(成员规则):明确什么组件属于、什么组件是协作者(Collaborators)、什么组件被排除(Excluded)——成员资格依据"公开职责"而非"是否 import 了某个组件或渲染了某个元素";
- Shared owner(共享属主):指出某个共享概念由哪个原语、Hook 或接缝拥有(如 Button 家族中 Button 拥有公共操作表面,IconButton 是 Button 的纯图标投影);
- Canonical concepts(规范概念):以表格列出共享概念及其取值/状态、默认语义与稳定性;
- Cross-component invariants(跨组件不变式):以FR1、FR2……编号列出每个成员 MUST/MAY/MUST NOT 的硬性规则;
- Allowed component variation(允许的组件差异):以AV1、AV2……编号列出成员被允许的刻意差异(这正是"族"区别于"单一组件"的地方);
- Representative matrix(代表矩阵):把"成员 + 状态"映射到"共享不变式 + 刻意差异";
- Adoption and exceptions(采纳与例外):逐组件记录当前采纳机制与已知缺口/例外,并明确哪些是"待关闭的实现缺口"而非"已批准的例外";
- Verification map(验证映射):把每个 FR 映射到具体测试/浏览器证据、代表成员与状态,以及"何种变异会让它失败"的失败预期。
此外还有Decision links(决策链接)、Open questions(开放问题)与Content boundary(内容边界)三个收尾小节。其中 Content boundary 尤其重要:它声明"本文件只拥有跨组件家族行为,不重复组件本地属性表、回调载荷类型、消费者文档、当前审计结果或系统规范依据"——这正是 INV2(一个事实只有一个属主)在家族层级的落点。
三、六大已生效家族契约详解
截至本文写作时,docs/families/下共有 6 份authority: current的家族契约,它们共同覆盖了 Astryx 核心组件库(packages/core)的主要共享行为面。下面逐一拆解其核心内容。
3.1 按钮家族(family:buttons)
记录位置:docs/families/buttons.md。批准人cixzhang,批准于 2026-09-04。
意图:无论动作是有标签的、纯图标的、持久的、独立的还是分组的,用户面对的都应当是一套连贯的按钮系统。成员共享相同的控件几何、可访问名称要求、交互反馈、异步动作模型与表面所有权,同时各自拥有瞬时动作、导航目标、持久按下状态或分组的专属语义。
成员(Members):Button、IconButton、ToggleButton、ButtonGroup、ToggleButtonGroup。协作者(Collaborators):Spinner(挂起反馈)、Tooltip(可见解释)、LinkProvider(Button 的导航渲染器)、SizeContext(继承的控件尺寸)、DropdownMenu(可向 ButtonGroup 提供按钮触发器)。协作者不会因此成为按钮家族成员。排除(Excluded):Link 的主职是导航而非按钮表面;Switch、CheckboxInput、RadioList 表达设置或表单值而非按钮动作;SegmentedControl 与 TabList 在各自的选区/导航契约下切换视图或目标。
规范概念表(节选核心行):
| 概念 | 取值/状态 | 默认语义 | 稳定性 |
|---|---|---|---|
| 激活模型 | 瞬时动作 / 导航 / 持久按下 | Button 与 IconButton 激活一次;ToggleButton 表达保留的按下状态 | 已发布区分 |
| 内容模式 | 可见标签 / 自定义可见内容 / 纯图标 | 每个控件必须有可访问的label;纯图标时视觉隐藏 | 已发布家族规则 |
| 尺寸 | sm、md、lg | md;显式成员尺寸优先于继承的组尺寸 | 已发布家族轴 |
| 视觉状态 | rest、hover、focus、active、disabled、loading(按下处适用 pressed) | 状态保持控件几何与可访问目的 | 已发布家族规则 |
| 异步动作 | 无 / fire-once / 可中断持久动作 | 普通动作在挂起期间去重;持久切换保持可逆 | 已发布家族区分 |
| 高度(elevation) | none、low、med、high | none;绘制可见表面的元素拥有阴影 | 已发布家族轴 |
| 分组 | 独立 / 有间距集合 / 连接表面 | 语义与绘制包含决定所有权 | 家族规则 |
12 条跨组件不变式(FR1–FR12)中最值得注意的几条:
- FR1:每个控件必须有非空的可访问
label;纯图标控件通过aria-label暴露标签,图标不能替代程序化名称。源码中 Button.tsx 的isIconOnly映射与测试 Button.test.tsx 完全印证:测试断言isIconOnly时label被映射到aria-label("maps label to aria-label and keeps the icon when icon-only",断言toHaveAttribute('aria-label', 'Settings'))。 - FR2:原生动作语义是默认——渲染可操作的按钮、键盘激活、focus-visible 反馈、
type="button";href模式是显式导航变体,同时遵循family:navigation-destinations。 - FR4:挂起反馈必须设置
aria-busy、保持控件尺寸稳定、呈现 Spinner 而不改变可访问目的。测试中多处断言 loading 时aria-busy="true"同步设置("sets aria-busy synchronously while clickAction is pending"),且链接模式渲染的按钮同样暴露aria-busy。 - FR7:共享尺寸必须保持家族几何——
sm/md/lg映射到同一控件高度契约;纯图标成员在解析尺寸下为正方形;标签字重、按下状态、加载或图标替换不得改变外部尺寸。 - FR8/FR9:高度属于绘制的表面——独立成员拥有自己的静息高度;连接组只拥有一份共享高度,成员绘制
none;按下/hover/focus/loading 等交互状态不得改变高度的属主或层级。 - FR10:公开属性、渲染的
data-*状态与文档化主题视觉属性必须描述实际绘制的值,wrapper 不得把被忽略的子值报告为有效输出。 - FR12:连接组与有间距组保持区分——ButtonGroup 移除成员间隙、共享外边缘、拥有一个高度并使用文档化的 roving-focus 键盘模型;当前 ToggleButtonGroup 保持独立子表面以间隙分隔。
已验证缺口:ToggleButton 的高度(elevation)采用是已批准的实现缺口——把已有可选elevation轴加回去即可恢复家族对等性,且不改变无 prop 时的渲染。其透明 ghost 表面是已知视觉问题:阴影可能提供浮动边界而非不透明填充或描边,任何后续填充/描边方案都需要单独视觉评审。
3.2 输入字段家族(family:input-fields)
记录位置:docs/families/input-fields.md。批准人cixzhang、imdreamrunner,批准于 2026-09-09。这是成员最多的家族(13 个成员),也是决策记录最丰富的一份。
意图:用户面对一套连贯的输入系统:状态显示、行为、外观与尺寸在成员间使用一致的处理与 API 契约,同时每个组件保留其编辑值特有的交互模型。
成员:TextInput、TextArea、NumberInput、DateInput、DateRangeInput、DateTimeInput、TimeInput、FileInput、Selector、MultiSelector、ComplexSelector、Typeahead、Tokenizer。协作者:Field 与 FieldStatus(共享字段外壳)、FormLayout(字段排布)、InputGroup(显式采纳其能力契约的分组成员)、InputClearButton、Spinner、Tooltip、BaseTypeahead。排除:CheckboxInput、RadioList、Switch、Slider 使用带标签的控件模型但没有内容/末端通道几何;PowerSearch 与 ChatComposer 是消费成员行为的更高级组合;BaseTypeahead 是没有字段表面的 combobox 引擎。
8 条跨组件不变式(FR1–FR8)的关键含义:
- FR1 — 行内尺寸在普通字段状态间保持稳定:占位符变成值、或出现 busy/status/clear 控件,都不得仅因此改变字段外部可用行内尺寸。
Selector是 DEC-1 批准的例外,可跟随其显示内容。 - FR2 — 渲染的末端控件拥有非重叠空间:文本、token、光标与选中内容不得绘制或接收指针事件于清除动作、Spinner、状态控件、展开按钮或组件自有末端内容之下。这是可观察需求,不是对测量或共享通道原语的强制。
- FR3 — InputGroup 接纳要求完整的成组适配:成员只有在显式采纳成组模式并证明"从组解析兼容控件高度与尺寸、移除/抑制竞争的外层 Field/边框/圆角/表面几何、连贯地委托连接外边框/圆角/组级焦点呈现、通过组件自有截断/折叠/裁剪/溢出保持单行组几何、保留可访问名称与描述/值语义/键盘行为/焦点行为/编辑或选择模型"后才可参与。这是能力契约而非永久白名单——家族成员身份或上下文消费本身不足够。
- FR4 — 禁用原因保持可达:暴露
disabledMessage的成员,其非活动字段必须保持足够可聚焦以暴露原因,同时编辑/选择/激活仍被阻止。 - FR5 — 输入加载描述的是值,而非支撑数据:
isLoading表示字段值正在解析或保存,不得使独立提供的选项不可用或改变数据源 prop 的契约。 - FR6 — 过渡动作(changeAction)保持即时反馈:每条文档化的值变更路径先跑
onChange、乐观呈现提议的受控值、在 React transition 中运行changeAction、并贡献到同一 busy 呈现,直到受控值接受或替换它。 - FR8 — 状态放置跟随成员能力:每个输入必须提供
detached与tooltip;只有直接控件不透明、有边框、固定高度且其属主根能可靠反映用于重叠的解析尺寸时,成员才支持attached,且支持时attached为默认。直接 FieldStatus 仍只支持 attached/detached,Field 在渲染 FieldStatus 前消费tooltip。
5 条决策(DEC-1 ~ DEC-5)是这份契约的独特价值,记录于文档末尾:
- DEC-1(2026-08-30):独立 Selector 是行内尺寸例外,可按显示的占位符/选中值伸缩,但不解除 FormLayout、受支持的 InputGroup 或显式产品布局的约束;
- DEC-2(2026-08-30):
isLoading描述值解析或保存,不描述选项/支撑数据加载(对应 AST-001/DEC-1、DEC-2 应用于 Selector/MultiSelector:提供的选项保持可用、零选项是无选择状态、初始选项源挂起必须显式); - DEC-3(2026-08-30):
changeAction及其乐观/挂起行为属于输入家族契约; - DEC-4(2026-08-31):attached 是条件能力而非通用几何,并明确拒绝了"从后代
data-size推导通用 attached 重叠"的方案; - DEC-5(2026-09-09):InputGroup 接纳是能力制——拒绝永久硬编码白名单、Tokenzier 专属例外、无完整成组适配的上下文消费,以及强制独立多行/多 token 输入进入单行呈现。Tokenizer 在该规则下被接纳,成组默认使用组件自有单行溢出处理。
3.3 布局原语家族(family:layout-primitives)
记录位置:docs/families/layout-primitives.md。批准于 2026-08-30。
意图:构建者应当能用同一套小词汇表在一维或二维中排布任意内容、居中它、调整其布局框尺寸并表达空间关系。选择 Stack、Grid 或 Center 改变的是排布模型,而不是创造第二套间距刻度或共享 prop 名的新含义。
成员:Stack(及其HStack/VStack便捷形式)、StackItem、Grid、GridSpan、Center。成员资格遵循公开职责而非实现机制——仅仅因为源码用了 flexbox 或 grid 并不足以加入。
共享属主:SpacingStep拥有成员 gap/padding prop 使用的公开数字间距词汇;SizeValue拥有"数字即像素、字符串即 CSS 值"的盒子尺寸契约;architecture:container-padding拥有 bleed 几何——局部 padding 本身并不发布该协议。
规范概念:间距步长取值为0, 0.5, 1, 1.5, 2, 3, 4, 5, 6, 8, 10;盒子尺寸为数字或 CSS 值字符串;流动方向默认垂直;gap 是"排列项之间的空间,而非容器内边距"。
9 条不变式(FR1–FR9)中值得一提的:
- FR3 — Padding 优先级按边:显式边值 > 轴值 > 统一
padding,且覆盖只改该边; - FR4 — Gap 与 padding 保持区分:gap 分隔排列项,padding 在成员自身盒内嵌入内容;Grid 的
rowGap/columnGap只在其轴向上覆盖统一 gap; - FR6 — 修饰组件要求其父模型:StackItem 控制 Stack 中的参与;GridSpan 控制 Grid 中的参与;组件契约拥有在预期父级之外的行为;
- FR8 — 局部 padding 不是 bleed 信号:Stack 与 Center 当前应用 padding 而不发布容器内边几何,后代只有在
architecture:container-padding命名的发布者下才能依赖 bleed 补偿。
验证现状(诚实的缺口声明):当前测试是组件本地的,且多处只断言渲染成功或类变化,并未证明跨成员的计算 gap、对齐、逻辑方向或对等性。验证映射逐条列出"缺失证据"列:如 FR1/FR3 缺"跨成员或双向书写方向的计算值浏览器矩阵"、FR2 缺"LTR 与 RTL 下的 padding 阶梯渲染测试"、FR4 缺"与 Stack 的步长对比"。这正是家族契约体系的诚实性体现——已命名的验证缺口不会被宣称已修复。
3.4 布局区域家族(family:layout-regions)
记录位置:docs/families/layout-regions.md。批准于 2026-08-31。
意图:页面或有界工作区在添加产品内容之前就应有可预测的结构区域。构建者可以使用通用 Section、五槽位 Layout 原语或语境化 Toolbar,而不必让每个表面各自发明内边、边界、区域方向或内容所有权。AppShell 拥有组合这些低层区域与应用导航的页面外壳。
成员:Section;Layout及其Header、Content、Footer、Panel区域;Toolbar。
12 条不变式(FR1–FR12)的要点:
- FR1 — 区域拥有结构而非产品含义:成员建立空间边界并渲染调用方内容,不采纳内容的语义/状态/组件契约;
- FR3 — Layout 依据槽位在场选择几何:Header/Content/Footer/Panel 在接触 Layout 边界处应用外内边,在接触另一区域处应用内内边;省略的槽位不留下区域 wrapper;
- FR5 — 边界所有权在组合允许双属主时由调用方选择:可调整大小的面板组合在相邻 ResizeHandle 拥有分隔线时必须设置
LayoutPanel hasDivider={false}; - FR8 — 地标语义保持显式:命名视觉位置不会自动分配
banner/main/navigation/complementary/contentinfo,调用方必须提供受支持的角色与标签; - FR10 — 调整大小所有权保持委托:带
resizable的 LayoutPanel 使用 Hook 提供的当前尺寸而非widthprop; - FR12 — contentWidth 把滚动条保持在开放内容边缘:无面板时 LayoutContent 横跨可用中部区域并通过上下文感知行内内边对齐直接子级;恰有一个面板时该面板保持对齐居中框而内容延伸过对面开放侧;双侧面板或百分比/固有宽度/裸变量时整体保持约束,
calc(var(...))是显式的带长度值变量路径(对应 DEC-3)。
3 条决策:DEC-1 划定结构区域与组合原语分属两个家族;DEC-2 确定 AppShell 拥有页面外壳而 Layout 是通用五槽位原语;DEC-3 确定仅内容宽度对齐留在内容滚动口内。
源码佐证:Toolbar在 Toolbar.tsx 中把绘制表面、变体与选中分隔线边委托给 Section(注释与import {Section} from '../Section/Section'可见),而 Section.tsx 提供variant与dividers={['top','bottom','start','end']}的选中分隔线 API——与契约中"Toolbar delegates its painted surface, variant, and selected divider edges to Section"完全对应。
3.5 导航目标家族(family:navigation-destinations)
记录位置:docs/families/navigation-destinations.md。批准于 2026-08-31。
意图:用户应从每个接受或派生目标的 Astryx 组件获得相同的安全导航行为。组件的视觉角色、路由集成或放大的点击目标,不得决定一个被阻止的目标能否执行。
成员(开放清单):Avatar、BreadcrumbItem、Button链接模式、Citation、ClickableCard、Item、Link、ListItem、Markdown链接、NavHeadingMenuItem、SideNavHeading/SideNavItem、导航模式Tab、Token链接模式、TopNav系列、TreeListItem。排除:Outline 的 Astryx 生成#id链接、AppShell 的固定 skip-to-content 片段、作为子级提供的任意链接、Markdown 插件输出、图片/媒体/资源 URL,以及仅组合成员而不接受/派生其目标的组件。
8 条不变式(FR1–FR8):
- FR1 — 每个调用方控制的导航目标在进入其 sink 之前先被决策:任何成员不得在共享规则运行前把目标传给自定义路由或命令式浏览器 API;
- FR2/FR3 — 备选渲染与备选激活保持规则:通过
LinkProvider/as替换原生锚点、_blank/Cmd/Ctrl 点击/中键点击/同标签分配/委托表面点击,产生相同的接受/阻止决策; - FR4 — 被阻止的 scheme 无法执行:经过浏览器兼容的 scheme 归一化后,
javascript:、vbscript:、data:text/html不得成为导航; - FR6 — 两个自定义路由 prop 都是 sink:提供的
href与显式to被独立检查,任一个都不能通过 prop 优先级或 rest-prop 顺序绕过规则; - FR7 — disabled 与 rejected 是两回事:拒绝目标阻止导航,但不发明禁用状态、标签或视觉处理。
共享属主与当前采纳缺口:useLinkComponent拥有目标向原生/自定义链接组件的交接(路由侧的href/to接缝);useClickableContainer拥有放大表面的命令式导航(同标签/新标签/修饰点击/中键点击路径);Markdown 拥有把不可信源解析为目标并在渲染边界保持共享导航策略。文档诚实记录了两处待关闭缺口:当前main上useLinkComponent自定义 provider/as路径与useClickableContainer命令式路径仍在转发原始href/to(被标记为采纳缺口而非已批准例外),接受实现为 #5524,落地后需把新测试加入verified_by。
源码佐证:useLinkComponent.ts 的解析顺序为"per-componentasprop >LinkProvidercontext > native<a>",且当解析为自定义组件时用createLinkWithTo包装,同时传递href与to={href}以兼容 React Router、TanStack Router 等to系路由——直接呼应 FR6"两个 prop 都是 sink、独立检查"。
3.6 浮层关闭家族(family:overlay-dismissal)
记录位置:docs/families/overlay-dismissal.md。批准于 2026-08-30。这是最典型的"单一共享栈"治理案例。
意图:用户关闭分层 UI 时,只应影响最相关的顶层表面。当逻辑深度或 DOM 包含建立了顺序时,嵌套表面不得在同一次 Escape 或平台关闭请求中关闭其宿主。
成员:Dialog、AlertDialog、Popover、DropdownMenu、DropdownMenuSubMenu、MoreMenu、Tooltip、HoverCard、Lightbox、MobileNav、BottomSheet、BottomSheetSwitcher、CommandPalette、ContextMenu、PowerSearchEditPopover、Lab Drawer,以及大量组件自有弹层(输入类:ChatComposerInput、ComplexSelector、DateInput、DateRangeInput、DateTimeInput、Selector、MultiSelector、PowerSearch、BaseTypeahead、Typeahead、Tokenizer;其他:BreadcrumbItem、SideNavHeading/Item、TabMenu、TopNav 系列、Table 过滤、Lab TourStep、Lab ChatEmojiPicker)。成员资格是开放的——每个新上线的符合规则的表面都必须加入共享栈。
共享属主:
useLayerDismissal:注册活动表面,并把浏览器发起的关闭请求适配到共享顶层检查;layerStack:拥有在场过滤、排序与唯一的 document 级 Escape 监听器;LayerDepthProvider:通过 React 树携带逻辑嵌套,供提供给后代的成员使用;useFocusTrap:活动且收到onEscape回调时加入同一栈;没有onEscape的焦点陷阱不是可关闭表面,不注册。
7 条不变式(FR1–FR7):
- FR1 — 每个可关闭的浮层表面都参与:成员在场时必须加入共享关闭栈,组件自有 Escape 监听器或注册表不满足此不变式;
- FR2 — 一次请求影响一个表面:未被认领的 Escape 被路由到恰好一个最顶层的已注册在场成员;该成员要么调用其关闭回调要么阻止请求,请求不会继续传递到其后成员;
- FR3 — 平台关闭请求使用同一顶层规则:只有是最顶层已注册在场成员时才关闭;指向低层成员的请求被拒绝;
- FR4 — 可用嵌套信号高于宿主放置:提供
LayerDepthProvider的成员,其 React 树深度把后代层排在其之前,即使两者在同一 commit 挂载;DOM 包含可解析等深嵌套;稳定注册顺序解析无关表面; - FR5 — 内容可先认领 Escape:共享监听器运行在冒泡阶段,当内容已处理事件时退让;
- FR6 — 文本组合不是层关闭:用于取消活动 IME 组合的 Escape 被消费而不关闭成员,组合进行中平台关闭请求被拒绝;
- FR7 — 注册不定义打开状态所有权:栈调用被选成员的关闭回调,是否立即关闭由成员组件契约与调用方拥有。
采纳缺口(诚实声明):BottomSheet、CommandPalette、ContextMenu、DropdownMenuSubMenu、PowerSearchEditPopover、Lab Drawer 当前仍为"仅本地(local only)",必须从组件自有监听器/注册表迁移到共享属主;Tooltip/HoverCard 是"共享属主 + DOM 在场报告"但不为后代层提供嵌套深度。
源码级印证:useLayerDismissal.ts 的 JSDoc 直白地写道:"The layer does NOT attach a key listener — the stack owns one listener and routes each Escape press to the top-most REGISTERED layer, so one press dismisses exactly one of them",并列出当前已注册家族与六个仍跑自有监听器的组件(BottomSheet、CommandPalette、ContextMenu、DropdownMenuSubMenu、PowerSearchEditPopover、labDrawer),与契约的采纳表逐字对应。layerStack.ts 中可看到document.addEventListener('keydown', dispatchLayerEscapeKeyDown)、compositionstart/compositionend捕获监听与event.defaultPrevented退让逻辑(对应 FR5、FR6),以及registerLayer/isTopmostLayer的导出。
四、家族契约与仓库其他知识记录的关系
家族契约不是孤立的文档,而是 Astryx 分层知识体系的一层。knowledge-contracts.md 给出完整系统模型:
- 组件契约描述一个组件承诺的聚合行为;
- 模块契约描述由单个组件拥有的独立可契约公共 Hook/插件/工具/子系统;
- 家族契约描述兄弟组件共享的行为(本文主题);
- 设计规范记录人工拥有的视觉与交互决策;
- 主题规范记录一个包级主题的意图、token/调色板映射、配对/状态、例外与测量收据;
- 系统规范记录跨组件/跨主题或改变架构的决策;
- 消费者文档解释 props、示例与用法;
- 审计记录持有当前证据与发现(运营存储为 wiki
component-scores.json)。
评审者从改动代码出发,按"最近当前组件/模块契约 → 相关家族或设计要求 → 仅在被引用时进入架构/系统决策 → 映射的测试与审计证据"的路径查证(见上文的链路图)。关键不变量 INV2"一个事实只有一个属主"意味着组件记录不得复制家族内容,家族记录也不得重复组件本地属性表;INV9 规定当前记录之间没有隐式优先级——更新的、更窄的、更本地的当前记录不会静默覆盖另一条当前记录,冲突时评审停止并在规范属主处解决。家族契约中大量出现的deciding_specs(如spec:AST-002/DEC-1负责公共 API 准入)正是"决策归属权"的显式链接。
五、如何验证一份家族契约
每份家族契约的 Verification map 都回答了三个问题:这条 FR 由什么证据验证?覆盖哪些代表成员与状态?什么变异会让它失败?以按钮家族为例:
| 契约 | 验证 | 代表成员与状态 | 失败预期 |
|---|---|---|---|
| FR1–FR3 | role/name、键盘、回调、禁用、禁用原因测试 | 文本 Button、IconButton、ToggleButton、链接模式、成员/组禁用 | 成员丢失名称、键盘路径或在禁用时调用 |
| FR4–FR6 | Action 顺序、挂起、乐观、去重、可中断测试 | Button fire-once Action;ToggleButton 快速按下/松开 Action | 尺寸或目的变化、Action 绕过回调取消、陈旧状态胜出 |
| FR7 | 单元加真实浏览器几何检查 | 全部尺寸;文本/纯图标;按下/未按下;加载 | 家族高度分歧、纯图标不再是正方形、状态改变外部尺寸 |
| FR8–FR10 | data 属性、主题元数据、计算阴影测试 | 独立 Button/IconButton/ToggleButton;连接与有间距组;每个高度层级 | 阴影落在错误的盒上、状态改变深度、公开/主题/渲染值不一致 |
| FR11–FR12 | 组语义、传播、DOM、键盘、渲染表面测试 | 连接 ButtonGroup;有间距 ToggleButtonGroup;水平/垂直;禁用成员 | 组缺名称、默认传播失败、间距/连接所有权混淆 |
这些测试锚点真实存在于packages/core/src下(Button/Button.test.tsx、IconButton/IconButton.test.tsx、ButtonGroup/ButtonGroup.test.tsx、ToggleButton/ToggleButton.test.tsx)。输入字段家族的验证则要求真实 Chromium 下的行内尺寸前后测量、内容盒/控件盒重叠矩阵(窄宽度 + LTR/RTL)、InputGroup 渲染尺寸/高度/表面焦点所有权检查等——许多条目明确标注"当前缺失证据",由 scripts/check-knowledge.mjs 与仓库 CI 门禁共同把守。
六、结语:从"复制规则"到"拥有规则"
Astryx 的家族契约体系解决的是组件库规模增长后的经典难题:兄弟组件共享的行为规则一旦散落各处,就会在一次次"局部修补"中漂移。家族契约用一套可验证的治理机制给出了答案——共享规则只写一次、由规范属主拥有、以 draft/current/archived 三态管理权威性、以 FR/AV 表格承载不变式与允许差异、以验证映射绑定测试证据、以内容边界防止越界复制。
对读者而言,阅读任何一份家族契约都遵循同一套心智模型:先看 Intent 与 Membership rule 确认"这一族覆盖什么、谁属于、谁被排除",再读 Canonical concepts 表格掌握共享词汇表,随后用 Cross-component invariants 判断"成员必须做什么",用 Allowed component variation 判断"成员可以怎么不同",最后用 Adoption and exceptions 与 Verification map 核实"现状缺口在哪里、证据是否到位"。这套方法不仅适用于阅读本文介绍的六大家族,也适用于 Astryx 后续新增的任何家族记录——它们都共享 family-contract.md 这一模板骨架。
【免费下载链接】astryxAn open source design system that's fully customizable and agent ready项目地址: https://gitcode.com/GitHub_Trending/as/astryx
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考