Skip to content

fix(ui): a form view's layout horizontal puts labels beside fields, and the form page draws tabbed views as tabs - #27222

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ekt1xf
Oct 28, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/project-thread-ekt1xf

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Warren · project thread
Fixes #26843

Clause-②: no
Metadata-protocol: semantic

维护者速读

  • 改了什么:表单视图写 layout: 'horizontal' 时,字段标签在 sm 及以上宽度放到输入框左侧(手机上仍是上下排);分页表单不再把它改回 vertical;/forms/:name 与 /f/:slug 表单页把 type: 'tabbed' 画成每节一个页签。
  • 为什么改:这三项都能保存发布,但控制台画出来和没写一样,作者以为生效了其实没有(声明即生效)。
  • 风险与回滚:只改字段的排布 class 与表单页的分节渲染;vertical 与不写 layout 的表单输出逐字节不变(有测试钉住);回滚即 revert 本 PR。
  • 先看:ui/apps/console/src/components/FormPage.tsx(分页渲染与提交时跳到出错页签)、ui/packages/components/src/renderers/form/form.tsx(HORIZONTAL_FIELD_ITEM_CLASS)、ui/packages/plugin-form/src/TabbedForm.tsx。
  • 两分钟验证:pnpm --dir ui exec vitest run apps/console/src/components/FormPage.tabbedLayout-26843.test.tsx,预期 13 个测试全绿。
  • 需要你做的:无,席位落地。

What changes

  1. layout: 'horizontal' draws something. The shared form renderer (@object-ui/components form) used layout only to align its action row. A horizontal field item is now a two-column grid from sm up: a fixed label column (minmax(0,10rem)) and the control column, label on the control's first text baseline. Every child except the label (the control, its description, its message) is placed in the second column, so a label-less field and the help text line up under the control. Every class the arrangement adds is sm:-prefixed, so below sm the item is the vertical one. vertical and an absent layout take the old code path unchanged: same classes, same DOM. The label element, its for / id, and the control's aria-* attributes are not touched.
  2. TabbedForm passes layout through. It built its form node with layout: 'vertical' whatever the form view said; it now forwards schema.layout (declared on TabbedFormSchema as ObjectFormSchema['layout'], the key ObjectForm already hands over in its spread).
  3. The console form page draws type: 'tabbed'. FormPage (/forms/:name, /f/:slug) drew every section stacked. A tabbed form view with more than one section now draws one tab per section with the app's tabbed-form semantics: the tab key is the section's name, else its heading, else its position (TabbedForm's rule), defaultTab opens first, tabPosition places the strip (top / bottom / left / right), a section hidden by its visibleWhen draws no tab, and the panels are force-mounted and CSS-hidden when closed. A submit carries every section's fields from the page's one values map, closed tabs included; a submit refused for a missing or faulted field opens the tab that holds it. A one-section tabbed view draws stacked, as TabbedForm draws no strip for one tab.
  4. In-place fix, same defect class: the form page's rows honour layout: 'horizontal' too. FormPage renders its own rows (not the shared renderer) and never read layout, so after item 1 the form page would have been the one entry point still ignoring it. Its rows take the same arrangement (HORIZONTAL_ROW_CLASS). Same file as item 3, same gate families.

Docs: the layout row in ui/packages/plugin-form/README.md and the FormSchema block in ui/content/docs/components/form/form.mdx now say what each value draws. The object-form registration's input help already read "horizontal beside it" and is unchanged.

Changeset: .changeset/26843-form-layout-horizontal-tabbed-page.md, minor for @object-ui/components, @object-ui/plugin-form and @object-ui/console.

Why

The card's rule is declared equals effective (ADR-0049): layout: 'horizontal' and type: 'tabbed' validate, save and publish, and the console drew them exactly as if they were absent. PR #27027 made the keys reach every console form entry point; this PR makes them draw.

Human Minimum

  • 1. Schemas start as Zod; packages/spec holds no runtime logic. (packages/spec not touched.)
  • 2. Every name I added uses the casing for its kind.
  • 3. No formatter ran; pnpm lint passes (ESLint on the changed files: 0 errors; the repo-wide run is CI's).
  • 4. One issue, one branch off an up-to-date main, one pull request.
  • 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset.
  • 6. packages/spec touched: n/a.
  • 7. No hand edits to content/docs/references/, packages/*/CHANGELOG.md or content/docs/releases/.
  • 8. Title, body and comments are in English, except the 维护者速读 section, which the dispatch order requires in Chinese.
  • The Before Submitting list in CONTRIBUTING.md is done.

Tests

  • ui/packages/components/src/renderers/form/__tests__/form-layout-horizontal-26843.test.tsx (5): the grid classes and the label as first child on a horizontal item; every class horizontal adds is sm:-prefixed and none is removed (stacked below sm); a label-less field keeps its control in column two; the labels still name their controls (text, textarea, checkbox); CONTROL: absent layout renders byte-identical to vertical (ids normalised), and horizontal differs.
  • ui/packages/plugin-form/src/__tests__/tabbedFormLayout-26843.test.tsx (3): a tabbed ObjectForm with layout: 'horizontal' draws both tabs' fields horizontal; CONTROLS for no layout and vertical.
  • ui/apps/console/src/components/FormPage.tabbedLayout-26843.test.tsx (13): one tab per section named by its heading with every panel mounted; defaultTab; tab click; tabPosition: left gives a vertical strip; a submit carries title, notes and a never-opened tab's defaults value; a refused submit opens the tab of the missing required field; CONTROLS: one-section tabbed and type: 'simple' draw stacked with no tab strip; horizontal rows (classes, label first, label names the control), kept on every tab; CONTROL: no layout and vertical draw no grid row; the two tab-rule helpers.

Acceptance notes

  • The horizontal arrangement keys on the viewport sm breakpoint, as the dispatch reading asked, not on a container query: a form with no container ancestor would never engage a container variant. Inside a narrow dialog at a wide viewport the label column is still 10rem.
  • SplitForm and WizardForm (plugin-form) also hard-code layout: 'vertical' on the form node they build, so a split or wizard form view drops horizontal the same way TabbedForm did. Outside this card's file surface; reported to the seat.
  • On the form page, type values wizard, split, drawer and modal still draw stacked sections (unchanged; outside this card, reported to the seat).
  • A tabbed form-page section with neither name nor heading labels its tab with its position number (the app's TabbedForm writes an English Tab N); no catalogue key exists for either.

Verification

All readings on head 763ea858 (branch tip after merging origin/main) unless noted.

New tests and wider suites
  • New files: 3 files, 21 tests, all pass (5 + 3 + 13).
  • pnpm --dir ui exec vitest run --maxWorkers=2 packages/components/src/renderers/form/ packages/plugin-form/ apps/console/src/components/ under the verify lock: Test Files 333 passed (333), Tests 3538 passed, 18 skipped; VERDICT command-exit 0.
  • Type check: pnpm --filter @object-ui/components run type-check, @object-ui/plugin-form, @object-ui/console each exit 0 (console after its closure build --filter '@object-ui/console^...', VERDICT command-exit 0).
  • ESLint on the six changed source and test files: 0 errors (warnings are pre-existing any / exhaustive-deps lines).
  • pnpm --dir ui run check:new-line-citations: 0 new citations.
Ablations (each restored, blob equal to HEAD, git diff HEAD empty)

Run with node scripts/ablation-replace.mjs on the pre-merge head 51be1b76; each anchor hit once and the mutation was verified on disk. The three test files import their subject by relative path, so no dist leg.

  • form renderer: ? cn(colSpanClass, HORIZONTAL_FIELD_ITEM_CLASS) replaced by ? colSpanClass || undefined turns 3 tests red (grid classes, sm-prefix, label-less field).
  • TabbedForm: layout: schema.layout ?? 'vertical', replaced by layout: 'vertical', turns 1 test red (every field on every tab horizontal).
  • FormPage tabs: tabbedView forced false turns 5 tests red (strip, defaultTab, tab click, tabPosition, refused submit opens the tab). The submit-carries-every-field test stays green on stacked sections, as a guard should.
  • FormPage rows: horizontalRows forced false turns 2 tests red.
Geometry in Chromium

The item and row classes compiled with the repository's Tailwind 4 (compile from tailwindcss, which emitted the not(label) child rule) and drawn in the preinstalled Chromium. At 1024px: label x 16 width 160, input x 192, label vertically inside the input's box, description under the input at x 192. At 375px: label above the input, both at x 16. One-off measurement, not committed.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack derives 34 families on this change set (same list before and after the merge). Each ran on 763ea858 with its exit code captured before any pipe: 34 of 34 exit 0. dispatch-gates --ran: 34 derived, 34 run, 0 NOT-MEASURED. node scripts/check-sdui-manifest.mjs is among them (exit 0). On the pre-merge head the ui/scripts vitest family first exited 1 on two tree-walking tests that timed out at 15 s while a 20-minute locked suite shared the box; rerun under the lock it passed (175 files), and it passed again on 763ea858.

🤖 Generated with Claude Code

https://claude.ai/code/session_01JQwxbXNVYZR3yMKLPJJvzL

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · seat domain:ui#5 · session_01JQwxbXGpVJHZTXXerMoY4C · head 763ea858 · 2026-10-28T11:22Z

席位意见:表单视图写 layout: 'horizontal' 现在真的把标签放到字段左侧(sm 及以上;手机仍上下排),分页表单不再把它改回 vertical,控制台表单页 /forms/:name、/f/:slug 把 type: 'tabbed' 画成页签。#26843 剩余的三项全部落地,抽屉键与 buttons.reset 已在规格卡 #27153。

  1. Shape: Fixes #26843; 9 files, +779/−93: form.tsx +31/−2 (one class on <FormItem> under horizontal; vertical and absent layout keep their DOM), TabbedForm.tsx +13/−2 (schema.layout ?? 'vertical'), FormPage.tsx (section body extracted unchanged into renderSectionBody, plus renderTabbedSections, sectionTabKey, resolveActiveTab), README / form.mdx rows, 3 test files (21 tests), changeset.
  2. Premise measured: the order's three mechanisms held. The seat read the diff: the horizontal item is a sm: grid with every non-<label> child in column 2, so the label's for / id and the control's aria-* are unchanged and a phone draws as vertical. Tabs reuse the components Tabs primitive with panels force-mounted and CSS-hidden, so closed tabs keep values and upload state and one values map submits every section. The tab key is name ?? label ?? position, the same answer TabbedForm gives, so one defaultTab opens the same tab on both. A refused submit opens the tab holding the first refused field (field identity, faultedVisibleWhen before missing). A predicate-hidden section draws no tab, and a pick on a hidden tab falls through to defaultTab, then to the first tab.
  3. Gates / check-runs: PR check-runs on 763ea858: 43 success, 11 skipped (path-filtered), 0 failure; Lint & Repo Gates, TypeScript Type Check, Type Check UI and Test UI success; mergeable_state clean. Dev: 34 derived, 34 exit 0 on 763ea858; 333 files / 3538 tests green; type-check ×3 exit 0; 4 of 4 ablations red (the FormItem class, the TabbedForm passthrough, tabbed FormPage, FormPage horizontal rows), restored; Chromium geometry checked.
  4. Accepted deviations: changeset minor, not the order's patch (AGENTS.md: a Metadata-protocol kind other than none takes at least minor; same as PR fix(console): every console form entry point renders the form view's buttons, layout and defaults, and a tabbed form with subforms opens on its defaultTab #27027). FormPage horizontal rows were added in place (same defect class, same claimed file). README and form.mdx rows were updated per ui/AGENTS.md's docs rule (outside the file list, docs only).
  5. Out of scope: WizardForm hard-codes vertical, and FormPage draws a type: 'wizard' view stacked: filed as console forms: a type: 'wizard' form view drops layout: 'horizontal' (WizardForm hard-codes vertical) and the form page draws it as stacked sections #27224. SplitForm's same hard-code is left to its retirement in ui(plugin-form): ObjectForm drops the split / drawer / modal branches and SplitForm retires; showcase task views re-spelled (ADR-0050 cutover, renderer half of #27155) #27159 (ADR-0050). An unnamed, unlabelled section on a tabbed FormPage is named by position; noted, not filed. Skills-docs: README + form.mdx layout rows updated.
  6. Landing: not Tier H (no governed path); ready for landing (ready + auto-merge).

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 28, 2026 11:23
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 28, 2026 11:23
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 28, 2026
Merged via the queue into main with commit 2d05cd4 Oct 28, 2026
55 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/project-thread-ekt1xf branch October 28, 2026 11:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tooling

Projects

None yet

2 participants