Skip to content

fix(app-shell): the console toaster steps clear of an open right-edge drawer (objectui#11685) - #11722

Merged
objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11685-toast-clears-drawer-chrome
Oct 6, 2026
Merged

objectstack-fleet[bot] merged 1 commit into
mainfrom
claude/issue-11685-toast-clears-drawer-chrome

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #11685

Clause-②: no — nothing is added to the package entry: no export, prop, type member or language-pack key. ConsoleToaster keeps its one export and its props type. The change stays inside packages/app-shell/src/chrome/ConsoleToaster.tsx.

What changed

The toaster keeps position="top-right". objectui#7482 chose that corner because bottom-right belongs to the ChatDock composer and its FAB, and sonner pauses dismiss while the pointer is inside its region. That pin, ConsoleToaster anchor (objectui#7482), is untouched and green. While a right-edge drawer is open, ConsoleToaster now hands sonner an offset / mobileOffset instead of moving the anchor:

  • Room beside the drawer (window width minus drawer width is at least one toast plus two 24px gaps): the toaster moves into the strip left of the drawer: --offset-right = drawer width + 24px. The strip is under the drawer's modal overlay, so no control sits there, and no part of the drawer is covered.
  • No room (phones, or a drawer nearly as wide as the window): the toaster drops below the drawer's header: --offset-top = header bottom + 24px, and --mobile-offset-top = header bottom + 16px (sonner's own phone gap).
  • No drawer open: offset is undefined, which is sonner's default and today's behaviour. A caller's own offset prop still wins through the existing {...props} spread.

The signal adds no export. A right-edge drawer is read off the DOM: an open Radix dialog (role="dialog", data-state="open") whose computed style is position: fixed with top, right and bottom all zero. Every side="right" sheet matches: the record drawer that ObjectView opens through NavigationOverlay, and the metadata-admin, activity, build-debug and draft-changes sheets. None of these match:

  • popovers (Radix popover content is also role="dialog", but it is not itself fixed);
  • centred dialogs;
  • left and bottom sheets.

The header is the dialog's top-level child that holds its aria-labelledby title. Sizes are layout sizes (offsetWidth, offsetTop + offsetHeight), so the drawer's slide-in transform does not skew them. These observers re-measure on the next animation frame:

  • two MutationObservers on body, one for portal mount and unmount and one for data-state flips;
  • a ResizeObserver on each open drawer and its header, for drag-resize;
  • window resize.

State updates only when the value changes.

The landing site is the one triage named. The overlap comes from the toaster's placement, so no drawer code changed.

Measurement (one-off, real layout, Chromium)

happy-dom has no layout, so the geometry was measured in headless Chromium 1194 (/opt/pw-browsers/chromium) against the console's own Vite dev server, with the workspace src aliases and real Tailwind. A throwaway entry page, not committed, mounted three real components:

  • ConsoleToaster;
  • NavigationOverlay in drawer mode, with the props ObjectView passes it: title, onExpand, expandLabel, storageKey, and width from overlayWidthFor('auto', ...) with 3 / 8 / 15 / 20 fields, which gives the sm / md / lg / xl buckets;
  • the embedded RecordDetailView, with two record_header actions (Approve, Reject).

Clicking the page's New button opened the drawer and raised toast.success('Product created'), the shape of the create flow. 900ms later the page raised a second toast.success('Approved').

⚠️ This is not the full app against a backend. The objectstack checkout in this container has no built CLI (packages/cli/dist is absent), so the showcase backend could not start without building a shared checkout. The components, the CSS and the toaster are the real ones; only the toast's trigger is scripted.

Boxes are getBoundingClientRect() in CSS px. The drawer header's bottom edge is at 45px. At 1440 the expand button spans x 1366–1394 and the close button x 1396–1424, both at y 8–36.

Before (base c910630). The first toast sits at top: 24px; right: 24px in every configuration; at 1440 that is x 1060–1416, y 24–77.5, and the second toast is at y 91.5–145. In all 16 configurations (4 viewports, 4 drawer sizes) the toast intersects the drawer header, the expand button, the close button and the chrome title. In all 12 desktop configurations the second toast also covers the Approve action.

After (this branch):

viewport drawer drawer x-range path first toast x, y header / expand / close / title record title Approve
1440x900 sm, md 576–1440 beside 196–552, 24–77.5 no no no
1440x900 lg 480–1440 beside 100–456, 24–77.5 no no no
1440x900 xl 240–1440 below header 1060–1416, 69–122.5 no no yes
1280x800 sm, md 512–1280 beside 132–488, 24–77.5 no no no
1280x800 lg, xl 320 or 102.4 – 1280 below header 900–1256, 69–122.5 no no yes
1024x768 sm 409.6–1024 beside 30–386, 24–77.5 no no no
1024x768 md 304–1024 below header 644–1000, 69–122.5 no yes yes
1024x768 lg, xl 81.9–1024 below header 644–1000, 69–122.5 no no yes
390x844 all 0–390 below header (phone offsets) 16–374, 61–114.5 no yes yes

The card's pin holds in all 16 configurations: the toast's box does not intersect the drawer's header controls. Where the strip beside the drawer is too narrow, the toast still sits over the drawer's first content row (the record header and its actions). That is the cost of the card's "below the drawer header" route, listed under Acceptance notes.

Dynamic checks (same harness, drawer size sm):

  1. Open at 1440: --offset-right: 888px.
  2. Window resized to 1024: 638px (drawer at 410–1024).
  3. Resized to 900: falls back to --offset-top: 69px.
  4. Back to 1440: 888px.
  5. Drawer drag-resized narrower (now 563px wide): 587px. The toast follows the drawer's edge.
  6. Drawer closed with its own close button: back to 24px / 24px.

Tests

The new pin is ConsoleToaster.drawerClearance-11685.test.tsx (ConsoleToaster clears an open right-edge drawer (objectui#11685)). Its six cases:

  1. the no-drawer control;
  2. the beside path;
  3. the below-header path, including the phone offset;
  4. back to the corner on data-state="closed" and on unmount, and forward again on re-open;
  5. centred, left-sheet and popover-shaped dialogs are ignored, with a non-vacuity tail;
  6. a caller's offset still wins.

happy-dom has no layout, so the test assigns the Chromium-measured values to the drawer's offsetWidth and the header's offsetTop / offsetHeight. The file says so in its header.

Ablation, run from the committed fix and restored from HEAD. Removing the offset / mobileOffset wiring takes the two anchors from 1/1 to 0/0 (marker 0 to 1) and turns 4 of 6 red, with expected '24px' to be '888px' and expected '24px' to be '69px'. The four are the beside, below-header and close/unmount cases and the ignore case's non-vacuity tail. The control and the caller-wins case stay green, as designed. After the restore, git diff HEAD is 0 bytes and the file's blob ade5562728ef equals the HEAD blob.

Gates (local; the union ran on bd2663d)

command exit verdict
pnpm --workspace-concurrency=2 --filter '@object-ui/app-shell^...' build 0 VERDICT command-exit 0 (29 packages)
pnpm exec vitest run --maxWorkers=2 packages/app-shell/src/chrome/ plus the 5 files named below 0 Test Files 14 passed (14), Tests 68 passed (68)
pnpm --filter @object-ui/app-shell type-check 0 VERDICT command-exit 0; tsc -p tsconfig.test.json --listFiles reads 5039 files, the new test among them
pnpm check:control-bytes 0 OK (scanned 7664 tracked text file(s); skipped 85 binary)
pnpm check:new-line-citations 0 0 new citation(s)
pnpm check:changeset-claims 0 No pending changeset names a file this change touches.
pnpm check:pending-changeset-literals 0 No test source names a pending changeset.
added from the diff: check:test-path-roots, check:unreferenced-sources, check:phantom-deps, and scripts/check-changeset-presence / -no-major / -fixed / -overwrite 0 each ✅ / OK each

Declared narrowing. I started the full pnpm --filter @object-ui/app-shell test (1033 test files) and stopped it after it had held the shared verify lock for 32 minutes while other seats timed out behind it. It ended in VERDICT command-exit 143, so the full suite is NOT MEASURED locally; CI runs it.

The narrowed run covered the whole chrome/ directory, plus every test file outside it that names ConsoleToaster (git grep -l ConsoleToaster over test files). Those are the four console App tests, which mock it to a null component, and WizardForm.outcomeToastSupersede, which names it in a comment. Why that is enough: ConsoleToaster.tsx has no import-time side effect, and its export and props type are byte-unchanged. Only a test that renders the real component can observe the change, and the only tests that do are the chrome pins.

The repo-wide pnpm lint belongs to CI. eslint on the two touched files ran with --no-inline-config --format json: 2 files, 0 errors, 0 warnings. The lint is not type-aware (eslint.config.js sets no parserOptions.project).

Acceptance notes

  • Cost of the fallback path. Sometimes the strip beside the drawer is narrower than a toast: at 1440 xl, 1280 lg and xl, 1024 md and wider, and on phones. Then the toast drops below the drawer header and still covers the drawer's first content row: the record header's actions, and at 1024 md and on phones the record title too. The card's pin, about the header controls, holds there.
  • Toasts are click-through under any Radix modal, before and after this change (out of scope; reported to the seat). Radix sets pointer-events: none on body while a modal is open, and sonner sets nothing on its toaster. So with a drawer open the toaster computes pointer-events: none (measured in every configuration above). A toast's close button and its action buttons cannot be clicked; the click lands on whatever is beneath.
    • Measured on this branch, drawer open: clicking a toast's Undo action hits the drawer's overlay (elementFromPoint returns the overlay div). Undo does not fire, and the drawer closes.
    • On the base, by the measured boxes: the same click lands on the drawer itself, on the header's expand or close button, or on the record's Approve under the second toast.
    • Who raises such toasts: RecordDetailView and useConsoleActionRuntime raise success toasts with an Undo action.
    • Why it is not fixed here: making the toaster interactive under a modal also makes a toast on the fallback path block the drawer content under it and pause on hover, which is the objectui#7482 failure mode. That is a design choice, not a mechanical fix.
  • Docs: no guide page describes the toaster's position. content/docs/guide/notifications.md names ConsoleToaster only as where toasts go, so no doc changed.

Session: https://claude.ai/code/session_01DrKzdPdyLLBW3qpZ4vtk7z


Generated by Claude Code

… drawer (objectui#11685)

The toaster keeps its top-right corner (objectui#7482). While a right-edge
drawer is open it is offset instead: into the strip of page left of the
drawer when a toast fits there, otherwise below the drawer's header, so a
success toast no longer lands on the drawer's expand and close buttons or
its title. The drawer is read off the DOM (an open Radix dialog laid out
fixed to the top, right and bottom edges); nothing is added to the package
entry.

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

github-actions Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 331 chunks) 3324.4 KB 3330.4 KB
Main entry chunk (gzip) 154.1 KB 350 KB
Entry file index-DzSnDlT3.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) 17.82KB 6.58KB
app-shell (runtime-config.js) 22.52KB 7.86KB
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) 40.70KB 10.94KB
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) 574.74KB 137.95KB
core (index.js) 10.00KB 3.96KB
create-plugin (index.js) 27.94KB 9.51KB
data-objectstack (index.js) 233.72KB 64.83KB
fields (index.js) 262.75KB 66.62KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
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) 35.66KB 9.49KB
i18n (useSafeTranslation.js) 7.14KB 2.92KB
layout (index.js) 41.18KB 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) 13.86KB 5.00KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.52KB 2.26KB
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.39KB 15.52KB
plugin-charts (index.js) 84.26KB 23.05KB
plugin-chatbot (index.js) 198.81KB 47.14KB
plugin-dashboard (index.js) 143.75KB 38.87KB
plugin-designer (index.js) 231.46KB 48.87KB
plugin-detail (index.js) 247.25KB 65.05KB
plugin-editor (index.js) 2.23KB 1.05KB
plugin-form (index.js) 177.09KB 45.89KB
plugin-gantt (index.js) 179.16KB 45.06KB
plugin-grid (index.js) 238.48KB 65.51KB
plugin-kanban (index.js) 52.17KB 16.37KB
plugin-list (index.js) 116.72KB 29.10KB
plugin-map (index.js) 25.60KB 8.62KB
plugin-markdown (index.js) 13.88KB 4.80KB
plugin-report (index.js) 44.12KB 12.29KB
plugin-timeline (index.js) 38.90KB 11.74KB
plugin-tree (index.js) 14.51KB 5.15KB
plugin-view (index.js) 90.23KB 22.73KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.81KB 3.58KB
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.08KB 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.25KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 8.11KB 3.32KB
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

console: the success toast covers the record drawer's expand/close buttons and title

2 participants