Repository navigation
Description
状态:✅ 已实现并浏览器端到端验证(2026-07-04)。 分支
feat/adaptive-layout-2578(framework + objectui),单测 / 类型全绿,待提 PR。下方「开发方案」为设计记录,保留。
✅ 开发效果(Delivered)
一句话
字段多的对象,新增 / 编辑 / 查看详情自动切换到整页窗口;字段少的对象保持抽屉 / 弹窗。全部由平台按字段数自动推断,AI 写元数据时零额外配置——不暴露 recordSurface 这类可写 key,所以 AI 无从写错。
Before → After
| 维度 | 开发前 | 开发后 |
|---|---|---|
| 字段多对象的记录详情 | 一律抽屉,几十个字段挤在侧栏 | 可授权字段数 ≥ 12 → 整页(有 URL、可后退);< 12 → 抽屉。移动端一律整页 |
录入表单多列(section.columns) |
type:'simple' 忽略 per-section 列数,分组内字段始终单列(#2515) |
按声明渲染多列,每个分组按自己的列密度在共享网格内排布 |
| 字段级宽度 | 只有绝对 colSpan (1–4),换表面(手机 / 弹窗 / 整页)就错位 |
语义 span(auto / full),与派生列数解耦,任何列数都不溢出;遗留 colSpan 渲染期 clamp,永不越界 |
| AI 写错字段引用 / 布局 | Zod 合法但静默失效,作者发现不了 | 编写期 warning + 修复提示(form-field-unknown / absolute-colspan-discouraged),AI 与人手写同一把尺(ADR-0019 / 0078) |
| 协议演进 | 倾向新增 recordSurface 开关 + 新 ADR |
不加 object key、不开新 ADR:字段数机器能推 → 派生;显式覆盖走 assigned page(ADR-0085 §2 准入测试) |
浏览器验证(真 console + app-showcase 后端,端到端跑通)
| 对象 | 可授权字段 | 行点击 → 结果 | 观测到的 URL / 形态 |
|---|---|---|---|
字段动物园 field_zoo |
57 | 整页 | /…/showcase_field_zoo/record/<id>,面包屑 + 两列详情,无遮罩 |
产品 product |
6 | 抽屉 | 停在列表 ?recordId=<id>,右侧抽屉浮层覆盖虚化的列表 |
录入表单同时验证多列:Email │ URL 两列、Phone │ Password 两列、Textarea 独占整行(宽控件 span:auto 自动整行)。
用户 / AI 视角
- AI 建一个几十字段的对象、不写任何表面或列配置 → 记录自动整页 + 表单自动多列 + 长文本自动整行。
- 想让某个小对象也强制整页 → 指派一个 record page(ADR-0085 认定的 per-page 正道),而不是加新 key。
交付与验证
- framework
f79263a19:deriveRecordSurface(spec/src/data/record-surface.ts)+FormFieldSchema.span+packages/lint/src/validate-form-layout.ts+ ADR-0085 §2 澄清 + changeset。spec 6677 测试、api-surface(regen)、lint 121 全绿。 - objectui
97726afa4+a56cb420f:autoLayout.resolveColSpan(span 感知 / clamp / per-section 密度)+ObjectFormgrouped 路径修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 +FormField.span透传 + app-shell 记录表面接入deriveRecordSurface。autoLayout 43 + plugin-form 173 测试、turbo type-check 29/29 全绿。 - 关键实现点:console 的记录表面在
packages/app-shell/src/views/ObjectView.tsx决定(原默认drawer),已在其detailNavigation默认接入deriveRecordSurface——底层 plugin-view 改动不够,必须在此层接入才在 console 生效。
已知边界 / 后续
- console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515 的「显式
section.columns」路径:现有 showcase 对象都不声明 per-section 列数,该分支由单测覆盖(per-section 密度 4 个用例),无浏览器截图。 - objectui 侧
deriveRecordSurface目前是本地镜像(objectui pin@objectstack/spec@^11.7,早于该导出);待 spec ≥ 11.10 发版后换成@objectstack/spec/data的 import(代码已留 TODO 注释,逻辑与阈值与 spec 一致)。
背景
字段特别多的对象(50+ 字段),来自 #2574 的两个诉求:
- 多列编辑/显示;
- 部分对象在新增/编辑/查看详情时自动切到全屏页面,而不是弹窗。
北极星约束:所有元数据都是 AI 写的,所以设计的核心目标不是"能配",而是让 AI 无从写错。下面每个决策都围绕这一点。
关键决策(为什么不走最直觉的做法)
1. 不新增 recordSurface 之类的对象级 key
按 ADR-0085 §2 的"未来 key 准入测试",呈现表面(modal/page)两条都不过:
- 判据是字段数,恰恰机器能推 → 机器能推的不该做成 AI 要填的 key;
- modal-vs-page 是纯重排呈现,不是数据的业务事实。ADR-0085 明确 reject 掉这类 per-surface 开关,并规定 per-page 控制走 assigned page。
→ 呈现表面 = 渲染器启发式(auto) + assigned page 覆盖,不是 authored key。
2. 不新写 ADR / 设计文档
方案完全落在 ADR-0085 已画的边界内(auto 派生 + assigned page),理由进 changeset + 代码注释即可(合"fewer ADRs"取向)。唯一可选动作:往 ADR-0085 §2 的 rejected-keys 清单补一行 "modal-vs-page presentation surface" 例子,防止有人(讨论中我本人就提过又撤回)再提 recordSurface。
3. 绝对 colSpan 在自动适应下 fragile,改语义 span
列数是派生的(手机 1 / modal 2 / 整页 3–4),绝对 colSpan: 3 只在脑补的那个宽度下对,换表面就溢出/错位。
→ 字段级排版原语改为与列数解耦的 span: 'auto' | 'full'(默认 auto)。
auto:渲染器按字段类型 × 派生列数自动排(标量按密度并排,textarea/richtext/子表等宽控件自动整行)——90% 情况 AI 一个都不用写。full:任何列数下独占整行。half:本期不做(三者里 AI-safety 最弱:奇数列要取整、很多成对关系渲染器能自己推、多一个可选错的旋钮)。等 dogfood 真的暴露"成对字段在宽页被拆散"再议。
4. 多列的地基已经有了
ADR-0085 的 fieldGroups + Field.group + collapse、FormSection.columns(1–4)、RecordDetails.columns 都已在 spec,并已被 validate-semantic-roles.ts 守住字段引用。缺口:表单视图内 sections[].fields 引用 + colSpan 没有对应交叉校验;renderer 侧 #2515(section.columns 没渲染成多列)。
贯穿全案的防呆原则(设计脊椎)
闭合枚举(编译期拒) > 智能默认(让 AI 不用写) > 单一派生器(不漂移) > 两级 lint(warning + hint) > schema.describe 即提示词。
依据:ADR-0078(no-silently-inert)、ADR-0019(lint 对 AI 和人手写同一把尺)、ADR-0085(object-level 语义角色 / 准入测试)。
开发计划
Step 1 — framework(一个 PR;纯函数 + 文档;可单测;无需浏览器;独立可合)
deriveRecordSurface(fieldCount, groups)—— 放packages/spec/src/data/,与deriveFieldGroupLayout并列。从字段数/分组单源派生 page/modal/drawer(移动端强制 page)。是派生,不是 authored key(ADR-0085 §5「one shared derivation, every surface」)。单测覆盖阈值边界、有/无 fieldGroups、mobile。FormFieldSchema.span: z.enum(['auto','full']).default('auto')—— 加在packages/spec/src/ui/view.zod.ts。保留colSpan兼容。.describe()写成 AI 引导语:"默认省略(按类型自动);整行用full;别用绝对列数,表面列数是派生的。"packages/lint/src/validate-form-layout.ts(照validate-semantic-roles.ts模子):form-field-unknown(warning):section 引用了对象上不存在的字段;absolute-colspan-discouraged(warning + hint):用了colSpan,引导改span:'full'(自动列数下 overflow 无良定义,故不做比大小)。
导出进 lint index。
- ADR-0085 §2 一行澄清:rejected-keys 清单显式加 "modal-vs-page presentation surface"。
- changeset(理由写这里,不新开 ADR)。
验收:单测绿;os validate 对种子坏对象吐出两条新 warning;spec 为 additive(不升 major)。
Step 2 — objectui + 浏览器 dogfood(一个 PR)
ObjectView/ObjectForm消费deriveRecordSurface();create/edit/detail 统一走派生表面,替掉硬编码的layout='drawer'默认。- 字段渲染:
span:'auto'→ 宽度按widget 类型 × 派生列数(宽控件自动整行);span:'full'→ 整行;遗留colSpan渲染期 clamp =min(colSpan, 当前列数)(降级不破版,ADR-0078)。 - 修 console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515(
section.columns未渲染成多列)作为本 PR 一部分。 - Dogfood 一个 ~60 字段对象(如 contract):AI 元数据里不写任何表面/colSpan 配置 → 自动全屏 + 多列分组 + textarea 自动整行;并验证 assigned page 覆盖能强制全屏。走"prove-it-runs"浏览器验证。
Non-goals / 已否决的替代方案(避免重复讨论)
- ❌
recordSurface(或任何 object 级 modal/page 开关)—— 被 ADR-0085 §2 准入测试否决。 - ❌ 新 ADR / 独立设计文档 —— 不必要,落在 ADR-0085 边界内。
- ❌
span:'half'—— 暂缓,等 dogfood 暴露真实成对断裂再议。 - ❌ 绝对
colSpan作为主要排版原语 —— 仅保留兼容,lint 引导迁移。
Refs
- 回应 有的对象字段特别多,如何配置多列编辑/显示,如何支持部分对象新增编辑查看详情时自动切换到全屏幕窗口而不是弹出窗口 #2574(原始问题)
- 归并 renderer bug console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515(Step 2)
- ADR-0085(语义角色 / 准入测试)、ADR-0078(no-silently-inert)、ADR-0019(lint 对 AI+人手写同一把尺)
Activity
os-zhuang commentedon Jul 4, 2026
设计细化(讨论后 · 2026-07-05)
讨论收敛出一条更硬的原则 + 几个具体改动。
原则:表现全部运行时派生,AI 零像素授权
AI 只写数据事实(字段 +
fieldGroups/highlightFields等语义角色)。surface / 宽度 / 列数一律运行时算——它们要么机器能推(字段数),要么只有客户端知道(视口)。AI 不知道客户端浏览器宽度,所以任何像素/宽度都不该进元数据。两个正交轴,都默认
autoauto/ drawer / modal / pageauto/ sm / md / lg / xl / fullspec 清理:像素宽度作废/弃用,统一档位
NavigationConfig.width(string | number 像素)、FormView.drawerWidth(像素)。渲染器保留读取作后备,但标记 deprecated、AI 不写。modalSize: sm/default/lg/xl/full对齐(加auto默认、default→md)。抽屉 / 弹窗 / 详情导航共用一套词汇。auto(默认,AI 零书写)= 运行时按字段数选档 +min(档位, 92vw)夹住视口。modalSize已证明粗档位既有用又不会写错)。列数:容器查询按实际宽度定,上限随字段数放大
@container+@md/@2xl/@4xl:grid-cols-N),看容器实际宽度、不看视口。inferColumns上限卡在 2。改为按字段数放大上限(→ 3 / 4),容器查询负责当前宽度实际显示几列。本轮开发
inferColumns上限按字段数放大(≤3→1 / ≤8→2 / ≤15→3 / ≥16→4);列数随容器实际宽度响应。NavigationConfig.size档位枚举(auto默认)+ 弃用像素width/drawerWidth。size+ 运行时按字段数派生宽度上限,min(档位, 92vw)。仍待定(下一轮设计,先不做)
feat(spec,lint): adaptive record surface + semantic field span (#2578)
feat(spec): navigation.size bucket replaces pixel width for overlays (#…
docs(changeset): note navigation.size (#2578)
feat(spec,lint): adaptive record surface + semantic span + navigation…
docs(skills): teach adaptive record presentation in objectstack-ui sk…
os-zhuang commentedon Jul 4, 2026
✅ Shipped & merged (all green)
deriveRecordSurface+FormField.span+NavigationConfig.size(deprecates pixelwidth/drawerWidth) +validateFormLayoutlint + ADR-0085 §2 clarification.columnsfix (console(11.5.0): 录入表单 type:'simple' 未按 spec 声明的 section.columns 渲染多列(分组内字段始终单列) #2515) +spanconsumption + responsive column cap + runtime overlay width.objectstack-uiauthoring skill: new "Record Presentation — surface / width / columns are auto-derived" section.Delivered effect (browser-verified): field-heavy records open full-page, light ones as a drawer — both derived from field count, zero AI-authored presentation. Forms render multi-column following the real rendered width (1→2→3→4 via container queries). Verified end-to-end:
field_zoo(57 fields) → full-page detail + 4-col form at 1680px / 2-col at 760px / 1-col at 430px;product(6 fields) → drawer.Deferred to a follow-up
The create/edit/subtable return-flow model (full-screen modal vs route; subtable-create as a modal over the parent detail) was discussed but intentionally not implemented here — adjacent to this issue's scope. Recommend a dedicated issue.
Closing — core scope delivered.
os-zhuang commentedon Jul 5, 2026
Return-flow follow-up filed: #2604 (record create/edit/subtable surface + return model).
feat(spec): deriveRecordFlowSurface — flow-aware record-surface deriv…
feat(spec): deriveRecordFlowSurface — flow-aware record-surface deriv…
test(spec): the last data/ file group's test titles state each cited …