/** * shared/client/ui — 跨包客户端 UI 原语层(T1 五原语 + 共享类型面,单文件)。 * * 档位:§2.4 档 C(跨包共享,仓库根 shared/)。从 dsh-provider-usage 的端内面 * (src/client/shared/ui/)上提而来,生产消费方 ≥2(provider-usage / notifier), * 由 scripts/gate/verify-shared-fanin.mjs 按生产 src 口径实时派生,不手抄快照。 * * 职责:把「按钮 / 字段 / 分段器 / 状态点与徽标 / 容器」五种最常复用的呈现收成 * 一处定义;各包只剩 variant 与内容不同。文案走各包 locales.ts,色值走各自 * style.css,本层零文案零色值。 * * 【为什么是单文件(本层最容易被下一个人「好心拆开」的形状约束)】 * verify-shared-fanin.mjs 的枚举口径是 **shared/ 下每个 .js / .d.ts 文件**(本仓 * shared 是 TS 化 + 原地 emit,故枚举到的是 tsc 产物),每个文件聚成一个「模块」并 * 各自要扇入 ≥2 包。R2 实测:在 shared/client/ 下放两个 0 消费的探针 .ts,tsc emit * 之后门禁立刻报 * FAIL 值面 client/probe-alpha.js | 0 包(下限 2):(无) * FAIL 值面 client/probe-beta.js | 0 包(下限 2):(无) * 即 **shared/ 下任何 0 消费的 .ts 都会因 emit 成 .js 而成为独立模块并判红**。 * 拆成 button/field/segmented/… 六个文件时,两个包都只从门面 index 导入,五个实现 * 文件各自 0 消费者 → 直接判红。**单文件是门禁枚举粒度逼出来的唯一可行形状,不是 * 风格偏好**;代价是本文件偏长,故按原语分段并各段自带文件内注释。 * 将来若要拆文件,前置条件是先解决「每个文件都得有 ≥2 个包直接 import」这条约束 * (例如把门面也变成按原语分片的多个入口,且两个包都逐一 import 每个分片)。 * * 【React 为什么要值 import(本仓对 shared/client/** 的禁令与 JSX 运行时的冲突)】 * 仓库根 tsconfig.base.json 定的是 `"jsx": "react"`(经典运行时,JSX 编译成 * `React.createElement`),esbuild 侧同样是经典默认,故 JSX 文件作用域内必须有 * `React` 标识符。改 `jsx: "react-jsx"` 也不成立:产物构建走 esbuild 的默认经典 * 运行时,源码却按自动运行时写 → 运行时 React is not defined。 * 故本文件保留 `import * as React from "react"`。这与 §2.4「client 面禁 bare * 第三方包」的**立法意图不冲突**:该禁令针对的是会被静默内联进浏览器产物的普通第三方 * 包(dompurify 之类),而 React 在本仓是**宿主 loader 经 factory 注入的 external** * (build-client「React externals」路径,无全局 React、无内联副本),各包 .tsx 本来 * 就这么写。本文件是 shared/client 下**唯一**允许的 bare import,范围仅此一个符号。 * 其余一律禁止:禁第二个第三方包、禁任何 `node:*` 值引用。 * * 冻结契约(四层,各原语段内复述自己那层): * 1. 语义层冻结:role 与 aria-* 逐个列名,见各原语段注释。 * **永久禁用 `role="navigation"`**(宿主 `:not(:has([role=navigation]))` 规则, * 命中即整弹窗退回桌面 row 布局,手机内容区被压至约 106px);SegmentedControl * 同时禁用 `role="tablist"`(与 keep-mounted + hidden 显隐形态冲突,且会把按钮 * 语义换成标签页语义)。 * 2. 样式钩子冻结:类名 `dsu-*` 与 `data-dsu-*`(dsh-ui 前缀,**包无关**)。 * 消费包的领域类(provider-usage 的 `.dou-btn`、notifier 的 `.dn-*`)**不归本层**, * 由调用点经 className / itemClassName 传入——这正是它能跨包复用的前提。 * 3. DOM 结构与第三方 `data-*` 都不是契约:改层级不得改语义层。 * 4. props 面冻结:只冻结形状与默认值,不冻结内部实现。 * * 取舍总纲:语义增强只做「原来就有可访问名、只是没落到语义树上」的那一类 * (如 Field 的 label 包裹);绝不借重构加 role 或改读屏播报次数。 */ import * as React from "react"; /** 状态色调档:ok/warn/err 对齐宿主 --dsw-alias-state-*;off=无信号;neutral=中性提示。 */ export type Tone = "neutral" | "ok" | "warn" | "err" | "off"; /** 控件尺寸档(sm=紧凑行内,md=窗格主操作)。 */ export type Size = "sm" | "md"; /** 徽标尺寸档:sm=身份药丸(11px);xs=行内成败小徽标(10px)。 */ export type BadgeSize = "sm" | "xs"; /** Surface 形态档:pane=设置窗格外层;card=整页外壳大卡。 */ export type SurfaceVariant = "pane" | "card"; /** 分段器档位键的取值域:热力范围是 number(天),其余分段器是 string,故两者都收。 */ export type SegmentedValue = string | number; /** * SegmentedControl 形态档: * bar = 设置页顶部导航条(带下边框、横向滚动容器) * pill = 行内药丸筛选组 * plain = 朴素按钮组,项沿用消费包的按钮外观(类名由调用点给) * 三档而非两档:热力范围切换的项收敛前就是直角小钮,硬塞进 pill 档会把 999px * 圆角药丸强加到它身上,属视觉改版而非收敛。 */ export type SegmentedVariant = "bar" | "pill" | "plain"; /** 分段器单个选项。disabled 由调用点按数据态给出(如热力图当前档位即禁用档)。 */ export interface SegmentedOption { value: T; label: React.ReactNode; disabled?: boolean; } export interface ButtonProps extends React.ButtonHTMLAttributes { size?: Size; /** * 缺省 "button"。R3 修正:R2 交付时这里是 `Omit<..., "type">`——类型面宣称 type 不属于 * 契约,实现面却把 {...rest} 整体透传、写在后面的 type 会覆盖前面写死的 type="button", * 两面自相矛盾(且「传了 type 却可能不生效」比「不能传」更坏)。现把 type **显式纳回 * 契约**并在实现里显式取值:缺省 button,只有显式传 submit/reset 才覆盖。 * 收窄成三档联合而非原生 ButtonHTMLAttributes["type"]:menu/image 等取值对原生 *