Repository navigation
fix(app-shell): Studio on a smaller or resized window (objectui#11795) - #12015
Conversation
The flow canvas re-fits when its own width changes (a window resize, the chat dock, a side panel), capped at 100%; node adds, drags, edits and height-only changes leave the viewport where it is. On an Automations row too narrow for the Configuration aside beside one flow column, the aside folds away and a selection opens the configuration in a drawer over the canvas. The row is measured on its own, so the chat dock beside it counts; wider rows keep the aside unchanged. On a phone, one line under the Studio header says Studio is built for a desktop screen. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
|
| Step | Outcome |
|---|---|
| Build packages | success |
| Check console performance budget | failure |
Which half objected:
| Eager-closure half | Verdict |
|---|---|
| Aggregate closure ceiling | ❌ over its ceiling |
| Per-chunk ceilings | ✅ pass |
| Per-chunk membership (declared packages) | ✅ pass |
| Ceiling sensitivity (headroom) | ✅ pass |
| Ceiling freshness (checkout vs. base branch) |
⚠️ A superseded ceiling is neither a size regression nor a drifted gauge. The bundle was measured correctly and the ceilings on the base branch are correct — but this checkout predates a change to them, so the verdicts above were weighed against numbers that are no longer in force. Nothing grew. Update this branch onto the base branch and letBundle Analysisrun again; do not widen a ceiling to clear it. TheCheck console performance budgetstep log names each superseded constant with both values (objectui#6245).
Reason: The entry chunk measured 71.5 KB, but the eager-closure half of this gate returned no trustworthy VERDICT: the report could not be read, a ceiling has drifted out of range of the regression it must catch, or (objectui#6245) a ceiling was replaced on the base branch after this checkout was made. The step log says which. This is not a passing budget — and it is not a size regression either.
See the workflow run for details.
📦 Bundle Size Report
| Package | Size | Gzipped |
|---|---|---|
| app-shell (consoleActionDispatch.js) | 0.20KB | 0.19KB |
| app-shell (index.js) | 18.33KB | 6.73KB |
| 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.00KB | 3.96KB |
| create-plugin (index.js) | 27.94KB | 9.51KB |
| data-objectstack (index.js) | 240.39KB | 67.09KB |
| fields (index.js) | 268.74KB | 68.04KB |
| 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) | 41.50KB | 11.82KB |
| 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.43KB | 15.54KB |
| plugin-charts (index.js) | 84.71KB | 23.25KB |
| plugin-chatbot (index.js) | 199.63KB | 47.46KB |
| plugin-dashboard (index.js) | 144.20KB | 38.95KB |
| plugin-designer (index.js) | 233.53KB | 49.80KB |
| plugin-detail (index.js) | 248.57KB | 65.47KB |
| plugin-editor (index.js) | 2.23KB | 1.05KB |
| plugin-form (index.js) | 177.11KB | 45.88KB |
| plugin-gantt (index.js) | 179.17KB | 45.07KB |
| plugin-grid (index.js) | 249.56KB | 69.16KB |
| plugin-kanban (index.js) | 52.22KB | 16.38KB |
| plugin-list (index.js) | 117.80KB | 29.42KB |
| 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.10KB | 11.81KB |
| plugin-tree (index.js) | 15.07KB | 5.33KB |
| plugin-view (index.js) | 91.16KB | 22.97KB |
| 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.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.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
…dio-narrow-screens The claim has this branch merge main once objectui#11982 is on it: that change re-pinned the console's eager-closure ceilings, which Bundle Analysis measures this pull request against. Claude-Session: https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8 Co-authored-by: Claude <noreply@anthropic.com>
✅ Console Performance Budget
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
Size Limits
|
Fixes #11795
Clause-②: no
Studio stays a desktop tool (triage direction
6043019702): the flow canvas fits to view on resize, below the width where the inspector overlaps the canvas it opens as a drawer, and a phone gets a one-line hint. All of it is@object-ui/app-shell's Studio layout and flow canvas; no contract, export or published language pack changes.What changed
FlowCanvas.tsx). AResizeObserveron the canvas viewport re-frames the diagram when the viewport's width differs from the width it was last framed at: centred, zoomed out only as far as the whole flow needs, never past 100% (the scale a flow opens at, so a 3-node flow is not zoomed up to 145% by a wider window). The mount centering is untouched.lg. Re-fitting on those would move the viewport right after a node edit, which the mount centering's comment forbids. A window resize, the chat dock and the side panels beside the canvas all change its width.StudioDesignSurface.tsxAutomationsPillar layout,wideViewport.ts). When the pillar's own row is narrower thanAUTOMATIONS_CONFIG_FOLD_WIDTH, the fixed 288px aside is not rendered at all (it no longer reserves its width), and a selection opens the same configuration body in the sharedSheetover the canvas. Closing the drawer clears the selection, as the aside's ✕ does. Wider rows keep the aside exactly as before. The row is measured withuseIsNarrowerThan(aResizeObserveron the row, read before paint); a row that measures 0 is unmeasured and keeps the aside.StudioDesignSurfaceheader). Belowmd(useIsMobile), onerole="note"line under the Studio header readsengine.studio.desktopHint: "Studio is built for a desktop screen." / "Studio 专为桌面屏幕设计。" New en and zh rows inmetadata-admin/i18n.tsunder the existingengine.studio.prefix.The threshold, from measurement
The overlap is decided by the pillar's own row, not by the window: the Studio chat dock sits outside that row and takes 340 to 420px of the window when it is open. One window width therefore answers both ways (1024px: canvas about 460px with no dock, about 40px with it), which a viewport breakpoint cannot express.
AUTOMATIONS_CONFIG_FOLD_WIDTH= flow rail 208 (w-52) + chrome between the row and the canvas viewport 68 (mainp-4, framed cardp-4and borders; read in Chromium) + aside 288 (w-72) + one flow column at 100% (NODE_W+ 2 xPADDING= 296, the width a linear flow draws at) = 860. Below it the aside leaves less than one flow column. Every row belowmdis narrower than 860, so a phone always folds.Browser readings, before and after (Chromium 141, the Automations pillar)
The real
AutomationsPillarwith the real registeredFlowPreviewandFlowInspector, served by the console's Vite from this worktree, over a fetch stub that answers one 3-node flow (start, assignment, end). "dock" is a 420px column beside the pillar, the chat dock's default width. The harness files were temporary and are not in this PR.Controls, after: at 1440 a node added through the toolbar palette left the diagram's translate and scale unchanged; a node select did not move the viewport. Toggling the Variables panel at 1440 narrowed the canvas from 876 to 636px and re-fitted it, centred at 100%; at 390 the same panel stacks below the canvas (a height change) and nothing moved.
Mechanism hypotheses, as measured
dom,dom-heavy) install a no-opResizeObserverpolyfill (vitest.setup.base.ts); the pins replace it with a double whose notifications they deliver and put the original back, the shapeChartContainerImpl.settleRemount.test.tsxuses. The claim said "viewport size changes"; this re-fits on width only, for the reason above.showFoldedTabsanduseIsWideViewportbelong to the Interfaces pillar; Automations renders its own fixedw-72aside at every width, independent of them. Which pillars use which layout:aiSlot): belowxlthe inspector is a center tab, fromxlit sits beside the canvas. No overlap, so its fold decision and its rail are not edited here.t()frommetadata-admin/i18n.tswith theengine.studio.prefix;check:i18n-designer-paritygreen.Pins
FlowCanvas.resizeFit-11795.test.tsx: opened narrow then widened, the diagram is framed for the new width; narrowed below the diagram, it zooms out until the whole flow fits; a wider canvas does not zoom past 100%. Controls: a node add (with the re-armed observer's notification at the same width), a height-only change and a 0-wide canvas each leave the view exactly as it was.AutomationsPillar.narrowRow-11795.test.tsx: one pixel under the threshold, no aside in the row, and a node click opens the configuration in a dialog named Configuration with an enabled Label field; Escape closes it and clears the selection; a row resized from 1200 to 600 folds its aside in place. Controls: at the threshold and unmeasured (0), the aside stays and the click fills it, with no dialog.StudioDesignSurface.desktopHint-11795.test.tsx: at 390 the hint renders once, as a note, directly under the header; at 768 it does not.Reverse verification (each through
ablation-replace, committed tree, restore proven)observer.observe(vp);replaced so the observer never observes. Blobd42d997b917ato42476ad28aec; the resize file readTests 3 failed | 3 passed (6), the three re-fit pins red and the three controls green. Restored: blob == HEAD (d42d997b917a),git diff HEADempty.useIsNarrowerThan(layoutRowRef, AUTOMATIONS_CONFIG_FOLD_WIDTH)replaced with a 0 threshold. The pillar file readTests 3 failed | 2 passed (5), the three narrow-row pins red and the two controls green. Restored: blob == HEAD (ff0517aa737d),git diff HEADempty.Local verification (HEAD
54dc0e51a)pnpm exec vitest run packages/app-shell/:Test Files 1206 passed | 1 skipped (1207),Tests 11715 passed | 9 skipped (11724).pnpm --filter @object-ui/app-shell type-check(bothtsc --noEmitandtsc -p tsconfig.test.json, after building the dependency closure): exit 0.tsc -p tsconfig.test.json --listFilesOnlylists all three new test files.pnpm exec eslinton the seven touched files: 0 errors; the 15 warnings all sit on lines this diff does not add.check:control-bytes,check:test-path-roots,check:changeset-claims,check:pending-changeset-literals,check:i18n-keys,check:i18n-drift,check:i18n-dead-keys,check:i18n-designer-parity,check:new-line-citations,check:vi-mock-inherit,check:installed-pin-claims,check:doc-types, andscripts/check-changeset-presence.mjs/-no-major/-fixed/-overwrite: all exit 0.pnpm --filter @object-ui/app-shell build, neitherdist/index.d.tsnordist/views/studio-design/StudioDesignSurface.d.tsnameswideViewport,useIsNarrowerThanorAUTOMATIONS_CONFIG_FOLD_WIDTH; the only added exports are inwideViewport.ts, which the entry does not reach. HenceClause-②: noand apatchchangeset.After the merge, on
28566ca79:pnpm --filter @object-ui/app-shell type-check: exit 0.studio-design/,metadata-admin/previews/,metadata-admin/__tests__/and the objectui#11939 entry pin):Test Files 278 passed (278),Tests 2612 passed (2612).Acceptance notes
4babf4035, and this branch merged it (28566ca79).Bundle Analysison28566ca79passes: the eager closure is 3160.1 KB gzip (289 chunks) against a 3204.6 KB budget, and the main entry chunk is 71.6 KB of 350 KB. The first-load cost here is in the lazily loaded Studio chunk (StudioRouteloadsStudioDesignSurfacewithlazy()), pluswideViewport.tsnow importing two constants from the dependency-freeflow-canvas-layout.ts.overflow-x-autois deliberate (its comment: every pillar and Publish stay reachable), so a phone header is its own change, not this layout fix. Noted, not filed (no carrier).w-80) renders only while a field is selected, and at a phone width it would take most of the row. Same family as this card. Noted, not filed (no carrier).aiSlot(the cloud seam) keeps its classic three-zone layout with a fixed aside at every width; the comment there says the cloud edition migrates on its own schedule. Noted, not filed.packages/app-shell/README.md(the flow canvas "Navigate" line) andcontent/docs/guide/console.md(the Studio feature table) are outside the claim's file surface, so neither is edited here.t().Session:
https://claude.ai/code/session_01CGZy1BGCjdN5cXqL9cnvB8Generated by Claude Code