Skip to content

fix(console): a long flow screen or action parameter list keeps Submit / Confirm on screen (objectui#12080) - #12088

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12080-screen-dialog-reach
Oct 10, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-12080-screen-dialog-reach

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12080
Clause-②: no

A flow screen dialog without an object form, and the action parameter dialog, had no height bound. A screen or a params list taller than the window overflowed both edges of the fixed overlay: the heading above the top, Submit or Confirm below the bottom. The overlay locks page scrolling, so a mouse wheel moved nothing. This PR bounds both dialogs and keeps their primary action on screen. A long screen or list also gets a wider, two-column dialog.

Implemented by the dispatched os-dev agent for the domain:ui#3 seat, session https://claude.ai/code/session_01B1gHb9baeX7oioD5sHVm7z, on the claim 6095670021. It follows triage's first grade 6093518840: reach first, width is the renderer's choice, and no new spec layout key.

What changed

Two files, the ones the claim named: FlowRunner.tsx and ActionParamDialog.tsx in packages/app-shell/src/views/.

  • Bound (both dialogs, every screen). DialogContent becomes a flex column at most 90vh tall. Only the field region between the header and the footer gets min-h-0 flex-1 overflow-y-auto, so it is the only part that gives up height and scrolls. The footer sits outside that region and stays on screen. The region runs to the dialog edges (-mx-6 px-6), so the scrollbar sits on the border and focus rings are not clipped.
  • Width (by length, no metadata key). A flat screen with more than eight declared fields gets sm:max-w-3xl, the width the object-form step already used, and the screen body becomes grid gap-4 sm:grid-cols-2. An action with more than eight shown params gets the same. Shorter screens and lists keep their width (sm:max-w-md and the default max-w-lg) and their single column. The screen counts declared fields, so a field that visibleWhen reveals mid-edit does not make the dialog jump width. The params dialog counts the params its visible gate shows, which do not change while the dialog is open.
  • The object-form step keeps sm:max-w-3xl and max-h-[90vh]. The scroll moved from the whole dialog to the body, so the step's heading and close button now stay in view. Its own Save bar is still inside the body, as before.
  • The failure and refusal notices in the screen dialog keep their place between the header and the body. They are outside the scrolling region now, so they stay in view however far the screen is scrolled. Two comments that described the old scroll-away behaviour were updated.
  • No prop, no export, no locale key, no spec key (Clause-② stays no). ScreenView is unchanged: the runner passes the grid classes through its existing className.

Why not a shared primitive. The unbounded default comes from DialogContent in @object-ui/components ui/dialog.tsx, the Shadcn-synced no-touch zone (AGENTS.md section 5, rule 7). A bounded wrapper under custom/ would be a new export of @object-ui/components, which would change Clause-②. It would also fix nothing until each consumer moved to it. So the fix stays in the two consumers named on the card. A parity pin makes sure their bounds cannot drift apart without a test going red.

Measured in Chromium: a real mouse wheel and a real click

Harness: a scratch entry served by the console's own Vite config, aliases and index.css. It mounts the real FlowRunner (13 flat fields, in the card's intake order) and the real ActionParamDialog (22 params). The harness was never committed. Chromium 1194 (/opt/pw-browsers/chromium), reduced motion. Steps for each case:

  1. Read the geometry.
  2. Move the mouse to the middle of the dialog and send ten mouse.wheel ticks of 300px.
  3. Read the geometry again, then hit-test the primary action with elementFromPoint at its centre.
  4. mouse.click that point and read the result. This is the toast for missing required fields, or the dialog's required errors.

Final run at ca44673.

1440×900 dialog (w×h, top) primary action, y in viewport / hit-test wheel moved the body real click reached it
BASE 023f00d, 13-field screen 448×1218, top −159 (overflow-y visible, no scroll container) 994–1034 no / no nothing moves not reachable
BASE 023f00d, 22 params 512×1868, top −484 1319–1359 no / no nothing moves not reachable
this PR, 13-field screen 768×762, top 69, 2 columns, body 600/600 766–806 yes / yes (fits, no scroll needed) toast: required fields listed
this PR, 22 params 768×810, top 45, 2 columns, body 644/932 790–830 yes / yes scrollTop 0 → 288 3 required errors shown
control: 3-field screen, BASE and this PR 448×402, top 249, both 586–626, both yes / yes — toast
control: 2 params, BASE and this PR 512×338, top 281, both 554–594, both yes / yes — 1 required error

Other windows, this PR only:

  • 1366×768, 13-field screen: 768×691, body 529/600, wheel moved it 71px, Submit at 665–705 was hit-tested and clicked.
  • 1366×768, 22 params: body 525/932, wheel moved it 407px, Confirm was clicked.
  • 390×844, both dialogs: one column, 390×760. The wheel moved the 13-field body 498px and the 22-param body 1164px. The primary action at 697–737 was clicked.
  • Threshold, 1440×900: 8 fields stay narrow at 448×778 and fit without scrolling (body 616/616). 9 fields go wide at 768×562 in two columns.

Unit pins (the class and structure contract that measurement rests on)

The test DOM has no layout, so these files pin the contract, not the pixels:

  • packages/app-shell/src/views/__tests__/FlowRunner.dialogReach-12080.test.tsx
  • packages/app-shell/src/views/ActionParamDialog.dialogReach-12080.test.tsx

They check five things:

  • The bound tokens are on DialogContent, and the dialog as a whole does not scroll.
  • Every input is inside the scrolling body. The heading, Submit / Confirm and Cancel are outside it, in header → body → footer order.
  • Long screens and lists are wide and two-column. The short-screen and short-list controls are unchanged.
  • The width switches at more than eight fields (declared fields for a screen, shown params for a list). The object-form step's bound is covered, and the resume-failure alert sits above the body.
  • A parity case: both dialogs carry the same bound tokens.

Reverse verification, from the committed fix. git checkout 023f00d -- FlowRunner.tsx ActionParamDialog.tsx. Disk check: isLongScreen 3 → 0 hits, isLongList 3 → 0, max-h-[90vh] in the params dialog → 0. Result: 15 of 15 tests failed, Test Files 2 failed (2). Restored with git checkout HEAD -- …. Both blobs then hash to HEAD's (481cb37d, f409340c), git diff HEAD is empty, the index is clean, and both hit counts are back to 3. Read this together with the controls: the control cases fail on BASE because the scrolling body they read does not exist there, not because the width changed. Their unchanged width is shown by the browser rows above.

Gates (at ca44673)

  • pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build → exit 0 (dependency closure for type-check)
  • pnpm --filter @object-ui/app-shell type-check → exit 0 (tsc --noEmit && tsc -p tsconfig.test.json; the test config's include is src/**/*.test.tsx, so both new files are covered)
  • pnpm exec vitest run --maxWorkers=2 over the 24 affected files (the 9 FlowRunner* + FlowRefusalNotice, the 9 ActionParamDialog*, the ScreenView* files, and the two paramDialogTitle consumers) → Test Files 24 passed (24), Tests 206 passed (206)
  • eslint on the 4 touched files (--format json: 4 files, 0 errors). The 16 warnings are all on untouched lines (no-explicit-any, set-state-in-effect, only-export-components). The narrowing is sound because type-aware linting is off: eslint.config.js sets no parserOptions.project or projectService, so this diff cannot change a verdict on an untouched file.
  • pnpm check:new-line-citations → 0 new citation(s); pnpm check:control-bytes → OK
  • node scripts/check-changeset-presence.mjs → ok (1 changeset); check-changeset-no-major.mjs → ok; check:changeset-claims, check:pending-changeset-literals, check:i18n-keys, check:test-path-roots, check:unreferenced-sources → all exit 0
  • NOT MEASURED: check:eager-closure. Reason: it needs a full console production build. No module joins or leaves the first-load closure: the only new imports are cn from @object-ui/components and screenFields from ./ScreenView, and both modules were already imported. The byte delta is the added class literals and is left to CI's Bundle Analysis.

Acceptance notes

  • Not swept: other console dialogs on the bare DialogContent. Most of them show short, fixed content. None was measured, and none is touched here. Instrument: git grep -n "DialogContent" -- 'packages/*/src/**/*.tsx', read per call site. Carrier: none.
  • The eight-field threshold is a renderer choice, per the triage ruling. It is the most single-line fields the narrow column showed whole inside the 90vh bound at 1440×900, measured once (above). The comment on the constant says that it is a one-time reading.

Generated by Claude Code

… on screen (objectui#12080)

A flat flow screen and the action parameter dialog had no height bound,
so a screen or params list taller than the window overflowed both edges
of the fixed, scroll-locked overlay: heading above the top, primary
action below the bottom, nothing a mouse wheel could move.

Both dialogs are now a flex column at most 90vh tall: the header on top,
the fields in the one scrolling region, the footer outside it. More than
eight declared fields (or shown params) also widens the dialog to the
object-form step's sm:max-w-3xl with two columns from sm up; shorter
screens and lists keep their width and single column.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 290 chunks) 3168.9 KB 3204.6 KB
Main entry chunk (gzip) 73.7 KB 350 KB
Entry file index-CKbozXUQ.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) 19.75KB 7.29KB
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.29KB 0.66KB
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.11KB 7.97KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.28KB 2.60KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
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) 587.83KB 141.44KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 240.39KB 67.09KB
fields (index.js) 269.55KB 68.24KB
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.26KB 11.71KB
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) 14.32KB 5.17KB
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) 53.43KB 15.54KB
plugin-charts (index.js) 84.72KB 23.27KB
plugin-chatbot (index.js) 201.52KB 47.99KB
plugin-dashboard (index.js) 144.20KB 38.95KB
plugin-designer (index.js) 233.53KB 49.80KB
plugin-detail (index.js) 249.19KB 65.68KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.30KB 45.92KB
plugin-gantt (index.js) 179.17KB 45.07KB
plugin-grid (index.js) 249.43KB 69.15KB
plugin-kanban (index.js) 52.77KB 16.56KB
plugin-list (index.js) 120.09KB 30.26KB
plugin-map (index.js) 27.24KB 9.03KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 39.06KB 11.80KB
plugin-tree (index.js) 15.07KB 5.33KB
plugin-view (index.js) 91.93KB 23.24KB
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.80KB
sdui-parser (provenance.js) 3.84KB 1.90KB
sdui-parser (types.js) 0.28KB 0.23KB
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-components.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
objectstack-fleet Bot marked this pull request as ready for review October 10, 2026 09:45
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 10, 2026 09:45
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 10, 2026
Merged via the queue into main with commit 4d0ff23 Oct 10, 2026
45 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12080-screen-dialog-reach branch October 10, 2026 10:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants