| author | |
| committer | |
| log | 29a23fdf1481485593d7d90e195b7c8bf2a705a8 |
| tree | de38807a6b63ea7b761333eba8701112f501867c |
| parent | 803f94e787faef498edecba5dbd07493ac8bc817 |
| signature | Signed by PGP key B5690EEEBB952194 |
12 files changed, 109 insertions(+), 124 deletions(-)
apps/mobile/src/Stack.tsx+38-23| ... | @@ -11,7 +11,14 @@ import { | ... | @@ -11,7 +11,14 @@ import { |
| 11 | type NativeStackNavigationOptions, | 11 | type NativeStackNavigationOptions, |
| 12 | } from "@react-navigation/native-stack"; | 12 | } from "@react-navigation/native-stack"; |
| 13 | import { useEffect, useRef } from "react"; | 13 | import { useEffect, useRef } from "react"; |
| 14 | import { Platform, Pressable, ScrollView, StyleSheet, View } from "react-native"; | 14 | import { |
| 15 | Platform, | ||
| 16 | Pressable, | ||
| 17 | ScrollView, | ||
| 18 | StyleSheet, | ||
| 19 | View, | ||
| 20 | useWindowDimensions, | ||
| 21 | } from "react-native"; | ||
| 15 | import { useResolveClassNames } from "uniwind"; | 22 | import { useResolveClassNames } from "uniwind"; |
| 16 | 23 | ||
| 17 | import { AppText as Text } from "./components/AppText"; | 24 | import { AppText as Text } from "./components/AppText"; |
| ... | @@ -93,6 +100,7 @@ import { | ... | @@ -93,6 +100,7 @@ import { |
| 93 | transitionIncomingSharePresentation, | 100 | transitionIncomingSharePresentation, |
| 94 | } from "./features/sharing/incoming-share-presentation"; | 101 | } from "./features/sharing/incoming-share-presentation"; |
| 95 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass"; | 102 | import { NATIVE_LIQUID_GLASS_SUPPORTED } from "./native/native-glass"; |
| 103 | import { deriveLayout } from "./lib/layout"; | ||
| 96 | import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; | 104 | import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; |
| 97 | import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface"; | 105 | import { FORM_SHEET_PRESENTATION_OPTIONS } from "./native/sheet-surface"; |
| 98 | import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; | 106 | import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; |
| ... | @@ -339,9 +347,8 @@ const SettingsSheetStack = createNativeStackNavigator({ | ... | @@ -339,9 +347,8 @@ const SettingsSheetStack = createNativeStackNavigator({ |
| 339 | // the same deep-link URLs the nested config produced. | 347 | // the same deep-link URLs the nested config produced. |
| 340 | const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId"; | 348 | const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId"; |
| 341 | 349 | ||
| 342 | // New-task / add-project flow: nested navigator inside the formSheet (Settings-sheet | 350 | // New-task / add-project flow: the nested navigator owns the header and pushes |
| 343 | // pattern — a plain formSheet screen cannot render a stack header; the header and | 351 | // whether the flow opens in the workspace or in a compact form sheet. |
| 344 | // in-sheet pushes come from this nested stack). | ||
| 345 | const NewTaskSheetStack = createNativeStackNavigator({ | 352 | const NewTaskSheetStack = createNativeStackNavigator({ |
| 346 | initialRouteName: "NewTask", | 353 | initialRouteName: "NewTask", |
| 347 | screenOptions: { | 354 | screenOptions: { |
| ... | @@ -570,7 +577,7 @@ function NotFoundScreen() { | ... | @@ -570,7 +577,7 @@ function NotFoundScreen() { |
| 570 | ); | 577 | ); |
| 571 | } | 578 | } |
| 572 | 579 | ||
| 573 | export const RootStack = createNativeStackNavigator({ | 580 | const RootStackConfig = createNativeStackNavigator({ |
| 574 | initialRouteName: "Home", | 581 | initialRouteName: "Home", |
| 575 | layout: RootStackLayout, | 582 | layout: RootStackLayout, |
| 576 | screenOptions: { | 583 | screenOptions: { |
| ... | @@ -698,15 +705,6 @@ export const RootStack = createNativeStackNavigator({ | ... | @@ -698,15 +705,6 @@ export const RootStack = createNativeStackNavigator({ |
| 698 | options: { | 705 | options: { |
| 699 | gestureEnabled: true, | 706 | gestureEnabled: true, |
| 700 | headerShown: false, | 707 | headerShown: false, |
| 701 | // Android pushes settings as a regular full page with an in-screen | ||
| 702 | // back header; iOS keeps the detented form sheet. | ||
| 703 | ...(Platform.OS === "android" | ||
| 704 | ? { presentation: "card" as const } | ||
| 705 | : { | ||
| 706 | ...FORM_SHEET_PRESENTATION_OPTIONS, | ||
| 707 | sheetAllowedDetents: [0.92], | ||
| 708 | sheetGrabberVisible: true, | ||
| 709 | }), | ||
| 710 | }, | 708 | }, |
| 711 | }), | 709 | }), |
| 712 | SettingsLegal: createNativeStackScreen({ | 710 | SettingsLegal: createNativeStackScreen({ |
| ... | @@ -770,15 +768,6 @@ export const RootStack = createNativeStackNavigator({ | ... | @@ -770,15 +768,6 @@ export const RootStack = createNativeStackNavigator({ |
| 770 | options: { | 768 | options: { |
| 771 | gestureEnabled: true, | 769 | gestureEnabled: true, |
| 772 | headerShown: false, | 770 | headerShown: false, |
| 773 | // Android pushes the flow as a regular full page — the draft should | ||
| 774 | // read like a thread that just doesn't exist yet; iOS keeps the sheet. | ||
| 775 | ...(Platform.OS === "android" | ||
| 776 | ? { presentation: "card" as const } | ||
| 777 | : { | ||
| 778 | ...FORM_SHEET_PRESENTATION_OPTIONS, | ||
| 779 | sheetAllowedDetents: [0.92], | ||
| 780 | sheetGrabberVisible: true, | ||
| 781 | }), | ||
| 782 | }, | 771 | }, |
| 783 | }), | 772 | }), |
| 784 | NotFound: createNativeStackScreen({ | 773 | NotFound: createNativeStackScreen({ |
| ... | @@ -787,6 +776,32 @@ export const RootStack = createNativeStackNavigator({ | ... | @@ -787,6 +776,32 @@ export const RootStack = createNativeStackNavigator({ |
| 787 | }), | 776 | }), |
| 788 | }, | 777 | }, |
| 789 | }); | 778 | }); |
| 779 | |||
| 780 | export const RootStack = RootStackConfig.with(function AdaptiveRootStack({ Navigator }) { | ||
| 781 | const { width, height } = useWindowDimensions(); | ||
| 782 | const usesWorkspaceFlowScreens = | ||
| 783 | Platform.OS === "android" || deriveLayout({ width, height }).usesSplitView; | ||
| 784 | |||
| 785 | return ( | ||
| 786 | <Navigator | ||
| 787 | screenOptions={({ route }) => { | ||
| 788 | if (route.name !== "SettingsSheet" && route.name !== "NewTaskSheet") { | ||
| 789 | return {}; | ||
| 790 | } | ||
| 791 | |||
| 792 | // Follow the workspace viewport as it resizes; compact iOS keeps sheets. | ||
| 793 | return usesWorkspaceFlowScreens | ||
| 794 | ? { presentation: "card" } | ||
| 795 | : { | ||
| 796 | ...FORM_SHEET_PRESENTATION_OPTIONS, | ||
| 797 | sheetAllowedDetents: [0.92], | ||
| 798 | sheetGrabberVisible: true, | ||
| 799 | }; | ||
| 800 | }} | ||
| 801 | /> | ||
| 802 | ); | ||
| 803 | }); | ||
| 804 | |||
| 790 | type RootStackType = typeof RootStack; | 805 | type RootStackType = typeof RootStack; |
| 791 | 806 | ||
| 792 | const navigationPathConfig = { | 807 | const navigationPathConfig = { |
apps/mobile/src/components/AndroidScreenHeader.tsx+1-7| ... | @@ -25,13 +25,7 @@ export function AndroidHeaderIconButton(props: { | ... | @@ -25,13 +25,7 @@ export function AndroidHeaderIconButton(props: { |
| 25 | readonly disabled?: boolean; | 25 | readonly disabled?: boolean; |
| 26 | readonly selected?: boolean; | 26 | readonly selected?: boolean; |
| 27 | }) { | 27 | }) { |
| 28 | return ( | 28 | return <MaterialIconButton {...props} tintColorClassName="accent-header-foreground" />; |
| 29 | <MaterialIconButton | ||
| 30 | {...props} | ||
| 31 | tintColorClassName="accent-header-foreground" | ||
| 32 | variant={props.selected ? "tonal" : "standard"} | ||
| 33 | /> | ||
| 34 | ); | ||
| 35 | } | 29 | } |
| 36 | 30 | ||
| 37 | export function AndroidScreenHeader(props: { | 31 | export function AndroidScreenHeader(props: { |
apps/mobile/src/components/AppSymbol.tsx+2-2| ... | @@ -19,7 +19,7 @@ import IconArrowUp from "@tabler/icons-react-native/IconArrowUp"; | ... | @@ -19,7 +19,7 @@ import IconArrowUp from "@tabler/icons-react-native/IconArrowUp"; |
| 19 | import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle"; | 19 | import IconArrowUpCircle from "@tabler/icons-react-native/IconArrowUpCircle"; |
| 20 | import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight"; | 20 | import IconArrowUpRight from "@tabler/icons-react-native/IconArrowUpRight"; |
| 21 | import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle"; | 21 | import IconArrowUpRightCircle from "@tabler/icons-react-native/IconArrowUpRightCircle"; |
| 22 | import IconArrowsMaximize from "@tabler/icons-react-native/IconArrowsMaximize"; | 22 | import IconArrowsDiagonal2 from "@tabler/icons-react-native/IconArrowsDiagonal2"; |
| 23 | import IconArrowsMinimize from "@tabler/icons-react-native/IconArrowsMinimize"; | 23 | import IconArrowsMinimize from "@tabler/icons-react-native/IconArrowsMinimize"; |
| 24 | import IconBellRinging from "@tabler/icons-react-native/IconBellRinging"; | 24 | import IconBellRinging from "@tabler/icons-react-native/IconBellRinging"; |
| 25 | import IconBolt from "@tabler/icons-react-native/IconBolt"; | 25 | import IconBolt from "@tabler/icons-react-native/IconBolt"; |
| ... | @@ -122,7 +122,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = { | ... | @@ -122,7 +122,7 @@ const ANDROID_ICON_BY_SF_SYMBOL = { |
| 122 | "arrow.turn.left.up": IconArrowBackUp, | 122 | "arrow.turn.left.up": IconArrowBackUp, |
| 123 | "arrow.up": IconArrowUp, | 123 | "arrow.up": IconArrowUp, |
| 124 | "arrow.up.circle": IconArrowUpCircle, | 124 | "arrow.up.circle": IconArrowUpCircle, |
| 125 | "arrow.up.left.and.arrow.down.right": IconArrowsMaximize, | 125 | "arrow.up.left.and.arrow.down.right": IconArrowsDiagonal2, |
| 126 | "arrow.down.right.and.arrow.up.left": IconArrowsMinimize, | 126 | "arrow.down.right.and.arrow.up.left": IconArrowsMinimize, |
| 127 | "arrow.up.right": IconArrowUpRight, | 127 | "arrow.up.right": IconArrowUpRight, |
| 128 | "arrow.up.right.circle": IconArrowUpRightCircle, | 128 | "arrow.up.right.circle": IconArrowUpRightCircle, |
apps/mobile/src/components/ScreenHeader.tsx+17-13| ... | @@ -103,7 +103,7 @@ export function ScreenHeader(props: ScreenHeaderProps) { | ... | @@ -103,7 +103,7 @@ export function ScreenHeader(props: ScreenHeaderProps) { |
| 103 | ...props.options, | 103 | ...props.options, |
| 104 | }} | 104 | }} |
| 105 | /> | 105 | /> |
| 106 | {props.sidebar !== false && layout.usesSplitView ? ( | 106 | {layout.usesSplitView && (props.sidebar !== false || props.backInSplitView) ? ( |
| 107 | <NativeHeaderToolbar placement="left"> | 107 | <NativeHeaderToolbar placement="left"> |
| 108 | {props.backInSplitView && (props.backInSplitView.onPress || props.onBack) ? ( | 108 | {props.backInSplitView && (props.backInSplitView.onPress || props.onBack) ? ( |
| 109 | <ScreenHeaderButton | 109 | <ScreenHeaderButton |
| ... | @@ -111,18 +111,22 @@ export function ScreenHeader(props: ScreenHeaderProps) { | ... | @@ -111,18 +111,22 @@ export function ScreenHeader(props: ScreenHeaderProps) { |
| 111 | onPress={props.backInSplitView.onPress ?? props.onBack} | 111 | onPress={props.backInSplitView.onPress ?? props.onBack} |
| 112 | /> | 112 | /> |
| 113 | ) : null} | 113 | ) : null} |
| 114 | <ScreenHeaderButton | 114 | {props.sidebar !== false ? ( |
| 115 | accessibilityLabel={ | 115 | <ScreenHeaderButton |
| 116 | panes.primarySidebarVisible ? `Maximize ${props.title.toLowerCase()}` : "Show threads" | 116 | accessibilityLabel={ |
| 117 | } | 117 | panes.primarySidebarVisible |
| 118 | icon={ | 118 | ? `Maximize ${props.title.toLowerCase()}` |
| 119 | panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left" | 119 | : "Show threads" |
| 120 | } | 120 | } |
| 121 | onPress={togglePrimarySidebar} | 121 | icon={ |
| 122 | separateBackground={ | 122 | panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left" |
| 123 | props.backInSplitView ? props.backInSplitView.separateBackground : true | 123 | } |
| 124 | } | 124 | onPress={togglePrimarySidebar} |
| 125 | /> | 125 | separateBackground={ |
| 126 | props.backInSplitView ? props.backInSplitView.separateBackground : true | ||
| 127 | } | ||
| 128 | /> | ||
| 129 | ) : null} | ||
| 126 | </NativeHeaderToolbar> | 130 | </NativeHeaderToolbar> |
| 127 | ) : null} | 131 | ) : null} |
| 128 | {(props.actions?.length || | 132 | {(props.actions?.length || |
apps/mobile/src/features/home/HomeRouteScreen.tsx+4-10| ... | @@ -11,10 +11,6 @@ import { useWorkspaceState } from "../../state/workspace"; | ... | @@ -11,10 +11,6 @@ import { useWorkspaceState } from "../../state/workspace"; |
| 11 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; | 11 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 12 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; | 12 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; |
| 13 | import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail"; | 13 | import { WorkspaceEmptyDetail } from "../layout/WorkspaceEmptyDetail"; |
| 14 | import { | ||
| 15 | AndroidWorkspaceSidebarButton, | ||
| 16 | WorkspaceSidebarToolbar, | ||
| 17 | } from "../layout/workspace-sidebar-toolbar"; | ||
| 18 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; | 14 | import { AndroidScreenHeader } from "../../components/AndroidScreenHeader"; |
| 19 | import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates"; | 15 | import { checkForAppUpdateOnLaunch, startAppUpdateForegroundRecheck } from "../updates/app-updates"; |
| 20 | import { AndroidHomeFabLayout } from "./AndroidHomeFab"; | 16 | import { AndroidHomeFabLayout } from "./AndroidHomeFab"; |
| ... | @@ -121,18 +117,16 @@ export function HomeRouteScreen() { | ... | @@ -121,18 +117,16 @@ export function HomeRouteScreen() { |
| 121 | : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] } | 117 | : { title: "", headerTitle: "", unstable_headerLeftItems: () => [] } |
| 122 | } | 118 | } |
| 123 | /> | 119 | /> |
| 124 | <WorkspaceSidebarToolbar | 120 | {Platform.OS === "ios" ? ( |
| 125 | afterSidebarButton={ | 121 | <NativeHeaderToolbar placement="left"> |
| 126 | <NativeHeaderToolbar.Button | 122 | <NativeHeaderToolbar.Button |
| 127 | accessibilityLabel="New task" | 123 | accessibilityLabel="New task" |
| 128 | icon="square.and.pencil" | 124 | icon="square.and.pencil" |
| 129 | onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} | 125 | onPress={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} |
| 130 | /> | 126 | /> |
| 131 | } | 127 | </NativeHeaderToolbar> |
| 132 | /> | ||
| 133 | {Platform.OS === "android" ? ( | ||
| 134 | <AndroidScreenHeader title="Threads" leading={<AndroidWorkspaceSidebarButton />} /> | ||
| 135 | ) : null} | 128 | ) : null} |
| 129 | {Platform.OS === "android" ? <AndroidScreenHeader title="Threads" /> : null} | ||
| 136 | <WorkspaceEmptyDetail | 130 | <WorkspaceEmptyDetail |
| 137 | onAddConnection={ | 131 | onAddConnection={ |
| 138 | Platform.OS === "android" && !catalogState.hasConnections | 132 | Platform.OS === "android" && !catalogState.hasConnections |
apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx+14-14| ... | @@ -238,6 +238,7 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -238,6 +238,7 @@ function AdaptiveWorkspaceLayoutContent( |
| 238 | const navigation = useNavigation(); | 238 | const navigation = useNavigation(); |
| 239 | const activeRoleOwner = useRef<symbol | null>(null); | 239 | const activeRoleOwner = useRef<symbol | null>(null); |
| 240 | const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); | 240 | const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); |
| 241 | const showPrimarySidebar = pathname === "/" || primarySidebarPreferredVisible; | ||
| 241 | const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); | 242 | const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); |
| 242 | const [supplementaryPanePreferredWidth, setSupplementaryPanePreferredWidth] = useState< | 243 | const [supplementaryPanePreferredWidth, setSupplementaryPanePreferredWidth] = useState< |
| 243 | number | null | 244 | number | null |
| ... | @@ -262,17 +263,9 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -262,17 +263,9 @@ function AdaptiveWorkspaceLayoutContent( |
| 262 | viewportWidth: width, | 263 | viewportWidth: width, |
| 263 | preferredWidth: fileInspectorPreferredWidth ?? undefined, | 264 | preferredWidth: fileInspectorPreferredWidth ?? undefined, |
| 264 | reservedLeadingWidth: | 265 | reservedLeadingWidth: |
| 265 | shouldRenderPrimarySidebar && primarySidebarPreferredVisible | 266 | shouldRenderPrimarySidebar && showPrimarySidebar ? (layout.listPaneWidth ?? 0) : 0, |
| 266 | ? (layout.listPaneWidth ?? 0) | ||
| 267 | : 0, | ||
| 268 | }), | 267 | }), |
| 269 | [ | 268 | [fileInspectorPreferredWidth, layout, showPrimarySidebar, shouldRenderPrimarySidebar, width], |
| 270 | fileInspectorPreferredWidth, | ||
| 271 | layout, | ||
| 272 | primarySidebarPreferredVisible, | ||
| 273 | shouldRenderPrimarySidebar, | ||
| 274 | width, | ||
| 275 | ], | ||
| 276 | ); | 269 | ); |
| 277 | const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole = | 270 | const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole = |
| 278 | focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary"); | 271 | focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary"); |
| ... | @@ -289,7 +282,7 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -289,7 +282,7 @@ function AdaptiveWorkspaceLayoutContent( |
| 289 | deriveWorkspacePaneLayout({ | 282 | deriveWorkspacePaneLayout({ |
| 290 | layout, | 283 | layout, |
| 291 | viewportWidth: width, | 284 | viewportWidth: width, |
| 292 | primarySidebarPreferredVisible, | 285 | primarySidebarPreferredVisible: showPrimarySidebar, |
| 293 | auxiliaryPanePreferredVisible, | 286 | auxiliaryPanePreferredVisible, |
| 294 | auxiliaryPaneRole, | 287 | auxiliaryPaneRole, |
| 295 | auxiliaryPanePreferredWidth: auxiliaryPanePreferredWidth ?? undefined, | 288 | auxiliaryPanePreferredWidth: auxiliaryPanePreferredWidth ?? undefined, |
| ... | @@ -299,7 +292,7 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -299,7 +292,7 @@ function AdaptiveWorkspaceLayoutContent( |
| 299 | auxiliaryPaneRole, | 292 | auxiliaryPaneRole, |
| 300 | auxiliaryPanePreferredWidth, | 293 | auxiliaryPanePreferredWidth, |
| 301 | layout, | 294 | layout, |
| 302 | primarySidebarPreferredVisible, | 295 | showPrimarySidebar, |
| 303 | width, | 296 | width, |
| 304 | ], | 297 | ], |
| 305 | ); | 298 | ); |
| ... | @@ -357,13 +350,16 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -357,13 +350,16 @@ function AdaptiveWorkspaceLayoutContent( |
| 357 | }; | 350 | }; |
| 358 | }, []); | 351 | }, []); |
| 359 | const togglePrimarySidebar = useCallback(() => { | 352 | const togglePrimarySidebar = useCallback(() => { |
| 353 | if (pathname === "/") { | ||
| 354 | return; | ||
| 355 | } | ||
| 360 | if (!panes.primarySidebarVisible && panes.primarySidebarSuppressedByAuxiliary) { | 356 | if (!panes.primarySidebarVisible && panes.primarySidebarSuppressedByAuxiliary) { |
| 361 | setFileInspectorPreferredVisible(false); | 357 | setFileInspectorPreferredVisible(false); |
| 362 | setPrimarySidebarPreferredVisible(true); | 358 | setPrimarySidebarPreferredVisible(true); |
| 363 | return; | 359 | return; |
| 364 | } | 360 | } |
| 365 | setPrimarySidebarPreferredVisible((current) => !current); | 361 | setPrimarySidebarPreferredVisible((current) => !current); |
| 366 | }, [panes.primarySidebarSuppressedByAuxiliary, panes.primarySidebarVisible]); | 362 | }, [panes.primarySidebarSuppressedByAuxiliary, panes.primarySidebarVisible, pathname]); |
| 367 | const revealPrimarySidebar = useCallback(() => { | 363 | const revealPrimarySidebar = useCallback(() => { |
| 368 | if (panes.primarySidebarSuppressedByAuxiliary) { | 364 | if (panes.primarySidebarSuppressedByAuxiliary) { |
| 369 | setFileInspectorPreferredVisible(false); | 365 | setFileInspectorPreferredVisible(false); |
| ... | @@ -374,7 +370,11 @@ function AdaptiveWorkspaceLayoutContent( | ... | @@ -374,7 +370,11 @@ function AdaptiveWorkspaceLayoutContent( |
| 374 | togglePrimarySidebar(); | 370 | togglePrimarySidebar(); |
| 375 | return true; | 371 | return true; |
| 376 | }, [togglePrimarySidebar]); | 372 | }, [togglePrimarySidebar]); |
| 377 | useHardwareKeyboardCommand("toggleSidebar", handleToggleSidebarCommand); | 373 | const sidebarCommands = useMemo( |
| 374 | () => (pathname === "/" ? [] : (["toggleSidebar"] as const)), | ||
| 375 | [pathname], | ||
| 376 | ); | ||
| 377 | useHardwareKeyboardCommand(sidebarCommands, handleToggleSidebarCommand); | ||
| 378 | const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => { | 378 | const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => { |
| 379 | if (role === "inspector") { | 379 | if (role === "inspector") { |
| 380 | setFocusedAuxiliaryPaneRole("inspector"); | 380 | setFocusedAuxiliaryPaneRole("inspector"); |
apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx+2-34| ... | @@ -1,5 +1,3 @@ | ... | @@ -1,5 +1,3 @@ |
| 1 | import { NativeHeaderToolbar } from "../../native/StackHeader"; | ||
| 2 | import type { ReactNode } from "react"; | ||
| 3 | import { Platform } from "react-native"; | 1 | import { Platform } from "react-native"; |
| 4 | 2 | ||
| 5 | import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader"; | 3 | import { AndroidHeaderIconButton } from "../../components/AndroidScreenHeader"; |
| ... | @@ -12,39 +10,9 @@ export function AndroidWorkspaceSidebarButton() { | ... | @@ -12,39 +10,9 @@ export function AndroidWorkspaceSidebarButton() { |
| 12 | 10 | ||
| 13 | return ( | 11 | return ( |
| 14 | <AndroidHeaderIconButton | 12 | <AndroidHeaderIconButton |
| 15 | accessibilityLabel={ | 13 | accessibilityLabel={panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar"} |
| 16 | panes.primarySidebarVisible ? "Hide thread sidebar" : "Show thread sidebar" | 14 | icon={panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"} |
| 17 | } | ||
| 18 | icon="sidebar.left" | ||
| 19 | selected={panes.primarySidebarVisible} | ||
| 20 | onPress={togglePrimarySidebar} | 15 | onPress={togglePrimarySidebar} |
| 21 | /> | 16 | /> |
| 22 | ); | 17 | ); |
| 23 | } | 18 | } |
| 24 | |||
| 25 | export function WorkspaceSidebarToolbar( | ||
| 26 | props: { | ||
| 27 | readonly children?: ReactNode; | ||
| 28 | readonly afterSidebarButton?: ReactNode; | ||
| 29 | } = {}, | ||
| 30 | ) { | ||
| 31 | const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); | ||
| 32 | |||
| 33 | if (Platform.OS === "android" || !layout.usesSplitView) { | ||
| 34 | return null; | ||
| 35 | } | ||
| 36 | |||
| 37 | return ( | ||
| 38 | <NativeHeaderToolbar placement="left"> | ||
| 39 | {props.children} | ||
| 40 | <NativeHeaderToolbar.Button | ||
| 41 | accessibilityLabel={ | ||
| 42 | panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar" | ||
| 43 | } | ||
| 44 | icon={panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"} | ||
| 45 | onPress={togglePrimarySidebar} | ||
| 46 | /> | ||
| 47 | {props.afterSidebarButton} | ||
| 48 | </NativeHeaderToolbar> | ||
| 49 | ); | ||
| 50 | } |
apps/mobile/src/features/settings/SettingsRouteScreen.tsx+13-2| ... | @@ -6,7 +6,8 @@ import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-g | ... | @@ -6,7 +6,8 @@ import { deriveProjectGroupLabel } from "@t3tools/client-runtime/state/project-g |
| 6 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 6 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| 7 | 7 | ||
| 8 | import { hasCloudPublicConfig } from "../cloud/publicConfig"; | 8 | import { hasCloudPublicConfig } from "../cloud/publicConfig"; |
| 9 | import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; | 9 | import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; |
| 10 | import { NativeHeaderToolbar } from "../../native/StackHeader"; | ||
| 10 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; | 11 | import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; |
| 11 | import { SettingsRow } from "./components/SettingsRow"; | 12 | import { SettingsRow } from "./components/SettingsRow"; |
| 12 | import { SettingsSection } from "./components/SettingsSection"; | 13 | import { SettingsSection } from "./components/SettingsSection"; |
| ... | @@ -18,6 +19,8 @@ import { | ... | @@ -18,6 +19,8 @@ import { |
| 18 | import { useSettingsEnvironmentFilter } from "./settings-environment-filter"; | 19 | import { useSettingsEnvironmentFilter } from "./settings-environment-filter"; |
| 19 | 20 | ||
| 20 | export function SettingsRouteScreen() { | 21 | export function SettingsRouteScreen() { |
| 22 | const navigation = useNavigation(); | ||
| 23 | const { layout } = useAdaptiveWorkspaceLayout(); | ||
| 21 | const content = hasCloudPublicConfig() ? ( | 24 | const content = hasCloudPublicConfig() ? ( |
| 22 | <ConfiguredSettingsRouteScreen /> | 25 | <ConfiguredSettingsRouteScreen /> |
| 23 | ) : ( | 26 | ) : ( |
| ... | @@ -26,7 +29,15 @@ export function SettingsRouteScreen() { | ... | @@ -26,7 +29,15 @@ export function SettingsRouteScreen() { |
| 26 | 29 | ||
| 27 | return ( | 30 | return ( |
| 28 | <> | 31 | <> |
| 29 | <WorkspaceSidebarToolbar /> | 32 | {Platform.OS === "ios" && layout.usesSplitView ? ( |
| 33 | <NativeHeaderToolbar placement="left"> | ||
| 34 | <NativeHeaderToolbar.Button | ||
| 35 | accessibilityLabel="Go back" | ||
| 36 | icon="chevron.left" | ||
| 37 | onPress={() => navigation.goBack()} | ||
| 38 | /> | ||
| 39 | </NativeHeaderToolbar> | ||
| 40 | ) : null} | ||
| 30 | <SettingsEnvironmentFilterHeader closeSettings /> | 41 | <SettingsEnvironmentFilterHeader closeSettings /> |
| 31 | {Platform.OS === "android" ? ( | 42 | {Platform.OS === "android" ? ( |
| 32 | <SettingsScreen title="Settings" trailing={<AndroidSettingsEnvironmentFilter />}> | 43 | <SettingsScreen title="Settings" trailing={<AndroidSettingsEnvironmentFilter />}> |
apps/mobile/src/features/settings/components/SettingsEnvironmentFilterHeader.tsx+5-1| ... | @@ -5,10 +5,13 @@ import { ControlPillMenu } from "../../../components/ControlPill"; | ... | @@ -5,10 +5,13 @@ import { ControlPillMenu } from "../../../components/ControlPill"; |
| 5 | import { SymbolView } from "../../../components/AppSymbol"; | 5 | import { SymbolView } from "../../../components/AppSymbol"; |
| 6 | import { NativeStackScreenOptions } from "../../../native/StackHeader"; | 6 | import { NativeStackScreenOptions } from "../../../native/StackHeader"; |
| 7 | import { withNativeGlassHeaderItem } from "../../layout/native-glass-header-items"; | 7 | import { withNativeGlassHeaderItem } from "../../layout/native-glass-header-items"; |
| 8 | import { useAdaptiveWorkspaceLayout } from "../../layout/AdaptiveWorkspaceLayout"; | ||
| 8 | import { useSettingsEnvironmentFilter } from "../settings-environment-filter"; | 9 | import { useSettingsEnvironmentFilter } from "../settings-environment-filter"; |
| 9 | 10 | ||
| 10 | export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?: boolean }) { | 11 | export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings?: boolean }) { |
| 11 | const navigation = useNavigation(); | 12 | const navigation = useNavigation(); |
| 13 | const { layout } = useAdaptiveWorkspaceLayout(); | ||
| 14 | const closeSettings = props.closeSettings === true && !layout.usesSplitView; | ||
| 12 | const { | 15 | const { |
| 13 | availableTargets, | 16 | availableTargets, |
| 14 | selectedTargets, | 17 | selectedTargets, |
| ... | @@ -26,6 +29,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings? | ... | @@ -26,6 +29,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings? |
| 26 | ? "line.3.horizontal.decrease" | 29 | ? "line.3.horizontal.decrease" |
| 27 | : "line.3.horizontal.decrease.circle.fill"; | 30 | : "line.3.horizontal.decrease.circle.fill"; |
| 28 | const filterVersion = JSON.stringify({ | 31 | const filterVersion = JSON.stringify({ |
| 32 | closeSettings, | ||
| 29 | selection: selectedIds === null ? null : [...selectedIds].sort(), | 33 | selection: selectedIds === null ? null : [...selectedIds].sort(), |
| 30 | targets: availableTargets.map((entry) => [entry.environmentId, entry.label, entry.displayUrl]), | 34 | targets: availableTargets.map((entry) => [entry.environmentId, entry.label, entry.displayUrl]), |
| 31 | project: selectedProjectKey, | 35 | project: selectedProjectKey, |
| ... | @@ -94,7 +98,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings? | ... | @@ -94,7 +98,7 @@ export function SettingsEnvironmentFilterHeader(props: { readonly closeSettings? |
| 94 | ], | 98 | ], |
| 95 | }, | 99 | }, |
| 96 | }), | 100 | }), |
| 97 | ...(props.closeSettings | 101 | ...(closeSettings |
| 98 | ? [ | 102 | ? [ |
| 99 | withNativeGlassHeaderItem({ | 103 | withNativeGlassHeaderItem({ |
| 100 | accessibilityLabel: "Close settings", | 104 | accessibilityLabel: "Close settings", |
apps/mobile/src/features/settings/components/SettingsScreen.tsx+1-1| ... | @@ -23,7 +23,7 @@ export function SettingsScreen( | ... | @@ -23,7 +23,7 @@ export function SettingsScreen( |
| 23 | title={props.title} | 23 | title={props.title} |
| 24 | actions={props.actions} | 24 | actions={props.actions} |
| 25 | trailing={Platform.OS === "android" ? props.trailing : undefined} | 25 | trailing={Platform.OS === "android" ? props.trailing : undefined} |
| 26 | sidebar={Platform.OS === "android" && !props.formSheet} | 26 | sidebar={false} |
| 27 | onBack={() => navigation.goBack()} | 27 | onBack={() => navigation.goBack()} |
| 28 | hideBottomBorder={!props.formSheet} | 28 | hideBottomBorder={!props.formSheet} |
| 29 | /> | 29 | /> |
apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx+2-3| ... | @@ -100,9 +100,8 @@ function TerminalHeader(props: { | ... | @@ -100,9 +100,8 @@ function TerminalHeader(props: { |
| 100 | subtitle={props.subtitle} | 100 | subtitle={props.subtitle} |
| 101 | onBack={props.onCloseTerminal} | 101 | onBack={props.onCloseTerminal} |
| 102 | backInSplitView={{ | 102 | backInSplitView={{ |
| 103 | accessibilityLabel: "Close terminal", | 103 | accessibilityLabel: "Back to chat", |
| 104 | icon: "xmark", | 104 | icon: "chevron.left", |
| 105 | separateBackground: true, | ||
| 106 | }} | 105 | }} |
| 107 | menus={ | 106 | menus={ |
| 108 | props.isEnvironmentReady | 107 | props.isEnvironmentReady |
apps/mobile/src/features/threads/NewTaskRouteScreen.tsx+10-14| ... | @@ -98,28 +98,24 @@ function NewTaskHeader(props: { | ... | @@ -98,28 +98,24 @@ function NewTaskHeader(props: { |
| 98 | title={props.title} | 98 | title={props.title} |
| 99 | subtitle={props.subtitle ?? undefined} | 99 | subtitle={props.subtitle ?? undefined} |
| 100 | sidebar={false} | 100 | sidebar={false} |
| 101 | backInSplitView={{ | ||
| 102 | accessibilityLabel: "Go back", | ||
| 103 | icon: "chevron.left", | ||
| 104 | }} | ||
| 105 | options={{ headerBackVisible: !layout.usesSplitView }} | ||
| 101 | hideBottomBorder | 106 | hideBottomBorder |
| 102 | onBack={() => navigation.goBack()} | 107 | onBack={() => navigation.goBack()} |
| 103 | actions={[ | 108 | actions={ |
| 104 | ...(Platform.OS === "ios" && layout.usesSplitView | 109 | props.canAddProject |
| 105 | ? [ | ||
| 106 | { | ||
| 107 | accessibilityLabel: "Close new task", | ||
| 108 | icon: "xmark" as const, | ||
| 109 | onPress: () => navigation.goBack(), | ||
| 110 | }, | ||
| 111 | ] | ||
| 112 | : []), | ||
| 113 | ...(props.canAddProject | ||
| 114 | ? [ | 110 | ? [ |
| 115 | { | 111 | { |
| 116 | accessibilityLabel: "Add project", | 112 | accessibilityLabel: "Add project", |
| 117 | icon: "plus" as const, | 113 | icon: "plus", |
| 118 | onPress: () => navigation.dispatch(StackActions.push("AddProject")), | 114 | onPress: () => navigation.dispatch(StackActions.push("AddProject")), |
| 119 | }, | 115 | }, |
| 120 | ] | 116 | ] |
| 121 | : []), | 117 | : [] |
| 122 | ]} | 118 | } |
| 123 | search={{ | 119 | search={{ |
| 124 | value: props.searchText, | 120 | value: props.searchText, |
| 125 | onChangeText: props.onSearchTextChange, | 121 | onChangeText: props.onSearchTextChange, |