在实施 #3245(PR #3300,把 mobile_fullscreen 从 FormField 挪到字段元数据载体上)时顺带发现,不在 #3300 修 —— 那单的 scope 被限制在「flag 到不了自动生成字段的 TextAreaField」,且这里的修法要么动 packages/fields 源码、要么删一段公开文档承诺的行为,两者都需要单独裁决。
现象
ObjectForm 的长文本判定(packages/plugin-form/src/ObjectForm.tsx,#3300 之后的行号 1063-1065)对 5 种 type 盖 mobile_fullscreen:
const isTextarea = t === 'textarea' || t === 'field:textarea' ||
t === 'string-multiline' || t === 'field:markdown' || t === 'field:html';
全仓只有两个 reader:
| reader |
位置 |
覆盖的 type |
TextAreaField(读元数据 field.mobile_fullscreen) |
packages/fields/src/widgets/TextAreaField.tsx:63 |
field:textarea |
| form 渲染器内置分支(读 FormField props) |
packages/components/src/renderers/form/form.tsx:1893-1895 |
裸 textarea |
于是 5 个里有 3 个是盖了没人读:
field:markdown / field:html —— 两者都解析到 RichTextField(packages/fields/src/index.tsx:2127-2128,均 import('./widgets/RichTextField'))。RichTextField.tsx 里 grep -n "fullscreen\|mobile_" 零命中:它既不读元数据也不读 prop,全屏入口从来不渲染。
'string-multiline' —— 这个字符串在整个仓库里只出现在上面这一行(grep -rn "string-multiline" 排除 node_modules/dist 后仅 1 处命中,就是判定式自己)。没有任何生产者产出它,也不是注册表 key,是条死分支。
为什么这是缺陷而不是「将来再说」
ObjectFormSchema.mobile 的 JSDoc(packages/types/src/objectql.ts:1265)把 rich-text 写进了承诺里:
fullscreenLongText: true, // textarea/rich-text get an "expand" button
「rich-text 有 expand 按钮」这句话从写下起就没有实现过。这正是 ADR-0049 enforce-or-remove / AGENTS.md #0.1 针对的 declared-but-unenforced 形态:文档和类型注释各自声明了一个没有 enforcement 的行为,作者(包括 AI 生成的表单元数据)照着写、静默无效果、没有任何一处报错。#3245 的六步断链能藏这么久,也是同一个机制。
两条路(需裁决,不要照着猜)
A. 补齐 enforcement —— 让 RichTextField 也实现全屏入口(读同一个元数据 flag,与 TextAreaField 同一契约、同一 data-testid 约定),并给 string-multiline 一个真实生产者、或直接删掉这个分支。
- 长期正确性:文档承诺兑现,
mobile_fullscreen 的语义变成「所有长文本 widget 都实现的一个能力」,而不是「碰巧只有一个 widget 实现的一个键」。
- 代价:动
packages/fields 源码;富文本编辑器的全屏对话框不是 textarea 那种 <Textarea> 搬进 Dialog 就完事,draft/commit 语义要重新想(编辑器实例是否重建、未提交内容如何搬运)。
B. 收窄声明 —— 把判定式砍到真正有 reader 的两种(textarea / field:textarea),同步删掉 JSDoc 里的 "rich-text" 和死掉的 string-multiline。
- 长期正确性:declared = enforced 立刻成立,代价最小,没有任何行为回归(这三条今天本来就不产生效果)。
- 代价:公开文档的能力面缩小一格。如果后续真要做 rich-text 全屏,再走 A。
倾向 B 先落地、A 另开单:B 把「声明」拉回到「已实现」这条线上,是零风险的;A 是一个真实的 UX 特性,值得有自己的验收标准和浏览器验证,混在一起做会让「消除空承诺」这件事被富文本编辑器的复杂度拖住。但这取决于维护者是否认为 rich-text 全屏是近期要做的能力 —— 若是,直接走 A 更省一次往返。
补充:消费侧还有一个 as any 洞
TextAreaField.tsx:54 的 const textareaField = field as any; 让这个 widget 对元数据的每一次读取都不受类型检查(rows / maxLength / placeholder / label / mobile_fullscreen)。#3300 已经消掉了生产侧的 as FormField,并把 mobile_fullscreen 正式声明到 BaseFieldMetadata 上,但消费侧这一半没动 —— packages/fields 不在那单的 scope 内。彻底去掉它需要先决定 widget 如何收窄 FieldMetadata 联合(rows/max_length 只在长文本成员上)。记在这里,不必然与上面同单处理。
证据等级
静态证据(grep + 阅读四处源码),未做浏览器复现。RichTextField 无 reader 与 string-multiline 全仓仅一处命中都是可机械复核的事实;「用户看不到 expand 按钮」是由此推导的。
关联:#3245、PR #3300、#3232、#3233。
在实施 #3245(PR #3300,把
mobile_fullscreen从 FormField 挪到字段元数据载体上)时顺带发现,不在 #3300 修 —— 那单的 scope 被限制在「flag 到不了自动生成字段的 TextAreaField」,且这里的修法要么动packages/fields源码、要么删一段公开文档承诺的行为,两者都需要单独裁决。现象
ObjectForm的长文本判定(packages/plugin-form/src/ObjectForm.tsx,#3300 之后的行号 1063-1065)对 5 种 type 盖mobile_fullscreen:全仓只有两个 reader:
TextAreaField(读元数据field.mobile_fullscreen)packages/fields/src/widgets/TextAreaField.tsx:63field:textareapackages/components/src/renderers/form/form.tsx:1893-1895textarea于是 5 个里有 3 个是盖了没人读:
field:markdown/field:html—— 两者都解析到RichTextField(packages/fields/src/index.tsx:2127-2128,均import('./widgets/RichTextField'))。RichTextField.tsx里grep -n "fullscreen\|mobile_"零命中:它既不读元数据也不读 prop,全屏入口从来不渲染。'string-multiline'—— 这个字符串在整个仓库里只出现在上面这一行(grep -rn "string-multiline"排除node_modules/dist后仅 1 处命中,就是判定式自己)。没有任何生产者产出它,也不是注册表 key,是条死分支。为什么这是缺陷而不是「将来再说」
ObjectFormSchema.mobile的 JSDoc(packages/types/src/objectql.ts:1265)把 rich-text 写进了承诺里:「rich-text 有 expand 按钮」这句话从写下起就没有实现过。这正是 ADR-0049 enforce-or-remove / AGENTS.md #0.1 针对的 declared-but-unenforced 形态:文档和类型注释各自声明了一个没有 enforcement 的行为,作者(包括 AI 生成的表单元数据)照着写、静默无效果、没有任何一处报错。#3245 的六步断链能藏这么久,也是同一个机制。
两条路(需裁决,不要照着猜)
A. 补齐 enforcement —— 让
RichTextField也实现全屏入口(读同一个元数据 flag,与TextAreaField同一契约、同一data-testid约定),并给string-multiline一个真实生产者、或直接删掉这个分支。mobile_fullscreen的语义变成「所有长文本 widget 都实现的一个能力」,而不是「碰巧只有一个 widget 实现的一个键」。packages/fields源码;富文本编辑器的全屏对话框不是 textarea 那种<Textarea>搬进 Dialog 就完事,draft/commit 语义要重新想(编辑器实例是否重建、未提交内容如何搬运)。B. 收窄声明 —— 把判定式砍到真正有 reader 的两种(
textarea/field:textarea),同步删掉 JSDoc 里的 "rich-text" 和死掉的string-multiline。倾向 B 先落地、A 另开单:B 把「声明」拉回到「已实现」这条线上,是零风险的;A 是一个真实的 UX 特性,值得有自己的验收标准和浏览器验证,混在一起做会让「消除空承诺」这件事被富文本编辑器的复杂度拖住。但这取决于维护者是否认为 rich-text 全屏是近期要做的能力 —— 若是,直接走 A 更省一次往返。
补充:消费侧还有一个
as any洞TextAreaField.tsx:54的const textareaField = field as any;让这个 widget 对元数据的每一次读取都不受类型检查(rows/maxLength/placeholder/label/mobile_fullscreen)。#3300 已经消掉了生产侧的as FormField,并把mobile_fullscreen正式声明到BaseFieldMetadata上,但消费侧这一半没动 ——packages/fields不在那单的 scope 内。彻底去掉它需要先决定 widget 如何收窄FieldMetadata联合(rows/max_length只在长文本成员上)。记在这里,不必然与上面同单处理。证据等级
静态证据(grep + 阅读四处源码),未做浏览器复现。
RichTextField无 reader 与string-multiline全仓仅一处命中都是可机械复核的事实;「用户看不到 expand 按钮」是由此推导的。关联:#3245、PR #3300、#3232、#3233。