Skip to content

fix(components): every wide centred dialog reads one shared viewport gutter (objectui#12603) - #12636

Merged
objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12603-dialog-gutter
Oct 16, 2026
Merged

objectstack-fleet[bot] merged 3 commits into
mainfrom
claude/issue-12603-dialog-gutter

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #12603
Clause-②: yes

What changed

One shared rule for the viewport gutter of every wide centred dialog. ModalForm's private BOUNDED_SIZE_VIEWPORT_GUTTER (PR #12598) is promoted to one export of @object-ui/components: DIALOG_VIEWPORT_GUTTER = 'sm:w-[calc(100%-2rem)]', in the new module packages/components/src/custom/dialog-viewport-gutter.ts, re-exported through custom/index.ts. Every wide DialogContent now writes its cap and this constant together, and nothing spells the gutter literal of its own.

  • From sm up the dialog is the smaller of its max width and the window less 2rem. The primitive's left-[50%] / translate-x-[-50%] centres it, so it keeps 1rem either side.
  • Above its max width plus 2rem each dialog is exactly as wide as before.
  • The Shadcn-synced primitive packages/components/src/ui/dialog.tsx is not edited.

Sites:

  • app-shell/src/hooks/useActionModal.tsx SIZE_CLASS: lg and xl (the size a page-target modal with no size opens at). full, sm, default and SIDE_SIZE_CLASS (the sheet) are unchanged.
  • app-shell/src/views/ActionParamDialog.tsx (long list), FlowRunner.tsx (object form or long screen), metadata-admin/CelTestRunDialog.tsx.
  • components/src/custom/fullscreen-editor.tsx.
  • fields/src/widgets/RecordPickerDialog.tsx: an unconditional cn() argument, so every column-count rung reads it, the lg:max-w-5xl ceiling included. Below sm it stays 95vw.
  • fields/src/widgets/ImageLightbox.tsx: measured in the family (below).
  • plugin-grid/src/ImportWizard.tsx.
  • plugin-form/src/ModalForm.tsx reads the export instead of its private copy. Its class output is byte-identical.

Census packages/components/src/__tests__/dialogViewportGutterCensus-12603.test.ts (7 tests):

  • It reads every packages/*/src and apps/*/src source file whose comment-masked text (scripts/js-comment-mask.mjs) names DialogContent.
  • It follows each DialogContent / MobileDialogContent className through the TypeScript AST: cn() arguments, conditionals, same-file consts and maps, and same-file function returns.
  • A breakpoint cap is WIDE when a window at that breakpoint cannot hold it with 1rem a side: sm: above 608px, lg: above 992px, and so on. A cap it cannot size fails closed.
  • WIDE_SITES declares every wide centred dialog. Declared and found must agree, and each must read the gutter: a template literal that interpolates DIALOG_VIEWPORT_GUTTER beside the cap, or the constant as an unconditional cn() argument.
  • OWN_GUTTER is deletions-only. It holds one site, below.
  • Sheets are never judged.

Runtime pins:

  • app-shell/src/hooks/__tests__/useActionModal.viewportGutter-12603.test.tsx (4 tests): xl and lg read the gutter. CONTROLs: full keeps sm:max-w-[95vw] sm:w-full, default reads none, and a side placement (sheet) reads none.
  • fields/src/widgets/RecordPickerDialog.viewportGutter-12603.test.tsx (2 tests): the widest rung (six columns, sm:max-w-3xl lg:max-w-5xl) and the narrowest carry it. w-[95vw] stays for below sm.

Mechanism assumptions, as measured

  • A1, the record picker across both breakpoints: holds after the change. The premise that it ran edge to edge does NOT hold on main: its own w-[95vw] kept a 2.5vw margin (16px at 640, 19.2px at 768, 25.6px at 1024). Under the one rule it now reads the shared gutter like the rest: 16px at every width from sm up to the cap, and unchanged above the cap plus 2rem. The widest case is pinned.
  • A2, Clause-②: yes. Promoting the token adds a new named export, DIALOG_VIEWPORT_GUTTER, to the @object-ui/components package entry. That is a widening, so the changeset declares @object-ui/components minor, and each other touched package patch. The second line of this body is corrected to yes.
  • The class is still emitted. I ran the console's own Tailwind step: @tailwindcss/postcss over apps/console/src/index.css, whose @source lines cover packages/components/src. The emitted sheet carries .sm\:w-\[calc\(100\%-2rem\)\] { width: calc(100% - 2rem) } inside the sm media query. After this change the only tracked non-test source that spells the class is components/src/custom/dialog-viewport-gutter.ts, so the rule comes from there.
  • ImageLightbox is in the family. Its sm:max-w-4xl replaces the 92vw from sm up. Measured with a 3000px-wide image, before: 768 gives 768 · 0/0 and 640 gives 640 · 0/0, edge to edge. After: 736 · 16/16 and 608 · 16/16. Folded in.

Real-layout reading (headless Chromium 1194, static harness)

Each class list is the primitive's class string merged with the site's class through tailwind-merge, as cn does, over the console's emitted CSS. The viewport height is 900. Each cell is width · left/right gutter in px, before → after.

Viewport action modal xl lg / CelTestRunDialog 3xl sites (param list, flow, editor) 4xl sites (import wizard, lightbox image) record picker, six columns
1440 1024 · 208/208 → same 672 · 384/384 → same 768 · 336/336 → same 896 · 272/272 → same 1024 · 208/208 → same
1100 1024 · 38/38 → same 672 · 214/214 → same 768 · 166/166 → same 896 · 102/102 → same 1024 · 38/38 → same
1024 1024 · 0/0 → 992 · 16/16 672 · 176/176 → same 768 · 128/128 → same 896 · 64/64 → same 972.8 · 25.6/25.6 → 992 · 16/16
900 900 · 0/0 → 868 · 16/16 672 · 114/114 → same 768 · 66/66 → same 896 · 2/2 → 868 · 16/16 768 · 66/66 → same
768 768 · 0/0 → 736 · 16/16 672 · 48/48 → same 768 · 0/0 → 736 · 16/16 768 · 0/0 → 736 · 16/16 729.6 · 19.2/19.2 → 736 · 16/16
700 700 · 0/0 → 668 · 16/16 672 · 14/14 → 668 · 16/16 700 · 0/0 → 668 · 16/16 700 · 0/0 → 668 · 16/16 665 · 17.5/17.5 → 668 · 16/16
640 640 · 0/0 → 608 · 16/16 640 · 0/0 → 608 · 16/16 640 · 0/0 → 608 · 16/16 640 · 0/0 → 608 · 16/16 608 · 16/16 → same
390 unchanged unchanged unchanged unchanged unchanged

CONTROLs, identical before and after at every viewport: the action modal's full (1368 · 36/36 at 1440, 608 · 16/16 at 640) and default (512 wide).

Tests and gates

The heavy runs went through the shared verify lock. CI runs the full farm.

  • New and touched pins: the census (7 tests), the action modal pin (4) and the picker pin (2) all pass.
  • Package suites:
    • pnpm exec vitest run --maxWorkers=2 packages/components/: Test Files 389 passed, 1 skipped (390). Tests 3926 passed, 24 skipped (3950).
    • packages/fields/: Test Files 258 passed, 1 skipped (259). Tests 4086 passed, 7 skipped (4093).
    • packages/plugin-form/: Test Files 187 passed (187). Tests 2291 passed, 1 skipped (2292).
    • packages/plugin-grid/: Test Files 217 passed (217). Tests 2018 passed (2018).
    • All four ran at 5bfde250b.
  • @object-ui/app-shell, narrowed (declared): the full packages/app-shell/ run (1434 test files) was stopped before it took the lock, because it would have held the shared lock for most of an hour with other seats queued. In its place, 126 files ran at 5bfde250b and passed: Test Files 126 passed (126), Tests 1127 passed (1127). They are every app-shell test that names useActionModal, ActionParamDialog, FlowRunner or CelTestRunDialog (git grep -l), plus the package's ratchet and census tests (product-neutral-renderer, no-component-any-cast, actions-envelope, flow-envelope and the rest). The blind spot: a test that renders one of these dialogs without naming its module is not in the set. CI runs the whole package.
  • Type-check: pnpm turbo run build --filter='@object-ui/app-shell^...' --concurrency=2 exited 0 (28 tasks). Then type-check (tsc --noEmit && tsc -p tsconfig.test.json) for components, app-shell, fields, plugin-grid and plugin-form exited 0. --listFilesOnly confirms that each test tsconfig compiles its new pin.
  • ESLint on the 14 changed .ts / .tsx files (--no-inline-config): 0 errors. The new files carry no warnings. Every warning on the touched files is a pre-existing no-explicit-any / react-hooks warning on lines this diff does not touch.
  • Pre-tsc battery, each exit 0: check:phantom-deps, check:self-import, check:unreferenced-sources, check:doc-example-readers, check:handler-key-reads, check:side-effects-array, check:element-data-source-declaration, check:spec-symbols, check:action-forward-parity, check:designer-field-key-parity, check:icon-record-names, check:i18n-keys, check:i18n-drift, check:i18n-designer-parity, check:control-bytes, and node scripts/check-hand-rolled-comment-mask.mjs.
  • Changeset gates, each exit 0: check-changeset-fixed, check-changeset-presence (14 source files of 5 released packages, 1 changeset declared), check-changeset-no-major. Also check:new-line-citations (0 new) and check:test-path-roots.
  • Governed surface: check-governed-queue-guard --test on the 15 paths answers NOT GOVERNED.

Ablation

The fix was committed first. ablation-replace mutated the action modal's xl entry from the template with the gutter to the bare 'sm:max-w-5xl', with a trapped restore. On disk the anchor count went from 1 to 0, and the blob changed from c02389544865 to ee8915849adb.

  • The census went red: every listed wide dialog reads the shared gutter named packages/app-shell/src/hooks/useActionModal.tsx · DialogContent · sm:max-w-2xl sm:max-w-5xl.
  • The runtime pin went red: xl, the size a page target with no size opens at, reads the gutter beside its cap.
  • Every other test stayed green: Tests 2 failed, 9 passed (11).
  • After the restore, the blob equals HEAD and git diff HEAD is empty.

Acceptance notes

  • The census threshold is geometric, not "above lg" taken literally. A sm:max-w-xl (576px) or sm:max-w-[560px] dialog keeps at least 32px a side in the narrowest sm window, so it is never in this defect. The census counts a cap as wide only when a window at its breakpoint cannot hold it with 1rem a side. All the card's enumerated sites are caught, and one more as well (next note).
  • One wide dialog keeps a gutter of its own and is frozen in OWN_GUTTER. It is components/src/custom/navigation-overlay.tsx's modal mode: w-[calc(100vw-1rem)] under a host-set --ov-w cap, 42rem by default. It is never edge to edge, but it is a 0.5rem literal of its own. It was outside the card's enumeration and this PR's file surface, so it is not converged here. Carrier: none.
  • The record picker gets slightly wider between sm and its cap. It used 95vw, and is now the window less 2rem: for example 736px instead of 729.6px at 768. That is the one rule replacing a per-site margin.
  • Not covered by the census: a className that arrives from outside the file (a prop, schema.className), an inline style width, and a wrapper that renders a DialogContent under another tag name. Each is stated in the census header.
  • No docs page states these dialog widths, so no docs edit.

Implemented by the os-dev run in session https://claude.ai/code/session_01XVp19E1MYUTFM3dnkwz8gB.


Generated by Claude Code

…gutter

Promote ModalForm's objectui#12593 gutter to one export,
DIALOG_VIEWPORT_GUTTER in @object-ui/components, and have every wide
centred DialogContent read it beside its sm cap: the action modal's lg/xl,
ActionParamDialog, FlowRunner, CelTestRunDialog, the fullscreen editor,
ImageLightbox, RecordPickerDialog and ImportWizard. A census in
@object-ui/components enumerates every wide centred dialog and fails one
that does not read the gutter.

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

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 70 chunks) 3093.7 KB 3119.4 KB
Main entry chunk (gzip) 73.3 KB 350 KB
Entry file index-Vqerncfn.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) 21.19KB 7.76KB
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) 598.49KB 144.31KB
core (index.js) 10.18KB 4.04KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 248.20KB 69.22KB
fields (index.js) 283.17KB 72.17KB
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.89KB 48.13KB
plugin-dashboard (index.js) 146.15KB 39.55KB
plugin-designer (index.js) 233.95KB 50.08KB
plugin-detail (index.js) 262.91KB 69.49KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 180.92KB 46.73KB
plugin-gantt (index.js) 178.36KB 45.08KB
plugin-grid (index.js) 265.13KB 73.29KB
plugin-kanban (index.js) 53.23KB 16.71KB
plugin-list (index.js) 129.90KB 33.12KB
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.43KB 23.94KB
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

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 5bfde250b208529a3fc7347b030ae39f77d3a1e3
Local-runs: none

Read in this act: card #12603 (body, triage 6207794844, the claim 6209329731 as revised, the dev report 6210857581), objectui#12593 and merged PR #12598 (the token's origin, 85e8961f28), PR #12636's body, its 15-file list, the net diff against main at this head (merge base 63d00da3de), and this head's check-runs. No earlier contract review exists on this PR; the head is reviewed whole. Nothing was built, run or re-run.

① Derived judgments

  • The export. DIALOG_VIEWPORT_GUTTER = 'sm:w-[calc(100%-2rem)]' in the new packages/components/src/custom/dialog-viewport-gutter.ts, one export * line added to the existing custom/index.ts barrel, which packages/components/src/index.ts already re-exports; package.json exports is untouched (. only), so the constant is reached through the one entry that exists. Minimal: one string constant, no prop, type member or second spelling. Named for what it is. Documented in the module's own header (the mechanism, the 100%-not-100vw reasoning carried over verbatim from PR fix(plugin-form): keep a 1rem viewport gutter on lg/xl ModalForm dialogs from sm up (objectui#12593) #12598, the sm:-only scope, and the pairing rule), and in the changeset, which ships as the package CHANGELOG. Docs line: packages/components/README.md enumerates no custom export (MobileDialogContent, FullscreenEditor, NavigationOverlay, EmptyStateLink are absent from it and from content/docs), and the last custom-barrel addition (b434f5f47, objectui#12270) documented its export at the consumer, not in that README. No docs page states a dialog width (content/docs/plugins/plugin-form.mdx names modalSize levels only). So no README or guide line is owed on THIS PR; the changeset is the documentation of record for the export. Read as acceptable, flagged in ③.
  • Value identity. PR fix(plugin-form): keep a 1rem viewport gutter on lg/xl ModalForm dialogs from sm up (objectui#12593) #12598 defined BOUNDED_SIZE_VIEWPORT_GUTTER = 'sm:w-[calc(100%-2rem)]'; the new export is the same bytes, and the census pins it (the gutter is the one class objectui#12593 measured). ModalForm's lg / xl rows change only the identifier inside the template, so the class string is byte-identical; the 12593 pin modalViewportGutter-12593.test.tsx is untouched by the diff.
  • The primitive. packages/components/src/ui/dialog.tsx is not in the diff; nothing under packages/components/src/ui/ is. The claim's stop condition holds.
  • Class lists. At every site the token is appended beside the cap, either in a template literal or as an unconditional cn() argument. The only w- tokens it meets are w-[95vw] (RecordPickerDialog) and the primitive's w-full, both unprefixed, so tailwind-merge keeps them next to the sm: token with no conflict; ImageLightbox's max-w-[92vw] sm:max-w-4xl is max-w-, a different group. The full size keeps sm:max-w-[95vw] sm:w-full and does not read the token. Emission: the PR body reports the console's own @tailwindcss/postcss step emitting .sm\:w-\[calc\(100\%-2rem\)\] under the sm media query, with the literal now spelled once in dialog-viewport-gutter.ts; the console's @source covers packages/components/src, and PR fix(plugin-form): keep a 1rem viewport gutter on lg/xl ModalForm dialogs from sm up (objectui#12593) #12598 measured the same literal emitted from plugin-form/src. Not re-run here; consistent with the diff and the origin PR.
  • Census threshold. Triage's ask was "every DialogContent with a sm:max-w- above lg". The census judges a breakpoint cap WIDE when it exceeds the breakpoint less 2rem (sm: above 608px, lg: above 992px), fails closed on a cap it cannot size, and skips a viewport-relative cap below 100. This is a sound reading: it catches every cap the literal reading would (2xl and up at sm: are all above 608px), drops only sm:max-w-xl (576px) and sm:max-w-[560px] / [480px] / [425px], each of which keeps at least 32px a side in a 640px window and cannot show the defect, and it ADDS md: / lg: prefixed caps, arbitrary px / rem caps and unsizable caps, which the literal reading would miss. My own enumeration of every (sm|md|lg|xl):max-w- in non-test sources at this head finds the same nine centred sites and no other: every remaining wide cap is on a SheetContent (FlowRunsPage, AuditLogPage, MetadataDetailDrawer, PermissionMatrixEditor, ResourceEditPage, ObjectChart, DrillDownDrawer, ObjectView's form panel), which the card excludes by design. Fails loudly: a NEW wide dialog fails declared and found agree until it is listed, and every listed wide dialog reads the shared gutter until it reads the export; a gutter literal of its own does not count (the own probe). Stated blind spots are honest: a className from a prop or schema.className, an inline style, a wrapper under another tag name, and an unprefixed cap (max-w-5xl with no breakpoint). I probed the last one: no DialogContent at this head carries an unprefixed wide cap.
  • OWN_GUTTER. The one entry is navigation-overlay.tsx's modal mode: w-[calc(100vw-1rem)] max-w-none sm:max-w-[var(--ov-w,42rem)], caught by the fail-closed arm. It keeps 0.5rem a side at every width and is never edge to edge, so it is not the card's defect; it sits outside the enumeration and the claimed file surface, and converging it would be a visible width change on a component this card did not measure. Freezing it deletions-only, with a test that each entry is still a wide site without the shared gutter, is acceptable. Noted as a follow-up in the dev report with dedupe words; no card filed, which is right for polish.
  • RecordPickerDialog. The card's table said it ran edge to edge; it did not, because w-[95vw] kept 2.5vw. Now the unconditional token applies under every rung. Between sm and its cap the dialog is 100% - 2rem instead of 95vw: identical at 640 (608 both), 6.4px wider at 768, 19.2px wider at 1024, never above its cap, never under 1rem a side, unchanged below sm and above cap plus 2rem. That is the one rule replacing a per-site margin, which is exactly what triage scoped; the gutter never shrinks below the family's floor. Not a regression.

② Semver level

.changeset/12603-dialog-gutter.md: @object-ui/components minor (one new named export on the package entry), @object-ui/app-shell, @object-ui/fields, @object-ui/plugin-grid, @object-ui/plugin-form patch; no major. All five are in the fixed group. The PR body's second line reads Clause-②: yes, and the claim was revised in place to Clause-②: yes (widening) with the same measurement. Widening at minor is the floor the rule sets; nothing is removed or renamed, so no migration text or ADR-0087 marker is owed. The changeset body cites by symbol and class string, no line address. Correct.

③ Boundary flags

  • Census threshold (geometric, not the literal "above lg"). Answered in ①: a superset of the defect-producing sites, a strict subset of the literal reading only where the literal would force a no-op. Accepted.
  • navigation-overlay.tsx exemption. Answered in ①: outside the file surface, never edge to edge, frozen deletions-only. Accepted; follow-up left to the seat.
  • RecordPickerDialog premise correction. The dev measured the card's claim false for this site and said so rather than reporting a repair. Converging it anyway is within triage's "one shared rule" scope. Accepted.
  • Docs line. No README or guide edit; the package documents no custom export in its README and the precedent put docs at the consumer. The changeset is the shipped record. Accepted; a maintainer who wants @object-ui/components to enumerate its custom exports owns that as a separate card.
  • app-shell suite narrowed to 126 files, declared. On a pull_request run scripts/select-affected-tests.mjs selects every package a changed file sits in plus every transitive dependent as whole-directory filters, so packages/app-shell/ is in CI's affected set in full across the 16 shards; the full battery on main runs within the hour either way. The dev's blind spot (a test rendering one of these dialogs without naming its module) is covered there. Not a gap in the record, provided the shards go green.
  • Check-runs on this head (read at 2026-10-16T08:04Z, 22 of 35 complete): every completed non-skipped run is success: Lint, Repo Gates, Line Citation Gate, Changeset Declaration, Changeset Bump Policy, Changeset Claim Re-read, Changeset Fixed Group Check, Changeset Overwrite Report, Governed Surface Queue Guard, Build & E2E, Build Docs, Bundle Analysis, Test (dist pins), Live E2E (informational), label, and Test shards 1, 2, 3 and 15 of 16. Still running, NOT claimed by this record: Type Check and Test shards 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14 and 16 of 16. The three skipped runs are the coverage and dependabot lanes that never run on a pull request. This verdict is on the diff and the completed gates; the running ones remain CI's to conclude, and a red among them stands over this record.
  • PR body footer. The dev used the role file's session-URL footer instead of the harness attribution form and said so. Reporting-only; no model identifier appears in the PR body, the changeset or the diff.

Implemented-by: claude/issue-12603-dialog-gutter
Reviewed-by: session_01XVp19E1MYUTFM3dnkwz8gB

VERDICT: PASS


Generated by Claude Code

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Review: ACCEPT — objectui PR #12636 · domain:ui seat 2 · session_01XVp19E1MYUTFM3dnkwz8gB · 2026-10-16T08:07Z

Reviewed on GitHub at head 5bfde25. I checked it against triage's scope (6207794844) and the claim (6209329731, whose Clause-②: line I revised in place), not against the dev's report.

  • Contract review: PASS, 6210976786, at this head.
  • Shape: a draft against main. The body opens Fixes #12603, the only closing keyword, followed by Clause-②: yes. It touches 15 files.
  • One rule: PR fix(plugin-form): keep a 1rem viewport gutter on lg/xl ModalForm dialogs from sm up (objectui#12593) #12598's gutter token moves from ModalForm's private constant to one export, DIALOG_VIEWPORT_GUTTER (sm:w-[calc(100%-2rem)]), reached through the existing custom/index.ts barrel.
    • Every wide centred dialog reads it beside its cap: the action modal lg/xl, ActionParamDialog, FlowRunner, CelTestRunDialog, fullscreen-editor, ImportWizard, RecordPickerDialog, and ImageLightbox, which was measured into the family.
    • ModalForm's output is byte-identical to before.
    • The Shadcn ui/dialog.tsx primitive is untouched.
  • Measured before the fix (Chromium over the console CSS):
    • the action modal xl ran edge to edge at 1024 and 768;
    • ImageLightbox ran edge to edge at 768.
  • Measured after: a 16/16 px gutter at every width from sm up to each cap, and unchanged above it. The CONTROLs are unchanged: sheets, full (95vw), the default size, and the phone width below sm.
  • Census pin: dialogViewportGutterCensus-12603 fails loudly when a new wide dialog lacks the token.
    • Its "wide" threshold is geometric: a cap is wide only if a window at its breakpoint cannot hold it with 1rem a side. The contract review found this a sound superset of triage's ask.
    • The review's own enumeration found the same nine centred sites.
  • Ablation: a bare xl turns the census and the runtime pin red; restore was proven by blob hash.
  • Clause-② yes (widening): @object-ui/components is minor; app-shell, fields, plugin-grid and plugin-form are patch. No package is major.
  • Deviations, accepted (each answered in contract review ③):
    • The geometric threshold, explained above.
    • RecordPickerDialog's premise was partly false: it kept w-[95vw] and was never edge to edge. It now reads the one rule. That widens it slightly between sm and its cap, but never below the 1rem floor.
    • navigation-overlay.tsx keeps its own 0.5rem gutter. It is frozen in a deletions-only OWN_GUTTER entry and is never edge to edge.
    • The app-shell suite was narrowed to 126 files locally. CI selects the whole package across its 16 shards.
  • Acceptance notes (not filed): navigation-overlay's modal could converge on the shared token later. This is polish, not a defect.
  • CI at head: 33 success, 3 skipped, 0 failures (filter=latest, 36 runs), 16 of 16 test shards green.

Next: pr_ready + automerge_enable.


Generated by Claude Code

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review October 16, 2026 08:08
@objectstack-fleet
objectstack-fleet Bot enabled auto-merge October 16, 2026 08:08
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Oct 16, 2026
Merged via the queue into main with commit 726d8c9 Oct 16, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-12603-dialog-gutter branch October 16, 2026 08:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants