diff --git a/.changeset/25992-objectview-page-header-overlay-split.md b/.changeset/25992-objectview-page-header-overlay-split.md
new file mode 100644
index 00000000000..14a66af6d7c
--- /dev/null
+++ b/.changeset/25992-objectview-page-header-overlay-split.md
@@ -0,0 +1,12 @@
+---
+'@object-ui/app-shell': patch
+---
+
+refactor(app-shell): the Console `ObjectView`'s page header and record overlay move into sibling sub-components
+
+Clause-②: no
+Metadata-protocol: none
+
+Two blocks of `ObjectViewInner`'s JSX in `views/ObjectView.tsx` move verbatim into sub-components. The page header (title, managed-by badge, icon, create button, Import, the toolbar actions, the overflow menu and the folded face) moves into `ObjectViewPageHeader` in `objectViewPageHeader.tsx`. The record detail overlay drawn for every navigation mode but `split` moves into `ObjectViewRecordOverlay` in `objectViewRecordOverlay.tsx`. The component passes every value in as a prop. No logic, name or rendered output changes, and the package entry is unchanged.
+
+What to do: nothing.
diff --git a/ui/packages/app-shell/src/views/ObjectView.tsx b/ui/packages/app-shell/src/views/ObjectView.tsx
index 657bf3eea4a..f469dfe870d 100644
--- a/ui/packages/app-shell/src/views/ObjectView.tsx
+++ b/ui/packages/app-shell/src/views/ObjectView.tsx
@@ -9,7 +9,7 @@
* - ListView delegation for non-grid view types (kanban, calendar, chart, etc.)
*/
-import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense, createElement, type ReactNode } from 'react';
+import { useMemo, useState, useCallback, useEffect, useRef, lazy, Suspense, type ReactNode } from 'react';
import { useParams, useSearchParams, useNavigate, useLocation, useHref } from 'react-router-dom';
import { DENSITY_MODE_TO_ROW_HEIGHT, normalizeListViewSchema, resolveAffordance, type FilterTokenScope, type SchemaLike } from '@object-ui/core';
import { buildListFilterKey, writeListFilterState } from './listFilterStorage.js';
@@ -27,20 +27,14 @@ import { ObjectView as PluginObjectView, ManageViewsDialog } from '@object-ui/pl
// uses ComponentRegistry.registerLazy so heavy plugins stay code-split).
// Do NOT add eager `import '@object-ui/plugin-*'` side-effect imports here.
import {
- Button,
- DropdownMenu,
- DropdownMenuContent,
- DropdownMenuItem,
- DropdownMenuTrigger,
NavigationOverlay,
} from '@object-ui/components';
-import { Plus, Upload, MoreHorizontal, RotateCcw } from 'lucide-react';
-import { FavoriteMenuItem, useObjectListFavorite } from '../layout/SidebarItemMenu.js';
+import { Plus } from 'lucide-react';
+import { useObjectListFavorite } from '../layout/SidebarItemMenu.js';
import { useMetaItemRefusal } from '../hooks/useMetaItemRefusal.js';
import { useTenancyPosture } from '../hooks/useTenancyPosture.js';
import { useServedViewItems } from '../hooks/useServedViewItems.js';
import { viewTabLabel } from './viewTabLabel.js';
-import { getIcon } from '../utils/getIcon.js';
import type { DataSource, ListViewSchema, TreeViewConfig } from '@object-ui/types';
import { MetadataPanel, useMetadataInspector } from './MetadataInspector.js';
import { ViewConfigPanel } from './ViewConfigPanel.js';
@@ -53,10 +47,8 @@ import {
PREVIEW_QUERY_FLAG,
PREVIEW_QUERY_VALUE,
} from '../preview/PreviewModeContext.js';
-import { PageHeader } from '../layout/PageHeader.js';
import { useMobileViewSwitcherRegistration } from '../layout/MobileViewSwitcherContext.js';
import type { MobileViewSwitcherItem } from '../layout/MobileViewSwitcherContext.js';
-import { ManagedByBadge } from '../components/ManagedByBadge.js';
import { isSetupAppSegment } from '../console/setupApp.js';
import { RecordDetailView } from './RecordDetailView.js';
import { IDENTITY_IMPORT_WRITE_MODES } from './identityImport.js';
@@ -71,10 +63,9 @@ import { useObjectActions } from '../hooks/useObjectActions.js';
import { useObjectTranslation, useObjectLabel } from '@object-ui/i18n';
import { usePermissions } from '@object-ui/permissions';
import { useAuth, useWorkspaceAdminStatus } from '@object-ui/auth';
-import { ActionProvider, SchemaRenderer, useActionTextLocalizer, RelatedRecordActionsProvider, notifyDataChanged } from '@object-ui/react';
+import { ActionProvider, useActionTextLocalizer, RelatedRecordActionsProvider, notifyDataChanged } from '@object-ui/react';
import { toast } from 'sonner';
import { useConsoleActionRuntime } from '../hooks/useConsoleActionRuntime.js';
-import { EnvironmentListToolbar } from '../environment/EnvironmentListToolbar.js';
import {
VIEW_TYPE_ICONS,
FALLBACK_USER,
@@ -108,6 +99,8 @@ import { useObjectViewListUrlState, useObjectViewUrlFilters } from './objectView
import { useObjectViewTabBar } from './objectViewTabBar.js';
import { useObjectViewPluginSchema } from './objectViewPluginSchema.js';
import { useObjectViewHeaderChrome } from './objectViewHeaderChrome.js';
+import { ObjectViewPageHeader } from './objectViewPageHeader.js';
+import { ObjectViewRecordOverlay } from './objectViewRecordOverlay.js';
// objectstack#25992 slice 1: these module-level helpers moved to the sibling
// `objectView*.ts` modules above. Every name this file exported stays
@@ -1984,120 +1977,26 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
chrome that real-app conventions (Gmail / Notion / Linear)
reclaim for content. The create / import / overflow
actions move to a floating action button (see below). */}
-
- }
- subtitle={objectDef.description ? objectDesc(objectDef) : undefined}
- // `createElement`, not a JSX tag: the looked-up icon is a value,
- // which react-hooks/static-components refuses in tag position
- // (the convention ObjectDataPage set).
- icon={createElement(getIcon((objectDef as any)?.icon), { className: 'h-4 w-4' })}
- actions={
- <>
- {/* No favorite star here since objectui#12423: the one-row
- header is icon, title, description and these actions.
- The favorites entry is in the overflow menu at the end
- of this row (objectui#12427). */}
-
- {/* Primary action (see `createButton`). */}
- {createButton}
-
- {/* Data import — desktop only on phones. CSV imports
- are inherently a desk/laptop workflow; the button
- was eating header space on mobile next to the
- primary "+" action. */}
- {/* [#5142] `objectCanImport && importVisible` — the object-level
- verdict, then the toolbar-scope `visibleWhen` layer on top of
- it. Greyed, not gone, is the `disabledWhen` case below. */}
- {importShown && (
-
- )}
-
- {/* Schema-driven toolbar actions */}
- {listActions?.some((a: any) => a.locations?.includes('list_toolbar')) && (
- isEnvironmentList ? (
-
- ) : (
-
- )
- )}
-
- {/* The page's overflow menu (objectui#12427), last in the
- row: the favorites entry that replaced the header star.
- Favorites is a once-and-forget act, so it lives behind
- the ⋯ rather than in the header's most prominent slot.
- "Reset view" (objectui#12418) is the same kind of act and
- lives beside it, so the one-row header keeps its shape
- as the user sorts. The ⋯ is drawn when either applies. */}
- {(listFavorite || activeViewHasPreference) && (
-
-
-
-
-
- {listFavorite && (
-
- )}
- {/* objectui#12418 — the user's own list state for
- this view (sort, filters, column layout) back to
- the view's. Only while they have stored some. */}
- {activeViewHasPreference && (
-
-
- {t('console.objectView.resetView')}
-
- )}
-
-
- )}
-
- {/*
- Design Tools dropdown removed — its items were redundant:
- - "Edit view" → use chevron menu's "Edit view config"
- - "Add view" → use the `+` button on the tab bar or the "Add new view" footer in Manage Views
- - "Metadata inspector" → still toggleable via the `?__debug` URL flag (see useMetadataInspector)
- */}
- >
- }
- foldedActions={foldedHeaderActions}
+ {/* objectstack#25992 slice 10: the header moved to `./objectViewPageHeader.tsx`. */}
+
-
{/* Floating "+" action button — phone-only counterpart to the
PageHeader's primary create action we just hid. Positioned
@@ -2344,28 +2243,18 @@ function ObjectViewInner({ dataSource, objects, onEdit, externalRefreshKey }: Co
{/* Record Detail Overlay — navigation mode driven by objectDef.navigation */}
{navOverlay.mode !== 'split' && (
- { if (!open) handleDrawerClose(); }}
- title={objectLabel(objectDef)}
- onExpand={handleExpandDrawer}
- expandLabel={t('console.objectView.expandToPage', { defaultValue: 'Open as full page' })}
- storageKey={`drawer-width:${objectDef.name}`}
- >
- {(record: Record) => {
- const recordId = (record.id || record._id) as string;
- return (
-
- );
- }}
-
+ /* objectstack#25992 slice 10: the overlay moved to `./objectViewRecordOverlay.tsx`. */
+
)}
{actionRuntime.dialogs}
diff --git a/ui/packages/app-shell/src/views/objectViewPageHeader.tsx b/ui/packages/app-shell/src/views/objectViewPageHeader.tsx
new file mode 100644
index 00000000000..d10a02b9516
--- /dev/null
+++ b/ui/packages/app-shell/src/views/objectViewPageHeader.tsx
@@ -0,0 +1,187 @@
+/**
+ * The page header of the Console `ObjectView` (`./ObjectView.tsx`): the
+ * object's plural title, managed-by badge, description and icon, with the
+ * header's actions (the create button, Import, the schema-driven toolbar and
+ * the overflow menu with favorites and "Reset view") and their folded face,
+ * moved out of `ObjectViewInner`'s JSX verbatim (objectstack#25992, slice 10).
+ * Every value is passed in as a prop.
+ */
+
+import { createElement } from 'react';
+import type { Dispatch, SetStateAction } from 'react';
+import {
+ Button,
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from '@object-ui/components';
+import { Upload, MoreHorizontal, RotateCcw } from 'lucide-react';
+import { SchemaRenderer } from '@object-ui/react';
+import type { useObjectLabel, useObjectTranslation } from '@object-ui/i18n';
+import { FavoriteMenuItem } from '../layout/SidebarItemMenu.js';
+import type { useObjectListFavorite } from '../layout/SidebarItemMenu.js';
+import { getIcon } from '../utils/getIcon.js';
+import { PageHeader } from '../layout/PageHeader.js';
+import { ManagedByBadge } from '../components/ManagedByBadge.js';
+import { EnvironmentListToolbar } from '../environment/EnvironmentListToolbar.js';
+import type { useObjectViewToolbarAffordances } from './objectViewToolbarAffordances.js';
+import type { useObjectViewHeaderToolbar } from './objectViewHeaderToolbar.js';
+import type { useObjectViewListUrlState } from './objectViewListUrlState.js';
+import type { useObjectViewHeaderChrome } from './objectViewHeaderChrome.js';
+import type { ConsoleObjectViewProps } from './ObjectView.js';
+
+export function ObjectViewPageHeader({
+ objectDef,
+ objectPluralLabel,
+ objectDesc,
+ createButton,
+ importShown,
+ setShowImport,
+ importDisabled,
+ t,
+ listActions,
+ isEnvironmentList,
+ headerToolbarActions,
+ environmentEntitlements,
+ toolbarBar,
+ listFavorite,
+ activeViewHasPreference,
+ handleResetView,
+ foldedHeaderActions,
+}: {
+ objectDef: ConsoleObjectViewProps['objects'][number];
+ objectPluralLabel: ReturnType['objectPluralLabel'];
+ objectDesc: ReturnType['objectDescription'];
+ createButton: ReturnType['createButton'];
+ importShown: ReturnType['importShown'];
+ setShowImport: Dispatch>;
+ importDisabled: ReturnType['importDisabled'];
+ t: ReturnType['t'];
+ listActions: ReturnType['listActions'];
+ isEnvironmentList: ReturnType['isEnvironmentList'];
+ headerToolbarActions: ReturnType['headerToolbarActions'];
+ environmentEntitlements: ReturnType['environmentEntitlements'];
+ toolbarBar: ReturnType['toolbarBar'];
+ listFavorite: ReturnType;
+ activeViewHasPreference: ReturnType['activeViewHasPreference'];
+ handleResetView: ReturnType['handleResetView'];
+ foldedHeaderActions: ReturnType['foldedHeaderActions'];
+}) {
+ return (
+
+ }
+ subtitle={objectDef.description ? objectDesc(objectDef) : undefined}
+ // `createElement`, not a JSX tag: the looked-up icon is a value,
+ // which react-hooks/static-components refuses in tag position
+ // (the convention ObjectDataPage set).
+ icon={createElement(getIcon((objectDef as any)?.icon), { className: 'h-4 w-4' })}
+ actions={
+ <>
+ {/* No favorite star here since objectui#12423: the one-row
+ header is icon, title, description and these actions.
+ The favorites entry is in the overflow menu at the end
+ of this row (objectui#12427). */}
+
+ {/* Primary action (see `createButton`). */}
+ {createButton}
+
+ {/* Data import — desktop only on phones. CSV imports
+ are inherently a desk/laptop workflow; the button
+ was eating header space on mobile next to the
+ primary "+" action. */}
+ {/* [#5142] `objectCanImport && importVisible` — the object-level
+ verdict, then the toolbar-scope `visibleWhen` layer on top of
+ it. Greyed, not gone, is the `disabledWhen` case below. */}
+ {importShown && (
+
+ )}
+
+ {/* Schema-driven toolbar actions */}
+ {listActions?.some((a: any) => a.locations?.includes('list_toolbar')) && (
+ isEnvironmentList ? (
+
+ ) : (
+
+ )
+ )}
+
+ {/* The page's overflow menu (objectui#12427), last in the
+ row: the favorites entry that replaced the header star.
+ Favorites is a once-and-forget act, so it lives behind
+ the ⋯ rather than in the header's most prominent slot.
+ "Reset view" (objectui#12418) is the same kind of act and
+ lives beside it, so the one-row header keeps its shape
+ as the user sorts. The ⋯ is drawn when either applies. */}
+ {(listFavorite || activeViewHasPreference) && (
+
+
+
+
+
+ {listFavorite && (
+
+ )}
+ {/* objectui#12418 — the user's own list state for
+ this view (sort, filters, column layout) back to
+ the view's. Only while they have stored some. */}
+ {activeViewHasPreference && (
+
+
+ {t('console.objectView.resetView')}
+
+ )}
+
+
+ )}
+
+ {/*
+ Design Tools dropdown removed — its items were redundant:
+ - "Edit view" → use chevron menu's "Edit view config"
+ - "Add view" → use the `+` button on the tab bar or the "Add new view" footer in Manage Views
+ - "Metadata inspector" → still toggleable via the `?__debug` URL flag (see useMetadataInspector)
+ */}
+ >
+ }
+ foldedActions={foldedHeaderActions}
+ />
+
+ );
+}
diff --git a/ui/packages/app-shell/src/views/objectViewRecordOverlay.tsx b/ui/packages/app-shell/src/views/objectViewRecordOverlay.tsx
new file mode 100644
index 00000000000..e62bd702ba5
--- /dev/null
+++ b/ui/packages/app-shell/src/views/objectViewRecordOverlay.tsx
@@ -0,0 +1,61 @@
+/**
+ * The record detail overlay of the Console `ObjectView` (`./ObjectView.tsx`)
+ * for every navigation mode but `split` (drawer, modal, popover): the
+ * `NavigationOverlay` that draws the opened record's `RecordDetailView`, moved
+ * out of `ObjectViewInner`'s JSX verbatim (objectstack#25992, slice 10). The
+ * `split` face stays in the component, where it wraps the list itself. Every
+ * value is passed in as a prop.
+ */
+
+import { NavigationOverlay } from '@object-ui/components';
+import type { useObjectLabel, useObjectTranslation } from '@object-ui/i18n';
+import { RecordDetailView } from './RecordDetailView.js';
+import type { useObjectViewRecordNavigation } from './objectViewRecordNavigation.js';
+import type { ConsoleObjectViewProps } from './ObjectView.js';
+
+export function ObjectViewRecordOverlay({
+ navOverlay,
+ handleDrawerClose,
+ objectLabel,
+ objectDef,
+ handleExpandDrawer,
+ t,
+ dataSource,
+ objects,
+ onEdit,
+}: {
+ navOverlay: ReturnType['navOverlay'];
+ handleDrawerClose: ReturnType['handleDrawerClose'];
+ objectLabel: ReturnType['objectLabel'];
+ objectDef: ConsoleObjectViewProps['objects'][number];
+ handleExpandDrawer: ReturnType['handleExpandDrawer'];
+ t: ReturnType['t'];
+ dataSource: ConsoleObjectViewProps['dataSource'];
+ objects: ConsoleObjectViewProps['objects'];
+ onEdit: ConsoleObjectViewProps['onEdit'];
+}) {
+ return (
+ { if (!open) handleDrawerClose(); }}
+ title={objectLabel(objectDef)}
+ onExpand={handleExpandDrawer}
+ expandLabel={t('console.objectView.expandToPage', { defaultValue: 'Open as full page' })}
+ storageKey={`drawer-width:${objectDef.name}`}
+ >
+ {(record: Record) => {
+ const recordId = (record.id || record._id) as string;
+ return (
+
+ );
+ }}
+
+ );
+}