Skip to content

fix(plugin-form): each form section draws its own columns count per row (objectui#12548) - #12556

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12548-section-columns
Oct 15, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12548-section-columns

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12548
Clause-②: no — no type, prop, export or spec declaration changes; sectionedGridLayout / TWELVE_TRACK_GRID_COLS live in autoLayout.ts and are not re-exported from the package entry.

What changed

A sectioned form draws all its sections in ONE field grid, sized by the widest section, and each field spanned Math.round(grid / sectionColumns) cells of it. When a section's count did not fit that grid, the section drew the wrong number of fields per row. Measured on main at 9c2785b, ModalForm, 2 / 3 / 1 form:

field grid cell
columns: 2 field grid-cols-1 @md:grid-cols-2 @2xl:grid-cols-3 @md:col-span-2 → one per row at every tier
columns: 3 field same (none) → 2 / 3 per row (correct)
columns: 1 field, span: 'full' same @md:col-span-2 @2xl:col-span-3 (correct)
columns: 3 beside columns: 4 … @4xl:grid-cols-4 (none) → four per row at @4xl

ObjectForm, SplitForm, TabbedForm and WizardForm (under a form-level columns: 3) drew the same classes.

One shared rule, sectionedGridLayout(formColumns, sectionColumns[]) in packages/plugin-form/src/autoLayout.ts, now called by all five hosts in place of the per-host applyAutoColSpan + containerGridColsFor pair:

  • Every section is 1 or the grid width: the rule returns the old CONTAINER_GRID_COLS grid and the old applyAutoColSpan spans. Single-density forms, and forms with no section columns, are therefore byte-identical.
  • Otherwise: the grid is grid gap-4 grid-cols-1 @md:grid-cols-12, and each field's colSpan counts twelfths of the row:
    • span: 'full' and wide widgets take 12;
    • an authored colSpan counts that many of its own section's columns, clamped to them;
    • any other field takes 12 / the section's columns.
    • A section's count is still clamped to the form grid, so a form-level columns still outranks it.

@object-ui/components form renderer (where the span classes are drawn): on a container carrying @md:grid-cols-12, a twelfths colSpan maps through a literal table, TWELVE_TRACK_SPAN_CLASS. The table keeps each count's existing step ladder:

section columns twelfths classes per row at @md / @2xl / @4xl
1 12 @md:col-span-12 1 / 1 / 1
2 6 @md:col-span-6 2 / 2 / 2
3 4 @md:col-span-6 @2xl:col-span-4 2 / 3 / 3
4 3 @md:col-span-6 @2xl:col-span-4 @4xl:col-span-3 2 / 3 / 4
authored 2 of 3 8 @md:col-span-12 @2xl:col-span-8
authored 3 of 4 9 @md:col-span-12 @4xl:col-span-9

Below @md the form stays one column. Every other container is rendered by the old code path, unchanged. The literal '@md:grid-cols-12' sits in components' source, so the published style.css carries the utility too.

Verification record (at 88bfef0)

  • New pins, plugin-form/src/__tests__/mixedSectionColumns-12548.test.tsx (25 tests):
    • per host (modal / object / split / tabbed, plus wizard under a form-level columns: 3), a 2 / 3 / 1 form reads 2 / 3 / 1 per row at @4xl and 2 / 2 / 1 at @md, and the 3-column section reads 3 at @2xl;
    • span: 'full' takes the row; base grid-cols-1; 3-beside-4 reads three per row;
    • CONTROL: all columns: 2 and all columns: 3, per host, keep the literal grid and span classes main drew;
    • a drift guard: each count's ladder on the 12-track grid equals min(columns, tier) against CONTAINER_GRID_COLS[4]'s tiers;
    • pure-rule pins.
  • Renderer pins, components/src/renderers/form/__tests__/form-twelve-track-span-12548.test.tsx (7), including a CONTROL that the 1–4 containers emit as before.
  • Before the fix, the new file ran 6 failed / 8 passed on main: the mixed pins red, the CONTROLs green.
  • Ablation 1: sectionedGridLayout forced to the old path. 12 mixed pins red, 13 green (all 8 CONTROLs among them). Restore was proven by blob hash and an empty git diff HEAD.
  • Ablation 2: the renderer's 12-track detection turned off. 16 red, 16 green, restored the same way.
  • pnpm --filter '@object-ui/plugin-form^...' build → 0
  • pnpm --filter @object-ui/plugin-form type-check → 0
  • pnpm --filter @object-ui/components type-check → 0
  • tsc over both new test files → 0 (the package type-checks exclude tests)
  • Whole packages/plugin-form suite: 184 files, 2269 passed, 1 skipped → 0
  • Every suite that renders a host or touches the census, plus components/src/renderers/form/__tests__/*: 225 files → 0
    • packages: 208 files, 3067 passed, 17 skipped
    • apps / examples / scripts: 17 files, 1465 passed
  • check:control-bytes, check:new-line-citations (0 new), check-changeset-presence, check-changeset-no-major, check:changeset-claims, check:pending-changeset-literals, check:test-path-roots, check:vi-mock-specifiers, check:vi-mock-inherit, check:vi-mock-override-shape, check:unreferenced-sources, eslint on the 9 changed files (0 errors) → all 0
  • Tailwind v4 compile of apps/console/src/index.css emits @md:grid-cols-12, @md:col-span-{6,12}, @2xl:col-span-{4,8} and @4xl:col-span-{3,9}; @md:col-span-8 and @2xl:col-span-12 are absent, as expected. Compiling packages/components/src/index.css also emits @md:grid-cols-12.
  • NOT MEASURED locally: check:readme-exports (needs every package's dist; no export changed) and performance-budget (a full console build). Both are left to CI.

What the landing seat's browser reading should show

HotCRM Leads → New. Pre-merge reading: the HotCRM-shaped DOM rendered by ModalForm, measured in Chromium with the console's Tailwind build:

dialog Contact Information (2) last_name (span: 'full') Lead Classification (3) Company (2) Additional (1)
1368px, before 1 per row, 875px 1320px 3 per row, 429px 1 per row, 875px 1320px
1368px, after 2 per row, 652px each 1320px 3 per row, 429px (unchanged) 2 per row, 652px 1320px
1024px, after 2 per row, 480px 976px 3 per row, 315px (unchanged) 2 per row, 480px 976px
640px, after 2 per row, 288px 592px 2 per row, 288px (unchanged ladder) 2 per row 592px
400px, after 1 per row 1 per row 1 per row 1 per row 1 per row

At 1440 the seat should see:

  • Contact Information at two fields per row, of one width;
  • last_name on its own full row;
  • Lead Classification at three per row;
  • nothing empty in the right third except a row's natural remainder.

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_01DN3fpUznSTqNMkk9nXd5BG

…ow (objectui#12548)

A sectioned form draws every section in one field grid sized by the widest
section, and each field spanned Math.round(grid / sectionColumns) cells. A
section whose count did not fit that grid drew the wrong number per row: a
columns: 2 section beside a columns: 3 one drew one field per row, and a
columns: 3 section beside a columns: 4 one drew four.

The five sectioned hosts now share one rule, sectionedGridLayout in
autoLayout.ts. A form whose sections are all 1 or the grid width keeps its
grid and spans byte for byte. Otherwise the grid is 12 tracks from @md and a
field spans twelfths of the row; the components form renderer draws a
twelfths colSpan with each count's existing step ladder (2 per row at @md,
3 at @2XL, 4 at @4XL).

Claude-Session: https://claude.ai/code/session_01DN3fpUznSTqNMkk9nXd5BG
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 69 chunks) 3090.7 KB 3119.4 KB
Main entry chunk (gzip) 73.3 KB 350 KB
Entry file index-ClDJJx-H.js —
Status PASS —

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 20.90KB 7.67KB
app-shell (runtime-config.js) 22.59KB 7.89KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.11KB 3.87KB
auth (ActiveOrganizationStorage.js) 27.95KB 10.04KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.22KB 10.61KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.40KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.72KB 2.24KB
auth (SocialSignInButtons.js) 9.70KB 3.93KB
auth (UserMenu.js) 3.39KB 1.21KB
auth (auth-gate-events.js) 1.62KB 0.83KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 41.19KB 11.12KB
auth (createAuthenticatedFetch.js) 8.54KB 3.46KB
auth (index.js) 3.63KB 1.64KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 11.08KB 4.58KB
collaboration (CommentThread.js) 27.26KB 8.03KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (commentAuthorship.js) 1.39KB 0.78KB
collaboration (index.js) 1.93KB 0.85KB
collaboration (useCollaborationTranslation.js) 6.50KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 596.60KB 143.79KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 254.55KB 71.19KB
fields (index.js) 283.09KB 72.13KB
i18n (LocalizationContext.js) 2.92KB 1.42KB
i18n (builtinAggregateLabels.js) 0.86KB 0.49KB
i18n (currency.js) 2.59KB 1.22KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 8.87KB 3.64KB
i18n (index.js) 5.52KB 2.39KB
i18n (pickLocalized.js) 9.86KB 3.95KB
i18n (provider.js) 39.35KB 12.88KB
i18n (translateFn.js) 0.20KB 0.18KB
i18n (useDisplayLocale.js) 3.52KB 1.76KB
i18n (useObjectLabel.js) 36.87KB 9.88KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 40.67KB 11.85KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.99KB 0.87KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 6.62KB 2.45KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useSpecGesture.js) 5.52KB 2.10KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 15.35KB 5.51KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.82KB 2.38KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 8.33KB 3.07KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 16.04KB 3.92KB
plugin-calendar (index.js) 54.94KB 16.05KB
plugin-charts (index.js) 84.97KB 23.36KB
plugin-chatbot (index.js) 201.72KB 48.07KB
plugin-dashboard (index.js) 143.88KB 38.88KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 262.87KB 69.48KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.25KB 46.78KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 262.33KB 72.76KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 127.76KB 32.64KB
plugin-map (index.js) 27.07KB 8.96KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 46.66KB 13.13KB
plugin-timeline (index.js) 39.21KB 11.85KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 93.05KB 23.86KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 12.07KB 3.68KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 120.63KB 39.56KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 4.50KB 2.06KB
react (schema-input.js) 4.31KB 2.07KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (body-dialect.js) 4.50KB 1.99KB
sdui-parser (codegen.js) 9.45KB 3.76KB
sdui-parser (dashboard-widget-options.js) 3.07KB 1.30KB
sdui-parser (index.js) 7.30KB 3.12KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 25.28KB 7.81KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.29KB 0.24KB
sdui-parser (validate.js) 23.87KB 7.83KB
types (ai.js) 4.39KB 2.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 4.12KB 1.61KB
types (authoring-nodes.js) 0.20KB 0.19KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (cloud.js) 0.20KB 0.18KB
types (complex.js) 4.44KB 2.07KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (dashboard-widget-layout.js) 2.06KB 0.96KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (expression.js) 0.20KB 0.18KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 1.13KB 0.65KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 5.78KB 2.70KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 5.00KB 2.39KB
types (navigation.js) 0.20KB 0.18KB
types (node-slots.js) 7.18KB 2.34KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 2.52KB 1.31KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-approval-decision.js) 0.20KB 0.20KB
types (record-components.js) 0.20KB 0.19KB
types (record-panels.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (select-option.js) 0.20KB 0.19KB
types (spec-report.js) 4.99KB 1.96KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (strict-authoring-face.js) 19.93KB 7.26KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.48KB 3.50KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Seat ACCEPT: objectui PR #12556 at 88bfef068f · domain:ui seat 1 · session_01JZnppgHRp4sREcouhkQYDB · 2026-10-15T18:19Z

Verdict: ACCEPT. Reviewed against the card objectui#12548, the claim 6197286546, and the cloud session's report, which the seat posted verbatim at 6198542884. The seat opened this PR on the session's behalf, with the dev's own title and body.

No contract-review face applies:

  • Clause-②: no on line 2;

  • sectionedGridLayout and TWELVE_TRACK_GRID_COLS live in autoLayout.ts and are not re-exported from the package entry;

  • the components form renderer gains an internal span table keyed on the existing fieldContainerClass, with no new prop;

  • no published schema and no governed path.

  • Shape: draft; base main; assignee sunhaolin. Line 1 is Fixes #12548. 10 files, +620/−27. Changesets: @object-ui/plugin-form and @object-ui/components, both patch. Mergeable clean.

    • One rule, sectionedGridLayout, serves ModalForm, ObjectForm, SplitForm, TabbedForm and WizardForm.
    • A form whose sections all fit one 1–4 grid keeps that grid and applyAutoColSpan, byte for byte.
    • Otherwise the grid is grid-cols-1 @md:grid-cols-12, and spans count twelfths through a literal per-tier table that keeps each density's step ladder.
  • CI on 88bfef068f: 34 success, 3 expected skips, 0 failures.

  • Browser reading. HotCRM 63db83e was served with its pinned @objectstack/cli 18.0.0-next.1. Readings were taken in Chromium against the PR head and base main 518a8e3084, by the seat's read-only helper, from each field's [data-field] cell rect.

    Claim Head Base Verdict
    A. Leads → New, 1440 Contact rows [2,1,2,2,2] at 627 / 627 px, last_name alone at 1270; Classification [3,3,1] at 412.7 Contact 1 per row at 841.3 (the card's reading); Classification [3,3,1] at 412.7 PASS
    A, the collapsed sections, on the full-page /crm_lead/new Company [2] at 440 / 440, Privacy [2], Additional [1,1], Address [1] (its one field is span: 'full') Company and Privacy 1 per row at 592 PASS
    B. 1024 (container 874.8, the @2xl tier) Contact 2 per row at 429.4; Classification [3,3,1] at 280.9 Contact 1 per row at 577.9 PASS
    B. 768 (container 631.6, the @md tier) Contact 2 per row; Classification [2,2,2,1], so a 3-column section reads 2 per row at @md as claimed Contact 1 per row PASS
    C. Phone sheet, 390 1 column, 358 px identical PASS
    D. CONTROL, crm_task (all columns: 2) grid @md:grid-cols-2, counts and widths identical to base, 0 px difference — PASS
    E. Lead → Edit (the same dialog, ?form=) identical to A Contact 1 per row PASS
  • The dev's open questions, answered by the seat. Each is an implementation choice inside the rulings:

    1. B: keep each density's step ladder through the literal table, which leaves the classification section's widths unchanged.
    2. A: in a mixed form, an authored colSpan counts its own section's columns, read against the per-section FormSectionSchema.columns. A single-density form keeps the old reading.
    3. A: one function, which returns the old grid when every section fits, so the byte-identical CONTROL holds.
  • Out of scope:

    • The components style.css scan note goes to Acceptance notes, with no carrier.
    • The helper found a defect on head and base alike: in the Leads create/edit dialog, clicking a collapsed section's header remounts the dialog and the section stays collapsed. It is filed as a finding (next comment on the card).
  • Serial: PR objectui#12546 (objectui#12521, held on a decision) edits ModalForm.tsx and autoLayout.ts in other regions. It merges main in its rework round.

Landing: ready and auto-merge now. Its merge closes objectui#12548. The seat archives the cloud session session_01DN3fpUznSTqNMkk9nXd5BG after the merge.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 15, 2026 18:20
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 15, 2026 18:21
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 15, 2026
Merged via the queue into main with commit 5f8ecf6 Oct 15, 2026
38 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12548-section-columns branch October 15, 2026 18:33
sunhaolin pushed a commit that referenced this pull request Oct 16, 2026
Resolves ModalForm.tsx against objectui#12548 (PR #12556, the per-section
12-track grid) and objectui#12559 (PR #12571, the dialog-owned Suspense
boundary). The sectioned dialog keeps this branch's grid width
(sectionedDialogColumns) and lays each section out through
sectionedGridLayout, so both landed behaviours stand.

Claude-Session: https://claude.ai/code/session_01JZnppgHRp4sREcouhkQYDB
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants