Skip to content

Commit 3a64069

Browse files
fix(mobile): the iPad inspector hides the sidebar where the page would get too narrow
On iOS 26 the native inspector column opened beside the sidebar at any width, so a portrait 13" iPad left the thread about 290pt. The JS columns already step the sidebar aside when the page would drop under 560pt; the native path was exempt because UIKit sizes its inspector column on its own. The shell now owns the native inspector's width and applies the same rule with a 320pt floor: landscape 11" and 13" iPads keep three columns, portrait iPads and the landscape mini show the thread and inspector, and the sidebar button closes the inspector to bring the sidebar back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 124a394 commit 3a64069

5 files changed

Lines changed: 65 additions & 41 deletions

File tree

‎apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ import {
4646
deriveWorkspacePaneLayout,
4747
isPrimarySidebarPinned,
4848
isWorkspaceListPath,
49+
nativeInspectorColumnWidth,
4950
} from "../../lib/layout";
5051
import type { ThreadListFilters } from "../../persistence/mobile-preferences";
5152
import { appAtomRegistry } from "../../state/atom-registry";
@@ -228,6 +229,11 @@ function AdaptiveWorkspaceLayoutContent(
228229
const navigation = useNavigation();
229230
const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true);
230231
const [fileInspectorPreferredVisible, setFileInspectorPreferredVisible] = useState(true);
232+
const [nativeInspectorWidthFraction, setNativeInspectorWidthFraction] = useState(0.4);
233+
const nativeInspectorWidth = nativeInspectorColumnWidth(
234+
viewportWidth,
235+
nativeInspectorWidthFraction,
236+
);
231237
const [fileInspectorPreferredWidth, setFileInspectorPreferredWidth] = useState<number | null>(
232238
null,
233239
);
@@ -376,8 +382,10 @@ function AdaptiveWorkspaceLayoutContent(
376382
auxiliaryPanePreferredVisible:
377383
!workspacePage && inspectorActive && fileInspectorPreferredVisible,
378384
auxiliaryPanePreferredWidth: fileInspectorPreferredWidth ?? undefined,
385+
nativeInspectorWidth,
379386
}),
380387
[
388+
nativeInspectorWidth,
381389
fileInspectorPreferredVisible,
382390
fileInspectorPreferredWidth,
383391
inspectorActive,
@@ -805,6 +813,11 @@ function AdaptiveWorkspaceLayoutContent(
805813
setPrimarySidebarVisible,
806814
openWorkspaceTab,
807815
setAuxiliaryPaneWidth: setFileInspectorPreferredWidth,
816+
nativeInspectorWidth,
817+
setNativeInspectorWidth: (width: number) =>
818+
setNativeInspectorWidthFraction(
819+
nativeInspectorColumnWidth(viewportWidth, width / viewportWidth) / viewportWidth,
820+
),
808821
composerKeyboardActive,
809822
setComposerEditing,
810823
sidebarByOrientation,

‎apps/mobile/src/features/layout/adaptive-workspace-context.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ interface AdaptiveWorkspaceContextValue {
4545
options?: { readonly columns?: boolean },
4646
) => void;
4747
readonly setAuxiliaryPaneWidth: (width: number) => void;
48+
readonly nativeInspectorWidth: number;
49+
readonly setNativeInspectorWidth: (width: number) => void;
4850
/**
4951
* Android split view while the thread composer has the soft keyboard: the
5052
* sidebar and rail step aside and the thread header drops to one line.
@@ -86,6 +88,8 @@ export const AdaptiveWorkspaceContext = createContext<AdaptiveWorkspaceContextVa
8688
setPrimarySidebarVisible: () => undefined,
8789
openWorkspaceTab: () => undefined,
8890
setAuxiliaryPaneWidth: () => undefined,
91+
nativeInspectorWidth: 0,
92+
setNativeInspectorWidth: () => undefined,
8993
composerKeyboardActive: false,
9094
sidebarByOrientation: null,
9195
setComposerEditing: () => undefined,

‎apps/mobile/src/features/layout/createWorkspaceStackNavigator.ios.tsx‎

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import {
2020
type NativeStackTypeBag,
2121
} from "@react-navigation/native-stack";
2222
import { Fragment, use, useEffect, useRef, useState, type ComponentProps } from "react";
23-
import { StyleSheet, useWindowDimensions, View } from "react-native";
23+
import { StyleSheet, View } from "react-native";
2424
import { Split, type SplitHostCommands } from "react-native-screens";
2525
import { SafeAreaView } from "react-native-safe-area-context";
2626
import { GestureHandlerRootView } from "react-native-gesture-handler";
@@ -194,16 +194,11 @@ function WorkspaceColumns(
194194
startNewTask,
195195
openWorkspaceTab,
196196
sidebarByOrientation,
197+
nativeInspectorWidth,
198+
setNativeInspectorWidth,
197199
} = useAdaptiveWorkspaceLayout();
198200
const deviceType = useDeviceType();
199-
const windowDimensions = useWindowDimensions();
200-
const nativeMetrics = useNativeLayoutMetrics();
201-
const viewportWidth = nativeMetrics?.width ?? windowDimensions.width;
202-
const [inspectorWidthFraction, setInspectorWidthFraction] = useState(0.4);
203-
const inspectorWidth = Math.max(
204-
260,
205-
Math.min(viewportWidth * 0.6, viewportWidth * inspectorWidthFraction),
206-
);
201+
const inspectorWidth = nativeInspectorWidth;
207202
const resizeStartWidth = useRef(inspectorWidth);
208203
const settingsDetailIn = (detail: readonly Route[]) =>
209204
layout.usesSplitView && detail[0] && isSettingsRoute(detail[0].name) ? detail[0] : undefined;
@@ -470,13 +465,9 @@ function WorkspaceColumns(
470465
onResizeStart={() => {
471466
resizeStartWidth.current = inspectorWidth;
472467
}}
473-
onResizeBy={(delta) => {
474-
const width = Math.max(
475-
260,
476-
Math.min(viewportWidth * 0.6, resizeStartWidth.current + delta),
477-
);
478-
setInspectorWidthFraction(width / viewportWidth);
479-
}}
468+
onResizeBy={(delta) =>
469+
setNativeInspectorWidth(resizeStartWidth.current + delta)
470+
}
480471
/>
481472
</View>
482473
</View>

‎apps/mobile/src/lib/layout.test.ts‎

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
deriveThreadArrangementSizing,
88
deriveThreadWorkLogSizing,
99
deriveWorkspacePaneLayout,
10+
nativeInspectorColumnWidth,
1011
isPrimarySidebarPinned,
1112
} from "./layout";
1213

@@ -153,24 +154,31 @@ describe("isPrimarySidebarPinned", () => {
153154
});
154155

155156
describe("deriveWorkspacePaneLayout", () => {
156-
it("lets UIKit keep the sidebar independent of the inspector in landscape", () => {
157-
const layout = deriveLayout({ width: 1_194, height: 834 });
158-
for (const primarySidebarPreferredVisible of [true, false]) {
159-
expect(
160-
deriveWorkspacePaneLayout({
161-
layout,
162-
viewportWidth: 1_194,
163-
nativeWorkspace: true,
164-
primarySidebarPreferredVisible,
165-
auxiliaryPanePreferredVisible: true,
166-
}),
167-
).toMatchObject({
168-
primarySidebarVisible: primarySidebarPreferredVisible,
169-
primarySidebarSuppressedByAuxiliary: false,
170-
auxiliaryPaneVisible: true,
171-
});
172-
}
173-
});
157+
it.each([
158+
{ width: 1_194, height: 834, suppressed: false },
159+
{ width: 1_032, height: 1_376, suppressed: true },
160+
])(
161+
"steps the sidebar aside for the native inspector only when the page gets too narrow ($width wide)",
162+
({ width, height, suppressed }) => {
163+
const layout = deriveLayout({ width, height });
164+
for (const primarySidebarPreferredVisible of [true, false]) {
165+
expect(
166+
deriveWorkspacePaneLayout({
167+
layout,
168+
viewportWidth: width,
169+
nativeWorkspace: true,
170+
primarySidebarPreferredVisible,
171+
auxiliaryPanePreferredVisible: true,
172+
nativeInspectorWidth: nativeInspectorColumnWidth(width, 0.4),
173+
}),
174+
).toMatchObject({
175+
primarySidebarVisible: primarySidebarPreferredVisible && !suppressed,
176+
primarySidebarSuppressedByAuxiliary: primarySidebarPreferredVisible && suppressed,
177+
auxiliaryPaneVisible: true,
178+
});
179+
}
180+
},
181+
);
174182

175183
it("keeps an explicitly hidden thread sidebar hidden when the file inspector is visible", () => {
176184
const layout = deriveLayout({ width: 1_024, height: 1_366 });

‎apps/mobile/src/lib/layout.ts‎

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ const AUXILIARY_PANE_MAX_WIDTH = 480;
5858
const AUXILIARY_PANE_DEFAULT_MAX_WIDTH = 320;
5959
const FILE_INSPECTOR_MIN_VIEWPORT_WIDTH = 820;
6060
const FILE_INSPECTOR_MIN_MAIN_WIDTH = 560;
61+
// UIKit's inspector column keeps its own width, so the sidebar steps aside only when the page
62+
// would get narrower than this: landscape 11" and 13" iPads keep three columns, portrait ones don't.
63+
const NATIVE_INSPECTOR_MIN_MAIN_WIDTH = 320;
6164

6265
/**
6366
* The window's width class. Compact: the sidebar is the root and pages push over it. Medium: the
@@ -221,6 +224,8 @@ export function deriveWorkspacePaneLayout(input: {
221224
readonly primarySidebarPreferredVisible: boolean;
222225
readonly auxiliaryPanePreferredVisible: boolean;
223226
readonly auxiliaryPanePreferredWidth?: number;
227+
/** The native inspector column's width; UIKit lays it out, not this layout. */
228+
readonly nativeInspectorWidth?: number;
224229
}): WorkspacePaneLayout {
225230
const viewportWidth = Math.max(0, input.viewportWidth);
226231
const floats = input.primarySidebarFloats ?? input.layout.variant === "medium";
@@ -237,18 +242,17 @@ export function deriveWorkspacePaneLayout(input: {
237242
reservedLeadingWidth: preferredPrimarySidebarWidth,
238243
});
239244
const auxiliaryPaneVisible = fileInspector.supported && input.auxiliaryPanePreferredVisible;
245+
const inspectorWidth = input.nativeWorkspace
246+
? (input.nativeInspectorWidth ?? null)
247+
: fileInspector.width;
240248
const primarySidebarSuppressedByAuxiliary =
241-
!input.nativeWorkspace &&
242249
preferredPrimarySidebarVisible &&
243250
!floats &&
244251
auxiliaryPaneVisible &&
245-
fileInspector.width !== null &&
252+
inspectorWidth !== null &&
246253
input.layout.listPaneWidth !== null &&
247-
viewportWidth -
248-
input.layout.listPaneWidth -
249-
(input.layout.listPaneGap ?? 0) -
250-
fileInspector.width <
251-
FILE_INSPECTOR_MIN_MAIN_WIDTH;
254+
viewportWidth - input.layout.listPaneWidth - (input.layout.listPaneGap ?? 0) - inspectorWidth <
255+
(input.nativeWorkspace ? NATIVE_INSPECTOR_MIN_MAIN_WIDTH : FILE_INSPECTOR_MIN_MAIN_WIDTH);
252256
const primarySidebarVisible =
253257
preferredPrimarySidebarVisible && !primarySidebarSuppressedByAuxiliary;
254258
const primarySidebarWidth = primarySidebarVisible ? preferredPrimarySidebarWidth : 0;
@@ -264,6 +268,10 @@ export function deriveWorkspacePaneLayout(input: {
264268
};
265269
}
266270

271+
export function nativeInspectorColumnWidth(viewportWidth: number, fraction: number) {
272+
return Math.max(260, Math.min(viewportWidth * 0.6, viewportWidth * fraction));
273+
}
274+
267275
export function deriveFileInspectorPaneLayout(input: {
268276
readonly layout: Layout;
269277
readonly viewportWidth: number;

0 commit comments

Comments
 (0)