管的是「全局与应用级」这一类 9 篇(
C12-00到C12-08)之间的路由:App、ConfigProvider、DarkModeToggle、FloatButton、Ripple、Portal、FocusTrap、SplitButton 各管哪一段,近义的怎么选,坑落在哪一篇的第几条。 不管:注入链本身的合取规则(A05)、样式引擎与注水(A04)、Token 四层(A03)、浮层时序与层级(A07、I02、I10)。这一类是「全库的地基出口」,机制大多在 A/I 层已经取证,下面只给编号。
这一类装着两种形状迥异的东西:容器型(App、ConfigProvider)与行为型(Portal、FocusTrap、Ripple、DarkModeToggle)。C12-00 论点 2 给了统一口径:本类组件一律是「composable + 组件壳」双出口,组件壳只做三件事且不产生自己的语义。这条口径的实际价值是:当你发现某个行为型组件「包一层就失灵」,失灵点几乎总在那三件事里(前缀、样式注册、attrs 落点),而不是在行为本身。
第二件事是同一诉求在本类普遍有两到三套实现,且选型依据不是功能。传送门有三套互不复用的实现,C12-00 论点 1 明确「选型依据不是功能而是调用方是否在 trigger 上下文里」;C12-06 论点 1 进一步给出后果——公开 XYPortal 与浮层基座完全不通,连 ConfigProvider 的全局容器都不继承,「统一挂载点」在库里是两套口径。点击反馈同理:wave 与 ripple 是同一诉求的两套实现(C12-00 论点 3),本类只提供了其中一套的组件形态。SSR 闸门也有三种不同写法,本类内部没有统一策略(论点 5),C12-06 论点 2 数出那条路只靠 isClient 常量加 mounted 门闸这一对,wrapSSR 与 <Teleport> 自带的 defer 都没用上。
第三件事是状态隔离度不一致,且粗的那一头是全局的。C12-00 论点 6:一个用闭包、一个用模块作用域。C12-05 论点 3 把后果写实——timeout 是模块级单变量,全库所有涟漪共享一个定时器;同一篇的 论点 2 则指出宿主类名有两个写入者,禁用态的正确性只靠「没有重渲染」。这类设计在单实例页面上看不出来,在同页多个宿主或 SSR 并发请求下才现形。
第四件事是应用级容器的响应性缺口。C12-01 论点 1:App 的 provide 传的是 .value 快照,两个 context 因此不具响应性;论点 4:脱离 <xy-app> 时不抛错,而是给一组空方法,并且这个行为被测试钉住了;论点 7:useMessage 在 holder 未挂载时返回永不 resolve 的假 thenable。这三条合起来解释了「静态方法在没包 App 的页面里静默不响」这个高频现象。ConfigProvider 侧的对应事实是 C12-02 论点 4:三层包裹里只有 DesignTokenProvider 是条件挂载,条件是 props.theme 而不是合并后的主题——这决定了静态 API 的主题为什么有时不生效。
第五件事是**「声明了但没人读」在本类成簇出现**:C12-02 论点 6 有四处已无人消费的公开声明,同时被三个文件反向 import 形成模块环(环能跑是因为所有回读都推迟进函数体);C12-06 论点 6 的 position 与 group 是「API 面存活、实现死亡」的一对,而且用法手册把它们写成了可用特性;C12-04 论点 6 的 zIndexPopup 被声明却没人读;C12-08 论点 6 的门面索引签名承诺透传额外属性、实现只读七个具名键。判读这一类时,「文档里有」不构成「源码里读」的证据。
| 要解决的问题 | 选它 | 而不是另一个 | 判据 |
|---|---|---|---|
让 message/notification/modal 的静态方法吃到主题与 locale | 包一层 App(C12-01) | 只挂 ConfigProvider | App 装配的三套 API 各有自己的 holder 与主题解析,脱离 App 拿到的是空方法而非异常(C12-01 论点 4、论点 7) |
| 组件内部要用带上下文的 message 实例 | App.useApp()(C12-01) | 直接调静态方法 | AppConfigContext 是「父层配置 → App 合并 → 再向下提供」的单向漏斗(C12-01 论点 2) |
把一段 DOM 挂到 body,与弹层无关 | 公开 XYPortal(C12-06) | 内部 _util/Portal.tsx | 三套传送门互不复用,公开那个不继承全局容器(C12-00 论点 1、C12-06 论点 1) |
| 浮层(tooltip、select、dropdown)的挂载与销毁 | 走 trigger 上下文那条容器编排链 | 手写 appendChild | 内部 Portal 的渲染闸门在「没有 provider」时默认放行,getContainer 的 required 契约靠上游默认值兜住(C12-06 论点 7) |
| 按钮点击要有水波纹 | Ripple(C12-05) | _util/wave | 同一诉求两套实现,本类提供的是 Ripple 的组件形态(C12-00 论点 3);宿主是行内元素时 overflow: hidden 不裁切 ink(C12-05 论点 5) |
| 站点级暗色开关 | DarkModeToggle(C12-03) | 自己加 html.dark 类 | 它改的是 data-theme 属性,既不是 html.dark 也不是 prefers-color-scheme(C12-03 论点 1);持久化与 FOUC 组件不提供,宿主必须提供(论点 7) |
| 把焦点圈在某个子树里 | useFocusTrap composable | 组件壳 XYFocusTrap | 组件壳的前缀走 config-provider,而哨兵类名与样式落地各走各路(C12-07 论点 6) |
| 一个右下角悬浮按钮,还要能回顶 | 包根 BackTop(C12-04) | FloatButton.BackTop | 两者是两个不同的组件,而 xy-back-top 这个标签只有一个位置、注册先到先得(C12-04 论点 5) |
| 主操作按钮加一个下拉箭头 | SplitButton(C12-08) | 两个按钮并排自拼 | 两条点击路径不串台靠的是组合层「解构即丢弃」,不是事件契约(C12-08 论点 1) |
| 全局换文案、换空状态渲染 | ConfigProvider(C12-02) | 逐组件传 prop | renderEmpty 的四级解析发生在调用期,代价是把 Empty 钉进每条组件的依赖链(C12-02 论点 2) |
C12-01 论点 4(空方法组);如果调用方还 await 了它,C12-01 论点 7:holder 未挂载时 useMessage 返回永不 resolve 的假 thenable。props.message 配置但已渲染的实例不跟着变 → C12-01 论点 1:provide 传的是 .value 快照,两个 context 不具响应性;同一文件里两种写法并存(函数版正确、setup 期常量版是快照),见 论点 8。C12-01 论点 5:.xy-app 这层容器真的会插进布局,attrs 走默认透传。C12-01 论点 3:holder 先渲染、default slot 后渲染,这个顺序是功能性的,改动时不能调换。ConfigProvider 的覆盖行为与直觉不符 → C12-02 论点 1:25 个 props 里只有 2 个带真默认值,而这 2 个恰好让父链回落恒不触发,嵌套覆盖实际是四类不是三类。ConfigProvider 实例并存时 RTL 类属性「最后挂载者胜」 → C12-02 论点 5:三条副作用出射边都没有卸载清理,且 direction 的真默认值让每次挂载都写一次 rtl: false。locale 在某个消费者那里读到的是旧值或另一种类型 → C12-02 论点 3:它是全模块唯一有四条出口的字段,四条出口的求值时机互不相同,其中一条走字符串注入而不是 InjectionKey。ConfigProvider 牵出一圈循环依赖 → C12-02 论点 6:四处公开声明已无人消费,同时被三个文件反向 import,环能跑只因为回读推迟进函数体;论点 2 的 renderEmpty 与 useConfigInject 也形成三模块环。data-theme → C12-03 论点 4(useDarkMode 用 getCurrentInstance() 分流);开关风格下 focus() 是空实现 → 论点 5;无障碍与系统偏好零测试覆盖 → 论点 6。<xy-float-button @click> 收不到事件 → C12-04 论点 1:onClick 被声明成 prop 后,handleClick 去 attrs 里找它,同时把 restProps 里那份覆盖掉。FloatButton.BackTop 出现了却点不动 → C12-04 论点 4:滚动侧是活的、点击侧是死的,三条支撑代码里一条恒不生效、一条是死字段。C12-04 论点 2;分组内子按钮自己的 shape 被静默忽略,而 dev 警告读的恰是这个被忽略的值 → 论点 3。C12-04 论点 6:分组吸附靠后代选择器的特异性,层级完全绕开 token,zIndexPopup 声明却没人读;编排口径归 I10。C12-04 分组侧的 emit('openChange') + callEvent(props.onOpenChange)(修前一次悬停 @open-change 实测 2 次)与 C12-03 的 setDark(emit('change') 之后又 props.onChange?.(dark),修前一次点击 2 次),2026-09-29 都改成只留 emit、实测各 1 次;helper 定义随之删除。计数回归 packages/xiaoye-ui/src/_util/__tests__/event-dispatch-count.test.tsx(23 条,判据一律 toHaveBeenCalledTimes(1)),三形态的全库口径见总导览「事件双投递」那条。C12-05 论点 3:timeout 是模块级单变量;两处滚动补偿既冗余又跨轴写反,只被标准模式下的零值掩盖 → 论点 4;ink 是「一次创建、长期复用」的节点 → 论点 1。<xy-portal> 上的 class 与 style 丢失 → C12-06 论点 5:inheritAttrs: false 配了一条不透传 attrs 的渲染路径。portal 的 position/group 配了没用 → C12-06 论点 6;portal/style/ 那个返回空对象的样式 hook 仍会为一条零命中的属性选择器付出注册成本 → 论点 4。C12-07 论点 2:回环的搜索范围是 el.parentElement,比陷阱本身大一层;本库有两份不等价的可聚焦判据、按侧选择器只落地一半 → 论点 3;apply 每次变更都会重跑并抢焦点,disabled 与 autoFocus 都不是「别动我焦点」的开关 → 论点 4。C12-08 论点 4:这条样式规则有三个各自足以致死的独立原因。C12-08 论点 5(门面只留六条通道)与 论点 6(索引签名承诺透传、实现只读七个具名键);顶层开关被翻译成四次下发与两次重算、短路下游两条兜底 → 论点 2;RTL 死分支在组合层被重新写了一遍 → 论点 3。C12-01 论点 1、论点 7 → C12-02 论点 4 → A05。改 provide 为函数式包装属于行为变更,两个 context 的响应性要一起看。C12-06 论点 1、论点 3、论点 7 → A07 → I02;appendTo 四态里 'self' 是唯一需要提前截走的值(C12-06 论点 3)。C12-00 论点 3 → C12-05 论点 1、论点 2、论点 6(三级开关链、默认 true)→ Button 侧的挂载判据在 C01-01 论点 5。C12-03 论点 1、论点 2(syncGlobalTheme 默认开启且真的灌给了 ConfigProvider)、论点 3(文档站用的是另一套机制,二者只差在谁持久化)→ A03。C12-07 全篇,其中「浮层家族没有接焦点陷阱、Modal 自己手写 Tab 记账」这条按分工归 A07,本类不重新论证;C12-07 的 §五 已用两种 grep 交叉复核过该断言并给出增量。C12-08 论点 5、论点 6 → 同族实例还有 C01-04 论点 6、C02-13 论点 2。C12-00 论点 5(三种闸门写法)→ C12-06 论点 2 → C12-05 论点 3(模块级量跨请求泄漏)→ A04。