From 80d2b26d9472ea9c7c5e246b7af4fd5febc337fd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 22 Jun 2026 20:44:02 -0700 Subject: [PATCH 01/71] Add split-view mobile workspace layout - Enable adaptive sidebar navigation on wide mobile windows - Keep compact single-pane behavior on phones - Extract and test shared thread grouping and layout logic Co-authored-by: codex --- apps/mobile/app.config.ts | 2 +- apps/mobile/src/app/_layout.tsx | 57 ++--- apps/mobile/src/app/index.tsx | 21 +- .../layout/AdaptiveWorkspaceLayout.tsx | 65 ++++++ .../features/layout/WorkspaceEmptyDetail.tsx | 21 ++ .../src/features/threads/ThreadFeed.tsx | 11 +- .../threads/ThreadNavigationDrawer.tsx | 36 +-- .../threads/ThreadNavigationSidebar.tsx | 219 ++++++++++++++++++ .../features/threads/ThreadRouteScreen.tsx | 38 ++- .../threads/thread-navigation-groups.test.ts | 94 ++++++++ .../threads/thread-navigation-groups.ts | 63 +++++ apps/mobile/src/lib/layout.test.ts | 51 ++++ apps/mobile/src/lib/layout.ts | 24 +- 13 files changed, 623 insertions(+), 79 deletions(-) create mode 100644 apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx create mode 100644 apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx create mode 100644 apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx create mode 100644 apps/mobile/src/features/threads/thread-navigation-groups.test.ts create mode 100644 apps/mobile/src/features/threads/thread-navigation-groups.ts create mode 100644 apps/mobile/src/lib/layout.test.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 8cdf6f2e25c..a75f9f06d1b 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -64,7 +64,7 @@ const config: ExpoConfig = { runtimeVersion: { policy: process.env.MOBILE_VERSION_POLICY ?? "appVersion", }, - orientation: "portrait", + orientation: "default", icon: "./assets/icon.png", userInterfaceStyle: "automatic", updates: { diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 968be6c14a8..de7e3c784ae 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -26,6 +26,7 @@ import { useClerkSettingsSheetDetent, } from "../features/cloud/ClerkSettingsSheetDetent"; import { useAgentNotificationNavigation } from "../features/agent-awareness/notificationNavigation"; +import { AdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout"; import { useThemeColor } from "../lib/useThemeColor"; function AppNavigator() { @@ -92,33 +93,35 @@ function AppNavigatorContent() { backgroundColor={String(statusBarBg)} translucent /> - - - - - - - + + + + + + + + + ); } diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index 7f9962efc98..552becc1709 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -10,7 +10,7 @@ import { } from "@t3tools/contracts"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; -import { useRouter } from "expo-router"; +import { Stack, useRouter } from "expo-router"; import { useCallback, useMemo, useState } from "react"; import { useProjects, useThreadShells } from "../state/entities"; @@ -21,6 +21,8 @@ import { HomeScreen } from "../features/home/HomeScreen"; import { HomeHeader } from "../features/home/HomeHeader"; import type { HomeProjectSortOrder } from "../features/home/homeThreadList"; import { useThreadListActions } from "../features/home/useThreadListActions"; +import { useAdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout"; +import { WorkspaceEmptyDetail } from "../features/layout/WorkspaceEmptyDetail"; interface HomeListOptions { readonly selectedEnvironmentId: EnvironmentId | null; @@ -32,6 +34,7 @@ interface HomeListOptions { /* ─── Route screen ───────────────────────────────────────────────────── */ export default function HomeRouteScreen() { + const { layout } = useAdaptiveWorkspaceLayout(); const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); @@ -80,6 +83,22 @@ export default function HomeRouteScreen() { setListOptions((current) => ({ ...current, projectGroupingMode })); }, []); + if (layout.usesSplitView) { + return ( + <> + + + + ); + } + return ( <> ({ + layout: compactLayout, +}); + +function firstRouteParam(value: string | string[] | undefined): string | null { + return Array.isArray(value) ? (value[0] ?? null) : (value ?? null); +} + +export function useAdaptiveWorkspaceLayout(): AdaptiveWorkspaceContextValue { + return use(AdaptiveWorkspaceContext); +} + +export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) { + const { width, height } = useWindowDimensions(); + const router = useRouter(); + const params = useGlobalSearchParams<{ + environmentId?: string | string[]; + threadId?: string | string[]; + }>(); + const layout = useMemo(() => deriveLayout({ width, height }), [height, width]); + const environmentId = firstRouteParam(params.environmentId); + const threadId = firstRouteParam(params.threadId); + const selectedThreadKey = + environmentId !== null && threadId !== null + ? scopedThreadKey(EnvironmentId.make(environmentId), ThreadId.make(threadId)) + : null; + const contextValue = useMemo(() => ({ layout }), [layout]); + + const handleSelectThread = (thread: EnvironmentThreadShell) => { + router.replace(buildThreadRoutePath(thread)); + }; + + return ( + + + {layout.usesSplitView && layout.listPaneWidth !== null ? ( + router.push("/settings")} + onSelectThread={handleSelectThread} + onStartNewTask={() => router.push("/new")} + /> + ) : null} + {props.children} + + + ); +} diff --git a/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx b/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx new file mode 100644 index 00000000000..9d052a89a20 --- /dev/null +++ b/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx @@ -0,0 +1,21 @@ +import { SymbolView } from "expo-symbols"; +import { View } from "react-native"; + +import { AppText as Text } from "../../components/AppText"; +import { useThemeColor } from "../../lib/useThemeColor"; + +export function WorkspaceEmptyDetail() { + const iconColor = useThemeColor("--color-icon-subtle"); + + return ( + + + + Select a thread + + Choose a thread from the sidebar or start a new task. + + + + ); +} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index a82e7c49ccd..4ba6fbed582 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -25,6 +25,7 @@ import { ActivityIndicator, Image, Linking, + type LayoutChangeEvent, Pressable, ScrollView, StyleSheet, @@ -1121,7 +1122,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const foldSettleSecondFrameRef = useRef(null); const disclosureAnchorKeyRef = useRef(null); const previousLatestTurnRef = useRef(props.latestTurn); - const { width: viewportWidth } = useWindowDimensions(); + const { width: windowWidth } = useWindowDimensions(); + const [viewportWidth, setViewportWidth] = useState(windowWidth); const [disclosureToggleSettling, setDisclosureToggleSettling] = useState(false); const [interactionState, setInteractionState] = useState<{ readonly copiedRowId: string | null; @@ -1147,6 +1149,11 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const topContentInset = props.contentTopInset ?? insets.top + 44; const bottomContentInset = props.contentBottomInset ?? 18; + const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { + const nextWidth = Math.round(event.nativeEvent.layout.width); + setViewportWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); + }, []); + const iconSubtleColor = useThemeColor("--color-icon-subtle"); const userBubbleColor = useThemeColor("--color-user-bubble"); const onMarkdownLinkPress = useCallback( @@ -1451,7 +1458,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { return ( <> - + ({ - activityAt: new Date(thread.updatedAt ?? thread.createdAt).getTime(), - title: thread.title, - }), -); +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { buildThreadNavigationGroups } from "./thread-navigation-groups"; export function ThreadNavigationDrawer(props: { readonly visible: boolean; @@ -192,24 +179,9 @@ function ThreadNavigationDrawerContent(props: { }) { const projects = useProjects(); const threads = useThreadShells(); - const repositoryGroups = useMemo( - () => groupProjectsByRepository({ projects, threads }), - [projects, threads], - ); const groupedThreads = useMemo( - () => - repositoryGroups.map((group) => { - const threads: EnvironmentThreadShell[] = []; - for (const projectGroup of group.projects) { - threads.push(...projectGroup.threads); - } - return { - key: group.key, - title: group.projects[0]?.project.title ?? group.title, - threads: Arr.sort(threads, threadActivityOrder), - }; - }), - [repositoryGroups], + () => buildThreadNavigationGroups({ projects, threads }), + [projects, threads], ); return ( diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx new file mode 100644 index 00000000000..811b620a3f0 --- /dev/null +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -0,0 +1,219 @@ +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { SymbolView } from "expo-symbols"; +import { useMemo, useState } from "react"; +import { Pressable, ScrollView, StyleSheet, TextInput, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { AppText as Text } from "../../components/AppText"; +import { StatusPill } from "../../components/StatusPill"; +import { scopedThreadKey } from "../../lib/scopedEntities"; +import { relativeTime } from "../../lib/time"; +import { useThemeColor } from "../../lib/useThemeColor"; +import { useProjects, useThreadShells } from "../../state/entities"; +import { buildThreadNavigationGroups } from "./thread-navigation-groups"; +import { threadStatusTone } from "./threadPresentation"; + +export function ThreadNavigationSidebar(props: { + readonly width: number; + readonly selectedThreadKey: string | null; + readonly onOpenSettings: () => void; + readonly onSelectThread: (thread: EnvironmentThreadShell) => void; + readonly onStartNewTask: () => void; +}) { + const insets = useSafeAreaInsets(); + const projects = useProjects(); + const threads = useThreadShells(); + const [searchQuery, setSearchQuery] = useState(""); + const groups = useMemo( + () => buildThreadNavigationGroups({ projects, threads, searchQuery }), + [projects, searchQuery, threads], + ); + + const backgroundColor = useThemeColor("--color-drawer"); + const borderColor = useThemeColor("--color-border"); + const foregroundColor = useThemeColor("--color-foreground"); + const iconColor = useThemeColor("--color-icon-muted"); + const mutedColor = useThemeColor("--color-foreground-muted"); + const placeholderColor = useThemeColor("--color-placeholder"); + const searchBackgroundColor = useThemeColor("--color-subtle-strong"); + const selectedBackgroundColor = useThemeColor("--color-subtle-strong"); + const pressedBackgroundColor = useThemeColor("--color-subtle"); + + return ( + + + + Threads + + [ + styles.headerButton, + { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + ]} + > + + + [ + styles.headerButton, + { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + ]} + > + + + + + + + + + + + {groups.length === 0 ? ( + + {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} + + ) : ( + groups.map((group) => ( + + + {group.title} + + + {group.threads.length === 0 ? ( + No threads yet + ) : ( + group.threads.map((thread) => { + const threadKey = scopedThreadKey(thread.environmentId, thread.id); + const selected = threadKey === props.selectedThreadKey; + + return ( + props.onSelectThread(thread)} + style={({ pressed }) => [ + styles.threadRow, + { + backgroundColor: selected + ? selectedBackgroundColor + : pressed + ? pressedBackgroundColor + : "transparent", + }, + ]} + > + + + {thread.title} + + + {relativeTime(thread.updatedAt ?? thread.createdAt)} + + + + + ); + }) + )} + + )) + )} + + + ); +} + +const styles = StyleSheet.create({ + header: { + minHeight: 48, + paddingLeft: 18, + paddingRight: 8, + flexDirection: "row", + alignItems: "center", + gap: 2, + }, + headerButton: { + width: 44, + height: 44, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + }, + searchField: { + height: 36, + marginTop: 6, + marginHorizontal: 14, + paddingLeft: 10, + paddingRight: 5, + borderRadius: 10, + flexDirection: "row", + alignItems: "center", + gap: 7, + }, + searchInput: { + flex: 1, + height: 36, + paddingVertical: 0, + paddingHorizontal: 0, + fontFamily: "DMSans_400Regular", + fontSize: 15, + }, + section: { + gap: 4, + }, + threadRow: { + minHeight: 58, + borderRadius: 10, + paddingHorizontal: 10, + paddingVertical: 8, + flexDirection: "row", + alignItems: "center", + gap: 8, + }, + threadText: { + minWidth: 0, + flex: 1, + gap: 2, + }, +}); diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index f8c916974e5..ba112c25d49 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,5 +1,5 @@ import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import * as Option from "effect/Option"; import { EnvironmentId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; @@ -47,6 +47,7 @@ import { useSelectedThreadWorktree } from "../../state/use-selected-thread-workt import { useThreadComposerState } from "../../state/use-thread-composer-state"; import { threadEnvironment } from "../../state/threads"; import { projectThreadContentPresentation } from "./threadContentPresentation"; +import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -61,6 +62,7 @@ function OpeningThreadLoadingScreen() { } export function ThreadRouteScreen() { + const { layout } = useAdaptiveWorkspaceLayout(); const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); @@ -145,8 +147,16 @@ export function ThreadRouteScreen() { const gitActionProgress = useGitActionProgress(gitActionProgressTarget); const handleOpenDrawer = useCallback(() => { - setDrawerVisible(true); - }, []); + if (!layout.usesSplitView) { + setDrawerVisible(true); + } + }, [layout.usesSplitView]); + + useEffect(() => { + if (layout.usesSplitView) { + setDrawerVisible(false); + } + }, [layout.usesSplitView]); const handleOpenConnectionEditor = useCallback(() => { void router.push("/connections"); @@ -328,6 +338,7 @@ export function ThreadRouteScreen() { headerStyle: { backgroundColor: "transparent" }, headerShadowVisible: false, headerTintColor: iconColor, + headerBackVisible: !layout.usesSplitView, headerBackTitle: "", headerTitle: () => ( - setDrawerVisible(false)} - onSelectThread={(thread) => { - router.replace(buildThreadRoutePath(thread)); - }} - onStartNewTask={() => router.push("/new")} - /> + {layout.usesSplitView ? null : ( + setDrawerVisible(false)} + onSelectThread={(thread) => { + router.replace(buildThreadRoutePath(thread)); + }} + onStartNewTask={() => router.push("/new")} + /> + )} ); diff --git a/apps/mobile/src/features/threads/thread-navigation-groups.test.ts b/apps/mobile/src/features/threads/thread-navigation-groups.test.ts new file mode 100644 index 00000000000..fb2f0ee5768 --- /dev/null +++ b/apps/mobile/src/features/threads/thread-navigation-groups.test.ts @@ -0,0 +1,94 @@ +import type { + EnvironmentProject, + EnvironmentThreadShell, +} from "@t3tools/client-runtime/state/shell"; +import { EnvironmentId, ProjectId, ProviderInstanceId, ThreadId } from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { buildThreadNavigationGroups } from "./thread-navigation-groups"; + +const environmentId = EnvironmentId.make("environment-1"); + +function makeProject(input: Pick): EnvironmentProject { + return { + environmentId, + workspaceRoot: `/workspaces/${input.id}`, + repositoryIdentity: null, + defaultModelSelection: null, + scripts: [], + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + ...input, + }; +} + +function makeThread( + input: Pick & + Partial, +): EnvironmentThreadShell { + return { + environmentId, + modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" }, + runtimeMode: "full-access", + interactionMode: "default", + branch: null, + worktreePath: null, + latestTurn: null, + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + archivedAt: null, + session: null, + latestUserMessageAt: null, + hasPendingApprovals: false, + hasPendingUserInput: false, + hasActionableProposedPlan: false, + ...input, + }; +} + +describe("buildThreadNavigationGroups", () => { + const project = makeProject({ id: ProjectId.make("project-1"), title: "T3 Code" }); + const threads = [ + makeThread({ + id: ThreadId.make("older"), + projectId: project.id, + title: "Fix reconnect flow", + updatedAt: "2026-06-02T00:00:00.000Z", + }), + makeThread({ + id: ThreadId.make("newer"), + projectId: project.id, + title: "Build adaptive sidebar", + updatedAt: "2026-06-03T00:00:00.000Z", + }), + ]; + + it("sorts each group by recent activity", () => { + expect( + buildThreadNavigationGroups({ projects: [project], threads })[0]?.threads.map( + (thread) => thread.id, + ), + ).toEqual(["newer", "older"]); + }); + + it("matches thread titles without dropping their group", () => { + const groups = buildThreadNavigationGroups({ + projects: [project], + threads, + searchQuery: "reconnect", + }); + + expect(groups).toHaveLength(1); + expect(groups[0]?.threads.map((thread) => thread.id)).toEqual(["older"]); + }); + + it("keeps every thread when the project title matches", () => { + expect( + buildThreadNavigationGroups({ + projects: [project], + threads, + searchQuery: "t3 code", + })[0]?.threads.map((thread) => thread.id), + ).toEqual(["newer", "older"]); + }); +}); diff --git a/apps/mobile/src/features/threads/thread-navigation-groups.ts b/apps/mobile/src/features/threads/thread-navigation-groups.ts new file mode 100644 index 00000000000..ae50031b6d5 --- /dev/null +++ b/apps/mobile/src/features/threads/thread-navigation-groups.ts @@ -0,0 +1,63 @@ +import type { + EnvironmentProject, + EnvironmentThreadShell, +} from "@t3tools/client-runtime/state/shell"; +import * as Arr from "effect/Array"; +import * as Order from "effect/Order"; + +import { groupProjectsByRepository } from "../../lib/repositoryGroups"; + +export interface ThreadNavigationGroup { + readonly key: string; + readonly title: string; + readonly threads: ReadonlyArray; +} + +const threadActivityOrder = Order.mapInput( + Order.Struct({ + activityAt: Order.flip(Order.Number), + title: Order.String, + }), + (thread: EnvironmentThreadShell) => ({ + activityAt: new Date(thread.updatedAt ?? thread.createdAt).getTime(), + title: thread.title, + }), +); + +export function buildThreadNavigationGroups(input: { + readonly projects: ReadonlyArray; + readonly threads: ReadonlyArray; + readonly searchQuery?: string; +}): ReadonlyArray { + const query = input.searchQuery?.trim().toLocaleLowerCase() ?? ""; + + return groupProjectsByRepository(input).flatMap((group) => { + const threads = Arr.sort( + group.projects.flatMap((projectGroup) => projectGroup.threads), + threadActivityOrder, + ); + const title = group.projects[0]?.project.title ?? group.title; + const groupMatches = + query.length === 0 || + title.toLocaleLowerCase().includes(query) || + group.title.toLocaleLowerCase().includes(query) || + group.projects.some((projectGroup) => + projectGroup.project.title.toLocaleLowerCase().includes(query), + ); + const matchingThreads = groupMatches + ? threads + : threads.filter((thread) => thread.title.toLocaleLowerCase().includes(query)); + + if (query.length > 0 && matchingThreads.length === 0) { + return []; + } + + return [ + { + key: group.key, + title, + threads: matchingThreads, + }, + ]; + }); +} diff --git a/apps/mobile/src/lib/layout.test.ts b/apps/mobile/src/lib/layout.test.ts new file mode 100644 index 00000000000..e9646722e56 --- /dev/null +++ b/apps/mobile/src/lib/layout.test.ts @@ -0,0 +1,51 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { deriveLayout, SPLIT_LAYOUT_MIN_HEIGHT, SPLIT_LAYOUT_MIN_WIDTH } from "./layout"; + +describe("deriveLayout", () => { + it.each([ + { name: "small iPhone portrait", width: 375, height: 667 }, + { name: "large iPhone portrait", width: 430, height: 932 }, + { name: "small iPhone landscape", width: 667, height: 375 }, + { name: "large iPhone landscape", width: 932, height: 430 }, + { name: "short wide window", width: 1_024, height: 599 }, + { name: "narrow tall window", width: 719, height: 1_024 }, + ])("keeps a $name in the compact shell", ({ width, height }) => { + expect(deriveLayout({ width, height })).toEqual({ + variant: "compact", + usesSplitView: false, + listPaneWidth: null, + shellPadding: 0, + }); + }); + + it.each([ + { name: "small tablet portrait", width: 744, height: 1_133 }, + { name: "tablet landscape", width: 1_024, height: 768 }, + { name: "large resizable window", width: 1_366, height: 1_024 }, + { name: "foldable-sized window", width: 800, height: 700 }, + ])("uses the split shell for a $name", ({ width, height }) => { + expect(deriveLayout({ width, height })).toMatchObject({ + variant: "split", + usesSplitView: true, + }); + }); + + it("switches only after both space requirements are met", () => { + expect( + deriveLayout({ width: SPLIT_LAYOUT_MIN_WIDTH, height: SPLIT_LAYOUT_MIN_HEIGHT }).variant, + ).toBe("split"); + expect( + deriveLayout({ width: SPLIT_LAYOUT_MIN_WIDTH - 1, height: SPLIT_LAYOUT_MIN_HEIGHT }).variant, + ).toBe("compact"); + expect( + deriveLayout({ width: SPLIT_LAYOUT_MIN_WIDTH, height: SPLIT_LAYOUT_MIN_HEIGHT - 1 }).variant, + ).toBe("compact"); + }); + + it("keeps the sidebar within usable native-column bounds", () => { + expect(deriveLayout({ width: 720, height: 1_000 }).listPaneWidth).toBe(280); + expect(deriveLayout({ width: 1_024, height: 768 }).listPaneWidth).toBe(328); + expect(deriveLayout({ width: 1_600, height: 1_000 }).listPaneWidth).toBe(380); + }); +}); diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index 2ae4314fdba..35b9f571c14 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -2,6 +2,19 @@ function clamp(value: number, min: number, max: number): number { return Math.min(Math.max(value, min), max); } +/** + * Use available space, not device or orientation labels, to choose the shell. + * + * The height floor deliberately keeps every current iPhone in the compact shell + * when it rotates to landscape, while still allowing iPad and foldable-sized + * windows to adopt the persistent sidebar as they resize. + */ +export const SPLIT_LAYOUT_MIN_WIDTH = 720; +export const SPLIT_LAYOUT_MIN_HEIGHT = 600; + +const SPLIT_SIDEBAR_MIN_WIDTH = 280; +const SPLIT_SIDEBAR_MAX_WIDTH = 380; + export type LayoutVariant = "compact" | "split"; export interface Layout { @@ -13,8 +26,7 @@ export interface Layout { export function deriveLayout(input: { readonly width: number; readonly height: number }): Layout { const { width, height } = input; - const shortestEdge = Math.min(width, height); - const wideEnoughForSplit = width >= 900 || (width >= 700 && shortestEdge >= 700); + const wideEnoughForSplit = width >= SPLIT_LAYOUT_MIN_WIDTH && height >= SPLIT_LAYOUT_MIN_HEIGHT; if (!wideEnoughForSplit) { return { @@ -28,7 +40,11 @@ export function deriveLayout(input: { readonly width: number; readonly height: n return { variant: "split", usesSplitView: true, - listPaneWidth: clamp(Math.round(width * 0.34), 320, 420), - shellPadding: width >= 1180 ? 20 : 14, + listPaneWidth: clamp( + Math.round(width * 0.32), + SPLIT_SIDEBAR_MIN_WIDTH, + SPLIT_SIDEBAR_MAX_WIDTH, + ), + shellPadding: 0, }; } From cf0f731bc8b34f7b7e416fde63db2ee50d5857fd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 23 Jun 2026 22:08:57 -0700 Subject: [PATCH 02/71] Add split-view mobile navigation and native controls - Adapt iPad/mobile layout for sidebar, inspector, and sheets - Move review diff payloads off Fabric props and add native scrolling - Add iOS header button module for consistent toolbar actions Co-authored-by: codex --- apps/mobile/.swiftlint.yml | 1 + .../expo-module.config.json | 6 + .../ios/T3HeaderButtonView.swift | 62 ++ .../ios/T3NativeControls.podspec | 19 + .../ios/T3NativeControlsModule.swift | 18 + .../ios/T3ReviewDiffModule.swift | 48 +- .../t3-review-diff/ios/T3ReviewDiffView.swift | 285 ++++++-- apps/mobile/src/app/_layout.tsx | 127 ++-- apps/mobile/src/app/index.tsx | 2 + apps/mobile/src/app/new/_layout.tsx | 23 + apps/mobile/src/app/new/index.tsx | 10 + apps/mobile/src/app/settings/index.tsx | 22 +- .../[environmentId]/[threadId]/_layout.tsx | 8 +- .../diffs/nativeReviewDiffSurface.test.ts | 7 +- .../features/diffs/nativeReviewDiffSurface.ts | 131 +++- .../src/features/files/FileTreeBrowser.tsx | 95 ++- .../src/features/files/SourceFileSurface.tsx | 20 +- .../features/files/ThreadFilesRouteScreen.tsx | 208 +++++- .../features/files/preload-workspace-file.ts | 74 +++ .../files/source-file-document.test.ts | 16 + .../features/files/source-file-document.ts | 54 ++ .../files/thread-file-navigator-pane.tsx | 90 +++ .../layout/AdaptiveWorkspaceLayout.tsx | 215 +++++- .../layout/adaptive-inspector-layout.tsx | 72 +++ .../layout/workspace-pane-transition.ts | 10 + .../layout/workspace-sidebar-toolbar.tsx | 26 + .../src/features/review/ReviewSheet.tsx | 610 ++++++++++++++---- .../review/nativeReviewDiffAdapter.test.ts | 56 ++ .../review/nativeReviewDiffAdapter.ts | 50 ++ .../review/review-section-menu.test.ts | 40 ++ .../features/review/review-section-menu.ts | 21 + .../review/reviewPaneSelection.test.ts | 35 + .../features/review/reviewPaneSelection.ts | 16 + .../src/features/review/useReviewDiffData.ts | 20 +- .../review/useReviewDiffPrewarming.ts | 101 +++ .../src/features/threads/ThreadComposer.tsx | 6 +- .../features/threads/ThreadDetailScreen.tsx | 65 +- .../src/features/threads/ThreadFeed.tsx | 12 +- .../features/threads/ThreadGitControls.tsx | 32 +- .../threads/ThreadNavigationSidebar.tsx | 190 +++--- .../features/threads/ThreadRouteScreen.tsx | 387 ++++++++--- .../features/threads/git/GitOverviewSheet.tsx | 51 +- .../threads/sidebar-header-actions.ios.tsx | 21 + .../threads/sidebar-header-actions.tsx | 65 ++ .../thread-inspector-content-stack.tsx | 101 +++ .../src/lib/adaptive-navigation.test.ts | 59 ++ apps/mobile/src/lib/adaptive-navigation.ts | 33 + apps/mobile/src/lib/layout.test.ts | 227 ++++++- apps/mobile/src/lib/layout.ts | 144 +++++ apps/mobile/src/native/T3HeaderButton.ios.tsx | 26 + 50 files changed, 3408 insertions(+), 609 deletions(-) create mode 100644 apps/mobile/modules/t3-native-controls/expo-module.config.json create mode 100644 apps/mobile/modules/t3-native-controls/ios/T3HeaderButtonView.swift create mode 100644 apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec create mode 100644 apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift create mode 100644 apps/mobile/src/features/files/preload-workspace-file.ts create mode 100644 apps/mobile/src/features/files/source-file-document.test.ts create mode 100644 apps/mobile/src/features/files/source-file-document.ts create mode 100644 apps/mobile/src/features/files/thread-file-navigator-pane.tsx create mode 100644 apps/mobile/src/features/layout/adaptive-inspector-layout.tsx create mode 100644 apps/mobile/src/features/layout/workspace-pane-transition.ts create mode 100644 apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx create mode 100644 apps/mobile/src/features/review/nativeReviewDiffAdapter.test.ts create mode 100644 apps/mobile/src/features/review/review-section-menu.test.ts create mode 100644 apps/mobile/src/features/review/review-section-menu.ts create mode 100644 apps/mobile/src/features/review/reviewPaneSelection.test.ts create mode 100644 apps/mobile/src/features/review/reviewPaneSelection.ts create mode 100644 apps/mobile/src/features/review/useReviewDiffPrewarming.ts create mode 100644 apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx create mode 100644 apps/mobile/src/features/threads/sidebar-header-actions.tsx create mode 100644 apps/mobile/src/features/threads/thread-inspector-content-stack.tsx create mode 100644 apps/mobile/src/lib/adaptive-navigation.test.ts create mode 100644 apps/mobile/src/lib/adaptive-navigation.ts create mode 100644 apps/mobile/src/native/T3HeaderButton.ios.tsx diff --git a/apps/mobile/.swiftlint.yml b/apps/mobile/.swiftlint.yml index 0714ce90e63..c56a56251f7 100644 --- a/apps/mobile/.swiftlint.yml +++ b/apps/mobile/.swiftlint.yml @@ -1,6 +1,7 @@ included: - ios/T3Code - modules/t3-composer-editor/ios + - modules/t3-native-controls/ios - modules/t3-terminal/ios - modules/t3-review-diff/ios diff --git a/apps/mobile/modules/t3-native-controls/expo-module.config.json b/apps/mobile/modules/t3-native-controls/expo-module.config.json new file mode 100644 index 00000000000..e47aa8bbfd8 --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/expo-module.config.json @@ -0,0 +1,6 @@ +{ + "platforms": ["apple"], + "apple": { + "modules": ["T3NativeControlsModule"] + } +} diff --git a/apps/mobile/modules/t3-native-controls/ios/T3HeaderButtonView.swift b/apps/mobile/modules/t3-native-controls/ios/T3HeaderButtonView.swift new file mode 100644 index 00000000000..7b7f9db6707 --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/ios/T3HeaderButtonView.swift @@ -0,0 +1,62 @@ +import ExpoModulesCore +import UIKit + +public final class T3HeaderButtonView: ExpoView { + private static let size: CGFloat = 44 + private static let symbolSize: CGFloat = 18 + + private let button = UIButton(type: .system) + private var systemImage = "circle" + + let onTriggered = EventDispatcher() + + public required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + + isAccessibilityElement = false + button.frame = bounds + button.autoresizingMask = [.flexibleWidth, .flexibleHeight] + button.addTarget(self, action: #selector(handlePress), for: .primaryActionTriggered) + addSubview(button) + applyConfiguration() + } + + public override var intrinsicContentSize: CGSize { + CGSize(width: Self.size, height: Self.size) + } + + public func setLabel(_ label: String) { + button.accessibilityLabel = label + } + + public func setSystemImage(_ systemImage: String) { + guard self.systemImage != systemImage else { + return + } + self.systemImage = systemImage + applyConfiguration() + } + + private func applyConfiguration() { + var configuration: UIButton.Configuration + if #available(iOS 26.0, *) { + configuration = .glass() + configuration.cornerStyle = .capsule + } else { + configuration = .plain() + } + + configuration.baseForegroundColor = .label + configuration.contentInsets = .zero + configuration.image = UIImage(systemName: systemImage) + configuration.preferredSymbolConfigurationForImage = UIImage.SymbolConfiguration( + pointSize: Self.symbolSize, + weight: .regular + ) + button.configuration = configuration + } + + @objc private func handlePress() { + onTriggered() + } +} diff --git a/apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec b/apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec new file mode 100644 index 00000000000..29735c301cc --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/ios/T3NativeControls.podspec @@ -0,0 +1,19 @@ +Pod::Spec.new do |s| + s.name = 'T3NativeControls' + s.version = '1.0.0' + s.summary = 'Native UIKit controls for T3 Code mobile.' + s.description = 'UIKit-backed controls that match native iOS navigation chrome.' + s.author = 'T3 Tools' + s.homepage = 'https://t3tools.com' + s.platforms = { + :ios => '18.0', + } + s.source = { :path => '.' } + s.static_framework = true + + s.dependency 'ExpoModulesCore' + s.pod_target_xcconfig = { + 'DEFINES_MODULE' => 'YES', + } + s.source_files = '**/*.{h,m,mm,swift,hpp,cpp}' +end diff --git a/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift b/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift new file mode 100644 index 00000000000..33e1dc9086c --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/ios/T3NativeControlsModule.swift @@ -0,0 +1,18 @@ +import ExpoModulesCore + +public final class T3NativeControlsModule: Module { + public func definition() -> ModuleDefinition { + Name("T3NativeControls") + + View(T3HeaderButtonView.self) { + Prop("label") { (view: T3HeaderButtonView, label: String) in + view.setLabel(label) + } + Prop("systemImage") { (view: T3HeaderButtonView, systemImage: String) in + view.setSystemImage(systemImage) + } + + Events("onTriggered") + } + } +} diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift index 81cdd8417d3..346588b798f 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift @@ -5,22 +5,14 @@ public class T3ReviewDiffModule: Module { Name("T3ReviewDiffSurface") View(T3ReviewDiffView.self) { - Prop("rowsJson") { (view: T3ReviewDiffView, rowsJson: String) in - view.setRowsJson(rowsJson) - } - - Prop("tokensJson") { (view: T3ReviewDiffView, tokensJson: String) in - view.setTokensJson(tokensJson) - } - - Prop("tokensPatchJson") { (view: T3ReviewDiffView, tokensPatchJson: String) in - view.setTokensPatchJson(tokensPatchJson) - } - Prop("tokensResetKey") { (view: T3ReviewDiffView, tokensResetKey: String) in view.setTokensResetKey(tokensResetKey) } + Prop("contentResetKey") { (view: T3ReviewDiffView, contentResetKey: String) in + view.setContentResetKey(contentResetKey) + } + Prop("collapsedFileIdsJson") { (view: T3ReviewDiffView, collapsedFileIdsJson: String) in view.setCollapsedFileIdsJson(collapsedFileIdsJson) } @@ -61,7 +53,37 @@ public class T3ReviewDiffModule: Module { view.setInitialRowIndex(initialRowIndex) } - Events("onDebug", "onToggleFile", "onToggleViewedFile", "onPressLine", "onToggleComment") + Events( + "onDebug", + "onVisibleFileChange", + "onToggleFile", + "onToggleViewedFile", + "onPressLine", + "onToggleComment" + ) + + AsyncFunction("scrollToFile") { (view: T3ReviewDiffView, fileId: String, animated: Bool) in + view.scrollToFile(fileId, animated: animated) + } + + AsyncFunction("scrollToTop") { (view: T3ReviewDiffView, animated: Bool) in + view.scrollToTop(animated: animated) + } + + // Large, frequently changing JSON values cannot be regular Fabric props. Expo's + // prop adapter compares strings on the main thread before invoking a setter, which + // makes a syntax-token patch capable of blocking a frame by itself. + AsyncFunction("setRowsJson") { (view: T3ReviewDiffView, rowsJson: String) in + view.setRowsJson(rowsJson) + } + + AsyncFunction("setTokensJson") { (view: T3ReviewDiffView, tokensJson: String) in + view.setTokensJson(tokensJson) + } + + AsyncFunction("setTokensPatchJson") { (view: T3ReviewDiffView, tokensPatchJson: String) in + view.setTokensPatchJson(tokensPatchJson) + } } } } diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index f8e080e7609..07e57b9db3c 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -1,7 +1,7 @@ import ExpoModulesCore import UIKit -private struct ReviewDiffNativeRow: Decodable { +private struct ReviewDiffNativeRow: Decodable, Sendable { let kind: String let id: String let fileId: String? @@ -21,18 +21,18 @@ private struct ReviewDiffNativeRow: Decodable { let commentSectionTitle: String? } -private struct ReviewDiffNativeWordDiffRange: Decodable { +private struct ReviewDiffNativeWordDiffRange: Decodable, Sendable { let start: Int let end: Int } -private struct ReviewDiffNativeToken: Decodable { +private struct ReviewDiffNativeToken: Decodable, Sendable { let content: String let color: String? let fontStyle: Int? } -private struct ReviewDiffNativeTokenPatch: Decodable { +private struct ReviewDiffNativeTokenPatch: Decodable, Sendable { let resetKey: String? let chunkIndex: Int? let tokensByRowId: [String: [ReviewDiffNativeToken]]? @@ -312,6 +312,10 @@ private struct ReviewDiffNativeStyle { } public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { + private let payloadDecodeQueue = DispatchQueue( + label: "com.t3tools.review-diff.payload-decode", + qos: .userInitiated + ) private let scrollView = UIScrollView() private let contentView = ReviewDiffContentView() private var rows: [ReviewDiffNativeRow] = [] @@ -323,10 +327,18 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { private var lastMetricsDebugKey = "" private var lastVisibleRangeDebugKey = "" private var tokensResetKey = "" + private var contentResetKey = "" private var initialRowIndex: Int? private var hasAppliedInitialRowIndex = false + private var lastVisibleFileId: String? + private var pendingScrollFileId: String? + private var pendingScrollAnimated = false + private var isProgrammaticScrollActive = false + private var rowsDecodeGeneration = 0 + private var tokensDecodeGeneration = 0 let onDebug = EventDispatcher() + let onVisibleFileChange = EventDispatcher() let onToggleFile = EventDispatcher() let onToggleViewedFile = EventDispatcher() let onPressLine = EventDispatcher() @@ -379,6 +391,13 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { updateViewportFrame() } + public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { + // A direct gesture takes ownership from any interrupted programmatic jump. + // Resume visible-file events immediately so the inspector follows the finger. + isProgrammaticScrollActive = false + contentView.isVerticalScrollActive = true + } + public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { guard !decelerate else { return @@ -396,77 +415,120 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { } func setRowsJson(_ rowsJson: String) { - guard let data = rowsJson.data(using: .utf8) else { - return - } + rowsDecodeGeneration += 1 + let generation = rowsDecodeGeneration - do { - rows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data) - contentView.rows = rows - hasAppliedInitialRowIndex = false - emitDebug("rows-decoded", [ - "rows": rows.count, - "firstKind": rows.first?.kind ?? "none", - ]) - updateContentMetrics() - } catch { - rows = [] - contentView.rows = [] - hasAppliedInitialRowIndex = false - updateContentMetrics() - emitDebug("rows-decode-failed", [ - "error": error.localizedDescription, - ]) + payloadDecodeQueue.async { [weak self] in + guard let data = rowsJson.data(using: .utf8) else { + return + } + + do { + let decodedRows = try JSONDecoder().decode([ReviewDiffNativeRow].self, from: data) + DispatchQueue.main.async { [weak self] in + guard let self, generation == self.rowsDecodeGeneration else { + return + } + self.rows = decodedRows + self.contentView.rows = decodedRows + self.hasAppliedInitialRowIndex = false + self.lastVisibleFileId = nil + self.emitDebug("rows-decoded", [ + "rows": decodedRows.count, + "firstKind": decodedRows.first?.kind ?? "none", + ]) + self.updateContentMetrics() + self.applyPendingScrollIfNeeded() + } + } catch { + let message = error.localizedDescription + DispatchQueue.main.async { [weak self] in + guard let self, generation == self.rowsDecodeGeneration else { + return + } + self.rows = [] + self.contentView.rows = [] + self.hasAppliedInitialRowIndex = false + self.lastVisibleFileId = nil + self.pendingScrollFileId = nil + self.updateContentMetrics() + self.emitDebug("rows-decode-failed", ["error": message]) + } + } } } func setTokensJson(_ tokensJson: String) { - guard let data = tokensJson.data(using: .utf8) else { - return - } + tokensDecodeGeneration += 1 + let generation = tokensDecodeGeneration - do { - contentView.tokensByRowId = try JSONDecoder().decode( - [String: [ReviewDiffNativeToken]].self, - from: data - ) - } catch { - contentView.tokensByRowId = [:] - emitDebug("tokens-decode-failed", [ - "error": error.localizedDescription, - ]) + payloadDecodeQueue.async { [weak self] in + guard let data = tokensJson.data(using: .utf8) else { + return + } + + do { + let decodedTokens = try JSONDecoder().decode( + [String: [ReviewDiffNativeToken]].self, + from: data + ) + DispatchQueue.main.async { [weak self] in + guard let self, generation == self.tokensDecodeGeneration else { + return + } + self.contentView.tokensByRowId = decodedTokens + } + } catch { + let message = error.localizedDescription + DispatchQueue.main.async { [weak self] in + guard let self, generation == self.tokensDecodeGeneration else { + return + } + self.contentView.tokensByRowId = [:] + self.emitDebug("tokens-decode-failed", ["error": message]) + } + } } } func setTokensPatchJson(_ tokensPatchJson: String) { - guard let data = tokensPatchJson.data(using: .utf8) else { - return - } - - do { - let patch = try JSONDecoder().decode(ReviewDiffNativeTokenPatch.self, from: data) - if let resetKey = patch.resetKey, resetKey != tokensResetKey { - tokensResetKey = resetKey - contentView.tokensByRowId = [:] - } - - let tokensByRowId = patch.tokensByRowId ?? [:] - if tokensByRowId.isEmpty { + payloadDecodeQueue.async { [weak self] in + guard let data = tokensPatchJson.data(using: .utf8) else { return } - contentView.mergeTokensByRowId(tokensByRowId) - if let chunkIndex = patch.chunkIndex, chunkIndex < 5 || chunkIndex.isMultiple(of: 10) { - emitDebug("tokens-patch-decoded", [ - "chunkIndex": chunkIndex, - "rows": tokensByRowId.count, - "totalRows": contentView.tokensByRowId.count, - ]) + do { + let patch = try JSONDecoder().decode(ReviewDiffNativeTokenPatch.self, from: data) + DispatchQueue.main.async { [weak self] in + guard let self else { + return + } + // A highlighter request from the previous file can finish after the view has + // already reset. Never let that stale patch roll the native token state back. + if let resetKey = patch.resetKey, resetKey != self.tokensResetKey { + return + } + + let tokensByRowId = patch.tokensByRowId ?? [:] + if tokensByRowId.isEmpty { + return + } + + self.contentView.mergeTokensByRowId(tokensByRowId) + if let chunkIndex = patch.chunkIndex, chunkIndex < 5 || chunkIndex.isMultiple(of: 10) { + self.emitDebug("tokens-patch-decoded", [ + "chunkIndex": chunkIndex, + "rows": tokensByRowId.count, + "totalRows": self.contentView.tokensByRowId.count, + ]) + } + } + } catch { + let message = error.localizedDescription + DispatchQueue.main.async { [weak self] in + self?.emitDebug("tokens-patch-decode-failed", ["error": message]) + } } - } catch { - emitDebug("tokens-patch-decode-failed", [ - "error": error.localizedDescription, - ]) } } @@ -482,6 +544,21 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { ]) } + func setContentResetKey(_ contentResetKey: String) { + guard contentResetKey != self.contentResetKey else { + return + } + + self.contentResetKey = contentResetKey + hasAppliedInitialRowIndex = false + lastVisibleFileId = nil + pendingScrollFileId = nil + isProgrammaticScrollActive = false + scrollView.setContentOffset(.zero, animated: false) + updateViewportFrame() + applyInitialRowIndexIfNeeded() + } + func setCollapsedFileIdsJson(_ collapsedFileIdsJson: String) { let nextCollapsedFileIds = decodeFileIdSet(collapsedFileIdsJson) let changedFileIds = contentView.collapsedFileIds.symmetricDifference(nextCollapsedFileIds) @@ -573,6 +650,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { contentView.invalidateVisibleViewport() contentView.setNeedsDisplay() applyInitialRowIndexIfNeeded() + emitVisibleFileIfNeeded() let debugKey = "\(rows.count):\(Int(bounds.width)):\(Int(bounds.height)):\(Int(height))" if debugKey != lastMetricsDebugKey { @@ -596,11 +674,26 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { } private func finishVerticalScroll() { + isProgrammaticScrollActive = false contentView.isVerticalScrollActive = false updateViewportFrame() emitVisibleRange(reason: "scroll-end") } + private func emitVisibleFileIfNeeded() { + // Keep the explicit destination selected while UIKit animates through the files + // between the old and new offsets. Emitting every intermediate header forces a + // React render per crossing and makes the navigator visibly flash. + guard !isProgrammaticScrollActive, + let fileId = contentView.visibleFileId(atVerticalOffset: scrollView.contentOffset.y), + fileId != lastVisibleFileId else { + return + } + + lastVisibleFileId = fileId + onVisibleFileChange(["fileId": fileId]) + } + private func emitVisibleRange(reason: String) { guard let range = contentView.currentVisibleRowRange() else { return @@ -670,6 +763,18 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { applyInitialRowIndexIfNeeded() } + func scrollToFile(_ fileId: String, animated: Bool) { + pendingScrollFileId = fileId + pendingScrollAnimated = animated + applyPendingScrollIfNeeded() + } + + func scrollToTop(animated: Bool) { + pendingScrollFileId = nil + pendingScrollAnimated = false + setVerticalContentOffset(0, animated: animated) + } + private func applyStyle() { contentView.style = ReviewDiffNativeStyle .resolve(stylePayload) @@ -686,6 +791,38 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { ) contentView.verticalOffset = scrollView.contentOffset.y contentView.invalidateVisibleViewport() + emitVisibleFileIfNeeded() + } + + private func applyPendingScrollIfNeeded() { + guard let fileId = pendingScrollFileId, + bounds.height > 0 else { + return + } + + guard let headerOffset = contentView.fileHeaderOffset(forFileId: fileId) else { + if !rows.isEmpty { + pendingScrollFileId = nil + } + return + } + + let animated = pendingScrollAnimated + pendingScrollFileId = nil + pendingScrollAnimated = false + setVerticalContentOffset(headerOffset, animated: animated) + } + + private func setVerticalContentOffset(_ targetOffset: CGFloat, animated: Bool) { + let maxOffset = max(scrollView.contentSize.height - scrollView.bounds.height, 0) + let clampedOffset = min(max(targetOffset, 0), maxOffset) + let shouldAnimate = animated && abs(scrollView.contentOffset.y - clampedOffset) > 0.5 + isProgrammaticScrollActive = shouldAnimate + contentView.isVerticalScrollActive = shouldAnimate + scrollView.setContentOffset(CGPoint(x: 0, y: clampedOffset), animated: shouldAnimate) + if !shouldAnimate { + updateViewportFrame() + } } private func applyInitialRowIndexIfNeeded() { @@ -1255,6 +1392,32 @@ private final class ReviewDiffContentView: UIView, UIGestureRecognizerDelegate { return rowOffsets[rowIndex] } + func visibleFileId(atVerticalOffset verticalOffset: CGFloat) -> String? { + guard let firstHeaderRowIndex = fileHeaderRowIndices.first else { + return nil + } + + var lowerBound = 0 + var upperBound = fileHeaderRowIndices.count + while lowerBound < upperBound { + let midpoint = (lowerBound + upperBound) / 2 + let rowIndex = fileHeaderRowIndices[midpoint] + if rowOffsets[rowIndex] <= verticalOffset + 0.5 { + lowerBound = midpoint + 1 + } else { + upperBound = midpoint + } + } + + let rowIndex = lowerBound > 0 + ? fileHeaderRowIndices[lowerBound - 1] + : firstHeaderRowIndex + guard rows.indices.contains(rowIndex) else { + return nil + } + return resolvedFileId(for: rows[rowIndex]) + } + private func fileHeaderRowIndex(forFileId fileId: String) -> Int? { fileHeaderRowIndices.first { rowIndex in rows.indices.contains(rowIndex) && resolvedFileId(for: rows[rowIndex]) == fileId diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index de7e3c784ae..43b490edcbd 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -8,7 +8,7 @@ import { import { usePathname } from "expo-router"; import Stack from "expo-router/stack"; import { useCallback } from "react"; -import { StatusBar, useColorScheme } from "react-native"; +import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -26,7 +26,11 @@ import { useClerkSettingsSheetDetent, } from "../features/cloud/ClerkSettingsSheetDetent"; import { useAgentNotificationNavigation } from "../features/agent-awareness/notificationNavigation"; -import { AdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout"; +import { + AdaptiveWorkspaceLayout, + useAdaptiveWorkspaceLayout, +} from "../features/layout/AdaptiveWorkspaceLayout"; +import { deriveStableFormSheetDetent } from "../lib/layout"; import { useThemeColor } from "../lib/useThemeColor"; function AppNavigator() { @@ -43,13 +47,35 @@ function AppNavigator() { function AppNavigatorContent() { const { state } = useWorkspaceState(); - const { collapse, isExpanded } = useClerkSettingsSheetDetent(); const colorScheme = useColorScheme(); const statusBarBg = useThemeColor("--color-status-bar"); - const sheetStyle = useResolveClassNames("bg-sheet"); useAgentNotificationNavigation(); useThreadOutboxDrain(); + if (state.isLoadingConnections) { + return ; + } + + return ( + <> + + + + + + ); +} + +function WorkspaceNavigator() { + const { collapse, isExpanded } = useClerkSettingsSheetDetent(); + const { layout } = useAdaptiveWorkspaceLayout(); + const { height } = useWindowDimensions(); + const sheetStyle = useResolveClassNames("bg-sheet"); + const handleSettingsTransitionEnd = useCallback( (event: { data: { closing: boolean } }) => { if (event.data.closing) { @@ -59,70 +85,63 @@ function AppNavigatorContent() { [collapse], ); - const newTaskScreenOptions = { + const connectionSheetScreenOptions = { contentStyle: sheetStyle, gestureEnabled: true, headerShown: false, presentation: "formSheet" as const, - sheetAllowedDetents: [0.92], + sheetAllowedDetents: [0.55, 0.7], sheetGrabberVisible: true, }; - - const connectionSheetScreenOptions = { + const settingsScreenOptions = layout.usesSplitView + ? { + animation: "fade" as const, + contentStyle: sheetStyle, + gestureEnabled: false, + headerShown: false, + presentation: "card" as const, + } + : { + ...connectionSheetScreenOptions, + sheetAllowedDetents: isExpanded ? [0.92] : [0.7], + }; + const newTaskScreenOptions = { contentStyle: sheetStyle, gestureEnabled: true, headerShown: false, presentation: "formSheet" as const, - sheetAllowedDetents: [0.55, 0.7], - sheetGrabberVisible: true, - }; - - const settingsSheetScreenOptions = { - ...connectionSheetScreenOptions, - sheetAllowedDetents: isExpanded ? [0.92] : [0.7], + sheetAllowedDetents: [layout.usesSplitView ? deriveStableFormSheetDetent(height) : 0.92], + sheetGrabberVisible: !layout.usesSplitView, }; - if (state.isLoadingConnections) { - return ; - } - return ( - <> - + - - - - - - - - - - + + + + + ); } diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index 552becc1709..db1682d2a68 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -23,6 +23,7 @@ import type { HomeProjectSortOrder } from "../features/home/homeThreadList"; import { useThreadListActions } from "../features/home/useThreadListActions"; import { useAdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout"; import { WorkspaceEmptyDetail } from "../features/layout/WorkspaceEmptyDetail"; +import { WorkspaceSidebarToolbar } from "../features/layout/workspace-sidebar-toolbar"; interface HomeListOptions { readonly selectedEnvironmentId: EnvironmentId | null; @@ -94,6 +95,7 @@ export default function HomeRouteScreen() { headerTitle: "", }} /> + ); diff --git a/apps/mobile/src/app/new/_layout.tsx b/apps/mobile/src/app/new/_layout.tsx index 2113b13311c..a2ae08b4fb1 100644 --- a/apps/mobile/src/app/new/_layout.tsx +++ b/apps/mobile/src/app/new/_layout.tsx @@ -1,6 +1,10 @@ +import { useRouter } from "expo-router"; import Stack from "expo-router/stack"; +import { SymbolView } from "expo-symbols"; +import { Pressable } from "react-native"; import { useResolveClassNames } from "uniwind"; +import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout"; import { NewTaskFlowProvider } from "../../features/threads/new-task-flow-provider"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -8,7 +12,25 @@ export const unstable_settings = { anchor: "index", }; +function NewTaskCloseButton() { + const router = useRouter(); + const tintColor = useThemeColor("--color-foreground"); + + return ( + router.back()} + > + + + ); +} + export default function NewTaskLayout() { + const { layout } = useAdaptiveWorkspaceLayout(); const sheetStyle = useResolveClassNames("bg-sheet"); const sheetBg = useThemeColor("--color-sheet"); const headerTint = useThemeColor("--color-foreground"); @@ -24,6 +46,7 @@ export default function NewTaskLayout() { headerStyle: { backgroundColor: sheetBg }, headerTintColor: headerTint, headerTitleStyle: { fontFamily: "DMSans_700Bold" }, + headerRight: layout.usesSplitView ? NewTaskCloseButton : undefined, }} > diff --git a/apps/mobile/src/app/new/index.tsx b/apps/mobile/src/app/new/index.tsx index 6e2aa64ce11..41a7963807d 100644 --- a/apps/mobile/src/app/new/index.tsx +++ b/apps/mobile/src/app/new/index.tsx @@ -12,6 +12,7 @@ import { useProjects, useThreadShells } from "../../state/entities"; import type { WorkspaceState } from "../../state/workspaceModel"; import { useWorkspaceState } from "../../state/workspace"; import { groupProjectsByRepository } from "../../lib/repositoryGroups"; +import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout"; function deriveProjectEmptyState(catalogState: WorkspaceState): { readonly title: string; @@ -73,6 +74,7 @@ export default function NewTaskRoute() { const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); const router = useRouter(); + const { layout } = useAdaptiveWorkspaceLayout(); const insets = useSafeAreaInsets(); const chevronColor = useThemeColor("--color-chevron"); const accentColor = useThemeColor("--color-icon-muted"); @@ -110,6 +112,14 @@ export default function NewTaskRoute() { + {layout.usesSplitView ? ( + router.back()} + separateBackground + /> + ) : null} router.push("/new/add-project")} diff --git a/apps/mobile/src/app/settings/index.tsx b/apps/mobile/src/app/settings/index.tsx index 41799ae7b8b..76b90108163 100644 --- a/apps/mobile/src/app/settings/index.tsx +++ b/apps/mobile/src/app/settings/index.tsx @@ -25,6 +25,8 @@ import { hasCloudPublicConfig, resolveRelayClerkTokenOptions, } from "../../features/cloud/publicConfig"; +import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout"; +import { WorkspaceSidebarToolbar } from "../../features/layout/workspace-sidebar-toolbar"; import { runtime } from "../../lib/runtime"; import { loadPreferences } from "../../lib/storage"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -34,7 +36,25 @@ type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported"; type LiveActivityStatus = "checking" | "enabled" | "disabled" | "signed-out" | "linking"; export default function SettingsRouteScreen() { - return hasCloudPublicConfig() ? : ; + const router = useRouter(); + const { layout } = useAdaptiveWorkspaceLayout(); + + return ( + <> + + {layout.usesSplitView ? ( + + router.back()} + separateBackground + /> + + ) : null} + {hasCloudPublicConfig() ? : } + + ); } function LocalSettingsRouteScreen() { diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx index 92e90920e2d..6f2fb22833f 100644 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +++ b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx @@ -1,8 +1,10 @@ import Stack from "expo-router/stack"; import { StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; +import { useAdaptiveWorkspaceLayout } from "../../../../features/layout/AdaptiveWorkspaceLayout"; export default function ThreadLayout() { + const { fileInspector } = useAdaptiveWorkspaceLayout(); const sheetStyle = StyleSheet.flatten(useResolveClassNames("bg-sheet")); const headerBg = { backgroundColor: (sheetStyle as { backgroundColor?: string })?.backgroundColor, @@ -58,8 +60,9 @@ export default function ThreadLayout() { { expect(expoMocks.requireNativeView).not.toHaveBeenCalled(); }); - it("returns the native review diff view when the view config is installed", async () => { + it("returns the payload bridge when the native review diff view is installed", async () => { setExpoViewConfigAvailable(); expoMocks.requireNativeView.mockReturnValue(nativeView); const { resolveNativeReviewDiffView } = await import("./nativeReviewDiffSurface"); - expect(resolveNativeReviewDiffView()).toBe(nativeView); + const resolvedView = resolveNativeReviewDiffView(); + expect(resolvedView).not.toBeNull(); + expect(resolvedView).not.toBe(nativeView); + expect(resolveNativeReviewDiffView()).toBe(resolvedView); expect(expoMocks.requireNativeView).toHaveBeenCalledWith("T3ReviewDiffSurface"); }); diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index 7660a047752..87fd51565d4 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -1,4 +1,11 @@ -import type { ComponentType } from "react"; +import { + createElement, + useEffect, + useImperativeHandle, + useRef, + type ComponentType, + type Ref, +} from "react"; import type { NativeSyntheticEvent, ViewProps } from "react-native"; import { requireNativeView } from "expo"; @@ -103,6 +110,7 @@ export interface NativeReviewDiffViewProps extends ViewProps { readonly tokensJson?: string; readonly tokensPatchJson?: string; readonly tokensResetKey?: string; + readonly contentResetKey?: string; readonly collapsedFileIdsJson?: string; readonly viewedFileIdsJson?: string; readonly selectedRowIdsJson?: string; @@ -113,7 +121,11 @@ export interface NativeReviewDiffViewProps extends ViewProps { readonly rowHeight: number; readonly contentWidth: number; readonly initialRowIndex?: number; + readonly nativeViewRef?: Ref; readonly onDebug?: (event: NativeSyntheticEvent>) => void; + readonly onVisibleFileChange?: ( + event: NativeSyntheticEvent<{ readonly fileId?: string }>, + ) => void; readonly onToggleFile?: (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => void; readonly onToggleViewedFile?: (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => void; readonly onPressLine?: ( @@ -129,18 +141,125 @@ export interface NativeReviewDiffViewProps extends ViewProps { readonly onToggleComment?: (event: NativeSyntheticEvent<{ readonly commentId?: string }>) => void; } -let cachedNativeReviewDiffView: ComponentType | undefined; +export interface NativeReviewDiffViewHandle { + readonly scrollToFile: (fileId: string, animated?: boolean) => Promise; + readonly scrollToTop: (animated?: boolean) => Promise; +} + +interface NativeReviewDiffViewRef { + readonly setRowsJson: (rowsJson: string) => Promise; + readonly setTokensJson: (tokensJson: string) => Promise; + readonly setTokensPatchJson: (tokensPatchJson: string) => Promise; + readonly scrollToFile: (fileId: string, animated: boolean) => Promise; + readonly scrollToTop: (animated: boolean) => Promise; +} + +type NativeReviewDiffRawViewProps = Omit< + NativeReviewDiffViewProps, + "nativeViewRef" | "rowsJson" | "tokensJson" | "tokensPatchJson" +> & { + readonly ref?: Ref; +}; + +let cachedNativeReviewDiffRawView: ComponentType | undefined; let nativeReviewDiffViewResolutionFailed = false; +type NativeReviewDiffPayloadMethod = "setRowsJson" | "setTokensJson" | "setTokensPatchJson"; + +function isPendingNativeViewRegistration(error: unknown): boolean { + return ( + error instanceof Error && error.message.includes("Unable to find the 'T3ReviewDiffView' view") + ); +} + +function useNativeReviewDiffPayload( + nativeRef: React.RefObject, + method: NativeReviewDiffPayloadMethod, + payload: string | undefined, +) { + useEffect(() => { + if (payload === undefined) { + return; + } + + let cancelled = false; + let frame: number | null = null; + let attempts = 0; + + const dispatch = () => { + if (cancelled) { + return; + } + + const view = nativeRef.current; + const command = view?.[method]; + if (!view || !command) { + if (attempts < 4) { + attempts += 1; + frame = requestAnimationFrame(dispatch); + } + return; + } + + void command.call(view, payload).catch((error: unknown) => { + if (!cancelled && attempts < 4 && isPendingNativeViewRegistration(error)) { + attempts += 1; + frame = requestAnimationFrame(dispatch); + return; + } + console.error(`[native-review-diff] ${method} failed`, error); + }); + }; + + // Fabric attaches the React ref before Expo registers the native tag used by + // view functions. Starting on the next frame avoids racing that registration. + frame = requestAnimationFrame(dispatch); + + return () => { + cancelled = true; + if (frame !== null) { + cancelAnimationFrame(frame); + } + }; + }, [method, nativeRef, payload]); +} + function getExpoViewConfig(moduleName: string) { return (globalThis as typeof globalThis & ExpoGlobalWithViewConfig).expo?.getViewConfig?.( moduleName, ); } +function NativeReviewDiffView(props: NativeReviewDiffViewProps) { + const { nativeViewRef, rowsJson, tokensJson, tokensPatchJson, ...nativeProps } = props; + const nativeRef = useRef(null); + useNativeReviewDiffPayload(nativeRef, "setRowsJson", rowsJson); + useNativeReviewDiffPayload(nativeRef, "setTokensJson", tokensJson); + useNativeReviewDiffPayload(nativeRef, "setTokensPatchJson", tokensPatchJson); + useImperativeHandle( + nativeViewRef, + () => ({ + scrollToFile: async (fileId, animated = true) => { + await nativeRef.current?.scrollToFile(fileId, animated); + }, + scrollToTop: async (animated = true) => { + await nativeRef.current?.scrollToTop(animated); + }, + }), + [], + ); + + const RawNativeView = cachedNativeReviewDiffRawView; + if (!RawNativeView) { + return null; + } + + return createElement(RawNativeView, { ...nativeProps, ref: nativeRef }); +} + export function resolveNativeReviewDiffView(): ComponentType | null { - if (cachedNativeReviewDiffView) { - return cachedNativeReviewDiffView; + if (cachedNativeReviewDiffRawView) { + return NativeReviewDiffView; } if (nativeReviewDiffViewResolutionFailed) { @@ -152,7 +271,7 @@ export function resolveNativeReviewDiffView(): ComponentType( + cachedNativeReviewDiffRawView = requireNativeView( NATIVE_REVIEW_DIFF_MODULE_NAME, ); } catch (cause) { @@ -166,5 +285,5 @@ export function resolveNativeReviewDiffView(): ComponentType, ReadonlyArray>(); +const FILE_TREE_INITIAL_RENDER_COUNT = 20; +const FILE_TREE_RENDER_BATCH_SIZE = 12; + +function cachedFileTree(entries: ReadonlyArray): ReadonlyArray { + const cached = fileTreeCache.get(entries); + if (cached !== undefined) { + return cached; + } + const tree = buildFileTree(entries); + fileTreeCache.set(entries, tree); + return tree; +} + function ancestorPaths(path: string): ReadonlyArray { const parts = path.split("/").filter(Boolean); const ancestors: string[] = []; @@ -25,19 +40,24 @@ function ancestorPaths(path: string): ReadonlyArray { const FileTreeRow = memo(function FileTreeRow(props: { readonly item: VisibleFileTreeNode; - readonly selectedPath: string | null; + readonly selected: boolean; readonly expanded: boolean; readonly iconColor: string; readonly onPressDirectory: (path: string) => void; + readonly onPreviewFile?: (path: string) => void; readonly onPressFile: (path: string) => void; }) { const { node, depth } = props.item; - const selected = node.kind === "file" && node.path === props.selectedPath; return ( { + if (node.kind === "file") { + props.onPreviewFile?.(node.path); + } + }} onPress={() => { if (node.kind === "directory") { props.onPressDirectory(node.path); @@ -47,7 +67,7 @@ const FileTreeRow = memo(function FileTreeRow(props: { }} className={cn( "mx-2 min-h-[42px] flex-row items-center gap-2 rounded-[12px] px-2 active:bg-subtle", - selected && "bg-subtle-strong", + props.selected && "bg-subtle-strong", )} style={{ paddingLeft: 8 + depth * 18 }} > @@ -65,7 +85,9 @@ const FileTreeRow = memo(function FileTreeRow(props: { @@ -86,13 +108,25 @@ export function FileTreeBrowser(props: { readonly isPending: boolean; readonly searchQuery: string; readonly selectedPath: string | null; + readonly onPreviewFile?: (path: string) => void; readonly onRefresh: () => void; readonly onSelectFile: (path: string) => void; }) { const [expandedPaths, setExpandedPaths] = useState>(() => new Set()); + const [pendingSelection, setPendingSelection] = useState<{ + readonly path: string; + readonly selectedPathAtPress: string | null; + } | null>(null); const iconColor = String(useThemeColor("--color-icon-muted")); + const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props; + const controlledSelectedPathRef = useRef(controlledSelectedPath); + controlledSelectedPathRef.current = controlledSelectedPath; - const tree = useMemo(() => buildFileTree(props.entries), [props.entries]); + const selectedPath = + pendingSelection?.selectedPathAtPress === controlledSelectedPath + ? pendingSelection.path + : controlledSelectedPath; + const tree = useMemo(() => cachedFileTree(props.entries), [props.entries]); const defaultExpanded = useMemo(() => defaultExpandedTreePaths(tree), [tree]); const visibleNodes = useMemo( () => @@ -114,17 +148,21 @@ export function FileTreeBrowser(props: { }, [defaultExpanded]); useEffect(() => { - if (!props.selectedPath) { + if (!controlledSelectedPath) { return; } setExpandedPaths((current) => { + const ancestors = ancestorPaths(controlledSelectedPath); + if (ancestors.every((ancestor) => current.has(ancestor))) { + return current; + } const next = new Set(current); - for (const ancestor of ancestorPaths(props.selectedPath ?? "")) { + for (const ancestor of ancestors) { next.add(ancestor); } return next; }); - }, [props.selectedPath]); + }, [controlledSelectedPath]); const toggleDirectory = useCallback((path: string) => { setExpandedPaths((current) => { @@ -137,6 +175,30 @@ export function FileTreeBrowser(props: { return next; }); }, []); + const handleSelectFile = useCallback( + (path: string) => { + setPendingSelection({ + path, + selectedPathAtPress: controlledSelectedPathRef.current, + }); + onSelectFile(path); + }, + [onSelectFile], + ); + const renderItem = useCallback( + ({ item }: { readonly item: VisibleFileTreeNode }) => ( + + ), + [expandedPaths, handleSelectFile, iconColor, onPreviewFile, selectedPath, toggleDirectory], + ); return ( @@ -152,20 +214,15 @@ export function FileTreeBrowser(props: { contentInsetAdjustmentBehavior="automatic" keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" + initialNumToRender={FILE_TREE_INITIAL_RENDER_COUNT} + maxToRenderPerBatch={FILE_TREE_RENDER_BATCH_SIZE} + updateCellsBatchingPeriod={16} + windowSize={5} contentContainerStyle={{ paddingVertical: 8 }} refreshControl={ } - renderItem={({ item }) => ( - - )} + renderItem={renderItem} ListEmptyComponent={ {props.isPending ? ( diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index b96d6515951..d1644ae2a81 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -20,13 +20,13 @@ import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter"; import { cn } from "../../lib/cn"; import { MOBILE_CODE_SURFACE } from "../../lib/typography"; import { - buildNativeSourceRows, buildNativeSourceTokens, NATIVE_SOURCE_CONTENT_WIDTH, NATIVE_SOURCE_ROW_HEIGHT, NATIVE_SOURCE_STYLE, nativeSourceRowId, } from "./nativeSourceFileAdapter"; +import { prepareSourceFileDocument } from "./source-file-document"; import { sourceHighlightAtom } from "./sourceHighlightingState"; const SOURCE_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight; @@ -41,10 +41,6 @@ interface SourceFileSurfaceProps { type SourceHighlightStatus = "highlighting" | "ready" | "error"; -function splitSourceLines(contents: string): ReadonlyArray { - return contents.replace(/\r\n?/g, "\n").split("\n"); -} - const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { readonly index: number; readonly line: string; @@ -119,11 +115,8 @@ const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { function useSourceFileModel(props: SourceFileSurfaceProps) { const colorScheme = useColorScheme(); const theme: "dark" | "light" = colorScheme === "dark" ? "dark" : "light"; - const normalizedContents = useMemo( - () => props.contents.replace(/\r\n?/g, "\n"), - [props.contents], - ); - const lines = useMemo(() => splitSourceLines(normalizedContents), [normalizedContents]); + const document = useMemo(() => prepareSourceFileDocument(props.contents), [props.contents]); + const { contents: normalizedContents, lines, rowsJson } = document; const targetIndex = props.initialLine !== null && props.initialLine !== undefined && props.initialLine > 0 ? Math.min(Math.floor(props.initialLine) - 1, Math.max(0, lines.length - 1)) @@ -140,7 +133,7 @@ function useSourceFileModel(props: SourceFileSurfaceProps) { ? "ready" : "highlighting"; - return { lines, status, targetIndex, theme, tokens }; + return { lines, rowsJson, status, targetIndex, theme, tokens }; } function SourceHighlightStatusView(props: { readonly status: SourceHighlightStatus }) { @@ -163,8 +156,7 @@ function NativeSourceFileSurface( }, ) { const { NativeView } = props; - const { lines, status, targetIndex, theme, tokens } = useSourceFileModel(props); - const rowsJson = useMemo(() => JSON.stringify(buildNativeSourceRows(lines)), [lines]); + const { rowsJson, status, targetIndex, theme, tokens } = useSourceFileModel(props); const tokensJson = useMemo(() => JSON.stringify(buildNativeSourceTokens(tokens)), [tokens]); const selectedRowIdsJson = useMemo( () => JSON.stringify(targetIndex === null ? [] : [nativeSourceRowId(targetIndex)]), @@ -176,11 +168,11 @@ function NativeSourceFileSurface( { + if (router.canGoBack()) { + router.back(); + return; + } + if (environmentId !== null && threadId !== null) { + router.replace(buildThreadRoutePath({ environmentId, threadId })); + } + }, [environmentId, router, threadId]); const handleSelectFile = useCallback( (path: string) => { if (environmentId === null || threadId === null) { return; } - router.push(buildThreadFilesNavigation({ environmentId, threadId }, path)); + const destination = buildThreadFilesNavigation({ environmentId, threadId }, path); + const navigationAction = resolveFileSelectionNavigationAction({ + hasPersistentFileInspector: fileInspector.supported, + }); + if (navigationAction === "replace") { + router.replace(destination); + return; + } + router.push(destination); + }, + [environmentId, fileInspector.supported, router, threadId], + ); + const renderInspector = useCallback( + () => + environmentId !== null && cwd !== null ? ( + + ) : null, + [cwd, environmentId, handleSelectFile, headerHeight, projectName], + ); + const handlePreviewFile = useCallback( + (relativePath: string) => { + if (environmentId === null || cwd === null) { + return; + } + preloadWorkspaceFileContents({ + cwd, + environmentId, + relativePath, + theme: highlightTheme, + }); }, - [environmentId, router, threadId], + [cwd, environmentId, highlightTheme], + ); + const renderHeaderTitle = useCallback( + () => , + [projectName], ); if (selectedThread === null || environmentId === null || threadId === null) { @@ -474,6 +546,15 @@ export function ThreadFilesTreeScreen() { return ; } + if (fileInspector.supported) { + return ( + + ); + } + return ( , + headerTitle: renderHeaderTitle, headerSearchBarOptions: { allowToolbarIntegration: true, autoCapitalize: "none", @@ -499,6 +580,18 @@ export function ThreadFilesTreeScreen() { }} /> + {layout.usesSplitView ? ( + + ) : null} @@ -523,6 +617,9 @@ export function ThreadFilesTreeScreen() { } export function ThreadFileScreen() { + useAdaptiveWorkspacePaneRole("inspector"); + const router = useRouter(); + const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); const params = useLocalSearchParams<{ line?: string | string[]; path?: string | string[]; @@ -568,6 +665,33 @@ export function ThreadFileScreen() { ); const fileData = fileQuery.data as ProjectReadFileResult | null; + const handleSelectFile = useCallback( + (path: string) => { + // We are already on the catch-all file route. Updating its params keeps + // the current native screen mounted while replacing the selected file in + // place, avoiding an RNSScreen snapshot/unmount for every tree click. + router.setParams({ + line: undefined, + path: path.split("/").filter(Boolean), + }); + }, + [router], + ); + const renderInspector = useCallback( + () => + fileInspector.supported && environmentId !== null && cwd !== null ? ( + + ) : undefined, + [cwd, environmentId, fileInspector.supported, handleSelectFile, projectName, relativePath], + ); + if (selectedThread === null || environmentId === null || threadId === null) { return ; } @@ -588,9 +712,37 @@ export function ThreadFileScreen() { return ( - + + + {fileInspector.supported ? ( + { + if (router.canGoBack()) { + router.back(); + return; + } + router.replace(buildThreadRoutePath({ environmentId, threadId })); + }} + /> + ) : null} + + {fileInspector.supported ? ( + + ) : null} { if (resolvedActiveMode === "preview" && (isBrowserFile || isImageFile)) { @@ -601,25 +753,29 @@ export function ThreadFileScreen() { }} /> - { - setModeOverride({ path: relativePath, mode }); - }} - /> - + + { + setModeOverride({ path: relativePath, mode }); + }} + /> + + ); diff --git a/apps/mobile/src/features/files/preload-workspace-file.ts b/apps/mobile/src/features/files/preload-workspace-file.ts new file mode 100644 index 00000000000..b9e21cfd98f --- /dev/null +++ b/apps/mobile/src/features/files/preload-workspace-file.ts @@ -0,0 +1,74 @@ +import { executeAtomQuery } from "@t3tools/client-runtime/state/runtime"; +import type { EnvironmentId } from "@t3tools/contracts"; + +import { appAtomRegistry } from "../../state/atom-registry"; +import { projectEnvironment } from "../../state/projects"; +import { isBrowserPreviewFile, isImagePreviewFile } from "./filePath"; +import { prepareSourceFileDocument } from "./source-file-document"; +import { sourceHighlightAtom } from "./sourceHighlightingState"; +import type { ReviewDiffTheme } from "../review/shikiReviewHighlighter"; + +const inFlightPreloads = new Map>(); +const MAX_HIGHLIGHT_PRELOAD_CHARACTERS = 256 * 1024; + +function preloadKey(input: { + readonly cwd: string; + readonly environmentId: EnvironmentId; + readonly relativePath: string; +}): string { + return JSON.stringify([input.environmentId, input.cwd, input.relativePath]); +} + +export function preloadWorkspaceFileContents(input: { + readonly cwd: string; + readonly environmentId: EnvironmentId; + readonly relativePath: string; + readonly theme: ReviewDiffTheme; +}): void { + if (isBrowserPreviewFile(input.relativePath) || isImagePreviewFile(input.relativePath)) { + return; + } + + const key = preloadKey(input); + if (inFlightPreloads.has(key)) { + return; + } + + const preload = executeAtomQuery( + appAtomRegistry, + projectEnvironment.readFile({ + environmentId: input.environmentId, + input: { cwd: input.cwd, relativePath: input.relativePath }, + }), + { + label: "workspace file preload", + reportDefect: false, + reportFailure: false, + }, + ) + .then(async (result) => { + if (result._tag === "Success") { + const document = prepareSourceFileDocument(result.value.contents); + if (document.contents.length <= MAX_HIGHLIGHT_PRELOAD_CHARACTERS) { + await executeAtomQuery( + appAtomRegistry, + sourceHighlightAtom({ + path: input.relativePath, + contents: document.contents, + theme: input.theme, + }), + { + label: "workspace source highlight preload", + reportDefect: false, + reportFailure: false, + }, + ); + } + } + }) + .finally(() => { + inFlightPreloads.delete(key); + }); + + inFlightPreloads.set(key, preload); +} diff --git a/apps/mobile/src/features/files/source-file-document.test.ts b/apps/mobile/src/features/files/source-file-document.test.ts new file mode 100644 index 00000000000..04b3046994a --- /dev/null +++ b/apps/mobile/src/features/files/source-file-document.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { prepareSourceFileDocument } from "./source-file-document"; + +describe("prepareSourceFileDocument", () => { + it("normalizes and serializes source rows once for repeated consumers", () => { + const first = prepareSourceFileDocument("const value = 1;\r\n\tvalue;\r"); + const second = prepareSourceFileDocument("const value = 1;\r\n\tvalue;\r"); + const rows = JSON.parse(first.rowsJson) as ReadonlyArray<{ readonly content: string }>; + + expect(first.contents).toBe("const value = 1;\n\tvalue;\n"); + expect(first.lines).toEqual(["const value = 1;", "\tvalue;", ""]); + expect(rows.map((row) => row.content)).toEqual(["const value = 1;", " value;", ""]); + expect(second).toBe(first); + }); +}); diff --git a/apps/mobile/src/features/files/source-file-document.ts b/apps/mobile/src/features/files/source-file-document.ts new file mode 100644 index 00000000000..d78ead3288f --- /dev/null +++ b/apps/mobile/src/features/files/source-file-document.ts @@ -0,0 +1,54 @@ +import { buildNativeSourceRows } from "./nativeSourceFileAdapter"; + +const MAX_CACHED_DOCUMENTS = 8; +const MAX_CACHED_CHARACTERS = 4 * 1024 * 1024; + +export interface SourceFileDocument { + readonly contents: string; + readonly lines: ReadonlyArray; + readonly rowsJson: string; +} + +const documentCache = new Map(); +let cachedCharacterCount = 0; + +function removeOldestCachedDocument(): void { + const oldestKey = documentCache.keys().next().value; + if (typeof oldestKey !== "string") { + return; + } + const document = documentCache.get(oldestKey); + documentCache.delete(oldestKey); + cachedCharacterCount -= (document?.contents.length ?? 0) + (document?.rowsJson.length ?? 0); +} + +export function prepareSourceFileDocument(contents: string): SourceFileDocument { + const cached = documentCache.get(contents); + if (cached !== undefined) { + documentCache.delete(contents); + documentCache.set(contents, cached); + return cached; + } + + const normalizedContents = contents.replace(/\r\n?/g, "\n"); + const lines = normalizedContents.split("\n"); + const document = { + contents: normalizedContents, + lines, + rowsJson: JSON.stringify(buildNativeSourceRows(lines)), + } satisfies SourceFileDocument; + const characterCount = document.contents.length + document.rowsJson.length; + + if (characterCount <= MAX_CACHED_CHARACTERS) { + while ( + documentCache.size >= MAX_CACHED_DOCUMENTS || + cachedCharacterCount + characterCount > MAX_CACHED_CHARACTERS + ) { + removeOldestCachedDocument(); + } + documentCache.set(contents, document); + cachedCharacterCount += characterCount; + } + + return document; +} diff --git a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx new file mode 100644 index 00000000000..c11095b7254 --- /dev/null +++ b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx @@ -0,0 +1,90 @@ +import type { EnvironmentId, ProjectListEntriesResult } from "@t3tools/contracts"; +import { SymbolView } from "expo-symbols"; +import { useCallback, useState } from "react"; +import { Pressable, useColorScheme, View } from "react-native"; + +import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; +import { useThemeColor } from "../../lib/useThemeColor"; +import { projectEnvironment } from "../../state/projects"; +import { useEnvironmentQuery } from "../../state/query"; +import { FileTreeBrowser } from "./FileTreeBrowser"; +import { preloadWorkspaceFileContents } from "./preload-workspace-file"; + +export function ThreadFileNavigatorPane(props: { + readonly cwd: string; + readonly environmentId: EnvironmentId; + readonly headerInset: number; + readonly projectName: string; + readonly selectedPath: string | null; + readonly onSelectFile: (path: string) => void; +}) { + const [searchQuery, setSearchQuery] = useState(""); + const colorScheme = useColorScheme(); + const highlightTheme = colorScheme === "dark" ? "dark" : "light"; + const iconColor = String(useThemeColor("--color-icon-muted")); + const entriesQuery = useEnvironmentQuery( + projectEnvironment.listEntries({ + environmentId: props.environmentId, + input: { cwd: props.cwd }, + }), + ); + const entriesData = entriesQuery.data as ProjectListEntriesResult | null; + const handlePreviewFile = useCallback( + (relativePath: string) => { + preloadWorkspaceFileContents({ + cwd: props.cwd, + environmentId: props.environmentId, + relativePath, + theme: highlightTheme, + }); + }, + [highlightTheme, props.cwd, props.environmentId], + ); + + return ( + + + + + Files + + {props.projectName} + + + + + + + + + + + + + + ); +} diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index cf728867655..825481fb858 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,21 +1,63 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useGlobalSearchParams, useRouter } from "expo-router"; -import { createContext, use, useMemo, type ReactNode } from "react"; +import { useFocusEffect, useGlobalSearchParams, usePathname, useRouter } from "expo-router"; +import { + createContext, + use, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import { useWindowDimensions, View } from "react-native"; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"; -import { deriveLayout, type Layout } from "../../lib/layout"; +import { + deriveFileInspectorPaneLayout, + deriveLayout, + deriveWorkspacePaneLayout, + type FileInspectorPaneLayout, + type Layout, + type WorkspaceAuxiliaryPaneRole, + type WorkspacePaneLayout, +} from "../../lib/layout"; +import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation"; import { buildThreadRoutePath } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; +import { WORKSPACE_PANE_LAYOUT_TRANSITION } from "./workspace-pane-transition"; interface AdaptiveWorkspaceContextValue { readonly layout: Layout; + readonly panes: WorkspacePaneLayout; + readonly fileInspector: FileInspectorPaneLayout; + readonly activateAuxiliaryPaneRole: (role: WorkspaceAuxiliaryPaneRole) => () => void; + readonly showAuxiliaryPane: (role: WorkspaceAuxiliaryPaneRole) => void; + readonly toggleAuxiliaryPane: () => void; + readonly togglePrimarySidebar: () => void; } const compactLayout = deriveLayout({ width: 0, height: 0 }); +const compactPanes = deriveWorkspacePaneLayout({ + layout: compactLayout, + viewportWidth: 0, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, +}); +const compactFileInspector = deriveFileInspectorPaneLayout({ + layout: compactLayout, + viewportWidth: 0, +}); const AdaptiveWorkspaceContext = createContext({ layout: compactLayout, + panes: compactPanes, + fileInspector: compactFileInspector, + activateAuxiliaryPaneRole: () => () => undefined, + showAuxiliaryPane: () => undefined, + toggleAuxiliaryPane: () => undefined, + togglePrimarySidebar: () => undefined, }); function firstRouteParam(value: string | string[] | undefined): string | null { @@ -26,39 +68,186 @@ export function useAdaptiveWorkspaceLayout(): AdaptiveWorkspaceContextValue { return use(AdaptiveWorkspaceContext); } +export function useAdaptiveWorkspacePaneRole(role: WorkspaceAuxiliaryPaneRole) { + const { activateAuxiliaryPaneRole } = useAdaptiveWorkspaceLayout(); + + useFocusEffect( + useCallback(() => activateAuxiliaryPaneRole(role), [activateAuxiliaryPaneRole, role]), + ); +} + export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) { const { width, height } = useWindowDimensions(); + const pathname = usePathname(); const router = useRouter(); + const activeRoleOwner = useRef(null); + const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); + const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); + const [fileInspectorPreferredVisible, setFileInspectorPreferredVisible] = useState(true); + const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = + useState(null); + const sidebarProgress = useSharedValue(1); const params = useGlobalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); const layout = useMemo(() => deriveLayout({ width, height }), [height, width]); + const fileInspector = useMemo( + () => deriveFileInspectorPaneLayout({ layout, viewportWidth: width }), + [layout, width], + ); + const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole = + focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary"); + const auxiliaryPanePreferredVisible = + auxiliaryPaneRole === "inspector" + ? fileInspectorPreferredVisible + : supplementaryPanePreferredVisible; + const panes = useMemo( + () => + deriveWorkspacePaneLayout({ + layout, + viewportWidth: width, + primarySidebarPreferredVisible, + auxiliaryPanePreferredVisible, + auxiliaryPaneRole, + }), + [ + auxiliaryPanePreferredVisible, + auxiliaryPaneRole, + layout, + primarySidebarPreferredVisible, + width, + ], + ); const environmentId = firstRouteParam(params.environmentId); const threadId = firstRouteParam(params.threadId); const selectedThreadKey = environmentId !== null && threadId !== null ? scopedThreadKey(EnvironmentId.make(environmentId), ThreadId.make(threadId)) : null; - const contextValue = useMemo(() => ({ layout }), [layout]); + const activateAuxiliaryPaneRole = useCallback((role: WorkspaceAuxiliaryPaneRole) => { + const owner = Symbol(role); + activeRoleOwner.current = owner; + setFocusedAuxiliaryPaneRole(role); + + return () => { + if (activeRoleOwner.current !== owner) { + return; + } + activeRoleOwner.current = null; + setFocusedAuxiliaryPaneRole(null); + }; + }, []); + const togglePrimarySidebar = useCallback(() => { + if (!panes.primarySidebarVisible && panes.primarySidebarSuppressedByAuxiliary) { + setFileInspectorPreferredVisible(false); + setPrimarySidebarPreferredVisible(true); + return; + } + setPrimarySidebarPreferredVisible((current) => !current); + }, [panes.primarySidebarSuppressedByAuxiliary, panes.primarySidebarVisible]); + const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => { + if (role === "inspector") { + setFileInspectorPreferredVisible(true); + return; + } + setSupplementaryPanePreferredVisible(true); + }, []); + const toggleAuxiliaryPane = useCallback(() => { + if (auxiliaryPaneRole === "inspector") { + setFileInspectorPreferredVisible((current) => !current); + return; + } + setSupplementaryPanePreferredVisible((current) => !current); + }, [auxiliaryPaneRole]); + const contextValue = useMemo( + () => ({ + layout, + panes, + fileInspector, + activateAuxiliaryPaneRole, + showAuxiliaryPane, + toggleAuxiliaryPane, + togglePrimarySidebar, + }), + [ + activateAuxiliaryPaneRole, + fileInspector, + layout, + panes, + showAuxiliaryPane, + toggleAuxiliaryPane, + togglePrimarySidebar, + ], + ); + + useEffect(() => { + sidebarProgress.value = withTiming(panes.primarySidebarVisible ? 1 : 0, { duration: 220 }); + }, [panes.primarySidebarVisible, sidebarProgress]); + const sidebarStyle = useAnimatedStyle(() => ({ + opacity: sidebarProgress.value, + transform: [{ translateX: (sidebarProgress.value - 1) * 24 }], + })); const handleSelectThread = (thread: EnvironmentThreadShell) => { - router.replace(buildThreadRoutePath(thread)); + const destination = buildThreadRoutePath(thread); + const navigationAction = resolveThreadSelectionNavigationAction({ + usesSplitView: layout.usesSplitView, + pathname, + }); + if (navigationAction === "set-params") { + // Auxiliary content belongs to the current thread. Close it before + // reusing the current native detail screen for a peer thread selection. + setFileInspectorPreferredVisible(false); + router.setParams({ + environmentId: String(thread.environmentId), + threadId: String(thread.id), + }); + return; + } + if (navigationAction === "replace") { + setFileInspectorPreferredVisible(false); + router.replace(destination); + return; + } + router.push(destination); }; return ( {layout.usesSplitView && layout.listPaneWidth !== null ? ( - router.push("/settings")} - onSelectThread={handleSelectThread} - onStartNewTask={() => router.push("/new")} - /> + + router.push("/settings")} + onSelectThread={handleSelectThread} + onStartNewTask={() => router.push("/new")} + /> + ) : null} - {props.children} + + {props.children} + ); diff --git a/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx b/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx new file mode 100644 index 00000000000..0920f409ebc --- /dev/null +++ b/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx @@ -0,0 +1,72 @@ +import { useEffect, type ReactNode } from "react"; +import { View } from "react-native"; +import Animated, { + Easing, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; + +import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; +import { WORKSPACE_PANE_LAYOUT_TRANSITION } from "./workspace-pane-transition"; + +export function AdaptiveInspectorLayout(props: { + readonly children: ReactNode; + readonly renderInspector?: () => ReactNode; +}) { + const { panes } = useAdaptiveWorkspaceLayout(); + const inspectorWidth = panes.auxiliaryPaneWidth; + const inspectorSupported = props.renderInspector !== undefined && inspectorWidth !== null; + const inspectorVisible = inspectorSupported && panes.auxiliaryPaneVisible; + + // A file-to-file replace remounts the route. Initialize an already-visible + // inspector at its final position so route replacement never replays an + // entering transition. Only an explicit visibility change animates it. + const inspectorProgress = useSharedValue(inspectorVisible ? 1 : 0); + + useEffect(() => { + inspectorProgress.value = withTiming(inspectorVisible ? 1 : 0, { + duration: inspectorVisible ? 220 : 160, + easing: inspectorVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), + }); + }, [inspectorProgress, inspectorVisible]); + + const inspectorStyle = useAnimatedStyle( + () => ({ + opacity: inspectorProgress.value, + transform: [{ translateX: (1 - inspectorProgress.value) * 24 }], + }), + [], + ); + + return ( + + + {props.children} + + {inspectorSupported ? ( + + {props.renderInspector?.()} + + ) : null} + + ); +} diff --git a/apps/mobile/src/features/layout/workspace-pane-transition.ts b/apps/mobile/src/features/layout/workspace-pane-transition.ts new file mode 100644 index 00000000000..d688b6b739b --- /dev/null +++ b/apps/mobile/src/features/layout/workspace-pane-transition.ts @@ -0,0 +1,10 @@ +import { Easing, LinearTransition } from "react-native-reanimated"; + +/** + * Animates between final Yoga layouts on the UI thread. Keeping pane widths + * out of animated styles avoids recalculating the entire workspace on every + * display frame while a sidebar or inspector moves. + */ +export const WORKSPACE_PANE_LAYOUT_TRANSITION = LinearTransition.duration(220).easing( + Easing.out(Easing.cubic), +); diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx new file mode 100644 index 00000000000..0bc729f6223 --- /dev/null +++ b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx @@ -0,0 +1,26 @@ +import { Stack } from "expo-router"; +import type { ReactNode } from "react"; + +import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; + +export function WorkspaceSidebarToolbar(props: { readonly children?: ReactNode } = {}) { + const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); + + if (!layout.usesSplitView) { + return null; + } + + return ( + + {props.children} + + + ); +} diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 92203c0ed4e..e4bef426693 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -1,10 +1,22 @@ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useLocalSearchParams } from "expo-router"; +import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; import Stack from "expo-router/stack"; import { SymbolView } from "expo-symbols"; -import { memo, type ReactElement, useCallback, useMemo } from "react"; +import { + memo, + type Ref, + type ReactElement, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useRef, + useState, +} from "react"; import { ActivityIndicator, + FlatList, Pressable, ScrollView, type NativeSyntheticEvent, @@ -23,20 +35,30 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; +import { AdaptiveInspectorLayout } from "../layout/adaptive-inspector-layout"; +import { + useAdaptiveWorkspaceLayout, + useAdaptiveWorkspacePaneRole, +} from "../layout/AdaptiveWorkspaceLayout"; import { useReviewCacheForThread } from "./reviewState"; -import { resolveNativeReviewDiffView } from "../diffs/nativeReviewDiffSurface"; +import { + type NativeReviewDiffViewHandle, + resolveNativeReviewDiffView, +} from "../diffs/nativeReviewDiffSurface"; import { NATIVE_REVIEW_DIFF_CONTENT_WIDTH, NATIVE_REVIEW_DIFF_ROW_HEIGHT, } from "./nativeReviewDiffAdapter"; import { useReviewDiffData } from "./useReviewDiffData"; +import { useReviewDiffPrewarming } from "./useReviewDiffPrewarming"; import { useReviewFileVisibility } from "./reviewFileVisibility"; import { useReviewSections } from "./useReviewSections"; import { useNativeReviewDiffBridge } from "./useNativeReviewDiffBridge"; import { useReviewCommentSelectionController } from "./useReviewCommentSelectionController"; import { resolveReviewAvailability } from "./reviewAvailability"; +import { resolveSelectedReviewFileId } from "./reviewPaneSelection"; +import { buildReviewSectionMenu } from "./review-section-menu"; -const IOS_NAV_BAR_HEIGHT = 44; const REVIEW_HEADER_SPACING = 0; const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { @@ -110,8 +132,261 @@ function ReviewSelectionActionBar(props: { ); } +interface ReviewNavigatorFile { + readonly id: string; + readonly path: string; + readonly additions: number; + readonly deletions: number; +} + +const ReviewFileNavigatorRow = memo(function ReviewFileNavigatorRow(props: { + readonly file: ReviewNavigatorFile; + readonly selected: boolean; + readonly onSelectFile: (fileId: string | null) => void; +}) { + const { file, selected, onSelectFile } = props; + const handlePress = useCallback(() => { + onSelectFile(file.id); + }, [file.id, onSelectFile]); + + return ( + + + {file.path} + + + +{file.additions} + -{file.deletions} + + + ); +}); + +interface ReviewFileNavigatorHandle { + readonly setVisibleFile: (fileId: string) => void; +} + +interface ReviewFileNavigatorProps { + readonly files: ReadonlyArray; + readonly headerInset: number; + readonly sectionId: string | null; + readonly onSelectFile: (fileId: string | null) => void; + readonly ref?: Ref; +} + +function ReviewFileNavigator({ + files, + headerInset, + sectionId, + onSelectFile, + ref, +}: ReviewFileNavigatorProps) { + const [fileSelection, setFileSelection] = useState<{ + readonly sectionId: string | null; + readonly fileId: string | null; + }>({ sectionId: null, fileId: null }); + const availableFileIds = useMemo(() => files.map((file) => file.id), [files]); + const selectedFileId = resolveSelectedReviewFileId({ + selection: fileSelection, + sectionId, + availableFileIds, + }); + + useImperativeHandle( + ref, + () => ({ + setVisibleFile: (fileId) => { + if (!availableFileIds.includes(fileId)) { + return; + } + setFileSelection((current) => { + if (current.sectionId === sectionId && current.fileId === fileId) { + return current; + } + return { sectionId, fileId }; + }); + }, + }), + [availableFileIds, sectionId], + ); + + const handleSelectFile = useCallback( + (fileId: string | null) => { + setFileSelection({ sectionId, fileId }); + onSelectFile(fileId); + }, + [onSelectFile, sectionId], + ); + + const renderFile = useCallback( + ({ item }: { readonly item: ReviewNavigatorFile }) => ( + + ), + [handleSelectFile, selectedFileId], + ); + + return ( + + + + Changed files + + {files.length} {files.length === 1 ? "file" : "files"} + + + + file.id} + contentContainerStyle={{ paddingHorizontal: 8, paddingVertical: 8 }} + ListHeaderComponent={ + handleSelectFile(null)} + > + All files + + {files.length} changed {files.length === 1 ? "file" : "files"} + + + } + renderItem={renderFile} + /> + + ); +} + +function ReviewHeaderTitle(props: { + readonly additions: string | null; + readonly deletions: string | null; + readonly foregroundColor: string; + readonly mutedColor: string; + readonly pendingCommentCount: number; + readonly sectionTitle: string; +}) { + return ( + + + Files Changed + + + {props.additions && props.deletions ? ( + <> + + {props.additions} + + + {props.deletions} + + {props.pendingCommentCount > 0 ? ( + + {props.pendingCommentCount} pending + + ) : null} + + ) : ( + + + {props.sectionTitle} + + {props.pendingCommentCount > 0 ? ( + + {props.pendingCommentCount} pending + + ) : null} + + )} + + + ); +} + export function ReviewSheet() { + useAdaptiveWorkspacePaneRole("inspector"); + const { layout, panes, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar } = + useAdaptiveWorkspaceLayout(); const insets = useSafeAreaInsets(); + const headerHeight = useHeaderHeight(); const colorScheme = useColorScheme(); const headerForeground = String(useThemeColor("--color-foreground")); const headerMuted = String(useThemeColor("--color-foreground-muted")); @@ -126,7 +401,11 @@ export function ReviewSheet() { const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); const reviewCache = useReviewCacheForThread({ environmentId, threadId }); const selectedTheme = colorScheme === "dark" ? "dark" : "light"; - const topContentInset = insets.top + IOS_NAV_BAR_HEIGHT; + const topContentInset = headerHeight; + + useEffect(() => { + showAuxiliaryPane("inspector"); + }, [environmentId, showAuxiliaryPane, threadId]); const { error, loadingGitDiffs, @@ -141,6 +420,11 @@ export function ReviewSheet() { threadId, reviewCache, }); + useReviewDiffPrewarming({ + threadKey: reviewCache.threadKey, + sections: reviewSections, + selectedSectionId: selectedSection?.id ?? null, + }); const { headerDiffSummary, nativeReviewDiffData, parsedDiff, pendingReviewCommentCount } = useReviewDiffData({ threadKey: reviewCache.threadKey, @@ -148,6 +432,8 @@ export function ReviewSheet() { draftMessage, }); const NativeReviewDiffView = resolveNativeReviewDiffView()!; + const nativeReviewDiffViewRef = useRef(null); + const reviewFileNavigatorRef = useRef(null); const reviewFiles = parsedDiff.kind === "files" ? parsedDiff.files : []; const fileVisibility = useReviewFileVisibility({ threadKey: reviewCache.threadKey, @@ -179,6 +465,45 @@ export function ReviewSheet() { canHighlight: parsedDiff.kind === "files", }); + const handleSelectFile = useCallback( + (fileId: string | null) => { + commentSelection.clearSelection(); + if (fileId !== null && collapsedFileIds.includes(fileId)) { + toggleExpandedFile(fileId); + } + const navigation = + fileId === null + ? nativeReviewDiffViewRef.current?.scrollToTop(true) + : nativeReviewDiffViewRef.current?.scrollToFile(fileId, true); + void navigation?.catch((error: unknown) => { + console.error("[review] Failed to navigate to diff file", error); + }); + }, + [collapsedFileIds, commentSelection, toggleExpandedFile], + ); + const handleVisibleFileChange = useCallback( + (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { + const { fileId } = event.nativeEvent; + if (!fileId) { + return; + } + reviewFileNavigatorRef.current?.setVisibleFile(fileId); + }, + [], + ); + const renderInspector = useCallback( + () => ( + + ), + [handleSelectFile, headerHeight, nativeReviewDiffData.files, selectedSection?.id], + ); + const handleNativeToggleFile = useCallback( (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { const { fileId } = event.nativeEvent; @@ -203,6 +528,7 @@ export function ReviewSheet() { parsedDiff.kind === "files" || parsedDiff.kind === "raw" ? parsedDiff.notice : null; const hasCachedSelectedDiff = selectedSection?.diff != null; const hasAnyCachedDiff = reviewSections.some((section) => section.diff != null); + const sectionMenu = useMemo(() => buildReviewSectionMenu(reviewSections), [reviewSections]); const { showConnectionNotice, showSectionToolbar } = resolveReviewAvailability({ hasEnvironmentPresentation: environment.isReady, isEnvironmentConnected: isEnvironmentReady, @@ -235,6 +561,26 @@ export function ReviewSheet() { return <>{children}; }, [error, parsedDiffNotice]); + const renderHeaderTitle = useCallback( + () => ( + + ), + [ + headerDiffSummary.additions, + headerDiffSummary.deletions, + headerForeground, + headerMuted, + pendingReviewCommentCount, + selectedSection?.title, + ], + ); return ( <> @@ -246,122 +592,98 @@ export function ReviewSheet() { headerStyle: { backgroundColor: "transparent", }, - headerTitle: () => ( - - - Files Changed - - - {headerDiffSummary.additions && headerDiffSummary.deletions ? ( - <> - - {headerDiffSummary.additions} - - - {headerDiffSummary.deletions} - - {pendingReviewCommentCount > 0 ? ( - - {pendingReviewCommentCount} pending - - ) : null} - - ) : ( - - - {selectedSection?.title ?? "Review changes"} - - {pendingReviewCommentCount > 0 ? ( - - {pendingReviewCommentCount} pending - - ) : null} - - )} - - - ), + headerTitle: renderHeaderTitle, }} /> - {showSectionToolbar ? ( + {layout.usesSplitView || showSectionToolbar || panes.supportsAuxiliaryPane ? ( - - {reviewSections.map((section) => ( + {layout.usesSplitView ? ( + + ) : null} + {panes.supportsAuxiliaryPane ? ( + + ) : null} + {showSectionToolbar ? ( + + + { + if (sectionMenu.workingTree) { + selectSection(sectionMenu.workingTree.id); + } + }} + > + Working tree + + { + if (sectionMenu.branchChanges) { + selectSection(sectionMenu.branchChanges.id); + } + }} + > + Branch changes + + { + if (sectionMenu.latestTurn) { + selectSection(sectionMenu.latestTurn.id); + } + }} + > + Latest turn + + {sectionMenu.turns.length > 0 ? ( + + {sectionMenu.turns.map((section) => ( + selectSection(section.id)} + subtitle={section.subtitle ?? undefined} + > + {section.title} + + ))} + + ) : null} + selectSection(section.id)} - subtitle={section.subtitle ?? undefined} + icon="arrow.clockwise" + disabled={ + loadingGitDiffs || + (selectedSection?.kind === "turn" && loadingTurnIds[selectedSection.id] === true) + } + onPress={() => void refreshSelectedSection()} + subtitle="Reload current diff" > - {section.title} + Refresh - ))} - void refreshSelectedSection()} - subtitle="Reload current diff" - > - Refresh - - + + ) : null} ) : null} @@ -386,35 +708,43 @@ export function ReviewSheet() { className="flex-1" style={{ backgroundColor: nativeBridge.theme.background, - paddingTop: topContentInset + REVIEW_HEADER_SPACING, }} > - {listHeader} - - - + + + {listHeader} + + + + + ) : ( { + it("reuses the row model for equivalent empty comment arrays", () => { + const first = getCachedNativeReviewDiffData(buildInput([])); + const second = getCachedNativeReviewDiffData(buildInput([])); + + expect(second).toBe(first); + }); + + it("reuses equivalent comment contents and invalidates changed comments", () => { + const first = getCachedNativeReviewDiffData(buildInput([makeComment("First")])); + const equivalent = getCachedNativeReviewDiffData(buildInput([makeComment("First")])); + const changed = getCachedNativeReviewDiffData(buildInput([makeComment("Changed")])); + + expect(equivalent).toBe(first); + expect(changed).not.toBe(first); + }); +}); diff --git a/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts b/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts index f60fdfe70e0..a38e0019369 100644 --- a/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts +++ b/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts @@ -75,6 +75,33 @@ export interface BuildNativeReviewDiffDataInput { readonly comments?: ReadonlyArray; } +interface CachedNativeReviewDiffData { + readonly commentsKey: string; + readonly data: NativeReviewDiffData; +} + +const nativeReviewDiffDataCache = new WeakMap(); + +function buildReviewCommentsCacheKey(comments: ReadonlyArray): string { + if (comments.length === 0) { + return "none"; + } + + return comments + .map((comment) => + [ + comment.id, + comment.sectionId, + comment.filePath, + comment.startIndex, + comment.endIndex, + comment.rangeLabel, + comment.text, + ].join("\u001f"), + ) + .join("\u001e"); +} + export function createNativeReviewDiffTheme( scheme: TerminalAppearanceScheme, ): NativeReviewDiffTheme { @@ -430,3 +457,26 @@ export function buildNativeReviewDiffData( deletions: parsedDiff.deletions, }; } + +/** + * Reuses the expensive flattened native row model across React development + * render probes and unrelated draft updates. Only the latest comment version + * is retained for each parsed diff so editing a comment cannot grow the cache. + */ +export function getCachedNativeReviewDiffData( + input: BuildNativeReviewDiffDataInput, +): NativeReviewDiffData { + const comments = input.comments ?? []; + const commentsKey = buildReviewCommentsCacheKey(comments); + const cached = nativeReviewDiffDataCache.get(input.parsedDiff); + if (cached?.commentsKey === commentsKey) { + return cached.data; + } + + const data = buildNativeReviewDiffData({ + parsedDiff: input.parsedDiff, + comments, + }); + nativeReviewDiffDataCache.set(input.parsedDiff, { commentsKey, data }); + return data; +} diff --git a/apps/mobile/src/features/review/review-section-menu.test.ts b/apps/mobile/src/features/review/review-section-menu.test.ts new file mode 100644 index 00000000000..7eca4ce3b4d --- /dev/null +++ b/apps/mobile/src/features/review/review-section-menu.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vite-plus/test"; + +import type { ReviewSectionItem, ReviewSectionKind } from "./reviewModel"; +import { buildReviewSectionMenu } from "./review-section-menu"; + +function section(id: string, kind: ReviewSectionKind): ReviewSectionItem { + return { + id, + kind, + title: id, + subtitle: null, + diff: null, + isLoading: false, + }; +} + +describe("buildReviewSectionMenu", () => { + it("exposes git scopes and the latest turn at the top level", () => { + const turn28 = section("turn:28", "turn"); + const turn27 = section("turn:27", "turn"); + const workingTree = section("git:working-tree", "working-tree"); + const branchChanges = section("git:branch-range", "branch-range"); + + expect(buildReviewSectionMenu([turn28, turn27, workingTree, branchChanges])).toEqual({ + workingTree, + branchChanges, + latestTurn: turn28, + turns: [turn28, turn27], + }); + }); + + it("keeps unavailable scopes empty while data loads", () => { + expect(buildReviewSectionMenu([])).toEqual({ + workingTree: null, + branchChanges: null, + latestTurn: null, + turns: [], + }); + }); +}); diff --git a/apps/mobile/src/features/review/review-section-menu.ts b/apps/mobile/src/features/review/review-section-menu.ts new file mode 100644 index 00000000000..87d10266529 --- /dev/null +++ b/apps/mobile/src/features/review/review-section-menu.ts @@ -0,0 +1,21 @@ +import type { ReviewSectionItem } from "./reviewModel"; + +export interface ReviewSectionMenu { + readonly workingTree: ReviewSectionItem | null; + readonly branchChanges: ReviewSectionItem | null; + readonly latestTurn: ReviewSectionItem | null; + readonly turns: ReadonlyArray; +} + +export function buildReviewSectionMenu( + sections: ReadonlyArray, +): ReviewSectionMenu { + const turns = sections.filter((section) => section.kind === "turn"); + + return { + workingTree: sections.find((section) => section.kind === "working-tree") ?? null, + branchChanges: sections.find((section) => section.kind === "branch-range") ?? null, + latestTurn: turns[0] ?? null, + turns, + }; +} diff --git a/apps/mobile/src/features/review/reviewPaneSelection.test.ts b/apps/mobile/src/features/review/reviewPaneSelection.test.ts new file mode 100644 index 00000000000..5b1574d8f42 --- /dev/null +++ b/apps/mobile/src/features/review/reviewPaneSelection.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { resolveSelectedReviewFileId } from "./reviewPaneSelection"; + +describe("resolveSelectedReviewFileId", () => { + it("keeps a visible file selected within the active section", () => { + expect( + resolveSelectedReviewFileId({ + selection: { sectionId: "worktree", fileId: "second" }, + sectionId: "worktree", + availableFileIds: ["first", "second"], + }), + ).toBe("second"); + }); + + it("clears selection when the review section changes", () => { + expect( + resolveSelectedReviewFileId({ + selection: { sectionId: "turn-1", fileId: "first" }, + sectionId: "turn-2", + availableFileIds: ["first"], + }), + ).toBeNull(); + }); + + it("clears a file that no longer exists in the diff", () => { + expect( + resolveSelectedReviewFileId({ + selection: { sectionId: "worktree", fileId: "removed" }, + sectionId: "worktree", + availableFileIds: ["first", "second"], + }), + ).toBeNull(); + }); +}); diff --git a/apps/mobile/src/features/review/reviewPaneSelection.ts b/apps/mobile/src/features/review/reviewPaneSelection.ts new file mode 100644 index 00000000000..6f3b7e41c1f --- /dev/null +++ b/apps/mobile/src/features/review/reviewPaneSelection.ts @@ -0,0 +1,16 @@ +export interface ReviewPaneFileSelection { + readonly sectionId: string | null; + readonly fileId: string | null; +} + +export function resolveSelectedReviewFileId(input: { + readonly selection: ReviewPaneFileSelection; + readonly sectionId: string | null; + readonly availableFileIds: ReadonlyArray; +}): string | null { + if (input.selection.sectionId !== input.sectionId || input.selection.fileId === null) { + return null; + } + + return input.availableFileIds.includes(input.selection.fileId) ? input.selection.fileId : null; +} diff --git a/apps/mobile/src/features/review/useReviewDiffData.ts b/apps/mobile/src/features/review/useReviewDiffData.ts index ee04673dfc0..85aa6b032fa 100644 --- a/apps/mobile/src/features/review/useReviewDiffData.ts +++ b/apps/mobile/src/features/review/useReviewDiffData.ts @@ -1,11 +1,13 @@ import { useEffect, useMemo } from "react"; import { countReviewCommentContexts, parseReviewInlineComments } from "./reviewCommentSelection"; -import { buildNativeReviewDiffData } from "./nativeReviewDiffAdapter"; +import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter"; import { markReviewEvent, measureReviewWork } from "./reviewPerf"; import { getCachedReviewParsedDiff } from "./reviewState"; import type { ReviewParsedDiff, ReviewSectionItem } from "./reviewModel"; +const EMPTY_INLINE_REVIEW_COMMENTS = Object.freeze([]); + function isReviewDiffDebugLoggingEnabled(): boolean { return typeof __DEV__ !== "undefined" ? __DEV__ : false; } @@ -43,6 +45,7 @@ export function useReviewDiffData(input: { readonly draftMessage: string; }) { const { draftMessage, selectedSection, threadKey } = input; + const selectedSectionId = selectedSection?.id ?? null; const parsedDiff = useMemo( () => measureReviewWork("parse-diff", () => @@ -59,17 +62,16 @@ export function useReviewDiffData(input: { () => parseReviewInlineComments(draftMessage), [draftMessage], ); - const selectedSectionInlineComments = useMemo( - () => - selectedSection - ? inlineReviewComments.filter((comment) => comment.sectionId === selectedSection.id) - : [], - [inlineReviewComments, selectedSection], - ); + const selectedSectionInlineComments = useMemo(() => { + if (!selectedSectionId || inlineReviewComments.length === 0) { + return EMPTY_INLINE_REVIEW_COMMENTS; + } + return inlineReviewComments.filter((comment) => comment.sectionId === selectedSectionId); + }, [inlineReviewComments, selectedSectionId]); const nativeReviewDiffData = useMemo( () => measureReviewWork("build-native-diff-data", () => - buildNativeReviewDiffData({ + getCachedNativeReviewDiffData({ parsedDiff, comments: selectedSectionInlineComments, }), diff --git a/apps/mobile/src/features/review/useReviewDiffPrewarming.ts b/apps/mobile/src/features/review/useReviewDiffPrewarming.ts new file mode 100644 index 00000000000..80a2a64dbf6 --- /dev/null +++ b/apps/mobile/src/features/review/useReviewDiffPrewarming.ts @@ -0,0 +1,101 @@ +import { useEffect } from "react"; + +import { getCachedNativeReviewDiffData } from "./nativeReviewDiffAdapter"; +import type { ReviewSectionItem } from "./reviewModel"; +import { getCachedReviewParsedDiff } from "./reviewState"; + +interface IdleDeadlineLike { + readonly didTimeout: boolean; + timeRemaining(): number; +} + +type IdleCallback = (deadline: IdleDeadlineLike) => void; + +function scheduleIdle(callback: IdleCallback): number { + if (typeof globalThis.requestIdleCallback === "function") { + return globalThis.requestIdleCallback(callback, { timeout: 2_000 }); + } + + return setTimeout( + () => callback({ didTimeout: true, timeRemaining: () => 0 }), + 100, + ) as unknown as number; +} + +function cancelIdle(handle: number): void { + if (typeof globalThis.cancelIdleCallback === "function") { + globalThis.cancelIdleCallback(handle); + return; + } + clearTimeout(handle); +} + +export function prewarmReviewDiffSection(input: { + readonly threadKey: string; + readonly section: ReviewSectionItem; +}): void { + const { section, threadKey } = input; + if (section.diff === null) { + return; + } + + const parsedDiff = getCachedReviewParsedDiff({ + threadKey, + sectionId: section.id, + diff: section.diff, + }); + getCachedNativeReviewDiffData({ parsedDiff, comments: [] }); +} + +/** Warms one cached section per idle period, after navigation animations finish. */ +export function useReviewDiffPrewarming(input: { + readonly threadKey: string | null; + readonly sections: ReadonlyArray; + readonly selectedSectionId: string | null; +}): void { + const { sections, selectedSectionId, threadKey } = input; + + useEffect(() => { + if (!threadKey) { + return; + } + + const pendingSections = sections.filter( + (section) => section.id !== selectedSectionId && section.diff !== null, + ); + if (pendingSections.length === 0) { + return; + } + + let cancelled = false; + let idleHandle: number | null = null; + let nextSectionIndex = 0; + + const scheduleNext = () => { + idleHandle = scheduleIdle(() => { + if (cancelled) { + return; + } + + const section = pendingSections[nextSectionIndex]; + if (!section) { + return; + } + nextSectionIndex += 1; + prewarmReviewDiffSection({ threadKey, section }); + + if (nextSectionIndex < pendingSections.length) { + scheduleNext(); + } + }); + }; + + scheduleNext(); + return () => { + cancelled = true; + if (idleHandle !== null) { + cancelIdle(idleHandle); + } + }; + }, [sections, selectedSectionId, threadKey]); +} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 75991cae885..a44020ea8db 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -76,6 +76,7 @@ export interface ThreadComposerProps { readonly draftMessage: string; readonly draftAttachments: ReadonlyArray; readonly placeholder: string; + readonly contentMaxWidth?: number; readonly bottomInset?: number; readonly connectionState: RemoteClientConnectionState; readonly connectionError: string | null; @@ -629,7 +630,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer : "linear-gradient(to bottom, rgba(255,255,255,0) 0%, rgba(255,255,255,0.85) 40%, rgba(255,255,255,0.95) 100%)", }} > - + {composerTrigger && composerMenuItems.length > 0 ? ( @@ -398,39 +400,46 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread style={{ position: "absolute", bottom: 0, left: 0, right: 0 }} offset={{ closed: 0, opened: 0 }} > - - {props.activeWorkStartedAt ? ( - - ) : null} - - {props.activePendingApproval || props.activePendingUserInput ? ( - - {props.activePendingApproval ? ( - - ) : null} - {props.activePendingUserInput ? ( - - ) : null} - - ) : null} + + + {props.activeWorkStartedAt ? ( + + ) : null} + + {props.activePendingApproval || props.activePendingUserInput ? ( + + {props.activePendingApproval ? ( + + ) : null} + {props.activePendingUserInput ? ( + + ) : null} + + ) : null} + ; readonly contentTopInset?: number; readonly contentBottomInset?: number; + readonly contentMaxWidth?: number; readonly layoutVariant?: LayoutVariant; readonly skills?: ReadonlyArray; } @@ -1142,7 +1143,12 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { headers?: Record; } | null>(null); const horizontalPadding = props.layoutVariant === "split" ? 20 : 16; - const contentWidth = Math.max(0, viewportWidth - horizontalPadding * 2); + const contentHorizontalPadding = deriveCenteredContentHorizontalPadding({ + viewportWidth, + maxContentWidth: props.contentMaxWidth ?? null, + minimumPadding: horizontalPadding, + }); + const contentWidth = Math.max(0, viewportWidth - contentHorizontalPadding * 2); const userBubbleMaxWidth = contentWidth * 0.85; const reviewCommentBubbleWidth = Math.min(Math.max(280, contentWidth * 0.85), contentWidth); const insets = useSafeAreaInsets(); @@ -1496,7 +1502,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ListHeaderComponent={} contentContainerStyle={{ paddingTop: 12, - paddingHorizontal: horizontalPadding, + paddingHorizontal: contentHorizontalPadding, }} /> {props.feed.length === 0 ? ( diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index d5920a72411..478aae3d515 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -66,6 +66,10 @@ function compactMenuStatus(gitStatus: VcsStatusResult | null): string { } export function ThreadGitControls(props: { + readonly auxiliaryPaneControl?: { + readonly accessibilityLabel: string; + readonly onPress: () => void; + }; readonly currentBranch: string | null; readonly gitStatus: VcsStatusResult | null; readonly gitOperationLabel: string | null; @@ -73,6 +77,8 @@ export function ThreadGitControls(props: { readonly canOpenFiles: boolean; readonly projectScripts: ReadonlyArray; readonly terminalSessions: ReadonlyArray; + readonly onOpenFilesInspector?: () => void; + readonly onOpenGitInspector?: () => void; readonly onOpenTerminal: (terminalId?: string | null) => void; readonly onOpenNewTerminal: () => void; readonly onRunProjectScript: (script: ProjectScript) => Promise; @@ -183,6 +189,14 @@ export function ThreadGitControls(props: { return ( + {props.auxiliaryPaneControl ? ( + + ) : null} {props.projectScripts.length > 0 ? ( props.projectScripts.map((script) => ( @@ -259,19 +273,29 @@ export function ThreadGitControls(props: { router.push(buildThreadFilesNavigation({ environmentId, threadId }))} + onPress={() => { + if (props.onOpenFilesInspector) { + props.onOpenFilesInspector(); + return; + } + router.push(buildThreadFilesNavigation({ environmentId, threadId })); + }} subtitle="Browse this workspace" > Files + onPress={() => { + if (props.onOpenGitInspector) { + props.onOpenGitInspector(); + return; + } router.push({ pathname: "/threads/[environmentId]/[threadId]/git", params: { environmentId, threadId }, - }) - } + }); + }} subtitle="Commit, files, branches" > More diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 811b620a3f0..f8499122800 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -11,6 +11,7 @@ import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; import { buildThreadNavigationGroups } from "./thread-navigation-groups"; +import { SidebarHeaderActions } from "./sidebar-header-actions"; import { threadStatusTone } from "./threadPresentation"; export function ThreadNavigationSidebar(props: { @@ -32,7 +33,6 @@ export function ThreadNavigationSidebar(props: { const backgroundColor = useThemeColor("--color-drawer"); const borderColor = useThemeColor("--color-border"); const foregroundColor = useThemeColor("--color-foreground"); - const iconColor = useThemeColor("--color-icon-muted"); const mutedColor = useThemeColor("--color-foreground-muted"); const placeholderColor = useThemeColor("--color-placeholder"); const searchBackgroundColor = useThemeColor("--color-subtle-strong"); @@ -42,42 +42,25 @@ export function ThreadNavigationSidebar(props: { return ( Threads - [ - styles.headerButton, - { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, - ]} - > - - - [ - styles.headerButton, - { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, - ]} - > - - + @@ -96,90 +79,84 @@ export function ThreadNavigationSidebar(props: { /> - - {groups.length === 0 ? ( - - {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} - - ) : ( - groups.map((group) => ( - - - {group.title} - + + + {groups.length === 0 ? ( + + {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} + + ) : ( + groups.map((group) => ( + + + {group.title} + - {group.threads.length === 0 ? ( - No threads yet - ) : ( - group.threads.map((thread) => { - const threadKey = scopedThreadKey(thread.environmentId, thread.id); - const selected = threadKey === props.selectedThreadKey; + {group.threads.length === 0 ? ( + No threads yet + ) : ( + group.threads.map((thread) => { + const threadKey = scopedThreadKey(thread.environmentId, thread.id); + const selected = threadKey === props.selectedThreadKey; - return ( - props.onSelectThread(thread)} - style={({ pressed }) => [ - styles.threadRow, - { - backgroundColor: selected - ? selectedBackgroundColor - : pressed - ? pressedBackgroundColor - : "transparent", - }, - ]} - > - - - {thread.title} - - - {relativeTime(thread.updatedAt ?? thread.createdAt)} - - - - - ); - }) - )} - - )) - )} - + return ( + props.onSelectThread(thread)} + style={({ pressed }) => [ + styles.threadRow, + { + backgroundColor: selected + ? selectedBackgroundColor + : pressed + ? pressedBackgroundColor + : "transparent", + }, + ]} + > + + + {thread.title} + + + {relativeTime(thread.updatedAt ?? thread.createdAt)} + + + + + ); + }) + )} + + )) + )} + + ); } const styles = StyleSheet.create({ + container: { + flex: 1, + }, header: { - minHeight: 48, + height: 44, paddingLeft: 18, paddingRight: 8, flexDirection: "row", alignItems: "center", gap: 2, }, - headerButton: { - width: 44, - height: 44, - borderRadius: 12, - alignItems: "center", - justifyContent: "center", - }, searchField: { height: 36, marginTop: 6, @@ -199,6 +176,15 @@ const styles = StyleSheet.create({ fontFamily: "DMSans_400Regular", fontSize: 15, }, + threadList: { + flex: 1, + }, + threadListContent: { + gap: 18, + paddingHorizontal: 10, + paddingTop: 16, + paddingBottom: 16, + }, section: { gap: 4, }, diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index ba112c25d49..e4cb9ee343f 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,5 +1,6 @@ -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useEffect, useMemo, useState } from "react"; +import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; +import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; +import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import * as Option from "effect/Option"; import { EnvironmentId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; @@ -12,7 +13,11 @@ import { vcsEnvironment } from "../../state/vcs"; import { EmptyState } from "../../components/EmptyState"; import { LoadingScreen } from "../../components/LoadingScreen"; -import { buildThreadRoutePath, buildThreadTerminalNavigation } from "../../lib/routes"; +import { + buildThreadFilesNavigation, + buildThreadRoutePath, + buildThreadTerminalNavigation, +} from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { connectionTone } from "../connection/connectionTone"; @@ -38,6 +43,7 @@ import { import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen } from "./ThreadDetailScreen"; import { ThreadGitControls } from "./ThreadGitControls"; +import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; import { useAtomCommand } from "../../state/use-atom-command"; import { useSelectedThreadGitActions } from "../../state/use-selected-thread-git-actions"; @@ -47,7 +53,27 @@ import { useSelectedThreadWorktree } from "../../state/use-selected-thread-workt import { useThreadComposerState } from "../../state/use-thread-composer-state"; import { threadEnvironment } from "../../state/threads"; import { projectThreadContentPresentation } from "./threadContentPresentation"; -import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; +import { AdaptiveInspectorLayout } from "../layout/adaptive-inspector-layout"; +import { + useAdaptiveWorkspaceLayout, + useAdaptiveWorkspacePaneRole, +} from "../layout/AdaptiveWorkspaceLayout"; +import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; +import { ThreadFileNavigatorPane } from "../files/thread-file-navigator-pane"; +import { + ThreadInspectorContentStack, + type ThreadInspectorMode, +} from "./thread-inspector-content-stack"; + +interface ThreadInspectorSelection { + readonly routeThreadIdentity: string | null; + readonly mode: ThreadInspectorMode; +} + +function InspectorPaneRoleActivation() { + useAdaptiveWorkspacePaneRole("inspector"); + return null; +} function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -61,8 +87,56 @@ function OpeningThreadLoadingScreen() { return ; } -export function ThreadRouteScreen() { - const { layout } = useAdaptiveWorkspaceLayout(); +function ThreadHeaderTitle(props: { + readonly foregroundColor: string; + readonly secondaryForegroundColor: string; + readonly subtitle: string; + readonly title: string; +}) { + return ( + { + // TODO: trigger rename modal + }} + > + + {props.title} + + + {props.subtitle} + + + ); +} + +export function ThreadRouteScreen( + props: { + readonly onReturnToThread?: () => void; + readonly renderInspector?: () => ReactNode; + } = {}, +) { + const { fileInspector, layout, showAuxiliaryPane, toggleAuxiliaryPane } = + useAdaptiveWorkspaceLayout(); + const headerHeight = useHeaderHeight(); const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); @@ -85,6 +159,28 @@ export function ThreadRouteScreen() { const environmentIdRaw = firstRouteParam(params.environmentId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; const threadId = firstRouteParam(params.threadId); + const routeThreadIdentity = + environmentIdRaw !== null && threadId !== null ? `${environmentIdRaw}:${threadId}` : null; + const [inspectorSelection, setInspectorSelection] = useState( + () => (props.renderInspector ? { routeThreadIdentity, mode: "route" } : null), + ); + const inspectorMode = + inspectorSelection?.routeThreadIdentity === routeThreadIdentity + ? inspectorSelection.mode + : null; + + useFocusEffect( + useCallback(() => { + return () => { + if (props.renderInspector === undefined) { + // Inspectors are contextual to this chat destination. Clear the + // hidden chat copy after a native push so returning from Files, + // Review, or Terminal cannot reserve an empty trailing pane. + setInspectorSelection(null); + } + }; + }, [props.renderInspector]), + ); const routeEnvironmentRuntime = useRemoteEnvironmentRuntime(environmentId); const routeConnectionState = routeEnvironmentRuntime?.connectionState ?? (environmentId ? "available" : connectionState); @@ -106,6 +202,23 @@ export function ThreadRouteScreen() { const iconColor = String(useThemeColor("--color-icon")); const foregroundColor = String(useThemeColor("--color-foreground")); const secondaryFg = String(useThemeColor("--color-foreground-secondary")); + const headerSubtitle = [ + selectedThreadProject?.title ?? null, + selectedEnvironmentConnection?.environmentLabel ?? null, + ] + .filter(Boolean) + .join(" · "); + const renderHeaderTitle = useCallback( + () => ( + + ), + [foregroundColor, headerSubtitle, secondaryFg, selectedThread?.title], + ); /* ─── Git status for native header trigger ───────────────────────── */ const gitStatus = useEnvironmentQuery( @@ -158,6 +271,92 @@ export function ThreadRouteScreen() { } }, [layout.usesSplitView]); + const handleOpenGitInspector = useCallback(() => { + setInspectorSelection({ routeThreadIdentity, mode: "git" }); + showAuxiliaryPane("inspector"); + }, [routeThreadIdentity, showAuxiliaryPane]); + const handleOpenFilesInspector = useCallback(() => { + if (!fileInspector.supported || selectedThread === null || selectedThreadCwd === null) { + return; + } + setInspectorSelection({ + routeThreadIdentity, + mode: props.renderInspector === undefined ? "files" : "route", + }); + showAuxiliaryPane("inspector"); + }, [ + fileInspector.supported, + props.renderInspector, + routeThreadIdentity, + selectedThread, + selectedThreadCwd, + showAuxiliaryPane, + ]); + const inspectorToggleActionRef = useRef({ + inspectorMode, + openFilesInspector: handleOpenFilesInspector, + toggleAuxiliaryPane, + }); + inspectorToggleActionRef.current = { + inspectorMode, + openFilesInspector: handleOpenFilesInspector, + toggleAuxiliaryPane, + }; + const handleToggleInspector = useCallback(() => { + const action = inspectorToggleActionRef.current; + if (action.inspectorMode === null) { + action.openFilesInspector(); + return; + } + action.toggleAuxiliaryPane(); + }, []); + const handleSelectInspectorFile = useCallback( + (path: string) => { + if (selectedThread === null) { + return; + } + router.push(buildThreadFilesNavigation(selectedThread, path)); + }, + [router, selectedThread], + ); + const GitInspector = useCallback( + () => , + [headerHeight], + ); + const FilesInspector = useCallback( + () => + selectedThread !== null && selectedThreadCwd !== null ? ( + + ) : null, + [ + handleSelectInspectorFile, + headerHeight, + selectedThread, + selectedThreadCwd, + selectedThreadProject?.title, + ], + ); + const renderInspectorStack = useCallback( + () => + inspectorMode === null ? null : ( + + ), + [FilesInspector, GitInspector, inspectorMode, props.renderInspector], + ); + const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; + const handleOpenConnectionEditor = useCallback(() => { void router.push("/connections"); }, [router]); @@ -322,15 +521,9 @@ export function ThreadRouteScreen() { }); const serverConfig = routeEnvironmentRuntime?.serverConfig ?? null; - const headerSubtitle = [ - selectedThreadProject?.title ?? null, - selectedEnvironmentConnection?.environmentLabel ?? null, - ] - .filter(Boolean) - .join(" · "); - return ( <> + {activeInspectorRenderer ? : null} ( - { - // TODO: trigger rename modal - }} - > - - {selectedThread.title} - - - {headerSubtitle} - - - ), + headerTitle: renderHeaderTitle, }} /> + + {props.onReturnToThread ? ( + + ) : null} + + - - - - {layout.usesSplitView ? null : ( - setDrawerVisible(false)} - onSelectThread={(thread) => { - router.replace(buildThreadRoutePath(thread)); - }} - onStartNewTask={() => router.push("/new")} + + + - )} - + + {layout.usesSplitView ? null : ( + setDrawerVisible(false)} + onSelectThread={(thread) => { + router.replace(buildThreadRoutePath(thread)); + }} + onStartNewTask={() => router.push("/new")} + /> + )} + + ); } diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 0db7876a774..075f0d916dd 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -23,9 +23,16 @@ import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-wo import { vcsEnvironment } from "../../../state/vcs"; import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents"; -export function GitOverviewSheet() { +export function GitOverviewSheet( + props: { + readonly headerInset?: number; + readonly presentation?: "sheet" | "inspector"; + } = {}, +) { const router = useRouter(); const insets = useSafeAreaInsets(); + const presentation = props.presentation ?? "sheet"; + const isInspector = presentation === "inspector"; const { environmentId, threadId } = useLocalSearchParams<{ environmentId: EnvironmentId; threadId: ThreadId; @@ -120,10 +127,12 @@ export function GitOverviewSheet() { return; } - router.dismiss(); + if (!isInspector) { + router.dismiss(); + } await gitActions.onRunSelectedThreadGitAction(input); }, - [environmentId, gitActions, gitStatus.data, isDefaultRef, router, threadId], + [environmentId, gitActions, gitStatus.data, isDefaultRef, isInspector, router, threadId], ); const onPressMenuItem = useCallback( @@ -152,10 +161,24 @@ export function GitOverviewSheet() { ); return ( - - + + - + - Branch + {isInspector ? "Repository" : "Branch"} + + + {currentBranchLabel} - {currentBranchLabel} {statusSummary(gitStatus.data)} @@ -187,12 +212,18 @@ export function GitOverviewSheet() { style={{ flex: 1 }} contentInset={{ bottom: Math.max(insets.bottom, 18) + 18 }} contentContainerStyle={{ - paddingHorizontal: 20, + paddingHorizontal: isInspector ? 12 : 20, paddingTop: 8, gap: 14, }} > - + {sheetMenuItems.map(({ item, disabledReason }, index) => ( {index > 0 ? ( diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx new file mode 100644 index 00000000000..ba9d715bfc8 --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx @@ -0,0 +1,21 @@ +import { View } from "react-native"; + +import { T3HeaderButton } from "../../native/T3HeaderButton.ios"; +import type { SidebarHeaderActionsProps } from "./sidebar-header-actions"; + +export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { + return ( + + + + + ); +} diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.tsx new file mode 100644 index 00000000000..292866c2025 --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-header-actions.tsx @@ -0,0 +1,65 @@ +import { SymbolView } from "expo-symbols"; +import { Pressable, StyleSheet, View } from "react-native"; + +import { useThemeColor } from "../../lib/useThemeColor"; + +export interface SidebarHeaderActionsProps { + readonly onOpenSettings: () => void; + readonly onStartNewTask: () => void; +} + +function FallbackHeaderButton(props: { + readonly accessibilityLabel: string; + readonly icon: "gearshape" | "square.and.pencil"; + readonly onPress: () => void; +}) { + const iconColor = useThemeColor("--color-icon-muted"); + const pressedBackgroundColor = useThemeColor("--color-subtle"); + + return ( + [ + styles.button, + { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + ]} + > + + + ); +} + +export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { + return ( + + + + + ); +} + +const styles = StyleSheet.create({ + actions: { + flexDirection: "row", + alignItems: "center", + gap: 2, + }, + button: { + width: 44, + height: 44, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + }, +}); diff --git a/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx b/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx new file mode 100644 index 00000000000..2c8ec73342d --- /dev/null +++ b/apps/mobile/src/features/threads/thread-inspector-content-stack.tsx @@ -0,0 +1,101 @@ +import { useEffect, useState, type ComponentType, type ReactNode } from "react"; +import { View } from "react-native"; + +export type ThreadInspectorMode = "route" | "git" | "files"; + +const INSPECTOR_PREWARM_DELAY_MS = 350; + +function InspectorContentPane(props: { + readonly children: ReactNode; + readonly mounted: boolean; + readonly visible: boolean; +}) { + if (!props.mounted) { + return null; + } + + return ( + + {props.children} + + ); +} + +export function ThreadInspectorContentStack(props: { + readonly Files: ComponentType; + readonly Git: ComponentType; + readonly mode: ThreadInspectorMode; + readonly Route?: ComponentType; +}) { + const [mountedModes, setMountedModes] = useState>( + () => new Set([props.mode]), + ); + + useEffect(() => { + setMountedModes((current) => { + if (current.has(props.mode)) { + return current; + } + return new Set([...current, props.mode]); + }); + + if (props.mode === "route") { + return; + } + + // The file tree is expensive to detach because UIKit rebuilds its focus + // graph. Keep both chat inspectors alive after the opening animation so a + // later Files/Git switch only changes visibility. + const alternateMode = props.mode === "files" ? "git" : "files"; + const timeout = setTimeout(() => { + setMountedModes((current) => { + if (current.has(alternateMode)) { + return current; + } + return new Set([...current, alternateMode]); + }); + }, INSPECTOR_PREWARM_DELAY_MS); + + return () => clearTimeout(timeout); + }, [props.mode]); + + const Files = props.Files; + const Git = props.Git; + const Route = props.Route; + + return ( + + + + + + + + {Route ? ( + + + + ) : null} + + ); +} diff --git a/apps/mobile/src/lib/adaptive-navigation.test.ts b/apps/mobile/src/lib/adaptive-navigation.test.ts new file mode 100644 index 00000000000..7f8ce80a1ae --- /dev/null +++ b/apps/mobile/src/lib/adaptive-navigation.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + isBaseThreadRoute, + resolveFileSelectionNavigationAction, + resolveThreadSelectionNavigationAction, +} from "./adaptive-navigation"; + +describe("isBaseThreadRoute", () => { + it("recognizes only the thread detail route", () => { + expect(isBaseThreadRoute("/threads/environment/thread")).toBe(true); + expect(isBaseThreadRoute("/threads/environment/thread/")).toBe(true); + expect(isBaseThreadRoute("/threads/environment/thread/files")).toBe(false); + expect(isBaseThreadRoute("/threads/environment/thread/review")).toBe(false); + }); +}); + +describe("resolveThreadSelectionNavigationAction", () => { + it("updates params when a persistent sidebar selects a peer thread", () => { + expect( + resolveThreadSelectionNavigationAction({ + usesSplitView: true, + pathname: "/threads/environment/thread", + }), + ).toBe("set-params"); + }); + + it("replaces nested thread content when a persistent sidebar selects a peer", () => { + expect( + resolveThreadSelectionNavigationAction({ + usesSplitView: true, + pathname: "/threads/environment/thread/files/path", + }), + ).toBe("replace"); + }); + + it("pushes compact list selections onto the native stack", () => { + expect( + resolveThreadSelectionNavigationAction({ + usesSplitView: false, + pathname: "/threads/environment/thread", + }), + ).toBe("push"); + }); +}); + +describe("resolveFileSelectionNavigationAction", () => { + it("replaces the wide file browser with the selected preview", () => { + expect(resolveFileSelectionNavigationAction({ hasPersistentFileInspector: true })).toBe( + "replace", + ); + }); + + it("pushes a preview above the compact file browser", () => { + expect(resolveFileSelectionNavigationAction({ hasPersistentFileInspector: false })).toBe( + "push", + ); + }); +}); diff --git a/apps/mobile/src/lib/adaptive-navigation.ts b/apps/mobile/src/lib/adaptive-navigation.ts new file mode 100644 index 00000000000..0d0fa59f1fb --- /dev/null +++ b/apps/mobile/src/lib/adaptive-navigation.ts @@ -0,0 +1,33 @@ +export type AdaptiveNavigationAction = "push" | "replace" | "set-params"; + +const BASE_THREAD_ROUTE_PATTERN = /^\/threads\/[^/]+\/[^/]+\/?$/; + +export function isBaseThreadRoute(pathname: string): boolean { + return BASE_THREAD_ROUTE_PATTERN.test(pathname); +} + +/** + * A persistent sidebar selects a peer destination in place. A compact list + * drills into a new destination so the native back stack remains available. + */ +export function resolveThreadSelectionNavigationAction(input: { + readonly usesSplitView: boolean; + readonly pathname: string; +}): AdaptiveNavigationAction { + if (!input.usesSplitView) { + return "push"; + } + + return isBaseThreadRoute(input.pathname) ? "set-params" : "replace"; +} + +/** + * On regular-width layouts, the file browser and preview occupy one workspace + * destination. Replacing the browser route keeps a single back step to chat. + * Compact layouts retain the browser as the previous stack screen. + */ +export function resolveFileSelectionNavigationAction(input: { + readonly hasPersistentFileInspector: boolean; +}): AdaptiveNavigationAction { + return input.hasPersistentFileInspector ? "replace" : "push"; +} diff --git a/apps/mobile/src/lib/layout.test.ts b/apps/mobile/src/lib/layout.test.ts index e9646722e56..58fd95c3e89 100644 --- a/apps/mobile/src/lib/layout.test.ts +++ b/apps/mobile/src/lib/layout.test.ts @@ -1,6 +1,46 @@ import { describe, expect, it } from "vite-plus/test"; -import { deriveLayout, SPLIT_LAYOUT_MIN_HEIGHT, SPLIT_LAYOUT_MIN_WIDTH } from "./layout"; +import { + deriveCenteredContentHorizontalPadding, + deriveFileInspectorPaneLayout, + deriveLayout, + deriveStableFormSheetDetent, + deriveWorkspacePaneLayout, + SPLIT_LAYOUT_MIN_HEIGHT, + SPLIT_LAYOUT_MIN_WIDTH, +} from "./layout"; + +describe("deriveCenteredContentHorizontalPadding", () => { + it("keeps the minimum padding while the viewport fits the reading width", () => { + expect( + deriveCenteredContentHorizontalPadding({ + viewportWidth: 744, + maxContentWidth: 960, + minimumPadding: 20, + }), + ).toBe(20); + }); + + it("centers only the content inside a wider full-width scroll host", () => { + expect( + deriveCenteredContentHorizontalPadding({ + viewportWidth: 1_032, + maxContentWidth: 960, + minimumPadding: 20, + }), + ).toBe(56); + }); + + it("supports unconstrained compact content", () => { + expect( + deriveCenteredContentHorizontalPadding({ + viewportWidth: 430, + maxContentWidth: null, + minimumPadding: 16, + }), + ).toBe(16); + }); +}); describe("deriveLayout", () => { it.each([ @@ -49,3 +89,188 @@ describe("deriveLayout", () => { expect(deriveLayout({ width: 1_600, height: 1_000 }).listPaneWidth).toBe(380); }); }); + +describe("deriveWorkspacePaneLayout", () => { + it("keeps the auxiliary pane out of a standard iPad detail column", () => { + const layout = deriveLayout({ width: 1_194, height: 834 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_194, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + }), + ).toEqual({ + primarySidebarVisible: true, + primarySidebarSuppressedByAuxiliary: false, + contentPaneWidth: 814, + supportsAuxiliaryPane: false, + auxiliaryPaneVisible: false, + auxiliaryPaneWidth: null, + }); + }); + + it("offers an auxiliary pane when maximizing a standard iPad landscape window", () => { + const layout = deriveLayout({ width: 1_194, height: 834 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_194, + primarySidebarPreferredVisible: false, + auxiliaryPanePreferredVisible: true, + }), + ).toEqual({ + primarySidebarVisible: false, + primarySidebarSuppressedByAuxiliary: false, + contentPaneWidth: 1_194, + supportsAuxiliaryPane: true, + auxiliaryPaneVisible: true, + auxiliaryPaneWidth: 320, + }); + }); + + it("prioritizes a trailing file inspector over the thread sidebar at medium widths", () => { + const layout = deriveLayout({ width: 1_024, height: 1_366 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_024, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + auxiliaryPaneRole: "inspector", + }), + ).toEqual({ + primarySidebarVisible: false, + primarySidebarSuppressedByAuxiliary: true, + contentPaneWidth: 1_024, + supportsAuxiliaryPane: true, + auxiliaryPaneVisible: true, + auxiliaryPaneWidth: 287, + }); + }); + + it("keeps threads, content, and the file inspector visible in a large landscape window", () => { + const layout = deriveLayout({ width: 1_366, height: 1_024 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_366, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + auxiliaryPaneRole: "inspector", + }), + ).toEqual({ + primarySidebarVisible: true, + primarySidebarSuppressedByAuxiliary: false, + contentPaneWidth: 986, + supportsAuxiliaryPane: true, + auxiliaryPaneVisible: true, + auxiliaryPaneWidth: 320, + }); + }); + + it("restores the thread sidebar when the file inspector is hidden", () => { + const layout = deriveLayout({ width: 1_024, height: 1_366 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_024, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: false, + auxiliaryPaneRole: "inspector", + }), + ).toMatchObject({ + primarySidebarVisible: true, + primarySidebarSuppressedByAuxiliary: false, + auxiliaryPaneVisible: false, + }); + }); + + it("keeps file navigation on the native stack below the inspector breakpoint", () => { + const layout = deriveLayout({ width: 744, height: 1_133 }); + + expect(deriveFileInspectorPaneLayout({ layout, viewportWidth: 744 })).toEqual({ + supported: false, + width: null, + }); + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 744, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + auxiliaryPaneRole: "inspector", + }), + ).toMatchObject({ + primarySidebarVisible: true, + supportsAuxiliaryPane: false, + auxiliaryPaneVisible: false, + }); + }); + + it("supports three visible columns in a sufficiently large window", () => { + const layout = deriveLayout({ width: 1_366, height: 1_024 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_366, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + }), + ).toMatchObject({ + primarySidebarVisible: true, + contentPaneWidth: 986, + supportsAuxiliaryPane: true, + auxiliaryPaneVisible: true, + auxiliaryPaneWidth: 276, + }); + }); + + it("respects a hidden auxiliary-pane preference", () => { + const layout = deriveLayout({ width: 1_366, height: 1_024 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_366, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: false, + }).auxiliaryPaneVisible, + ).toBe(false); + }); + + it("never exposes workspace panes in compact layouts", () => { + const layout = deriveLayout({ width: 430, height: 932 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 430, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + }), + ).toMatchObject({ + primarySidebarVisible: false, + supportsAuxiliaryPane: false, + auxiliaryPaneVisible: false, + auxiliaryPaneWidth: null, + }); + }); +}); + +describe("deriveStableFormSheetDetent", () => { + it.each([ + { height: 1_194, expected: 0.62 }, + { height: 834, expected: 0.863 }, + { height: 600, expected: 0.893 }, + { height: 0, expected: 0.92 }, + ])("derives a stable sheet detent for height $height", ({ height, expected }) => { + expect(deriveStableFormSheetDetent(height)).toBe(expected); + }); +}); diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index 35b9f571c14..dab90a79ac5 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -15,6 +15,18 @@ export const SPLIT_LAYOUT_MIN_HEIGHT = 600; const SPLIT_SIDEBAR_MIN_WIDTH = 280; const SPLIT_SIDEBAR_MAX_WIDTH = 380; +export const AUXILIARY_PANE_MIN_CONTENT_WIDTH = 960; +export const CHAT_CONTENT_MAX_WIDTH = 960; + +const AUXILIARY_PANE_MIN_WIDTH = 260; +const AUXILIARY_PANE_MAX_WIDTH = 320; +const FILE_INSPECTOR_MIN_VIEWPORT_WIDTH = 820; +const FILE_INSPECTOR_MIN_MAIN_WIDTH = 560; +const STABLE_FORM_SHEET_MAX_HEIGHT = 720; +const STABLE_FORM_SHEET_VERTICAL_MARGIN = 64; +const STABLE_FORM_SHEET_MIN_DETENT = 0.62; +const STABLE_FORM_SHEET_MAX_DETENT = 0.92; + export type LayoutVariant = "compact" | "split"; export interface Layout { @@ -24,6 +36,22 @@ export interface Layout { readonly shellPadding: number; } +export interface WorkspacePaneLayout { + readonly primarySidebarVisible: boolean; + readonly primarySidebarSuppressedByAuxiliary: boolean; + readonly contentPaneWidth: number; + readonly supportsAuxiliaryPane: boolean; + readonly auxiliaryPaneVisible: boolean; + readonly auxiliaryPaneWidth: number | null; +} + +export interface FileInspectorPaneLayout { + readonly supported: boolean; + readonly width: number | null; +} + +export type WorkspaceAuxiliaryPaneRole = "supplementary" | "inspector"; + export function deriveLayout(input: { readonly width: number; readonly height: number }): Layout { const { width, height } = input; const wideEnoughForSplit = width >= SPLIT_LAYOUT_MIN_WIDTH && height >= SPLIT_LAYOUT_MIN_HEIGHT; @@ -48,3 +76,119 @@ export function deriveLayout(input: { readonly width: number; readonly height: n shellPadding: 0, }; } + +export function deriveWorkspacePaneLayout(input: { + readonly layout: Layout; + readonly viewportWidth: number; + readonly primarySidebarPreferredVisible: boolean; + readonly auxiliaryPanePreferredVisible: boolean; + readonly auxiliaryPaneRole?: WorkspaceAuxiliaryPaneRole; +}): WorkspacePaneLayout { + const viewportWidth = Math.max(0, input.viewportWidth); + const auxiliaryPaneRole = input.auxiliaryPaneRole ?? "supplementary"; + const preferredPrimarySidebarVisible = + input.layout.usesSplitView && input.primarySidebarPreferredVisible; + const preferredPrimarySidebarWidth = preferredPrimarySidebarVisible + ? (input.layout.listPaneWidth ?? 0) + : 0; + + if (auxiliaryPaneRole === "inspector") { + const fileInspector = deriveFileInspectorPaneLayout({ + layout: input.layout, + viewportWidth, + }); + const auxiliaryPaneVisible = fileInspector.supported && input.auxiliaryPanePreferredVisible; + const primarySidebarSuppressedByAuxiliary = + auxiliaryPaneVisible && + fileInspector.width !== null && + input.layout.listPaneWidth !== null && + viewportWidth - input.layout.listPaneWidth - fileInspector.width < + FILE_INSPECTOR_MIN_MAIN_WIDTH; + const primarySidebarVisible = + preferredPrimarySidebarVisible && !primarySidebarSuppressedByAuxiliary; + const primarySidebarWidth = primarySidebarVisible ? (input.layout.listPaneWidth ?? 0) : 0; + + return { + primarySidebarVisible, + primarySidebarSuppressedByAuxiliary, + contentPaneWidth: Math.max(0, viewportWidth - primarySidebarWidth), + supportsAuxiliaryPane: fileInspector.supported, + auxiliaryPaneVisible, + auxiliaryPaneWidth: fileInspector.width, + }; + } + + const contentPaneWidth = Math.max(0, viewportWidth - preferredPrimarySidebarWidth); + const supportsAuxiliaryPane = + input.layout.usesSplitView && contentPaneWidth >= AUXILIARY_PANE_MIN_CONTENT_WIDTH; + const auxiliaryPaneVisible = supportsAuxiliaryPane && input.auxiliaryPanePreferredVisible; + + return { + primarySidebarVisible: preferredPrimarySidebarVisible, + primarySidebarSuppressedByAuxiliary: false, + contentPaneWidth, + supportsAuxiliaryPane, + auxiliaryPaneVisible, + auxiliaryPaneWidth: supportsAuxiliaryPane + ? clamp( + Math.round(contentPaneWidth * 0.28), + AUXILIARY_PANE_MIN_WIDTH, + AUXILIARY_PANE_MAX_WIDTH, + ) + : null, + }; +} + +export function deriveFileInspectorPaneLayout(input: { + readonly layout: Layout; + readonly viewportWidth: number; +}): FileInspectorPaneLayout { + const viewportWidth = Math.max(0, input.viewportWidth); + const supported = + input.layout.usesSplitView && viewportWidth >= FILE_INSPECTOR_MIN_VIEWPORT_WIDTH; + + return { + supported, + width: supported + ? clamp(Math.round(viewportWidth * 0.28), AUXILIARY_PANE_MIN_WIDTH, AUXILIARY_PANE_MAX_WIDTH) + : null, + }; +} + +export function deriveCenteredContentHorizontalPadding(input: { + readonly viewportWidth: number; + readonly maxContentWidth: number | null; + readonly minimumPadding: number; +}): number { + const viewportWidth = Number.isFinite(input.viewportWidth) ? Math.max(0, input.viewportWidth) : 0; + const minimumPadding = Number.isFinite(input.minimumPadding) + ? Math.max(0, input.minimumPadding) + : 0; + + if ( + input.maxContentWidth === null || + !Number.isFinite(input.maxContentWidth) || + input.maxContentWidth <= 0 + ) { + return minimumPadding; + } + + return minimumPadding + Math.max(0, (viewportWidth - input.maxContentWidth) / 2); +} + +export function deriveStableFormSheetDetent(containerHeight: number): number { + if (!Number.isFinite(containerHeight) || containerHeight <= 0) { + return STABLE_FORM_SHEET_MAX_DETENT; + } + + const targetHeight = Math.min( + STABLE_FORM_SHEET_MAX_HEIGHT, + Math.max(0, containerHeight - STABLE_FORM_SHEET_VERTICAL_MARGIN), + ); + const detent = clamp( + targetHeight / containerHeight, + STABLE_FORM_SHEET_MIN_DETENT, + STABLE_FORM_SHEET_MAX_DETENT, + ); + return Math.round(detent * 1_000) / 1_000; +} diff --git a/apps/mobile/src/native/T3HeaderButton.ios.tsx b/apps/mobile/src/native/T3HeaderButton.ios.tsx new file mode 100644 index 00000000000..74908abd16c --- /dev/null +++ b/apps/mobile/src/native/T3HeaderButton.ios.tsx @@ -0,0 +1,26 @@ +import { requireNativeView } from "expo"; +import type { NativeSyntheticEvent, StyleProp, ViewProps, ViewStyle } from "react-native"; + +interface NativeHeaderButtonProps extends ViewProps { + readonly label: string; + readonly systemImage: "gearshape" | "square.and.pencil"; + readonly onTriggered: (event: NativeSyntheticEvent>) => void; +} + +const NativeHeaderButton = requireNativeView("T3NativeControls"); + +export function T3HeaderButton(props: { + readonly accessibilityLabel: string; + readonly icon: NativeHeaderButtonProps["systemImage"]; + readonly onPress: () => void; + readonly style?: StyleProp; +}) { + return ( + + ); +} From a896942bc7177337a28b53e3bf25cc6ce9ca2b98 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 23 Jun 2026 22:47:55 -0700 Subject: [PATCH 03/71] Improve mobile thread navigation and diff selection - Keep sidebar threads swipeable with archive/delete actions - Hide archived threads from navigation groups - Stabilize review diff visibility and optimistic file selection Co-authored-by: codex --- .../t3-review-diff/ios/T3ReviewDiffView.swift | 21 +- .../diffs/nativeReviewDiffSurface.test.ts | 17 ++ .../features/diffs/nativeReviewDiffSurface.ts | 7 +- .../src/features/files/FileTreeBrowser.tsx | 18 ++ apps/mobile/src/features/home/HomeScreen.tsx | 223 +++++++----------- .../features/home/thread-swipe-actions.tsx | 105 ++++++++- .../src/features/review/ReviewSheet.tsx | 12 +- .../src/features/threads/ThreadFeed.tsx | 6 +- .../threads/ThreadNavigationSidebar.tsx | 95 ++++++-- .../threads/thread-navigation-groups.test.ts | 17 ++ .../threads/thread-navigation-groups.ts | 3 +- 11 files changed, 336 insertions(+), 188 deletions(-) diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index 07e57b9db3c..9a7a5004daf 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -550,6 +550,8 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { } self.contentResetKey = contentResetKey + tokensDecodeGeneration += 1 + contentView.tokensByRowId = [:] hasAppliedInitialRowIndex = false lastVisibleFileId = nil pendingScrollFileId = nil @@ -684,8 +686,23 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { // Keep the explicit destination selected while UIKit animates through the files // between the old and new offsets. Emitting every intermediate header forces a // React render per crossing and makes the navigator visibly flash. - guard !isProgrammaticScrollActive, - let fileId = contentView.visibleFileId(atVerticalOffset: scrollView.contentOffset.y), + guard !isProgrammaticScrollActive else { + return + } + + // The top of the combined diff is the explicit "All files" destination. + // Treat it as a first-class selection instead of immediately resolving the + // first file header and undoing the navigator's optimistic selection. + if scrollView.contentOffset.y <= 0.5 { + guard lastVisibleFileId != nil else { + return + } + lastVisibleFileId = nil + onVisibleFileChange(["fileId": NSNull()]) + return + } + + guard let fileId = contentView.visibleFileId(atVerticalOffset: scrollView.contentOffset.y), fileId != lastVisibleFileId else { return } diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts index 1ea15344c34..0409655583b 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.test.ts @@ -81,3 +81,20 @@ describe("resolveNativeReviewDiffView", () => { expect(consoleError).toHaveBeenCalledTimes(1); }); }); + +describe("isPendingNativeViewRegistration", () => { + it("recognizes registration races for the installed native view name", async () => { + const { isPendingNativeViewRegistration } = await import("./nativeReviewDiffSurface"); + + expect( + isPendingNativeViewRegistration( + new Error("Unable to find the 'T3ReviewDiffSurface' view for this native tag"), + ), + ).toBe(true); + expect( + isPendingNativeViewRegistration( + new Error("Unable to find the 'T3ReviewDiffView' view for this native tag"), + ), + ).toBe(false); + }); +}); diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index 87fd51565d4..a4896dc4011 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -124,7 +124,7 @@ export interface NativeReviewDiffViewProps extends ViewProps { readonly nativeViewRef?: Ref; readonly onDebug?: (event: NativeSyntheticEvent>) => void; readonly onVisibleFileChange?: ( - event: NativeSyntheticEvent<{ readonly fileId?: string }>, + event: NativeSyntheticEvent<{ readonly fileId?: string | null }>, ) => void; readonly onToggleFile?: (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => void; readonly onToggleViewedFile?: (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => void; @@ -166,9 +166,10 @@ let nativeReviewDiffViewResolutionFailed = false; type NativeReviewDiffPayloadMethod = "setRowsJson" | "setTokensJson" | "setTokensPatchJson"; -function isPendingNativeViewRegistration(error: unknown): boolean { +export function isPendingNativeViewRegistration(error: unknown): boolean { return ( - error instanceof Error && error.message.includes("Unable to find the 'T3ReviewDiffView' view") + error instanceof Error && + error.message.includes(`Unable to find the '${NATIVE_REVIEW_DIFF_MODULE_NAME}' view`) ); } diff --git a/apps/mobile/src/features/files/FileTreeBrowser.tsx b/apps/mobile/src/features/files/FileTreeBrowser.tsx index 0f89092ddda..77a477c5649 100644 --- a/apps/mobile/src/features/files/FileTreeBrowser.tsx +++ b/apps/mobile/src/features/files/FileTreeBrowser.tsx @@ -18,6 +18,7 @@ import { const fileTreeCache = new WeakMap, ReadonlyArray>(); const FILE_TREE_INITIAL_RENDER_COUNT = 20; const FILE_TREE_RENDER_BATCH_SIZE = 12; +const OPTIMISTIC_SELECTION_TIMEOUT_MS = 1_000; function cachedFileTree(entries: ReadonlyArray): ReadonlyArray { const cached = fileTreeCache.get(entries); @@ -120,6 +121,7 @@ export function FileTreeBrowser(props: { const iconColor = String(useThemeColor("--color-icon-muted")); const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props; const controlledSelectedPathRef = useRef(controlledSelectedPath); + const pendingSelectionTimeoutRef = useRef | null>(null); controlledSelectedPathRef.current = controlledSelectedPath; const selectedPath = @@ -164,6 +166,15 @@ export function FileTreeBrowser(props: { }); }, [controlledSelectedPath]); + useEffect( + () => () => { + if (pendingSelectionTimeoutRef.current !== null) { + clearTimeout(pendingSelectionTimeoutRef.current); + } + }, + [], + ); + const toggleDirectory = useCallback((path: string) => { setExpandedPaths((current) => { const next = new Set(current); @@ -177,10 +188,17 @@ export function FileTreeBrowser(props: { }, []); const handleSelectFile = useCallback( (path: string) => { + if (pendingSelectionTimeoutRef.current !== null) { + clearTimeout(pendingSelectionTimeoutRef.current); + } setPendingSelection({ path, selectedPathAtPress: controlledSelectedPathRef.current, }); + pendingSelectionTimeoutRef.current = setTimeout(() => { + pendingSelectionTimeoutRef.current = null; + setPendingSelection((current) => (current?.path === path ? null : current)); + }, OPTIMISTIC_SELECTION_TIMEOUT_MS); onSelectFile(path); }, [onSelectFile], diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 7ee5660edf1..ed10660b8f0 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -7,13 +7,10 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import * as Haptics from "expo-haptics"; import { SymbolView } from "expo-symbols"; import { useCallback, useMemo, useRef, useState } from "react"; import { ActivityIndicator, Pressable, ScrollView, useWindowDimensions, View } from "react-native"; -import ReanimatedSwipeable, { - type SwipeableMethods, -} from "react-native-gesture-handler/ReanimatedSwipeable"; +import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import Animated, { Easing, LinearTransition, @@ -32,11 +29,7 @@ import type { SavedRemoteConnection } from "../../lib/connection"; import { relativeTime } from "../../lib/time"; import { threadStatusTone } from "../threads/threadPresentation"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; -import { - THREAD_SWIPE_ACTIONS_WIDTH, - THREAD_SWIPE_SPRING, - ThreadSwipeActions, -} from "./thread-swipe-actions"; +import { ThreadSwipeable } from "./thread-swipe-actions"; /* ─── Types ──────────────────────────────────────────────────────────── */ @@ -219,13 +212,10 @@ function ThreadRow(props: { readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly isLast: boolean; }) { - const swipeableRef = useRef(null); - const fullSwipeArmedRef = useRef(false); const { width: windowWidth } = useWindowDimensions(); const separatorColor = useThemeColor("--color-separator"); const iconSubtleColor = useThemeColor("--color-icon-subtle"); const cardColor = useThemeColor("--color-card"); - const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, (windowWidth - 32) * 0.58); const { bg, fg } = statusColors(props.thread); const tone = threadStatusTone(props.thread); const timestamp = relativeTime( @@ -235,150 +225,107 @@ function ThreadRow(props: { const subtitleParts = [props.environmentLabel, branch].filter((part): part is string => Boolean(part), ); - const handleFullSwipeArmedChange = useCallback((armed: boolean) => { - if (armed && !fullSwipeArmedRef.current && process.env.EXPO_OS === "ios") { - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - } - fullSwipeArmedRef.current = armed; - }, []); return ( - { - fullSwipeArmedRef.current = false; - if (swipeableRef.current) { - props.onSwipeableClose(swipeableRef.current); - } - }} - onSwipeableOpenStartDrag={() => { - if (swipeableRef.current) { - props.onSwipeableWillOpen(swipeableRef.current); - } - }} - onSwipeableWillOpen={() => { - const methods = swipeableRef.current; - if (!methods) { - return; - } - - props.onSwipeableWillOpen(methods); - if (fullSwipeArmedRef.current) { - fullSwipeArmedRef.current = false; - methods.close(); - props.onDelete(); - } + ( - - )} - rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} + threadTitle={props.thread.title} > - { - swipeableRef.current?.close(); - props.onPress(); - }} - style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} - > - ( + { + close(); + props.onPress(); }} + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} > - - - - - - - {props.thread.title} - - - - - {tone.label} - - - - {timestamp} - - + + - {subtitleParts.length > 0 ? ( - - + + - {subtitleParts.join(" · ")} + {props.thread.title} + + + + {tone.label} + + + + {timestamp} + + - ) : null} + + {subtitleParts.length > 0 ? ( + + + + {subtitleParts.join(" · ")} + + + ) : null} + - - - + + )} + ); } diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index dd0e2901bba..e8b88885bbe 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -1,8 +1,11 @@ import { SymbolView } from "expo-symbols"; -import type { ComponentProps } from "react"; -import type { ColorValue } from "react-native"; +import * as Haptics from "expo-haptics"; +import { useCallback, useRef, type ComponentProps, type ReactNode } from "react"; +import type { ColorValue, StyleProp, ViewStyle } from "react-native"; import { Pressable, View } from "react-native"; -import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; +import ReanimatedSwipeable, { + type SwipeableMethods, +} from "react-native-gesture-handler/ReanimatedSwipeable"; import Animated, { Extrapolation, interpolate, @@ -26,6 +29,95 @@ export const THREAD_SWIPE_SPRING = { stiffness: 330, }; +interface ThreadSwipePrimaryAction { + readonly accessibilityLabel: string; + readonly icon: ComponentProps["name"]; + readonly label: string; + readonly onPress: () => void; +} + +export function ThreadSwipeable(props: { + readonly backgroundColor: ColorValue; + readonly children: (close: () => void) => ReactNode; + readonly containerStyle?: StyleProp; + readonly fullSwipeWidth: number; + readonly onDelete: () => void; + readonly onSwipeableClose?: (methods: SwipeableMethods) => void; + readonly onSwipeableWillOpen?: (methods: SwipeableMethods) => void; + readonly primaryAction: ThreadSwipePrimaryAction; + readonly threadTitle: string; +}) { + const swipeableRef = useRef(null); + const fullSwipeArmedRef = useRef(false); + const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, props.fullSwipeWidth * 0.58); + const close = useCallback(() => swipeableRef.current?.close(), []); + const handleFullSwipeArmedChange = useCallback((armed: boolean) => { + if (armed && !fullSwipeArmedRef.current && process.env.EXPO_OS === "ios") { + void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); + } + fullSwipeArmedRef.current = armed; + }, []); + + return ( + { + fullSwipeArmedRef.current = false; + if (swipeableRef.current) { + props.onSwipeableClose?.(swipeableRef.current); + } + }} + onSwipeableOpenStartDrag={() => { + if (swipeableRef.current) { + props.onSwipeableWillOpen?.(swipeableRef.current); + } + }} + onSwipeableWillOpen={() => { + const methods = swipeableRef.current; + if (!methods) { + return; + } + + props.onSwipeableWillOpen?.(methods); + if (fullSwipeArmedRef.current) { + fullSwipeArmedRef.current = false; + methods.close(); + props.onDelete(); + } + }} + overshootFriction={1} + overshootRight + renderRightActions={(_progress, translation, methods) => ( + { + methods.close(); + props.primaryAction.onPress(); + }, + }} + swipeableMethods={methods} + threadTitle={props.threadTitle} + translation={translation} + /> + )} + rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} + > + {props.children(close)} + + ); +} + function SwipeActionButton(props: { readonly accessibilityLabel: string; readonly backgroundColor: string; @@ -179,12 +271,7 @@ export function ThreadSwipeActions(props: { readonly fullSwipeThreshold: number; readonly onDelete: () => void; readonly onFullSwipeArmedChange: (armed: boolean) => void; - readonly primaryAction: { - readonly accessibilityLabel: string; - readonly icon: ComponentProps["name"]; - readonly label: string; - readonly onPress: () => void; - }; + readonly primaryAction: ThreadSwipePrimaryAction; readonly swipeableMethods: SwipeableMethods; readonly threadTitle: string; readonly translation: SharedValue; diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index e4bef426693..82c7b5b8796 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -179,7 +179,7 @@ const ReviewFileNavigatorRow = memo(function ReviewFileNavigatorRow(props: { }); interface ReviewFileNavigatorHandle { - readonly setVisibleFile: (fileId: string) => void; + readonly setVisibleFile: (fileId: string | null) => void; } interface ReviewFileNavigatorProps { @@ -212,7 +212,7 @@ function ReviewFileNavigator({ ref, () => ({ setVisibleFile: (fileId) => { - if (!availableFileIds.includes(fileId)) { + if (fileId !== null && !availableFileIds.includes(fileId)) { return; } setFileSelection((current) => { @@ -482,12 +482,8 @@ export function ReviewSheet() { [collapsedFileIds, commentSelection, toggleExpandedFile], ); const handleVisibleFileChange = useCallback( - (event: NativeSyntheticEvent<{ readonly fileId?: string }>) => { - const { fileId } = event.nativeEvent; - if (!fileId) { - return; - } - reviewFileNavigatorRef.current?.setVisibleFile(fileId); + (event: NativeSyntheticEvent<{ readonly fileId?: string | null }>) => { + reviewFileNavigatorRef.current?.setVisibleFile(event.nativeEvent.fileId ?? null); }, [], ); diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 181e21c9f4a..9d5d4808823 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1124,7 +1124,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const disclosureAnchorKeyRef = useRef(null); const previousLatestTurnRef = useRef(props.latestTurn); const { width: windowWidth } = useWindowDimensions(); - const [viewportWidth, setViewportWidth] = useState(windowWidth); + const [viewportWidth, setViewportWidth] = useState(() => + props.layoutVariant === "split" ? 0 : windowWidth, + ); const [disclosureToggleSettling, setDisclosureToggleSettling] = useState(false); const [interactionState, setInteractionState] = useState<{ readonly copiedRowId: string | null; @@ -1201,6 +1203,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { markdownStyles, reviewCommentColors, userBubbleColor, + viewportWidth, }), [ copiedRowId, @@ -1209,6 +1212,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { markdownStyles, reviewCommentColors, userBubbleColor, + viewportWidth, ], ); const expandedWorkGroupIds = useMemo(() => { diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index f8499122800..959c99b5c7c 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1,7 +1,8 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { SymbolView } from "expo-symbols"; -import { useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { Pressable, ScrollView, StyleSheet, TextInput, View } from "react-native"; +import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; @@ -10,6 +11,8 @@ import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; +import { ThreadSwipeable } from "../home/thread-swipe-actions"; +import { useThreadListActions } from "../home/useThreadListActions"; import { buildThreadNavigationGroups } from "./thread-navigation-groups"; import { SidebarHeaderActions } from "./sidebar-header-actions"; import { threadStatusTone } from "./threadPresentation"; @@ -25,6 +28,8 @@ export function ThreadNavigationSidebar(props: { const projects = useProjects(); const threads = useThreadShells(); const [searchQuery, setSearchQuery] = useState(""); + const openSwipeableRef = useRef(null); + const { archiveThread, confirmDeleteThread } = useThreadListActions(); const groups = useMemo( () => buildThreadNavigationGroups({ projects, threads, searchQuery }), [projects, searchQuery, threads], @@ -38,6 +43,17 @@ export function ThreadNavigationSidebar(props: { const searchBackgroundColor = useThemeColor("--color-subtle-strong"); const selectedBackgroundColor = useThemeColor("--color-subtle-strong"); const pressedBackgroundColor = useThemeColor("--color-subtle"); + const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { + if (openSwipeableRef.current !== methods) { + openSwipeableRef.current?.close(); + openSwipeableRef.current = methods; + } + }, []); + const handleSwipeableClose = useCallback((methods: SwipeableMethods) => { + if (openSwipeableRef.current === methods) { + openSwipeableRef.current = null; + } + }, []); return ( openSwipeableRef.current?.close()} showsVerticalScrollIndicator={false} style={styles.threadList} > @@ -106,33 +123,55 @@ export function ThreadNavigationSidebar(props: { const selected = threadKey === props.selectedThreadKey; return ( - props.onSelectThread(thread)} - style={({ pressed }) => [ - styles.threadRow, - { - backgroundColor: selected - ? selectedBackgroundColor - : pressed - ? pressedBackgroundColor - : "transparent", - }, - ]} + backgroundColor={backgroundColor} + containerStyle={styles.threadRowContainer} + fullSwipeWidth={props.width - 20} + onDelete={() => confirmDeleteThread(thread)} + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + primaryAction={{ + accessibilityLabel: `Archive ${thread.title}`, + icon: "archivebox", + label: "Archive", + onPress: () => archiveThread(thread), + }} + threadTitle={thread.title} > - - - {thread.title} - - - {relativeTime(thread.updatedAt ?? thread.createdAt)} - - - - + {(close) => ( + { + close(); + props.onSelectThread(thread); + }} + style={({ pressed }) => [ + styles.threadRow, + { + backgroundColor: selected + ? selectedBackgroundColor + : pressed + ? pressedBackgroundColor + : backgroundColor, + }, + ]} + > + + + {thread.title} + + + {relativeTime(thread.updatedAt ?? thread.createdAt)} + + + + + )} + ); }) )} @@ -197,6 +236,10 @@ const styles = StyleSheet.create({ alignItems: "center", gap: 8, }, + threadRowContainer: { + borderRadius: 10, + overflow: "hidden", + }, threadText: { minWidth: 0, flex: 1, diff --git a/apps/mobile/src/features/threads/thread-navigation-groups.test.ts b/apps/mobile/src/features/threads/thread-navigation-groups.test.ts index fb2f0ee5768..2cfda2f4865 100644 --- a/apps/mobile/src/features/threads/thread-navigation-groups.test.ts +++ b/apps/mobile/src/features/threads/thread-navigation-groups.test.ts @@ -91,4 +91,21 @@ describe("buildThreadNavigationGroups", () => { })[0]?.threads.map((thread) => thread.id), ).toEqual(["newer", "older"]); }); + + it("excludes archived threads from the navigation sidebar", () => { + const archived = makeThread({ + id: ThreadId.make("archived"), + projectId: project.id, + title: "Archived work", + archivedAt: "2026-06-04T00:00:00.000Z", + updatedAt: "2026-06-04T00:00:00.000Z", + }); + + expect( + buildThreadNavigationGroups({ + projects: [project], + threads: [...threads, archived], + })[0]?.threads.map((thread) => thread.id), + ).toEqual(["newer", "older"]); + }); }); diff --git a/apps/mobile/src/features/threads/thread-navigation-groups.ts b/apps/mobile/src/features/threads/thread-navigation-groups.ts index ae50031b6d5..1531f6deb67 100644 --- a/apps/mobile/src/features/threads/thread-navigation-groups.ts +++ b/apps/mobile/src/features/threads/thread-navigation-groups.ts @@ -30,8 +30,9 @@ export function buildThreadNavigationGroups(input: { readonly searchQuery?: string; }): ReadonlyArray { const query = input.searchQuery?.trim().toLocaleLowerCase() ?? ""; + const activeThreads = input.threads.filter((thread) => thread.archivedAt === null); - return groupProjectsByRepository(input).flatMap((group) => { + return groupProjectsByRepository({ ...input, threads: activeThreads }).flatMap((group) => { const threads = Arr.sort( group.projects.flatMap((projectGroup) => projectGroup.threads), threadActivityOrder, From d3534764174bb32b8d0ee86998aaf22996ba5a69 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 24 Jun 2026 09:25:28 -0700 Subject: [PATCH 04/71] Memoize thread navigation callbacks - Stabilize thread selection routing in the adaptive workspace layout - Extract and memoize thread sidebar rows to reduce unnecessary rerenders Co-authored-by: codex --- .../layout/AdaptiveWorkspaceLayout.tsx | 47 +++--- .../threads/ThreadNavigationSidebar.tsx | 145 ++++++++++++------ 2 files changed, 125 insertions(+), 67 deletions(-) diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 825481fb858..3b975217837 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -189,29 +189,32 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) transform: [{ translateX: (sidebarProgress.value - 1) * 24 }], })); - const handleSelectThread = (thread: EnvironmentThreadShell) => { - const destination = buildThreadRoutePath(thread); - const navigationAction = resolveThreadSelectionNavigationAction({ - usesSplitView: layout.usesSplitView, - pathname, - }); - if (navigationAction === "set-params") { - // Auxiliary content belongs to the current thread. Close it before - // reusing the current native detail screen for a peer thread selection. - setFileInspectorPreferredVisible(false); - router.setParams({ - environmentId: String(thread.environmentId), - threadId: String(thread.id), + const handleSelectThread = useCallback( + (thread: EnvironmentThreadShell) => { + const destination = buildThreadRoutePath(thread); + const navigationAction = resolveThreadSelectionNavigationAction({ + usesSplitView: layout.usesSplitView, + pathname, }); - return; - } - if (navigationAction === "replace") { - setFileInspectorPreferredVisible(false); - router.replace(destination); - return; - } - router.push(destination); - }; + if (navigationAction === "set-params") { + // Auxiliary content belongs to the current thread. Close it before + // reusing the current native detail screen for a peer thread selection. + setFileInspectorPreferredVisible(false); + router.setParams({ + environmentId: String(thread.environmentId), + threadId: String(thread.id), + }); + return; + } + if (navigationAction === "replace") { + setFileInspectorPreferredVisible(false); + router.replace(destination); + return; + } + router.push(destination); + }, + [layout.usesSplitView, pathname, router], + ); return ( diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 959c99b5c7c..db84429f87e 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1,6 +1,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { SymbolView } from "expo-symbols"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { memo, useCallback, useMemo, useRef, useState } from "react"; +import type { ColorValue } from "react-native"; import { Pressable, ScrollView, StyleSheet, TextInput, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -17,6 +18,95 @@ import { buildThreadNavigationGroups } from "./thread-navigation-groups"; import { SidebarHeaderActions } from "./sidebar-header-actions"; import { threadStatusTone } from "./threadPresentation"; +const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { + readonly backgroundColor: ColorValue; + readonly fullSwipeWidth: number; + readonly onArchiveThread: (thread: EnvironmentThreadShell) => void; + readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; + readonly onSelectThread: (thread: EnvironmentThreadShell) => void; + readonly onSwipeableClose: (methods: SwipeableMethods) => void; + readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; + readonly pressedBackgroundColor: ColorValue; + readonly selected: boolean; + readonly selectedBackgroundColor: ColorValue; + readonly thread: EnvironmentThreadShell; +}) { + const { + backgroundColor, + fullSwipeWidth, + onArchiveThread, + onDeleteThread, + onSelectThread, + onSwipeableClose, + onSwipeableWillOpen, + pressedBackgroundColor, + selected, + selectedBackgroundColor, + thread, + } = props; + const handleArchive = useCallback(() => { + onArchiveThread(thread); + }, [onArchiveThread, thread]); + const handleDelete = useCallback(() => { + onDeleteThread(thread); + }, [onDeleteThread, thread]); + const primaryAction = useMemo( + () => ({ + accessibilityLabel: `Archive ${thread.title}`, + icon: "archivebox" as const, + label: "Archive", + onPress: handleArchive, + }), + [handleArchive, thread.title], + ); + + return ( + + {(close) => ( + { + close(); + onSelectThread(thread); + }} + style={({ pressed }) => [ + styles.threadRow, + { + backgroundColor: selected + ? selectedBackgroundColor + : pressed + ? pressedBackgroundColor + : backgroundColor, + }, + ]} + > + + + {thread.title} + + + {relativeTime(thread.updatedAt ?? thread.createdAt)} + + + + + )} + + ); +}); + export function ThreadNavigationSidebar(props: { readonly width: number; readonly selectedThreadKey: string | null; @@ -123,55 +213,20 @@ export function ThreadNavigationSidebar(props: { const selected = threadKey === props.selectedThreadKey; return ( - confirmDeleteThread(thread)} + onArchiveThread={archiveThread} + onDeleteThread={confirmDeleteThread} + onSelectThread={props.onSelectThread} onSwipeableClose={handleSwipeableClose} onSwipeableWillOpen={handleSwipeableWillOpen} - primaryAction={{ - accessibilityLabel: `Archive ${thread.title}`, - icon: "archivebox", - label: "Archive", - onPress: () => archiveThread(thread), - }} - threadTitle={thread.title} - > - {(close) => ( - { - close(); - props.onSelectThread(thread); - }} - style={({ pressed }) => [ - styles.threadRow, - { - backgroundColor: selected - ? selectedBackgroundColor - : pressed - ? pressedBackgroundColor - : backgroundColor, - }, - ]} - > - - - {thread.title} - - - {relativeTime(thread.updatedAt ?? thread.createdAt)} - - - - - )} - + pressedBackgroundColor={pressedBackgroundColor} + selected={selected} + selectedBackgroundColor={selectedBackgroundColor} + thread={thread} + /> ); }) )} From 8351e7ca31107045f8fcb90adad6bfc16c18c429 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 25 Jun 2026 22:43:21 -0700 Subject: [PATCH 05/71] Refine mobile layout for iPad and native glass headers - add thread selection context for split-view routing - switch thread feeds to automatic content insets on iOS glass headers - add native scroll-edge effect helper and app domain config --- apps/mobile/app.config.ts | 10 +- apps/mobile/src/app/_layout.tsx | 2 +- .../[environmentId]/[threadId]/_layout.tsx | 194 +++++++++--------- .../layout/AdaptiveWorkspaceLayout.tsx | 65 +++--- .../layout/adaptive-inspector-layout.tsx | 8 +- .../layout/workspace-pane-transition.ts | 10 - .../features/threads/ThreadDetailScreen.tsx | 2 + .../src/features/threads/ThreadFeed.tsx | 16 +- .../threads/ThreadNavigationSidebar.tsx | 16 +- .../features/threads/ThreadRouteScreen.tsx | 166 ++++++++++----- .../src/lib/native-scroll-edge-effect.test.ts | 18 ++ .../src/lib/native-scroll-edge-effect.ts | 23 +++ apps/mobile/src/state/use-thread-selection.ts | 62 ++++-- 13 files changed, 361 insertions(+), 231 deletions(-) delete mode 100644 apps/mobile/src/features/layout/workspace-pane-transition.ts create mode 100644 apps/mobile/src/lib/native-scroll-edge-effect.test.ts create mode 100644 apps/mobile/src/lib/native-scroll-edge-effect.ts diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index a75f9f06d1b..680d689f36d 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -17,6 +17,7 @@ const VARIANT_CONFIG: Record< readonly iosIcon: string; readonly iosBundleIdentifier: string; readonly androidPackage: string; + readonly relyingParty?: string; } > = { development: { @@ -25,6 +26,7 @@ const VARIANT_CONFIG: Record< iosIcon: "./assets/icon-composer-dev.icon", iosBundleIdentifier: "com.t3tools.t3code.dev", androidPackage: "com.t3tools.t3code.dev", + relyingParty: "clerk.t3.codes", }, preview: { appName: "T3 Code Preview", @@ -32,6 +34,7 @@ const VARIANT_CONFIG: Record< iosIcon: "./assets/icon-composer-prod.icon", iosBundleIdentifier: "com.t3tools.t3code.preview", androidPackage: "com.t3tools.t3code.preview", + relyingParty: "clerk.t3.codes", }, production: { appName: "T3 Code", @@ -39,6 +42,7 @@ const VARIANT_CONFIG: Record< iosIcon: "./assets/icon-composer-prod.icon", iosBundleIdentifier: "com.t3tools.t3code", androidPackage: "com.t3tools.t3code", + relyingParty: "clerk.t3.codes", }, }; @@ -64,7 +68,7 @@ const config: ExpoConfig = { runtimeVersion: { policy: process.env.MOBILE_VERSION_POLICY ?? "appVersion", }, - orientation: "default", + orientation: "portrait", icon: "./assets/icon.png", userInterfaceStyle: "automatic", updates: { @@ -77,6 +81,10 @@ const config: ExpoConfig = { icon: variant.iosIcon, supportsTablet: true, bundleIdentifier: variant.iosBundleIdentifier, + associatedDomains: [ + `applinks:${variant.relyingParty}`, + `webcredentials:${variant.relyingParty}`, + ], infoPlist: { NSAppTransportSecurity: { NSAllowsArbitraryLoads: true, diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 43b490edcbd..23762c4cdce 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -95,7 +95,7 @@ function WorkspaceNavigator() { }; const settingsScreenOptions = layout.usesSplitView ? { - animation: "fade" as const, + animation: "none" as const, contentStyle: sheetStyle, gestureEnabled: false, headerShown: false, diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx index 6f2fb22833f..d69e42310bd 100644 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +++ b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx @@ -2,6 +2,7 @@ import Stack from "expo-router/stack"; import { StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; import { useAdaptiveWorkspaceLayout } from "../../../../features/layout/AdaptiveWorkspaceLayout"; +import { ThreadSelectionProvider } from "../../../../state/use-thread-selection"; export default function ThreadLayout() { const { fileInspector } = useAdaptiveWorkspaceLayout(); @@ -11,101 +12,102 @@ export default function ThreadLayout() { }; return ( - - - - - - - - - - + + + + + + + + + + + + ); } diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 3b975217837..2dccb7c3571 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,18 +1,8 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useFocusEffect, useGlobalSearchParams, usePathname, useRouter } from "expo-router"; -import { - createContext, - use, - useCallback, - useEffect, - useMemo, - useRef, - useState, - type ReactNode, -} from "react"; +import { createContext, use, useCallback, useMemo, useRef, useState, type ReactNode } from "react"; import { useWindowDimensions, View } from "react-native"; -import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"; import { deriveFileInspectorPaneLayout, @@ -27,7 +17,6 @@ import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navig import { buildThreadRoutePath } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; -import { WORKSPACE_PANE_LAYOUT_TRANSITION } from "./workspace-pane-transition"; interface AdaptiveWorkspaceContextValue { readonly layout: Layout; @@ -86,7 +75,6 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const [fileInspectorPreferredVisible, setFileInspectorPreferredVisible] = useState(true); const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = useState(null); - const sidebarProgress = useSharedValue(1); const params = useGlobalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; @@ -181,13 +169,12 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) ], ); - useEffect(() => { - sidebarProgress.value = withTiming(panes.primarySidebarVisible ? 1 : 0, { duration: 220 }); - }, [panes.primarySidebarVisible, sidebarProgress]); - const sidebarStyle = useAnimatedStyle(() => ({ - opacity: sidebarProgress.value, - transform: [{ translateX: (sidebarProgress.value - 1) * 24 }], - })); + const handleOpenSettings = useCallback(() => { + router.push("/settings"); + }, [router]); + const handleStartNewTask = useCallback(() => { + router.push("/new"); + }, [router]); const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { @@ -197,8 +184,10 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) pathname, }); if (navigationAction === "set-params") { - // Auxiliary content belongs to the current thread. Close it before - // reusing the current native detail screen for a peer thread selection. + const nextThreadKey = scopedThreadKey(thread.environmentId, thread.id); + if (nextThreadKey === selectedThreadKey) { + return; + } setFileInspectorPreferredVisible(false); router.setParams({ environmentId: String(thread.environmentId), @@ -213,44 +202,36 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) } router.push(destination); }, - [layout.usesSplitView, pathname, router], + [layout.usesSplitView, pathname, router, selectedThreadKey], ); return ( {layout.usesSplitView && layout.listPaneWidth !== null ? ( - router.push("/settings")} + onOpenSettings={handleOpenSettings} onSelectThread={handleSelectThread} - onStartNewTask={() => router.push("/new")} + onStartNewTask={handleStartNewTask} /> - + ) : null} - + {props.children} - + ); diff --git a/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx b/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx index 0920f409ebc..4af0e8e3130 100644 --- a/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx +++ b/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx @@ -8,7 +8,6 @@ import Animated, { } from "react-native-reanimated"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; -import { WORKSPACE_PANE_LAYOUT_TRANSITION } from "./workspace-pane-transition"; export function AdaptiveInspectorLayout(props: { readonly children: ReactNode; @@ -41,11 +40,7 @@ export function AdaptiveInspectorLayout(props: { return ( - + {props.children} {inspectorSupported ? ( @@ -53,7 +48,6 @@ export function AdaptiveInspectorLayout(props: { accessibilityElementsHidden={!inspectorVisible} collapsable={false} importantForAccessibility={inspectorVisible ? "auto" : "no-hide-descendants"} - layout={WORKSPACE_PANE_LAYOUT_TRANSITION} pointerEvents={inspectorVisible ? "auto" : "none"} style={[ { diff --git a/apps/mobile/src/features/layout/workspace-pane-transition.ts b/apps/mobile/src/features/layout/workspace-pane-transition.ts deleted file mode 100644 index d688b6b739b..00000000000 --- a/apps/mobile/src/features/layout/workspace-pane-transition.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { Easing, LinearTransition } from "react-native-reanimated"; - -/** - * Animates between final Yoga layouts on the UI thread. Keeping pane widths - * out of animated styles avoids recalculating the entire workspace on every - * display frame while a sidebar or inspector moves. - */ -export const WORKSPACE_PANE_LAYOUT_TRANSITION = LinearTransition.duration(220).easing( - Easing.out(Easing.cubic), -); diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 39e4dc4c6d4..2f9e4a94821 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -69,6 +69,7 @@ export interface ThreadDetailScreenProps { readonly selectedThreadQueueCount: number; readonly serverConfig: T3ServerConfig | null; readonly layoutVariant?: LayoutVariant; + readonly usesAutomaticContentInsets?: boolean; readonly onOpenDrawer: () => void; readonly onOpenConnectionEditor: () => void; readonly onChangeDraftMessage: (value: string) => void; @@ -387,6 +388,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread contentBottomInset={estimatedOverlayHeight} contentMaxWidth={contentMaxWidth} layoutVariant={layoutVariant} + usesAutomaticContentInsets={props.usesAutomaticContentInsets} skills={selectedProviderSkills} /> diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 9d5d4808823..4ffdbe04d91 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -38,7 +38,7 @@ import { import { TouchableOpacity } from "react-native-gesture-handler"; import ImageViewing from "react-native-image-viewing"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import type { SharedValue } from "react-native-reanimated"; +import { type SharedValue } from "react-native-reanimated"; import { useThemeColor } from "../../lib/useThemeColor"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; import { @@ -108,6 +108,7 @@ export interface ThreadFeedProps { readonly contentBottomInset?: number; readonly contentMaxWidth?: number; readonly layoutVariant?: LayoutVariant; + readonly usesAutomaticContentInsets?: boolean; readonly skills?: ReadonlyArray; } @@ -1156,7 +1157,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const insets = useSafeAreaInsets(); const topContentInset = props.contentTopInset ?? insets.top + 44; const bottomContentInset = props.contentBottomInset ?? 18; - const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { const nextWidth = Math.round(event.nativeEvent.layout.width); setViewportWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); @@ -1473,8 +1473,14 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ref={props.listRef} key={props.threadId} style={{ flex: 1 }} + contentInsetAdjustmentBehavior="never" automaticallyAdjustsScrollIndicatorInsets={false} - scrollIndicatorInsets={{ top: topContentInset, bottom: 0 }} + {...(props.usesAutomaticContentInsets + ? { + contentInset: { top: topContentInset, left: 0, right: 0, bottom: 0 }, + scrollIndicatorInsets: { top: 0, left: 0, right: 0, bottom: 0 }, + } + : { scrollIndicatorInsets: { top: topContentInset, bottom: 0 } })} {...(anchoredEndSpace ? { anchoredEndSpace } : {})} contentInsetEndAdjustment={props.contentInsetEndAdjustment} freeze={props.freeze} @@ -1503,7 +1509,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { keyboardLiftBehavior="whenAtEnd" estimatedItemSize={180} initialScrollAtEnd - ListHeaderComponent={} + ListHeaderComponent={ + props.usesAutomaticContentInsets ? null : + } contentContainerStyle={{ paddingTop: 12, paddingHorizontal: contentHorizontalPadding, diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index db84429f87e..83341ccc55d 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -71,16 +71,13 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { primaryAction={primaryAction} threadTitle={thread.title} > - {(close) => ( + {() => ( { - close(); - onSelectThread(thread); - }} + onPress={() => onSelectThread(thread)} style={({ pressed }) => [ styles.threadRow, { @@ -144,6 +141,13 @@ export function ThreadNavigationSidebar(props: { openSwipeableRef.current = null; } }, []); + const handleSelectThread = useCallback( + (thread: EnvironmentThreadShell) => { + props.onSelectThread(thread); + openSwipeableRef.current?.close(); + }, + [props.onSelectThread], + ); return ( ; } +interface ThreadRouteScreenProps { + readonly onReturnToThread?: () => void; + readonly renderInspector?: () => ReactNode; +} + +function ThreadUnavailableScreen() { + return ( + + + + ); +} + +export function ThreadRouteScreen(props: ThreadRouteScreenProps = {}) { + const { state: workspaceState } = useWorkspaceState(); + const { connectionState } = useRemoteConnectionStatus(); + const { selectedThread } = useThreadSelection(); + const params = useLocalSearchParams<{ + environmentId?: string | string[]; + threadId?: string | string[]; + }>(); + const environmentIdRaw = firstRouteParam(params.environmentId); + const threadIdRaw = firstRouteParam(params.threadId); + const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; + const routeEnvironmentRuntime = useRemoteEnvironmentRuntime(environmentId); + const routeConnectionState = + routeEnvironmentRuntime?.connectionState ?? (environmentId ? "available" : connectionState); + const routeThreadKey = + environmentId !== null && threadIdRaw !== null + ? scopedThreadKey(environmentId, ThreadId.make(threadIdRaw)) + : null; + const selectedThreadKey = + selectedThread === null + ? null + : scopedThreadKey(selectedThread.environmentId, selectedThread.id); + const selectedThreadDetailState = useSelectedThreadDetailState(); + const hasThreadDetail = Option.isSome(selectedThreadDetailState.data); + const hasTerminalDetailState = + selectedThreadDetailState.status === "deleted" || + Option.isSome(selectedThreadDetailState.error); + + if (environmentId === null || threadIdRaw === null) { + return ; + } + + if (selectedThread !== null && selectedThreadKey === routeThreadKey) { + if (!hasThreadDetail && !hasTerminalDetailState) { + return ; + } + return ; + } + + const stillHydrating = + workspaceState.isLoadingConnections || + routeConnectionState === "connecting" || + routeConnectionState === "reconnecting"; + + if (stillHydrating) { + return ; + } + + return ; +} + function ThreadHeaderTitle(props: { readonly foregroundColor: string; readonly secondaryForegroundColor: string; @@ -128,21 +209,19 @@ function ThreadHeaderTitle(props: { ); } -export function ThreadRouteScreen( - props: { - readonly onReturnToThread?: () => void; - readonly renderInspector?: () => ReactNode; - } = {}, +function ThreadRouteContent( + props: ThreadRouteScreenProps & { + readonly selectedThreadDetailState: ReturnType; + }, ) { const { fileInspector, layout, showAuxiliaryPane, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); const headerHeight = useHeaderHeight(); - const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = useThreadSelection(); - const selectedThreadDetailState = useSelectedThreadDetailState(); + const selectedThreadDetailState = props.selectedThreadDetailState; const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data); const { selectedThreadCwd } = useSelectedThreadWorktree(); const composer = useThreadComposerState(); @@ -208,18 +287,6 @@ export function ThreadRouteScreen( ] .filter(Boolean) .join(" · "); - const renderHeaderTitle = useCallback( - () => ( - - ), - [foregroundColor, headerSubtitle, secondaryFg, selectedThread?.title], - ); - /* ─── Git status for native header trigger ───────────────────────── */ const gitStatus = useEnvironmentQuery( selectedThread !== null && selectedThreadCwd !== null @@ -484,32 +551,7 @@ export function ThreadRouteScreen( } if (!selectedThread) { - const stillHydrating = - workspaceState.isLoadingConnections || - routeConnectionState === "connecting" || - routeConnectionState === "reconnecting"; - - if (stillHydrating) { - return ; - } - - return ( - - - - ); + return ; } const selectedThreadKey = scopedThreadKey(selectedThread.environmentId, selectedThread.id); @@ -527,16 +569,37 @@ export function ThreadRouteScreen( + + + + {props.onReturnToThread ? ( { + it("keeps the automatic native treatment on iOS 26", () => { + expect(nativeTopScrollEdgeEffect("ios", "26.5")).toBe("automatic"); + }); + + it("uses the native hard treatment on iOS 27 and later", () => { + expect(nativeTopScrollEdgeEffect("ios", "27.0")).toBe("hard"); + expect(nativeTopScrollEdgeEffect("ios", 28)).toBe("hard"); + }); + + it("does not apply the iOS workaround to other platforms", () => { + expect(nativeTopScrollEdgeEffect("android", 27)).toBe("automatic"); + }); +}); diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.ts b/apps/mobile/src/lib/native-scroll-edge-effect.ts new file mode 100644 index 00000000000..a0704b5c1d0 --- /dev/null +++ b/apps/mobile/src/lib/native-scroll-edge-effect.ts @@ -0,0 +1,23 @@ +export type NativeTopScrollEdgeEffect = "automatic" | "hard"; + +function majorVersion(version: number | string): number { + if (typeof version === "number") { + return Math.trunc(version); + } + + const parsed = Number.parseInt(version, 10); + return Number.isNaN(parsed) ? 0 : parsed; +} + +/** + * iOS 27 beta currently renders the automatic/soft top scroll-edge effect as + * fully transparent. Keep the subtler automatic treatment on iOS 26 and use + * UIKit's native hard treatment on iOS 27+ until the platform regression is + * resolved. + */ +export function nativeTopScrollEdgeEffect( + os: string, + version: number | string, +): NativeTopScrollEdgeEffect { + return os === "ios" && majorVersion(version) >= 27 ? "hard" : "automatic"; +} diff --git a/apps/mobile/src/state/use-thread-selection.ts b/apps/mobile/src/state/use-thread-selection.ts index 06175b6d237..8f3a1c2d1cf 100644 --- a/apps/mobile/src/state/use-thread-selection.ts +++ b/apps/mobile/src/state/use-thread-selection.ts @@ -1,6 +1,11 @@ -import { useLocalSearchParams } from "expo-router"; -import { useMemo } from "react"; -import { EnvironmentId, ThreadId, type ScopedProjectRef } from "@t3tools/contracts"; +import { useGlobalSearchParams } from "expo-router"; +import { createContext, createElement, use, useMemo, useRef, type ReactNode } from "react"; +import { + EnvironmentId, + ThreadId, + type ScopedProjectRef, + type ScopedThreadRef, +} from "@t3tools/contracts"; import { useProject, useThreadShell } from "../state/entities"; import { @@ -16,12 +21,12 @@ function firstRouteParam(value: string | string[] | undefined): string | null { return value ?? null; } -export function useThreadSelection() { - const params = useLocalSearchParams<{ +function useResolvedThreadSelection() { + const params = useGlobalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); - const selectedThreadRef = useMemo(() => { + const routeThreadRef = useMemo(() => { const environmentId = firstRouteParam(params.environmentId); const threadId = firstRouteParam(params.threadId); if (!environmentId || !threadId) { @@ -33,6 +38,11 @@ export function useThreadSelection() { threadId: ThreadId.make(threadId), }; }, [params.environmentId, params.threadId]); + const lastRouteThreadRef = useRef(null); + if (routeThreadRef !== null) { + lastRouteThreadRef.current = routeThreadRef; + } + const selectedThreadRef = routeThreadRef ?? lastRouteThreadRef.current; const selectedThread = useThreadShell(selectedThreadRef); const selectedProjectRef = useMemo( () => @@ -49,11 +59,37 @@ export function useThreadSelection() { const selectedEnvironmentConnection = useSavedRemoteConnection(selectedEnvironmentId); const selectedEnvironmentRuntime = useRemoteEnvironmentRuntime(selectedEnvironmentId); - return { - selectedThreadRef, - selectedThread, - selectedThreadProject, - selectedEnvironmentConnection, - selectedEnvironmentRuntime, - }; + return useMemo( + () => ({ + selectedThreadRef, + selectedThread, + selectedThreadProject, + selectedEnvironmentConnection, + selectedEnvironmentRuntime, + }), + [ + selectedEnvironmentConnection, + selectedEnvironmentRuntime, + selectedThread, + selectedThreadProject, + selectedThreadRef, + ], + ); +} + +type ThreadSelectionState = ReturnType; + +const ThreadSelectionContext = createContext(null); + +export function ThreadSelectionProvider(props: { readonly children: ReactNode }) { + const selection = useResolvedThreadSelection(); + return createElement(ThreadSelectionContext.Provider, { value: selection }, props.children); +} + +export function useThreadSelection(): ThreadSelectionState { + const selection = use(ThreadSelectionContext); + if (selection === null) { + throw new Error("useThreadSelection must be used within ThreadSelectionProvider"); + } + return selection; } From babbf901971da3e4850911a6283ab1ad452b740e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 17:25:24 -0700 Subject: [PATCH 06/71] Update Expo iOS tooling and native glass header --- apps/mobile/app.config.ts | 1 + apps/mobile/package.json | 39 +- apps/mobile/plugins/withIosSceneLifecycle.cjs | 81 ++ .../features/threads/ThreadRouteScreen.tsx | 12 +- ...tch => @expo%2Fmetro-config@56.0.14.patch} | 0 patches/expo-modules-jsi@56.0.10.patch | 21 + pnpm-lock.yaml | 716 ++++++++++-------- pnpm-workspace.yaml | 5 +- 8 files changed, 546 insertions(+), 329 deletions(-) create mode 100644 apps/mobile/plugins/withIosSceneLifecycle.cjs rename patches/{@expo%2Fmetro-config@56.0.13.patch => @expo%2Fmetro-config@56.0.14.patch} (100%) create mode 100644 patches/expo-modules-jsi@56.0.10.patch diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 680d689f36d..074233a6c8a 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -163,6 +163,7 @@ const config: ExpoConfig = { ], }, ], + "./plugins/withIosSceneLifecycle.cjs", "./plugins/withAndroidCleartextTraffic.cjs", ], extra: { diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 963cefd6b7c..c9e8051ede3 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -43,7 +43,8 @@ "@clerk/expo": "catalog:", "@effect/atom-react": "catalog:", "@expo-google-fonts/dm-sans": "^0.4.2", - "@expo/ui": "~56.0.8", + "@expo/metro-runtime": "~56.0.15", + "@expo/ui": "~56.0.18", "@legendapp/list": "3.2.0", "@noble/curves": "catalog:", "@noble/hashes": "catalog:", @@ -62,38 +63,38 @@ "clsx": "^2.1.1", "diff": "8.0.3", "effect": "catalog:", - "expo": "^56.0.0", - "expo-asset": "~56.0.15", - "expo-auth-session": "~56.0.12", - "expo-build-properties": "~56.0.15", - "expo-camera": "~56.0.7", - "expo-clipboard": "~56.0.3", - "expo-constants": "~56.0.16", + "expo": "~56.0.12", + "expo-asset": "~56.0.17", + "expo-auth-session": "~56.0.14", + "expo-build-properties": "~56.0.19", + "expo-camera": "~56.0.8", + "expo-clipboard": "~56.0.4", + "expo-constants": "~56.0.18", "expo-crypto": "~56.0.4", - "expo-dev-client": "~56.0.16", - "expo-file-system": "~56.0.7", - "expo-font": "~56.0.5", + "expo-dev-client": "~56.0.20", + "expo-file-system": "~56.0.8", + "expo-font": "~56.0.7", "expo-glass-effect": "~56.0.4", "expo-haptics": "~56.0.3", - "expo-image-picker": "~56.0.14", - "expo-linking": "~56.0.12", + "expo-image-picker": "~56.0.18", + "expo-linking": "~56.0.14", "expo-network": "~56.0.5", - "expo-notifications": "~56.0.14", + "expo-notifications": "~56.0.18", "expo-paste-input": "^0.1.15", - "expo-router": "~56.2.7", + "expo-router": "~56.2.11", "expo-secure-store": "~56.0.4", "expo-splash-screen": "~56.0.10", - "expo-symbols": "~56.0.5", - "expo-updates": "~56.0.17", + "expo-symbols": "~56.0.6", + "expo-updates": "~56.0.19", "expo-web-browser": "~56.0.5", - "expo-widgets": "~56.0.15", + "expo-widgets": "~56.0.19", "punycode": "^2.3.1", "react": "19.2.3", "react-dom": "19.2.3", "react-native": "0.85.3", "react-native-gesture-handler": "~2.31.1", "react-native-image-viewing": "^0.2.2", - "react-native-keyboard-controller": "1.21.7", + "react-native-keyboard-controller": "1.21.6", "react-native-nitro-markdown": "^0.5.0", "react-native-nitro-modules": "0.35.9", "react-native-reanimated": "4.3.1", diff --git a/apps/mobile/plugins/withIosSceneLifecycle.cjs b/apps/mobile/plugins/withIosSceneLifecycle.cjs new file mode 100644 index 00000000000..cfb2a159c46 --- /dev/null +++ b/apps/mobile/plugins/withIosSceneLifecycle.cjs @@ -0,0 +1,81 @@ +const { withAppDelegate, withInfoPlist } = require("expo/config-plugins"); + +const SCENE_DELEGATE = ` + +class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard + let windowScene = scene as? UIWindowScene, + let appDelegate = UIApplication.shared.delegate as? AppDelegate, + let appWindow = appDelegate.window + else { + return + } + + window = appWindow + appWindow.windowScene = windowScene + appWindow.makeKeyAndVisible() + + if let url = connectionOptions.urlContexts.first?.url { + _ = appDelegate.application(UIApplication.shared, open: url, options: [:]) + } + } + + func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { + guard + let url = URLContexts.first?.url, + let appDelegate = UIApplication.shared.delegate as? AppDelegate + else { + return + } + + _ = appDelegate.application(UIApplication.shared, open: url, options: [:]) + } + + func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { + guard let appDelegate = UIApplication.shared.delegate as? AppDelegate else { + return + } + + _ = appDelegate.application( + UIApplication.shared, + continue: userActivity, + restorationHandler: { _ in }) + } +}`; + +module.exports = function withIosSceneLifecycle(config) { + config = withInfoPlist(config, (nextConfig) => { + nextConfig.modResults.UIApplicationSceneManifest = { + UIApplicationSupportsMultipleScenes: false, + UISceneConfigurations: { + UIWindowSceneSessionRoleApplication: [ + { + UISceneConfigurationName: "Default Configuration", + UISceneDelegateClassName: "$(PRODUCT_MODULE_NAME).SceneDelegate", + }, + ], + }, + }; + + return nextConfig; + }); + + return withAppDelegate(config, (nextConfig) => { + if (nextConfig.modResults.language !== "swift") { + throw new Error("The iOS scene lifecycle plugin requires a Swift AppDelegate."); + } + + if (!nextConfig.modResults.contents.includes("class SceneDelegate:")) { + nextConfig.modResults.contents += SCENE_DELEGATE; + } + + return nextConfig; + }); +}; diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 438fa5b2860..f0aeae4a724 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -570,8 +570,10 @@ function ThreadRouteContent( options={{ headerShown: true, headerTransparent: USES_NATIVE_GLASS_HEADER, + headerBlurEffect: USES_NATIVE_GLASS_HEADER ? "systemThinMaterial" : undefined, + headerShadowVisible: false, ...(USES_NATIVE_GLASS_HEADER - ? {} + ? { headerStyle: { backgroundColor: "transparent" } } : { headerStyle: { backgroundColor: "transparent" }, headerShadowVisible: false, @@ -580,10 +582,10 @@ function ThreadRouteContent( headerBackVisible: !layout.usesSplitView, headerBackTitle: "", scrollEdgeEffects: { - // iOS 27 beta currently renders automatic/soft as transparent. - // Keep UIKit's subtler automatic style on iOS 26 and use its - // native hard edge treatment on iOS 27+. - top: TOP_SCROLL_EDGE_EFFECT, + // The native header material handles the effect on iOS 27 because + // this virtualized list is not discoverable by the stack's native + // first-descendant scroll-view lookup. + top: USES_NATIVE_GLASS_HEADER ? "hidden" : TOP_SCROLL_EDGE_EFFECT, bottom: "hidden", left: "hidden", right: "hidden", diff --git a/patches/@expo%2Fmetro-config@56.0.13.patch b/patches/@expo%2Fmetro-config@56.0.14.patch similarity index 100% rename from patches/@expo%2Fmetro-config@56.0.13.patch rename to patches/@expo%2Fmetro-config@56.0.14.patch diff --git a/patches/expo-modules-jsi@56.0.10.patch b/patches/expo-modules-jsi@56.0.10.patch new file mode 100644 index 00000000000..afb1da5caaa --- /dev/null +++ b/patches/expo-modules-jsi@56.0.10.patch @@ -0,0 +1,21 @@ +diff --git a/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift b/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +index a0e3e24d6070c5ec0a220af3cb56fe3373fe1968..d610cc38dd8dee363ea2f1822aea0c4a492cf825 100644 +--- a/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift ++++ b/apple/Sources/ExpoModulesJSI/Runtime/JavaScriptRuntime.swift +@@ -215,8 +215,15 @@ open class JavaScriptRuntime: Equatable, @unchecked Sendable { + .toOpaque() + // Pass a null setter to C++ when the Swift setter is nil so that JS assignment + // raises a `jsi::JSError` directly, without crossing the Swift boundary. ++ if set == nil { ++ let callbacks = expo.HostObjectCallbacks( ++ context, getter, nil, propertyNamesGetter, deallocate) ++ let hostObject = expo.HostObject.makeObject(pointee, consume callbacks) ++ ++ return JavaScriptObject(self, hostObject) ++ } + let callbacks = expo.HostObjectCallbacks( +- context, getter, set == nil ? nil : setter, propertyNamesGetter, deallocate) ++ context, getter, setter, propertyNamesGetter, deallocate) + let hostObject = expo.HostObject.makeObject(pointee, consume callbacks) + + return JavaScriptObject(self, hostObject) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4645ecc34e9..9de9e9ac3ff 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -56,7 +56,7 @@ overrides: '@effect/sql-sqlite-bun': 4.0.0-beta.78 '@effect/vitest': 4.0.0-beta.78 '@effect/vitest>vitest': '-' - '@expo/metro-config': 56.0.13 + '@expo/metro-config': 56.0.14 '@pierre/diffs>@shikijs/transformers': ^4.2.0 '@types/node': 24.12.4 effect: 4.0.0-beta.78 @@ -69,9 +69,9 @@ patchedDependencies: '@effect/vitest@4.0.0-beta.78': hash: 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f path: patches/@effect__vitest@4.0.0-beta.78.patch - '@expo/metro-config@56.0.13': + '@expo/metro-config@56.0.14': hash: 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 - path: patches/@expo%2Fmetro-config@56.0.13.patch + path: patches/@expo%2Fmetro-config@56.0.14.patch '@ff-labs/fff-node@0.9.4': hash: 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 path: patches/@ff-labs__fff-node@0.9.4.patch @@ -81,6 +81,9 @@ patchedDependencies: effect@4.0.0-beta.78: hash: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 path: patches/effect@4.0.0-beta.78.patch + expo-modules-jsi@56.0.10: + hash: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f + path: patches/expo-modules-jsi@56.0.10.patch react-native-nitro-modules@0.35.9: hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 path: patches/react-native-nitro-modules@0.35.9.patch @@ -195,16 +198,19 @@ importers: version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@clerk/expo': specifier: 3.5.3-snapshot.v20260622234151 - version: 3.5.3-snapshot.v20260622234151(expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.16)(expo-crypto@56.0.4(expo@56.0.8))(expo-secure-store@56.0.4(expo@56.0.8))(expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 3.5.3-snapshot.v20260622234151(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@effect/atom-react': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(react@19.2.3)(scheduler@0.27.0) '@expo-google-fonts/dm-sans': specifier: ^0.4.2 version: 0.4.2 + '@expo/metro-runtime': + specifier: ~56.0.15 + version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/ui': - specifier: ~56.0.8 - version: 56.0.15(961c4aa6f32829b318e3c87ef20ad401) + specifier: ~56.0.18 + version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@legendapp/list': specifier: 3.2.0 version: 3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -240,7 +246,7 @@ importers: version: link:../../packages/contracts '@t3tools/mobile-markdown-text': specifier: file:./modules/t3-markdown-text - version: file:apps/mobile/modules/t3-markdown-text(f0ff241d48c23db461fff5d47e450578) + version: file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe) '@t3tools/mobile-review-diff-native': specifier: file:./modules/t3-review-diff version: file:apps/mobile/modules/t3-review-diff @@ -260,80 +266,80 @@ importers: specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5) expo: - specifier: ^56.0.0 - version: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + specifier: ~56.0.12 + version: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-asset: - specifier: ~56.0.15 - version: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + specifier: ~56.0.17 + version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-auth-session: - specifier: ~56.0.12 - version: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.14 + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-build-properties: - specifier: ~56.0.15 - version: 56.0.16(expo@56.0.8) + specifier: ~56.0.19 + version: 56.0.19(expo@56.0.12) expo-camera: - specifier: ~56.0.7 - version: 56.0.7(@types/emscripten@1.41.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.8 + version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-clipboard: - specifier: ~56.0.3 - version: 56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.4 + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-constants: - specifier: ~56.0.16 - version: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + specifier: ~56.0.18 + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.8) + version: 56.0.4(expo@56.0.12) expo-dev-client: - specifier: ~56.0.16 - version: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + specifier: ~56.0.20 + version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-file-system: - specifier: ~56.0.7 - version: 56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + specifier: ~56.0.8 + version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-font: - specifier: ~56.0.5 - version: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.7 + version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-glass-effect: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: specifier: ~56.0.3 - version: 56.0.3(expo@56.0.8) + version: 56.0.3(expo@56.0.12) expo-image-picker: - specifier: ~56.0.14 - version: 56.0.15(expo@56.0.8) + specifier: ~56.0.18 + version: 56.0.18(expo@56.0.12) expo-linking: - specifier: ~56.0.12 - version: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.14 + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-network: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.8)(react@19.2.3) + version: 56.0.5(expo@56.0.12)(react@19.2.3) expo-notifications: - specifier: ~56.0.14 - version: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + specifier: ~56.0.18 + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-paste-input: specifier: ^0.1.15 - version: 0.1.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: - specifier: ~56.2.7 - version: 56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310) + specifier: ~56.2.11 + version: 56.2.11(53652d9673d8ec829763e55ad4f6fd4e) expo-secure-store: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.8) + version: 56.0.4(expo@56.0.12) expo-splash-screen: specifier: ~56.0.10 - version: 56.0.10(expo@56.0.8)(typescript@6.0.3) + version: 56.0.10(expo@56.0.12)(typescript@6.0.3) expo-symbols: - specifier: ~56.0.5 - version: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.6 + version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-updates: - specifier: ~56.0.17 - version: 56.0.17(expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: ~56.0.19 + version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-web-browser: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: - specifier: ~56.0.15 - version: 56.0.16(961c4aa6f32829b318e3c87ef20ad401) + specifier: ~56.0.19 + version: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) punycode: specifier: ^2.3.1 version: 2.3.1 @@ -353,8 +359,8 @@ importers: specifier: ^0.2.2 version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-keyboard-controller: - specifier: 1.21.7 - version: 1.21.7(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: 1.21.6 + version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown: specifier: ^0.5.0 version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -403,7 +409,7 @@ importers: version: 19.2.16 babel-preset-expo: specifier: ~56.0.0 - version: 56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.16)(expo@56.0.8)(react-refresh@0.14.2) + version: 56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2) tailwindcss: specifier: ^4.0.0 version: 4.3.0 @@ -2375,8 +2381,8 @@ packages: '@expo-google-fonts/material-symbols@0.4.38': resolution: {integrity: sha512-IJkBtN1o8u9BW5fvSii1MyHPQ7Q0HxbWcVBvOrOzgMLpVtZw7R2w94wBTVR7kZwv3w1JNTESMmLA5Sqn1+Z36A==} - '@expo/cli@56.1.13': - resolution: {integrity: sha512-7n5VzlBr7TKW0BgWgpEopWy+v8buPhMvbSEsuXD+bI1YIJBopkfWAub0qTvlc357E8wWOvV5MJXYyoeRvoOjoQ==} + '@expo/cli@56.1.16': + resolution: {integrity: sha512-VBQn0mqAwc67b9Cn0RVXyeodghomAx5xGRhA/bXaQzuxDjMQk0zIOb6pXMZX7yiIwJW66UZt/zQiJNSv6aWJYw==} hasBin: true peerDependencies: expo: '*' @@ -2394,9 +2400,15 @@ packages: '@expo/config-plugins@56.0.8': resolution: {integrity: sha512-phTuyBhgVLfqUHMjQkAfRtbyoY6yTxoKja1awtpVnEkoJDxPJuXx1KX5uvq1eZtt4bJQ08OBJ6P95INqRSHpRg==} + '@expo/config-plugins@56.0.9': + resolution: {integrity: sha512-/6a/S9USwx8OC9tGjHxbviLFiBHyueN3aoNWMLvWDEJoZ1CIVW800ZBzwXq/FYNK2qzcN1LxFmQtzD1zeFQKNA==} + '@expo/config-types@56.0.5': resolution: {integrity: sha512-GsAHO/MwW9ZRdgnmyfRXqVGLCP/zejD6rWnp5OROp8mBGRObKm4HfrjlUyT1skjMwCj1OrURx9ZfIc6yeBAkIA==} + '@expo/config-types@56.0.6': + resolution: {integrity: sha512-4Y6Aum5J4Re5NnxGVofRNe1aDwUBOmWhQYkynZsqzRtX/zEA1ADUeyHXuEckv9YD9djiyT7bKtLt5gKL3mA6VQ==} + '@expo/config@56.0.9': resolution: {integrity: sha512-/lqFeWGSrhpKJVP8tTN8LjuoIe8u8q2w7FzBL0C+wHgl+WM8l1qUIEYWy/sMvsG/NbpUIUsDHJRhQvOkU58eIw==} @@ -2425,18 +2437,18 @@ packages: resolution: {integrity: sha512-9HnnIbzwTTdbwSjNLXTk0fPm9ZwMJ7c1/31tsni8HZ8Q62KzYCyspahH+V365vg5J6lr001DzNwBxVWSaYCQLg==} engines: {node: '>=20.12.0'} - '@expo/expo-modules-macros-plugin@0.0.9': - resolution: {integrity: sha512-odai6D7ng/gA7At8ukFcWcauNEeDdyVqzVPbQxDkyU2NTJ4kgphA4I5iigS5C4LXFicSIzEt2nzdlLM8sjsTdA==} + '@expo/expo-modules-macros-plugin@0.2.2': + resolution: {integrity: sha512-4IMzPDIo/VOXREQjsJtliSfqYVZvfzU2SLFS/9sKMWF848S8CHx+e/E+Vf0TcMvpWCCKX5umyqxb13KJJ+YUzg==} - '@expo/fingerprint@0.19.3': - resolution: {integrity: sha512-w9Au2IVrtc0Ct+WRa05DVHGNHXYq6VyPZWuFP+5x055OeZ5q6k5Yg+aJ1gfShmjdOhDftRcsvmWmTdTZlWaTZw==} + '@expo/fingerprint@0.19.4': + resolution: {integrity: sha512-PsowRlO8+S7JlO8go7yhNEXp7sqlsWDE2AlCwoss7zH0dcajXFo74Fy0KdXEc4UXK7kKoHD37oDgsZ8aHSLr7A==} hasBin: true '@expo/image-utils@0.10.1': resolution: {integrity: sha512-YDeefvmYdihS7Wp3ESDUVnOgOSWmj2Cczm9lVNDdm4MqQLdAKm/LPYg83HtFQPfefRlAxyHrQR/O9kIXN9C1Wg==} - '@expo/inline-modules@0.0.10': - resolution: {integrity: sha512-DKEfq877UTAmL/gOT5aFhlLNDg/EVmTSca7JQMKDGR6mjFSAcrmQf4GJNsi6ztiaqj6cTnIfoSz0hAYdnr6RJQ==} + '@expo/inline-modules@0.0.12': + resolution: {integrity: sha512-SNIZr/HWfIQPTZBwmukItxpc7ws1SgMUywYq1dnQvDknQDjJcuWAasIRFUjsK15yQ1xb4G5CP7VHtbN3V4lENg==} '@expo/json-file@10.2.0': resolution: {integrity: sha512-S6XzKe3R9GQeHiUPXc3xJjOv2VJhOEwFYf7xdC2z2cUqt3kZJ9mSO877sNQloVdnW/SUCtPY3bexlM7nwq+CAQ==} @@ -2444,16 +2456,16 @@ packages: '@expo/local-build-cache-provider@56.0.8': resolution: {integrity: sha512-UsuXwpNi57MNhzZ3be4XThc8xW6nzk3Wu37s1+2qcfZGeJcMLKDFfwO6n8YXeIiGlCsOi0Ee1rsTdgjrKt/YJQ==} - '@expo/log-box@56.0.12': - resolution: {integrity: sha512-budE6AGmJbpOJfGSOz+JVP3+FevElT82IEIg+ukQ4gZpW/dGO7QX1unFjanKdSaYgudBwJ4FCFGMwWhW/1tXVQ==} + '@expo/log-box@56.0.13': + resolution: {integrity: sha512-QWRZSpWPyjkDLVQio4R7oAzg/Av2MOt/DciFkfjr8qQ3qxGVn1Rt1oHP/80hvcWDcHFV7N6PqpyxRXw6nbxzKQ==} peerDependencies: '@expo/dom-webview': ^56.0.5 expo: '*' react: '*' react-native: '*' - '@expo/metro-config@56.0.13': - resolution: {integrity: sha512-OPyNYiex/6Ms8zT2POdIZsLhcAZYk7O+yJvpz5uG/4QRA7aiESfCy1I+0YHewMlR4P1YQeyxIrfTurs6m9xfZA==} + '@expo/metro-config@56.0.14': + resolution: {integrity: sha512-O3CIHruaTJhswPAf/nf3i8QQ3f2jl+mEwSea1eb3khuplabdy/wTQz+JvHN8VGUFyg7JKwUGU1QfO6T3JiSQqA==} peerDependencies: expo: '*' peerDependenciesMeta: @@ -2463,10 +2475,10 @@ packages: '@expo/metro-file-map@56.0.3': resolution: {integrity: sha512-5OGW3z8LgEYgMJOR7F3pC8llFLkb1fVqwAewbCl6S4Vkha8AFQMwOjT+9Wbka+V4rmpljpGqOnMhF4xZbD961w==} - '@expo/metro-runtime@56.0.13': - resolution: {integrity: sha512-aMaFa/RPYm2iQoyYOB5q8AxDmWvf4E2yFbZ6rmBIQWaIPDdixGVUlLQeV8DlDAfZ/j+pNYO7l5M+774WbgkTgg==} + '@expo/metro-runtime@56.0.15': + resolution: {integrity: sha512-WIWeVsL6kCSB57oYZdUA4MTkH7c67UFMIjdNoQzKXwxZYwBFE/xL2cGPDC3z8RWt0femzJTVxAVZUOW/hiqRzA==} peerDependencies: - '@expo/log-box': ^56.0.12 + '@expo/log-box': ^56.0.13 expo: '*' react: '*' react-dom: '*' @@ -2488,8 +2500,8 @@ packages: '@expo/plist@0.7.0': resolution: {integrity: sha512-vrpryU1GoqSIRNqRB2D3IjXDmzNYfiQpEF6AH/xknlD7eiYmEDt3mb26V7cLcedcPG8PY/1xWHdBXVQJfEAh6Q==} - '@expo/prebuild-config@56.0.14': - resolution: {integrity: sha512-JHdMqR7Mf5ApLC50ZwTL0Z86ezrHOMYwoSHcWT6Pha/+1TcC+/J+i7vjhP06wGXQ2Kvjt74p/3mKg2Pd12KjhQ==} + '@expo/prebuild-config@56.0.16': + resolution: {integrity: sha512-ce9ENfPWO4WUWUVQz0OaqL3KYZ7YofP8O35ncnn7CHCaKwQ7BqxcCGJbh+qvP1UjlWeNB3CjHPrXXJ3bnZwlJw==} '@expo/require-utils@56.1.3': resolution: {integrity: sha512-KyLeOn/zzQSvuPpV5YhB/FPKnpQytno4luN918bGdPDssLBoS3N/0UbC3W0rJAn9kSFu+XpfR81eABRVsSdfgQ==} @@ -2499,15 +2511,15 @@ packages: typescript: optional: true - '@expo/router-server@56.0.12': - resolution: {integrity: sha512-RqKV2/Z8BH/z8l0ngSpG6//5xxJPaF5dTQvSfPQ0nrvCjikGMeIvyj3B9BeLnmZZhxb3gBtXqrj3irAoiIp2aQ==} + '@expo/router-server@56.0.14': + resolution: {integrity: sha512-2UCTtZfcq1ZPgp3wk8/+sq9DvFI9UxrPr1jcEKMAF2DGAJLosnpc8GWNNg2hkjt6SHUOdFHIPxujWPYyho2y3A==} peerDependencies: - '@expo/metro-runtime': ^56.0.13 + '@expo/metro-runtime': ^56.0.15 expo: '*' - expo-constants: ^56.0.16 - expo-font: ^56.0.5 + expo-constants: ^56.0.18 + expo-font: ^56.0.6 expo-router: '*' - expo-server: ^56.0.4 + expo-server: ^56.0.5 react: '*' react-dom: '*' react-server-dom-webpack: ~19.0.1 || ~19.1.2 || ~19.2.1 @@ -2534,8 +2546,8 @@ packages: '@expo/sudo-prompt@9.3.2': resolution: {integrity: sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==} - '@expo/ui@56.0.15': - resolution: {integrity: sha512-PFZBzztQGCp2bRFP8wIOb5ntP2ORH2GdQkJMSJcDOd4NldoWMe1pFqv7PdthjNlaaTHHTTHK+RsQrz+M6z6isw==} + '@expo/ui@56.0.18': + resolution: {integrity: sha512-2XgH5obigGtXm8zlb/V3g87NSiIcBcJ1xoQOEQYPoExL1DCNsHzaIecTh1XG/f/45ardo4OZNJwpbfYJ9X3qrQ==} peerDependencies: '@babel/core': '*' expo: '*' @@ -2554,8 +2566,10 @@ packages: react-native-worklets: optional: true - '@expo/ws-tunnel@1.0.6': - resolution: {integrity: sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==} + '@expo/ws-tunnel@2.0.0': + resolution: {integrity: sha512-j+JfTRdCk820J9dU0sA2SqshQIKFOMo7ED84w9MJFcebfbNQgsLztEY/SABDkGnjatrW4xGqnUhVRxSBVyCkXw==} + peerDependencies: + ws: ^8.0.0 '@expo/xcpretty@4.4.4': resolution: {integrity: sha512-4aQzz9vgxcNXFfo/iyNgDDYfsU5XGKKxWxZopw0cVotHiW+U8IJbIxMaxsINs6bHhtkG3StKNPcOrn3eBuxKPw==} @@ -5361,6 +5375,21 @@ packages: expo-widgets: optional: true + babel-preset-expo@56.0.15: + resolution: {integrity: sha512-0MqbQoM6nBUbKvgu2xJ4VixZnUTGTq3HB2WwvOikdO4CiPxbQ+wGA25fOoHHSni5iEFW39wy6y1ookTWlq3wVw==} + peerDependencies: + '@babel/runtime': ^7.20.0 + expo: '*' + expo-widgets: ^56.0.18 + react-refresh: '>=0.14.0 <1.0.0' + peerDependenciesMeta: + '@babel/runtime': + optional: true + expo: + optional: true + expo-widgets: + optional: true + badgin@1.2.3: resolution: {integrity: sha512-NQGA7LcfCpSzIbGRbkgjgdWkjy7HI+Th5VLxTJfW5EeaAf3fnS+xWQaQOCYiny+q6QSvxqoSO04vCx+4u++EJw==} @@ -6289,26 +6318,26 @@ packages: peerDependencies: expo: '*' - expo-asset@56.0.15: - resolution: {integrity: sha512-BHGi2IAOPQTcOelkUdcz1WIknfCTRjkcpYHX1azjMwgYenrVC+J5qcqJGaC8eUOWLCRtkRJWGnmFQRYtLU1nUQ==} + expo-asset@56.0.17: + resolution: {integrity: sha512-GFN5j+8SPkyv0nfsiFHewmdB/D0tL237TsBE/gSfFOFy/J3a52py7IulcSqkA3sQE/u/UlD5BmvP5ssS4//nUg==} peerDependencies: expo: '*' react: '*' react-native: '*' - expo-auth-session@56.0.13: - resolution: {integrity: sha512-LR8Suq8BHKRFBUcAKTMmZufCcDcr0sQa8rIYit1r7kshrqAy9glIUU4aqHt8tflW/ISN0x1vU+HU8AQaackM0A==} + expo-auth-session@56.0.14: + resolution: {integrity: sha512-b6URDBKXVWBjHwypnbCPW6A3PrwYyFqzLXtTrrpTGpmDlsxk7xuz6wIA77sBNziz3hMxt11Nu70iY0fJZYT4jA==} peerDependencies: react: '*' react-native: '*' - expo-build-properties@56.0.16: - resolution: {integrity: sha512-C3avazYP2fR8efJBBmhx8yITjIRDaIe3ULPk0YfACP61QfnWC9u3LxaDNNaiIvYfZ+CLne30W+nS5F6pdgO/8g==} + expo-build-properties@56.0.19: + resolution: {integrity: sha512-InoviXcxWosNp4cC7L3SWoiY99Xr2HdgN+LYHb6mUm/BBVxy1mIMrZR+3PJ2gwDZzW6EJNDz8ioASWGHBTmzpA==} peerDependencies: expo: '*' - expo-camera@56.0.7: - resolution: {integrity: sha512-c8z+UheidFintQyP9XLEDP43aK4PS/o9+TFLW0zEOjdqkYCBgoWq6Mw/Ps62kjBeftFY7xrp5ZLITbenNvbTaw==} + expo-camera@56.0.8: + resolution: {integrity: sha512-UDOpUUMisFRmCv1XQV1MJCKGAH2CsIC1Rs6P9Bbc6JLVmbxEKAd5dK68y6cScOdWURxVfJ0PRcjYnSuc8ayyIQ==} peerDependencies: expo: '*' react: '*' @@ -6318,15 +6347,15 @@ packages: react-native-web: optional: true - expo-clipboard@56.0.3: - resolution: {integrity: sha512-8mCdhmAomm0yBIonJFjAhKUXvSkc2avdNh4+rBwoe7DSWF2AC4w3uy+pa419rvVFbTyVxOBmh83UHAbUwD6qAg==} + expo-clipboard@56.0.4: + resolution: {integrity: sha512-qb4DYlkiowHYHaUYVT2FN9nk/nI1xShXOUYsI7J9dVpQCOHcGFjCBPX1VAvEW4Ye4/Aagd6IuhOVAq/+scBOiA==} peerDependencies: expo: '*' react: '*' react-native: '*' - expo-constants@56.0.16: - resolution: {integrity: sha512-6tsiN+gmTUPp/atyA+uY9Tg8VOdXdmb4s/3TVGolfn6A/oCAraw1pcPZX5XllyD+xUguxB6eBSFAT8494hZVMA==} + expo-constants@56.0.18: + resolution: {integrity: sha512-8AMtbDGl/WVPnWlmbpGmvcdnNCy9E4PFnwdVwj600vljkMDPSxcAcjw8GVXEPk3PpZ+ngTqsrkltWyj0UKYAxw==} peerDependencies: expo: '*' react-native: '*' @@ -6336,13 +6365,13 @@ packages: peerDependencies: expo: '*' - expo-dev-client@56.0.18: - resolution: {integrity: sha512-pTfDcYTOvrs4vCgAaM+vP2OEO93oGkczgGpTAzCY7ZTIvtPhpekJURHBxfOnKvfn97IF3Hk+8J9tMozsNDj0Gw==} + expo-dev-client@56.0.20: + resolution: {integrity: sha512-KebW4r8HhIiRrPzs6ZqVhp/so8buyglAO1h4No0Ibr5C2XRnlIoGWCN4zC6rW7IsI3iKUXcofLAQV9OjoxjiwQ==} peerDependencies: expo: '*' - expo-dev-launcher@56.0.18: - resolution: {integrity: sha512-7acFJlkAbp3cMz7Uy787todMR/3A/Row2EOPD21RRoetvzJe4DTm9s7RwJ8PDtyNyued9rooD4+Q6rD8ijpTgw==} + expo-dev-launcher@56.0.20: + resolution: {integrity: sha512-cTuC3GkPl9CTwO3CKnVmEm9qoQ0WairhwvTh6qMlg+zr/QU/tdiU++uDBX67hf9+FuxQOkWGp5khFNosT+0cIg==} peerDependencies: expo: '*' react-native: '*' @@ -6352,8 +6381,8 @@ packages: peerDependencies: expo: '*' - expo-dev-menu@56.0.16: - resolution: {integrity: sha512-aVgoe+YGhrQnpwiB5BRI7G+uQnGHMUij32bBnEVdc6eJrVZCStxQlV9NeFbbXxrDhLJt6OSqbCHbLR+XToWUUA==} + expo-dev-menu@56.0.17: + resolution: {integrity: sha512-OofRkOOZnaDriSav3JDN4NP2lsLt2eOa/Ryptr5nMD62SwnFyK4R6n6PkPVaDU3LSsZqndAJHmN6inS+oziayQ==} peerDependencies: expo: '*' react-native: '*' @@ -6361,14 +6390,14 @@ packages: expo-eas-client@56.0.1: resolution: {integrity: sha512-r8h0ZIExacCrSRgY+ARfhMvFqosLHLJt1L7jyhvabfr1DN/ZDKDsYbovss2tzkpEUZGxZ3BPcB5epCwUsBBdOA==} - expo-file-system@56.0.7: - resolution: {integrity: sha512-dcKzo8ShPloM7jgfnMcJStgQebhP8owVjCkNI/aX6NMFV1CYB8bxKGMdnzJ3mXk5nfaiW+F/lSKr2UIJ02WAUA==} + expo-file-system@56.0.8: + resolution: {integrity: sha512-NrH41/8snGIBSbYicwVLB4txPdgCATd7ZYhMAGS3YJZ9GbnduhlAoV4/YCbGayjrbpE9bJb/6wegPL/zmvRMnQ==} peerDependencies: expo: '*' react-native: '*' - expo-font@56.0.5: - resolution: {integrity: sha512-WLoDu9hlEgPRKXJRR01HFLJ6Z2tFcORX/WFPRYBndmYc5kjQrFGH/j4BRaF3aBRPyYEAUXiUJybNLXkKCwEXQw==} + expo-font@56.0.7: + resolution: {integrity: sha512-hpU/vRwPzsby9lPGkA4blDqLIIXYzoWnCZHr6PxvcWbY/uPObAiyhh6q+e0WYsB65SthK+PLH95jEnVag7fwEg==} peerDependencies: expo: '*' react: '*' @@ -6391,8 +6420,8 @@ packages: peerDependencies: expo: '*' - expo-image-picker@56.0.15: - resolution: {integrity: sha512-7zmrUcqiNKwGGGL114gC+1aaquhmMbaXCNSHjUcoINdBzvIEVMJDm+DNCxkpftEYeb17X3shqInI4lCgJNVO5g==} + expo-image-picker@56.0.18: + resolution: {integrity: sha512-sCjQ8M27bhGUv2vUavIE+uWdYo79b2D7Q5h9B66BSDZ+Rd8YyLVSf7vYGfIzQ7nMVoENZ6c4xo/JiDkEeQ9iTg==} peerDependencies: expo: '*' @@ -6405,8 +6434,8 @@ packages: expo: '*' react: '*' - expo-linking@56.0.13: - resolution: {integrity: sha512-38YrpTh6xdiDxmYSDIUffDqev1hIcEggw2fZ3IZhNp2DVLF1xvqsbO6hJD1fuBKN8P34B3Ggc9Yy26fkqdfCOA==} + expo-linking@56.0.14: + resolution: {integrity: sha512-IvVQHWC+Cj4fK5qD3iEVYqpU2a4rLW0IpAAlGJ4MH+H1fyZiHh3eN6qg2WmoclOEPfYATSuEa+dQT6wfgVpXlQ==} peerDependencies: react: '*' react-native: '*' @@ -6416,12 +6445,12 @@ packages: peerDependencies: expo: '*' - expo-modules-autolinking@56.0.14: - resolution: {integrity: sha512-9ugtZkheNPYDkW4DZopY1rH2BCbUICaafUEPxRgbLDR5UNRF5K3cdHMIMEt8pxZPq2+eX4wCm+6pbSvdY/DPHg==} + expo-modules-autolinking@56.0.16: + resolution: {integrity: sha512-9JnL4N46P8ubDpDIfWolDn7nxU2j1rY67xY/dNVuyH0m+HG+r/JI16VYtjIf4COpZtEuFo4D3h3MBeFzGucMnw==} hasBin: true - expo-modules-core@56.0.14: - resolution: {integrity: sha512-dl1TlYRm1k7xk9QeAyDoMfFE2p6rNyzHUcH5ArcGwUzO8YKku+Z2tQ8+kG7zLe3OhfMoJcFR/czrFy7vGSVI6w==} + expo-modules-core@56.0.17: + resolution: {integrity: sha512-5J8whnT7Ccp+BrFClLmpF76omBqn95VZExroTm01Dgjm4vpty1Rb7U3we+ZUceNHtRd07Lw30u7FNfDgIhEbRQ==} peerDependencies: react: '*' react-native: '*' @@ -6430,8 +6459,8 @@ packages: react-native-worklets: optional: true - expo-modules-jsi@56.0.7: - resolution: {integrity: sha512-iBAj4Xeh/8HT201VVxFlmf+VBfmtQV1ZUoJdLQQENm0+j9gnD2QswZLJyNo3CmNNXl46esJeLR5lpGpYZts/zA==} + expo-modules-jsi@56.0.10: + resolution: {integrity: sha512-fHZcFpYO/o62GYa6fJyAQJZcAShzhoN0iMMDzbr7vD3ewET6e1vAlTonbEakN9F0VHEgBFJ4NREy87uwVcpCuA==} peerDependencies: react-native: '*' @@ -6441,8 +6470,8 @@ packages: expo: '*' react: '*' - expo-notifications@56.0.15: - resolution: {integrity: sha512-F+OasAePiVnHaPNKI9JAYV8fg8bdBwo7Mh9R3ydBp8S21fRQyxKOSgJvj8fX/HoPFFIC6V2B+y1LJbG5Ovh/Fg==} + expo-notifications@56.0.18: + resolution: {integrity: sha512-HHnrwyCLC5srFojcHYS2KskbNroy9o2fwPKdyhjrdjjrBu4sNRKm4LepcuZjDy98cZKEm89WIPW8O45vut8Rgw==} peerDependencies: expo: '*' react: '*' @@ -6455,15 +6484,15 @@ packages: react: '*' react-native: '*' - expo-router@56.2.8: - resolution: {integrity: sha512-l387I/ddPY/5SS+Rfpp1SrRV9gBKevxtPuZod7igMjR6L674QrxEwGiAILRq6AKCSbrP2I0ufKj7e5xz8JqA4Q==} + expo-router@56.2.11: + resolution: {integrity: sha512-08DBTrKv3QanOc9u1JNxSEChW9c/qNFbQ0dO28OLvufWWfdSRkSdHmh365D2FgoZg1qaOzZPCDuL3tM6nGSfkQ==} peerDependencies: - '@expo/log-box': ^56.0.12 - '@expo/metro-runtime': ^56.0.13 + '@expo/log-box': ^56.0.13 + '@expo/metro-runtime': ^56.0.15 '@testing-library/react-native': '>= 13.2.0' expo: '*' - expo-constants: ^56.0.16 - expo-linking: ^56.0.13 + expo-constants: ^56.0.18 + expo-linking: ^56.0.14 react: '*' react-dom: '*' react-native: '*' @@ -6492,8 +6521,8 @@ packages: peerDependencies: expo: '*' - expo-server@56.0.4: - resolution: {integrity: sha512-4dJ57KuAwDl7eQGD6aG9kTzBIftWAfHH1+6Zxy7NcPCBrKYis3/H5enGUz1asH8HHhONXfJ5BdJqfEWAEAgWxA==} + expo-server@56.0.5: + resolution: {integrity: sha512-SmM2p2g3Jrktpiazcst+OxhjSzOHXKAY4BPURHYHXvApzzoybMmrNF4IEZ8DKZ145BhSe4ydAmlEFCRTsdtgUQ==} engines: {node: '>=20.16.0'} expo-splash-screen@56.0.10: @@ -6504,8 +6533,8 @@ packages: expo-structured-headers@56.0.0: resolution: {integrity: sha512-Yv4x+SQxNnMQm4nu8NFfzx197YaDhdYH2N0u7tGErwWTmH9Tm1SAhqo7bLbBWLC9kf7W+kdzTshLU9rTiCXWGw==} - expo-symbols@56.0.5: - resolution: {integrity: sha512-RIukH0Xo80C7RU8qreipL2SPy2Py+Km8JFPbCmbPQpHkM3DW9Znlmg6VfhzbtUOlO5EuNSF0lAJ3l2VJi6qYrw==} + expo-symbols@56.0.6: + resolution: {integrity: sha512-BrA81DjcNafdj7gXVhdrExb9LtUiSVyOf/NavyMmDAHgHMY1GqeR5cnn1PSAZeYKnSgQhee/H89XUpAxtog5hg==} peerDependencies: expo: '*' expo-font: '*' @@ -6517,8 +6546,8 @@ packages: peerDependencies: expo: '*' - expo-updates@56.0.17: - resolution: {integrity: sha512-4005IRQQ7Vpwb01X3NSG0SwY61HTJzT2pZ0OrkeZ6guQE1WKiRfUKisekRgYXTI8sPbxSHUc9aOpIx6yC49XZQ==} + expo-updates@56.0.19: + resolution: {integrity: sha512-tTSPYO5h8wDA6a+wQ2v/SRdnOdz29x0npGHCv+4Ev31Fz5r05Ii1Wgfh3BlTXNz8mikMReDsZCf6YN71YeQKpw==} hasBin: true peerDependencies: expo: '*' @@ -6535,15 +6564,15 @@ packages: expo: '*' react-native: '*' - expo-widgets@56.0.16: - resolution: {integrity: sha512-EmWVvskad2XlgCk6j9wz/OqSAeaGxjD6x6Wji99TVjjFd+sTD6W3YJbz3QW/BKTVRcll95m6XjJDCa6B9t9f9A==} + expo-widgets@56.0.19: + resolution: {integrity: sha512-D2RWectoEalVdGwXyE2LX3L9T6q6jSKh8jjvk1K3JSE1qOcCVZk+TJtvBUveTq8OoLblkeFMXqQ2fHG+kg655w==} peerDependencies: expo: '*' react: '*' react-native: '*' - expo@56.0.8: - resolution: {integrity: sha512-GzQi5450yrCk5JRSlm0epsmtURBErh0wS77uWLZImFdnPICuX912MaRWooR+Q1Sw/7aQjp9F+KXH+dvrqGxpeQ==} + expo@56.0.12: + resolution: {integrity: sha512-FxgdI/Yqva6iJOThZIHfvxlKPxs4EC4uScUnEswwSArR/Fj9k430O13R590LcOQTsdNsjIs+GBHwjfoAY6vmAQ==} hasBin: true peerDependencies: '@expo/dom-webview': '*' @@ -8630,8 +8659,8 @@ packages: react: '*' react-native: '*' - react-native-keyboard-controller@1.21.7: - resolution: {integrity: sha512-gs+8nI8HYnRdDt4NWbk1iVuS6kDLf2taJvp+h/TjM1FBdtnQmlYLJ6buNiUqSnkIH4OFEAxdNr3/GOOYdLfkUQ==} + react-native-keyboard-controller@1.21.6: + resolution: {integrity: sha512-nAXCmar/W8Gn4iQV7O5fAVuTh57JszCsqTS+cfR95WFOLR/AfbwfPz/+sWyz/q2SOIe2VpyQzq6hzYiwErhqqw==} peerDependencies: react: '*' react-native: '*' @@ -9199,6 +9228,9 @@ packages: standard-as-callback@2.1.0: resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==} + standard-navigation@0.0.5: + resolution: {integrity: sha512-YAmzwAiiQVocZxO/VGPFiQHcu5pKiz09QIGC0MK6aRMoa3E0QkoTQgcqJr7ZZ3OMiNhu4DkaGElFI5htjOIDbw==} + standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} @@ -11216,23 +11248,23 @@ snapshots: electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/expo@3.5.3-snapshot.v20260622234151(expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.16)(expo-crypto@56.0.4(expo@56.0.8))(expo-secure-store@56.0.4(expo@56.0.8))(expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@clerk/expo@3.5.3-snapshot.v20260622234151(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: '@clerk/clerk-js': 6.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/react': 6.11.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) base-64: 1.0.0 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) tslib: 2.8.1 optionalDependencies: - expo-auth-session: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-crypto: 56.0.4(expo@56.0.8) - expo-secure-store: 56.0.4(expo@56.0.8) - expo-web-browser: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-crypto: 56.0.4(expo@56.0.12) + expo-secure-store: 56.0.4(expo@56.0.12) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react-dom: 19.2.3(react@19.2.3) '@clerk/react@6.11.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': @@ -11815,29 +11847,29 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.38': {} - '@expo/cli@56.1.13(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': + '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 56.0.9(typescript@6.0.3) - '@expo/config-plugins': 56.0.8(typescript@6.0.3) + '@expo/config-plugins': 56.0.9(typescript@6.0.3) '@expo/devcert': 1.2.1 '@expo/env': 2.3.0 '@expo/image-utils': 0.10.1(typescript@6.0.3) - '@expo/inline-modules': 0.0.10(typescript@6.0.3) + '@expo/inline-modules': 0.0.12(typescript@6.0.3) '@expo/json-file': 10.2.0 - '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@expo/metro-config': 56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) '@expo/metro-file-map': 56.0.3 '@expo/osascript': 2.6.0 '@expo/package-manager': 1.12.1 '@expo/plist': 0.7.0 - '@expo/prebuild-config': 56.0.14(typescript@6.0.3) + '@expo/prebuild-config': 56.0.16(typescript@6.0.3) '@expo/require-utils': 56.1.3(typescript@6.0.3) - '@expo/router-server': 56.0.12(@expo/metro-runtime@56.0.13)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo-server@56.0.4)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@expo/router-server': 56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@expo/schema-utils': 56.0.1 '@expo/spawn-async': 1.8.0 - '@expo/ws-tunnel': 1.0.6 + '@expo/ws-tunnel': 2.0.0(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6)) '@expo/xcpretty': 4.4.4 '@react-native/dev-middleware': 0.85.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) accepts: 1.3.8 @@ -11850,8 +11882,8 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.4 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-server: 56.0.4 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-server: 56.0.5 fetch-nodeshim: 0.4.10 getenv: 2.0.0 glob: 13.0.6 @@ -11876,7 +11908,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310) + expo-router: 56.2.11(53652d9673d8ec829763e55ad4f6fd4e) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -11914,11 +11946,32 @@ snapshots: - supports-color - typescript + '@expo/config-plugins@56.0.9(typescript@6.0.3)': + dependencies: + '@expo/config-types': 56.0.6 + '@expo/json-file': 10.2.0 + '@expo/plist': 0.7.0 + '@expo/require-utils': 56.1.3(typescript@6.0.3) + '@expo/sdk-runtime-versions': 1.0.0 + chalk: 4.1.2 + debug: 4.4.3 + getenv: 2.0.0 + glob: 13.0.6 + semver: 7.8.5 + slugify: 1.6.9 + xcode: 3.0.1 + xml2js: 0.6.0 + transitivePeerDependencies: + - supports-color + - typescript + '@expo/config-types@56.0.5': {} + '@expo/config-types@56.0.6': {} + '@expo/config@56.0.9(typescript@6.0.3)': dependencies: - '@expo/config-plugins': 56.0.8(typescript@6.0.3) + '@expo/config-plugins': 56.0.9(typescript@6.0.3) '@expo/config-types': 56.0.5 '@expo/json-file': 10.2.0 '@expo/require-utils': 56.1.3(typescript@6.0.3) @@ -11946,9 +11999,9 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@expo/dom-webview@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) @@ -11960,9 +12013,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/expo-modules-macros-plugin@0.0.9': {} + '@expo/expo-modules-macros-plugin@0.2.2': {} - '@expo/fingerprint@0.19.3': + '@expo/fingerprint@0.19.4': dependencies: '@expo/env': 2.3.0 '@expo/spawn-async': 1.8.0 @@ -11991,9 +12044,9 @@ snapshots: - supports-color - typescript - '@expo/inline-modules@0.0.10(typescript@6.0.3)': + '@expo/inline-modules@0.0.12(typescript@6.0.3)': dependencies: - '@expo/config-plugins': 56.0.8(typescript@6.0.3) + '@expo/config-plugins': 56.0.9(typescript@6.0.3) transitivePeerDependencies: - supports-color - typescript @@ -12011,16 +12064,16 @@ snapshots: - supports-color - typescript - '@expo/log-box@56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@expo/dom-webview': 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) anser: 1.4.10 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) stacktrace-parser: 0.1.11 - '@expo/metro-config@56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6)': + '@expo/metro-config@56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: '@babel/code-frame': 7.29.7 '@babel/core': 7.29.7 @@ -12042,12 +12095,11 @@ snapshots: hermes-parser: 0.33.3 jsc-safe-url: 0.2.4 lightningcss: 1.32.0 - msgpackr: 2.0.2 picomatch: 4.0.4 postcss: 8.5.15 resolve-from: 5.0.0 optionalDependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) transitivePeerDependencies: - bufferutil - supports-color @@ -12065,11 +12117,11 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) anser: 1.4.10 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) pretty-format: 29.7.0 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) @@ -12118,16 +12170,16 @@ snapshots: base64-js: 1.5.1 xmlbuilder: 15.1.1 - '@expo/prebuild-config@56.0.14(typescript@6.0.3)': + '@expo/prebuild-config@56.0.16(typescript@6.0.3)': dependencies: '@expo/config': 56.0.9(typescript@6.0.3) - '@expo/config-plugins': 56.0.8(typescript@6.0.3) - '@expo/config-types': 56.0.5 + '@expo/config-plugins': 56.0.9(typescript@6.0.3) + '@expo/config-types': 56.0.6 '@expo/image-utils': 0.10.1(typescript@6.0.3) '@expo/json-file': 10.2.0 '@react-native/normalize-colors': 0.85.3 debug: 4.4.3 - expo-modules-autolinking: 56.0.14(typescript@6.0.3) + expo-modules-autolinking: 56.0.16(typescript@6.0.3) resolve-from: 5.0.0 semver: 7.8.5 transitivePeerDependencies: @@ -12144,17 +12196,17 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/router-server@56.0.12(@expo/metro-runtime@56.0.13)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo-server@56.0.4)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@expo/router-server@56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: debug: 4.4.3 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-server: 56.0.4 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-server: 56.0.5 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-router: 56.2.11(53652d9673d8ec829763e55ad4f6fd4e) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -12169,9 +12221,9 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ui@56.0.15(961c4aa6f32829b318e3c87ef20ad401)': + '@expo/ui@56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0)': dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 @@ -12185,7 +12237,9 @@ snapshots: - '@types/react' - '@types/react-dom' - '@expo/ws-tunnel@1.0.6': {} + '@expo/ws-tunnel@2.0.0(ws@8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6))': + dependencies: + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@expo/xcpretty@4.4.4': dependencies: @@ -13800,12 +13854,12 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(f0ff241d48c23db461fff5d47e450578)': + '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe)': dependencies: - expo-asset: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) - expo-clipboard: 56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-haptics: 56.0.3(expo@56.0.8) - expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-haptics: 56.0.3(expo@56.0.12) + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -14876,7 +14930,7 @@ snapshots: transitivePeerDependencies: - '@babel/core' - babel-preset-expo@56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.16)(expo@56.0.8)(react-refresh@0.14.2): + babel-preset-expo@56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2): dependencies: '@babel/generator': 7.29.7 '@babel/helper-module-imports': 7.29.7 @@ -14923,8 +14977,61 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-widgets: 56.0.16(961c4aa6f32829b318e3c87ef20ad401) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) + transitivePeerDependencies: + - '@babel/core' + - supports-color + + babel-preset-expo@56.0.15(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2): + dependencies: + '@babel/generator': 7.29.7 + '@babel/helper-module-imports': 7.29.7 + '@babel/plugin-proposal-decorators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-proposal-export-default-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-dynamic-import': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-export-default-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-syntax-nullish-coalescing-operator': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-syntax-optional-chaining': 7.8.3(@babel/core@7.29.7) + '@babel/plugin-transform-async-generator-functions': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-async-to-generator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-block-scoping': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-properties': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-class-static-block': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-classes': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-destructuring': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-export-namespace-from': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-flow-strip-types': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-for-of': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-logical-assignment-operators': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-named-capturing-groups-regex': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-nullish-coalescing-operator': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-object-rest-spread': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-catch-binding': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-optional-chaining': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-parameters': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-methods': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-private-property-in-object': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-display-name': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx': 7.28.6(@babel/core@7.29.7) + '@babel/plugin-transform-react-jsx-development': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-react-pure-annotations': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-runtime': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.29.7) + '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) + '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) + '@react-native/babel-plugin-codegen': 0.85.3(@babel/core@7.29.7) + babel-plugin-react-compiler: 1.0.0 + babel-plugin-react-native-web: 0.21.2 + babel-plugin-syntax-hermes-parser: 0.33.3 + babel-plugin-transform-flow-enums: 0.0.2(@babel/core@7.29.7) + debug: 4.4.3 + react-refresh: 0.14.2 + optionalDependencies: + '@babel/runtime': 7.29.7 + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -15819,28 +15926,28 @@ snapshots: expect-type@1.4.0: {} - expo-application@56.0.3(expo@56.0.8): + expo-application@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-asset@56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-auth-session@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo-application: 56.0.3(expo@56.0.8) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-crypto: 56.0.4(expo@56.0.8) - expo-linking: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-web-browser: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-application: 56.0.3(expo@56.0.12) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-crypto: 56.0.4(expo@56.0.12) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) @@ -15848,112 +15955,112 @@ snapshots: - expo - supports-color - expo-build-properties@56.0.16(expo@56.0.8): + expo-build-properties@56.0.19(expo@56.0.12): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) resolve-from: 5.0.0 semver: 7.8.5 - expo-camera@56.0.7(@types/emscripten@1.41.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: barcode-detector: 3.2.0(@types/emscripten@1.41.5) - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@types/emscripten' - expo-clipboard@56.0.3(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-constants@56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/env': 2.3.0 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - expo-crypto@56.0.4(expo@56.0.8): + expo-crypto@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-dev-launcher: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-dev-menu: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-dev-menu-interface: 56.0.1(expo@56.0.8) - expo-manifests: 56.0.4(expo@56.0.8) - expo-updates-interface: 56.0.2(expo@56.0.8) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-menu-interface: 56.0.1(expo@56.0.12) + expo-manifests: 56.0.4(expo@56.0.12) + expo-updates-interface: 56.0.2(expo@56.0.12) transitivePeerDependencies: - react-native - expo-dev-launcher@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-dev-menu: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-manifests: 56.0.4(expo@56.0.8) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-manifests: 56.0.4(expo@56.0.12) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-dev-menu-interface@56.0.1(expo@56.0.8): + expo-dev-menu-interface@56.0.1(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-menu@56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-dev-menu-interface: 56.0.1(expo@56.0.8) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-menu-interface: 56.0.1(expo@56.0.12) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-eas-client@56.0.1: {} - expo-file-system@56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-font@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) fontfaceobserver: 2.3.0 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-glass-effect@56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-haptics@56.0.3(expo@56.0.8): + expo-haptics@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-image-loader@56.0.3(expo@56.0.8): + expo-image-loader@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-image-picker@56.0.15(expo@56.0.8): + expo-image-picker@56.0.18(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-image-loader: 56.0.3(expo@56.0.8) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-image-loader: 56.0.3(expo@56.0.12) expo-json-utils@56.0.0: {} - expo-keep-awake@56.0.3(expo@56.0.8)(react@19.2.3): + expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - expo-linking@56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) @@ -15961,12 +16068,12 @@ snapshots: - expo - supports-color - expo-manifests@56.0.4(expo@56.0.8): + expo-manifests@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-json-utils: 56.0.0 - expo-modules-autolinking@56.0.14(typescript@6.0.3): + expo-modules-autolinking@56.0.16(typescript@6.0.3): dependencies: '@expo/require-utils': 56.1.3(typescript@6.0.3) '@expo/spawn-async': 1.8.0 @@ -15976,51 +16083,51 @@ snapshots: - supports-color - typescript - expo-modules-core@56.0.14(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - '@expo/expo-modules-macros-plugin': 0.0.9 - expo-modules-jsi: 56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + '@expo/expo-modules-macros-plugin': 0.2.2 + expo-modules-jsi: 56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) optionalDependencies: react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-modules-jsi@56.0.7(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-network@56.0.5(expo@56.0.8)(react@19.2.3): + expo-network@56.0.5(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - expo-notifications@56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-application: 56.0.3(expo@56.0.8) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-application: 56.0.3(expo@56.0.12) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-paste-input@0.1.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.8(5bfdf39b8f760e4dc2d5c3acffc97310): + expo-router@56.2.11(53652d9673d8ec829763e55ad4f6fd4e): dependencies: - '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/schema-utils': 56.0.1 - '@expo/ui': 56.0.15(961c4aa6f32829b318e3c87ef20ad401) + '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16030,12 +16137,12 @@ snapshots: color: 4.2.3 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-glass-effect: 56.0.4(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-linking: 56.0.13(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-server: 56.0.4 - expo-symbols: 56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-server: 56.0.5 + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.12 @@ -16050,6 +16157,7 @@ snapshots: server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 + standard-navigation: 0.0.5 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: react-dom: 19.2.3(react@19.2.3) @@ -16064,17 +16172,17 @@ snapshots: - react-native-worklets - supports-color - expo-secure-store@56.0.4(expo@56.0.8): + expo-secure-store@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-server@56.0.4: {} + expo-server@56.0.5: {} - expo-splash-screen@56.0.10(expo@56.0.8)(typescript@6.0.3): + expo-splash-screen@56.0.10(expo@56.0.12)(typescript@6.0.3): dependencies: '@expo/config-plugins': 56.0.8(typescript@6.0.3) '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) xml2js: 0.6.0 transitivePeerDependencies: - supports-color @@ -16082,20 +16190,20 @@ snapshots: expo-structured-headers@56.0.0: {} - expo-symbols@56.0.5(expo-font@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.38 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) - expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 - expo-updates-interface@56.0.2(expo@56.0.8): + expo-updates-interface@56.0.2(expo@56.0.12): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-updates@56.0.17(expo-dev-client@56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.7.0 @@ -16103,11 +16211,11 @@ snapshots: arg: 4.1.3 chalk: 4.1.2 debug: 4.4.3 - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-eas-client: 56.0.1 - expo-manifests: 56.0.4(expo@56.0.8) + expo-manifests: 56.0.4(expo@56.0.12) expo-structured-headers: 56.0.0 - expo-updates-interface: 56.0.2(expo@56.0.8) + expo-updates-interface: 56.0.2(expo@56.0.12) getenv: 2.0.0 glob: 13.0.6 ignore: 5.3.2 @@ -16116,20 +16224,20 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 56.0.18(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) transitivePeerDependencies: - supports-color - expo-web-browser@56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@56.0.16(961c4aa6f32829b318e3c87ef20ad401): + expo-widgets@56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0): dependencies: '@expo/plist': 0.7.0 - '@expo/ui': 56.0.15(961c4aa6f32829b318e3c87ef20ad401) - expo: 56.0.8(63f7aade424ad9e7b1154b679fa2a14d) + '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: @@ -16140,35 +16248,35 @@ snapshots: - react-native-reanimated - react-native-worklets - expo@56.0.8(63f7aade424ad9e7b1154b679fa2a14d): + expo@56.0.12(8895228379997a2a064f9644cda56ed0): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 56.1.13(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.13)(bufferutil@4.1.0)(expo-constants@56.0.16)(expo-font@56.0.5)(expo-router@56.2.8)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) '@expo/config': 56.0.9(typescript@6.0.3) - '@expo/config-plugins': 56.0.8(typescript@6.0.3) + '@expo/config-plugins': 56.0.9(typescript@6.0.3) '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/fingerprint': 0.19.3 + '@expo/fingerprint': 0.19.4 '@expo/local-build-cache-provider': 56.0.8(typescript@6.0.3) - '@expo/log-box': 56.0.12(@expo/dom-webview@56.0.5)(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) - '@expo/metro-config': 56.0.13(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.8)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) '@ungap/structured-clone': 1.3.1 - babel-preset-expo: 56.0.14(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.16)(expo@56.0.8)(react-refresh@0.14.2) - expo-asset: 56.0.15(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) - expo-constants: 56.0.16(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-file-system: 56.0.7(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-font: 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-keep-awake: 56.0.3(expo@56.0.8)(react@19.2.3) - expo-modules-autolinking: 56.0.14(typescript@6.0.3) - expo-modules-core: 56.0.14(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + babel-preset-expo: 56.0.15(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-file-system: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-keep-awake: 56.0.3(expo@56.0.12)(react@19.2.3) + expo-modules-autolinking: 56.0.16(typescript@6.0.3) + expo-modules-core: 56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) pretty-format: 29.7.0 react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 56.0.5(expo@56.0.8)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/metro-runtime': 56.0.13(@expo/log-box@56.0.12)(expo@56.0.8)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: @@ -18619,7 +18727,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-keyboard-controller@1.21.7(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) @@ -19416,6 +19524,8 @@ snapshots: standard-as-callback@2.1.0: {} + standard-navigation@0.0.5: {} + standardwebhooks@1.0.0: dependencies: '@stablelib/base64': 1.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 446139ed6da..34867016252 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -71,7 +71,7 @@ overrides: # @effect/vitest is patched to use vite-plus/test; do not auto-install its # upstream Vitest peer and create a second Vite/Vitest toolchain. "@effect/vitest>vitest": "-" - "@expo/metro-config": 56.0.13 + "@expo/metro-config": 56.0.14 "@pierre/diffs>@shikijs/transformers": ^4.2.0 "@types/node": "catalog:" effect: "catalog:" @@ -91,10 +91,11 @@ packageExtensions: patchedDependencies: "@effect/vitest@4.0.0-beta.78": patches/@effect__vitest@4.0.0-beta.78.patch - "@expo/metro-config@56.0.13": patches/@expo%2Fmetro-config@56.0.13.patch + "@expo/metro-config@56.0.14": patches/@expo%2Fmetro-config@56.0.14.patch "@ff-labs/fff-node@0.9.4": patches/@ff-labs__fff-node@0.9.4.patch "@pierre/diffs@1.3.0-beta.5": patches/@pierre%2Fdiffs@1.3.0-beta.5.patch effect@4.0.0-beta.78: patches/effect@4.0.0-beta.78.patch + expo-modules-jsi@56.0.10: patches/expo-modules-jsi@56.0.10.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch peerDependencyRules: From 32e1983e610c150c47b80d87d6bc7d68196dfefc Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 17:55:40 -0700 Subject: [PATCH 07/71] Bump Clerk and Expo dependencies - Update Clerk packages across web, desktop, relay, and mobile - Refresh lockfile entries for related Expo and native packages --- pnpm-lock.yaml | 650 ++++++++++++++++++++++---------------------- pnpm-workspace.yaml | 14 +- 2 files changed, 331 insertions(+), 333 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9de9e9ac3ff..8c2f25f952c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,13 +35,13 @@ catalogs: version: 0.2.1 overrides: - '@clerk/backend': 3.8.3-snapshot.v20260622234151 - '@clerk/clerk-js': 6.21.0-snapshot.v20260622234151 - '@clerk/electron': 0.0.2-snapshot.v20260622234151 - '@clerk/electron-passkeys': 0.0.2-snapshot.v20260622234151 - '@clerk/expo': 3.5.3-snapshot.v20260622234151 - '@clerk/react': 6.11.0-snapshot.v20260622234151 - '@clerk/shared': 4.21.0-snapshot.v20260622234151 + '@clerk/backend': 3.8.4 + '@clerk/clerk-js': 6.22.0 + '@clerk/electron': 0.0.5 + '@clerk/electron-passkeys': 0.0.3 + '@clerk/expo': 3.6.2 + '@clerk/react': 6.11.1 + '@clerk/shared': 4.22.0 '@clerk/clerk-js>@base-org/account': '-' '@clerk/clerk-js>@coinbase/wallet-sdk': '-' '@clerk/clerk-js>@solana/wallet-adapter-base': '-' @@ -114,11 +114,11 @@ importers: apps/desktop: dependencies: '@clerk/electron': - specifier: 0.0.2-snapshot.v20260622234151 - version: 0.0.2-snapshot.v20260622234151(@clerk/electron-passkeys@0.0.2-snapshot.v20260622234151)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.5 + version: 0.0.5(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron-passkeys': - specifier: 0.0.2-snapshot.v20260622234151 - version: 0.0.2-snapshot.v20260622234151 + specifier: 0.0.3 + version: 0.0.3 '@effect/platform-node': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(bufferutil@4.1.0)(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(ioredis@5.11.0)(utf-8-validate@6.0.6) @@ -195,10 +195,10 @@ importers: dependencies: '@callstack/liquid-glass': specifier: ^0.7.1 - version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@clerk/expo': - specifier: 3.5.3-snapshot.v20260622234151 - version: 3.5.3-snapshot.v20260622234151(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + specifier: 3.6.2 + version: 3.6.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@effect/atom-react': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(react@19.2.3)(scheduler@0.27.0) @@ -207,13 +207,13 @@ importers: version: 0.4.2 '@expo/metro-runtime': specifier: ~56.0.15 - version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/ui': specifier: ~56.0.18 - version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) + version: 56.0.18(19413efe5eaad64848598eedfe3a0fd3) '@legendapp/list': specifier: 3.2.0 - version: 3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@noble/curves': specifier: 'catalog:' version: 1.9.1 @@ -225,7 +225,7 @@ importers: version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-menu/menu': specifier: ^2.0.0 - version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -246,7 +246,7 @@ importers: version: link:../../packages/contracts '@t3tools/mobile-markdown-text': specifier: file:./modules/t3-markdown-text - version: file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe) + version: file:apps/mobile/modules/t3-markdown-text(a6e4bd1e9376530ea93dbb7d8a53d32d) '@t3tools/mobile-review-diff-native': specifier: file:./modules/t3-review-diff version: file:apps/mobile/modules/t3-review-diff @@ -267,40 +267,40 @@ importers: version: 4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5) expo: specifier: ~56.0.12 - version: 56.0.12(8895228379997a2a064f9644cda56ed0) + version: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-asset: specifier: ~56.0.17 - version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-auth-session: specifier: ~56.0.14 - version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-build-properties: specifier: ~56.0.19 version: 56.0.19(expo@56.0.12) expo-camera: specifier: ~56.0.8 - version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-clipboard: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-constants: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) expo-dev-client: specifier: ~56.0.20 - version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-file-system: specifier: ~56.0.8 - version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-font: specifier: ~56.0.7 - version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-glass-effect: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: specifier: ~56.0.3 version: 56.0.3(expo@56.0.12) @@ -309,19 +309,19 @@ importers: version: 56.0.18(expo@56.0.12) expo-linking: specifier: ~56.0.14 - version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-network: specifier: ~56.0.5 version: 56.0.5(expo@56.0.12)(react@19.2.3) expo-notifications: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-paste-input: specifier: ^0.1.15 - version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(53652d9673d8ec829763e55ad4f6fd4e) + version: 56.2.11(3dd1c34e09949502ba8228b7f60c3e89) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -330,16 +330,16 @@ importers: version: 56.0.10(expo@56.0.12)(typescript@6.0.3) expo-symbols: specifier: ~56.0.6 - version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-updates: specifier: ~56.0.19 - version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-web-browser: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: specifier: ~56.0.19 - version: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) + version: 56.0.19(19413efe5eaad64848598eedfe3a0fd3) punycode: specifier: ^2.3.1 version: 2.3.1 @@ -351,43 +351,43 @@ importers: version: 19.2.3(react@19.2.3) react-native: specifier: 0.85.3 - version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-gesture-handler: specifier: ~2.31.1 - version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-image-viewing: specifier: ^0.2.2 - version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.6 - version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown: specifier: ^0.5.0 - version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-modules: specifier: 0.35.9 - version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-reanimated: specifier: 4.3.1 - version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-safe-area-context: specifier: ~5.7.0 - version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 - version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-svg: specifier: 15.15.4 - version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-webview: specifier: ^13.16.1 - version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-worklets: specifier: 0.8.3 - version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) shiki: specifier: 4.2.0 version: 4.2.0 @@ -396,7 +396,7 @@ importers: version: 3.6.0 uniwind: specifier: ^1.6.2 - version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) + version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) devDependencies: '@effect/vitest': specifier: 4.0.0-beta.78 @@ -487,11 +487,11 @@ importers: specifier: ^1.4.1 version: 1.5.0(@types/react@19.2.16)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/electron': - specifier: 0.0.2-snapshot.v20260622234151 - version: 0.0.2-snapshot.v20260622234151(@clerk/electron-passkeys@0.0.2-snapshot.v20260622234151)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 0.0.5 + version: 0.0.5(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@clerk/react': - specifier: 6.11.0-snapshot.v20260622234151 - version: 6.11.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 6.11.1 + version: 6.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@dnd-kit/core': specifier: ^6.3.1 version: 6.3.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) @@ -641,8 +641,8 @@ importers: infra/relay: dependencies: '@clerk/backend': - specifier: 3.8.3-snapshot.v20260622234151 - version: 3.8.3-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + specifier: 3.8.4 + version: 3.8.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@effect/sql-pg': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5)) @@ -1644,43 +1644,43 @@ packages: resolution: {integrity: sha512-EYlRokl8szrP9Z25qT5aepMdBjzBvHF9ZEhzIiUBc9guz/T31EqRgvD0QSgZcpE93xiwrr+OkB4nz0BZyF6fSA==} engines: {node: '>= 20.12.0'} - '@clerk/backend@3.8.3-snapshot.v20260622234151': - resolution: {integrity: sha512-B5goX0n/5pibc4dMQOfMmn4mPq7eqtrbNV20tOqO9qMPzFA+TKAj9SnB0oJvFsZAXKO6+/n16uYPbqeM2PN6CA==} + '@clerk/backend@3.8.4': + resolution: {integrity: sha512-3G+kEu8kalqQokJ/n0IynhBh2i6TtiilRzuAg5UWMburkK658/elrG0Uqsapd5yKhmkNuvizHwOPWwKAMxP2EQ==} engines: {node: '>=20.9.0'} - '@clerk/clerk-js@6.21.0-snapshot.v20260622234151': - resolution: {integrity: sha512-mRNn6H8GbeEkcCIzZ03WZ9c1Uy8znf70okYmmeJKzK72gsdwnrxEfbu3DYE/5yRbX6lvL7ugWaNTT3DvPEbBzg==} + '@clerk/clerk-js@6.22.0': + resolution: {integrity: sha512-4kN1J1GHUv7utZn1UunfOi10ZVyHOVY+d7tMGXoBxrckpb4vT/rFYep8uf8Xc6Q5QNEmi1/J0DMxZqnS3xy+ww==} engines: {node: '>=20.9.0'} - '@clerk/electron-passkeys-darwin-arm64@0.0.2-snapshot.v20260622234151': - resolution: {integrity: sha512-NZjIVGitAf0yyQvs1WXIAYOle9RjGsExpfwje2U20POTnNueFy56M0g39UTFQXJ42saTZ0zauLD8sd0cHqpOjA==} + '@clerk/electron-passkeys-darwin-arm64@0.0.3': + resolution: {integrity: sha512-oAlrd+GLqP0oREP3hNRc6yNZhXGaQgJss9iCWRQqXjpw1yZD1ZU+L+E2Gfg84vMj/NSUhxbU0Djy0RXhmeUwgg==} cpu: [arm64] os: [darwin] - '@clerk/electron-passkeys-darwin-x64@0.0.2-snapshot.v20260622234151': - resolution: {integrity: sha512-gGAwinVoIxa9lefCJjhf5D6oA8uq1sXQ/JwqffkBPJrvczYyAv9FYuQ1/ihji3jTplC0/bpTuIrwBkBCzwj5ug==} + '@clerk/electron-passkeys-darwin-x64@0.0.3': + resolution: {integrity: sha512-0UPAWQEni7o8gjWwd2sP97fPshJmT2w5b8eV/jOPVSP8NqIcFCm5yIFSEx1SfB53LGfzB01A4iEGi2pxiCHqyQ==} cpu: [x64] os: [darwin] - '@clerk/electron-passkeys-win32-arm64-msvc@0.0.2-snapshot.v20260622234151': - resolution: {integrity: sha512-anBFktMTAF7of37nLQsqQuW489w563J75l4QolWtblKbrBjlwFsEif95uj3vlQj0qWVVRWpAcxD21oD6wdJcwQ==} + '@clerk/electron-passkeys-win32-arm64-msvc@0.0.3': + resolution: {integrity: sha512-bbRvifm9A/gfPkwC1UyMyF9sYGEs0aen7J2kCcaOIW3K0PXcAbHtsR/gnx4dT0bjFdd21vSa2zlIdERvHc1BiA==} cpu: [arm64] os: [win32] - '@clerk/electron-passkeys-win32-x64-msvc@0.0.2-snapshot.v20260622234151': - resolution: {integrity: sha512-Pq4FOklTJNpyMTJpxY+/gq6CukvWHFJfGYjuz985cPHup1OSsVmuO/GYil36vh7Qbe8vDivT91wNe73DbEOcVw==} + '@clerk/electron-passkeys-win32-x64-msvc@0.0.3': + resolution: {integrity: sha512-gFaVqKlOKTF2SJ3lMwkZ0oonycQn93p1qSrp5kzzeZBeQ+tGB3gQnjXecrhQy21AzDUyWqVJJ8KrP2HRAwifWg==} cpu: [x64] os: [win32] - '@clerk/electron-passkeys@0.0.2-snapshot.v20260622234151': - resolution: {integrity: sha512-UvBweTg9+FCbygxARV7RvJ4/lcQgLN+gJC6Lj++cLmbxpgRwRAgR9xXRlKV6dVg1p5k81a4DcFNMu/oR6zBdlQ==} + '@clerk/electron-passkeys@0.0.3': + resolution: {integrity: sha512-OHhIe88qDL+FxyBalXdXNHAS5eEramr6Rerp+6iNkfkjqT8rx4hHNmfpmjg5/T1/am8QfknbOBZkqoXZlCrjPg==} engines: {node: '>=20.9.0'} - '@clerk/electron@0.0.2-snapshot.v20260622234151': - resolution: {integrity: sha512-T0LUJeAPaAZxpk13Q14b9LSVKKio/X/zFo67hgdr35MaOChsn4T6lQ/rEL7laScQBduskcs1yyjr3e9ndy5ojg==} + '@clerk/electron@0.0.5': + resolution: {integrity: sha512-usPkmcKsSBuqs9zUctVZLlde3Fhu+pYJ+8fHZPL7aPYfupIe0qvsWOgJIb6GXSrEshcBg104MdSzuVT2w7Dzxg==} engines: {node: '>=20.9.0'} peerDependencies: - '@clerk/electron-passkeys': 0.0.2-snapshot.v20260622234151 + '@clerk/electron-passkeys': 0.0.3 electron: '>=28' electron-store: ^8.2.0 react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 @@ -1693,11 +1693,11 @@ packages: react-dom: optional: true - '@clerk/expo@3.5.3-snapshot.v20260622234151': - resolution: {integrity: sha512-qeKTJYA7cTe5oCmRVCT4A2QEPRp2af0ox0VdXzIhWRqRVFNF5bnsZRNKE2e4QnsC16NYLsGHLR5uQGC15qBiug==} + '@clerk/expo@3.6.2': + resolution: {integrity: sha512-8h4j62YLEtKlAq6eptfwB6jauOy7QQA+lMqRCY00n9PL1oQG7Y+sTGDg6ylX66O/VG2YOsqnBdlZcNUgt3xd2A==} engines: {node: '>=20.9.0'} peerDependencies: - '@clerk/expo-passkeys': 1.1.9-snapshot.v20260622234151 + '@clerk/expo-passkeys': '>=0.0.6' expo: '>=53 <57' expo-apple-authentication: '>=7.0.0' expo-auth-session: '>=5' @@ -1729,15 +1729,15 @@ packages: react-dom: optional: true - '@clerk/react@6.11.0-snapshot.v20260622234151': - resolution: {integrity: sha512-yF4jQFJqEHAqZCpOtjQ/Kg9yqZlEG6vW2vmVR5kVwgESkpOR1KPJIEMU8o3b6W86RKQai4lt3CWtY+o7CJsDyw==} + '@clerk/react@6.11.1': + resolution: {integrity: sha512-iEWckisZa/V3siCFlvGTNg7Yj+kndRx7HKBZ/QMSd6uEU9kd2tZ8Ymvd4GFPCpClwcki4h7jC750FujN656Jqg==} engines: {node: '>=20.9.0'} peerDependencies: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 react-dom: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 - '@clerk/shared@4.21.0-snapshot.v20260622234151': - resolution: {integrity: sha512-OYu+hO0GHiHwYTwSFe/GCHmuQlyTHyNa7fJ8vrmtUyML09mf+dayRFeUnxgkwOYhGriyq40t1zxdVx53Td73xg==} + '@clerk/shared@4.22.0': + resolution: {integrity: sha512-GZ56kzUB2UBb8MCF+Eo8WIey+W4RP1tAkyOL+geiHxrQxJlUcgD+xalY2YPJLH8WVFwtOnfIl8KPEo0M0e/DRg==} engines: {node: '>=20.9.0'} peerDependencies: react: ^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0 @@ -11141,10 +11141,10 @@ snapshots: '@bruits/satteri-win32-x64-msvc@0.9.3': optional: true - '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@capsizecss/unpack@4.0.1': dependencies: @@ -11173,18 +11173,18 @@ snapshots: fast-wrap-ansi: 0.2.2 sisteransi: 1.0.5 - '@clerk/backend@3.8.3-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/backend@3.8.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) standardwebhooks: 1.0.0 tslib: 2.8.1 transitivePeerDependencies: - react - react-dom - '@clerk/clerk-js@6.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/clerk-js@6.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@stripe/stripe-js': 5.6.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': 5.100.14 @@ -11199,9 +11199,9 @@ snapshots: - react - react-dom - '@clerk/clerk-js@6.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/clerk-js@6.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) '@stripe/stripe-js': 5.6.0 '@swc/helpers': 0.5.21 '@tanstack/query-core': 5.100.14 @@ -11216,72 +11216,72 @@ snapshots: - react - react-dom - '@clerk/electron-passkeys-darwin-arm64@0.0.2-snapshot.v20260622234151': + '@clerk/electron-passkeys-darwin-arm64@0.0.3': optional: true - '@clerk/electron-passkeys-darwin-x64@0.0.2-snapshot.v20260622234151': + '@clerk/electron-passkeys-darwin-x64@0.0.3': optional: true - '@clerk/electron-passkeys-win32-arm64-msvc@0.0.2-snapshot.v20260622234151': + '@clerk/electron-passkeys-win32-arm64-msvc@0.0.3': optional: true - '@clerk/electron-passkeys-win32-x64-msvc@0.0.2-snapshot.v20260622234151': + '@clerk/electron-passkeys-win32-x64-msvc@0.0.3': optional: true - '@clerk/electron-passkeys@0.0.2-snapshot.v20260622234151': + '@clerk/electron-passkeys@0.0.3': optionalDependencies: - '@clerk/electron-passkeys-darwin-arm64': 0.0.2-snapshot.v20260622234151 - '@clerk/electron-passkeys-darwin-x64': 0.0.2-snapshot.v20260622234151 - '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.2-snapshot.v20260622234151 - '@clerk/electron-passkeys-win32-x64-msvc': 0.0.2-snapshot.v20260622234151 + '@clerk/electron-passkeys-darwin-arm64': 0.0.3 + '@clerk/electron-passkeys-darwin-x64': 0.0.3 + '@clerk/electron-passkeys-win32-arm64-msvc': 0.0.3 + '@clerk/electron-passkeys-win32-x64-msvc': 0.0.3 - '@clerk/electron@0.0.2-snapshot.v20260622234151(@clerk/electron-passkeys@0.0.2-snapshot.v20260622234151)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/electron@0.0.5(@clerk/electron-passkeys@0.0.3)(electron-store@8.2.0)(electron@41.5.0)(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/clerk-js': 6.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/react': 6.11.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/clerk-js': 6.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/react': 6.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) electron: 41.5.0 react: 19.2.6 tslib: 2.8.1 optionalDependencies: - '@clerk/electron-passkeys': 0.0.2-snapshot.v20260622234151 + '@clerk/electron-passkeys': 0.0.3 electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/expo@3.5.3-snapshot.v20260622234151(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@clerk/expo@3.6.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@clerk/clerk-js': 6.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@clerk/react': 6.11.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/clerk-js': 6.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/react': 6.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) base-64: 1.0.0 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) tslib: 2.8.1 optionalDependencies: - expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: 56.0.4(expo@56.0.12) expo-secure-store: 56.0.4(expo@56.0.12) - expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react-dom: 19.2.3(react@19.2.3) - '@clerk/react@6.11.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/react@6.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@clerk/shared': 4.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) react: 19.2.3 react-dom: 19.2.3(react@19.2.3) tslib: 2.8.1 - '@clerk/react@6.11.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/react@6.11.1(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: - '@clerk/shared': 4.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6) + '@clerk/shared': 4.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6) react: 19.2.6 react-dom: 19.2.6(react@19.2.6) tslib: 2.8.1 - '@clerk/shared@4.21.0-snapshot.v20260622234151(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + '@clerk/shared@4.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: '@tanstack/query-core': 5.100.14 dequal: 2.0.3 @@ -11291,7 +11291,7 @@ snapshots: react: 19.2.3 react-dom: 19.2.3(react@19.2.3) - '@clerk/shared@4.21.0-snapshot.v20260622234151(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': + '@clerk/shared@4.22.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: '@tanstack/query-core': 5.100.14 dequal: 2.0.3 @@ -11847,7 +11847,7 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.38': {} - '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': + '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 56.0.9(typescript@6.0.3) @@ -11857,7 +11857,7 @@ snapshots: '@expo/image-utils': 0.10.1(typescript@6.0.3) '@expo/inline-modules': 0.0.12(typescript@6.0.3) '@expo/json-file': 10.2.0 - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) '@expo/metro-file-map': 56.0.3 @@ -11882,7 +11882,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.4 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-server: 56.0.5 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -11908,8 +11908,8 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(53652d9673d8ec829763e55ad4f6fd4e) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo-router: 56.2.11(3dd1c34e09949502ba8228b7f60c3e89) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' - '@expo/metro-runtime' @@ -11992,18 +11992,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@expo/env@2.3.0': dependencies: @@ -12064,13 +12064,13 @@ snapshots: - supports-color - typescript - '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) anser: 1.4.10 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) stacktrace-parser: 0.1.11 '@expo/metro-config@56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6)': @@ -12099,7 +12099,7 @@ snapshots: postcss: 8.5.15 resolve-from: 5.0.0 optionalDependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - supports-color @@ -12117,14 +12117,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) anser: 1.4.10 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 optionalDependencies: @@ -12199,14 +12199,14 @@ snapshots: '@expo/router-server@56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: debug: 4.4.3 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-server: 56.0.5 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(53652d9673d8ec829763e55ad4f6fd4e) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-router: 56.2.11(3dd1c34e09949502ba8228b7f60c3e89) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -12221,18 +12221,18 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ui@56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0)': + '@expo/ui@56.0.18(19413efe5eaad64848598eedfe3a0fd3)': dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: '@babel/core': 7.29.7 react-dom: 19.2.3(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -12495,13 +12495,13 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@legendapp/list@3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: react-dom: 19.2.3(react@19.2.3) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13324,15 +13324,15 @@ snapshots: prompts: 2.4.2 tinyexec: 1.2.4 - '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@react-native/assets-registry@0.85.3': {} @@ -13392,7 +13392,7 @@ snapshots: tinyglobby: 0.2.17 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: '@react-native/dev-middleware': 0.85.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) debug: 4.4.3 @@ -13402,7 +13402,7 @@ snapshots: metro-core: 0.84.4 semver: 7.8.5 optionalDependencies: - '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@react-native/metro-config': 0.85.3(@babel/core@7.29.7) transitivePeerDependencies: - bufferutil - supports-color @@ -13450,7 +13450,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@react-native/metro-config@0.85.3(@babel/core@7.29.7)': dependencies: '@react-native/js-polyfills': 0.85.3 '@react-native/metro-babel-transformer': 0.85.3(@babel/core@7.29.7) @@ -13458,18 +13458,16 @@ snapshots: metro-runtime: 0.84.4 transitivePeerDependencies: - '@babel/core' - - bufferutil - supports-color - - utf-8-validate '@react-native/normalize-colors@0.85.3': {} - '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) optionalDependencies: '@types/react': 19.2.16 @@ -13854,15 +13852,15 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe)': + '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(a6e4bd1e9376530ea93dbb7d8a53d32d)': dependencies: - expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) - expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: 56.0.3(expo@56.0.12) - expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': {} @@ -14977,8 +14975,8 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-widgets: 56.0.19(19413efe5eaad64848598eedfe3a0fd3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -15030,8 +15028,8 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-widgets: 56.0.19(19413efe5eaad64848598eedfe3a0fd3) transitivePeerDependencies: - '@babel/core' - supports-color @@ -15928,29 +15926,29 @@ snapshots: expo-application@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: expo-application: 56.0.3(expo@56.0.12) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: 56.0.4(expo@56.0.12) - expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - expo - supports-color @@ -15958,119 +15956,119 @@ snapshots: expo-build-properties@56.0.19(expo@56.0.12): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) resolve-from: 5.0.0 semver: 7.8.5 - expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: barcode-detector: 3.2.0(@types/emscripten@1.41.5) - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@types/emscripten' - expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/env': 2.3.0 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color expo-crypto@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-dev-menu-interface: 56.0.1(expo@56.0.12) expo-manifests: 56.0.4(expo@56.0.12) expo-updates-interface: 56.0.2(expo@56.0.12) transitivePeerDependencies: - react-native - expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-manifests: 56.0.4(expo@56.0.12) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-dev-menu-interface@56.0.1(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-dev-menu-interface: 56.0.1(expo@56.0.12) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-eas-client@56.0.1: {} - expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) fontfaceobserver: 2.3.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-haptics@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-image-loader@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-image-picker@56.0.18(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-image-loader: 56.0.3(expo@56.0.12) expo-json-utils@56.0.0: {} expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - expo - supports-color expo-manifests@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-json-utils: 56.0.0 expo-modules-autolinking@56.0.16(typescript@6.0.3): @@ -16083,66 +16081,66 @@ snapshots: - supports-color - typescript - expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/expo-modules-macros-plugin': 0.2.2 - expo-modules-jsi: 56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-modules-jsi: 56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) optionalDependencies: - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-network@56.0.5(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-application: 56.0.3(expo@56.0.12) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(53652d9673d8ec829763e55ad4f6fd4e): + expo-router@56.2.11(3dd1c34e09949502ba8228b7f60c3e89): dependencies: - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/schema-utils': 56.0.1 - '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) + '@expo/ui': 56.0.18(19413efe5eaad64848598eedfe3a0fd3) '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) client-only: 0.0.1 color: 4.2.3 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-server: 56.0.5 - expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.12 @@ -16150,10 +16148,10 @@ snapshots: react: 19.2.3 react-fast-compare: 3.2.2 react-is: 19.2.7 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) - react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-drawer-layout: 4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -16161,8 +16159,8 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: react-dom: 19.2.3(react@19.2.3) - react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - '@testing-library/dom' @@ -16174,7 +16172,7 @@ snapshots: expo-secure-store@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-server@56.0.5: {} @@ -16182,7 +16180,7 @@ snapshots: dependencies: '@expo/config-plugins': 56.0.8(typescript@6.0.3) '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) xml2js: 0.6.0 transitivePeerDependencies: - supports-color @@ -16190,20 +16188,20 @@ snapshots: expo-structured-headers@56.0.0: {} - expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.38 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 expo-updates-interface@56.0.2(expo@56.0.12): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.7.0 @@ -16211,7 +16209,7 @@ snapshots: arg: 4.1.3 chalk: 4.1.2 debug: 4.4.3 - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) expo-eas-client: 56.0.1 expo-manifests: 56.0.4(expo@56.0.12) expo-structured-headers: 56.0.0 @@ -16221,25 +16219,25 @@ snapshots: ignore: 5.3.2 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) transitivePeerDependencies: - supports-color - expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0): + expo-widgets@56.0.19(19413efe5eaad64848598eedfe3a0fd3): dependencies: '@expo/plist': 0.7.0 - '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) - expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + '@expo/ui': 56.0.18(19413efe5eaad64848598eedfe3a0fd3) + expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@babel/core' - '@types/react' @@ -16248,37 +16246,37 @@ snapshots: - react-native-reanimated - react-native-worklets - expo@56.0.12(8895228379997a2a064f9644cda56ed0): + expo@56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) '@expo/config': 56.0.9(typescript@6.0.3) '@expo/config-plugins': 56.0.9(typescript@6.0.3) - '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/fingerprint': 0.19.4 '@expo/local-build-cache-provider': 56.0.8(typescript@6.0.3) - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) '@ungap/structured-clone': 1.3.1 babel-preset-expo: 56.0.15(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2) - expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-file-system: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-file-system: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-keep-awake: 56.0.3(expo@56.0.12)(react@19.2.3) expo-modules-autolinking: 56.0.16(typescript@6.0.3) - expo-modules-core: 56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-modules-core: 56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) - react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -18699,102 +18697,102 @@ snapshots: transitivePeerDependencies: - supports-color - react-native-drawer-layout@4.2.4(0e9729601f58a7a7ae26c76fe6017455): + react-native-drawer-layout@4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: color: 4.2.3 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) - react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@egjs/hammerjs': 2.0.17 '@types/react-test-renderer': 19.1.0 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) optionalDependencies: - react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) semver: 7.8.5 - react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: css-select: 5.2.2 css-tree: 1.1.3 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) whatwg-url-without-unicode: 8.0.0-3 - react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) @@ -18806,23 +18804,23 @@ snapshots: '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@react-native/metro-config': 0.85.3(@babel/core@7.29.7) convert-source-map: 2.0.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) semver: 7.8.5 transitivePeerDependencies: - supports-color - react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6): + react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6): dependencies: '@react-native/assets-registry': 0.85.3 '@react-native/codegen': 0.85.3(@babel/core@7.29.7) - '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@react-native/gradle-plugin': 0.85.3 '@react-native/js-polyfills': 0.85.3 '@react-native/normalize-colors': 0.85.3 - '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -19873,14 +19871,14 @@ snapshots: universalify@2.0.1: {} - uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): + uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 culori: 4.0.2 lightningcss: 1.30.1 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) tailwindcss: 4.3.0 unpipe@1.0.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 34867016252..aa27daf4914 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -6,13 +6,13 @@ packages: - scripts catalog: - "@clerk/backend": 3.8.3-snapshot.v20260622234151 - "@clerk/clerk-js": 6.21.0-snapshot.v20260622234151 - "@clerk/electron": 0.0.2-snapshot.v20260622234151 - "@clerk/electron-passkeys": 0.0.2-snapshot.v20260622234151 - "@clerk/expo": 3.5.3-snapshot.v20260622234151 - "@clerk/react": 6.11.0-snapshot.v20260622234151 - "@clerk/shared": 4.21.0-snapshot.v20260622234151 + "@clerk/backend": 3.8.4 + "@clerk/clerk-js": 6.22.0 + "@clerk/electron": 0.0.5 + "@clerk/electron-passkeys": 0.0.3 + "@clerk/expo": 3.6.2 + "@clerk/react": 6.11.1 + "@clerk/shared": 4.22.0 "@effect/atom-react": 4.0.0-beta.78 "@effect/openapi-generator": 4.0.0-beta.78 "@effect/platform-bun": 4.0.0-beta.78 From 4689c43c9803a810e8c6ae1510122b2d47ce0edb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 18:43:31 -0700 Subject: [PATCH 08/71] Refactor session handling and stream event processing --- .../ios/T3ComposerEditorModule.swift | 1 + .../ios/T3ComposerEditorView.swift | 22 + .../expo-module.config.json | 2 +- .../ios/T3KeyboardCommandsModule.swift | 131 +++++ apps/mobile/src/app/_layout.tsx | 13 +- apps/mobile/src/app/index.tsx | 59 +-- apps/mobile/src/features/home/HomeHeader.tsx | 65 +-- apps/mobile/src/features/home/HomeScreen.tsx | 63 +-- .../home/WorkspaceConnectionStatus.test.ts | 59 +++ .../home/WorkspaceConnectionStatus.tsx | 53 ++ .../features/home/home-list-options.test.ts | 31 ++ .../src/features/home/home-list-options.ts | 144 ++++++ .../home/workspace-connection-status.ts | 20 + .../HardwareKeyboardCommandProvider.tsx | 73 +++ .../keyboard/hardwareKeyboardCommands.test.ts | 28 ++ .../keyboard/hardwareKeyboardCommands.ts | 78 +++ .../layout/AdaptiveWorkspaceLayout.tsx | 187 ++++++-- .../layout/adaptive-inspector-layout.tsx | 61 ++- .../layout/workspace-pane-divider.tsx | 111 +++++ .../src/features/threads/ThreadComposer.tsx | 14 +- .../threads/ThreadNavigationSidebar.tsx | 454 +++++++++++++++--- apps/mobile/src/lib/layout.test.ts | 49 ++ apps/mobile/src/lib/layout.ts | 77 ++- .../src/native/T3ComposerEditor.ios.tsx | 3 + .../src/native/T3ComposerEditor.types.ts | 2 + .../src/native/T3KeyboardCommands.ios.tsx | 31 ++ apps/mobile/src/native/T3KeyboardCommands.tsx | 13 + 27 files changed, 1561 insertions(+), 283 deletions(-) create mode 100644 apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift create mode 100644 apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts create mode 100644 apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx create mode 100644 apps/mobile/src/features/home/home-list-options.test.ts create mode 100644 apps/mobile/src/features/home/home-list-options.ts create mode 100644 apps/mobile/src/features/home/workspace-connection-status.ts create mode 100644 apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx create mode 100644 apps/mobile/src/features/keyboard/hardwareKeyboardCommands.test.ts create mode 100644 apps/mobile/src/features/keyboard/hardwareKeyboardCommands.ts create mode 100644 apps/mobile/src/features/layout/workspace-pane-divider.tsx create mode 100644 apps/mobile/src/native/T3KeyboardCommands.ios.tsx create mode 100644 apps/mobile/src/native/T3KeyboardCommands.tsx diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorModule.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorModule.swift index d3b12770b04..a56619b7d48 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorModule.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorModule.swift @@ -47,6 +47,7 @@ public class T3ComposerEditorModule: Module { "onComposerSelectionChange", "onComposerFocus", "onComposerBlur", + "onComposerSubmit", "onComposerPasteImages", "onComposerContentSizeChange" ) diff --git a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift index afb3ba2d94f..8515abfbae8 100644 --- a/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift +++ b/apps/mobile/modules/t3-composer-editor/ios/T3ComposerEditorView.swift @@ -63,6 +63,24 @@ private final class ComposerTextView: UITextView { var onPasteImages: (([String]) -> Void)? var onAttributedMutation: (() -> Void)? + var onSubmit: (() -> Void)? + + override var keyCommands: [UIKeyCommand]? { + var commands = super.keyCommands ?? [] + let submit = UIKeyCommand( + input: "\r", + modifierFlags: .command, + action: #selector(submitMessage(_:)) + ) + submit.discoverabilityTitle = "Send Message" + submit.wantsPriorityOverSystemBehavior = true + commands.append(submit) + return commands + } + + @objc private func submitMessage(_ sender: UIKeyCommand) { + onSubmit?() + } override func canPerformAction(_ action: Selector, withSender sender: Any?) -> Bool { if action == #selector(paste(_:)) { @@ -299,6 +317,7 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { let onComposerSelectionChange = EventDispatcher() let onComposerFocus = EventDispatcher() let onComposerBlur = EventDispatcher() + let onComposerSubmit = EventDispatcher() let onComposerPasteImages = EventDispatcher() let onComposerContentSizeChange = EventDispatcher() @@ -320,6 +339,9 @@ public final class T3ComposerEditorView: ExpoView, UITextViewDelegate { textView.onAttributedMutation = { [weak self] in self?.emitTextChange() } + textView.onSubmit = { [weak self] in + self?.onComposerSubmit([:]) + } addSubview(textView) placeholderLabel.numberOfLines = 0 diff --git a/apps/mobile/modules/t3-native-controls/expo-module.config.json b/apps/mobile/modules/t3-native-controls/expo-module.config.json index e47aa8bbfd8..b53b3c50a03 100644 --- a/apps/mobile/modules/t3-native-controls/expo-module.config.json +++ b/apps/mobile/modules/t3-native-controls/expo-module.config.json @@ -1,6 +1,6 @@ { "platforms": ["apple"], "apple": { - "modules": ["T3NativeControlsModule"] + "modules": ["T3NativeControlsModule", "T3KeyboardCommandsModule"] } } diff --git a/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift b/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift new file mode 100644 index 00000000000..ea572cc7a01 --- /dev/null +++ b/apps/mobile/modules/t3-native-controls/ios/T3KeyboardCommandsModule.swift @@ -0,0 +1,131 @@ +import ExpoModulesCore +import UIKit + +public final class T3KeyboardCommandsModule: Module { + public func definition() -> ModuleDefinition { + Name("T3KeyboardCommands") + + View(T3KeyboardCommandsView.self) { + Prop("enabledCommands") { (view: T3KeyboardCommandsView, commands: [String]) in + view.setEnabledCommands(commands) + } + Events("onCommand") + } + } +} + +public final class T3KeyboardCommandsView: ExpoView { + let onCommand = EventDispatcher() + private var enabledCommands = Set() + + public override var canBecomeFirstResponder: Bool { true } + + public override var keyCommands: [UIKeyCommand]? { + [ + enabledCommand("newTask", input: "n", modifiers: .command, action: #selector(newTask), title: "New Task"), + enabledCommand("focusSearch", input: "f", modifiers: .command, action: #selector(focusSearch), title: "Find"), + enabledCommand("focusSearch", input: "k", modifiers: .command, action: #selector(focusSearch), title: "Focus Search"), + enabledCommand("back", input: "[", modifiers: .command, action: #selector(goBack), title: "Back"), + enabledCommand("files", input: "f", modifiers: [.command, .shift], action: #selector(openFiles), title: "Open Files"), + enabledCommand("terminal", input: "t", modifiers: [.command, .shift], action: #selector(openTerminal), title: "Open Terminal"), + enabledCommand("review", input: "r", modifiers: [.command, .shift], action: #selector(openReview), title: "Open Review"), + enabledCommand("toggleSidebar", input: "\\", modifiers: .command, action: #selector(handleToggleSidebar), title: "Toggle Sidebar"), + ].compactMap { $0 } + } + + func setEnabledCommands(_ commands: [String]) { + enabledCommands = Set(commands) + if isFirstResponder { + resignFirstResponder() + } + reclaimFirstResponderIfAvailable() + } + + private func enabledCommand( + _ identifier: String, + input: String, + modifiers: UIKeyModifierFlags, + action: Selector, + title: String + ) -> UIKeyCommand? { + guard enabledCommands.contains(identifier) else { return nil } + return command(input, modifiers: modifiers, action: action, title: title) + } + + public required init(appContext: AppContext? = nil) { + super.init(appContext: appContext) + NotificationCenter.default.addObserver( + self, + selector: #selector(reclaimFirstResponderIfAvailable), + name: UITextView.textDidEndEditingNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(reclaimFirstResponderIfAvailable), + name: UITextField.textDidEndEditingNotification, + object: nil + ) + NotificationCenter.default.addObserver( + self, + selector: #selector(reclaimFirstResponderIfAvailable), + name: UIApplication.didBecomeActiveNotification, + object: nil + ) + } + + deinit { + NotificationCenter.default.removeObserver(self) + } + + public override func didMoveToWindow() { + super.didMoveToWindow() + reclaimFirstResponderIfAvailable() + } + + public override func layoutSubviews() { + super.layoutSubviews() + reclaimFirstResponderIfAvailable() + } + + private func command( + _ input: String, + modifiers: UIKeyModifierFlags, + action: Selector, + title: String + ) -> UIKeyCommand { + let command = UIKeyCommand(input: input, modifierFlags: modifiers, action: action) + command.discoverabilityTitle = title + command.wantsPriorityOverSystemBehavior = true + return command + } + + @objc private func newTask() { emit("newTask") } + @objc private func focusSearch() { emit("focusSearch") } + @objc private func goBack() { emit("back") } + @objc private func openFiles() { emit("files") } + @objc private func openTerminal() { emit("terminal") } + @objc private func openReview() { emit("review") } + @objc private func handleToggleSidebar() { emit("toggleSidebar") } + + private func emit(_ command: String) { + onCommand(["command": command]) + } + + @objc private func reclaimFirstResponderIfAvailable() { + DispatchQueue.main.async { [weak self] in + guard let self, self.window?.t3FirstResponder == nil else { return } + self.becomeFirstResponder() + } + } +} + +private extension UIView { + var t3FirstResponder: UIResponder? { + if isFirstResponder { return self } + for subview in subviews { + if let responder = subview.t3FirstResponder { return responder } + } + return nil + } +} diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 23762c4cdce..c0e95c3d3b5 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -32,6 +32,7 @@ import { } from "../features/layout/AdaptiveWorkspaceLayout"; import { deriveStableFormSheetDetent } from "../lib/layout"; import { useThemeColor } from "../lib/useThemeColor"; +import { HardwareKeyboardCommandProvider } from "../features/keyboard/HardwareKeyboardCommandProvider"; function AppNavigator() { const pathname = usePathname(); @@ -157,11 +158,13 @@ export default function RootLayout() { - {fontsLoaded ? ( - - ) : ( - - )} + + {fontsLoaded ? ( + + ) : ( + + )} + diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index db1682d2a68..a75507e03df 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -1,17 +1,7 @@ -import type { - EnvironmentId, - SidebarProjectGroupingMode, - SidebarThreadSortOrder, -} from "@t3tools/contracts"; -import { - DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, - DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, - DEFAULT_SIDEBAR_THREAD_SORT_ORDER, -} from "@t3tools/contracts"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { Stack, useRouter } from "expo-router"; -import { useCallback, useMemo, useState } from "react"; +import { useMemo, useState } from "react"; import { useProjects, useThreadShells } from "../state/entities"; import { useWorkspaceState } from "../state/workspace"; @@ -19,19 +9,12 @@ import { buildThreadRoutePath } from "../lib/routes"; import { useSavedRemoteConnections } from "../state/use-remote-environment-registry"; import { HomeScreen } from "../features/home/HomeScreen"; import { HomeHeader } from "../features/home/HomeHeader"; -import type { HomeProjectSortOrder } from "../features/home/homeThreadList"; +import { useHomeListOptions } from "../features/home/home-list-options"; import { useThreadListActions } from "../features/home/useThreadListActions"; import { useAdaptiveWorkspaceLayout } from "../features/layout/AdaptiveWorkspaceLayout"; import { WorkspaceEmptyDetail } from "../features/layout/WorkspaceEmptyDetail"; import { WorkspaceSidebarToolbar } from "../features/layout/workspace-sidebar-toolbar"; -interface HomeListOptions { - readonly selectedEnvironmentId: EnvironmentId | null; - readonly projectSortOrder: HomeProjectSortOrder; - readonly threadSortOrder: SidebarThreadSortOrder; - readonly projectGroupingMode: SidebarProjectGroupingMode; -} - /* ─── Route screen ───────────────────────────────────────────────────── */ export default function HomeRouteScreen() { @@ -42,15 +25,6 @@ export default function HomeRouteScreen() { const { savedConnectionsById } = useSavedRemoteConnections(); const router = useRouter(); const [searchQuery, setSearchQuery] = useState(""); - const [listOptions, setListOptions] = useState({ - selectedEnvironmentId: null, - projectSortOrder: - DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual" - ? "updated_at" - : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, - threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER, - projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, - }); const { archiveThread, confirmDeleteThread } = useThreadListActions(); const environments = useMemo( () => @@ -66,23 +40,18 @@ export default function HomeRouteScreen() { ), [savedConnectionsById], ); - const selectedEnvironmentId = environments.some( - (environment) => environment.environmentId === listOptions.selectedEnvironmentId, - ) - ? listOptions.selectedEnvironmentId - : null; - const setSelectedEnvironmentId = useCallback((environmentId: EnvironmentId | null) => { - setListOptions((current) => ({ ...current, selectedEnvironmentId: environmentId })); - }, []); - const setProjectSortOrder = useCallback((projectSortOrder: HomeProjectSortOrder) => { - setListOptions((current) => ({ ...current, projectSortOrder })); - }, []); - const setThreadSortOrder = useCallback((threadSortOrder: SidebarThreadSortOrder) => { - setListOptions((current) => ({ ...current, threadSortOrder })); - }, []); - const setProjectGroupingMode = useCallback((projectGroupingMode: SidebarProjectGroupingMode) => { - setListOptions((current) => ({ ...current, projectGroupingMode })); - }, []); + const availableEnvironmentIds = useMemo( + () => new Set(environments.map((environment) => environment.environmentId)), + [environments], + ); + const { + options: listOptions, + setSelectedEnvironmentId, + setProjectGroupingMode, + setProjectSortOrder, + setThreadSortOrder, + } = useHomeListOptions(availableEnvironmentIds); + const selectedEnvironmentId = listOptions.selectedEnvironmentId; if (layout.usesSplitView) { return ( diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 9757d5fbf91..152b01b37f5 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -3,61 +3,27 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import { - DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, - DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, - DEFAULT_SIDEBAR_THREAD_SORT_ORDER, -} from "@t3tools/contracts"; import { Stack } from "expo-router"; +import { useCallback, useRef } from "react"; import { Text as RNText, View } from "react-native"; +import type { SearchBarCommands } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import type { HomeProjectSortOrder } from "./homeThreadList"; +import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import { + hasCustomHomeListOptions, + PROJECT_GROUPING_OPTIONS, + PROJECT_SORT_OPTIONS, + THREAD_SORT_OPTIONS, +} from "./home-list-options"; export interface HomeHeaderEnvironment { readonly environmentId: EnvironmentId; readonly label: string; } -const PROJECT_SORT_OPTIONS: ReadonlyArray<{ - readonly value: HomeProjectSortOrder; - readonly label: string; -}> = [ - { value: "updated_at", label: "Last user message" }, - { value: "created_at", label: "Created at" }, -]; - -const THREAD_SORT_OPTIONS: ReadonlyArray<{ - readonly value: SidebarThreadSortOrder; - readonly label: string; -}> = [ - { value: "updated_at", label: "Last user message" }, - { value: "created_at", label: "Created at" }, -]; - -const PROJECT_GROUPING_OPTIONS: ReadonlyArray<{ - readonly value: SidebarProjectGroupingMode; - readonly label: string; - readonly subtitle: string; -}> = [ - { - value: "repository", - label: "Group by repository", - subtitle: "Combine matching repositories across environments", - }, - { - value: "repository_path", - label: "Group by repository path", - subtitle: "Combine only matching paths within a repository", - }, - { - value: "separate", - label: "Keep separate", - subtitle: "Show every project path separately", - }, -]; - export function HomeHeader(props: { readonly environments: ReadonlyArray; readonly selectedEnvironmentId: EnvironmentId | null; @@ -72,14 +38,16 @@ export function HomeHeader(props: { readonly onOpenSettings: () => void; readonly onStartNewTask: () => void; }) { + const searchBarRef = useRef(null); const iconColor = useThemeColor("--color-icon"); const mutedColor = useThemeColor("--color-foreground-muted"); const subtleColor = useThemeColor("--color-subtle"); - const hasCustomListOptions = - props.selectedEnvironmentId !== null || - props.projectSortOrder !== DEFAULT_SIDEBAR_PROJECT_SORT_ORDER || - props.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER || - props.projectGroupingMode !== DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE; + const hasCustomListOptions = hasCustomHomeListOptions(props); + const focusSearch = useCallback(() => { + searchBarRef.current?.focus(); + return searchBarRef.current !== null; + }, []); + useHardwareKeyboardCommand("focusSearch", focusSearch); return ( <> @@ -92,6 +60,7 @@ export function HomeHeader(props: { headerTintColor: iconColor, headerTitle: "", headerSearchBarOptions: { + ref: searchBarRef, placeholder: "Search threads", hideNavigationBar: false, onChangeText: (event) => { diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index ed10660b8f0..d38ae93158a 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -30,6 +30,8 @@ import { relativeTime } from "../../lib/time"; import { threadStatusTone } from "../threads/threadPresentation"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; import { ThreadSwipeable } from "./thread-swipe-actions"; +import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; +import { shouldShowWorkspaceConnectionStatus } from "./workspace-connection-status"; /* ─── Types ──────────────────────────────────────────────────────────── */ @@ -331,58 +333,6 @@ function ThreadRow(props: { /* ─── Main screen ────────────────────────────────────────────────────── */ -function staleCatalogPillLabel(props: { readonly catalogState: WorkspaceState }): string { - if (props.catalogState.networkStatus === "offline") { - return "You are offline"; - } - const connectingEnvironments = props.catalogState.connectingEnvironments; - if (connectingEnvironments.length === 1) { - return `Reconnecting to ${connectingEnvironments[0]!.environmentLabel}`; - } - if (connectingEnvironments.length > 1) { - return `Reconnecting ${connectingEnvironments.length} environments`; - } - return "Not connected"; -} - -function StaleCatalogStatusPill(props: { - readonly catalogState: WorkspaceState; - readonly onPress: () => void; -}) { - const iconColor = useThemeColor("--color-icon-muted"); - const label = staleCatalogPillLabel(props); - const isReconnecting = props.catalogState.connectingEnvironments.length > 0; - - return ( - - {isReconnecting ? ( - - ) : ( - - )} - - {label} - - - ); -} - export function HomeScreen(props: HomeScreenProps) { const [expandedProjects, setExpandedProjects] = useState>(() => new Set()); const openSwipeableRef = useRef(null); @@ -442,10 +392,7 @@ export function HomeScreen(props: HomeScreenProps) { : (props.savedConnectionsById[props.selectedEnvironmentId]?.environmentLabel ?? "this environment"); const hasSearchQuery = props.searchQuery.trim().length > 0; - const shouldShowConnectionStatus = - props.catalogState.networkStatus === "offline" || - props.catalogState.hasConnectingEnvironment || - (props.catalogState.hasLoadedShellSnapshot && !props.catalogState.hasReadyEnvironment); + const shouldShowConnectionStatus = shouldShowWorkspaceConnectionStatus(props.catalogState); const emptyState = deriveEmptyState({ catalogState: props.catalogState, projectCount: props.projects.length, @@ -556,8 +503,8 @@ export function HomeScreen(props: HomeScreenProps) { className="absolute left-0 right-0 items-center" style={{ bottom: Math.max(insets.bottom, 18) + 76 }} > - diff --git a/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts b/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts new file mode 100644 index 00000000000..e92d38af8be --- /dev/null +++ b/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts @@ -0,0 +1,59 @@ +import { describe, expect, it } from "vite-plus/test"; + +import type { WorkspaceState } from "../../state/workspaceModel"; +import { + shouldShowWorkspaceConnectionStatus, + workspaceConnectionStatusLabel, +} from "./workspace-connection-status"; + +function workspaceState(overrides: Partial = {}): WorkspaceState { + return { + isLoadingConnections: false, + hasConnections: true, + hasLoadedShellSnapshot: true, + hasPendingShellSnapshot: false, + hasReadyEnvironment: true, + hasConnectingEnvironment: false, + connectingEnvironments: [], + connectionState: "connected", + connectionError: null, + shellSnapshotError: null, + latestCachedSnapshotReceivedAt: null, + networkStatus: "online", + ...overrides, + }; +} + +describe("workspace connection status", () => { + it("stays hidden while a ready environment is connected", () => { + expect(shouldShowWorkspaceConnectionStatus(workspaceState())).toBe(false); + }); + + it("surfaces offline snapshots", () => { + const state = workspaceState({ networkStatus: "offline", hasReadyEnvironment: false }); + + expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); + expect(workspaceConnectionStatusLabel(state)).toBe("You are offline"); + }); + + it("names the environment while reconnecting", () => { + const state = workspaceState({ + hasConnectingEnvironment: true, + hasReadyEnvironment: false, + connectingEnvironments: [ + { + environmentId: "environment-1" as never, + environmentLabel: "Julius’s Mac mini", + displayUrl: "", + isRelayManaged: false, + connectionState: "reconnecting", + connectionError: null, + connectionErrorTraceId: null, + }, + ], + }); + + expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); + expect(workspaceConnectionStatusLabel(state)).toBe("Reconnecting to Julius’s Mac mini"); + }); +}); diff --git a/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx b/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx new file mode 100644 index 00000000000..f41ac8c45d6 --- /dev/null +++ b/apps/mobile/src/features/home/WorkspaceConnectionStatus.tsx @@ -0,0 +1,53 @@ +import { SymbolView } from "expo-symbols"; +import { ActivityIndicator, Pressable } from "react-native"; + +import { AppText as Text } from "../../components/AppText"; +import { useThemeColor } from "../../lib/useThemeColor"; +import type { WorkspaceState } from "../../state/workspaceModel"; +import { workspaceConnectionStatusLabel } from "./workspace-connection-status"; + +export function WorkspaceConnectionStatus(props: { + readonly state: WorkspaceState; + readonly onPress: () => void; + readonly variant?: "floating" | "sidebar"; +}) { + const iconColor = useThemeColor("--color-icon-muted"); + const isReconnecting = props.state.connectingEnvironments.length > 0; + const variant = props.variant ?? "floating"; + + return ( + + {isReconnecting ? ( + + ) : ( + + )} + + {workspaceConnectionStatusLabel(props.state)} + + {variant === "sidebar" ? ( + + ) : null} + + ); +} diff --git a/apps/mobile/src/features/home/home-list-options.test.ts b/apps/mobile/src/features/home/home-list-options.test.ts new file mode 100644 index 00000000000..788be25906b --- /dev/null +++ b/apps/mobile/src/features/home/home-list-options.test.ts @@ -0,0 +1,31 @@ +import { + DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, + DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, + DEFAULT_SIDEBAR_THREAD_SORT_ORDER, +} from "@t3tools/contracts"; +import { describe, expect, it } from "vite-plus/test"; + +import { hasCustomHomeListOptions, type HomeListOptions } from "./home-list-options"; + +const defaults: HomeListOptions = { + selectedEnvironmentId: null, + projectSortOrder: + DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual" + ? "updated_at" + : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, + threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER, + projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, +}; + +describe("home list options", () => { + it("recognizes default options", () => { + expect(hasCustomHomeListOptions(defaults)).toBe(false); + }); + + it("marks environment filters and grouping changes as customized", () => { + expect( + hasCustomHomeListOptions({ ...defaults, selectedEnvironmentId: "environment-1" as never }), + ).toBe(true); + expect(hasCustomHomeListOptions({ ...defaults, projectGroupingMode: "separate" })).toBe(true); + }); +}); diff --git a/apps/mobile/src/features/home/home-list-options.ts b/apps/mobile/src/features/home/home-list-options.ts new file mode 100644 index 00000000000..64881034306 --- /dev/null +++ b/apps/mobile/src/features/home/home-list-options.ts @@ -0,0 +1,144 @@ +import type { + EnvironmentId, + SidebarProjectGroupingMode, + SidebarThreadSortOrder, +} from "@t3tools/contracts"; +import { + DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, + DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, + DEFAULT_SIDEBAR_THREAD_SORT_ORDER, +} from "@t3tools/contracts"; +import { + createContext, + createElement, + useCallback, + useContext, + useMemo, + useState, + type PropsWithChildren, + type Dispatch, + type SetStateAction, +} from "react"; + +import type { HomeProjectSortOrder } from "./homeThreadList"; + +export interface HomeListOptions { + readonly selectedEnvironmentId: EnvironmentId | null; + readonly projectSortOrder: HomeProjectSortOrder; + readonly threadSortOrder: SidebarThreadSortOrder; + readonly projectGroupingMode: SidebarProjectGroupingMode; +} + +export const PROJECT_SORT_OPTIONS: ReadonlyArray<{ + readonly value: HomeProjectSortOrder; + readonly label: string; +}> = [ + { value: "updated_at", label: "Last user message" }, + { value: "created_at", label: "Created at" }, +]; + +export const THREAD_SORT_OPTIONS: ReadonlyArray<{ + readonly value: SidebarThreadSortOrder; + readonly label: string; +}> = [ + { value: "updated_at", label: "Last user message" }, + { value: "created_at", label: "Created at" }, +]; + +export const PROJECT_GROUPING_OPTIONS: ReadonlyArray<{ + readonly value: SidebarProjectGroupingMode; + readonly label: string; + readonly subtitle: string; +}> = [ + { + value: "repository", + label: "Group by repository", + subtitle: "Combine matching repositories across environments", + }, + { + value: "repository_path", + label: "Group by repository path", + subtitle: "Combine only matching paths within a repository", + }, + { + value: "separate", + label: "Keep separate", + subtitle: "Show every project path separately", + }, +]; + +function defaultHomeListOptions(): HomeListOptions { + return { + selectedEnvironmentId: null, + projectSortOrder: + DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual" + ? "updated_at" + : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER, + threadSortOrder: DEFAULT_SIDEBAR_THREAD_SORT_ORDER, + projectGroupingMode: DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE, + }; +} + +interface HomeListOptionsContextValue { + readonly options: HomeListOptions; + readonly setOptions: Dispatch>; +} + +const HomeListOptionsContext = createContext(null); + +/** Keeps list preferences stable while the app moves between compact and split shells. */ +export function HomeListOptionsProvider({ children }: PropsWithChildren) { + const [options, setOptions] = useState(defaultHomeListOptions); + const value = useMemo(() => ({ options, setOptions }), [options]); + return createElement(HomeListOptionsContext, { value }, children); +} + +export function hasCustomHomeListOptions(options: HomeListOptions): boolean { + const defaultProjectSortOrder = + DEFAULT_SIDEBAR_PROJECT_SORT_ORDER === "manual" + ? "updated_at" + : DEFAULT_SIDEBAR_PROJECT_SORT_ORDER; + return ( + options.selectedEnvironmentId !== null || + options.projectSortOrder !== defaultProjectSortOrder || + options.threadSortOrder !== DEFAULT_SIDEBAR_THREAD_SORT_ORDER || + options.projectGroupingMode !== DEFAULT_SIDEBAR_PROJECT_GROUPING_MODE + ); +} + +export function useHomeListOptions(availableEnvironmentIds: ReadonlySet) { + const shared = useContext(HomeListOptionsContext); + const [localOptions, setLocalOptions] = useState(defaultHomeListOptions); + const options = shared?.options ?? localOptions; + const setOptions = shared?.setOptions ?? setLocalOptions; + const selectedEnvironmentId = + options.selectedEnvironmentId !== null && + availableEnvironmentIds.has(options.selectedEnvironmentId) + ? options.selectedEnvironmentId + : null; + const resolvedOptions = + selectedEnvironmentId === options.selectedEnvironmentId + ? options + : { ...options, selectedEnvironmentId }; + + const setSelectedEnvironmentId = useCallback((value: EnvironmentId | null) => { + setOptions((current) => ({ ...current, selectedEnvironmentId: value })); + }, []); + const setProjectSortOrder = useCallback((value: HomeProjectSortOrder) => { + setOptions((current) => ({ ...current, projectSortOrder: value })); + }, []); + const setThreadSortOrder = useCallback((value: SidebarThreadSortOrder) => { + setOptions((current) => ({ ...current, threadSortOrder: value })); + }, []); + const setProjectGroupingMode = useCallback((value: SidebarProjectGroupingMode) => { + setOptions((current) => ({ ...current, projectGroupingMode: value })); + }, []); + + return { + options: resolvedOptions, + setSelectedEnvironmentId, + setProjectSortOrder, + setThreadSortOrder, + setProjectGroupingMode, + } as const; +} diff --git a/apps/mobile/src/features/home/workspace-connection-status.ts b/apps/mobile/src/features/home/workspace-connection-status.ts new file mode 100644 index 00000000000..c9116d060cd --- /dev/null +++ b/apps/mobile/src/features/home/workspace-connection-status.ts @@ -0,0 +1,20 @@ +import type { WorkspaceState } from "../../state/workspaceModel"; + +export function shouldShowWorkspaceConnectionStatus(state: WorkspaceState): boolean { + return ( + state.networkStatus === "offline" || + state.hasConnectingEnvironment || + (state.hasLoadedShellSnapshot && !state.hasReadyEnvironment) + ); +} + +export function workspaceConnectionStatusLabel(state: WorkspaceState): string { + if (state.networkStatus === "offline") return "You are offline"; + if (state.connectingEnvironments.length === 1) { + return `Reconnecting to ${state.connectingEnvironments[0]!.environmentLabel}`; + } + if (state.connectingEnvironments.length > 1) { + return `Reconnecting ${state.connectingEnvironments.length} environments`; + } + return "Not connected"; +} diff --git a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx new file mode 100644 index 00000000000..4db0449ee4d --- /dev/null +++ b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx @@ -0,0 +1,73 @@ +import { usePathname, useRouter } from "expo-router"; +import { useCallback, useMemo, useSyncExternalStore, type PropsWithChildren } from "react"; + +import { + buildThreadFilesRoutePath, + buildThreadReviewRoutePath, + buildThreadTerminalRoutePath, + dismissRoute, +} from "../../lib/routes"; +import { T3KeyboardCommands } from "../../native/T3KeyboardCommands"; +import { + dispatchHardwareKeyboardCommand, + getHardwareKeyboardCommandRegistrationVersion, + getRegisteredHardwareKeyboardCommands, + parseActiveThreadPath, + subscribeToHardwareKeyboardCommandRegistrations, + type HardwareKeyboardCommand, +} from "./hardwareKeyboardCommands"; + +export function HardwareKeyboardCommandProvider({ children }: PropsWithChildren) { + const pathname = usePathname(); + const router = useRouter(); + const registrationVersion = useSyncExternalStore( + subscribeToHardwareKeyboardCommandRegistrations, + getHardwareKeyboardCommandRegistrationVersion, + getHardwareKeyboardCommandRegistrationVersion, + ); + const enabledCommands = useMemo(() => { + const commands = new Set(getRegisteredHardwareKeyboardCommands()); + commands.add("newTask"); + if (pathname !== "/" || router.canGoBack()) commands.add("back"); + if (parseActiveThreadPath(pathname)) { + commands.add("files"); + commands.add("terminal"); + commands.add("review"); + } + return [...commands]; + }, [pathname, registrationVersion, router]); + + const onCommand = useCallback( + (command: HardwareKeyboardCommand) => { + if (dispatchHardwareKeyboardCommand(command)) return; + + if (command === "newTask") { + router.push("/new"); + return; + } + if (command === "back") { + dismissRoute(router); + return; + } + + const thread = parseActiveThreadPath(pathname); + if (!thread) return; + if (command === "files" && !/\/files(?:\/|$)/.test(pathname)) { + router.push(buildThreadFilesRoutePath(thread)); + } + if (command === "terminal" && !/\/terminal(?:\/|$)/.test(pathname)) { + router.push(buildThreadTerminalRoutePath(thread)); + } + if (command === "review" && !/\/review(?:\/|$)/.test(pathname)) { + router.push(buildThreadReviewRoutePath(thread)); + } + }, + [pathname, router], + ); + + return ( + + {children} + + ); +} diff --git a/apps/mobile/src/features/keyboard/hardwareKeyboardCommands.test.ts b/apps/mobile/src/features/keyboard/hardwareKeyboardCommands.test.ts new file mode 100644 index 00000000000..55f1037c010 --- /dev/null +++ b/apps/mobile/src/features/keyboard/hardwareKeyboardCommands.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { parseActiveThreadPath } from "./hardwareKeyboardCommands"; + +describe("parseActiveThreadPath", () => { + it("extracts the active thread from thread subroutes", () => { + expect(parseActiveThreadPath("/threads/environment-1/thread-1/files/src/index.ts")).toEqual({ + environmentId: "environment-1", + threadId: "thread-1", + }); + }); + + it("decodes route components", () => { + expect(parseActiveThreadPath("/threads/local%20machine/thread%2Fone/review")).toEqual({ + environmentId: "local machine", + threadId: "thread/one", + }); + }); + + it("ignores non-thread routes", () => { + expect(parseActiveThreadPath("/settings")).toBeNull(); + expect(parseActiveThreadPath("/threads/environment-only")).toBeNull(); + }); + + it("ignores malformed encoded route components", () => { + expect(parseActiveThreadPath("/threads/%E0%A4%A/thread-1")).toBeNull(); + }); +}); diff --git a/apps/mobile/src/features/keyboard/hardwareKeyboardCommands.ts b/apps/mobile/src/features/keyboard/hardwareKeyboardCommands.ts new file mode 100644 index 00000000000..300434eb736 --- /dev/null +++ b/apps/mobile/src/features/keyboard/hardwareKeyboardCommands.ts @@ -0,0 +1,78 @@ +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { useEffect } from "react"; + +export type HardwareKeyboardCommand = + | "newTask" + | "focusSearch" + | "back" + | "files" + | "terminal" + | "review" + | "toggleSidebar"; + +type CommandHandler = () => boolean | void; + +const handlers = new Map>(); +const registrationListeners = new Set<() => void>(); +let registrationVersion = 0; + +/** + * Registers a context-specific hardware-keyboard action. The most recently mounted handler gets + * the first chance to consume the command, allowing focused screens to override app defaults. + */ +export function useHardwareKeyboardCommand( + command: HardwareKeyboardCommand, + handler: CommandHandler, +): void { + useEffect(() => { + const commandHandlers = handlers.get(command) ?? new Set(); + commandHandlers.add(handler); + handlers.set(command, commandHandlers); + registrationVersion += 1; + registrationListeners.forEach((listener) => listener()); + return () => { + commandHandlers.delete(handler); + if (commandHandlers.size === 0) handlers.delete(command); + registrationVersion += 1; + registrationListeners.forEach((listener) => listener()); + }; + }, [command, handler]); +} + +export function getRegisteredHardwareKeyboardCommands(): ReadonlySet { + return new Set(handlers.keys()); +} + +export function getHardwareKeyboardCommandRegistrationVersion(): number { + return registrationVersion; +} + +export function subscribeToHardwareKeyboardCommandRegistrations(listener: () => void): () => void { + registrationListeners.add(listener); + return () => registrationListeners.delete(listener); +} + +export function dispatchHardwareKeyboardCommand(command: HardwareKeyboardCommand): boolean { + const commandHandlers = handlers.get(command); + if (!commandHandlers) return false; + for (const handler of [...commandHandlers].toReversed()) { + if (handler() !== false) return true; + } + return false; +} + +export function parseActiveThreadPath(pathname: string): { + readonly environmentId: EnvironmentId; + readonly threadId: ThreadId; +} | null { + const match = /^\/threads\/([^/]+)\/([^/]+)(?:\/|$)/.exec(pathname); + if (!match?.[1] || !match[2]) return null; + try { + return { + environmentId: EnvironmentId.make(decodeURIComponent(match[1])), + threadId: ThreadId.make(decodeURIComponent(match[2])), + }; + } catch { + return null; + } +} diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 2dccb7c3571..8fa3675e51e 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,10 +1,27 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useFocusEffect, useGlobalSearchParams, usePathname, useRouter } from "expo-router"; -import { createContext, use, useCallback, useMemo, useRef, useState, type ReactNode } from "react"; +import { + createContext, + use, + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ReactNode, +} from "react"; import { useWindowDimensions, View } from "react-native"; +import Animated, { + Easing, + ReduceMotion, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; import { + constrainPrimarySidebarWidth, deriveFileInspectorPaneLayout, deriveLayout, deriveWorkspacePaneLayout, @@ -16,7 +33,10 @@ import { import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation"; import { buildThreadRoutePath } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; +import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import { HomeListOptionsProvider } from "../home/home-list-options"; import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; +import { WorkspacePaneDivider } from "./workspace-pane-divider"; interface AdaptiveWorkspaceContextValue { readonly layout: Layout; @@ -26,6 +46,7 @@ interface AdaptiveWorkspaceContextValue { readonly showAuxiliaryPane: (role: WorkspaceAuxiliaryPaneRole) => void; readonly toggleAuxiliaryPane: () => void; readonly togglePrimarySidebar: () => void; + readonly setAuxiliaryPaneWidth: (width: number) => void; } const compactLayout = deriveLayout({ width: 0, height: 0 }); @@ -47,6 +68,7 @@ const AdaptiveWorkspaceContext = createContext({ showAuxiliaryPane: () => undefined, toggleAuxiliaryPane: () => undefined, togglePrimarySidebar: () => undefined, + setAuxiliaryPaneWidth: () => undefined, }); function firstRouteParam(value: string | string[] | undefined): string | null { @@ -70,19 +92,47 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const pathname = usePathname(); const router = useRouter(); const activeRoleOwner = useRef(null); + const primarySidebarResizeStartWidth = useRef(0); const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); + const [primarySidebarPreferredWidth, setPrimarySidebarPreferredWidth] = useState( + null, + ); + const [primarySidebarResizing, setPrimarySidebarResizing] = useState(false); const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); + const [supplementaryPanePreferredWidth, setSupplementaryPanePreferredWidth] = useState< + number | null + >(null); const [fileInspectorPreferredVisible, setFileInspectorPreferredVisible] = useState(true); + const [fileInspectorPreferredWidth, setFileInspectorPreferredWidth] = useState( + null, + ); const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = useState(null); const params = useGlobalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); - const layout = useMemo(() => deriveLayout({ width, height }), [height, width]); + const baseLayout = useMemo(() => deriveLayout({ width, height }), [height, width]); + const layout = useMemo(() => { + if (!baseLayout.usesSplitView || baseLayout.listPaneWidth === null) { + return baseLayout; + } + return { + ...baseLayout, + listPaneWidth: constrainPrimarySidebarWidth( + primarySidebarPreferredWidth ?? baseLayout.listPaneWidth, + width, + ), + }; + }, [baseLayout, primarySidebarPreferredWidth, width]); const fileInspector = useMemo( - () => deriveFileInspectorPaneLayout({ layout, viewportWidth: width }), - [layout, width], + () => + deriveFileInspectorPaneLayout({ + layout, + viewportWidth: width, + preferredWidth: fileInspectorPreferredWidth ?? undefined, + }), + [fileInspectorPreferredWidth, layout, width], ); const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole = focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary"); @@ -90,6 +140,10 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) auxiliaryPaneRole === "inspector" ? fileInspectorPreferredVisible : supplementaryPanePreferredVisible; + const auxiliaryPanePreferredWidth = + auxiliaryPaneRole === "inspector" + ? fileInspectorPreferredWidth + : supplementaryPanePreferredWidth; const panes = useMemo( () => deriveWorkspacePaneLayout({ @@ -98,10 +152,12 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) primarySidebarPreferredVisible, auxiliaryPanePreferredVisible, auxiliaryPaneRole, + auxiliaryPanePreferredWidth: auxiliaryPanePreferredWidth ?? undefined, }), [ auxiliaryPanePreferredVisible, auxiliaryPaneRole, + auxiliaryPanePreferredWidth, layout, primarySidebarPreferredVisible, width, @@ -134,6 +190,17 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) } setPrimarySidebarPreferredVisible((current) => !current); }, [panes.primarySidebarSuppressedByAuxiliary, panes.primarySidebarVisible]); + const revealPrimarySidebar = useCallback(() => { + if (panes.primarySidebarSuppressedByAuxiliary) { + setFileInspectorPreferredVisible(false); + } + setPrimarySidebarPreferredVisible(true); + }, [panes.primarySidebarSuppressedByAuxiliary]); + const handleToggleSidebarCommand = useCallback(() => { + togglePrimarySidebar(); + return true; + }, [togglePrimarySidebar]); + useHardwareKeyboardCommand("toggleSidebar", handleToggleSidebarCommand); const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => { if (role === "inspector") { setFileInspectorPreferredVisible(true); @@ -148,6 +215,16 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) } setSupplementaryPanePreferredVisible((current) => !current); }, [auxiliaryPaneRole]); + const setAuxiliaryPaneWidth = useCallback( + (nextWidth: number) => { + if (auxiliaryPaneRole === "inspector") { + setFileInspectorPreferredWidth(nextWidth); + return; + } + setSupplementaryPanePreferredWidth(nextWidth); + }, + [auxiliaryPaneRole], + ); const contextValue = useMemo( () => ({ layout, @@ -157,6 +234,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar, + setAuxiliaryPaneWidth, }), [ activateAuxiliaryPaneRole, @@ -164,6 +242,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) layout, panes, showAuxiliaryPane, + setAuxiliaryPaneWidth, toggleAuxiliaryPane, togglePrimarySidebar, ], @@ -176,6 +255,44 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) router.push("/new"); }, [router]); + const renderedSidebarWidth = useSharedValue( + panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0, + ); + useEffect(() => { + const targetWidth = panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0; + renderedSidebarWidth.value = primarySidebarResizing + ? targetWidth + : withTiming(targetWidth, { + duration: panes.primarySidebarVisible ? 220 : 160, + easing: panes.primarySidebarVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), + reduceMotion: ReduceMotion.System, + }); + }, [ + layout.listPaneWidth, + panes.primarySidebarVisible, + primarySidebarResizing, + renderedSidebarWidth, + ]); + const sidebarAnimatedStyle = useAnimatedStyle(() => ({ + opacity: Math.min(1, renderedSidebarWidth.value / 80), + width: renderedSidebarWidth.value, + })); + const beginPrimarySidebarResize = useCallback(() => { + primarySidebarResizeStartWidth.current = layout.listPaneWidth ?? 0; + setPrimarySidebarResizing(true); + }, [layout.listPaneWidth]); + const resizePrimarySidebarBy = useCallback( + (delta: number) => { + setPrimarySidebarPreferredWidth( + constrainPrimarySidebarWidth(primarySidebarResizeStartWidth.current + delta, width), + ); + }, + [width], + ); + const endPrimarySidebarResize = useCallback(() => { + setPrimarySidebarResizing(false); + }, []); + const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { const destination = buildThreadRoutePath(thread); @@ -206,33 +323,45 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) ); return ( - - - {layout.usesSplitView && layout.listPaneWidth !== null ? ( - - + + + {layout.usesSplitView && layout.listPaneWidth !== null ? ( + + + + ) : null} + {layout.usesSplitView && panes.primarySidebarVisible ? ( + + ) : null} + + {props.children} - ) : null} - - {props.children} - - + + ); } diff --git a/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx b/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx index 4af0e8e3130..9e9f4fd68da 100644 --- a/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx +++ b/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx @@ -1,62 +1,99 @@ -import { useEffect, type ReactNode } from "react"; +import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; import { View } from "react-native"; import Animated, { Easing, + ReduceMotion, useAnimatedStyle, useSharedValue, withTiming, } from "react-native-reanimated"; +import { constrainAuxiliaryPaneWidth } from "../../lib/layout"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; +import { WorkspacePaneDivider } from "./workspace-pane-divider"; export function AdaptiveInspectorLayout(props: { readonly children: ReactNode; readonly renderInspector?: () => ReactNode; }) { - const { panes } = useAdaptiveWorkspaceLayout(); + const { panes, setAuxiliaryPaneWidth } = useAdaptiveWorkspaceLayout(); const inspectorWidth = panes.auxiliaryPaneWidth; const inspectorSupported = props.renderInspector !== undefined && inspectorWidth !== null; const inspectorVisible = inspectorSupported && panes.auxiliaryPaneVisible; + const resizeStartWidth = useRef(0); + const [resizing, setResizing] = useState(false); // A file-to-file replace remounts the route. Initialize an already-visible // inspector at its final position so route replacement never replays an - // entering transition. Only an explicit visibility change animates it. + // entering transition. Only visibility and explicit resizing change it. const inspectorProgress = useSharedValue(inspectorVisible ? 1 : 0); + const renderedInspectorWidth = useSharedValue(inspectorVisible ? (inspectorWidth ?? 0) : 0); useEffect(() => { inspectorProgress.value = withTiming(inspectorVisible ? 1 : 0, { duration: inspectorVisible ? 220 : 160, easing: inspectorVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), + reduceMotion: ReduceMotion.System, }); - }, [inspectorProgress, inspectorVisible]); + const targetWidth = inspectorVisible ? (inspectorWidth ?? 0) : 0; + renderedInspectorWidth.value = resizing + ? targetWidth + : withTiming(targetWidth, { + duration: inspectorVisible ? 220 : 160, + easing: inspectorVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), + reduceMotion: ReduceMotion.System, + }); + }, [inspectorProgress, inspectorVisible, inspectorWidth, renderedInspectorWidth, resizing]); const inspectorStyle = useAnimatedStyle( () => ({ opacity: inspectorProgress.value, transform: [{ translateX: (1 - inspectorProgress.value) * 24 }], + width: renderedInspectorWidth.value, }), [], ); + const beginResize = useCallback(() => { + resizeStartWidth.current = inspectorWidth ?? 0; + setResizing(true); + }, [inspectorWidth]); + const resizeBy = useCallback( + (delta: number) => { + setAuxiliaryPaneWidth( + constrainAuxiliaryPaneWidth({ + preferredWidth: resizeStartWidth.current + delta, + availableWidth: panes.contentPaneWidth, + }), + ); + }, + [panes.contentPaneWidth, setAuxiliaryPaneWidth], + ); + const endResize = useCallback(() => { + setResizing(false); + }, []); return ( {props.children} + {inspectorVisible ? ( + + ) : null} {inspectorSupported ? ( {props.renderInspector?.()} diff --git a/apps/mobile/src/features/layout/workspace-pane-divider.tsx b/apps/mobile/src/features/layout/workspace-pane-divider.tsx new file mode 100644 index 00000000000..3c453e0e58a --- /dev/null +++ b/apps/mobile/src/features/layout/workspace-pane-divider.tsx @@ -0,0 +1,111 @@ +import { useMemo, useRef, useState } from "react"; +import { + PanResponder, + Platform, + PlatformColor, + Pressable, + StyleSheet, + View, + type AccessibilityActionEvent, +} from "react-native"; + +const ACCESSIBILITY_RESIZE_STEP = 24; + +interface WorkspacePaneDividerProps { + readonly accessibilityLabel: string; + readonly currentWidth: number; + /** 1 when dragging right grows the pane, -1 when dragging left grows it. */ + readonly resizeDirection: 1 | -1; + readonly onResizeStart?: () => void; + readonly onResizeBy: (delta: number) => void; + readonly onResizeEnd?: () => void; +} + +/** A forgiving divider target for touch, pointer, and VoiceOver users. */ +export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { + const latestProps = useRef(props); + latestProps.current = props; + const [hovered, setHovered] = useState(false); + const [dragging, setDragging] = useState(false); + const panResponder = useMemo( + () => + PanResponder.create({ + onMoveShouldSetPanResponder: (_event, gesture) => { + const horizontalDistance = Math.abs(gesture.dx); + return horizontalDistance >= 4 && horizontalDistance > Math.abs(gesture.dy) * 1.25; + }, + onPanResponderGrant: () => { + setDragging(true); + latestProps.current.onResizeStart?.(); + }, + onPanResponderMove: (_event, gesture) => { + latestProps.current.onResizeBy(gesture.dx * latestProps.current.resizeDirection); + }, + onPanResponderRelease: () => { + setDragging(false); + latestProps.current.onResizeEnd?.(); + }, + onPanResponderTerminate: () => { + setDragging(false); + latestProps.current.onResizeEnd?.(); + }, + }), + [], + ); + + const handleAccessibilityAction = (event: AccessibilityActionEvent) => { + props.onResizeStart?.(); + if (event.nativeEvent.actionName === "increment") { + props.onResizeBy(ACCESSIBILITY_RESIZE_STEP); + } else if (event.nativeEvent.actionName === "decrement") { + props.onResizeBy(-ACCESSIBILITY_RESIZE_STEP); + } + props.onResizeEnd?.(); + }; + + return ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + style={styles.hitTarget} + > + + + ); +} + +const styles = StyleSheet.create({ + hitTarget: { + alignSelf: "stretch", + cursor: "pointer", + justifyContent: "center", + marginHorizontal: -22, + width: 44, + zIndex: 20, + }, + line: { + alignSelf: "center", + backgroundColor: + Platform.OS === "ios" ? PlatformColor("separator") : "rgba(120, 120, 128, 0.28)", + height: "100%", + opacity: 0.7, + width: StyleSheet.hairlineWidth, + }, + activeLine: { + backgroundColor: Platform.OS === "ios" ? PlatformColor("systemBlueColor") : "#0a84ff", + opacity: 1, + width: 2, + }, +}); diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index a44020ea8db..a48a2cc6cae 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -209,6 +209,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const inputRef = props.editorRef ?? fallbackInputRef; const [isFocused, setIsFocused] = useState(false); const wasExpandedBeforePreviewRef = useRef(false); + const sendInFlightRef = useRef(false); const { onExpandedChange } = props; const [previewImageUri, setPreviewImageUri] = useState(null); @@ -448,9 +449,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer // ── Handle command selection ────────────────────────────── const { onChangeDraftMessage, onUpdateInteractionMode, draftMessage, onSendMessage } = props; - const handleSend = useCallback(() => { - void onSendMessage(); - }, [onSendMessage]); + const handleSend = useCallback(async () => { + if (!canSend || sendInFlightRef.current) return; + sendInFlightRef.current = true; + try { + await onSendMessage(); + } finally { + sendInFlightRef.current = false; + } + }, [canSend, onSendMessage]); const handleCommandSelect = useCallback( (item: ComposerCommandItem) => { if (!composerTrigger) return; @@ -706,6 +713,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer placeholder={props.placeholder} onFocus={handleFocus} onBlur={handleBlur} + onSubmit={handleSend} scrollEnabled={isExpanded} contentInsetVertical={isExpanded ? 0 : 6} style={ diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 83341ccc55d..22ca6cb5d29 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1,20 +1,36 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import { LegendList } from "@legendapp/list/react-native"; +import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "expo-symbols"; +import { useRouter } from "expo-router"; import { memo, useCallback, useMemo, useRef, useState } from "react"; import type { ColorValue } from "react-native"; -import { Pressable, ScrollView, StyleSheet, TextInput, View } from "react-native"; +import { Pressable, StyleSheet, TextInput, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { AppText as Text } from "../../components/AppText"; +import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; +import { useWorkspaceState } from "../../state/workspace"; +import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; +import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import { + hasCustomHomeListOptions, + PROJECT_GROUPING_OPTIONS, + PROJECT_SORT_OPTIONS, + THREAD_SORT_OPTIONS, + useHomeListOptions, +} from "../home/home-list-options"; +import { buildHomeThreadGroups } from "../home/homeThreadList"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { useThreadListActions } from "../home/useThreadListActions"; -import { buildThreadNavigationGroups } from "./thread-navigation-groups"; +import { WorkspaceConnectionStatus } from "../home/WorkspaceConnectionStatus"; +import { shouldShowWorkspaceConnectionStatus } from "../home/workspace-connection-status"; import { SidebarHeaderActions } from "./sidebar-header-actions"; import { threadStatusTone } from "./threadPresentation"; @@ -30,7 +46,10 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly selected: boolean; readonly selectedBackgroundColor: ColorValue; readonly thread: EnvironmentThreadShell; + readonly environmentLabel: string | null; }) { + const iconColor = useThemeColor("--color-icon-muted"); + const [hovered, setHovered] = useState(false); const { backgroundColor, fullSwipeWidth, @@ -43,6 +62,7 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { selected, selectedBackgroundColor, thread, + environmentLabel, } = props; const handleArchive = useCallback(() => { onArchiveThread(thread); @@ -59,6 +79,23 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { }), [handleArchive, thread.title], ); + const threadActions = useMemo( + () => [ + { id: "archive", title: "Archive", image: "archivebox" }, + { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, + ], + [], + ); + const handleMenuAction = useCallback( + ({ nativeEvent }: { readonly nativeEvent: { readonly event: string } }) => { + if (nativeEvent.event === "archive") handleArchive(); + if (nativeEvent.event === "delete") handleDelete(); + }, + [handleArchive, handleDelete], + ); + const subtitle = [environmentLabel, thread.branch].filter((part): part is string => + Boolean(part), + ); return ( {() => ( - onSelectThread(thread)} - style={({ pressed }) => [ + - - - {thread.title} - - - {relativeTime(thread.updatedAt ?? thread.createdAt)} - - - - + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => onSelectThread(thread)} + style={({ pressed }) => [ + styles.threadSelectionTarget, + { + backgroundColor: pressed || hovered ? pressedBackgroundColor : "transparent", + cursor: "pointer", + }, + ]} + > + + + {thread.title} + + + {subtitle.length > 0 ? ( + + {subtitle.join(" · ")} + + ) : null} + + {relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} + + + + + + + [ + styles.moreButton, + { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + ]} + > + + + + )} ); }); +type SidebarListItem = + | { readonly kind: "section"; readonly key: string; readonly title: string } + | { + readonly kind: "thread"; + readonly key: string; + readonly thread: EnvironmentThreadShell; + }; + export function ThreadNavigationSidebar(props: { readonly width: number; + readonly visible: boolean; readonly selectedThreadKey: string | null; readonly onOpenSettings: () => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; readonly onStartNewTask: () => void; + readonly onRequestVisibility: () => void; }) { const insets = useSafeAreaInsets(); + const router = useRouter(); const projects = useProjects(); const threads = useThreadShells(); + const { state: catalogState } = useWorkspaceState(); + const { savedConnectionsById } = useSavedRemoteConnections(); const [searchQuery, setSearchQuery] = useState(""); + const searchInputRef = useRef(null); const openSwipeableRef = useRef(null); const { archiveThread, confirmDeleteThread } = useThreadListActions(); + const environments = useMemo( + () => + Object.values(savedConnectionsById) + .map((connection) => ({ + environmentId: connection.environmentId, + label: connection.environmentLabel, + })) + .sort((left, right) => left.label.localeCompare(right.label)), + [savedConnectionsById], + ); + const availableEnvironmentIds = useMemo( + () => new Set(environments.map((environment) => environment.environmentId)), + [environments], + ); + const { + options, + setSelectedEnvironmentId, + setProjectGroupingMode, + setProjectSortOrder, + setThreadSortOrder, + } = useHomeListOptions(availableEnvironmentIds); const groups = useMemo( - () => buildThreadNavigationGroups({ projects, threads, searchQuery }), - [projects, searchQuery, threads], + () => + buildHomeThreadGroups({ + projects, + threads, + environmentId: options.selectedEnvironmentId, + searchQuery, + projectSortOrder: options.projectSortOrder, + threadSortOrder: options.threadSortOrder, + projectGroupingMode: options.projectGroupingMode, + }), + [options, projects, searchQuery, threads], + ); + const listItems = useMemo>( + () => + groups.flatMap((group) => [ + { kind: "section" as const, key: `section:${group.key}`, title: group.title }, + ...group.threads.map((thread) => ({ + kind: "thread" as const, + key: scopedThreadKey(thread.environmentId, thread.id), + thread, + })), + ]), + [groups], + ); + const listMenuActions = useMemo( + () => [ + { + id: "environment", + title: "Environment", + subactions: [ + { + id: "environment:all", + title: "All environments", + subtitle: "Show threads from every environment", + state: options.selectedEnvironmentId === null ? "on" : "off", + }, + ...environments.map((environment) => ({ + id: `environment:${environment.environmentId}`, + title: environment.label, + state: + options.selectedEnvironmentId === environment.environmentId + ? ("on" as const) + : ("off" as const), + })), + ], + }, + { + id: "project-sort", + title: "Sort projects", + subactions: PROJECT_SORT_OPTIONS.map((option) => ({ + id: `project-sort:${option.value}`, + title: option.label, + state: options.projectSortOrder === option.value ? "on" : "off", + })), + }, + { + id: "thread-sort", + title: "Sort threads", + subactions: THREAD_SORT_OPTIONS.map((option) => ({ + id: `thread-sort:${option.value}`, + title: option.label, + state: options.threadSortOrder === option.value ? "on" : "off", + })), + }, + { + id: "project-grouping", + title: "Group projects", + subactions: PROJECT_GROUPING_OPTIONS.map((option) => ({ + id: `project-grouping:${option.value}`, + title: option.label, + subtitle: option.subtitle, + state: options.projectGroupingMode === option.value ? "on" : "off", + })), + }, + ], + [environments, options], + ); + const handleListMenuAction = useCallback( + ({ nativeEvent }: { readonly nativeEvent: { readonly event: string } }) => { + const event = nativeEvent.event; + if (event === "environment:all") { + setSelectedEnvironmentId(null); + return; + } + if (event.startsWith("environment:")) { + const environment = environments.find( + (candidate) => String(candidate.environmentId) === event.slice("environment:".length), + ); + if (environment) setSelectedEnvironmentId(environment.environmentId); + return; + } + const projectSort = PROJECT_SORT_OPTIONS.find( + (option) => `project-sort:${option.value}` === event, + ); + if (projectSort) { + setProjectSortOrder(projectSort.value); + return; + } + const threadSort = THREAD_SORT_OPTIONS.find( + (option) => `thread-sort:${option.value}` === event, + ); + if (threadSort) { + setThreadSortOrder(threadSort.value); + return; + } + const grouping = PROJECT_GROUPING_OPTIONS.find( + (option) => `project-grouping:${option.value}` === event, + ); + if (grouping) setProjectGroupingMode(grouping.value); + }, + [ + environments, + setProjectGroupingMode, + setProjectSortOrder, + setSelectedEnvironmentId, + setThreadSortOrder, + ], ); const backgroundColor = useThemeColor("--color-drawer"); @@ -148,6 +361,63 @@ export function ThreadNavigationSidebar(props: { }, [props.onSelectThread], ); + const focusSearch = useCallback(() => { + if (!props.visible) { + props.onRequestVisibility(); + setTimeout(() => searchInputRef.current?.focus(), 240); + } else { + searchInputRef.current?.focus(); + } + return true; + }, [props.onRequestVisibility, props.visible]); + useHardwareKeyboardCommand("focusSearch", focusSearch); + const renderListItem = useCallback( + ({ item }: { readonly item: SidebarListItem }) => { + if (item.kind === "section") { + return ( + + {item.title} + + ); + } + const thread = item.thread; + return ( + + + + ); + }, + [ + archiveThread, + backgroundColor, + confirmDeleteThread, + handleSelectThread, + handleSwipeableClose, + handleSwipeableWillOpen, + pressedBackgroundColor, + props.selectedThreadKey, + props.width, + savedConnectionsById, + selectedBackgroundColor, + ], + ); return ( Threads + + [ + styles.headerButton, + { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + ]} + > + + + + {shouldShowWorkspaceConnectionStatus(catalogState) ? ( + + router.push("/settings/environments")} + state={catalogState} + variant="sidebar" + /> + + ) : null} + - item.kind} + keyExtractor={(item) => item.key} + renderItem={renderListItem} contentContainerStyle={styles.threadListContent} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" onScrollBeginDrag={() => openSwipeableRef.current?.close()} showsVerticalScrollIndicator={false} style={styles.threadList} - > - {groups.length === 0 ? ( + ListEmptyComponent={ {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} - ) : ( - groups.map((group) => ( - - - {group.title} - - - {group.threads.length === 0 ? ( - No threads yet - ) : ( - group.threads.map((thread) => { - const threadKey = scopedThreadKey(thread.environmentId, thread.id); - const selected = threadKey === props.selectedThreadKey; - - return ( - - ); - }) - )} - - )) - )} - + } + /> ); @@ -255,6 +528,17 @@ const styles = StyleSheet.create({ alignItems: "center", gap: 2, }, + headerButton: { + width: 38, + height: 38, + borderRadius: 11, + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + }, + connectionStatus: { + paddingTop: 10, + }, searchField: { height: 36, marginTop: 6, @@ -278,18 +562,32 @@ const styles = StyleSheet.create({ flex: 1, }, threadListContent: { - gap: 18, paddingHorizontal: 10, paddingTop: 16, paddingBottom: 16, }, - section: { - gap: 4, + sectionTitle: { + paddingHorizontal: 8, + paddingBottom: 4, + paddingTop: 14, + }, + threadItem: { + paddingBottom: 4, }, threadRow: { minHeight: 58, borderRadius: 10, - paddingHorizontal: 10, + flexDirection: "row", + alignItems: "center", + paddingRight: 6, + }, + threadSelectionTarget: { + minWidth: 0, + flex: 1, + alignSelf: "stretch", + borderRadius: 10, + paddingLeft: 10, + paddingRight: 4, paddingVertical: 8, flexDirection: "row", alignItems: "center", @@ -304,4 +602,18 @@ const styles = StyleSheet.create({ flex: 1, gap: 2, }, + threadMetadata: { + minWidth: 0, + flexDirection: "row", + alignItems: "center", + gap: 6, + }, + moreButton: { + width: 30, + height: 30, + borderRadius: 9, + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + }, }); diff --git a/apps/mobile/src/lib/layout.test.ts b/apps/mobile/src/lib/layout.test.ts index 58fd95c3e89..a61a1ba40b9 100644 --- a/apps/mobile/src/lib/layout.test.ts +++ b/apps/mobile/src/lib/layout.test.ts @@ -1,6 +1,8 @@ import { describe, expect, it } from "vite-plus/test"; import { + constrainAuxiliaryPaneWidth, + constrainPrimarySidebarWidth, deriveCenteredContentHorizontalPadding, deriveFileInspectorPaneLayout, deriveLayout, @@ -10,6 +12,20 @@ import { SPLIT_LAYOUT_MIN_WIDTH, } from "./layout"; +describe("resizable pane constraints", () => { + it("keeps a preferred sidebar width across large windows and clamps it in a narrow split view", () => { + expect(constrainPrimarySidebarWidth(430, 1_366)).toBe(430); + expect(constrainPrimarySidebarWidth(430, 744)).toBe(384); + expect(constrainPrimarySidebarWidth(100, 1_366)).toBe(280); + }); + + it("preserves a useful main pane while constraining a trailing pane", () => { + expect(constrainAuxiliaryPaneWidth({ preferredWidth: 440, availableWidth: 1_100 })).toBe(440); + expect(constrainAuxiliaryPaneWidth({ preferredWidth: 440, availableWidth: 900 })).toBe(340); + expect(constrainAuxiliaryPaneWidth({ preferredWidth: 100, availableWidth: 1_100 })).toBe(260); + }); +}); + describe("deriveCenteredContentHorizontalPadding", () => { it("keeps the minimum padding while the viewport fits the reading width", () => { expect( @@ -232,6 +248,39 @@ describe("deriveWorkspacePaneLayout", () => { }); }); + it("uses a preferred inspector width when all three panes still fit", () => { + const layout = deriveLayout({ width: 1_366, height: 1_024 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_366, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + auxiliaryPaneRole: "inspector", + auxiliaryPanePreferredWidth: 420, + }), + ).toMatchObject({ + primarySidebarVisible: true, + auxiliaryPaneVisible: true, + auxiliaryPaneWidth: 420, + }); + }); + + it("clamps a preferred supplementary width before squeezing the main pane", () => { + const layout = deriveLayout({ width: 1_366, height: 1_024 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_366, + primarySidebarPreferredVisible: true, + auxiliaryPanePreferredVisible: true, + auxiliaryPanePreferredWidth: 460, + }).auxiliaryPaneWidth, + ).toBe(426); + }); + it("respects a hidden auxiliary-pane preference", () => { const layout = deriveLayout({ width: 1_366, height: 1_024 }); diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index dab90a79ac5..44d0470116e 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -12,14 +12,16 @@ function clamp(value: number, min: number, max: number): number { export const SPLIT_LAYOUT_MIN_WIDTH = 720; export const SPLIT_LAYOUT_MIN_HEIGHT = 600; -const SPLIT_SIDEBAR_MIN_WIDTH = 280; -const SPLIT_SIDEBAR_MAX_WIDTH = 380; +export const SPLIT_SIDEBAR_MIN_WIDTH = 280; +export const SPLIT_SIDEBAR_MAX_WIDTH = 460; +const SPLIT_SIDEBAR_DEFAULT_MAX_WIDTH = 380; export const AUXILIARY_PANE_MIN_CONTENT_WIDTH = 960; export const CHAT_CONTENT_MAX_WIDTH = 960; -const AUXILIARY_PANE_MIN_WIDTH = 260; -const AUXILIARY_PANE_MAX_WIDTH = 320; +export const AUXILIARY_PANE_MIN_WIDTH = 260; +export const AUXILIARY_PANE_MAX_WIDTH = 480; +const AUXILIARY_PANE_DEFAULT_MAX_WIDTH = 320; const FILE_INSPECTOR_MIN_VIEWPORT_WIDTH = 820; const FILE_INSPECTOR_MIN_MAIN_WIDTH = 560; const STABLE_FORM_SHEET_MAX_HEIGHT = 720; @@ -71,7 +73,7 @@ export function deriveLayout(input: { readonly width: number; readonly height: n listPaneWidth: clamp( Math.round(width * 0.32), SPLIT_SIDEBAR_MIN_WIDTH, - SPLIT_SIDEBAR_MAX_WIDTH, + SPLIT_SIDEBAR_DEFAULT_MAX_WIDTH, ), shellPadding: 0, }; @@ -83,6 +85,7 @@ export function deriveWorkspacePaneLayout(input: { readonly primarySidebarPreferredVisible: boolean; readonly auxiliaryPanePreferredVisible: boolean; readonly auxiliaryPaneRole?: WorkspaceAuxiliaryPaneRole; + readonly auxiliaryPanePreferredWidth?: number; }): WorkspacePaneLayout { const viewportWidth = Math.max(0, input.viewportWidth); const auxiliaryPaneRole = input.auxiliaryPaneRole ?? "supplementary"; @@ -96,6 +99,7 @@ export function deriveWorkspacePaneLayout(input: { const fileInspector = deriveFileInspectorPaneLayout({ layout: input.layout, viewportWidth, + preferredWidth: input.auxiliaryPanePreferredWidth, }); const auxiliaryPaneVisible = fileInspector.supported && input.auxiliaryPanePreferredVisible; const primarySidebarSuppressedByAuxiliary = @@ -122,6 +126,11 @@ export function deriveWorkspacePaneLayout(input: { const supportsAuxiliaryPane = input.layout.usesSplitView && contentPaneWidth >= AUXILIARY_PANE_MIN_CONTENT_WIDTH; const auxiliaryPaneVisible = supportsAuxiliaryPane && input.auxiliaryPanePreferredVisible; + const defaultAuxiliaryPaneWidth = clamp( + Math.round(contentPaneWidth * 0.28), + AUXILIARY_PANE_MIN_WIDTH, + AUXILIARY_PANE_DEFAULT_MAX_WIDTH, + ); return { primarySidebarVisible: preferredPrimarySidebarVisible, @@ -130,11 +139,10 @@ export function deriveWorkspacePaneLayout(input: { supportsAuxiliaryPane, auxiliaryPaneVisible, auxiliaryPaneWidth: supportsAuxiliaryPane - ? clamp( - Math.round(contentPaneWidth * 0.28), - AUXILIARY_PANE_MIN_WIDTH, - AUXILIARY_PANE_MAX_WIDTH, - ) + ? constrainAuxiliaryPaneWidth({ + preferredWidth: input.auxiliaryPanePreferredWidth ?? defaultAuxiliaryPaneWidth, + availableWidth: contentPaneWidth, + }) : null, }; } @@ -142,6 +150,7 @@ export function deriveWorkspacePaneLayout(input: { export function deriveFileInspectorPaneLayout(input: { readonly layout: Layout; readonly viewportWidth: number; + readonly preferredWidth?: number; }): FileInspectorPaneLayout { const viewportWidth = Math.max(0, input.viewportWidth); const supported = @@ -150,11 +159,57 @@ export function deriveFileInspectorPaneLayout(input: { return { supported, width: supported - ? clamp(Math.round(viewportWidth * 0.28), AUXILIARY_PANE_MIN_WIDTH, AUXILIARY_PANE_MAX_WIDTH) + ? constrainAuxiliaryPaneWidth({ + preferredWidth: + input.preferredWidth ?? + clamp( + Math.round(viewportWidth * 0.28), + AUXILIARY_PANE_MIN_WIDTH, + AUXILIARY_PANE_DEFAULT_MAX_WIDTH, + ), + availableWidth: viewportWidth, + }) : null, }; } +/** Keep a user-selected sidebar width useful as a window is resized. */ +export function constrainPrimarySidebarWidth( + preferredWidth: number, + viewportWidth = Number.POSITIVE_INFINITY, +): number { + const safeWidth = Number.isFinite(preferredWidth) ? preferredWidth : SPLIT_SIDEBAR_MIN_WIDTH; + const viewportMax = Number.isFinite(viewportWidth) + ? Math.max(SPLIT_SIDEBAR_MIN_WIDTH, viewportWidth - 360) + : SPLIT_SIDEBAR_MAX_WIDTH; + return clamp( + Math.round(safeWidth), + SPLIT_SIDEBAR_MIN_WIDTH, + Math.min(SPLIT_SIDEBAR_MAX_WIDTH, viewportMax), + ); +} + +/** + * Keep an auxiliary pane within native-feeling bounds without squeezing its + * neighboring content below a usable reading/editor width. + */ +export function constrainAuxiliaryPaneWidth(input: { + readonly preferredWidth: number; + readonly availableWidth: number; +}): number { + const safePreferredWidth = Number.isFinite(input.preferredWidth) + ? input.preferredWidth + : AUXILIARY_PANE_MIN_WIDTH; + const availableWidth = Number.isFinite(input.availableWidth) + ? Math.max(0, input.availableWidth) + : 0; + const maxWidth = Math.max( + AUXILIARY_PANE_MIN_WIDTH, + Math.min(AUXILIARY_PANE_MAX_WIDTH, availableWidth - FILE_INSPECTOR_MIN_MAIN_WIDTH), + ); + return clamp(Math.round(safePreferredWidth), AUXILIARY_PANE_MIN_WIDTH, maxWidth); +} + export function deriveCenteredContentHorizontalPadding(input: { readonly viewportWidth: number; readonly maxContentWidth: number | null; diff --git a/apps/mobile/src/native/T3ComposerEditor.ios.tsx b/apps/mobile/src/native/T3ComposerEditor.ios.tsx index eb935030e81..7425e7da228 100644 --- a/apps/mobile/src/native/T3ComposerEditor.ios.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.ios.tsx @@ -69,6 +69,7 @@ interface NativeComposerEditorProps extends ViewProps { readonly onComposerPasteImages?: (event: NativePasteImagesEvent) => void; readonly onComposerFocus?: () => void; readonly onComposerBlur?: () => void; + readonly onComposerSubmit?: () => void; } const NativeView = requireNativeView(NATIVE_MODULE_NAME); @@ -93,6 +94,7 @@ export function ComposerEditor({ onPasteImages, onFocus, onBlur, + onSubmit, contentInsetVertical = 0, ...props }: ComposerEditorProps) { @@ -270,6 +272,7 @@ export function ComposerEditor({ onComposerPasteImages={(event) => onPasteImages?.(event.nativeEvent.uris)} onComposerFocus={onFocus} onComposerBlur={onBlur} + onComposerSubmit={onSubmit} /> ); } diff --git a/apps/mobile/src/native/T3ComposerEditor.types.ts b/apps/mobile/src/native/T3ComposerEditor.types.ts index d70d63fa437..f7760c395ea 100644 --- a/apps/mobile/src/native/T3ComposerEditor.types.ts +++ b/apps/mobile/src/native/T3ComposerEditor.types.ts @@ -35,4 +35,6 @@ export interface ComposerEditorProps { readonly onPasteImages?: (uris: ReadonlyArray) => void; readonly onFocus?: () => void; readonly onBlur?: () => void; + /** Invoked by the native editor when Command-Return is pressed on a hardware keyboard. */ + readonly onSubmit?: () => void; } diff --git a/apps/mobile/src/native/T3KeyboardCommands.ios.tsx b/apps/mobile/src/native/T3KeyboardCommands.ios.tsx new file mode 100644 index 00000000000..ff4e817c200 --- /dev/null +++ b/apps/mobile/src/native/T3KeyboardCommands.ios.tsx @@ -0,0 +1,31 @@ +import { requireNativeView } from "expo"; +import type { PropsWithChildren } from "react"; +import type { NativeSyntheticEvent, ViewProps } from "react-native"; + +import type { HardwareKeyboardCommand } from "../features/keyboard/hardwareKeyboardCommands"; + +interface NativeKeyboardCommandsProps extends ViewProps, PropsWithChildren { + readonly enabledCommands: ReadonlyArray; + readonly onCommand: ( + event: NativeSyntheticEvent<{ readonly command: HardwareKeyboardCommand }>, + ) => void; +} + +const NativeKeyboardCommands = requireNativeView("T3KeyboardCommands"); + +export function T3KeyboardCommands( + props: PropsWithChildren<{ + readonly enabledCommands: ReadonlyArray; + readonly onCommand: (command: HardwareKeyboardCommand) => void; + }>, +) { + return ( + props.onCommand(event.nativeEvent.command)} + enabledCommands={props.enabledCommands} + style={{ flex: 1 }} + > + {props.children} + + ); +} diff --git a/apps/mobile/src/native/T3KeyboardCommands.tsx b/apps/mobile/src/native/T3KeyboardCommands.tsx new file mode 100644 index 00000000000..87629e6d676 --- /dev/null +++ b/apps/mobile/src/native/T3KeyboardCommands.tsx @@ -0,0 +1,13 @@ +import type { PropsWithChildren } from "react"; +import { View } from "react-native"; + +import type { HardwareKeyboardCommand } from "../features/keyboard/hardwareKeyboardCommands"; + +export function T3KeyboardCommands( + props: PropsWithChildren<{ + readonly enabledCommands: ReadonlyArray; + readonly onCommand: (command: HardwareKeyboardCommand) => void; + }>, +) { + return {props.children}; +} From c4e63e064804dc5cc24ab76e86856370378ec3c7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 22:01:12 -0700 Subject: [PATCH 09/71] Fix EAS iOS CocoaPods UUID cache --- apps/mobile/app.config.ts | 1 + .../plugins/withIosCocoaPodsUuidCache.cjs | 52 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 apps/mobile/plugins/withIosCocoaPodsUuidCache.cjs diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 074233a6c8a..1dd884aeb8d 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -147,6 +147,7 @@ const config: ExpoConfig = { }, }, ], + "./plugins/withIosCocoaPodsUuidCache.cjs", [ "expo-widgets", { diff --git a/apps/mobile/plugins/withIosCocoaPodsUuidCache.cjs b/apps/mobile/plugins/withIosCocoaPodsUuidCache.cjs new file mode 100644 index 00000000000..e0de313bbd4 --- /dev/null +++ b/apps/mobile/plugins/withIosCocoaPodsUuidCache.cjs @@ -0,0 +1,52 @@ +const fs = require("node:fs"); +const path = require("node:path"); + +const { withDangerousMod } = require("expo/config-plugins"); + +const MARKER = "# t3code: repair cached CocoaPods UUID allocation before SPM integration"; +const UUID_REPAIR = `${MARKER} + pods_project = installer.pods_project + existing_uuids = pods_project.objects.map(&:uuid) + uuid_prefix = pods_project.instance_variable_get(:@uuid_prefix)[0, 6] + sequential_uuid = /\\A#{Regexp.escape(uuid_prefix)}([0-9A-F]{7})0\\z/ + highest_index = existing_uuids.filter_map do |uuid| + match = sequential_uuid.match(uuid) + match && match[1].to_i(16) + end.max || -1 + + # Pod::Project generates sequential UUIDs without collision checks because CocoaPods + # normally creates the project in this process. EAS can restore Pods.xcodeproj from + # cache, leaving the allocator behind the loaded objects. React Native's SPM support + # then reuses an existing UUID and silently corrupts the project graph. + next_index = highest_index + 1 + pods_project.instance_variable_set(:@generated_uuids, Array.new(next_index)) + pods_project.instance_variable_set(:@available_uuids, []) + pods_project.generate_available_uuid_list(1_000) + Pod::UI.puts "T3Code: reset CocoaPods UUID allocator at #{next_index} (#{existing_uuids.length} existing objects)" +`; + +module.exports = function withIosCocoaPodsUuidCache(config) { + return withDangerousMod(config, [ + "ios", + (nextConfig) => { + const podfilePath = path.join(nextConfig.modRequest.platformProjectRoot, "Podfile"); + const podfile = fs.readFileSync(podfilePath, "utf8"); + + if (podfile.includes(MARKER)) { + return nextConfig; + } + + const postInstallStart = "post_install do |installer|\n"; + if (!podfile.includes(postInstallStart)) { + throw new Error("Unable to repair CocoaPods UUID allocation: post_install is missing."); + } + + fs.writeFileSync( + podfilePath, + podfile.replace(postInstallStart, `${postInstallStart}${UUID_REPAIR}`), + "utf8", + ); + return nextConfig; + }, + ]); +}; From 7f81813feb1d76394445d4ee44340e492e6abe90 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:15:16 -0700 Subject: [PATCH 10/71] Polish iPad mobile layout and native headers --- .../t3-terminal/ios/T3TerminalModule.swift | 4 + .../t3-terminal/ios/T3TerminalView.swift | 38 ++ apps/mobile/src/features/home/HomeScreen.tsx | 205 ++++++----- .../features/home/thread-swipe-actions.tsx | 7 +- .../layout/AdaptiveWorkspaceLayout.tsx | 64 +--- .../layout/workspace-pane-divider.tsx | 95 ++--- .../terminal/NativeTerminalSurface.tsx | 40 +-- .../features/terminal/nativeTerminalModule.ts | 1 + .../features/threads/ThreadDetailScreen.tsx | 2 + .../src/features/threads/ThreadFeed.tsx | 132 ++++--- .../threads/ThreadNavigationSidebar.tsx | 339 ++++++++++++------ .../features/threads/ThreadRouteScreen.tsx | 53 ++- .../threads/sidebar-filter-button.ios.tsx | 22 ++ .../threads/sidebar-filter-button.tsx | 41 +++ apps/mobile/src/native/T3HeaderButton.ios.tsx | 6 +- patches/react-native-screens@4.25.2.patch | 190 ++++++++++ pnpm-lock.yaml | 17 +- pnpm-workspace.yaml | 1 + 18 files changed, 847 insertions(+), 410 deletions(-) create mode 100644 apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx create mode 100644 apps/mobile/src/features/threads/sidebar-filter-button.tsx create mode 100644 patches/react-native-screens@4.25.2.patch diff --git a/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift b/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift index 8f35bcc3aa2..c4eacb311d4 100644 --- a/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift +++ b/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift @@ -17,6 +17,10 @@ public class T3TerminalModule: Module { view.fontSize = CGFloat(fontSize) } + Prop("focusRequest") { (view: T3TerminalView, focusRequest: Double) in + view.focusRequest = focusRequest + } + Prop("appearanceScheme") { (view: T3TerminalView, appearanceScheme: String) in view.appearanceScheme = appearanceScheme } diff --git a/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift b/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift index 685c2642a1f..0d63ddf6b0e 100644 --- a/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift +++ b/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift @@ -24,11 +24,37 @@ private enum GhosttyRuntime { private final class TerminalInputField: UITextField { var onDeleteBackward: (() -> Void)? + var onInsert: ((String) -> Void)? + + override var keyCommands: [UIKeyCommand]? { + [ + command(input: "\t", modifiers: UIKeyModifierFlags(), action: #selector(handleTab)), + command(input: "\t", modifiers: .shift, action: #selector(handleBackTab)), + ] + } override func deleteBackward() { onDeleteBackward?() super.deleteBackward() } + + private func command( + input: String, + modifiers: UIKeyModifierFlags, + action: Selector + ) -> UIKeyCommand { + let command = UIKeyCommand(input: input, modifierFlags: modifiers, action: action) + command.wantsPriorityOverSystemBehavior = true + return command + } + + @objc private func handleTab() { + onInsert?("\t") + } + + @objc private func handleBackTab() { + onInsert?("\u{1B}[Z") + } } private enum TerminalAppearanceScheme: String { @@ -108,6 +134,15 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate { } } + var focusRequest: Double = 0 { + didSet { + guard oldValue != focusRequest else { return } + DispatchQueue.main.async { [weak self] in + self?.requestKeyboardFocus() + } + } + } + var appearanceScheme: String = TerminalAppearanceScheme.dark.rawValue { didSet { guard oldValue != appearanceScheme else { return } @@ -167,6 +202,9 @@ public final class T3TerminalView: ExpoView, UITextFieldDelegate { inputField.onDeleteBackward = { [weak self] in self?.emitInput("\u{7F}") } + inputField.onInsert = { [weak self] data in + self?.emitInput(data) + } focusTapGesture.addTarget(self, action: #selector(handleViewportTap)) terminalViewport.addGestureRecognizer(focusTapGesture) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index d38ae93158a..59a002d18a9 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -8,8 +8,9 @@ import type { SidebarThreadSortOrder, } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; import { ActivityIndicator, Pressable, ScrollView, useWindowDimensions, View } from "react-native"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import Animated, { Easing, @@ -212,6 +213,9 @@ function ThreadRow(props: { readonly onDelete: () => void; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; + readonly simultaneousSwipeGesture?: ComponentProps< + typeof ThreadSwipeable + >["simultaneousWithExternalGesture"]; readonly isLast: boolean; }) { const { width: windowWidth } = useWindowDimensions(); @@ -241,6 +245,7 @@ function ThreadRow(props: { label: "Archive", onPress: props.onArchive, }} + simultaneousWithExternalGesture={props.simultaneousSwipeGesture} threadTitle={props.thread.title} > {(close) => ( @@ -336,6 +341,7 @@ function ThreadRow(props: { export function HomeScreen(props: HomeScreenProps) { const [expandedProjects, setExpandedProjects] = useState>(() => new Set()); const openSwipeableRef = useRef(null); + const homeScrollGesture = useMemo(() => Gesture.Native(), []); const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); @@ -400,104 +406,111 @@ export function HomeScreen(props: HomeScreenProps) { return ( - openSwipeableRef.current?.close()} - className="flex-1" - contentContainerStyle={{ - paddingHorizontal: 16, - paddingTop: 8, - paddingBottom: 24, - gap: 20, - }} - > - {!hasAnyThreads ? ( - + + openSwipeableRef.current?.close()} + className="flex-1" + contentContainerStyle={{ + paddingHorizontal: 16, + paddingTop: 8, + paddingBottom: 24, + gap: 20, + }} + > + {!hasAnyThreads ? ( + + + {emptyState.loading ? ( + + + + ) : null} + + ) : !hasResults && hasSearchQuery ? ( + + ) : !hasResults && selectedEnvironmentLabel ? ( - {emptyState.loading ? ( - - - - ) : null} - - ) : !hasResults && hasSearchQuery ? ( - - ) : !hasResults && selectedEnvironmentLabel ? ( - - ) : !hasResults ? ( - - ) : ( - projectGroups.map((group) => { - const isExpanded = expandedProjects.has(group.key); - const visibleThreads = isExpanded - ? group.threads - : group.threads.slice(0, COLLAPSED_THREAD_LIMIT); - - return ( - - toggleExpanded(group.key)} - project={group.representative} - title={group.title} - totalThreadCount={group.threads.length} - /> - + ) : ( + projectGroups.map((group) => { + const isExpanded = expandedProjects.has(group.key); + const visibleThreads = isExpanded + ? group.threads + : group.threads.slice(0, COLLAPSED_THREAD_LIMIT); + + return ( + - {visibleThreads.map((thread, i) => { - const threadKey = `${thread.environmentId}:${thread.id}`; - return ( - - props.onArchiveThread(thread)} - onDelete={() => props.onDeleteThread(thread)} - onPress={() => props.onSelectThread(thread)} - onSwipeableClose={handleSwipeableClose} - onSwipeableWillOpen={handleSwipeableWillOpen} - /> - - ); - })} - - - ); - }) - )} - + toggleExpanded(group.key)} + project={group.representative} + title={group.title} + totalThreadCount={group.threads.length} + /> + + {visibleThreads.map((thread, i) => { + const threadKey = `${thread.environmentId}:${thread.id}`; + return ( + + props.onArchiveThread(thread)} + onDelete={() => props.onDeleteThread(thread)} + onPress={() => props.onSelectThread(thread)} + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + simultaneousSwipeGesture={homeScrollGesture} + /> + + ); + })} + + + ); + }) + )} + + {shouldShowConnectionStatus ? ( void) => ReactNode; readonly containerStyle?: StyleProp; + readonly enableTrackpadSwipe?: boolean; readonly fullSwipeWidth: number; readonly onDelete: () => void; readonly onSwipeableClose?: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen?: (methods: SwipeableMethods) => void; readonly primaryAction: ThreadSwipePrimaryAction; + readonly simultaneousWithExternalGesture?: ComponentProps< + typeof ReanimatedSwipeable + >["simultaneousWithExternalGesture"]; readonly threadTitle: string; }) { const swipeableRef = useRef(null); @@ -65,7 +69,7 @@ export function ThreadSwipeable(props: { childrenContainerStyle={{ backgroundColor: props.backgroundColor }} containerStyle={[{ backgroundColor: props.backgroundColor }, props.containerStyle]} dragOffsetFromRightEdge={8} - enableTrackpadTwoFingerGesture + enableTrackpadTwoFingerGesture={props.enableTrackpadSwipe ?? true} friction={1} onSwipeableClose={() => { fullSwipeArmedRef.current = false; @@ -112,6 +116,7 @@ export function ThreadSwipeable(props: { /> )} rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} + simultaneousWithExternalGesture={props.simultaneousWithExternalGesture} > {props.children(close)} diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 8fa3675e51e..88821ded481 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -21,7 +21,6 @@ import Animated, { } from "react-native-reanimated"; import { - constrainPrimarySidebarWidth, deriveFileInspectorPaneLayout, deriveLayout, deriveWorkspacePaneLayout, @@ -36,7 +35,6 @@ import { scopedThreadKey } from "../../lib/scopedEntities"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { HomeListOptionsProvider } from "../home/home-list-options"; import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; -import { WorkspacePaneDivider } from "./workspace-pane-divider"; interface AdaptiveWorkspaceContextValue { readonly layout: Layout; @@ -92,12 +90,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const pathname = usePathname(); const router = useRouter(); const activeRoleOwner = useRef(null); - const primarySidebarResizeStartWidth = useRef(0); const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); - const [primarySidebarPreferredWidth, setPrimarySidebarPreferredWidth] = useState( - null, - ); - const [primarySidebarResizing, setPrimarySidebarResizing] = useState(false); const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); const [supplementaryPanePreferredWidth, setSupplementaryPanePreferredWidth] = useState< number | null @@ -113,18 +106,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) threadId?: string | string[]; }>(); const baseLayout = useMemo(() => deriveLayout({ width, height }), [height, width]); - const layout = useMemo(() => { - if (!baseLayout.usesSplitView || baseLayout.listPaneWidth === null) { - return baseLayout; - } - return { - ...baseLayout, - listPaneWidth: constrainPrimarySidebarWidth( - primarySidebarPreferredWidth ?? baseLayout.listPaneWidth, - width, - ), - }; - }, [baseLayout, primarySidebarPreferredWidth, width]); + const layout = baseLayout; const fileInspector = useMemo( () => deriveFileInspectorPaneLayout({ @@ -260,38 +242,16 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) ); useEffect(() => { const targetWidth = panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0; - renderedSidebarWidth.value = primarySidebarResizing - ? targetWidth - : withTiming(targetWidth, { - duration: panes.primarySidebarVisible ? 220 : 160, - easing: panes.primarySidebarVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), - reduceMotion: ReduceMotion.System, - }); - }, [ - layout.listPaneWidth, - panes.primarySidebarVisible, - primarySidebarResizing, - renderedSidebarWidth, - ]); + renderedSidebarWidth.value = withTiming(targetWidth, { + duration: panes.primarySidebarVisible ? 220 : 160, + easing: panes.primarySidebarVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), + reduceMotion: ReduceMotion.System, + }); + }, [layout.listPaneWidth, panes.primarySidebarVisible, renderedSidebarWidth]); const sidebarAnimatedStyle = useAnimatedStyle(() => ({ opacity: Math.min(1, renderedSidebarWidth.value / 80), width: renderedSidebarWidth.value, })); - const beginPrimarySidebarResize = useCallback(() => { - primarySidebarResizeStartWidth.current = layout.listPaneWidth ?? 0; - setPrimarySidebarResizing(true); - }, [layout.listPaneWidth]); - const resizePrimarySidebarBy = useCallback( - (delta: number) => { - setPrimarySidebarPreferredWidth( - constrainPrimarySidebarWidth(primarySidebarResizeStartWidth.current + delta, width), - ); - }, - [width], - ); - const endPrimarySidebarResize = useCallback(() => { - setPrimarySidebarResizing(false); - }, []); const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { @@ -347,16 +307,6 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) /> ) : null} - {layout.usesSplitView && panes.primarySidebarVisible ? ( - - ) : null} {props.children} diff --git a/apps/mobile/src/features/layout/workspace-pane-divider.tsx b/apps/mobile/src/features/layout/workspace-pane-divider.tsx index 3c453e0e58a..a892ab73655 100644 --- a/apps/mobile/src/features/layout/workspace-pane-divider.tsx +++ b/apps/mobile/src/features/layout/workspace-pane-divider.tsx @@ -1,6 +1,5 @@ -import { useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import { - PanResponder, Platform, PlatformColor, Pressable, @@ -8,6 +7,8 @@ import { View, type AccessibilityActionEvent, } from "react-native"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { runOnJS } from "react-native-reanimated"; const ACCESSIBILITY_RESIZE_STEP = 24; @@ -27,30 +28,32 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { latestProps.current = props; const [hovered, setHovered] = useState(false); const [dragging, setDragging] = useState(false); - const panResponder = useMemo( + const handleResizeStart = useCallback(() => { + setDragging(true); + latestProps.current.onResizeStart?.(); + }, []); + const handleResize = useCallback((translationX: number) => { + latestProps.current.onResizeBy(translationX * latestProps.current.resizeDirection); + }, []); + const handleResizeEnd = useCallback(() => { + setDragging(false); + latestProps.current.onResizeEnd?.(); + }, []); + const resizeGesture = useMemo( () => - PanResponder.create({ - onMoveShouldSetPanResponder: (_event, gesture) => { - const horizontalDistance = Math.abs(gesture.dx); - return horizontalDistance >= 4 && horizontalDistance > Math.abs(gesture.dy) * 1.25; - }, - onPanResponderGrant: () => { - setDragging(true); - latestProps.current.onResizeStart?.(); - }, - onPanResponderMove: (_event, gesture) => { - latestProps.current.onResizeBy(gesture.dx * latestProps.current.resizeDirection); - }, - onPanResponderRelease: () => { - setDragging(false); - latestProps.current.onResizeEnd?.(); - }, - onPanResponderTerminate: () => { - setDragging(false); - latestProps.current.onResizeEnd?.(); - }, - }), - [], + Gesture.Pan() + .activeOffsetX([-4, 4]) + .failOffsetY([-24, 24]) + .onStart(() => { + runOnJS(handleResizeStart)(); + }) + .onUpdate((event) => { + runOnJS(handleResize)(event.translationX); + }) + .onFinalize(() => { + runOnJS(handleResizeEnd)(); + }), + [handleResize, handleResizeEnd, handleResizeStart], ); const handleAccessibilityAction = (event: AccessibilityActionEvent) => { @@ -64,25 +67,26 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { }; return ( - setHovered(true)} - onHoverOut={() => setHovered(false)} - style={styles.hitTarget} - > - - + + setHovered(true)} + onHoverOut={() => setHovered(false)} + style={styles.hitTarget} + > + + + ); } @@ -92,8 +96,9 @@ const styles = StyleSheet.create({ cursor: "pointer", justifyContent: "center", marginHorizontal: -22, + position: "relative", width: 44, - zIndex: 20, + zIndex: 100, }, line: { alignSelf: "center", diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index ad693dcb445..970bafbf5b0 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -179,7 +179,6 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurfaceProps) { const fontSize = props.fontSize ?? MOBILE_TYPOGRAPHY.label.fontSize; - const keyboardInputRef = useRef(null); const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const theme = props.theme ?? getPierreTerminalTheme(appearanceScheme); const { onInput, onResize } = props; @@ -210,33 +209,13 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf [onResize], ); - // Reopen focus through React and forward input normally; this avoids a native focus-command bridge. - useEffect(() => { - if (!NativeTerminalSurfaceView || (props.keyboardFocusRequest ?? 0) <= 0) { - return undefined; - } - - keyboardInputRef.current?.blur(); - const focusFrame = requestAnimationFrame(() => keyboardInputRef.current?.focus()); - return () => cancelAnimationFrame(focusFrame); - }, [NativeTerminalSurfaceView, props.keyboardFocusRequest]); - - const handleKeyboardInput = useCallback( - (data: string) => { - if (data.length > 0) { - onInput(data); - keyboardInputRef.current?.clear(); - } - }, - [onInput], - ); - if (NativeTerminalSurfaceView) { return ( - { - if (event.nativeEvent.key === "Backspace") { - onInput("\u007f"); - } - }} - onSubmitEditing={() => onInput("\n")} - /> ); } diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.ts index e5b1f630073..25797dcbac5 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.ts @@ -23,6 +23,7 @@ interface TerminalResizeEvent { export interface NativeTerminalSurfaceProps extends ViewProps { readonly appearanceScheme?: "light" | "dark"; + readonly focusRequest?: number; readonly themeConfig?: string; readonly backgroundColor?: string; readonly foregroundColor?: string; diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 2f9e4a94821..9bc3e209391 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -70,6 +70,7 @@ export interface ThreadDetailScreenProps { readonly serverConfig: T3ServerConfig | null; readonly layoutVariant?: LayoutVariant; readonly usesAutomaticContentInsets?: boolean; + readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; readonly onOpenDrawer: () => void; readonly onOpenConnectionEditor: () => void; readonly onChangeDraftMessage: (value: string) => void; @@ -389,6 +390,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread contentMaxWidth={contentMaxWidth} layoutVariant={layoutVariant} usesAutomaticContentInsets={props.usesAutomaticContentInsets} + onHeaderMaterialVisibilityChange={props.onHeaderMaterialVisibilityChange} skills={selectedProviderSkills} /> diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 4ffdbe04d91..c2c24a32bca 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -26,6 +26,8 @@ import { Image, Linking, type LayoutChangeEvent, + type NativeScrollEvent, + type NativeSyntheticEvent, Pressable, ScrollView, StyleSheet, @@ -36,6 +38,7 @@ import { View, } from "react-native"; import { TouchableOpacity } from "react-native-gesture-handler"; +import { ScrollViewMarker } from "react-native-screens"; import ImageViewing from "react-native-image-viewing"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { type SharedValue } from "react-native-reanimated"; @@ -109,6 +112,7 @@ export interface ThreadFeedProps { readonly contentMaxWidth?: number; readonly layoutVariant?: LayoutVariant; readonly usesAutomaticContentInsets?: boolean; + readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; readonly skills?: ReadonlyArray; } @@ -1123,6 +1127,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const foldSettleFrameRef = useRef(null); const foldSettleSecondFrameRef = useRef(null); const disclosureAnchorKeyRef = useRef(null); + const headerMaterialVisibleRef = useRef(false); const previousLatestTurnRef = useRef(props.latestTurn); const { width: windowWidth } = useWindowDimensions(); const [viewportWidth, setViewportWidth] = useState(() => @@ -1215,6 +1220,28 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { viewportWidth, ], ); + const reportHeaderMaterialVisibility = useCallback( + (visible: boolean) => { + if (headerMaterialVisibleRef.current === visible) { + return; + } + headerMaterialVisibleRef.current = visible; + props.onHeaderMaterialVisibilityChange?.(visible); + }, + [props.onHeaderMaterialVisibilityChange], + ); + const handleScroll = useCallback( + (event: NativeSyntheticEvent) => { + reportHeaderMaterialVisibility(event.nativeEvent.contentOffset.y + topContentInset > 6); + }, + [reportHeaderMaterialVisibility, topContentInset], + ); + + useEffect(() => { + headerMaterialVisibleRef.current = false; + props.onHeaderMaterialVisibilityChange?.(false); + }, [props.onHeaderMaterialVisibilityChange, props.threadId]); + const expandedWorkGroupIds = useMemo(() => { const ids = new Set(); for (const [groupId, expanded] of Object.entries(expandedWorkGroups)) { @@ -1234,6 +1261,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ), [expandedTurnIds, expandedWorkGroupIds, props.feed, props.latestTurn], ); + const anchoredEndSpace = useMemo( () => resolveChatListAnchoredEndSpace( @@ -1469,54 +1497,66 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { return ( <> - entry.id} - getItemType={(entry) => - entry.type === "message" ? `message:${entry.message.role}` : entry.type - } - keyboardShouldPersistTaps="always" - keyboardDismissMode="none" - keyboardLiftBehavior="whenAtEnd" - estimatedItemSize={180} - initialScrollAtEnd - ListHeaderComponent={ - props.usesAutomaticContentInsets ? null : - } - contentContainerStyle={{ - paddingTop: 12, - paddingHorizontal: contentHorizontalPadding, + scrollEdgeEffects={{ + top: "automatic", + right: "hidden", + bottom: "hidden", + left: "hidden", }} - /> + > + entry.id} + getItemType={(entry) => + entry.type === "message" ? `message:${entry.message.role}` : entry.type + } + keyboardShouldPersistTaps="always" + keyboardDismissMode="none" + keyboardLiftBehavior="whenAtEnd" + estimatedItemSize={180} + initialScrollAtEnd + onScroll={handleScroll} + scrollEventThrottle={16} + ListHeaderComponent={ + props.usesAutomaticContentInsets ? null : + } + contentContainerStyle={{ + paddingTop: 12, + paddingHorizontal: contentHorizontalPadding, + }} + /> + {props.feed.length === 0 ? ( void; readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; @@ -45,6 +59,11 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly pressedBackgroundColor: ColorValue; readonly selected: boolean; readonly selectedBackgroundColor: ColorValue; + readonly selectedForegroundColor: ColorValue; + readonly selectedMutedColor: ColorValue; + readonly simultaneousSwipeGesture?: ComponentProps< + typeof ThreadSwipeable + >["simultaneousWithExternalGesture"]; readonly thread: EnvironmentThreadShell; readonly environmentLabel: string | null; }) { @@ -52,7 +71,9 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { const [hovered, setHovered] = useState(false); const { backgroundColor, + foregroundColor, fullSwipeWidth, + mutedColor, onArchiveThread, onDeleteThread, onSelectThread, @@ -61,6 +82,9 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { pressedBackgroundColor, selected, selectedBackgroundColor, + selectedForegroundColor, + selectedMutedColor, + simultaneousSwipeGesture, thread, environmentLabel, } = props; @@ -96,16 +120,26 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { const subtitle = [environmentLabel, thread.branch].filter((part): part is string => Boolean(part), ); + const statusTone = threadStatusTone(thread); + const displayedStatusTone = selected + ? { + ...statusTone, + pillClassName: "bg-white/20", + textClassName: "text-white", + } + : statusTone; return ( {() => ( @@ -132,21 +166,33 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { ]} > - + {thread.title} {subtitle.length > 0 ? ( - + {subtitle.join(" · ")} ) : null} - + {relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} - + void; }) { const insets = useSafeAreaInsets(); + const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; const router = useRouter(); const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); const [searchQuery, setSearchQuery] = useState(""); + const [headerIsOverContent, setHeaderIsOverContent] = useState(false); const searchInputRef = useRef(null); const openSwipeableRef = useRef(null); + const headerIsOverContentRef = useRef(false); + const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); const { archiveThread, confirmDeleteThread } = useThreadListActions(); const environments = useMemo( () => @@ -240,6 +290,7 @@ export function ThreadNavigationSidebar(props: { ]), [groups], ); + const showsConnectionStatus = shouldShowWorkspaceConnectionStatus(catalogState); const listMenuActions = useMemo( () => [ { @@ -340,9 +391,16 @@ export function ThreadNavigationSidebar(props: { const foregroundColor = useThemeColor("--color-foreground"); const mutedColor = useThemeColor("--color-foreground-muted"); const placeholderColor = useThemeColor("--color-placeholder"); - const searchBackgroundColor = useThemeColor("--color-subtle-strong"); - const selectedBackgroundColor = useThemeColor("--color-subtle-strong"); + const searchBackgroundColor = + colorScheme === "dark" ? IOS_SEARCH_FILL_DARK : IOS_SEARCH_FILL_LIGHT; + const selectedBackgroundColor = + colorScheme === "dark" ? IOS_SYSTEM_BLUE_DARK : IOS_SYSTEM_BLUE_LIGHT; + const selectedForegroundColor = IOS_SELECTED_FOREGROUND; + const selectedMutedColor = IOS_SELECTED_MUTED_FOREGROUND; const pressedBackgroundColor = useThemeColor("--color-subtle"); + const listThemeKey = `${colorScheme}:${String(backgroundColor)}:${String(selectedBackgroundColor)}`; + const headerFadeColor = String(backgroundColor); + const topListInset = insets.top + SIDEBAR_STICKY_HEADER_HEIGHT - 6; const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current !== methods) { openSwipeableRef.current?.close(); @@ -361,6 +419,14 @@ export function ThreadNavigationSidebar(props: { }, [props.onSelectThread], ); + const handleScroll = useCallback((event: NativeSyntheticEvent) => { + const next = event.nativeEvent.contentOffset.y > 6; + if (headerIsOverContentRef.current === next) { + return; + } + headerIsOverContentRef.current = next; + setHeaderIsOverContent(next); + }, []); const focusSearch = useCallback(() => { if (!props.visible) { props.onRequestVisibility(); @@ -376,9 +442,9 @@ export function ThreadNavigationSidebar(props: { if (item.kind === "section") { return ( {item.title} @@ -388,8 +454,11 @@ export function ThreadNavigationSidebar(props: { return ( @@ -407,6 +479,7 @@ export function ThreadNavigationSidebar(props: { [ archiveThread, backgroundColor, + foregroundColor, confirmDeleteThread, handleSelectThread, handleSwipeableClose, @@ -416,8 +489,15 @@ export function ThreadNavigationSidebar(props: { props.width, savedConnectionsById, selectedBackgroundColor, + listThemeKey, + mutedColor, + selectedForegroundColor, + selectedMutedColor, ], ); + const filterIcon = hasCustomHomeListOptions(options) + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease.circle"; return ( - - - Threads - - - [ - styles.headerButton, - { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + + + item.kind} + keyExtractor={(item) => item.key} + renderItem={renderListItem} + contentContainerStyle={[ + styles.threadListContent, + { + paddingBottom: 16 + insets.bottom, + paddingTop: showsConnectionStatus ? topListInset + 94 : topListInset, + }, ]} - > - - - - + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="handled" + onScroll={handleScroll} + onScrollBeginDrag={() => openSwipeableRef.current?.close()} + scrollEventThrottle={16} + showsVerticalScrollIndicator={false} + style={styles.threadList} + ListEmptyComponent={ + + {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} + + } + /> + - {shouldShowWorkspaceConnectionStatus(catalogState) ? ( - - router.push("/settings/environments")} - state={catalogState} - variant="sidebar" + + + + + + + + + + + + + + + + + Threads + + + + + - ) : null} - - - - + + + + + - - item.kind} - keyExtractor={(item) => item.key} - renderItem={renderListItem} - contentContainerStyle={styles.threadListContent} - keyboardDismissMode="on-drag" - keyboardShouldPersistTaps="handled" - onScrollBeginDrag={() => openSwipeableRef.current?.close()} - showsVerticalScrollIndicator={false} - style={styles.threadList} - ListEmptyComponent={ - - {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} - - } - /> + {showsConnectionStatus ? ( + + router.push("/settings/environments")} + state={catalogState} + variant="sidebar" + /> + + ) : null} ); @@ -520,39 +650,46 @@ const styles = StyleSheet.create({ container: { flex: 1, }, + stickyHeader: { + left: 0, + position: "absolute", + right: 0, + top: 0, + zIndex: 4, + }, + stickyHeaderWash: { + left: 0, + position: "absolute", + right: 0, + top: 0, + }, header: { height: 44, - paddingLeft: 18, + paddingLeft: 14, paddingRight: 8, flexDirection: "row", alignItems: "center", gap: 2, }, - headerButton: { - width: 38, - height: 38, - borderRadius: 11, - alignItems: "center", - justifyContent: "center", - cursor: "pointer", - }, connectionStatus: { paddingTop: 10, + paddingHorizontal: 14, }, searchField: { - height: 36, - marginTop: 6, - marginHorizontal: 14, - paddingLeft: 10, - paddingRight: 5, - borderRadius: 10, + height: 34, + marginTop: 5, + marginHorizontal: 12, + paddingLeft: 9, + paddingRight: 10, + borderRadius: 17, + borderWidth: StyleSheet.hairlineWidth, flexDirection: "row", alignItems: "center", - gap: 7, + gap: 6, }, searchInput: { flex: 1, - height: 36, + height: 34, paddingVertical: 0, paddingHorizontal: 0, fontFamily: "DMSans_400Regular", @@ -563,8 +700,6 @@ const styles = StyleSheet.create({ }, threadListContent: { paddingHorizontal: 10, - paddingTop: 16, - paddingBottom: 16, }, sectionTitle: { paddingHorizontal: 8, diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index f0aeae4a724..5fc5fa39ab2 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -4,7 +4,14 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro import * as Option from "effect/Option"; import { EnvironmentId, ThreadId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; -import { Platform, Pressable, ScrollView, Text as RNText, View } from "react-native"; +import { + Platform, + Pressable, + ScrollView, + Text as RNText, + View, + useColorScheme, +} from "react-native"; import { isLiquidGlassAvailable } from "expo-glass-effect"; import { useWorkspaceState } from "../../state/workspace"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -235,6 +242,7 @@ function ThreadRouteContent( threadId?: string | string[]; }>(); const [drawerVisible, setDrawerVisible] = useState(false); + const [headerMaterialVisible, setHeaderMaterialVisible] = useState(false); const environmentIdRaw = firstRouteParam(params.environmentId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; const threadId = firstRouteParam(params.threadId); @@ -278,9 +286,20 @@ function ThreadRouteContent( ); /* ─── Native header theming ──────────────────────────────────────── */ + const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; const iconColor = String(useThemeColor("--color-icon")); const foregroundColor = String(useThemeColor("--color-foreground")); const secondaryFg = String(useThemeColor("--color-foreground-secondary")); + const screenBackgroundColor = String(useThemeColor("--color-screen")); + const usesEdgeToEdgeGlassHeader = USES_NATIVE_GLASS_HEADER && !layout.usesSplitView; + // Compact/iPhone stacks use the edge-to-edge UIKit material seen in Messages. + // iPad split view keeps a clean pane header; native iPad Messages/Mail reserve + // the stronger glass treatment for local controls/floating elements instead. + const glassHeaderBlurEffect = + colorScheme === "dark" + ? ("systemUltraThinMaterialDark" as const) + : ("systemUltraThinMaterialLight" as const); + const showGlassHeaderMaterial = usesEdgeToEdgeGlassHeader && headerMaterialVisible; const headerSubtitle = [ selectedThreadProject?.title ?? null, selectedEnvironmentConnection?.environmentLabel ?? null, @@ -569,27 +588,28 @@ function ThreadRouteContent( @@ -668,7 +688,8 @@ function ThreadRouteContent( threadCwd={selectedThreadCwd} selectedThreadQueueCount={composer.selectedThreadQueueCount} layoutVariant={layout.variant} - usesAutomaticContentInsets={USES_NATIVE_GLASS_HEADER} + usesAutomaticContentInsets={usesEdgeToEdgeGlassHeader} + onHeaderMaterialVisibilityChange={setHeaderMaterialVisible} onOpenDrawer={handleOpenDrawer} onOpenConnectionEditor={handleOpenConnectionEditor} onChangeDraftMessage={composer.onChangeDraftMessage} diff --git a/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx b/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx new file mode 100644 index 00000000000..6244b19e34f --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx @@ -0,0 +1,22 @@ +import { View } from "react-native"; + +import { T3HeaderButton } from "../../native/T3HeaderButton.ios"; + +type SidebarFilterButtonIcon = + | "line.3.horizontal.decrease.circle" + | "line.3.horizontal.decrease.circle.fill"; + +export function SidebarFilterButton(props: { + readonly accessibilityLabel: string; + readonly icon: SidebarFilterButtonIcon; +}) { + return ( + + undefined} + /> + + ); +} diff --git a/apps/mobile/src/features/threads/sidebar-filter-button.tsx b/apps/mobile/src/features/threads/sidebar-filter-button.tsx new file mode 100644 index 00000000000..45cc910aa36 --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-filter-button.tsx @@ -0,0 +1,41 @@ +import { SymbolView } from "expo-symbols"; +import { Pressable, StyleSheet } from "react-native"; + +import { useThemeColor } from "../../lib/useThemeColor"; + +export type SidebarFilterButtonIcon = + | "line.3.horizontal.decrease.circle" + | "line.3.horizontal.decrease.circle.fill"; + +export function SidebarFilterButton(props: { + readonly accessibilityLabel: string; + readonly icon: SidebarFilterButtonIcon; +}) { + const iconColor = useThemeColor("--color-icon-muted"); + const pressedBackgroundColor = useThemeColor("--color-subtle"); + + return ( + [ + styles.button, + { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + ]} + > + + + ); +} + +const styles = StyleSheet.create({ + button: { + width: 44, + height: 44, + borderRadius: 12, + alignItems: "center", + justifyContent: "center", + cursor: "pointer", + }, +}); diff --git a/apps/mobile/src/native/T3HeaderButton.ios.tsx b/apps/mobile/src/native/T3HeaderButton.ios.tsx index 74908abd16c..49567d3c3ed 100644 --- a/apps/mobile/src/native/T3HeaderButton.ios.tsx +++ b/apps/mobile/src/native/T3HeaderButton.ios.tsx @@ -3,7 +3,11 @@ import type { NativeSyntheticEvent, StyleProp, ViewProps, ViewStyle } from "reac interface NativeHeaderButtonProps extends ViewProps { readonly label: string; - readonly systemImage: "gearshape" | "square.and.pencil"; + readonly systemImage: + | "gearshape" + | "line.3.horizontal.decrease.circle" + | "line.3.horizontal.decrease.circle.fill" + | "square.and.pencil"; readonly onTriggered: (event: NativeSyntheticEvent>) => void; } diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch new file mode 100644 index 00000000000..8685cf8dca9 --- /dev/null +++ b/patches/react-native-screens@4.25.2.patch @@ -0,0 +1,190 @@ +diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm +index 69d4d9a..6192c4a 100644 +--- a/ios/RNSScreen.mm ++++ b/ios/RNSScreen.mm +@@ -26,8 +26,10 @@ + #import "RNSScreenFooter.h" + #import "RNSScreenStack.h" + #import "RNSScreenStackHeaderConfig.h" ++#import "RNSScrollViewMarkerComponentView.h" + #import "RNSScrollViewFinder.h" + #import "RNSScrollViewHelper.h" ++#import "RNSScrollViewSeeking.h" + #import "RNSTabBarController.h" + + #import "RNSDefines.h" +@@ -61,6 +63,8 @@ struct ContentWrapperBox { + ContentWrapperBox _contentWrapperBox; + bool _sheetHasInitialDetentSet; + BOOL _shouldUpdateScrollEdgeEffects; ++ __weak UIScrollView *_markedContentScrollView; ++ __weak RNSScrollViewMarkerComponentView *_markedContentScrollViewMarker; + UITapGestureRecognizer *_backdropTapGestureRecognizer; + RCTSurfaceTouchHandler *_touchHandler; + react::RNSScreenShadowNode::ConcreteState::Shared _state; +@@ -1146,10 +1150,24 @@ RNS_IGNORE_SUPER_CALL_END + + - (void)updateContentScrollViewEdgeEffectsIfExists + { ++ if (_markedContentScrollView != nil && _markedContentScrollViewMarker != nil) { ++ [RNSScrollEdgeEffectApplicator applyToScrollView:_markedContentScrollView ++ withProvider:_markedContentScrollViewMarker]; ++ return; ++ } ++ + [RNSScrollEdgeEffectApplicator applyToScrollView:[RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:self] + withProvider:self]; + } + ++- (void)registerDescendantScrollView:(nonnull UIScrollView *)scrollView ++ fromMarker:(nonnull RNSScrollViewMarkerComponentView *)marker ++{ ++ _markedContentScrollView = scrollView; ++ _markedContentScrollViewMarker = marker; ++ [RNSScrollEdgeEffectApplicator applyToScrollView:scrollView withProvider:marker]; ++} ++ + #pragma mark - RNSSafeAreaProviding + + - (UIEdgeInsets)providerSafeAreaInsets +diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h +index 4badf56..7703df9 100644 +--- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h ++++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h +@@ -1,10 +1,11 @@ + #pragma once + + #import "RNSReactBaseView.h" ++#import "RNSScrollEdgeEffectApplicator.h" + + NS_ASSUME_NONNULL_BEGIN + +-@interface RNSScrollViewMarkerComponentView : RNSReactBaseView ++@interface RNSScrollViewMarkerComponentView : RNSReactBaseView + + @end + +diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm +index 52c61c8..f536579 100644 +--- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm ++++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm +@@ -2,6 +2,7 @@ + #import "RNSConversions-ScrollViewMarker.h" + #import "RNSEnums.h" + #import "RNSScrollEdgeEffectApplicator.h" ++#import "RNSScrollViewFinder.h" + #import "RNSScrollViewSeeking.h" + + #import +@@ -57,17 +58,15 @@ namespace react = facebook::react; + */ + - (nullable UIScrollView *)findScrollView + { +- // It allows 0 for cases where the child is unmounted +- RCTAssert( +- self.subviews.count <= 1, +- @"[RNScreens] ScrollViewMarker expects at most a single child. Subviews: %@", +- self.subviews); +- +- UIScrollView *_Nullable foundScrollView = [self resolveScrollViewFromChildView:self.subviews.firstObject]; ++ for (UIView *subview in self.subviews) { ++ UIScrollView *_Nullable foundScrollView = [self resolveScrollViewFromChildView:subview]; ++ if (foundScrollView != nil) { ++ return foundScrollView; ++ } ++ } + +- RCTAssert(foundScrollView != nil, @"[RNScreens] Failed to find ScrollView"); // debug assertion only +- return foundScrollView; ++ return nil; + } + + - (nullable id)findFirstSeekingAncestor + { +@@ -122,7 +123,8 @@ namespace react = facebook::react; + /** + * Tries to resolve UIScrollView from the passed childView. + * +- * Currently it supports only direct `UIScrollView` or react-native's `` component. ++ * Handles direct scroll views and scroll views hidden under wrapper views such ++ * as keyboard-aware list components. + */ + - (nullable UIScrollView *)resolveScrollViewFromChildView:(nullable UIView *)childView + { +@@ -138,7 +140,7 @@ namespace react = facebook::react; + return static_cast(childView).scrollView; + } + +- return nil; ++ return [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:childView]; + } + + #pragma mark - Override +diff --git a/lib/commonjs/index.js b/lib/commonjs/index.js +index de25040..48bb962 100644 +--- a/lib/commonjs/index.js ++++ b/lib/commonjs/index.js +@@ -202,6 +202,7 @@ var _utils = require("./utils"); + var _flags = require("./flags"); + var _useTransitionProgress = _interopRequireDefault(require("./useTransitionProgress")); + var _tabs = require("./components/tabs"); ++var _scrollViewMarker = require("./components/gamma/scroll-view-marker"); + Object.keys(_tabs).forEach(function (key) { + if (key === "default" || key === "__esModule") return; + if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; +@@ -213,7 +214,18 @@ Object.keys(_tabs).forEach(function (key) { + } + }); + }); ++Object.keys(_scrollViewMarker).forEach(function (key) { ++ if (key === "default" || key === "__esModule") return; ++ if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; ++ if (key in exports && exports[key] === _scrollViewMarker[key]) return; ++ Object.defineProperty(exports, key, { ++ enumerable: true, ++ get: function () { ++ return _scrollViewMarker[key]; ++ } ++ }); ++}); + function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } + function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } + function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } +-//# sourceMappingURL=index.js.map +\ No newline at end of file ++//# sourceMappingURL=index.js.map +diff --git a/lib/module/index.js b/lib/module/index.js +index 07caaf7..ba2e9c5 100644 +--- a/lib/module/index.js ++++ b/lib/module/index.js +@@ -43,4 +43,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; + * EXPERIMENTAL API BELOW. MIGHT CHANGE W/O ANY NOTICE + */ + export * from './components/tabs'; +-//# sourceMappingURL=index.js.map +\ No newline at end of file ++export * from './components/gamma/scroll-view-marker'; ++//# sourceMappingURL=index.js.map +diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts +index 8e4480f..7a2c094 100644 +--- a/lib/typescript/index.d.ts ++++ b/lib/typescript/index.d.ts +@@ -32,5 +32,6 @@ export { default as useTransitionProgress } from './useTransitionProgress'; + * EXPERIMENTAL API BELOW. MIGHT CHANGE W/O ANY NOTICE + */ + export * from './components/tabs'; ++export * from './components/gamma/scroll-view-marker'; + export type * from './components/shared/types'; +-//# sourceMappingURL=index.d.ts.map +\ No newline at end of file ++//# sourceMappingURL=index.d.ts.map +diff --git a/src/index.tsx b/src/index.tsx +index a405aec..cd649bd 100644 +--- a/src/index.tsx ++++ b/src/index.tsx +@@ -66,4 +66,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; + * EXPERIMENTAL API BELOW. MIGHT CHANGE W/O ANY NOTICE + */ + export * from './components/tabs'; ++export * from './components/gamma/scroll-view-marker'; + export type * from './components/shared/types'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8c2f25f952c..43540ffd62a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -87,6 +87,9 @@ patchedDependencies: react-native-nitro-modules@0.35.9: hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 path: patches/react-native-nitro-modules@0.35.9.patch + react-native-screens@4.25.2: + hash: 41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d + path: patches/react-native-screens@4.25.2.patch importers: @@ -321,7 +324,7 @@ importers: version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(3dd1c34e09949502ba8228b7f60c3e89) + version: 56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -375,7 +378,7 @@ importers: version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -11908,7 +11911,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(3dd1c34e09949502ba8228b7f60c3e89) + expo-router: 56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12206,7 +12209,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(3dd1c34e09949502ba8228b7f60c3e89) + expo-router: 56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16120,7 +16123,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(3dd1c34e09949502ba8228b7f60c3e89): + expo-router@56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16151,7 +16154,7 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-drawer-layout: 4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -18758,7 +18761,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index aa27daf4914..a85da0b620c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -97,6 +97,7 @@ patchedDependencies: effect@4.0.0-beta.78: patches/effect@4.0.0-beta.78.patch expo-modules-jsi@56.0.10: patches/expo-modules-jsi@56.0.10.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch + react-native-screens@4.25.2: patches/react-native-screens@4.25.2.patch peerDependencyRules: allowAny: From ea8339ec90e036bdc9383c2d6ab56d42e25a9ca1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:23:12 -0700 Subject: [PATCH 11/71] Address iPad layout review feedback --- .../features/diffs/nativeReviewDiffSurface.ts | 9 ++++++-- .../home/workspace-connection-status.ts | 1 + .../layout/AdaptiveWorkspaceLayout.tsx | 17 ++++++++++++-- .../layout/workspace-pane-divider.tsx | 4 ++-- apps/mobile/src/lib/layout.test.ts | 22 +++++++++++++++++-- apps/mobile/src/lib/layout.ts | 11 ++++++++-- 6 files changed, 54 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index a4896dc4011..9ac85b27157 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -12,6 +12,7 @@ import { requireNativeView } from "expo"; import { NativeViewResolutionError } from "../../native/nativeViewResolutionError"; const NATIVE_REVIEW_DIFF_MODULE_NAME = "T3ReviewDiffSurface"; +const NATIVE_REVIEW_DIFF_PAYLOAD_RETRY_FRAMES = 60; interface ExpoGlobalWithViewConfig { readonly expo?: { @@ -195,7 +196,7 @@ function useNativeReviewDiffPayload( const view = nativeRef.current; const command = view?.[method]; if (!view || !command) { - if (attempts < 4) { + if (attempts < NATIVE_REVIEW_DIFF_PAYLOAD_RETRY_FRAMES) { attempts += 1; frame = requestAnimationFrame(dispatch); } @@ -203,7 +204,11 @@ function useNativeReviewDiffPayload( } void command.call(view, payload).catch((error: unknown) => { - if (!cancelled && attempts < 4 && isPendingNativeViewRegistration(error)) { + if ( + !cancelled && + attempts < NATIVE_REVIEW_DIFF_PAYLOAD_RETRY_FRAMES && + isPendingNativeViewRegistration(error) + ) { attempts += 1; frame = requestAnimationFrame(dispatch); return; diff --git a/apps/mobile/src/features/home/workspace-connection-status.ts b/apps/mobile/src/features/home/workspace-connection-status.ts index c9116d060cd..d523684f03c 100644 --- a/apps/mobile/src/features/home/workspace-connection-status.ts +++ b/apps/mobile/src/features/home/workspace-connection-status.ts @@ -3,6 +3,7 @@ import type { WorkspaceState } from "../../state/workspaceModel"; export function shouldShowWorkspaceConnectionStatus(state: WorkspaceState): boolean { return ( state.networkStatus === "offline" || + state.connectionError !== null || state.hasConnectingEnvironment || (state.hasLoadedShellSnapshot && !state.hasReadyEnvironment) ); diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 88821ded481..4221e290b11 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -32,7 +32,10 @@ import { import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation"; import { buildThreadRoutePath } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; -import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import { + parseActiveThreadPath, + useHardwareKeyboardCommand, +} from "../keyboard/hardwareKeyboardCommands"; import { HomeListOptionsProvider } from "../home/home-list-options"; import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; @@ -113,8 +116,10 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) layout, viewportWidth: width, preferredWidth: fileInspectorPreferredWidth ?? undefined, + reservedLeadingWidth: + layout.usesSplitView && primarySidebarPreferredVisible ? (layout.listPaneWidth ?? 0) : 0, }), - [fileInspectorPreferredWidth, layout, width], + [fileInspectorPreferredWidth, layout, primarySidebarPreferredVisible, width], ); const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole = focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary"); @@ -190,6 +195,14 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) } setSupplementaryPanePreferredVisible(true); }, []); + const handleOpenFilesCommand = useCallback(() => { + if (!layout.usesSplitView || !fileInspector.supported || !parseActiveThreadPath(pathname)) { + return false; + } + showAuxiliaryPane("inspector"); + return true; + }, [fileInspector.supported, layout.usesSplitView, pathname, showAuxiliaryPane]); + useHardwareKeyboardCommand("files", handleOpenFilesCommand); const toggleAuxiliaryPane = useCallback(() => { if (auxiliaryPaneRole === "inspector") { setFileInspectorPreferredVisible((current) => !current); diff --git a/apps/mobile/src/features/layout/workspace-pane-divider.tsx b/apps/mobile/src/features/layout/workspace-pane-divider.tsx index a892ab73655..c6fd1fca8ef 100644 --- a/apps/mobile/src/features/layout/workspace-pane-divider.tsx +++ b/apps/mobile/src/features/layout/workspace-pane-divider.tsx @@ -59,9 +59,9 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { const handleAccessibilityAction = (event: AccessibilityActionEvent) => { props.onResizeStart?.(); if (event.nativeEvent.actionName === "increment") { - props.onResizeBy(ACCESSIBILITY_RESIZE_STEP); + props.onResizeBy(ACCESSIBILITY_RESIZE_STEP * props.resizeDirection); } else if (event.nativeEvent.actionName === "decrement") { - props.onResizeBy(-ACCESSIBILITY_RESIZE_STEP); + props.onResizeBy(-ACCESSIBILITY_RESIZE_STEP * props.resizeDirection); } props.onResizeEnd?.(); }; diff --git a/apps/mobile/src/lib/layout.test.ts b/apps/mobile/src/lib/layout.test.ts index a61a1ba40b9..6dea0beafbe 100644 --- a/apps/mobile/src/lib/layout.test.ts +++ b/apps/mobile/src/lib/layout.test.ts @@ -164,7 +164,7 @@ describe("deriveWorkspacePaneLayout", () => { contentPaneWidth: 1_024, supportsAuxiliaryPane: true, auxiliaryPaneVisible: true, - auxiliaryPaneWidth: 287, + auxiliaryPaneWidth: 260, }); }); @@ -185,7 +185,25 @@ describe("deriveWorkspacePaneLayout", () => { contentPaneWidth: 986, supportsAuxiliaryPane: true, auxiliaryPaneVisible: true, - auxiliaryPaneWidth: 320, + auxiliaryPaneWidth: 276, + }); + }); + + it("keeps an explicitly hidden thread sidebar hidden when the file inspector is visible", () => { + const layout = deriveLayout({ width: 1_024, height: 1_366 }); + + expect( + deriveWorkspacePaneLayout({ + layout, + viewportWidth: 1_024, + primarySidebarPreferredVisible: false, + auxiliaryPanePreferredVisible: true, + auxiliaryPaneRole: "inspector", + }), + ).toMatchObject({ + primarySidebarVisible: false, + primarySidebarSuppressedByAuxiliary: false, + auxiliaryPaneVisible: true, }); }); diff --git a/apps/mobile/src/lib/layout.ts b/apps/mobile/src/lib/layout.ts index 44d0470116e..eb0c45e0607 100644 --- a/apps/mobile/src/lib/layout.ts +++ b/apps/mobile/src/lib/layout.ts @@ -100,9 +100,11 @@ export function deriveWorkspacePaneLayout(input: { layout: input.layout, viewportWidth, preferredWidth: input.auxiliaryPanePreferredWidth, + reservedLeadingWidth: preferredPrimarySidebarWidth, }); const auxiliaryPaneVisible = fileInspector.supported && input.auxiliaryPanePreferredVisible; const primarySidebarSuppressedByAuxiliary = + preferredPrimarySidebarVisible && auxiliaryPaneVisible && fileInspector.width !== null && input.layout.listPaneWidth !== null && @@ -151,8 +153,13 @@ export function deriveFileInspectorPaneLayout(input: { readonly layout: Layout; readonly viewportWidth: number; readonly preferredWidth?: number; + readonly reservedLeadingWidth?: number; }): FileInspectorPaneLayout { const viewportWidth = Math.max(0, input.viewportWidth); + const reservedLeadingWidth = Number.isFinite(input.reservedLeadingWidth) + ? Math.max(0, input.reservedLeadingWidth ?? 0) + : 0; + const availableContentWidth = Math.max(0, viewportWidth - reservedLeadingWidth); const supported = input.layout.usesSplitView && viewportWidth >= FILE_INSPECTOR_MIN_VIEWPORT_WIDTH; @@ -163,11 +170,11 @@ export function deriveFileInspectorPaneLayout(input: { preferredWidth: input.preferredWidth ?? clamp( - Math.round(viewportWidth * 0.28), + Math.round(availableContentWidth * 0.28), AUXILIARY_PANE_MIN_WIDTH, AUXILIARY_PANE_DEFAULT_MAX_WIDTH, ), - availableWidth: viewportWidth, + availableWidth: availableContentWidth, }) : null, }; From 75f0e90c03c1e65d8afee220c568c11d1d471e3b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:27:13 -0700 Subject: [PATCH 12/71] Fix iPad header and input review issues --- .../terminal/NativeTerminalSurface.tsx | 7 +++- .../src/features/threads/ThreadFeed.tsx | 41 +++++++++++++++---- .../threads/sidebar-filter-button.ios.tsx | 18 ++++---- 3 files changed, 49 insertions(+), 17 deletions(-) diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index 970bafbf5b0..1d42af6ba85 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -195,9 +195,12 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf }, [hasNativeSurface, props.buffer.length, props.isRunning, props.terminalKey]); const handleNativeInput = useCallback( (event: NativeSyntheticEvent) => { + if (!props.isRunning) { + return; + } onInput(event.nativeEvent.data); }, - [onInput], + [onInput, props.isRunning], ); const handleNativeResize = useCallback( (event: NativeSyntheticEvent) => { @@ -215,7 +218,7 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf (null); const disclosureAnchorKeyRef = useRef(null); const headerMaterialVisibleRef = useRef(false); + const listContentHeightRef = useRef(0); + const listViewportHeightRef = useRef(0); const previousLatestTurnRef = useRef(props.latestTurn); const { width: windowWidth } = useWindowDimensions(); const [viewportWidth, setViewportWidth] = useState(() => @@ -1162,10 +1164,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const insets = useSafeAreaInsets(); const topContentInset = props.contentTopInset ?? insets.top + 44; const bottomContentInset = props.contentBottomInset ?? 18; - const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { - const nextWidth = Math.round(event.nativeEvent.layout.width); - setViewportWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); - }, []); const iconSubtleColor = useThemeColor("--color-icon-subtle"); const userBubbleColor = useThemeColor("--color-user-bubble"); @@ -1236,11 +1234,39 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { }, [reportHeaderMaterialVisibility, topContentInset], ); + const reportInitialHeaderMaterialVisibility = useCallback(() => { + const topInsetContribution = props.usesAutomaticContentInsets ? topContentInset : 0; + reportHeaderMaterialVisibility( + listContentHeightRef.current - listViewportHeightRef.current + topInsetContribution > 6, + ); + }, [props.usesAutomaticContentInsets, reportHeaderMaterialVisibility, topContentInset]); + const handleViewportLayout = useCallback( + (event: LayoutChangeEvent) => { + const nextWidth = Math.round(event.nativeEvent.layout.width); + const nextHeight = Math.round(event.nativeEvent.layout.height); + setViewportWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); + if (Math.abs(listViewportHeightRef.current - nextHeight) > 1) { + listViewportHeightRef.current = nextHeight; + reportInitialHeaderMaterialVisibility(); + } + }, + [reportInitialHeaderMaterialVisibility], + ); + const handleContentSizeChange = useCallback( + (_contentWidth: number, contentHeight: number) => { + const nextHeight = Math.round(contentHeight); + if (Math.abs(listContentHeightRef.current - nextHeight) > 1) { + listContentHeightRef.current = nextHeight; + reportInitialHeaderMaterialVisibility(); + } + }, + [reportInitialHeaderMaterialVisibility], + ); useEffect(() => { - headerMaterialVisibleRef.current = false; - props.onHeaderMaterialVisibilityChange?.(false); - }, [props.onHeaderMaterialVisibilityChange, props.threadId]); + listContentHeightRef.current = 0; + reportHeaderMaterialVisibility(false); + }, [props.threadId, reportHeaderMaterialVisibility]); const expandedWorkGroupIds = useMemo(() => { const ids = new Set(); @@ -1546,6 +1572,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { keyboardLiftBehavior="whenAtEnd" estimatedItemSize={180} initialScrollAtEnd + onContentSizeChange={handleContentSizeChange} onScroll={handleScroll} scrollEventThrottle={16} ListHeaderComponent={ diff --git a/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx b/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx index 6244b19e34f..8fa5ca6fdb2 100644 --- a/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx +++ b/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx @@ -1,4 +1,4 @@ -import { View } from "react-native"; +import { Pressable, View } from "react-native"; import { T3HeaderButton } from "../../native/T3HeaderButton.ios"; @@ -11,12 +11,14 @@ export function SidebarFilterButton(props: { readonly icon: SidebarFilterButtonIcon; }) { return ( - - undefined} - /> - + + + undefined} + /> + + ); } From e4ec3cfca7395eea01b565b5343daf634ee0e10c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:51:58 -0700 Subject: [PATCH 13/71] Resolve remaining iPad review issues --- .../files/sourceHighlightingState.test.ts | 2 +- .../home/WorkspaceConnectionStatus.test.ts | 11 ++++++ .../home/workspace-connection-status.ts | 1 + .../layout/AdaptiveWorkspaceLayout.tsx | 2 ++ .../layout/workspace-pane-divider.tsx | 4 +-- .../src/features/threads/ThreadComposer.tsx | 4 +++ .../features/threads/ThreadDetailScreen.tsx | 2 +- .../src/features/threads/ThreadFeed.tsx | 36 +++---------------- .../features/threads/ThreadRouteScreen.tsx | 17 +++++++++ 9 files changed, 43 insertions(+), 36 deletions(-) diff --git a/apps/mobile/src/features/files/sourceHighlightingState.test.ts b/apps/mobile/src/features/files/sourceHighlightingState.test.ts index 6c4c00e1663..50b4eec41b0 100644 --- a/apps/mobile/src/features/files/sourceHighlightingState.test.ts +++ b/apps/mobile/src/features/files/sourceHighlightingState.test.ts @@ -88,7 +88,7 @@ describe("sourceHighlightingState", () => { expect(AsyncResult.isSuccess(registry.get(atom))).toBe(true); }); firstUnmount(); - await new Promise((resolve) => setTimeout(resolve, 25)); + await new Promise((resolve) => setTimeout(resolve, 100)); const secondUnmount = registry.mount(atom); await vi.waitFor(() => { diff --git a/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts b/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts index e92d38af8be..767f0e3dd3f 100644 --- a/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts +++ b/apps/mobile/src/features/home/WorkspaceConnectionStatus.test.ts @@ -56,4 +56,15 @@ describe("workspace connection status", () => { expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); expect(workspaceConnectionStatusLabel(state)).toBe("Reconnecting to Julius’s Mac mini"); }); + + it("surfaces connection errors before the generic disconnected fallback", () => { + const state = workspaceState({ + connectionError: "Could not reach Julius’s Mac mini", + hasLoadedShellSnapshot: false, + hasReadyEnvironment: false, + }); + + expect(shouldShowWorkspaceConnectionStatus(state)).toBe(true); + expect(workspaceConnectionStatusLabel(state)).toBe("Could not reach Julius’s Mac mini"); + }); }); diff --git a/apps/mobile/src/features/home/workspace-connection-status.ts b/apps/mobile/src/features/home/workspace-connection-status.ts index d523684f03c..7d2c6d840d4 100644 --- a/apps/mobile/src/features/home/workspace-connection-status.ts +++ b/apps/mobile/src/features/home/workspace-connection-status.ts @@ -17,5 +17,6 @@ export function workspaceConnectionStatusLabel(state: WorkspaceState): string { if (state.connectingEnvironments.length > 1) { return `Reconnecting ${state.connectingEnvironments.length} environments`; } + if (state.connectionError !== null) return state.connectionError; return "Not connected"; } diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 4221e290b11..644abaefd23 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -190,9 +190,11 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) useHardwareKeyboardCommand("toggleSidebar", handleToggleSidebarCommand); const showAuxiliaryPane = useCallback((role: WorkspaceAuxiliaryPaneRole) => { if (role === "inspector") { + setFocusedAuxiliaryPaneRole("inspector"); setFileInspectorPreferredVisible(true); return; } + setFocusedAuxiliaryPaneRole("supplementary"); setSupplementaryPanePreferredVisible(true); }, []); const handleOpenFilesCommand = useCallback(() => { diff --git a/apps/mobile/src/features/layout/workspace-pane-divider.tsx b/apps/mobile/src/features/layout/workspace-pane-divider.tsx index c6fd1fca8ef..a892ab73655 100644 --- a/apps/mobile/src/features/layout/workspace-pane-divider.tsx +++ b/apps/mobile/src/features/layout/workspace-pane-divider.tsx @@ -59,9 +59,9 @@ export function WorkspacePaneDivider(props: WorkspacePaneDividerProps) { const handleAccessibilityAction = (event: AccessibilityActionEvent) => { props.onResizeStart?.(); if (event.nativeEvent.actionName === "increment") { - props.onResizeBy(ACCESSIBILITY_RESIZE_STEP * props.resizeDirection); + props.onResizeBy(ACCESSIBILITY_RESIZE_STEP); } else if (event.nativeEvent.actionName === "decrement") { - props.onResizeBy(-ACCESSIBILITY_RESIZE_STEP * props.resizeDirection); + props.onResizeBy(-ACCESSIBILITY_RESIZE_STEP); } props.onResizeEnd?.(); }; diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index a48a2cc6cae..4acd69e92fc 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -217,6 +217,10 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const isExpanded = isFocused; const canSend = hasContent; + useEffect(() => { + sendInFlightRef.current = false; + }, [props.selectedThread.id]); + const onPressImage = useCallback( (uri: string) => { wasExpandedBeforePreviewRef.current = isFocused; diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 9bc3e209391..15aea05dd2c 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -385,7 +385,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread freeze={freeze} anchorMessageId={anchorMessageId} contentInsetEndAdjustment={contentInsetEndAdjustment} - contentTopInset={headerHeight} + contentTopInset={props.usesAutomaticContentInsets ? headerHeight : 0} contentBottomInset={estimatedOverlayHeight} contentMaxWidth={contentMaxWidth} layoutVariant={layoutVariant} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 7fdcb5f3ff5..745fc1770b8 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1128,8 +1128,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const foldSettleSecondFrameRef = useRef(null); const disclosureAnchorKeyRef = useRef(null); const headerMaterialVisibleRef = useRef(false); - const listContentHeightRef = useRef(0); - const listViewportHeightRef = useRef(0); const previousLatestTurnRef = useRef(props.latestTurn); const { width: windowWidth } = useWindowDimensions(); const [viewportWidth, setViewportWidth] = useState(() => @@ -1234,37 +1232,12 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { }, [reportHeaderMaterialVisibility, topContentInset], ); - const reportInitialHeaderMaterialVisibility = useCallback(() => { - const topInsetContribution = props.usesAutomaticContentInsets ? topContentInset : 0; - reportHeaderMaterialVisibility( - listContentHeightRef.current - listViewportHeightRef.current + topInsetContribution > 6, - ); - }, [props.usesAutomaticContentInsets, reportHeaderMaterialVisibility, topContentInset]); - const handleViewportLayout = useCallback( - (event: LayoutChangeEvent) => { - const nextWidth = Math.round(event.nativeEvent.layout.width); - const nextHeight = Math.round(event.nativeEvent.layout.height); - setViewportWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); - if (Math.abs(listViewportHeightRef.current - nextHeight) > 1) { - listViewportHeightRef.current = nextHeight; - reportInitialHeaderMaterialVisibility(); - } - }, - [reportInitialHeaderMaterialVisibility], - ); - const handleContentSizeChange = useCallback( - (_contentWidth: number, contentHeight: number) => { - const nextHeight = Math.round(contentHeight); - if (Math.abs(listContentHeightRef.current - nextHeight) > 1) { - listContentHeightRef.current = nextHeight; - reportInitialHeaderMaterialVisibility(); - } - }, - [reportInitialHeaderMaterialVisibility], - ); + const handleViewportLayout = useCallback((event: LayoutChangeEvent) => { + const nextWidth = Math.round(event.nativeEvent.layout.width); + setViewportWidth((current) => (Math.abs(current - nextWidth) > 1 ? nextWidth : current)); + }, []); useEffect(() => { - listContentHeightRef.current = 0; reportHeaderMaterialVisibility(false); }, [props.threadId, reportHeaderMaterialVisibility]); @@ -1572,7 +1545,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { keyboardLiftBehavior="whenAtEnd" estimatedItemSize={180} initialScrollAtEnd - onContentSizeChange={handleContentSizeChange} onScroll={handleScroll} scrollEventThrottle={16} ListHeaderComponent={ diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 5fc5fa39ab2..e69294e9ea2 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -256,6 +256,23 @@ function ThreadRouteContent( ? inspectorSelection.mode : null; + useEffect(() => { + setInspectorSelection((current) => { + if (props.renderInspector === undefined) { + if (current === null || current.mode === "route") { + return null; + } + return { ...current, routeThreadIdentity }; + } + + if (current === null || current.mode === "route") { + return { routeThreadIdentity, mode: "route" }; + } + + return { ...current, routeThreadIdentity }; + }); + }, [props.renderInspector, routeThreadIdentity]); + useFocusEffect( useCallback(() => { return () => { From 5efa2df4abc20f24a63ec73f6559dea632b57f30 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:12:33 -0700 Subject: [PATCH 14/71] Address mobile review lifecycle issues --- .../t3-review-diff/ios/T3ReviewDiffView.swift | 6 ++++ apps/mobile/plugins/withIosSceneLifecycle.cjs | 22 ++++++++++-- apps/mobile/src/app/_layout.tsx | 6 ---- apps/mobile/src/app/new/_layout.tsx | 2 +- .../features/files/ThreadFilesRouteScreen.tsx | 34 ++++++++++++++----- .../features/threads/ThreadRouteScreen.tsx | 9 +++-- 6 files changed, 60 insertions(+), 19 deletions(-) diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index 9a7a5004daf..bd023ff3488 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -492,6 +492,8 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { } func setTokensPatchJson(_ tokensPatchJson: String) { + let contentResetKey = self.contentResetKey + payloadDecodeQueue.async { [weak self] in guard let data = tokensPatchJson.data(using: .utf8) else { return @@ -503,6 +505,9 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { guard let self else { return } + guard contentResetKey == self.contentResetKey else { + return + } // A highlighter request from the previous file can finish after the view has // already reset. Never let that stale patch roll the native token state back. if let resetKey = patch.resetKey, resetKey != self.tokensResetKey { @@ -550,6 +555,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { } self.contentResetKey = contentResetKey + rowsDecodeGeneration += 1 tokensDecodeGeneration += 1 contentView.tokensByRowId = [:] hasAppliedInitialRowIndex = false diff --git a/apps/mobile/plugins/withIosSceneLifecycle.cjs b/apps/mobile/plugins/withIosSceneLifecycle.cjs index cfb2a159c46..d426475250a 100644 --- a/apps/mobile/plugins/withIosSceneLifecycle.cjs +++ b/apps/mobile/plugins/withIosSceneLifecycle.cjs @@ -12,12 +12,23 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { ) { guard let windowScene = scene as? UIWindowScene, - let appDelegate = UIApplication.shared.delegate as? AppDelegate, - let appWindow = appDelegate.window + let appDelegate = UIApplication.shared.delegate as? AppDelegate else { return } + let appWindow: UIWindow + if let existingWindow = appDelegate.window { + appWindow = existingWindow + } else { + appWindow = UIWindow(windowScene: windowScene) + appDelegate.window = appWindow + appDelegate.reactNativeFactory?.startReactNative( + withModuleName: "main", + in: appWindow, + launchOptions: nil) + } + window = appWindow appWindow.windowScene = windowScene appWindow.makeKeyAndVisible() @@ -25,6 +36,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { if let url = connectionOptions.urlContexts.first?.url { _ = appDelegate.application(UIApplication.shared, open: url, options: [:]) } + + if let userActivity = connectionOptions.userActivities.first { + _ = appDelegate.application( + UIApplication.shared, + continue: userActivity, + restorationHandler: { _ in }) + } } func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index c0e95c3d3b5..7a73725bd9d 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -16,7 +16,6 @@ import { useResolveClassNames } from "uniwind"; import { LoadingScreen } from "../components/LoadingScreen"; -import { useWorkspaceState } from "../state/workspace"; import { useThreadOutboxDrain } from "../state/use-thread-outbox-drain"; import { RegistryContext } from "@effect/atom-react"; import { appAtomRegistry } from "../state/atom-registry"; @@ -47,16 +46,11 @@ function AppNavigator() { } function AppNavigatorContent() { - const { state } = useWorkspaceState(); const colorScheme = useColorScheme(); const statusBarBg = useThemeColor("--color-status-bar"); useAgentNotificationNavigation(); useThreadOutboxDrain(); - if (state.isLoadingConnections) { - return ; - } - return ( <> router.back()} + onPress={() => router.dismiss()} > diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index c6760cad044..f74f61e8f42 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -538,14 +538,6 @@ export function ThreadFilesTreeScreen() { [projectName], ); - if (selectedThread === null || environmentId === null || threadId === null) { - return ; - } - - if (cwd === null) { - return ; - } - if (fileInspector.supported) { return ( ; + } + + if (cwd === null) { + return ; + } + return ( { + if (router.canGoBack()) { + router.back(); + return; + } + if (environmentId !== null && threadId !== null) { + router.replace(buildThreadRoutePath({ environmentId, threadId })); + } + }, [environmentId, router, threadId]); const needsFileContents = relativePath !== null && (resolvedActiveMode === "source" || isMarkdownPreviewFile(relativePath)); @@ -692,6 +701,15 @@ export function ThreadFileScreen() { [cwd, environmentId, fileInspector.supported, handleSelectFile, projectName, relativePath], ); + if (fileInspector.supported && environmentId !== null && threadId !== null) { + return ( + + ); + } + if (selectedThread === null || environmentId === null || threadId === null) { return ; } diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index e69294e9ea2..5d5f36afc9c 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -418,9 +418,14 @@ function ThreadRouteContent( if (selectedThread === null) { return; } - router.push(buildThreadFilesNavigation(selectedThread, path)); + const navigation = buildThreadFilesNavigation(selectedThread, path); + if (fileInspector.supported) { + router.replace(navigation); + return; + } + router.push(navigation); }, - [router, selectedThread], + [fileInspector.supported, router, selectedThread], ); const GitInspector = useCallback( () => , From 42a2275bbddae421a67001b0a5b3920124d64faf Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:17:54 -0700 Subject: [PATCH 15/71] Fix split inspector review followups --- .../src/features/files/ThreadFilesRouteScreen.tsx | 14 ++++++-------- .../features/layout/AdaptiveWorkspaceLayout.tsx | 9 +++++---- .../src/features/threads/ThreadComposer.tsx | 15 ++++++--------- .../src/features/threads/ThreadRouteScreen.tsx | 6 +++--- 4 files changed, 20 insertions(+), 24 deletions(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index f74f61e8f42..a5e4b59e7ac 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -1,7 +1,6 @@ import Stack from "expo-router/stack"; import { SymbolView } from "expo-symbols"; import { useLocalSearchParams, useRouter } from "expo-router"; -import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; import { useCallback, useMemo, useRef, useState } from "react"; import { ActivityIndicator, @@ -463,7 +462,6 @@ function FilesToolbarBottomFade() { export function ThreadFilesTreeScreen() { useAdaptiveWorkspacePaneRole("inspector"); const router = useRouter(); - const headerHeight = useHeaderHeight(); const { fileInspector, layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const [searchQuery, setSearchQuery] = useState(""); const colorScheme = useColorScheme(); @@ -506,18 +504,18 @@ export function ThreadFilesTreeScreen() { [environmentId, fileInspector.supported, router, threadId], ); const renderInspector = useCallback( - () => + (headerInset: number) => environmentId !== null && cwd !== null ? ( ) : null, - [cwd, environmentId, handleSelectFile, headerHeight, projectName], + [cwd, environmentId, handleSelectFile, projectName], ); const handlePreviewFile = useCallback( (relativePath: string) => { @@ -687,12 +685,12 @@ export function ThreadFileScreen() { [router], ); const renderInspector = useCallback( - () => + (headerInset: number) => fileInspector.supported && environmentId !== null && cwd !== null ? ( renderInspector(0) : undefined} > { - if (!layout.usesSplitView || !fileInspector.supported || !parseActiveThreadPath(pathname)) { + const activeThread = parseActiveThreadPath(pathname); + if (!layout.usesSplitView || !fileInspector.supported || activeThread === null) { return false; } - showAuxiliaryPane("inspector"); + router.replace(buildThreadFilesNavigation(activeThread)); return true; - }, [fileInspector.supported, layout.usesSplitView, pathname, showAuxiliaryPane]); + }, [fileInspector.supported, layout.usesSplitView, pathname, router]); useHardwareKeyboardCommand("files", handleOpenFilesCommand); const toggleAuxiliaryPane = useCallback(() => { if (auxiliaryPaneRole === "inspector") { diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 4acd69e92fc..cc941e9f2bc 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -209,7 +209,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const inputRef = props.editorRef ?? fallbackInputRef; const [isFocused, setIsFocused] = useState(false); const wasExpandedBeforePreviewRef = useRef(false); - const sendInFlightRef = useRef(false); + const inFlightThreadIdsRef = useRef(new Set()); const { onExpandedChange } = props; const [previewImageUri, setPreviewImageUri] = useState(null); @@ -217,10 +217,6 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const isExpanded = isFocused; const canSend = hasContent; - useEffect(() => { - sendInFlightRef.current = false; - }, [props.selectedThread.id]); - const onPressImage = useCallback( (uri: string) => { wasExpandedBeforePreviewRef.current = isFocused; @@ -454,14 +450,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const { onChangeDraftMessage, onUpdateInteractionMode, draftMessage, onSendMessage } = props; const handleSend = useCallback(async () => { - if (!canSend || sendInFlightRef.current) return; - sendInFlightRef.current = true; + const threadId = props.selectedThread.id; + if (!canSend || inFlightThreadIdsRef.current.has(threadId)) return; + inFlightThreadIdsRef.current.add(threadId); try { await onSendMessage(); } finally { - sendInFlightRef.current = false; + inFlightThreadIdsRef.current.delete(threadId); } - }, [canSend, onSendMessage]); + }, [canSend, onSendMessage, props.selectedThread.id]); const handleCommandSelect = useCallback( (item: ComposerCommandItem) => { if (!composerTrigger) return; diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 5d5f36afc9c..f412825c8c4 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -101,7 +101,7 @@ function OpeningThreadLoadingScreen() { interface ThreadRouteScreenProps { readonly onReturnToThread?: () => void; - readonly renderInspector?: () => ReactNode; + readonly renderInspector?: (headerInset: number) => ReactNode; } function ThreadUnavailableScreen() { @@ -458,10 +458,10 @@ function ThreadRouteContent( Files={FilesInspector} Git={GitInspector} mode={inspectorMode} - Route={props.renderInspector} + Route={props.renderInspector ? () => props.renderInspector?.(headerHeight) : undefined} /> ), - [FilesInspector, GitInspector, inspectorMode, props.renderInspector], + [FilesInspector, GitInspector, headerHeight, inspectorMode, props.renderInspector], ); const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; From bcf070aa2c3bc8b5ae40b224509059fc5ddb0737 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:21:31 -0700 Subject: [PATCH 16/71] Fix file inspector route regressions --- apps/mobile/src/app/new/index.tsx | 2 +- .../features/files/ThreadFilesRouteScreen.tsx | 18 ------------------ .../threads/ThreadNavigationSidebar.tsx | 6 +++++- 3 files changed, 6 insertions(+), 20 deletions(-) diff --git a/apps/mobile/src/app/new/index.tsx b/apps/mobile/src/app/new/index.tsx index 41a7963807d..4e5339e7de2 100644 --- a/apps/mobile/src/app/new/index.tsx +++ b/apps/mobile/src/app/new/index.tsx @@ -116,7 +116,7 @@ export default function NewTaskRoute() { router.back()} + onPress={() => router.dismiss()} separateBackground /> ) : null} diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index a5e4b59e7ac..1942e5737da 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -650,15 +650,6 @@ export function ThreadFileScreen() { assetPreviewUri === null || previewRevision === 0 ? assetPreviewUri : `${assetPreviewUri}${assetPreviewUri.includes("?") ? "&" : "?"}revision=${previewRevision}`; - const handleReturnToThread = useCallback(() => { - if (router.canGoBack()) { - router.back(); - return; - } - if (environmentId !== null && threadId !== null) { - router.replace(buildThreadRoutePath({ environmentId, threadId })); - } - }, [environmentId, router, threadId]); const needsFileContents = relativePath !== null && (resolvedActiveMode === "source" || isMarkdownPreviewFile(relativePath)); @@ -699,15 +690,6 @@ export function ThreadFileScreen() { [cwd, environmentId, fileInspector.supported, handleSelectFile, projectName, relativePath], ); - if (fileInspector.supported && environmentId !== null && threadId !== null) { - return ( - - ); - } - if (selectedThread === null || environmentId === null || threadId === null) { return ; } diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index ab833848b80..ee4b5514505 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -537,7 +537,11 @@ export function ThreadNavigationSidebar(props: { style={styles.threadList} ListEmptyComponent={ - {searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} + {catalogState.isLoadingConnections + ? "Loading threads…" + : searchQuery.trim().length > 0 + ? "No matching threads" + : "No threads yet"} } /> From 43b97244180acdd27523eeb08d9bf4e351ed4af5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:24:47 -0700 Subject: [PATCH 17/71] Harden split inspector edge cases --- .../t3-review-diff/ios/T3ReviewDiffView.swift | 3 +++ .../features/files/ThreadFilesRouteScreen.tsx | 24 ++++++++++++------- .../layout/AdaptiveWorkspaceLayout.tsx | 14 +++++++---- .../src/features/threads/ThreadComposer.tsx | 4 ++-- .../features/threads/ThreadRouteScreen.tsx | 4 +++- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index bd023ff3488..31eeb9d7041 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -558,6 +558,8 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { rowsDecodeGeneration += 1 tokensDecodeGeneration += 1 contentView.tokensByRowId = [:] + rows = [] + contentView.rows = [] hasAppliedInitialRowIndex = false lastVisibleFileId = nil pendingScrollFileId = nil @@ -658,6 +660,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { contentView.invalidateVisibleViewport() contentView.setNeedsDisplay() applyInitialRowIndexIfNeeded() + applyPendingScrollIfNeeded() emitVisibleFileIfNeeded() let debugKey = "\(rows.count):\(Int(bounds.width)):\(Int(bounds.height)):\(Int(height))" diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 1942e5737da..75c71775bb6 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -536,6 +536,22 @@ export function ThreadFilesTreeScreen() { [projectName], ); + if (selectedThread === null || environmentId === null || threadId === null) { + if (fileInspector.supported) { + return ( + + ); + } + return ; + } + + if (cwd === null) { + return ; + } + if (fileInspector.supported) { return ( ; - } - - if (cwd === null) { - return ; - } - return ( { + if (environmentId === null || threadId === null) { + return null; + } + try { + return scopedThreadKey(EnvironmentId.make(environmentId), ThreadId.make(threadId)); + } catch { + return null; + } + }, [environmentId, threadId]); const activateAuxiliaryPaneRole = useCallback((role: WorkspaceAuxiliaryPaneRole) => { const owner = Symbol(role); activeRoleOwner.current = owner; diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index cc941e9f2bc..053d8b84b28 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -451,14 +451,14 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const handleSend = useCallback(async () => { const threadId = props.selectedThread.id; - if (!canSend || inFlightThreadIdsRef.current.has(threadId)) return; + if (inFlightThreadIdsRef.current.has(threadId)) return; inFlightThreadIdsRef.current.add(threadId); try { await onSendMessage(); } finally { inFlightThreadIdsRef.current.delete(threadId); } - }, [canSend, onSendMessage, props.selectedThread.id]); + }, [onSendMessage, props.selectedThread.id]); const handleCommandSelect = useCallback( (item: ComposerCommandItem) => { if (!composerTrigger) return; diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index f412825c8c4..2a96ee7ad28 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -254,7 +254,9 @@ function ThreadRouteContent( const inspectorMode = inspectorSelection?.routeThreadIdentity === routeThreadIdentity ? inspectorSelection.mode - : null; + : fileInspector.supported && selectedThreadCwd !== null + ? "files" + : null; useEffect(() => { setInspectorSelection((current) => { From bf00c7e8a6131fae404a215fe21219316918f763 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:30:36 -0700 Subject: [PATCH 18/71] Fix sidebar selection and file return --- apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx | 4 ---- apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx | 3 ++- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 75c71775bb6..5d532de6f6d 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -727,10 +727,6 @@ export function ThreadFileScreen() { accessibilityLabel="Return to chat" icon="chevron.left" onPress={() => { - if (router.canGoBack()) { - router.back(); - return; - } router.replace(buildThreadRoutePath({ environmentId, threadId })); }} /> diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index ee4b5514505..099c3ae7d85 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -399,6 +399,7 @@ export function ThreadNavigationSidebar(props: { const selectedMutedColor = IOS_SELECTED_MUTED_FOREGROUND; const pressedBackgroundColor = useThemeColor("--color-subtle"); const listThemeKey = `${colorScheme}:${String(backgroundColor)}:${String(selectedBackgroundColor)}`; + const listExtraData = `${listThemeKey}:${props.selectedThreadKey ?? ""}`; const headerFadeColor = String(backgroundColor); const topListInset = insets.top + SIDEBAR_STICKY_HEADER_HEIGHT - 6; const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { @@ -517,7 +518,7 @@ export function ThreadNavigationSidebar(props: { item.kind} keyExtractor={(item) => item.key} renderItem={renderListItem} From c2aba434a1f6f6c9ebb6a89835a75a8b0f40f0a7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:35:47 -0700 Subject: [PATCH 19/71] Polish inspector routing behavior --- .../t3-review-diff/ios/T3ReviewDiffView.swift | 1 + .../features/files/ThreadFilesRouteScreen.tsx | 10 ++++- .../layout/AdaptiveWorkspaceLayout.tsx | 6 ++- .../src/features/threads/ThreadComposer.tsx | 11 +++--- .../features/threads/ThreadRouteScreen.tsx | 38 +++++++++---------- 5 files changed, 38 insertions(+), 28 deletions(-) diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index 31eeb9d7041..a0018e54fbf 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -565,6 +565,7 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { pendingScrollFileId = nil isProgrammaticScrollActive = false scrollView.setContentOffset(.zero, animated: false) + updateContentMetrics() updateViewportFrame() applyInitialRowIndexIfNeeded() } diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 5d532de6f6d..639b6aa4269 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -1,7 +1,7 @@ import Stack from "expo-router/stack"; import { SymbolView } from "expo-symbols"; import { useLocalSearchParams, useRouter } from "expo-router"; -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, Pressable, @@ -462,7 +462,8 @@ function FilesToolbarBottomFade() { export function ThreadFilesTreeScreen() { useAdaptiveWorkspacePaneRole("inspector"); const router = useRouter(); - const { fileInspector, layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); + const { fileInspector, layout, panes, showAuxiliaryPane, togglePrimarySidebar } = + useAdaptiveWorkspaceLayout(); const [searchQuery, setSearchQuery] = useState(""); const colorScheme = useColorScheme(); const highlightTheme = colorScheme === "dark" ? "dark" : "light"; @@ -535,6 +536,11 @@ export function ThreadFilesTreeScreen() { () => , [projectName], ); + useEffect(() => { + if (fileInspector.supported) { + showAuxiliaryPane("inspector"); + } + }, [fileInspector.supported, showAuxiliaryPane]); if (selectedThread === null || environmentId === null || threadId === null) { if (fileInspector.supported) { diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 21947c6b909..d331ca3f806 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -208,9 +208,13 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) if (!layout.usesSplitView || !fileInspector.supported || activeThread === null) { return false; } + showAuxiliaryPane("inspector"); + if (/\/files(?:\/|$)/.test(pathname)) { + return true; + } router.replace(buildThreadFilesNavigation(activeThread)); return true; - }, [fileInspector.supported, layout.usesSplitView, pathname, router]); + }, [fileInspector.supported, layout.usesSplitView, pathname, router, showAuxiliaryPane]); useHardwareKeyboardCommand("files", handleOpenFilesCommand); const toggleAuxiliaryPane = useCallback(() => { if (auxiliaryPaneRole === "inspector") { diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 053d8b84b28..6f576df5c3b 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -26,6 +26,7 @@ import { } from "react-native"; import ImageViewing from "react-native-image-viewing"; import { useThemeColor } from "../../lib/useThemeColor"; +import { scopedThreadKey } from "../../lib/scopedEntities"; import { AppText as Text } from "../../components/AppText"; import { ComposerAttachmentStrip } from "../../components/ComposerAttachmentStrip"; @@ -450,15 +451,15 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer const { onChangeDraftMessage, onUpdateInteractionMode, draftMessage, onSendMessage } = props; const handleSend = useCallback(async () => { - const threadId = props.selectedThread.id; - if (inFlightThreadIdsRef.current.has(threadId)) return; - inFlightThreadIdsRef.current.add(threadId); + const threadKey = scopedThreadKey(props.environmentId, props.selectedThread.id); + if (inFlightThreadIdsRef.current.has(threadKey)) return; + inFlightThreadIdsRef.current.add(threadKey); try { await onSendMessage(); } finally { - inFlightThreadIdsRef.current.delete(threadId); + inFlightThreadIdsRef.current.delete(threadKey); } - }, [onSendMessage, props.selectedThread.id]); + }, [onSendMessage, props.environmentId, props.selectedThread.id]); const handleCommandSelect = useCallback( (item: ComposerCommandItem) => { if (!composerTrigger) return; diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 2a96ee7ad28..8c1952701ae 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,5 +1,4 @@ import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; -import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import * as Option from "effect/Option"; import { EnvironmentId, ThreadId, type ProjectScript } from "@t3tools/contracts"; @@ -223,7 +222,6 @@ function ThreadRouteContent( ) { const { fileInspector, layout, showAuxiliaryPane, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); - const headerHeight = useHeaderHeight(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = @@ -251,12 +249,18 @@ function ThreadRouteContent( const [inspectorSelection, setInspectorSelection] = useState( () => (props.renderInspector ? { routeThreadIdentity, mode: "route" } : null), ); - const inspectorMode = - inspectorSelection?.routeThreadIdentity === routeThreadIdentity - ? inspectorSelection.mode - : fileInspector.supported && selectedThreadCwd !== null - ? "files" - : null; + const inspectorMode = (() => { + if (inspectorSelection?.routeThreadIdentity === routeThreadIdentity) { + if (inspectorSelection.mode === "files" && selectedThreadCwd === null) { + return null; + } + return inspectorSelection.mode; + } + if (fileInspector.supported && selectedThreadCwd !== null) { + return "files"; + } + return null; + })(); useEffect(() => { setInspectorSelection((current) => { @@ -430,8 +434,8 @@ function ThreadRouteContent( [fileInspector.supported, router, selectedThread], ); const GitInspector = useCallback( - () => , - [headerHeight], + () => , + [], ); const FilesInspector = useCallback( () => @@ -439,19 +443,13 @@ function ThreadRouteContent( ) : null, - [ - handleSelectInspectorFile, - headerHeight, - selectedThread, - selectedThreadCwd, - selectedThreadProject?.title, - ], + [handleSelectInspectorFile, selectedThread, selectedThreadCwd, selectedThreadProject?.title], ); const renderInspectorStack = useCallback( () => @@ -460,10 +458,10 @@ function ThreadRouteContent( Files={FilesInspector} Git={GitInspector} mode={inspectorMode} - Route={props.renderInspector ? () => props.renderInspector?.(headerHeight) : undefined} + Route={props.renderInspector ? () => props.renderInspector?.(0) : undefined} /> ), - [FilesInspector, GitInspector, headerHeight, inspectorMode, props.renderInspector], + [FilesInspector, GitInspector, inspectorMode, props.renderInspector], ); const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; From 80208bc2c4a413e1013021022364345aca71550e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:44:54 -0700 Subject: [PATCH 20/71] Stabilize inspector pane state --- .../t3-review-diff/ios/T3ReviewDiffView.swift | 2 +- .../features/files/ThreadFilesRouteScreen.tsx | 4 ++-- .../features/threads/ThreadRouteScreen.tsx | 23 ++++++++++++++++--- 3 files changed, 23 insertions(+), 6 deletions(-) diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index a0018e54fbf..387ca365e39 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -561,12 +561,12 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { rows = [] contentView.rows = [] hasAppliedInitialRowIndex = false - lastVisibleFileId = nil pendingScrollFileId = nil isProgrammaticScrollActive = false scrollView.setContentOffset(.zero, animated: false) updateContentMetrics() updateViewportFrame() + lastVisibleFileId = nil applyInitialRowIndexIfNeeded() } diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 639b6aa4269..e4b9ac906a3 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -537,10 +537,10 @@ export function ThreadFilesTreeScreen() { [projectName], ); useEffect(() => { - if (fileInspector.supported) { + if (fileInspector.supported && cwd !== null) { showAuxiliaryPane("inspector"); } - }, [fileInspector.supported, showAuxiliaryPane]); + }, [cwd, fileInspector.supported, showAuxiliaryPane]); if (selectedThread === null || environmentId === null || threadId === null) { if (fileInspector.supported) { diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 8c1952701ae..2ab916f78be 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -220,7 +220,7 @@ function ThreadRouteContent( readonly selectedThreadDetailState: ReturnType; }, ) { - const { fileInspector, layout, showAuxiliaryPane, toggleAuxiliaryPane } = + const { fileInspector, layout, panes, showAuxiliaryPane, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); @@ -261,6 +261,22 @@ function ThreadRouteContent( } return null; })(); + useEffect(() => { + if ( + fileInspector.supported && + selectedThreadCwd === null && + inspectorMode === null && + panes.auxiliaryPaneVisible + ) { + toggleAuxiliaryPane(); + } + }, [ + fileInspector.supported, + inspectorMode, + panes.auxiliaryPaneVisible, + selectedThreadCwd, + toggleAuxiliaryPane, + ]); useEffect(() => { setInspectorSelection((current) => { @@ -451,6 +467,7 @@ function ThreadRouteContent( ) : null, [handleSelectInspectorFile, selectedThread, selectedThreadCwd, selectedThreadProject?.title], ); + const RouteInspector = useCallback(() => props.renderInspector?.(0), [props.renderInspector]); const renderInspectorStack = useCallback( () => inspectorMode === null ? null : ( @@ -458,10 +475,10 @@ function ThreadRouteContent( Files={FilesInspector} Git={GitInspector} mode={inspectorMode} - Route={props.renderInspector ? () => props.renderInspector?.(0) : undefined} + Route={props.renderInspector ? RouteInspector : undefined} /> ), - [FilesInspector, GitInspector, inspectorMode, props.renderInspector], + [FilesInspector, GitInspector, RouteInspector, inspectorMode, props.renderInspector], ); const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; From b83af8a48efeeba99766556a5078ecca6da7e6ef Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:54:20 -0700 Subject: [PATCH 21/71] Reveal files inspector once --- apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index e4b9ac906a3..662bcb4dca6 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -468,6 +468,7 @@ export function ThreadFilesTreeScreen() { const colorScheme = useColorScheme(); const highlightTheme = colorScheme === "dark" ? "dark" : "light"; const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(); + const revealedInspectorRef = useRef(false); const entriesQuery = useEnvironmentQuery( environmentId !== null && cwd !== null && !fileInspector.supported ? projectEnvironment.listEntries({ @@ -537,7 +538,8 @@ export function ThreadFilesTreeScreen() { [projectName], ); useEffect(() => { - if (fileInspector.supported && cwd !== null) { + if (fileInspector.supported && cwd !== null && !revealedInspectorRef.current) { + revealedInspectorRef.current = true; showAuxiliaryPane("inspector"); } }, [cwd, fileInspector.supported, showAuxiliaryPane]); From 873a000567a8b3b8f2a249340c9dd6e42e76e0d2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 13:10:36 -0700 Subject: [PATCH 22/71] Adopt native iOS glass navigation chrome --- apps/mobile/src/app/_layout.tsx | 80 +- apps/mobile/src/app/debug/rns-glass.tsx | 1114 ++++++++++++++ apps/mobile/src/app/index.tsx | 41 +- apps/mobile/src/features/home/HomeHeader.tsx | 288 ++-- apps/mobile/src/features/home/HomeScreen.tsx | 428 ++++-- .../features/home/home-list-filter-menu.ts | 120 ++ .../layout/workspace-sidebar-toolbar.tsx | 8 +- .../features/threads/ThreadGitControls.tsx | 23 + .../threads/ThreadNavigationSidebar.tsx | 305 +++- .../features/threads/ThreadRouteScreen.tsx | 91 +- .../threads/sidebar-filter-button.tsx | 14 +- .../threads/sidebar-header-actions.ios.tsx | 12 +- .../threads/sidebar-header-actions.tsx | 28 +- apps/mobile/src/lib/ios-native-chrome.ts | 10 + .../src/lib/native-scroll-edge-effect.ts | 11 +- .../project.pbxproj | 285 ++++ .../MessagesGlassLab/ContentView.swift | 386 +++++ .../MessagesGlassLabApp.swift | 10 + patches/expo-router@56.2.11.patch | 90 ++ patches/react-native-screens@4.25.2.patch | 1287 ++++++++++++++++- pnpm-lock.yaml | 670 +++++---- pnpm-workspace.yaml | 15 + 22 files changed, 4572 insertions(+), 744 deletions(-) create mode 100644 apps/mobile/src/app/debug/rns-glass.tsx create mode 100644 apps/mobile/src/features/home/home-list-filter-menu.ts create mode 100644 apps/mobile/src/lib/ios-native-chrome.ts create mode 100644 experiments/messages-glass-lab/MessagesGlassLab.xcodeproj/project.pbxproj create mode 100644 experiments/messages-glass-lab/MessagesGlassLab/ContentView.swift create mode 100644 experiments/messages-glass-lab/MessagesGlassLab/MessagesGlassLabApp.swift create mode 100644 patches/expo-router@56.2.11.patch diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 7a73725bd9d..785bc0b441f 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -1,14 +1,20 @@ -import "../../global.css"; import { DMSans_400Regular, DMSans_500Medium, DMSans_700Bold, useFonts, } from "@expo-google-fonts/dm-sans"; -import { usePathname } from "expo-router"; +import { usePathname, useRouter } from "expo-router"; import Stack from "expo-router/stack"; -import { useCallback } from "react"; -import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; +import { useCallback, useEffect } from "react"; +import { + LogBox, + Platform, + Settings, + StatusBar, + useColorScheme, + useWindowDimensions, +} from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -33,6 +39,17 @@ import { deriveStableFormSheetDetent } from "../lib/layout"; import { useThemeColor } from "../lib/useThemeColor"; import { HardwareKeyboardCommandProvider } from "../features/keyboard/HardwareKeyboardCommandProvider"; +const debugLaunchRoute = + __DEV__ && Platform.OS === "ios" ? Settings.get("T3DebugRoute") : undefined; + +if (debugLaunchRoute !== "/debug/rns-glass") { + require("../../global.css"); +} + +if (debugLaunchRoute === "/debug/rns-glass") { + LogBox.ignoreAllLogs(true); +} + function AppNavigator() { const pathname = usePathname(); const expandedSettingsRouteIsActive = @@ -46,11 +63,58 @@ function AppNavigator() { } function AppNavigatorContent() { + const pathname = usePathname(); + const isDebugRoute = pathname.startsWith("/debug/") || debugLaunchRoute === "/debug/rns-glass"; + + if (isDebugRoute) { + return ; + } + + return ; +} + +function DebugNavigatorHost(props: { readonly pathname: string }) { + const router = useRouter(); + + useEffect(() => { + if (debugLaunchRoute === "/debug/rns-glass" && props.pathname !== "/debug/rns-glass") { + router.replace("/debug/rns-glass" as never); + } + }, [props.pathname, router]); + + return ( + <> + + + + + + ); +} + +function WorkspaceNavigatorHost(props: { readonly pathname: string }) { const colorScheme = useColorScheme(); + const pathname = props.pathname; + const router = useRouter(); const statusBarBg = useThemeColor("--color-status-bar"); useAgentNotificationNavigation(); useThreadOutboxDrain(); + useEffect(() => { + if (!__DEV__ || Platform.OS !== "ios") return; + const debugRoute = Settings.get("T3DebugRoute"); + if (typeof debugRoute !== "string" || debugRoute.length === 0 || pathname === debugRoute) + return; + router.replace(debugRoute as never); + }, [pathname, router]); + return ( <> + ({ + id: `debug-code-line-${index}`, + line, +})); + +const THREADS = [ + { + id: "markdown", + initials: "MD", + title: "Markdown rendering test", + subtitle: "Renderer stress test, native header, and glass sampling", + time: "14h", + color: "#0A84FF", + }, + { + id: "ipad", + initials: "IP", + title: "iPad rectly text correction", + subtitle: "Split layout, sidebar search, keyboard and trackpad scroll", + time: "16h", + color: "#BF5AF2", + }, + { + id: "webview", + initials: "WV", + title: "Preview Webview Persists Off Panel", + subtitle: "Browser surface, native columns, and panel ownership", + time: "22m", + color: "#30D158", + }, + { + id: "diff", + initials: "DF", + title: "Diff renderer pass", + subtitle: "Review comments and code highlighting in wide layouts", + time: "2d", + color: "#FF9F0A", + }, + { + id: "terminal", + initials: "TY", + title: "Terminal pty keyboard routing", + subtitle: "Tab key, focus behavior, and hardware keyboard shortcuts", + time: "3d", + color: "#FF375F", + }, + { + id: "files", + initials: "FX", + title: "File explorer polish", + subtitle: "Sidebar disclosure, previews, and trackpad scrolling", + time: "4d", + color: "#64D2FF", + }, +] as const; + +type ThreadId = (typeof THREADS)[number]["id"]; + +const SIDEBAR_THREADS = THREADS.flatMap((thread) => [ + { ...thread, rowId: `${thread.id}-primary`, selectedCandidate: true }, + { ...thread, rowId: `${thread.id}-repeat`, selectedCandidate: false }, +]); + +interface DebugColors { + readonly background: string; + readonly card: string; + readonly foreground: string; + readonly secondary: string; + readonly separator: string; +} + +export default function RnsGlassDebugRoute() { + const colorScheme = useColorScheme(); + const isDark = colorScheme !== "light"; + const { width } = useWindowDimensions(); + const scrollRef = useRef(null); + const sidebarScrollRef = useRef(null); + const [searchQuery, setSearchQuery] = useState(""); + const [selectedThreadId, setSelectedThreadId] = useState("markdown"); + const usesWideSplit = Platform.OS === "ios" && width >= 700; + + useEffect(() => { + const stops = [0, 0, 190, 190, 0, 360, 80, 0] as const; + let index = 0; + + const reset = () => { + scrollRef.current?.scrollTo({ + animated: false, + y: 0, + }); + sidebarScrollRef.current?.scrollTo({ + animated: false, + y: 0, + }); + }; + + reset(); + const postLayoutReset = setTimeout(reset, 250); + + const tick = () => { + scrollRef.current?.scrollTo({ + animated: true, + y: stops[index % stops.length], + }); + sidebarScrollRef.current?.scrollTo({ + animated: true, + y: index % 2 === 0 ? 0 : 150, + }); + index += 1; + }; + + const initial = setTimeout(tick, 4200); + const interval = setInterval(tick, 4200); + + return () => { + clearTimeout(initial); + clearTimeout(postLayoutReset); + clearInterval(interval); + }; + }, []); + + const foreground = isDark ? "#F5F5F7" : "#111114"; + const secondary = isDark ? "rgba(245,245,247,0.62)" : "rgba(17,17,20,0.58)"; + const background = isDark ? "#050507" : "#F4F4F7"; + const card = isDark ? "#10131B" : "#FFFFFF"; + const separator = isDark ? "rgba(255,255,255,0.14)" : "rgba(0,0,0,0.12)"; + const colors = { background, card, foreground, secondary, separator }; + + return ( + <> + + + {usesWideSplit ? ( + + ) : ( + + )} + + + ); +} + +function SidebarColumn(props: { + readonly compact: boolean; + readonly colors: DebugColors; + readonly onSelectThread: (threadId: ThreadId) => void; + readonly onSearchQueryChange: (query: string) => void; + readonly searchQuery: string; + readonly scrollRef: React.RefObject; + readonly selectedThreadId: ThreadId; +}) { + const { colors } = props; + const insets = useSafeAreaInsets(); + const compactTopInset = 18; + const headerButtonTint = + colors.background === "#050507" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; + + if (props.compact) { + return ( + + + + + + {}, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + ] as ComponentProps["headerRightBarButtonItems"] + } + headerToolbarItems={ + [ + { + composeButtonId: "rns-glass-compose", + composeSystemImageName: "square.and.pencil", + filterButtonId: "rns-glass-filter", + filterSystemImageName: "line.3.horizontal.decrease", + onSearchTextChange: props.onSearchQueryChange, + placeholder: "Search", + searchTextChangeId: "rns-glass-search-text", + type: "mailSearchToolbar", + useFallbackSearchField: true, + }, + ] as ComponentProps["headerToolbarItems"] + } + hideBackButton + hideShadow={false} + largeTitle={false} + navigationItemStyle="editor" + subtitle="t3code · Ready" + title="Threads" + titleColor={colors.foreground} + titleFontSize={18} + titleFontWeight="800" + translucent + > + + + ); + } + + return ( + + + + + + {props.compact ? null : ( + + + + + + )} + + + ); +} + +function MailWideSplitDemo(props: { + readonly colors: DebugColors; + readonly detailScrollRef: React.RefObject; + readonly onSearchQueryChange: (query: string) => void; + readonly onSelectThread: (threadId: ThreadId) => void; + readonly searchQuery: string; + readonly selectedThreadId: ThreadId; + readonly sidebarScrollRef: React.RefObject; +}) { + const { colors } = props; + + return ( + + + + + + + + + ); +} + +function MailWideSidebarNativePane(props: { + readonly colors: DebugColors; + readonly onSearchQueryChange: (query: string) => void; + readonly onSelectThread: (threadId: ThreadId) => void; + readonly searchQuery: string; + readonly scrollRef: React.RefObject; + readonly selectedThreadId: ThreadId; +}) { + const { colors } = props; + const insets = useSafeAreaInsets(); + const headerInset = insets.top + 76; + const headerButtonTint = + colors.background === "#050507" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; + + return ( + + + + + + {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + { + accessibilityLabel: "More sidebar options", + icon: { name: "ellipsis", type: "sfSymbol" }, + identifier: "rns-glass-ipad-more", + onPress: () => {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + ] as ComponentProps["headerRightBarButtonItems"] + } + hideBackButton + hideShadow={false} + largeTitle={false} + navigationItemStyle="editor" + subtitle="t3code · Ready" + title="Threads" + titleColor={colors.foreground} + titleFontSize={17} + titleFontWeight="800" + translucent + /> + + + ); +} + +function MailWideDetailNativePane(props: { + readonly colors: DebugColors; + readonly onSearchQueryChange: (query: string) => void; + readonly scrollRef: React.RefObject; +}) { + const { colors } = props; + const insets = useSafeAreaInsets(); + const headerInset = insets.top + 88; + const headerButtonTint = + colors.background === "#050507" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; + + return ( + + + + + + Yoooo respond with a bunch of markdown so i can test rendering stuff. Do some tool + calls first to research the project a bit, then respond with a long markdown text + + + + {SWATCHES.map((color, index) => ( + + {index + 1} + + ))} + + + + T3 Code Renderer Stress Test + + + This wide spike maps T3 Code surfaces onto the Mail iPad header structure: sidebar + controls stay with the list, thread controls stay with the detail pane, and the + content keeps scrolling underneath the glass. + + + + {CODE_ROWS.map(({ id, line }, index) => ( + + {index + 1} + {line || " "} + + ))} + + + {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + { + accessibilityLabel: "Open files", + icon: { name: "folder", type: "sfSymbol" }, + identifier: "rns-glass-ipad-files", + onPress: () => {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + { + accessibilityLabel: "Open terminal", + icon: { name: "terminal", type: "sfSymbol" }, + identifier: "rns-glass-ipad-terminal", + onPress: () => {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + { + accessibilityLabel: "New chat", + icon: { name: "square.and.pencil", type: "sfSymbol" }, + identifier: "rns-glass-ipad-compose", + onPress: () => {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + { + accessibilityLabel: "Expand detail", + icon: { name: "arrow.up.left.and.arrow.down.right", type: "sfSymbol" }, + identifier: "rns-glass-ipad-expand", + onPress: () => {}, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + }, + ] as ComponentProps["headerLeftBarButtonItems"] + } + headerRightBarButtonItems={ + [ + { + activatesSearchController: true, + type: "searchBarPlacement", + }, + ] as ComponentProps["headerRightBarButtonItems"] + } + hideBackButton + hideShadow={false} + largeTitle={false} + navigationItemStyle="editor" + subtitle="t3code · Julius’s Mac mini" + title="Markdown rendering test" + titleColor={colors.foreground} + titleFontSize={17} + titleFontWeight="800" + translucent + > + + ) => { + props.onSearchQueryChange(event.nativeEvent.text ?? ""); + }} + placement="automatic" + placeholder="Search" + textColor={colors.foreground} + tintColor={colors.foreground} + /> + + + + + ); +} + +function SidebarThreadRows(props: { + readonly compact: boolean; + readonly colors: DebugColors; + readonly highlightsSelection?: boolean; + readonly onSelectThread: (threadId: ThreadId) => void; + readonly searchQuery?: string; + readonly selectedThreadId: ThreadId; +}) { + const { colors } = props; + const normalizedQuery = props.searchQuery?.trim().toLocaleLowerCase() ?? ""; + const threads = + normalizedQuery.length === 0 + ? SIDEBAR_THREADS + : SIDEBAR_THREADS.filter((thread) => + `${thread.title} ${thread.subtitle}`.toLocaleLowerCase().includes(normalizedQuery), + ); + + return ( + <> + t3code + {threads.map((thread) => { + const selected = + props.highlightsSelection !== false && + thread.id === props.selectedThreadId && + thread.selectedCandidate; + return ( + props.onSelectThread(thread.id)} + style={[ + props.compact ? styles.mailThreadRow : styles.threadRow, + selected + ? { backgroundColor: props.compact ? "#0A84FF" : colors.card } + : { backgroundColor: "rgba(255,255,255,0)" }, + ]} + > + + {thread.initials} + + + + + {thread.title} + + + {thread.time} + + + + {thread.subtitle} + + + + ); + })} + + ); +} + +const styles = StyleSheet.create({ + avatar: { + alignItems: "center", + borderRadius: 32, + height: 64, + justifyContent: "center", + width: 64, + }, + avatarText: { + color: "white", + fontSize: 22, + fontWeight: "800", + }, + body: { + fontSize: 17, + lineHeight: 24, + }, + card: { + borderRadius: 28, + borderWidth: StyleSheet.hairlineWidth, + gap: 10, + marginHorizontal: 18, + marginTop: 22, + padding: 22, + }, + cardTitle: { + fontSize: 22, + fontWeight: "700", + letterSpacing: -0.4, + }, + codeCard: { + borderRadius: 24, + borderWidth: StyleSheet.hairlineWidth, + marginHorizontal: 18, + marginTop: 22, + overflow: "hidden", + paddingVertical: 14, + }, + codeLine: { + flexDirection: "row", + gap: 16, + minHeight: 28, + paddingHorizontal: 16, + }, + codeText: { + flex: 1, + fontFamily: Platform.select({ default: "Menlo", ios: "Menlo" }), + fontSize: 16, + lineHeight: 25, + }, + compactListContent: { + paddingBottom: 150, + paddingHorizontal: 18, + }, + glassIconButton: { + alignItems: "center", + justifyContent: "center", + }, + glassIconGroup: { + borderRadius: 28, + flexDirection: "row", + overflow: "hidden", + }, + glassIconGroupItem: { + alignItems: "center", + justifyContent: "center", + }, + hero: { + gap: 10, + paddingHorizontal: 18, + paddingTop: 8, + }, + host: { + flex: 1, + }, + kicker: { + fontSize: 15, + fontWeight: "700", + letterSpacing: 0.5, + textTransform: "uppercase", + }, + lineNumber: { + fontFamily: Platform.select({ default: "Menlo", ios: "Menlo" }), + fontSize: 16, + lineHeight: 25, + textAlign: "right", + width: 34, + }, + detailMessageBubble: { + alignSelf: "flex-end", + backgroundColor: "#0A84FF", + borderRadius: 28, + marginHorizontal: 28, + marginTop: 8, + maxWidth: 720, + paddingHorizontal: 24, + paddingVertical: 16, + }, + detailMessageText: { + color: "white", + fontSize: 20, + lineHeight: 28, + }, + mailThreadRow: { + borderRadius: 24, + flexDirection: "row", + gap: 14, + minHeight: 96, + paddingHorizontal: 14, + paddingVertical: 12, + }, + mailThreadSubtitle: { + fontSize: 17, + lineHeight: 22, + }, + mailThreadTime: { + fontSize: 17, + lineHeight: 24, + }, + mailThreadTitle: { + flex: 1, + fontSize: 20, + fontWeight: "800", + letterSpacing: -0.3, + lineHeight: 25, + }, + mailBottomChrome: { + alignItems: "center", + bottom: 0, + elevation: 20, + flexDirection: "row", + gap: 12, + left: 16, + pointerEvents: "box-none", + position: "absolute", + right: 16, + zIndex: 20, + }, + mailNavTitle: { + fontSize: 22, + fontWeight: "800", + letterSpacing: -0.5, + lineHeight: 26, + }, + mailNavSubtitle: { + fontSize: 14, + fontWeight: "600", + lineHeight: 18, + }, + mailSearchInput: { + flex: 1, + fontSize: 28, + fontWeight: "600", + height: 58, + padding: 0, + }, + mailSearchPill: { + alignItems: "center", + borderRadius: 32, + flex: 1, + flexDirection: "row", + gap: 10, + height: 64, + paddingHorizontal: 20, + }, + mailTitleBlock: { + flex: 1, + minWidth: 0, + }, + mailTopActions: { + flexDirection: "row", + gap: 10, + }, + mailTopChrome: { + elevation: 20, + left: 0, + pointerEvents: "box-none", + position: "absolute", + right: 0, + top: 0, + zIndex: 20, + }, + mailTopGlass: { + borderBottomWidth: StyleSheet.hairlineWidth, + borderRadius: 0, + flex: 1, + }, + mailTopRow: { + alignItems: "center", + flexDirection: "row", + gap: 16, + height: 58, + paddingHorizontal: 20, + }, + screen: { + flex: 1, + }, + detailPane: { + flex: 1, + }, + scrollContent: { + paddingBottom: 80, + }, + scrollView: { + flex: 1, + }, + sidebarContent: { + paddingBottom: 60, + paddingHorizontal: 14, + }, + sidebarPane: { + borderRightWidth: StyleSheet.hairlineWidth, + width: 390, + }, + wideDetailHeaderRow: { + alignItems: "center", + flexDirection: "row", + gap: 18, + height: 64, + paddingHorizontal: 20, + }, + wideDetailLeftActions: { + flexDirection: "row", + gap: 10, + }, + wideDetailSpacer: { + flex: 1, + minWidth: 24, + }, + wideSidebarContent: { + paddingHorizontal: 18, + }, + wideSidebarHeaderRow: { + alignItems: "center", + flexDirection: "row", + gap: 14, + height: 62, + paddingHorizontal: 18, + }, + wideSidebarPane: { + borderRightWidth: StyleSheet.hairlineWidth, + width: 420, + }, + wideSidebarSubtitle: { + fontSize: 15, + fontWeight: "700", + lineHeight: 19, + }, + wideSidebarTitle: { + fontSize: 26, + fontWeight: "800", + letterSpacing: -0.6, + lineHeight: 31, + }, + sidebarSection: { + fontSize: 15, + fontWeight: "700", + letterSpacing: 0.2, + marginBottom: 8, + paddingHorizontal: 8, + textTransform: "lowercase", + }, + stack: { + flex: 1, + }, + swatch: { + alignItems: "center", + borderRadius: 30, + height: 60, + justifyContent: "center", + width: 60, + }, + swatchRow: { + flexDirection: "row", + flexWrap: "wrap", + gap: 12, + paddingHorizontal: 22, + paddingTop: 24, + }, + swatchText: { + color: "white", + fontSize: 20, + fontWeight: "800", + }, + splitTitle: { + fontSize: 32, + fontWeight: "800", + letterSpacing: -0.8, + lineHeight: 36, + }, + threadCopy: { + flex: 1, + gap: 3, + }, + threadRow: { + borderRadius: 18, + flexDirection: "row", + gap: 14, + minHeight: 92, + paddingHorizontal: 12, + paddingVertical: 12, + }, + threadSubtitle: { + fontSize: 15, + lineHeight: 19, + }, + threadTime: { + fontSize: 15, + lineHeight: 22, + }, + threadTitle: { + flex: 1, + fontSize: 18, + fontWeight: "700", + letterSpacing: -0.2, + lineHeight: 22, + }, + threadTitleRow: { + alignItems: "center", + flexDirection: "row", + gap: 8, + }, + title: { + fontSize: 48, + fontWeight: "800", + letterSpacing: -1.6, + lineHeight: 52, + }, + wideSplit: { + flex: 1, + flexDirection: "row", + }, +}); diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index a75507e03df..cc1b9632d9f 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -2,6 +2,7 @@ import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { Stack, useRouter } from "expo-router"; import { useMemo, useState } from "react"; +import { Platform } from "react-native"; import { useProjects, useThreadShells } from "../state/entities"; import { useWorkspaceState } from "../state/workspace"; @@ -72,30 +73,42 @@ export default function HomeRouteScreen() { return ( <> - router.push("/settings")} - onProjectGroupingModeChange={setProjectGroupingMode} - onProjectSortOrderChange={setProjectSortOrder} - onSearchQueryChange={setSearchQuery} - onStartNewTask={() => router.push("/new")} - onThreadSortOrderChange={setThreadSortOrder} - /> + {Platform.OS === "ios" ? ( + + ) : ( + router.push("/settings")} + onProjectGroupingModeChange={setProjectGroupingMode} + onProjectSortOrderChange={setProjectSortOrder} + onSearchQueryChange={setSearchQuery} + onStartNewTask={() => router.push("/new")} + onThreadSortOrderChange={setThreadSortOrder} + /> + )} router.push("/connections/new")} onArchiveThread={archiveThread} onDeleteThread={confirmDeleteThread} + onEnvironmentChange={setSelectedEnvironmentId} onOpenEnvironments={() => router.push("/settings/environments")} + onOpenSettings={() => router.push("/settings")} + onProjectGroupingModeChange={setProjectGroupingMode} + onProjectSortOrderChange={setProjectSortOrder} + onSearchQueryChange={setSearchQuery} onSelectThread={(thread) => { router.push(buildThreadRoutePath(thread)); }} + onStartNewTask={() => router.push("/new")} + onThreadSortOrderChange={setThreadSortOrder} projectGroupingMode={listOptions.projectGroupingMode} projects={projects} projectSortOrder={listOptions.projectSortOrder} diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 152b01b37f5..8c565522d5b 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -5,13 +5,17 @@ import type { } from "@t3tools/contracts"; import { Stack } from "expo-router"; import { useCallback, useRef } from "react"; -import { Text as RNText, View } from "react-native"; +import { Platform, useColorScheme } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; -import type { HomeProjectSortOrder } from "./homeThreadList"; +import { iosNativeGlassButtonTint } from "../../lib/ios-native-chrome"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import type { HomeProjectSortOrder } from "./homeThreadList"; +import { + buildHomeListFilterMenu, + type HomeListFilterMenuEnvironment, +} from "./home-list-filter-menu"; import { hasCustomHomeListOptions, PROJECT_GROUPING_OPTIONS, @@ -19,10 +23,7 @@ import { THREAD_SORT_OPTIONS, } from "./home-list-options"; -export interface HomeHeaderEnvironment { - readonly environmentId: EnvironmentId; - readonly label: string; -} +export type HomeHeaderEnvironment = HomeListFilterMenuEnvironment; export function HomeHeader(props: { readonly environments: ReadonlyArray; @@ -39,15 +40,16 @@ export function HomeHeader(props: { readonly onStartNewTask: () => void; }) { const searchBarRef = useRef(null); + const colorScheme = useColorScheme(); const iconColor = useThemeColor("--color-icon"); - const mutedColor = useThemeColor("--color-foreground-muted"); - const subtleColor = useThemeColor("--color-subtle"); + const headerButtonTint = iosNativeGlassButtonTint(colorScheme); const hasCustomListOptions = hasCustomHomeListOptions(props); const focusSearch = useCallback(() => { searchBarRef.current?.focus(); return searchBarRef.current !== null; }, []); useHardwareKeyboardCommand("focusSearch", focusSearch); + const filterMenu = buildHomeListFilterMenu(props); return ( <> @@ -55,160 +57,162 @@ export function HomeHeader(props: { options={{ headerShown: true, headerTransparent: true, - headerStyle: { backgroundColor: "transparent" }, headerShadowVisible: false, + headerLargeTitle: false, + headerStyle: { backgroundColor: "transparent" }, headerTintColor: iconColor, - headerTitle: "", + headerTitle: "Threads", + headerTitleStyle: { + fontSize: 18, + fontWeight: "800", + }, + unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, + unstable_headerRightItems: + Platform.OS === "ios" + ? () => [ + { + accessibilityLabel: "Open settings", + icon: { name: "gearshape", type: "sfSymbol" }, + identifier: "home-settings", + label: "", + onPress: props.onOpenSettings, + sharesBackground: true, + tintColor: headerButtonTint, + type: "button", + variant: "prominent", + width: 58, + }, + ] + : undefined, + unstable_headerToolbarItems: + Platform.OS === "ios" + ? () => [ + { + composeButtonId: "home-new-task", + composeSystemImageName: "square.and.pencil", + filterMenu, + filterSystemImageName: hasCustomListOptions + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease", + onComposePress: props.onStartNewTask, + placeholder: "Search", + type: "mailSearchToolbar", + }, + ] + : undefined, headerSearchBarOptions: { ref: searchBarRef, - placeholder: "Search threads", + allowToolbarIntegration: true, hideNavigationBar: false, - onChangeText: (event) => { - props.onSearchQueryChange(event.nativeEvent.text); - }, + placeholder: "Search", onCancelButtonPress: () => { props.onSearchQueryChange(""); }, - allowToolbarIntegration: true, + onChangeText: (event) => { + props.onSearchQueryChange(event.nativeEvent.text); + }, }, }} /> - - - - - T3 Code - - - - Alpha - - - - - + {Platform.OS === "ios" ? null : ( + + + + )} - - - - Environment - props.onEnvironmentChange(null)} - subtitle="Show threads from every environment" - > - All environments + {Platform.OS === "ios" ? null : ( + + + + Settings - {props.environments.map((environment) => ( - props.onEnvironmentChange(environment.environmentId)} - > - {environment.label} - - ))} - - - - Sort projects - {PROJECT_SORT_OPTIONS.map((option) => ( - props.onProjectSortOrderChange(option.value)} - > - {option.label} - - ))} - - - Sort threads - {THREAD_SORT_OPTIONS.map((option) => ( + + Environment props.onThreadSortOrderChange(option.value)} + isOn={props.selectedEnvironmentId === null} + onPress={() => props.onEnvironmentChange(null)} + subtitle="Show threads from every environment" > - {option.label} + All environments - ))} - + {props.environments.map((environment) => ( + props.onEnvironmentChange(environment.environmentId)} + > + {environment.label} + + ))} + - - Group projects - {PROJECT_GROUPING_OPTIONS.map((option) => ( - props.onProjectGroupingModeChange(option.value)} - subtitle={option.subtitle} - > - {option.label} - - ))} - - + + Sort projects + {PROJECT_SORT_OPTIONS.map((option) => ( + props.onProjectSortOrderChange(option.value)} + > + {option.label} + + ))} + - - + + Sort threads + {THREAD_SORT_OPTIONS.map((option) => ( + props.onThreadSortOrderChange(option.value)} + > + {option.label} + + ))} + - - - - - + + Group projects + {PROJECT_GROUPING_OPTIONS.map((option) => ( + props.onProjectGroupingModeChange(option.value)} + subtitle={option.subtitle} + > + {option.label} + + ))} + + + + + + + + )} ); } diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 59a002d18a9..95f8186dbe9 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -9,8 +9,15 @@ import type { } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; import { useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; -import { ActivityIndicator, Pressable, ScrollView, useWindowDimensions, View } from "react-native"; -import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import { + ActivityIndicator, + Platform, + Pressable, + ScrollView, + useColorScheme, + useWindowDimensions, + View, +} from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import Animated, { Easing, @@ -20,7 +27,10 @@ import Animated, { withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; +import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; +import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { iosNativeGlassButtonTint } from "../../lib/ios-native-chrome"; import { AppText as Text } from "../../components/AppText"; import { EmptyState } from "../../components/EmptyState"; @@ -29,6 +39,11 @@ import type { WorkspaceState } from "../../state/workspaceModel"; import type { SavedRemoteConnection } from "../../lib/connection"; import { relativeTime } from "../../lib/time"; import { threadStatusTone } from "../threads/threadPresentation"; +import { + buildHomeListFilterMenu, + type HomeListFilterMenuEnvironment, +} from "./home-list-filter-menu"; +import { hasCustomHomeListOptions } from "./home-list-options"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; import { ThreadSwipeable } from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; @@ -41,13 +56,21 @@ interface HomeScreenProps { readonly threads: ReadonlyArray; readonly catalogState: WorkspaceState; readonly savedConnectionsById: Readonly>; + readonly environments: ReadonlyArray; readonly searchQuery: string; readonly selectedEnvironmentId: EnvironmentId | null; readonly projectSortOrder: HomeProjectSortOrder; readonly threadSortOrder: SidebarThreadSortOrder; readonly projectGroupingMode: SidebarProjectGroupingMode; + readonly onSearchQueryChange: (query: string) => void; + readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; + readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void; + readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void; + readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void; readonly onAddConnection: () => void; readonly onOpenEnvironments: () => void; + readonly onOpenSettings: () => void; + readonly onStartNewTask: () => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; readonly onArchiveThread: (thread: EnvironmentThreadShell) => void; readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; @@ -58,19 +81,20 @@ interface HomeScreenProps { function statusColors(thread: EnvironmentThreadShell): { bg: string; fg: string } { switch (thread.session?.status) { case "running": - return { bg: "rgba(249,115,22,0.14)", fg: "#f97316" }; + return { bg: "rgba(249,115,22,0.22)", fg: "#ff9f0a" }; case "ready": - return { bg: "rgba(34,197,94,0.14)", fg: "#22c55e" }; + return { bg: "rgba(48,209,88,0.22)", fg: "#30d158" }; case "starting": - return { bg: "rgba(59,130,246,0.14)", fg: "#3b82f6" }; + return { bg: "rgba(10,132,255,0.22)", fg: "#0a84ff" }; case "error": - return { bg: "rgba(239,68,68,0.14)", fg: "#ef4444" }; + return { bg: "rgba(255,69,58,0.22)", fg: "#ff453a" }; default: - return { bg: "rgba(163,163,163,0.10)", fg: "#a3a3a3" }; + return { bg: "rgba(142,142,147,0.22)", fg: "#8e8e93" }; } } const COLLAPSED_THREAD_LIMIT = 6; +const HOME_COMPACT_HEADER_THRESHOLD = 48; const THREAD_LAYOUT_TRANSITION = LinearTransition.duration(220).easing(Easing.out(Easing.cubic)); function threadRowExit(values: ExitAnimationsValues) { @@ -174,7 +198,7 @@ function ProjectGroupLabel(props: { const hiddenCount = props.totalThreadCount - COLLAPSED_THREAD_LIMIT; return ( - + @@ -203,6 +227,10 @@ function ProjectGroupLabel(props: { ); } +function HomeTopContentSpacer(props: { readonly topInset: number }) { + return ; +} + /* ─── Thread row ─────────────────────────────────────────────────────── */ function ThreadRow(props: { @@ -221,7 +249,7 @@ function ThreadRow(props: { const { width: windowWidth } = useWindowDimensions(); const separatorColor = useThemeColor("--color-separator"); const iconSubtleColor = useThemeColor("--color-icon-subtle"); - const cardColor = useThemeColor("--color-card"); + const screenColor = useThemeColor("--color-screen"); const { bg, fg } = statusColors(props.thread); const tone = threadStatusTone(props.thread); const timestamp = relativeTime( @@ -234,7 +262,7 @@ function ThreadRow(props: { return ( { close(); props.onPress(); @@ -263,32 +291,39 @@ function ThreadRow(props: { - + - + {props.thread.title} @@ -303,11 +338,17 @@ function ThreadRow(props: { {timestamp} + @@ -319,11 +360,7 @@ function ThreadRow(props: { tintColor={iconSubtleColor} type="monochrome" /> - + {subtitleParts.join(" · ")} @@ -341,10 +378,26 @@ function ThreadRow(props: { export function HomeScreen(props: HomeScreenProps) { const [expandedProjects, setExpandedProjects] = useState>(() => new Set()); const openSwipeableRef = useRef(null); - const homeScrollGesture = useMemo(() => Gesture.Native(), []); const insets = useSafeAreaInsets(); + const { height: windowHeight } = useWindowDimensions(); const accentColor = useThemeColor("--color-icon-muted"); - + const foregroundColor = useThemeColor("--color-foreground"); + const screenColor = useThemeColor("--color-screen"); + const colorScheme = useColorScheme(); + const nativeHeaderButtonTint = iosNativeGlassButtonTint(colorScheme); + const hasCustomListOptions = hasCustomHomeListOptions(props); + const filterMenu = buildHomeListFilterMenu({ + environments: props.environments, + selectedEnvironmentId: props.selectedEnvironmentId, + projectSortOrder: props.projectSortOrder, + threadSortOrder: props.threadSortOrder, + projectGroupingMode: props.projectGroupingMode, + onEnvironmentChange: props.onEnvironmentChange, + onProjectSortOrderChange: props.onProjectSortOrderChange, + onThreadSortOrderChange: props.onThreadSortOrderChange, + onProjectGroupingModeChange: props.onProjectGroupingModeChange, + onOpenSettings: props.onOpenSettings, + }); const toggleExpanded = useCallback((key: string) => { setExpandedProjects((prev) => { const next = new Set(prev); @@ -366,7 +419,6 @@ export function HomeScreen(props: HomeScreenProps) { openSwipeableRef.current = null; } }, []); - const projectGroups = useMemo( () => buildHomeThreadGroups({ @@ -404,124 +456,202 @@ export function HomeScreen(props: HomeScreenProps) { projectCount: props.projects.length, }); - return ( - - - openSwipeableRef.current?.close()} - className="flex-1" - contentContainerStyle={{ - paddingHorizontal: 16, - paddingTop: 8, - paddingBottom: 24, - gap: 20, - }} - > - {!hasAnyThreads ? ( - - - {emptyState.loading ? ( - - - - ) : null} - - ) : !hasResults && hasSearchQuery ? ( - - ) : !hasResults && selectedEnvironmentLabel ? ( - - ) : !hasResults ? ( - - ) : ( - projectGroups.map((group) => { - const isExpanded = expandedProjects.has(group.key); - const visibleThreads = isExpanded - ? group.threads - : group.threads.slice(0, COLLAPSED_THREAD_LIMIT); - - return ( - - toggleExpanded(group.key)} - project={group.representative} - title={group.title} - totalThreadCount={group.threads.length} - /> - - {visibleThreads.map((thread, i) => { - const threadKey = `${thread.environmentId}:${thread.id}`; - return ( - - props.onArchiveThread(thread)} - onDelete={() => props.onDeleteThread(thread)} - onPress={() => props.onSelectThread(thread)} - onSwipeableClose={handleSwipeableClose} - onSwipeableWillOpen={handleSwipeableWillOpen} - simultaneousSwipeGesture={homeScrollGesture} - /> - - ); - })} - - - ); - }) - )} - - - {shouldShowConnectionStatus ? ( - - + {Platform.OS === "ios" ? null : } + + {!hasAnyThreads ? ( + + + {emptyState.loading ? ( + + + + ) : null} - ) : null} + ) : !hasResults && hasSearchQuery ? ( + + ) : !hasResults && selectedEnvironmentLabel ? ( + + ) : !hasResults ? ( + + ) : ( + projectGroups.map((group) => { + const isExpanded = expandedProjects.has(group.key); + const visibleThreads = isExpanded + ? group.threads + : group.threads.slice(0, COLLAPSED_THREAD_LIMIT); + + return ( + + toggleExpanded(group.key)} + project={group.representative} + title={group.title} + totalThreadCount={group.threads.length} + /> + + {visibleThreads.map((thread, i) => { + const threadKey = `${thread.environmentId}:${thread.id}`; + return ( + + props.onArchiveThread(thread)} + onDelete={() => props.onDeleteThread(thread)} + onPress={() => props.onSelectThread(thread)} + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + /> + + ); + })} + + + ); + }) + )} + + ); + + const scrollView = ( + openSwipeableRef.current?.close()} + scrollEventThrottle={16} + className="flex-1 bg-screen" + contentContainerStyle={{ + minHeight: windowHeight + HOME_COMPACT_HEADER_THRESHOLD + insets.top, + paddingHorizontal: 0, + paddingTop: Platform.OS === "ios" ? 18 : 0, + paddingBottom: Platform.OS === "ios" ? Math.max(insets.bottom, 24) + 132 : 24, + gap: 0, + }} + scrollIndicatorInsets={ + Platform.OS === "ios" + ? { + bottom: Math.max(insets.bottom, 16) + 92, + top: insets.top + 72, + } + : undefined + } + > + {listContent} + + ); + + const connectionStatus = shouldShowConnectionStatus ? ( + + + ) : null; + + if (Platform.OS === "ios") { + return ( + + + {scrollView} + {connectionStatus} + ["headerRightBarButtonItems"] + } + headerToolbarItems={ + [ + { + composeButtonId: "home-new-task", + composeSystemImageName: "square.and.pencil", + filterButtonId: "home-filter", + filterMenu, + filterSystemImageName: hasCustomListOptions + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease", + onComposePress: props.onStartNewTask, + onSearchTextChange: props.onSearchQueryChange, + placeholder: "Search", + searchTextChangeId: "home-search-text", + type: "mailSearchToolbar", + useFallbackSearchField: true, + }, + ] as ComponentProps["headerToolbarItems"] + } + hideBackButton + hideShadow={false} + largeTitle={false} + navigationItemStyle="editor" + title="Threads" + titleColor={foregroundColor} + titleFontSize={18} + titleFontWeight="800" + translucent + /> + + + ); + } + + return ( + <> + {scrollView} + {connectionStatus} + ); } diff --git a/apps/mobile/src/features/home/home-list-filter-menu.ts b/apps/mobile/src/features/home/home-list-filter-menu.ts new file mode 100644 index 00000000000..46ea639677b --- /dev/null +++ b/apps/mobile/src/features/home/home-list-filter-menu.ts @@ -0,0 +1,120 @@ +import type { + EnvironmentId, + SidebarProjectGroupingMode, + SidebarThreadSortOrder, +} from "@t3tools/contracts"; + +import type { HomeProjectSortOrder } from "./homeThreadList"; +import { + PROJECT_GROUPING_OPTIONS, + PROJECT_SORT_OPTIONS, + THREAD_SORT_OPTIONS, +} from "./home-list-options"; + +export interface HomeListFilterMenuEnvironment { + readonly environmentId: EnvironmentId; + readonly label: string; +} + +type HomeListFilterMenuAction = { + readonly type: "action"; + readonly title: string; + readonly subtitle?: string; + readonly state?: "on" | "off"; + readonly onPress: () => void; +}; + +type HomeListFilterMenuSubmenu = { + readonly type: "submenu"; + readonly title: string; + readonly items: HomeListFilterMenuAction[]; +}; + +export interface HomeListFilterMenu { + readonly title: string; + readonly items: Array; +} + +export function buildHomeListFilterMenu(props: { + readonly environments: ReadonlyArray; + readonly selectedEnvironmentId: EnvironmentId | null; + readonly projectSortOrder: HomeProjectSortOrder; + readonly threadSortOrder: SidebarThreadSortOrder; + readonly projectGroupingMode: SidebarProjectGroupingMode; + readonly onEnvironmentChange: (environmentId: EnvironmentId | null) => void; + readonly onProjectSortOrderChange: (sortOrder: HomeProjectSortOrder) => void; + readonly onThreadSortOrderChange: (sortOrder: SidebarThreadSortOrder) => void; + readonly onProjectGroupingModeChange: (mode: SidebarProjectGroupingMode) => void; + readonly onOpenSettings?: () => void; +}): HomeListFilterMenu { + const items: Array = []; + + if (props.onOpenSettings) { + items.push({ + type: "action", + title: "Settings", + onPress: props.onOpenSettings, + }); + } + + items.push( + { + type: "submenu", + title: "Environment", + items: [ + { + type: "action", + title: "All environments", + subtitle: "Show threads from every environment", + state: props.selectedEnvironmentId === null ? "on" : "off", + onPress: () => props.onEnvironmentChange(null), + }, + ...props.environments.map((environment) => ({ + type: "action" as const, + title: environment.label, + state: + props.selectedEnvironmentId === environment.environmentId + ? ("on" as const) + : ("off" as const), + onPress: () => props.onEnvironmentChange(environment.environmentId), + })), + ], + }, + { + type: "submenu", + title: "Sort projects", + items: PROJECT_SORT_OPTIONS.map((option) => ({ + type: "action", + title: option.label, + state: props.projectSortOrder === option.value ? "on" : "off", + onPress: () => props.onProjectSortOrderChange(option.value), + })), + }, + { + type: "submenu", + title: "Sort threads", + items: THREAD_SORT_OPTIONS.map((option) => ({ + type: "action", + title: option.label, + state: props.threadSortOrder === option.value ? "on" : "off", + onPress: () => props.onThreadSortOrderChange(option.value), + })), + }, + { + type: "submenu", + title: "Group projects", + items: PROJECT_GROUPING_OPTIONS.map((option) => ({ + type: "action", + title: option.label, + subtitle: option.subtitle, + state: props.projectGroupingMode === option.value ? "on" : "off", + onPress: () => props.onProjectGroupingModeChange(option.value), + })), + }, + ); + + return { + title: "Thread list options", + items, + }; +} diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx index 0bc729f6223..2e9027fb636 100644 --- a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx +++ b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx @@ -3,7 +3,12 @@ import type { ReactNode } from "react"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; -export function WorkspaceSidebarToolbar(props: { readonly children?: ReactNode } = {}) { +export function WorkspaceSidebarToolbar( + props: { + readonly children?: ReactNode; + readonly afterSidebarButton?: ReactNode; + } = {}, +) { const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); if (!layout.usesSplitView) { @@ -21,6 +26,7 @@ export function WorkspaceSidebarToolbar(props: { readonly children?: ReactNode } onPress={togglePrimarySidebar} separateBackground /> + {props.afterSidebarButton} ); } diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 478aae3d515..4e93a91e419 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -77,6 +77,8 @@ export function ThreadGitControls(props: { readonly canOpenFiles: boolean; readonly projectScripts: ReadonlyArray; readonly terminalSessions: ReadonlyArray; + readonly showDirectFileControl?: boolean; + readonly showSearchSlot?: boolean; readonly onOpenFilesInspector?: () => void; readonly onOpenGitInspector?: () => void; readonly onOpenTerminal: (terminalId?: string | null) => void; @@ -245,6 +247,21 @@ export function ThreadGitControls(props: { Open new terminal + {props.showDirectFileControl ? ( + { + if (props.onOpenFilesInspector) { + props.onOpenFilesInspector(); + return; + } + router.push(buildThreadFilesNavigation({ environmentId, threadId })); + }} + separateBackground + /> + ) : null} More + {props.showSearchSlot ? ( + <> + + + + ) : null} ); } diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 099c3ae7d85..28b67c2a4c9 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -5,7 +5,7 @@ import { SymbolView } from "expo-symbols"; import { useRouter } from "expo-router"; import { memo, useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; -import { Pressable, StyleSheet, TextInput, View, useColorScheme } from "react-native"; +import { Platform, Pressable, StyleSheet, TextInput, View, useColorScheme } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -16,6 +16,7 @@ import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; +import { iosNativeGlassButtonTint } from "../../lib/ios-native-chrome"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; import { useWorkspaceState } from "../../state/workspace"; @@ -37,14 +38,14 @@ import { SidebarHeaderActions } from "./sidebar-header-actions"; import { SidebarFilterButton } from "./sidebar-filter-button"; import { threadStatusTone } from "./threadPresentation"; -const SIDEBAR_STICKY_HEADER_HEIGHT = 94; -const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 38; -const IOS_SYSTEM_BLUE_DARK = "#0A84FF"; -const IOS_SYSTEM_BLUE_LIGHT = "#007AFF"; +const SIDEBAR_STICKY_HEADER_HEIGHT = 106; +const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 44; const IOS_SEARCH_FILL_DARK = "rgba(118, 118, 128, 0.24)"; const IOS_SEARCH_FILL_LIGHT = "rgba(118, 118, 128, 0.12)"; -const IOS_SELECTED_FOREGROUND = "#FFFFFF"; -const IOS_SELECTED_MUTED_FOREGROUND = "rgba(255, 255, 255, 0.78)"; +const SIDEBAR_HEADER_WASH_OPACITY = { + dark: [0.22, 0.14, 0.04], + light: [0.46, 0.3, 0.08], +} as const; const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly backgroundColor: ColorValue; @@ -59,8 +60,6 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly pressedBackgroundColor: ColorValue; readonly selected: boolean; readonly selectedBackgroundColor: ColorValue; - readonly selectedForegroundColor: ColorValue; - readonly selectedMutedColor: ColorValue; readonly simultaneousSwipeGesture?: ComponentProps< typeof ThreadSwipeable >["simultaneousWithExternalGesture"]; @@ -82,8 +81,6 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { pressedBackgroundColor, selected, selectedBackgroundColor, - selectedForegroundColor, - selectedMutedColor, simultaneousSwipeGesture, thread, environmentLabel, @@ -121,13 +118,6 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { Boolean(part), ); const statusTone = threadStatusTone(thread); - const displayedStatusTone = selected - ? { - ...statusTone, - pillClassName: "bg-white/20", - textClassName: "text-white", - } - : statusTone; return ( {thread.title} @@ -178,21 +168,17 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { {subtitle.join(" · ")} ) : null} - + {relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} - + { if (openSwipeableRef.current !== methods) { openSwipeableRef.current?.close(); @@ -429,14 +416,19 @@ export function ThreadNavigationSidebar(props: { setHeaderIsOverContent(next); }, []); const focusSearch = useCallback(() => { + if (usesNativeSidebarChrome) { + return false; + } if (!props.visible) { props.onRequestVisibility(); - setTimeout(() => searchInputRef.current?.focus(), 240); + setTimeout(() => { + searchInputRef.current?.focus(); + }, 240); } else { searchInputRef.current?.focus(); } return true; - }, [props.onRequestVisibility, props.visible]); + }, [props.onRequestVisibility, props.visible, usesNativeSidebarChrome]); useHardwareKeyboardCommand("focusSearch", focusSearch); const renderListItem = useCallback( ({ item }: { readonly item: SidebarListItem }) => { @@ -468,8 +460,6 @@ export function ThreadNavigationSidebar(props: { pressedBackgroundColor={pressedBackgroundColor} selected={item.key === props.selectedThreadKey} selectedBackgroundColor={selectedBackgroundColor} - selectedForegroundColor={selectedForegroundColor} - selectedMutedColor={selectedMutedColor} simultaneousSwipeGesture={sidebarScrollGesture} thread={thread} environmentLabel={savedConnectionsById[thread.environmentId]?.environmentLabel ?? null} @@ -492,13 +482,193 @@ export function ThreadNavigationSidebar(props: { selectedBackgroundColor, listThemeKey, mutedColor, - selectedForegroundColor, - selectedMutedColor, ], ); const filterIcon = hasCustomHomeListOptions(options) ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle"; + const nativeHeaderButtonTint = iosNativeGlassButtonTint(colorScheme); + + if (usesNativeSidebarChrome) { + const { Screen, ScreenStack, ScreenStackHeaderConfig } = + require("react-native-screens") as typeof import("react-native-screens"); + const nativeHeaderRightBarButtonItems = [ + { + accessibilityLabel: "Filter and sort threads", + icon: { name: filterIcon, type: "sfSymbol" }, + identifier: "thread-sidebar-filter", + menu: { + title: "Thread list options", + items: [ + { + type: "submenu", + title: "Environment", + items: [ + { + onPress: () => setSelectedEnvironmentId(null), + state: options.selectedEnvironmentId === null ? "on" : "off", + subtitle: "Show threads from every environment", + title: "All environments", + type: "action", + }, + ...environments.map((environment) => ({ + onPress: () => setSelectedEnvironmentId(environment.environmentId), + state: + options.selectedEnvironmentId === environment.environmentId + ? ("on" as const) + : ("off" as const), + title: environment.label, + type: "action" as const, + })), + ], + }, + { + type: "submenu", + title: "Sort projects", + items: PROJECT_SORT_OPTIONS.map((option) => ({ + onPress: () => setProjectSortOrder(option.value), + state: + options.projectSortOrder === option.value ? ("on" as const) : ("off" as const), + title: option.label, + type: "action" as const, + })), + }, + { + type: "submenu", + title: "Sort threads", + items: THREAD_SORT_OPTIONS.map((option) => ({ + onPress: () => setThreadSortOrder(option.value), + state: + options.threadSortOrder === option.value ? ("on" as const) : ("off" as const), + title: option.label, + type: "action" as const, + })), + }, + { + type: "submenu", + title: "Group projects", + items: PROJECT_GROUPING_OPTIONS.map((option) => ({ + onPress: () => setProjectGroupingMode(option.value), + state: + options.projectGroupingMode === option.value ? ("on" as const) : ("off" as const), + subtitle: option.subtitle, + title: option.label, + type: "action" as const, + })), + }, + ], + }, + sharesBackground: true, + tintColor: nativeHeaderButtonTint, + type: "menu", + variant: "prominent", + width: 58, + }, + { + accessibilityLabel: "Open settings", + icon: { name: "gearshape", type: "sfSymbol" }, + identifier: "thread-sidebar-settings", + onPress: props.onOpenSettings, + sharesBackground: true, + tintColor: nativeHeaderButtonTint, + type: "button", + variant: "prominent", + width: 58, + }, + ] as ComponentProps["headerRightBarButtonItems"]; + + return ( + + + + + + item.kind} + keyExtractor={(item) => item.key} + renderItem={renderListItem} + contentContainerStyle={[ + styles.threadListContent, + { + paddingBottom: 16 + insets.bottom, + paddingTop: nativeTopListInset, + }, + ]} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="handled" + onScroll={handleScroll} + onScrollBeginDrag={() => openSwipeableRef.current?.close()} + scrollEventThrottle={16} + showsVerticalScrollIndicator={false} + style={styles.threadList} + ListHeaderComponent={ + showsConnectionStatus ? ( + + router.push("/settings/environments")} + state={catalogState} + variant="sidebar" + /> + + ) : null + } + ListEmptyComponent={ + + {catalogState.isLoadingConnections + ? "Loading threads…" + : searchQuery.trim().length > 0 + ? "No matching threads" + : "No threads yet"} + + } + /> + + + + + + + + ); + } return ( @@ -595,7 +765,7 @@ export function ThreadNavigationSidebar(props: { @@ -615,8 +785,6 @@ export function ThreadNavigationSidebar(props: { styles.searchField, { backgroundColor: searchBackgroundColor, - borderColor: - colorScheme === "dark" ? "rgba(255,255,255,0.08)" : "rgba(255,255,255,0.72)", }, ]} > @@ -634,7 +802,6 @@ export function ThreadNavigationSidebar(props: { style={[styles.searchInput, { color: foregroundColor }]} value={searchQuery} /> - {showsConnectionStatus ? ( @@ -669,25 +836,33 @@ const styles = StyleSheet.create({ top: 0, }, header: { - height: 44, - paddingLeft: 14, + height: 50, + paddingLeft: 20, paddingRight: 8, flexDirection: "row", - alignItems: "center", + alignItems: "flex-end", gap: 2, }, connectionStatus: { paddingTop: 10, paddingHorizontal: 14, }, + nativeConnectionStatus: { + paddingBottom: 10, + paddingHorizontal: 14, + }, + nativeHeaderActions: { + alignItems: "center", + flexDirection: "row", + gap: 2, + }, searchField: { - height: 34, - marginTop: 5, - marginHorizontal: 12, - paddingLeft: 9, + height: 38, + marginTop: 9, + marginHorizontal: 16, + paddingLeft: 11, paddingRight: 10, - borderRadius: 17, - borderWidth: StyleSheet.hairlineWidth, + borderRadius: 12, flexDirection: "row", alignItems: "center", gap: 6, @@ -704,19 +879,19 @@ const styles = StyleSheet.create({ flex: 1, }, threadListContent: { - paddingHorizontal: 10, + paddingHorizontal: 8, }, sectionTitle: { - paddingHorizontal: 8, + paddingHorizontal: 20, paddingBottom: 4, - paddingTop: 14, + paddingTop: 16, }, threadItem: { - paddingBottom: 4, + paddingBottom: 0, }, threadRow: { - minHeight: 58, - borderRadius: 10, + minHeight: 64, + borderRadius: 12, flexDirection: "row", alignItems: "center", paddingRight: 6, @@ -725,16 +900,16 @@ const styles = StyleSheet.create({ minWidth: 0, flex: 1, alignSelf: "stretch", - borderRadius: 10, - paddingLeft: 10, - paddingRight: 4, - paddingVertical: 8, + borderRadius: 12, + paddingLeft: 14, + paddingRight: 6, + paddingVertical: 10, flexDirection: "row", alignItems: "center", gap: 8, }, threadRowContainer: { - borderRadius: 10, + borderRadius: 12, overflow: "hidden", }, threadText: { diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 2ab916f78be..4ddf3567b73 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -3,15 +3,8 @@ import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } fro import * as Option from "effect/Option"; import { EnvironmentId, ThreadId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; -import { - Platform, - Pressable, - ScrollView, - Text as RNText, - View, - useColorScheme, -} from "react-native"; -import { isLiquidGlassAvailable } from "expo-glass-effect"; +import { Platform, Pressable, ScrollView, Text as RNText, View } from "react-native"; +import type { SearchBarCommands } from "react-native-screens"; import { useWorkspaceState } from "../../state/workspace"; import { useThemeColor } from "../../lib/useThemeColor"; import { useEnvironmentQuery } from "../../state/query"; @@ -72,13 +65,13 @@ import { ThreadInspectorContentStack, type ThreadInspectorMode, } from "./thread-inspector-content-stack"; +import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; interface ThreadInspectorSelection { readonly routeThreadIdentity: string | null; readonly mode: ThreadInspectorMode; } -const USES_NATIVE_GLASS_HEADER = Platform.OS === "ios" && isLiquidGlassAvailable(); const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); function InspectorPaneRoleActivation() { @@ -227,6 +220,7 @@ function ThreadRouteContent( const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = useThreadSelection(); const selectedThreadDetailState = props.selectedThreadDetailState; + const threadSearchBarRef = useRef(null); const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data); const { selectedThreadCwd } = useSelectedThreadWorktree(); const composer = useThreadComposerState(); @@ -240,7 +234,6 @@ function ThreadRouteContent( threadId?: string | string[]; }>(); const [drawerVisible, setDrawerVisible] = useState(false); - const [headerMaterialVisible, setHeaderMaterialVisible] = useState(false); const environmentIdRaw = firstRouteParam(params.environmentId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; const threadId = firstRouteParam(params.threadId); @@ -325,20 +318,20 @@ function ThreadRouteContent( ); /* ─── Native header theming ──────────────────────────────────────── */ - const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; const iconColor = String(useThemeColor("--color-icon")); const foregroundColor = String(useThemeColor("--color-foreground")); const secondaryFg = String(useThemeColor("--color-foreground-secondary")); const screenBackgroundColor = String(useThemeColor("--color-screen")); - const usesEdgeToEdgeGlassHeader = USES_NATIVE_GLASS_HEADER && !layout.usesSplitView; - // Compact/iPhone stacks use the edge-to-edge UIKit material seen in Messages. - // iPad split view keeps a clean pane header; native iPad Messages/Mail reserve - // the stronger glass treatment for local controls/floating elements instead. - const glassHeaderBlurEffect = - colorScheme === "dark" - ? ("systemUltraThinMaterialDark" as const) - : ("systemUltraThinMaterialLight" as const); - const showGlassHeaderMaterial = usesEdgeToEdgeGlassHeader && headerMaterialVisible; + const usesNativeHeaderGlass = Platform.OS === "ios"; + const usesThreadSearchToolbar = Platform.OS === "ios" && layout.usesSplitView; + const focusThreadSearch = useCallback(() => { + if (!usesThreadSearchToolbar) { + return false; + } + threadSearchBarRef.current?.focus(); + return true; + }, [usesThreadSearchToolbar]); + useHardwareKeyboardCommand("focusSearch", focusThreadSearch); const headerSubtitle = [ selectedThreadProject?.title ?? null, selectedEnvironmentConnection?.environmentLabel ?? null, @@ -627,10 +620,9 @@ function ThreadRouteContent( [ + { + activatesSearchController: true, + type: "searchBarPlacement", }, - }), + ] + : undefined, + unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> @@ -661,7 +666,16 @@ function ThreadRouteContent( /> - + router.push("/new")} + separateBackground + /> + } + > {props.onReturnToThread ? ( [ styles.button, - { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + { + backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, + borderColor, + }, ]} > @@ -33,7 +40,8 @@ const styles = StyleSheet.create({ button: { width: 44, height: 44, - borderRadius: 12, + borderRadius: 22, + borderWidth: StyleSheet.hairlineWidth, alignItems: "center", justifyContent: "center", cursor: "pointer", diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx index ba9d715bfc8..56f908d1b02 100644 --- a/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx +++ b/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx @@ -11,11 +11,13 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { icon="gearshape" onPress={props.onOpenSettings} /> - + {props.onStartNewTask ? ( + + ) : null} ); } diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.tsx index 292866c2025..5d0cb5d2e82 100644 --- a/apps/mobile/src/features/threads/sidebar-header-actions.tsx +++ b/apps/mobile/src/features/threads/sidebar-header-actions.tsx @@ -1,11 +1,11 @@ import { SymbolView } from "expo-symbols"; -import { Pressable, StyleSheet, View } from "react-native"; +import { Pressable, StyleSheet, View, useColorScheme } from "react-native"; import { useThemeColor } from "../../lib/useThemeColor"; export interface SidebarHeaderActionsProps { readonly onOpenSettings: () => void; - readonly onStartNewTask: () => void; + readonly onStartNewTask?: () => void; } function FallbackHeaderButton(props: { @@ -15,6 +15,10 @@ function FallbackHeaderButton(props: { }) { const iconColor = useThemeColor("--color-icon-muted"); const pressedBackgroundColor = useThemeColor("--color-subtle"); + const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; + const idleBackgroundColor = + colorScheme === "dark" ? "rgba(118,118,128,0.24)" : "rgba(255,255,255,0.72)"; + const borderColor = colorScheme === "dark" ? "rgba(255,255,255,0.08)" : "rgba(0,0,0,0.08)"; return ( [ styles.button, - { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + { + backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, + borderColor, + }, ]} > @@ -40,11 +47,13 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { icon="gearshape" onPress={props.onOpenSettings} /> - + {props.onStartNewTask ? ( + + ) : null} ); } @@ -58,7 +67,8 @@ const styles = StyleSheet.create({ button: { width: 44, height: 44, - borderRadius: 12, + borderRadius: 22, + borderWidth: StyleSheet.hairlineWidth, alignItems: "center", justifyContent: "center", }, diff --git a/apps/mobile/src/lib/ios-native-chrome.ts b/apps/mobile/src/lib/ios-native-chrome.ts new file mode 100644 index 00000000000..e45b81e21fa --- /dev/null +++ b/apps/mobile/src/lib/ios-native-chrome.ts @@ -0,0 +1,10 @@ +export type NativeChromeColorScheme = "dark" | "light" | "unspecified" | null | undefined; + +/** + * Tint used for iOS 27 glass header buttons. UIKit still owns the actual + * material; this color only nudges the sampled glass toward Mail/Messages' + * button tone without baking custom blur into React views. + */ +export function iosNativeGlassButtonTint(colorScheme: NativeChromeColorScheme): string { + return colorScheme === "dark" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; +} diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.ts b/apps/mobile/src/lib/native-scroll-edge-effect.ts index a0704b5c1d0..fd19a7f33db 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.ts +++ b/apps/mobile/src/lib/native-scroll-edge-effect.ts @@ -1,4 +1,4 @@ -export type NativeTopScrollEdgeEffect = "automatic" | "hard"; +export type NativeTopScrollEdgeEffect = "automatic" | "soft"; function majorVersion(version: number | string): number { if (typeof version === "number") { @@ -10,14 +10,13 @@ function majorVersion(version: number | string): number { } /** - * iOS 27 beta currently renders the automatic/soft top scroll-edge effect as - * fully transparent. Keep the subtler automatic treatment on iOS 26 and use - * UIKit's native hard treatment on iOS 27+ until the platform regression is - * resolved. + * iOS 27's system apps use a soft scroll-edge treatment for Messages-style + * chrome. Avoid the `hard` style here: it adds the dividing line that makes the + * header feel custom and heavier than Messages/Mail. */ export function nativeTopScrollEdgeEffect( os: string, version: number | string, ): NativeTopScrollEdgeEffect { - return os === "ios" && majorVersion(version) >= 27 ? "hard" : "automatic"; + return os === "ios" && majorVersion(version) >= 27 ? "soft" : "automatic"; } diff --git a/experiments/messages-glass-lab/MessagesGlassLab.xcodeproj/project.pbxproj b/experiments/messages-glass-lab/MessagesGlassLab.xcodeproj/project.pbxproj new file mode 100644 index 00000000000..dec9d28b85c --- /dev/null +++ b/experiments/messages-glass-lab/MessagesGlassLab.xcodeproj/project.pbxproj @@ -0,0 +1,285 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXBuildFile section */ + 8A01A0012D10000100A00001 /* MessagesGlassLabApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A01A0002D10000100A00001 /* MessagesGlassLabApp.swift */; }; + 8A01A0032D10000100A00001 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8A01A0022D10000100A00001 /* ContentView.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 8A01A0002D10000100A00001 /* MessagesGlassLabApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessagesGlassLabApp.swift; sourceTree = ""; }; + 8A01A0022D10000100A00001 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + 8A01A0102D10000100A00001 /* MessagesGlassLab.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = MessagesGlassLab.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + + 8A01A0202D10000100A00001 /* MessagesGlassLab */ = { + isa = PBXGroup; + children = ( + 8A01A0002D10000100A00001 /* MessagesGlassLabApp.swift */, + 8A01A0022D10000100A00001 /* ContentView.swift */, + ); + path = MessagesGlassLab; + sourceTree = ""; + }; + +/* Begin PBXFrameworksBuildPhase section */ + 8A01A0112D10000100A00001 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 8A01A0302D10000100A00001 = { + isa = PBXGroup; + children = ( + 8A01A0202D10000100A00001 /* MessagesGlassLab */, + 8A01A0312D10000100A00001 /* Products */, + ); + sourceTree = ""; + }; + 8A01A0312D10000100A00001 /* Products */ = { + isa = PBXGroup; + children = ( + 8A01A0102D10000100A00001 /* MessagesGlassLab.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 8A01A0402D10000100A00001 /* MessagesGlassLab */ = { + isa = PBXNativeTarget; + buildConfigurationList = 8A01A0502D10000100A00001 /* Build configuration list for PBXNativeTarget "MessagesGlassLab" */; + buildPhases = ( + 8A01A0412D10000100A00001 /* Sources */, + 8A01A0112D10000100A00001 /* Frameworks */, + 8A01A0422D10000100A00001 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = MessagesGlassLab; + packageProductDependencies = ( + ); + productName = MessagesGlassLab; + productReference = 8A01A0102D10000100A00001 /* MessagesGlassLab.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 8A01A0602D10000100A00001 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2700; + LastUpgradeCheck = 2700; + TargetAttributes = { + 8A01A0402D10000100A00001 = { + CreatedOnToolsVersion = 27.0; + }; + }; + }; + buildConfigurationList = 8A01A0612D10000100A00001 /* Build configuration list for PBXProject "MessagesGlassLab" */; + compatibilityVersion = "Xcode 16.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 8A01A0302D10000100A00001; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 8A01A0312D10000100A00001 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 8A01A0402D10000100A00001 /* MessagesGlassLab */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 8A01A0422D10000100A00001 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 8A01A0412D10000100A00001 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 8A01A0012D10000100A00001 /* MessagesGlassLabApp.swift in Sources */, + 8A01A0032D10000100A00001 /* ContentView.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 8A01A0622D10000100A00001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 6.0; + }; + name = Debug; + }; + 8A01A0632D10000100A00001 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_VERSION = 6.0; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 8A01A0512D10000100A00001 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = ""; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Messages Glass Lab"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; + PRODUCT_BUNDLE_IDENTIFIER = com.t3tools.messagesglasslab; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 8A01A0522D10000100A00001 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = ""; + CODE_SIGNING_ALLOWED = NO; + CODE_SIGNING_REQUIRED = NO; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_CFBundleDisplayName = "Messages Glass Lab"; + INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools"; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 27.0; + PRODUCT_BUNDLE_IDENTIFIER = com.t3tools.messagesglasslab; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 6.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 8A01A0612D10000100A00001 /* Build configuration list for PBXProject "MessagesGlassLab" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8A01A0622D10000100A00001 /* Debug */, + 8A01A0632D10000100A00001 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; + 8A01A0502D10000100A00001 /* Build configuration list for PBXNativeTarget "MessagesGlassLab" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 8A01A0512D10000100A00001 /* Debug */, + 8A01A0522D10000100A00001 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Debug; + }; +/* End XCConfigurationList section */ + }; + rootObject = 8A01A0602D10000100A00001 /* Project object */; +} diff --git a/experiments/messages-glass-lab/MessagesGlassLab/ContentView.swift b/experiments/messages-glass-lab/MessagesGlassLab/ContentView.swift new file mode 100644 index 00000000000..9d19a6666f9 --- /dev/null +++ b/experiments/messages-glass-lab/MessagesGlassLab/ContentView.swift @@ -0,0 +1,386 @@ +import SwiftUI + +struct LabThread: Identifiable, Hashable { + let id = UUID() + let title: String + let subtitle: String + let time: String + let initials: String + let tint: Color + let preview: String +} + +private let threads: [LabThread] = [ + .init(title: "Markdown rendering test", subtitle: "t3code · Julius’s Mac mini", time: "14h", initials: "MD", tint: .blue, preview: "Renderer stress test, terminal snippets, code blocks, and a very long markdown transcript."), + .init(title: "iPad rectly text correction", subtitle: "Julius’s Mac mini · main", time: "16h", initials: "IP", tint: .purple, preview: "Fix iPad layout, search behavior, hardware keyboard, and trackpad scrolling."), + .init(title: "Preview Webview Persists Off Panel", subtitle: "codething-mvp · Julius’s MacBook Pro", time: "22m", initials: "PW", tint: .teal, preview: "The browser preview should not leak outside the active panel when switching threads."), + .init(title: "Add file preview action buttons", subtitle: "codex/connection-preview", time: "10d", initials: "FP", tint: .orange, preview: "Open files at exact lines and expose copy/open actions in the renderer."), + .init(title: "Investigate v2 pipeline slowdown", subtitle: "codex-turn-runner", time: "1d", initials: "V2", tint: .gray, preview: "Compare orchestration traces and find why streamed events are delayed under load."), + .init(title: "Fix dark-mode header glass", subtitle: "t3code/ipad-responsive-mobile-layout", time: "2h", initials: "DG", tint: .indigo, preview: "Compare dark scroll-edge material against Messages and Mail, then map the behavior back to React Native Screens."), + .init(title: "Magic Keyboard sidebar scroll", subtitle: "mobile/input-polish", time: "3h", initials: "MK", tint: .cyan, preview: "Trackpad scrolling should remain fluid while swipe actions still work for touch gestures."), + .init(title: "Terminal tab key routing", subtitle: "terminal/native-pty", time: "5h", initials: "⌘", tint: .green, preview: "Hardware Tab should go to the terminal session instead of escaping to the thread search field."), + .init(title: "Diff renderer split inspector", subtitle: "review-diff/native", time: "8h", initials: "Δ", tint: .red, preview: "Keep file navigation, sticky headers, and selected hunk state stable in a three-column iPad layout."), + .init(title: "Composer glass affordance", subtitle: "composer/liquid-glass", time: "9h", initials: "CG", tint: .pink, preview: "Prototype a bottom composer that feels native without covering too much content while scrolling."), + .init(title: "Search placement audit", subtitle: "navigation/native-search", time: "11h", initials: "SP", tint: .mint, preview: "Validate whether search belongs in the bottom toolbar on iPhone and the sidebar chrome on iPad."), + .init(title: "Thread row density pass", subtitle: "home/messages-list", time: "12h", initials: "TR", tint: .brown, preview: "Tune row height, separators, preview text, chevrons, and status glyphs to match native list rhythm."), + .init(title: "Files navigator polish", subtitle: "files/inspector", time: "1d", initials: "FN", tint: .yellow, preview: "Make the file explorer feel like an iPad side inspector instead of a cramped web sidebar."), + .init(title: "Toolbar grouping experiment", subtitle: "native-toolbar-glass", time: "2d", initials: "TB", tint: .blue.opacity(0.7), preview: "Compare separate glass buttons with merged toolbar groups and spacing behavior."), + .init(title: "Scroll-edge fade comparison", subtitle: "swiftui/messages-lab", time: "3d", initials: "SE", tint: .purple.opacity(0.8), preview: "Record scroll positions to see where native headers start becoming visible over content."), + .init(title: "iPad sidebar selection state", subtitle: "split-view/sidebar", time: "4d", initials: "SS", tint: .teal.opacity(0.75), preview: "Find the right selected-row treatment for dark and light mode in a persistent sidebar."), + .init(title: "Preview webview panel bug", subtitle: "preview/browser", time: "5d", initials: "WB", tint: .orange.opacity(0.8), preview: "Ensure preview browser surfaces stay clipped to the active detail pane during navigation."), + .init(title: "Keyboard shortcuts overlay", subtitle: "hardware-keyboard", time: "6d", initials: "KS", tint: .gray.opacity(0.9), preview: "Expose discoverable commands and keep focus behavior aligned with iPadOS keyboard conventions."), + .init(title: "Thread loading skeleton", subtitle: "mobile/perceived-performance", time: "1w", initials: "LS", tint: .green.opacity(0.75), preview: "Replace jarring empty states with native-feeling loading rows while snapshots hydrate."), + .init(title: "Connection recovery UX", subtitle: "lan-pairing", time: "2w", initials: "CR", tint: .red.opacity(0.75), preview: "Make reconnect banners and retry affordances less intrusive during scroll and composition."), +] + +struct ContentView: View { + @Environment(\.horizontalSizeClass) private var horizontalSizeClass + @State private var searchText = "" + @State private var selectedThread: LabThread? = threads[0] + + var body: some View { + if horizontalSizeClass == .regular { + NativeSplitLab(searchText: $searchText, selectedThread: $selectedThread) + } else { + NativePhoneLab(searchText: $searchText, selectedThread: $selectedThread) + } + } +} + +private var filteredThreads: [LabThread] { + threads +} + +private let glassDebugCodeLines: [String] = [ + "# Native RNS glass debug route", + "", + "This screen intentionally avoids Expo Router headers.", + "The native header below is owned by react-native-screens.", + "", + "Expected iOS 26 behavior:", + "- At rest: header should feel like app background", + "- While scrolled: content should blur behind the header", + "- No gray custom overlay", + "- No JS blur view", + "", + "Scroll edge effect should sample actual content:", + "const header = { translucent: true }", + "const scrollEdgeEffects = { top: 'soft' }", + "", + "Bright rows below make sampling failures obvious.", + "", + "node_modules", + "/.pnp", + ".pnp.*", + ".yarn/*", + "!.yarn/patches", + "!.yarn/plugins", + "!.yarn/releases", + "!.yarn/versions", + "", + "# testing", + "/coverage", + ".convex", + "e2e/.local-dev.json", + "e2e/playwright-report", + "e2e/test-results", + "", + "# app surfaces", + "threads", + "terminal", + "diff renderer", + "file explorer", + "composer", + "native header", + "scroll edge", + "liquid glass", +] + +private let glassDebugSwatches: [Color] = [ + .blue, + .green, + .orange, + .purple, + .cyan, + .pink, +] + +struct NativePhoneLab: View { + @Binding var searchText: String + @Binding var selectedThread: LabThread? + + var body: some View { + NavigationStack { + NativeThreadLab(thread: selectedThread ?? threads[0]) + } + } +} + +struct NativeSplitLab: View { + @Binding var searchText: String + @Binding var selectedThread: LabThread? + + var body: some View { + NavigationSplitView { + List(filteredThreads, selection: $selectedThread) { thread in + MessageSidebarRow(thread: thread) + .tag(thread) + } + .listStyle(.sidebar) + .navigationTitle("Threads") + .searchable(text: $searchText, placement: .sidebar, prompt: "Search") + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Menu { + Button("All Threads", systemImage: "tray.full") {} + Button("Ready", systemImage: "checkmark.circle") {} + Button("Running", systemImage: "bolt.circle") {} + } label: { + Image(systemName: "line.3.horizontal.decrease") + } + .buttonStyle(.glass) + + Button {} label: { + Image(systemName: "gearshape") + } + .buttonStyle(.glass) + + Button {} label: { + Image(systemName: "square.and.pencil") + } + .buttonStyle(.glass) + } + } + } detail: { + if let selectedThread { + NativeThreadLab(thread: selectedThread) + } else { + ContentUnavailableView("Select a thread", systemImage: "sidebar.left") + } + } + } +} + +struct MessageListRow: View { + let thread: LabThread + + var body: some View { + HStack(alignment: .top, spacing: 14) { + Circle() + .fill(thread.tint.gradient) + .frame(width: 52, height: 52) + .overlay { + Text(thread.initials) + .font(.headline.weight(.bold)) + .foregroundStyle(.white) + } + + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(thread.title) + .font(.headline.weight(.semibold)) + .lineLimit(1) + Spacer(minLength: 8) + Text(thread.time) + .foregroundStyle(.secondary) + Image(systemName: "chevron.right") + .font(.caption.weight(.semibold)) + .foregroundStyle(.tertiary) + } + + Text(thread.preview) + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(2) + } + .padding(.vertical, 12) + } + } +} + +struct MessageSidebarRow: View { + let thread: LabThread + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + HStack { + Text(thread.title) + .font(.headline.weight(.semibold)) + .lineLimit(1) + Spacer() + Text(thread.time) + .font(.subheadline) + .foregroundStyle(.secondary) + } + Text(thread.subtitle) + .font(.subheadline) + .foregroundStyle(.secondary) + .lineLimit(1) + } + .padding(.vertical, 6) + } +} + +struct NativeThreadLab: View { + let thread: LabThread + @State private var draft = "Ask the repo agent, or run a command..." + @State private var scrollStep = 0 + + private let scrollTimer = Timer.publish(every: 2.8, on: .main, in: .common).autoconnect() + private let scrollTargets = ["top", "swatches", "top", "code", "card"] + + var body: some View { + ScrollViewReader { proxy in + ScrollView { + LazyVStack(alignment: .leading, spacing: 22) { + Color.clear + .frame(height: 0) + .id("top") + + hero + .padding(.top, 8) + + swatches + .id("swatches") + + explanationCard + .id("card") + + codeCard + .id("code") + } + .padding(.horizontal, 18) + .padding(.top, 8) + .padding(.bottom, 96) + } + .background(Color(uiColor: .systemBackground)) + .onReceive(scrollTimer) { _ in + guard !scrollTargets.isEmpty else { return } + + let target = scrollTargets[scrollStep % scrollTargets.count] + scrollStep += 1 + + withAnimation(.smooth(duration: 1.0)) { + proxy.scrollTo(target, anchor: .top) + } + } + } + .navigationTitle("RNS Glass") + .navigationSubtitle("plain react-native-screens") + .toolbar { + ToolbarItemGroup(placement: .topBarTrailing) { + Button {} label: { Image(systemName: "plus") } + .buttonStyle(.glass) + Button {} label: { Image(systemName: "magnifyingglass") } + .buttonStyle(.glass) + } + } + .safeAreaInset(edge: .bottom) { + composer + } + } + + private var hero: some View { + VStack(alignment: .leading, spacing: 10) { + Text("plain react-native-screens") + .font(.subheadline.weight(.bold)) + .textCase(.uppercase) + .tracking(0.5) + .foregroundStyle(.secondary) + + Text("Native scroll-edge glass") + .font(.system(size: 48, weight: .heavy, design: .default)) + .lineLimit(nil) + .minimumScaleFactor(0.72) + + Text("This route uses RNS directly. The script scrolls automatically so the native header is captured both at rest and with bright content behind it.") + .font(.title3) + .foregroundStyle(.secondary) + } + } + + private var swatches: some View { + LazyVGrid( + columns: [GridItem(.adaptive(minimum: 60), spacing: 12)], + alignment: .leading, + spacing: 12 + ) { + ForEach(Array(glassDebugSwatches.enumerated()), id: \.offset) { index, color in + Circle() + .fill(color.gradient) + .frame(width: 60, height: 60) + .overlay { + Text("\(index + 1)") + .font(.title2.weight(.heavy)) + .foregroundStyle(.white) + } + } + } + } + + private var explanationCard: some View { + VStack(alignment: .leading, spacing: 10) { + Text("What this isolates") + .font(.title2.weight(.bold)) + + Text("Native transparent header + iOS 26 scroll edge effect. No Expo Router header config, no custom blur overlay, no large title requirement.") + .font(.title3) + .foregroundStyle(.secondary) + } + .padding(22) + .frame(maxWidth: .infinity, alignment: .leading) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 28, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 28, style: .continuous) + .stroke(.separator.opacity(0.35), lineWidth: 0.5) + } + } + + private var codeCard: some View { + VStack(alignment: .leading, spacing: 0) { + ForEach(Array((glassDebugCodeLines + glassDebugCodeLines).enumerated()), id: \.offset) { index, line in + HStack(alignment: .top, spacing: 16) { + Text("\(index + 1)") + .foregroundStyle(.secondary) + .frame(width: 34, alignment: .trailing) + + Text(line.isEmpty ? " " : line) + .frame(maxWidth: .infinity, alignment: .leading) + } + .font(.system(size: 16, design: .monospaced)) + .lineSpacing(4) + .padding(.horizontal, 16) + .padding(.vertical, 3) + } + } + .padding(.vertical, 14) + .background(.thinMaterial, in: RoundedRectangle(cornerRadius: 24, style: .continuous)) + .overlay { + RoundedRectangle(cornerRadius: 24, style: .continuous) + .stroke(.separator.opacity(0.35), lineWidth: 0.5) + } + } + + private var composer: some View { + HStack(spacing: 10) { + Text(draft) + .foregroundStyle(.secondary) + .lineLimit(1) + Spacer() + Image(systemName: "arrow.up") + .font(.headline.weight(.bold)) + .frame(width: 44, height: 44) + .glassEffect(.regular.interactive(), in: Circle()) + } + .padding(.leading, 18) + .padding(.trailing, 6) + .frame(height: 56) + .glassEffect(.clear.interactive(), in: Capsule()) + .padding(.horizontal) + .padding(.vertical, 8) + } +} + +#Preview { + ContentView() +} diff --git a/experiments/messages-glass-lab/MessagesGlassLab/MessagesGlassLabApp.swift b/experiments/messages-glass-lab/MessagesGlassLab/MessagesGlassLabApp.swift new file mode 100644 index 00000000000..5cf4769194b --- /dev/null +++ b/experiments/messages-glass-lab/MessagesGlassLab/MessagesGlassLabApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct MessagesGlassLabApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/patches/expo-router@56.2.11.patch b/patches/expo-router@56.2.11.patch new file mode 100644 index 00000000000..8ec3076d74e --- /dev/null +++ b/patches/expo-router@56.2.11.patch @@ -0,0 +1,90 @@ +diff --git a/android/src/main/java/expo/modules/router/ExpoRouterModule.kt b/android/src/main/java/expo/modules/router/ExpoRouterModule.kt +deleted file mode 100644 +index 369cd65886415cb894b858e38604dc59b5c6ba95..0000000000000000000000000000000000000000 +diff --git a/build/react-navigation/native-stack/types.d.ts b/build/react-navigation/native-stack/types.d.ts +index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..ed5a88b308e022b2ef49456dfb21c3a6a8aeea4f 100644 +--- a/build/react-navigation/native-stack/types.d.ts ++++ b/build/react-navigation/native-stack/types.d.ts +@@ -306,6 +306,22 @@ export type NativeStackNavigationOptions = { + * @platform ios + */ + unstable_headerRightItems?: (props: NativeStackHeaderItemProps) => NativeStackHeaderItem[]; ++ /** ++ * Function which returns an array of toolbar items for the current screen. ++ * ++ * This is an unstable API and might change in the future. ++ * ++ * @platform ios ++ */ ++ unstable_headerToolbarItems?: (props: NativeStackHeaderItemProps) => NonNullable; ++ /** ++ * Native iOS navigation item style to apply to the header. ++ * ++ * This is an unstable API and might change in the future. ++ * ++ * @platform ios ++ */ ++ unstable_navigationItemStyle?: ScreenStackHeaderConfigProps['navigationItemStyle']; + /** + * String or a function that returns a React Element to be used by the header. + * Defaults to screen `title` or route name. +@@ -1129,13 +1145,14 @@ export type NativeStackHeaderItemCustom = { + */ + hidesSharedBackground?: boolean; + }; ++export type NativeStackHeaderRawRNSItem = NonNullable[number] | NonNullable[number]; + /** + * An item that can be displayed in the header. + * It can be a button, a menu, spacing, or a custom element. + * + * On iOS 26, when showing items on the right side of the header, + * if the items don't fit the available space, they will be collapsed into a menu automatically. + * Items with `type: 'custom'` will not be included in this automatic collapsing behavior. + */ +-export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom; ++export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom | NativeStackHeaderRawRNSItem; + export type NativeStackNavigatorProps = DefaultNavigatorOptions, NativeStackNavigationOptions, NativeStackNavigationEventMap, NativeStackNavigationProp> & StackRouterOptions & NativeStackNavigationConfig; +diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts +index 218c4291fdb57600a45d32fbcefe3707a520fe79..ce3bbadec2d52949d133401f1df01b95899aa5bf 100644 +--- a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts ++++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts +@@ -10,6 +10,6 @@ type Props = NativeStackNavigationOptions & { + } | undefined; + route: Route; + }; +-export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }: Props): ScreenStackHeaderConfigProps; ++export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }: Props): ScreenStackHeaderConfigProps; + export {}; + //# sourceMappingURL=useHeaderConfigProps.d.ts.map +diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.js b/build/react-navigation/native-stack/views/useHeaderConfigProps.js +index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..6fb1840deedd52e1d788c6f93982da26d4a58efc 100644 +--- a/build/react-navigation/native-stack/views/useHeaderConfigProps.js ++++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.js +@@ -18,6 +18,9 @@ const processBarButtonItems = (items, colors, fonts) => { + } + return item; + } ++ if (item.type === 'searchField' || item.type === 'searchBarPlacement' || item.type === 'mailSearchToolbar') { ++ return item; ++ } + if (item.type === 'button' || item.type === 'menu') { + if (item.type === 'menu' && item.menu == null) { + throw new Error(`Menu item must have a 'menu' property defined: ${JSON.stringify(item)}`); +@@ -108,7 +108,7 @@ const getMenuItem = (item) => { + subtitle: description, + }; + }; +-function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }) { ++function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }) { + const { direction } = (0, native_1.useLocale)(); + const { colors, fonts, dark } = (0, native_1.useTheme)(); + const tintColor = headerTintColor ?? (react_native_1.Platform.OS === 'ios' ? colors.primary : colors.text); +@@ -270,6 +270,8 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac + children, + headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts), + headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), ++ headerToolbarItems, ++ navigationItemStyle, + experimental_userInterfaceStyle: dark ? 'dark' : 'light', + }; + } diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index 8685cf8dca9..678d2264888 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -1,5 +1,18 @@ +diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.h +index ea5325ea8d17b1ddfa790ff8dab48ce83142d4d3..acd2fd7ceb7162ed300abc4d3c9f0c24f4d63898 100644 +--- a/ios/RNSBarButtonItem.h ++++ b/ios/RNSBarButtonItem.h +@@ -13,4 +13,8 @@ typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); + menuAction:(RNSBarButtonMenuItemAction)menuAction + imageLoader:(RCTImageLoader *)imageLoader; + +++ (UIMenu *)initUIMenuWithDict:(NSDictionary *)dict ++ menuAction:(RNSBarButtonMenuItemAction)menuAction ++ imageLoader:(RCTImageLoader *)imageLoader; ++ + @end diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm -index 69d4d9a..6192c4a 100644 +index 69d4d9a4704a7bef3fa207de2dbb2d5119846ccb..6192c4a23b1ce08cc2a085c5e7fb0f4a8c33deea 100644 --- a/ios/RNSScreen.mm +++ b/ios/RNSScreen.mm @@ -26,8 +26,10 @@ @@ -47,8 +60,558 @@ index 69d4d9a..6192c4a 100644 #pragma mark - RNSSafeAreaProviding - (UIEdgeInsets)providerSafeAreaInsets +diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h +index 919b984edc9f91ee9ac26faf257d8a721e26457c..3f54fad0a22d37bb7542679ae9d14088b14d07e3 100644 +--- a/ios/RNSScreenStackHeaderConfig.h ++++ b/ios/RNSScreenStackHeaderConfig.h +@@ -21,6 +21,8 @@ + NS_ASSUME_NONNULL_BEGIN + + @property (nonatomic, retain) NSString *title; ++@property (nonatomic, retain) NSString *subtitle; ++@property (nonatomic, retain) NSString *largeSubtitle; + @property (nonatomic, retain) NSString *titleFontFamily; + @property (nonatomic, retain) NSNumber *titleFontSize; + @property (nonatomic, retain) NSString *titleFontWeight; +@@ -45,9 +47,11 @@ NS_ASSUME_NONNULL_BEGIN + @property (nonatomic) BOOL backButtonInCustomView; + @property (nonatomic) UISemanticContentAttribute direction; + @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode; ++@property (nonatomic) NSInteger navigationItemStyle; + @property (nonatomic) RNSBlurEffectStyle blurEffect; + @property (nonatomic, copy, nullable) NSArray *> *headerRightBarButtonItems; + @property (nonatomic, copy, nullable) NSArray *> *headerLeftBarButtonItems; ++@property (nonatomic, copy, nullable) NSArray *> *headerToolbarItems; + @property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled; + + NS_ASSUME_NONNULL_END +diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm +index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab660844998e 100644 +--- a/ios/RNSScreenStackHeaderConfig.mm ++++ b/ios/RNSScreenStackHeaderConfig.mm +@@ -30,6 +30,20 @@ namespace react = facebook::react; + static const NSNumber *const DEFAULT_TITLE_FONT_SIZE = @17; + static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; + ++static NSInteger navigationItemStyleFromCppEquivalent( ++ react::RNSScreenStackHeaderConfigNavigationItemStyle navigationItemStyle) ++{ ++ switch (navigationItemStyle) { ++ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Browser: ++ return UINavigationItemStyleBrowser; ++ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Editor: ++ return UINavigationItemStyleEditor; ++ case react::RNSScreenStackHeaderConfigNavigationItemStyle::Navigator: ++ default: ++ return UINavigationItemStyleNavigator; ++ } ++} ++ + @interface RCTImageLoader (Private) + - (id)imageCache; + @end +@@ -81,6 +95,7 @@ static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; + self.hidden = YES; + _reactSubviews = [NSMutableArray new]; + _backTitleVisible = YES; ++ _navigationItemStyle = UINavigationItemStyleNavigator; + _blurEffect = RNSBlurEffectStyleNone; + } + +@@ -496,6 +511,10 @@ RNS_IGNORE_SUPER_CALL_END + + if (shouldHide) { + navitem.title = config.title; ++ if (@available(iOS 26.0, *)) { ++ navitem.subtitle = config.subtitle; ++ navitem.largeSubtitle = config.largeSubtitle; ++ } + + // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. + [navctr setNavigationBarHidden:YES animated:animated]; +@@ -512,11 +531,19 @@ RNS_IGNORE_SUPER_CALL_END + } + navitem.largeTitleDisplayMode = + config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; ++ ++ if (@available(iOS 16.0, *)) { ++ navitem.style = (UINavigationItemStyle)config.navigationItemStyle; ++ } + #endif + + UINavigationBarAppearance *appearance = [self buildAppearance:vc withConfig:config]; + navitem.standardAppearance = appearance; + navitem.compactAppearance = appearance; ++ if (@available(iOS 26.0, *)) { ++ navitem.subtitle = config.subtitle; ++ navitem.largeSubtitle = config.largeSubtitle; ++ } + + // appearance does not apply to the tvOS so we need to use lagacy customization + #if TARGET_OS_TV +@@ -638,9 +665,196 @@ RNS_IGNORE_SUPER_CALL_END + // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) + navitem.title = config.title; + navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems +- withCurrentItems:navitem.leftBarButtonItems]; ++ withCurrentItems:navitem.leftBarButtonItems ++ navigationItem:navitem]; + navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems +- withCurrentItems:navitem.rightBarButtonItems]; ++ withCurrentItems:navitem.rightBarButtonItems ++ navigationItem:navitem]; ++ NSDictionary *mailSearchToolbarConfig = nil; ++ for (NSDictionary *toolbarConfig in config.headerToolbarItems) { ++ if (toolbarConfig[@"mailSearchToolbar"]) { ++ mailSearchToolbarConfig = toolbarConfig; ++ break; ++ } ++ } ++ if (mailSearchToolbarConfig == nil) { ++ for (NSDictionary *rightConfig in config.headerRightBarButtonItems) { ++ if ([rightConfig[@"bottomMailSearchToolbar"] boolValue]) { ++ mailSearchToolbarConfig = rightConfig; ++ break; ++ } ++ } ++ } ++ ++ static NSInteger const RNSMailSearchToolbarViewTag = 260628; ++ UIView *chromeHostView = vc.view; ++ UIView *existingMailSearchToolbar = [chromeHostView viewWithTag:RNSMailSearchToolbarViewTag]; ++ if (existingMailSearchToolbar == nil) { ++ existingMailSearchToolbar = [vc.view viewWithTag:RNSMailSearchToolbarViewTag]; ++ } ++ [existingMailSearchToolbar removeFromSuperview]; ++ ++ if (mailSearchToolbarConfig != nil) { ++#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ if (@available(iOS 26.0, *)) { ++ CGFloat horizontalInset = 18.0; ++ NSNumber *width = mailSearchToolbarConfig[@"width"]; ++ CGFloat hostWidth = chromeHostView.bounds.size.width; ++ if (hostWidth <= 0.0) { ++ hostWidth = vc.view.bounds.size.width; ++ } ++ if (hostWidth <= 0.0) { ++ hostWidth = [UIScreen mainScreen].bounds.size.width; ++ } ++ CGFloat fallbackWidth = MIN(560.0, MAX(300.0, hostWidth - (horizontalInset * 2.0))); ++ CGFloat resolvedWidth = width != nil ? width.doubleValue : fallbackWidth; ++ if (hostWidth > 0.0) { ++ resolvedWidth = MIN(resolvedWidth, MAX(260.0, hostWidth - 12.0)); ++ } ++ ++ UIView *toolbarHost = [[UIView alloc] init]; ++ toolbarHost.tag = RNSMailSearchToolbarViewTag; ++ toolbarHost.translatesAutoresizingMaskIntoConstraints = NO; ++ [chromeHostView addSubview:toolbarHost]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [toolbarHost.centerXAnchor constraintEqualToAnchor:chromeHostView.centerXAnchor], ++ [toolbarHost.bottomAnchor constraintEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor constant:-16.0], ++ [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth], ++ [toolbarHost.heightAnchor constraintEqualToConstant:56.0], ++ ]]; ++ [chromeHostView bringSubviewToFront:toolbarHost]; ++ ++ UIGlassEffect *glassEffect = [UIGlassEffect effectWithStyle:UIGlassEffectStyleRegular]; ++ glassEffect.interactive = YES; ++ UIVisualEffectView *glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect]; ++ glassView.clipsToBounds = YES; ++ glassView.layer.cornerRadius = 28.0; ++ glassView.translatesAutoresizingMaskIntoConstraints = NO; ++ [toolbarHost addSubview:glassView]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [glassView.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor constant:64.0], ++ [glassView.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor constant:-64.0], ++ [glassView.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], ++ [glassView.heightAnchor constraintEqualToConstant:56.0], ++ ]]; ++ ++ void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) { ++ auto eventEmitter = std::static_pointer_cast( ++ config->_eventEmitter); ++ if (eventEmitter && buttonId) { ++ eventEmitter->onPressHeaderBarButtonItem( ++ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem{ ++ .buttonId = std::string([buttonId UTF8String])}); ++ } ++ }; ++ ++ void (^emitMenuPress)(NSString *) = ^(NSString *menuId) { ++ auto eventEmitter = std::static_pointer_cast( ++ config->_eventEmitter); ++ if (eventEmitter && menuId) { ++ eventEmitter->onPressHeaderBarButtonMenuItem( ++ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem{ ++ .menuId = std::string([menuId UTF8String])}); ++ } ++ }; ++ ++ UIButton *(^makeGlassButton)(NSString *, NSString *, NSDictionary *) = ++ ^UIButton *(NSString *systemImageName, NSString *buttonId, NSDictionary *menuConfig) { ++ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; ++ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; ++ configuration.image = [UIImage systemImageNamed:systemImageName ?: @"circle"]; ++ configuration.baseForegroundColor = UIColor.labelColor; ++ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; ++ if (menuConfig != nil) { ++ button.menu = [RNSBarButtonItem initUIMenuWithDict:menuConfig ++ menuAction:emitMenuPress ++ imageLoader:config->_imageLoader]; ++ button.showsMenuAsPrimaryAction = YES; ++ } else if (buttonId != nil) { ++ UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { ++ emitButtonPress(buttonId); ++ }]; ++ [button addAction:action forControlEvents:UIControlEventTouchUpInside]; ++ } ++ return button; ++ }; ++ ++ BOOL useFallbackSearchField = [mailSearchToolbarConfig[@"useFallbackSearchField"] boolValue]; ++ UISearchBar *searchBar = ++ !useFallbackSearchField && navitem.searchController != nil ? navitem.searchController.searchBar : nil; ++ NSString *placeholder = mailSearchToolbarConfig[@"placeholder"]; ++ if (searchBar != nil) { ++ if (placeholder != nil) { ++ searchBar.placeholder = placeholder; ++ } ++ searchBar.hidden = NO; ++ searchBar.alpha = 1.0; ++ searchBar.userInteractionEnabled = YES; ++ searchBar.searchBarStyle = UISearchBarStyleMinimal; ++ searchBar.backgroundImage = [UIImage new]; ++ searchBar.searchTextField.hidden = NO; ++ searchBar.searchTextField.alpha = 1.0; ++ searchBar.searchTextField.backgroundColor = UIColor.clearColor; ++ searchBar.searchTextField.textColor = UIColor.labelColor; ++ searchBar.searchTextField.tintColor = UIColor.labelColor; ++ searchBar.translatesAutoresizingMaskIntoConstraints = NO; ++ [glassView.contentView addSubview:searchBar]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [searchBar.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:8.0], ++ [searchBar.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-8.0], ++ [searchBar.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor], ++ [searchBar.heightAnchor constraintEqualToConstant:50.0], ++ ]]; ++ } else { ++ UISearchTextField *searchField = [UISearchTextField new]; ++ if (placeholder != nil) { ++ searchField.placeholder = placeholder; ++ } ++ NSString *searchTextChangeId = mailSearchToolbarConfig[@"searchTextChangeId"]; ++ if (searchTextChangeId != nil) { ++ [searchField addAction:[UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { ++ UISearchTextField *field = (UISearchTextField *)action.sender; ++ emitButtonPress([NSString stringWithFormat:@"%@:%@", searchTextChangeId, field.text ?: @""]); ++ }] ++ forControlEvents:UIControlEventEditingChanged]; ++ } ++ searchField.borderStyle = UITextBorderStyleNone; ++ searchField.translatesAutoresizingMaskIntoConstraints = NO; ++ [glassView.contentView addSubview:searchField]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [searchField.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:16.0], ++ [searchField.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-16.0], ++ [searchField.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor], ++ [searchField.heightAnchor constraintEqualToConstant:48.0], ++ ]]; ++ } ++ ++ UIButton *filterButton = makeGlassButton( ++ mailSearchToolbarConfig[@"filterSystemImageName"] ?: @"line.3.horizontal.decrease", ++ mailSearchToolbarConfig[@"filterButtonId"], ++ mailSearchToolbarConfig[@"filterMenu"]); ++ filterButton.translatesAutoresizingMaskIntoConstraints = NO; ++ [toolbarHost addSubview:filterButton]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [filterButton.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor], ++ [filterButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], ++ [filterButton.widthAnchor constraintEqualToConstant:56.0], ++ [filterButton.heightAnchor constraintEqualToConstant:56.0], ++ ]]; ++ ++ UIButton *composeButton = makeGlassButton( ++ mailSearchToolbarConfig[@"composeSystemImageName"] ?: @"square.and.pencil", ++ mailSearchToolbarConfig[@"composeButtonId"], ++ mailSearchToolbarConfig[@"composeMenu"]); ++ composeButton.translatesAutoresizingMaskIntoConstraints = NO; ++ [toolbarHost addSubview:composeButton]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [composeButton.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor], ++ [composeButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], ++ [composeButton.widthAnchor constraintEqualToConstant:56.0], ++ [composeButton.heightAnchor constraintEqualToConstant:56.0], ++ ]]; ++ } ++#endif ++ } ++ ++ NSArray *> *navigationToolbarConfigs = config.headerToolbarItems; ++ if (mailSearchToolbarConfig != nil) { ++ navigationToolbarConfigs = @[]; ++ } ++ ++ NSArray *toolbarItems = [config barButtonItemsFromConfigs:navigationToolbarConfigs ++ withCurrentItems:@[] ++ navigationItem:navitem]; ++ if (toolbarItems.count > 0) { ++ vc.toolbarItems = toolbarItems; ++ [navctr setToolbarHidden:NO animated:animated]; ++ } else { ++ vc.toolbarItems = nil; ++ [navctr setToolbarHidden:YES animated:animated]; ++ } + + // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items + // (setting nav bar visibility should be done after `navitem.*BarButtonItems`). +@@ -773,6 +987,7 @@ RNS_IGNORE_SUPER_CALL_END + + - (NSArray *)barButtonItemsFromConfigs:(NSArray *> *)dicts + withCurrentItems:(NSArray *)currentItems ++ navigationItem:(UINavigationItem *)navitem + { + if (dicts.count == 0) { + return currentItems; +@@ -781,7 +996,161 @@ RNS_IGNORE_SUPER_CALL_END + [items addObjectsFromArray:currentItems]; + for (NSUInteger i = 0; i < dicts.count; i++) { + NSDictionary *dict = dicts[i]; +- if (dict[@"buttonId"] || dict[@"menu"]) { ++ if (dict[@"mailSearchToolbar"]) { ++#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ if (@available(iOS 26.0, *)) { ++ NSNumber *width = dict[@"width"]; ++ CGFloat resolvedWidth = width != nil ? width.doubleValue : MAX(320.0, [UIScreen mainScreen].bounds.size.width - 56.0); ++ CGFloat resolvedHeight = 58.0; ++ UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, resolvedWidth, resolvedHeight)]; ++ container.translatesAutoresizingMaskIntoConstraints = NO; ++ [container.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; ++ [container.heightAnchor constraintEqualToConstant:resolvedHeight].active = YES; ++ ++ UIStackView *stackView = [[UIStackView alloc] initWithFrame:container.bounds]; ++ stackView.axis = UILayoutConstraintAxisHorizontal; ++ stackView.alignment = UIStackViewAlignmentCenter; ++ stackView.distribution = UIStackViewDistributionFill; ++ stackView.spacing = 10.0; ++ stackView.translatesAutoresizingMaskIntoConstraints = NO; ++ [container addSubview:stackView]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [stackView.leadingAnchor constraintEqualToAnchor:container.leadingAnchor], ++ [stackView.trailingAnchor constraintEqualToAnchor:container.trailingAnchor], ++ [stackView.centerYAnchor constraintEqualToAnchor:container.centerYAnchor], ++ [stackView.heightAnchor constraintEqualToConstant:50.0], ++ ]]; ++ ++ void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) { ++ auto eventEmitter = std::static_pointer_cast( ++ self->_eventEmitter); ++ if (eventEmitter && buttonId) { ++ eventEmitter->onPressHeaderBarButtonItem( ++ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonItem{ ++ .buttonId = std::string([buttonId UTF8String])}); ++ } ++ }; ++ ++ void (^emitMenuPress)(NSString *) = ^(NSString *menuId) { ++ auto eventEmitter = std::static_pointer_cast( ++ self->_eventEmitter); ++ if (eventEmitter && menuId) { ++ eventEmitter->onPressHeaderBarButtonMenuItem( ++ facebook::react::RNSScreenStackHeaderConfigEventEmitter::OnPressHeaderBarButtonMenuItem{ ++ .menuId = std::string([menuId UTF8String])}); ++ } ++ }; ++ ++ UIButton *(^makeGlassButton)(NSString *, NSString *, NSDictionary *) = ++ ^UIButton *(NSString *systemImageName, NSString *buttonId, NSDictionary *menuConfig) { ++ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; ++ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; ++ configuration.image = [UIImage systemImageNamed:systemImageName ?: @"circle"]; ++ ++ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; ++ button.translatesAutoresizingMaskIntoConstraints = NO; ++ [button.widthAnchor constraintEqualToConstant:58.0].active = YES; ++ [button.heightAnchor constraintEqualToConstant:50.0].active = YES; ++ if (menuConfig != nil) { ++ button.menu = [RNSBarButtonItem initUIMenuWithDict:menuConfig ++ menuAction:emitMenuPress ++ imageLoader:self->_imageLoader]; ++ button.showsMenuAsPrimaryAction = YES; ++ } else if (buttonId != nil) { ++ UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { ++ emitButtonPress(buttonId); ++ }]; ++ [button addAction:action forControlEvents:UIControlEventTouchUpInside]; ++ } ++ return button; ++ }; ++ ++ UIButton *filterButton = makeGlassButton( ++ dict[@"filterSystemImageName"] ?: @"line.3.horizontal.decrease", ++ dict[@"filterButtonId"], ++ dict[@"filterMenu"]); ++ [stackView addArrangedSubview:filterButton]; ++ ++ UISearchBar *searchBar = navitem.searchController != nil ? navitem.searchController.searchBar : [UISearchBar new]; ++ NSString *placeholder = dict[@"placeholder"]; ++ if (placeholder != nil) { ++ searchBar.placeholder = placeholder; ++ } ++ searchBar.searchBarStyle = UISearchBarStyleMinimal; ++ searchBar.translatesAutoresizingMaskIntoConstraints = NO; ++ [searchBar.heightAnchor constraintEqualToConstant:50.0].active = YES; ++ [stackView addArrangedSubview:searchBar]; ++ ++ UIButton *composeButton = makeGlassButton( ++ dict[@"composeSystemImageName"] ?: @"square.and.pencil", ++ dict[@"composeButtonId"], ++ dict[@"composeMenu"]); ++ [stackView addArrangedSubview:composeButton]; ++ ++ UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:container]; ++ [items addObject:item]; ++ } ++#endif ++ } else if (dict[@"searchField"]) { ++#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ if (@available(iOS 26.0, *)) { ++ NSNumber *width = dict[@"width"]; ++ CGFloat resolvedWidth = width != nil ? width.doubleValue : 280.0; ++ UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, resolvedWidth, 44.0)]; ++ container.translatesAutoresizingMaskIntoConstraints = NO; ++ [container.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; ++ [container.heightAnchor constraintEqualToConstant:44.0].active = YES; ++ ++ UISearchTextField *searchField = [UISearchTextField new]; ++ NSString *placeholder = dict[@"placeholder"]; ++ if (placeholder != nil) { ++ searchField.placeholder = placeholder; ++ } ++ searchField.translatesAutoresizingMaskIntoConstraints = NO; ++ [container addSubview:searchField]; ++ [NSLayoutConstraint activateConstraints:@[ ++ [searchField.leadingAnchor constraintEqualToAnchor:container.leadingAnchor], ++ [searchField.trailingAnchor constraintEqualToAnchor:container.trailingAnchor], ++ [searchField.centerYAnchor constraintEqualToAnchor:container.centerYAnchor], ++ [searchField.heightAnchor constraintEqualToConstant:40.0], ++ ]]; ++ ++ UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithCustomView:container]; ++ [items addObject:item]; ++ } ++#endif ++ } else if (dict[@"searchBarPlacement"]) { ++#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ if (@available(iOS 26.0, *)) { ++ NSNumber *width = dict[@"width"]; ++ UIBarButtonItem *item = nil; ++ if ([dict[@"activatesSearchController"] boolValue]) { ++ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; ++ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; ++ configuration.image = [UIImage systemImageNamed:@"magnifyingglass"]; ++ configuration.baseForegroundColor = UIColor.labelColor; ++ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; ++ button.translatesAutoresizingMaskIntoConstraints = NO; ++ [button.widthAnchor constraintEqualToConstant:56.0].active = YES; ++ [button.heightAnchor constraintEqualToConstant:56.0].active = YES; ++ UIAction *action = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull action) { ++ if (navitem.searchController != nil) { ++ [navitem.searchController setActive:YES]; ++ [navitem.searchController.searchBar becomeFirstResponder]; ++ } ++ }]; ++ [button addAction:action forControlEvents:UIControlEventTouchUpInside]; ++ item = [[UIBarButtonItem alloc] initWithCustomView:button]; ++ } else if ([dict[@"customView"] boolValue] && navitem.searchController != nil) { ++ UISearchBar *searchBar = navitem.searchController.searchBar; ++ CGFloat resolvedWidth = width != nil ? width.doubleValue : searchBar.frame.size.width; ++ CGFloat resolvedHeight = searchBar.frame.size.height > 0 ? searchBar.frame.size.height : 44.0; ++ UIView *container = [[UIView alloc] initWithFrame:CGRectMake(0, 0, resolvedWidth, resolvedHeight)]; ++ container.translatesAutoresizingMaskIntoConstraints = NO; ++ [container.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; ++ [container.heightAnchor constraintEqualToConstant:resolvedHeight].active = YES; ++ searchBar.frame = container.bounds; ++ searchBar.autoresizingMask = UIViewAutoresizingFlexibleWidth | UIViewAutoresizingFlexibleHeight; ++ [container addSubview:searchBar]; ++ item = [[UIBarButtonItem alloc] initWithCustomView:container]; ++ } else { ++ item = navitem.searchBarPlacementBarButtonItem; ++ if (width != nil) { ++ item.width = width.doubleValue; ++ } ++ } ++ NSNumber *index = dict[@"index"]; ++ if (index != nil && index.integerValue < items.count) { ++ [items insertObject:item atIndex:index.integerValue]; ++ } else { ++ [items addObject:item]; ++ } ++ } ++#endif ++ } else if (dict[@"buttonId"] || dict[@"menu"]) { + RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict + action:^(NSString *buttonId) { + auto eventEmitter = std::static_pointer_cast( +@@ -809,11 +1178,15 @@ RNS_IGNORE_SUPER_CALL_END + [items addObject:item]; + } + } else if (dict[@"spacing"]) { +- UIBarButtonItem *item = [[UIBarButtonItem alloc] initWithBarButtonSystemItem:UIBarButtonSystemItemFixedSpace +- target:nil +- action:nil]; ++ BOOL flexible = [dict[@"flexible"] boolValue]; ++ UIBarButtonItem *item = [[UIBarButtonItem alloc] ++ initWithBarButtonSystemItem:(flexible ? UIBarButtonSystemItemFlexibleSpace : UIBarButtonSystemItemFixedSpace) ++ target:nil ++ action:nil]; + NSNumber *spacingValue = dict[@"spacing"]; +- item.width = [spacingValue doubleValue]; ++ if (!flexible) { ++ item.width = [spacingValue doubleValue]; ++ } + NSNumber *index = dict[@"index"]; + if (index.integerValue < items.count) { + [items insertObject:item atIndex:index.integerValue]; +@@ -1013,6 +1386,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) + } + + _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); ++ _subtitle = RCTNSStringFromStringNilIfEmpty(newScreenProps.subtitle); ++ _largeSubtitle = RCTNSStringFromStringNilIfEmpty(newScreenProps.largeSubtitle); + if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { + _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); + } +@@ -1038,6 +1413,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) + _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; + _backButtonDisplayMode = + [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; ++ _navigationItemStyle = navigationItemStyleFromCppEquivalent(newScreenProps.navigationItemStyle); + + if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { + _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; +@@ -1084,6 +1460,18 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) + _headerRightBarButtonItems = array; + } + ++ if (newScreenProps.headerToolbarItems != oldScreenProps.headerToolbarItems) { ++ const auto &vec = newScreenProps.headerToolbarItems; ++ NSMutableArray *> *array = [NSMutableArray arrayWithCapacity:vec.size()]; ++ for (const auto &item : vec) { ++ NSDictionary *dict = [RNSConvert idFromFollyDynamic:item]; ++ if ([dict isKindOfClass:[NSDictionary class]]) { ++ [array addObject:dict]; ++ } ++ } ++ _headerToolbarItems = array; ++ } ++ + [self updateViewControllerIfNeeded]; + + if (needsNavigationControllerLayout) { diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h -index 4badf56..7703df9 100644 +index 4badf565055086e814808b7ef3bcefc5f89e8af4..7703df97d1a4572e301acf50ca8cba6d29ff5666 100644 --- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h +++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h @@ -1,10 +1,11 @@ @@ -65,7 +628,7 @@ index 4badf56..7703df9 100644 @end diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm -index 52c61c8..f536579 100644 +index 52c61c863c53c289569a741445ed08c0355d95ba..cb4d6be752f98e06dfd3b40453c2bd881886ad88 100644 --- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm +++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm @@ -2,6 +2,7 @@ @@ -76,7 +639,7 @@ index 52c61c8..f536579 100644 #import "RNSScrollViewSeeking.h" #import -@@ -57,17 +58,15 @@ namespace react = facebook::react; +@@ -60,16 +61,14 @@ namespace react = facebook::react; */ - (nullable UIScrollView *)findScrollView { @@ -100,8 +663,7 @@ index 52c61c8..f536579 100644 } - (nullable id)findFirstSeekingAncestor - { -@@ -122,7 +123,8 @@ namespace react = facebook::react; +@@ -122,7 +121,8 @@ namespace react = facebook::react; /** * Tries to resolve UIScrollView from the passed childView. * @@ -111,7 +673,7 @@ index 52c61c8..f536579 100644 */ - (nullable UIScrollView *)resolveScrollViewFromChildView:(nullable UIView *)childView { -@@ -138,7 +140,7 @@ namespace react = facebook::react; +@@ -138,7 +138,7 @@ namespace react = facebook::react; return static_cast(childView).scrollView; } @@ -120,8 +682,126 @@ index 52c61c8..f536579 100644 } #pragma mark - Override +diff --git a/lib/commonjs/components/ScreenStackHeaderConfig.js b/lib/commonjs/components/ScreenStackHeaderConfig.js +index 16b979bb3dfb41ff247403f1632c300a9a60d549..4c1b5a7491fb6555cdacb9fa5ddb473d7e9c4556 100644 +--- a/lib/commonjs/components/ScreenStackHeaderConfig.js ++++ b/lib/commonjs/components/ScreenStackHeaderConfig.js +@@ -23,18 +23,35 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ + } = (0, _TopInsetApplicationContext.useTopInsetApplication)(!props.hidden, props.disableTopInsetApplication ?? false); + const { + headerLeftBarButtonItems, +- headerRightBarButtonItems ++ headerRightBarButtonItems, ++ headerToolbarItems + } = props; + const preparedHeaderLeftBarButtonItems = headerLeftBarButtonItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerLeftBarButtonItems, 'left') : undefined; + const preparedHeaderRightBarButtonItems = headerRightBarButtonItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerRightBarButtonItems, 'right') : undefined; +- const hasHeaderBarButtonItems = _utils.isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length); ++ const preparedHeaderToolbarItems = headerToolbarItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerToolbarItems, 'right') : undefined; ++ const hasHeaderBarButtonItems = _utils.isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length || preparedHeaderToolbarItems?.length); + + // Handle bar button item presses + const onPressHeaderBarButtonItem = hasHeaderBarButtonItems ? event => { +- const pressedItem = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])].find(item => item && 'buttonId' in item && item.buttonId === event.nativeEvent.buttonId); ++ const buttonId = event.nativeEvent.buttonId; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; ++ const pressedItem = allItems.find(item => item && 'buttonId' in item && item.buttonId === buttonId); + if (pressedItem && pressedItem.type === 'button' && pressedItem.onPress) { + pressedItem.onPress(); + } ++ for (const item of allItems) { ++ if (!item || item.type !== 'mailSearchToolbar') { ++ continue; ++ } ++ if (item.filterButtonId === buttonId) { ++ item.onFilterPress?.(); ++ return; ++ } ++ if (item.composeButtonId === buttonId) { ++ item.onComposePress?.(); ++ return; ++ } ++ const searchTextChangePrefix = item.searchTextChangeId ? `${item.searchTextChangeId}:` : undefined; ++ if (searchTextChangePrefix && buttonId.startsWith(searchTextChangePrefix)) { ++ item.onSearchTextChange?.(buttonId.slice(searchTextChangePrefix.length)); ++ return; ++ } ++ } + } : undefined; + + // Handle bar button menu item presses by deep-searching nested menus +@@ -56,7 +73,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ + }; + + // Check each bar-button item with a menu +- const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])]; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; + for (const item of allItems) { + if (item && item.type === 'menu' && item.menu) { + const action = findInMenu(item.menu, event.nativeEvent.menuId); +@@ -64,6 +81,15 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ + action.onPress(); + return; + } ++ } else if (item && item.type === 'mailSearchToolbar') { ++ const toolbarMenus = [item.filterMenu, item.composeMenu].filter(Boolean); ++ for (const toolbarMenu of toolbarMenus) { ++ const action = findInMenu(toolbarMenu, event.nativeEvent.menuId); ++ if (action) { ++ action.onPress(); ++ return; ++ } ++ } + } + } + } : undefined; +@@ -71,6 +97,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ + userInterfaceStyle: props.experimental_userInterfaceStyle, + headerLeftBarButtonItems: preparedHeaderLeftBarButtonItems, + headerRightBarButtonItems: preparedHeaderRightBarButtonItems, ++ headerToolbarItems: preparedHeaderToolbarItems, + onPressHeaderBarButtonItem: onPressHeaderBarButtonItem, + onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, + ref: ref, +diff --git a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js +index ab93f62e4a7049d63c6681cecbd6cf8b1d07ce10..3a7196894cae4e13ec3357e178f181578407dd2a 100644 +--- a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js ++++ b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js +@@ -41,10 +41,31 @@ const prepareMenu = (menu, index, side, path = '') => { + }; + }; + const prepareHeaderBarButtonItems = (barButtonItems, side) => { +- return barButtonItems?.map((item, index) => { ++ const items = Array.isArray(barButtonItems) ? barButtonItems : barButtonItems && typeof barButtonItems === 'object' && 'type' in barButtonItems ? [barButtonItems] : undefined; ++ return items?.map((item, index) => { + if (item.type === 'spacing') { + return item; + } ++ if (item.type === 'searchBarPlacement') { ++ return { ++ ...item, ++ searchBarPlacement: true ++ }; ++ } ++ if (item.type === 'searchField') { ++ return { ++ ...item, ++ searchField: true ++ }; ++ } ++ if (item.type === 'mailSearchToolbar') { ++ return { ++ ...item, ++ mailSearchToolbar: true, ++ filterMenu: item.filterMenu ? prepareMenu(item.filterMenu, index, side, 'filter') : undefined, ++ composeMenu: item.composeMenu ? prepareMenu(item.composeMenu, index, side, 'compose') : undefined ++ }; ++ } + let imageSource, templateSource; + if (item.icon?.type === 'imageSource') { + imageSource = _reactNative.Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/commonjs/index.js b/lib/commonjs/index.js -index de25040..48bb962 100644 +index de25040bfec8587ca311f5a42661240640989419..bf73f41744e08bc60ffac09029419d19a94425ee 100644 --- a/lib/commonjs/index.js +++ b/lib/commonjs/index.js @@ -202,6 +202,7 @@ var _utils = require("./utils"); @@ -132,7 +812,7 @@ index de25040..48bb962 100644 Object.keys(_tabs).forEach(function (key) { if (key === "default" || key === "__esModule") return; if (Object.prototype.hasOwnProperty.call(_exportNames, key)) return; -@@ -213,7 +214,18 @@ Object.keys(_tabs).forEach(function (key) { +@@ -213,6 +214,17 @@ Object.keys(_tabs).forEach(function (key) { } }); }); @@ -150,23 +830,171 @@ index de25040..48bb962 100644 function _interopRequireDefault(e) { return e && e.__esModule ? e : { default: e }; } function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } --//# sourceMappingURL=index.js.map -\ No newline at end of file -+//# sourceMappingURL=index.js.map +diff --git a/lib/module/components/ScreenStackHeaderConfig.js b/lib/module/components/ScreenStackHeaderConfig.js +index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..5b83738e58d03192452b03d06453292ece929b41 100644 +--- a/lib/module/components/ScreenStackHeaderConfig.js ++++ b/lib/module/components/ScreenStackHeaderConfig.js +@@ -19,18 +19,35 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref + } = useTopInsetApplication(!props.hidden, props.disableTopInsetApplication ?? false); + const { + headerLeftBarButtonItems, +- headerRightBarButtonItems ++ headerRightBarButtonItems, ++ headerToolbarItems + } = props; + const preparedHeaderLeftBarButtonItems = headerLeftBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerLeftBarButtonItems, 'left') : undefined; + const preparedHeaderRightBarButtonItems = headerRightBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerRightBarButtonItems, 'right') : undefined; +- const hasHeaderBarButtonItems = isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length); ++ const preparedHeaderToolbarItems = headerToolbarItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerToolbarItems, 'right') : undefined; ++ const hasHeaderBarButtonItems = isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length || preparedHeaderToolbarItems?.length); + + // Handle bar button item presses + const onPressHeaderBarButtonItem = hasHeaderBarButtonItems ? event => { +- const pressedItem = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])].find(item => item && 'buttonId' in item && item.buttonId === event.nativeEvent.buttonId); ++ const buttonId = event.nativeEvent.buttonId; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; ++ const pressedItem = allItems.find(item => item && 'buttonId' in item && item.buttonId === buttonId); + if (pressedItem && pressedItem.type === 'button' && pressedItem.onPress) { + pressedItem.onPress(); + } ++ for (const item of allItems) { ++ if (!item || item.type !== 'mailSearchToolbar') { ++ continue; ++ } ++ if (item.filterButtonId === buttonId) { ++ item.onFilterPress?.(); ++ return; ++ } ++ if (item.composeButtonId === buttonId) { ++ item.onComposePress?.(); ++ return; ++ } ++ const searchTextChangePrefix = item.searchTextChangeId ? `${item.searchTextChangeId}:` : undefined; ++ if (searchTextChangePrefix && buttonId.startsWith(searchTextChangePrefix)) { ++ item.onSearchTextChange?.(buttonId.slice(searchTextChangePrefix.length)); ++ return; ++ } ++ } + } : undefined; + + // Handle bar button menu item presses by deep-searching nested menus +@@ -52,7 +69,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref + }; + + // Check each bar-button item with a menu +- const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])]; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; + for (const item of allItems) { + if (item && item.type === 'menu' && item.menu) { + const action = findInMenu(item.menu, event.nativeEvent.menuId); +@@ -60,6 +77,15 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref + action.onPress(); + return; + } ++ } else if (item && item.type === 'mailSearchToolbar') { ++ const toolbarMenus = [item.filterMenu, item.composeMenu].filter(Boolean); ++ for (const toolbarMenu of toolbarMenus) { ++ const action = findInMenu(toolbarMenu, event.nativeEvent.menuId); ++ if (action) { ++ action.onPress(); ++ return; ++ } ++ } + } + } + } : undefined; +@@ -67,6 +93,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref + userInterfaceStyle: props.experimental_userInterfaceStyle, + headerLeftBarButtonItems: preparedHeaderLeftBarButtonItems, + headerRightBarButtonItems: preparedHeaderRightBarButtonItems, ++ headerToolbarItems: preparedHeaderToolbarItems, + onPressHeaderBarButtonItem: onPressHeaderBarButtonItem, + onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, + ref: ref, +diff --git a/lib/module/components/helpers/prepareHeaderBarButtonItems.js b/lib/module/components/helpers/prepareHeaderBarButtonItems.js +index 8a70ffd78617147418d628c03c580bb7ff9a8a72..749ca41df55394c264601c56dad2a59970ac0e4d 100644 +--- a/lib/module/components/helpers/prepareHeaderBarButtonItems.js ++++ b/lib/module/components/helpers/prepareHeaderBarButtonItems.js +@@ -35,10 +35,31 @@ const prepareMenu = (menu, index, side, path = '') => { + }; + }; + export const prepareHeaderBarButtonItems = (barButtonItems, side) => { +- return barButtonItems?.map((item, index) => { ++ const items = Array.isArray(barButtonItems) ? barButtonItems : barButtonItems && typeof barButtonItems === 'object' && 'type' in barButtonItems ? [barButtonItems] : undefined; ++ return items?.map((item, index) => { + if (item.type === 'spacing') { + return item; + } ++ if (item.type === 'searchBarPlacement') { ++ return { ++ ...item, ++ searchBarPlacement: true ++ }; ++ } ++ if (item.type === 'searchField') { ++ return { ++ ...item, ++ searchField: true ++ }; ++ } ++ if (item.type === 'mailSearchToolbar') { ++ return { ++ ...item, ++ mailSearchToolbar: true, ++ filterMenu: item.filterMenu ? prepareMenu(item.filterMenu, index, side, 'filter') : undefined, ++ composeMenu: item.composeMenu ? prepareMenu(item.composeMenu, index, side, 'compose') : undefined ++ }; ++ } + let imageSource, templateSource; + if (item.icon?.type === 'imageSource') { + imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/module/index.js b/lib/module/index.js -index 07caaf7..ba2e9c5 100644 +index 07caaf7f04c1d6484727b43b694ccf76b1988984..ba2e9c5e0c788a64db13733f403c9087567ac6a0 100644 --- a/lib/module/index.js +++ b/lib/module/index.js @@ -43,4 +43,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; * EXPERIMENTAL API BELOW. MIGHT CHANGE W/O ANY NOTICE */ export * from './components/tabs'; --//# sourceMappingURL=index.js.map -\ No newline at end of file +export * from './components/gamma/scroll-view-marker'; -+//# sourceMappingURL=index.js.map + //# sourceMappingURL=index.js.map +diff --git a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..ec0e5ec08f0240155beaa3ada54bcda9ddf83415 100644 +--- a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts ++++ b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +@@ -9,6 +9,7 @@ type OnPressHeaderBarButtonMenuItemEvent = Readonly<{ + menuId: string; + }>; + type BackButtonDisplayMode = 'minimal' | 'default' | 'generic'; ++type NavigationItemStyle = 'navigator' | 'browser' | 'editor'; + type BlurEffect = 'none' | 'extraLight' | 'light' | 'dark' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark'; + type UserInterfaceStyle = 'unspecified' | 'light' | 'dark'; + export interface NativeProps extends ViewProps { +@@ -32,18 +33,22 @@ export interface NativeProps extends ViewProps { + largeTitleColor?: ColorValue | undefined; + translucent?: boolean | undefined; + title?: string | undefined; ++ subtitle?: string | undefined; ++ largeSubtitle?: string | undefined; + titleFontFamily?: string | undefined; + titleFontSize?: CT.Int32 | undefined; + titleFontWeight?: string | undefined; + titleColor?: ColorValue | undefined; + disableBackButtonMenu?: boolean | undefined; + backButtonDisplayMode?: CT.WithDefault; ++ navigationItemStyle?: CT.WithDefault; + hideBackButton?: boolean | undefined; + backButtonInCustomView?: boolean | undefined; + blurEffect?: CT.WithDefault; + topInsetEnabled?: boolean | undefined; + headerLeftBarButtonItems?: CT.UnsafeMixed[] | undefined; + headerRightBarButtonItems?: CT.UnsafeMixed[] | undefined; ++ headerToolbarItems?: CT.UnsafeMixed[] | undefined; + onPressHeaderBarButtonItem?: CT.DirectEventHandler | undefined; + onPressHeaderBarButtonMenuItem?: CT.DirectEventHandler | undefined; + synchronousShadowStateUpdatesEnabled?: CT.WithDefault; diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts -index 8e4480f..7a2c094 100644 +index 8e4480f901d5d557244643a32341d46e9a497e68..883c5f264301058cbcd9090bee2702ee02346161 100644 --- a/lib/typescript/index.d.ts +++ b/lib/typescript/index.d.ts @@ -32,5 +32,6 @@ export { default as useTransitionProgress } from './useTransitionProgress'; @@ -175,11 +1003,318 @@ index 8e4480f..7a2c094 100644 export * from './components/tabs'; +export * from './components/gamma/scroll-view-marker'; export type * from './components/shared/types'; --//# sourceMappingURL=index.d.ts.map + //# sourceMappingURL=index.d.ts.map \ No newline at end of file -+//# sourceMappingURL=index.d.ts.map +diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts +index 3b384e03891e38e936f370372a682d73440e7ec2..9f643bfd4f99fbb36d2a8214fdbf8d207b4cb9ec 100644 +--- a/lib/typescript/types.d.ts ++++ b/lib/typescript/types.d.ts +@@ -10,6 +10,7 @@ export type SearchBarCommands = { + cancelSearch: () => void; + }; + export type BackButtonDisplayMode = 'default' | 'generic' | 'minimal'; ++export type NavigationItemStyle = 'navigator' | 'browser' | 'editor'; + export type StackPresentationTypes = 'push' | 'modal' | 'transparentModal' | 'containedModal' | 'containedTransparentModal' | 'fullScreenModal' | 'formSheet' | 'pageSheet'; + export type StackAnimationTypes = 'default' | 'fade' | 'fade_from_bottom' | 'flip' | 'none' | 'simple_push' | 'slide_from_bottom' | 'slide_from_right' | 'slide_from_left' | 'ios_from_right' | 'ios_from_left'; + export type BlurEffectTypes = BlurEffect; +@@ -631,6 +632,14 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { + * @platform ios + */ + backButtonDisplayMode?: BackButtonDisplayMode | undefined; ++ /** ++ * Controls the UIKit `UINavigationItem.style` used by native iOS headers. ++ * ++ * `editor` matches modern document/editor-style bars with leading-aligned compact titles and glass controls. ++ * ++ * @platform ios ++ */ ++ navigationItemStyle?: NavigationItemStyle | undefined; + /** + * Array of UIBarButtomItems to the left side of the header. + * +@@ -643,6 +652,16 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { + * @platform ios + */ + headerRightBarButtonItems?: HeaderBarButtonItem[] | undefined; ++ /** ++ * Array of UIBarButtonItems to display in the native toolbar attached to ++ * the current screen's navigation controller. ++ * ++ * On iOS 26+, this can contain a `searchBarPlacement` item to let UIKit ++ * render integrated Mail-style search chrome using the attached SearchBar. ++ * ++ * @platform ios ++ */ ++ headerToolbarItems?: HeaderBarButtonItem[] | undefined; + /** + * When set to true the header will be hidden while the parent Screen is on the top of the stack. The default value is false. + */ +@@ -704,6 +723,20 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { + * String that can be displayed in the header as a fallback for `headerTitle`. + */ + title?: string | undefined; ++ /** ++ * String displayed below the compact navigation title on iOS 26+. ++ * ++ * @platform ios ++ */ ++ subtitle?: string | undefined; ++ /** ++ * String displayed below the large navigation title on iOS 26+. ++ * ++ * Falls back to `subtitle` when omitted. ++ * ++ * @platform ios ++ */ ++ largeSubtitle?: string | undefined; + /** + * Allows for setting text color of the title. + */ +@@ -1145,8 +1178,33 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { + export interface HeaderBarButtonItemSpacing { + type: 'spacing'; + spacing: number; ++ flexible?: boolean | undefined; ++} ++export interface HeaderBarButtonSearchBarPlacementItem { ++ type: 'searchBarPlacement'; ++ activatesSearchController?: boolean | undefined; ++ customView?: boolean | undefined; ++ index?: number | undefined; ++ width?: number | undefined; ++} ++export interface HeaderBarButtonSearchFieldItem { ++ type: 'searchField'; ++ placeholder?: string | undefined; ++ width?: number | undefined; ++} ++export interface HeaderBarButtonMailSearchToolbarItem { ++ type: 'mailSearchToolbar'; ++ composeButtonId?: string | undefined; ++ composeMenu?: HeaderBarButtonItemWithMenu['menu'] | undefined; ++ composeSystemImageName?: string | undefined; ++ filterButtonId?: string | undefined; ++ filterMenu?: HeaderBarButtonItemWithMenu['menu'] | undefined; ++ filterSystemImageName?: string | undefined; ++ onComposePress?: (() => void) | undefined; ++ onFilterPress?: (() => void) | undefined; ++ onSearchTextChange?: ((text: string) => void) | undefined; ++ placeholder?: string | undefined; ++ searchTextChangeId?: string | undefined; ++ useFallbackSearchField?: boolean | undefined; ++ width?: number | undefined; + } +-export type HeaderBarButtonItem = HeaderBarButtonItemWithAction | HeaderBarButtonItemWithMenu | HeaderBarButtonItemSpacing; ++export type HeaderBarButtonItem = HeaderBarButtonItemWithAction | HeaderBarButtonItemWithMenu | HeaderBarButtonMailSearchToolbarItem | HeaderBarButtonSearchFieldItem | HeaderBarButtonSearchBarPlacementItem | HeaderBarButtonItemSpacing; + /** + * Custom Screen Transition + */ +diff --git a/src/components/ScreenStackHeaderConfig.tsx b/src/components/ScreenStackHeaderConfig.tsx +index 421b3c2545426ae957271bd6515bcf827437541c..7adcf68a0f53f4b49bfb6ad77d6090c110862aea 100644 +--- a/src/components/ScreenStackHeaderConfig.tsx ++++ b/src/components/ScreenStackHeaderConfig.tsx +@@ -39,7 +39,11 @@ export const ScreenStackHeaderConfig = React.forwardRef< + props.disableTopInsetApplication ?? false, + ); + +- const { headerLeftBarButtonItems, headerRightBarButtonItems } = props; ++ const { ++ headerLeftBarButtonItems, ++ headerRightBarButtonItems, ++ headerToolbarItems, ++ } = props; + + const preparedHeaderLeftBarButtonItems = + headerLeftBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform +@@ -49,22 +53,30 @@ export const ScreenStackHeaderConfig = React.forwardRef< + headerRightBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform + ? prepareHeaderBarButtonItems(headerRightBarButtonItems, 'right') + : undefined; ++ const preparedHeaderToolbarItems = ++ headerToolbarItems && isHeaderBarButtonsAvailableForCurrentPlatform ++ ? prepareHeaderBarButtonItems(headerToolbarItems, 'right') ++ : undefined; + const hasHeaderBarButtonItems = + isHeaderBarButtonsAvailableForCurrentPlatform && + (preparedHeaderLeftBarButtonItems?.length || +- preparedHeaderRightBarButtonItems?.length); ++ preparedHeaderRightBarButtonItems?.length || ++ preparedHeaderToolbarItems?.length); + + // Handle bar button item presses + const onPressHeaderBarButtonItem = hasHeaderBarButtonItems + ? (event: NativeSyntheticEvent<{ buttonId: string }>) => { +- const pressedItem = [ ++ const buttonId = event.nativeEvent.buttonId; ++ const allItems = [ + ...(preparedHeaderLeftBarButtonItems ?? []), + ...(preparedHeaderRightBarButtonItems ?? []), +- ].find( ++ ...(preparedHeaderToolbarItems ?? []), ++ ]; ++ const pressedItem = allItems.find( + item => + item && + 'buttonId' in item && +- item.buttonId === event.nativeEvent.buttonId, ++ item.buttonId === buttonId, + ); + if ( + pressedItem && +@@ -73,6 +85,19 @@ export const ScreenStackHeaderConfig = React.forwardRef< + ) { + pressedItem.onPress(); + } ++ for (const item of allItems) { ++ if (!item || item.type !== 'mailSearchToolbar') { ++ continue; ++ } ++ if (item.filterButtonId === buttonId) { ++ item.onFilterPress?.(); ++ return; ++ } ++ if (item.composeButtonId === buttonId) { ++ item.onComposePress?.(); ++ return; ++ } ++ const searchTextChangePrefix = item.searchTextChangeId ++ ? `${item.searchTextChangeId}:` ++ : undefined; ++ if ( ++ searchTextChangePrefix && ++ buttonId.startsWith(searchTextChangePrefix) ++ ) { ++ item.onSearchTextChange?.( ++ buttonId.slice(searchTextChangePrefix.length), ++ ); ++ return; ++ } ++ } + } + : undefined; + +@@ -102,6 +127,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< + const allItems = [ + ...(preparedHeaderLeftBarButtonItems ?? []), + ...(preparedHeaderRightBarButtonItems ?? []), ++ ...(preparedHeaderToolbarItems ?? []), + ]; + for (const item of allItems) { + if (item && item.type === 'menu' && item.menu) { +@@ -110,6 +136,17 @@ export const ScreenStackHeaderConfig = React.forwardRef< + action.onPress(); + return; + } ++ } else if (item && item.type === 'mailSearchToolbar') { ++ const toolbarMenus = [item.filterMenu, item.composeMenu].filter( ++ Boolean, ++ ); ++ for (const toolbarMenu of toolbarMenus) { ++ const action = findInMenu(toolbarMenu, event.nativeEvent.menuId); ++ if (action) { ++ action.onPress(); ++ return; ++ } ++ } + } + } + } +@@ -121,6 +158,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< + userInterfaceStyle={props.experimental_userInterfaceStyle} + headerLeftBarButtonItems={preparedHeaderLeftBarButtonItems} + headerRightBarButtonItems={preparedHeaderRightBarButtonItems} ++ headerToolbarItems={preparedHeaderToolbarItems} + onPressHeaderBarButtonItem={onPressHeaderBarButtonItem} + onPressHeaderBarButtonMenuItem={onPressHeaderBarButtonMenuItem} + ref={ref} +diff --git a/src/components/helpers/prepareHeaderBarButtonItems.ts b/src/components/helpers/prepareHeaderBarButtonItems.ts +index be2c24dfee77f5883b5ab1d7473be80933b5dc6f..0b038d2005b2d51bbb2ab1d7dba7eaccda83d42f 100644 +--- a/src/components/helpers/prepareHeaderBarButtonItems.ts ++++ b/src/components/helpers/prepareHeaderBarButtonItems.ts +@@ -50,13 +50,43 @@ const prepareMenu = ( + }; + + export const prepareHeaderBarButtonItems = ( +- barButtonItems: HeaderBarButtonItem[], ++ barButtonItems: ++ | HeaderBarButtonItem[] ++ | HeaderBarButtonItem ++ | null ++ | undefined, + side: 'left' | 'right', + ) => { +- return barButtonItems?.map((item, index) => { ++ const items = Array.isArray(barButtonItems) ++ ? barButtonItems ++ : barButtonItems && typeof barButtonItems === 'object' && 'type' in barButtonItems ++ ? [barButtonItems] ++ : undefined; ++ ++ return items?.map((item, index) => { + if (item.type === 'spacing') { + return item; + } ++ if (item.type === 'searchBarPlacement') { ++ return { ++ ...item, ++ searchBarPlacement: true, ++ }; ++ } ++ if (item.type === 'searchField') { ++ return { ++ ...item, ++ searchField: true, ++ }; ++ } ++ if (item.type === 'mailSearchToolbar') { ++ return { ++ ...item, ++ mailSearchToolbar: true, ++ filterMenu: item.filterMenu ? prepareMenu(item.filterMenu, index, side, 'filter') : undefined, ++ composeMenu: item.composeMenu ? prepareMenu(item.composeMenu, index, side, 'compose') : undefined, ++ }; ++ } + let imageSource, templateSource; + if (item.icon?.type === 'imageSource') { + imageSource = Image.resolveAssetSource(item.icon.imageSource); +diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +index ba2479de0f49c112470f78c5084059ddd9e2f3e8..811d41a20d1212a5d05f8cdf86d6b63755ed1ce1 100644 +--- a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts ++++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +@@ -14,6 +14,7 @@ type OnPressHeaderBarButtonItemEvent = Readonly<{ buttonId: string }>; + type OnPressHeaderBarButtonMenuItemEvent = Readonly<{ menuId: string }>; + + type BackButtonDisplayMode = 'minimal' | 'default' | 'generic'; ++type NavigationItemStyle = 'navigator' | 'browser' | 'editor'; + + type BlurEffect = + | 'none' +@@ -61,12 +62,15 @@ export interface NativeProps extends ViewProps { + largeTitleColor?: ColorValue | undefined; + translucent?: boolean | undefined; + title?: string | undefined; ++ subtitle?: string | undefined; ++ largeSubtitle?: string | undefined; + titleFontFamily?: string | undefined; + titleFontSize?: CT.Int32 | undefined; + titleFontWeight?: string | undefined; + titleColor?: ColorValue | undefined; + disableBackButtonMenu?: boolean | undefined; + backButtonDisplayMode?: CT.WithDefault; ++ navigationItemStyle?: CT.WithDefault; + hideBackButton?: boolean | undefined; + backButtonInCustomView?: boolean | undefined; + blurEffect?: CT.WithDefault; +@@ -74,6 +78,7 @@ export interface NativeProps extends ViewProps { + topInsetEnabled?: boolean | undefined; + headerLeftBarButtonItems?: CT.UnsafeMixed[] | undefined; + headerRightBarButtonItems?: CT.UnsafeMixed[] | undefined; ++ headerToolbarItems?: CT.UnsafeMixed[] | undefined; + onPressHeaderBarButtonItem?: + | CT.DirectEventHandler + | undefined; diff --git a/src/index.tsx b/src/index.tsx -index a405aec..cd649bd 100644 +index a405aec437bd879d8be7a706d29e965d77658899..cd649bd7ab7de7b7131302e8be682ec6d0081a9e 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -66,4 +66,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; @@ -188,3 +1323,115 @@ index a405aec..cd649bd 100644 export * from './components/tabs'; +export * from './components/gamma/scroll-view-marker'; export type * from './components/shared/types'; +diff --git a/src/types.tsx b/src/types.tsx +index 76a83f3acb6fd3f0af7f027798848b7124100286..7d6a81fecd759c393a2ba9bf797ea55e52721f32 100644 +--- a/src/types.tsx ++++ b/src/types.tsx +@@ -26,6 +26,7 @@ export type SearchBarCommands = { + }; + + export type BackButtonDisplayMode = 'default' | 'generic' | 'minimal'; ++export type NavigationItemStyle = 'navigator' | 'browser' | 'editor'; + export type StackPresentationTypes = + | 'push' + | 'modal' +@@ -735,6 +736,14 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { + * @platform ios + */ + backButtonDisplayMode?: BackButtonDisplayMode | undefined; ++ /** ++ * Controls the UIKit `UINavigationItem.style` used by native iOS headers. ++ * ++ * `editor` matches modern document/editor-style bars with leading-aligned compact titles and glass controls. ++ * ++ * @platform ios ++ */ ++ navigationItemStyle?: NavigationItemStyle | undefined; + /** + * Array of UIBarButtomItems to the left side of the header. + * +@@ -747,6 +756,16 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { + * @platform ios + */ + headerRightBarButtonItems?: HeaderBarButtonItem[] | undefined; ++ /** ++ * Array of UIBarButtonItems to display in the native toolbar attached to ++ * the current screen's navigation controller. ++ * ++ * On iOS 26+, this can contain a `searchBarPlacement` item to let UIKit ++ * render integrated Mail-style search chrome using the attached SearchBar. ++ * ++ * @platform ios ++ */ ++ headerToolbarItems?: HeaderBarButtonItem[] | undefined; + /** + * When set to true the header will be hidden while the parent Screen is on the top of the stack. The default value is false. + */ +@@ -808,6 +827,20 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { + * String that can be displayed in the header as a fallback for `headerTitle`. + */ + title?: string | undefined; ++ /** ++ * String displayed below the compact navigation title on iOS 26+. ++ * ++ * @platform ios ++ */ ++ subtitle?: string | undefined; ++ /** ++ * String displayed below the large navigation title on iOS 26+. ++ * ++ * Falls back to `subtitle` when omitted. ++ * ++ * @platform ios ++ */ ++ largeSubtitle?: string | undefined; + /** + * Allows for setting text color of the title. + */ +@@ -1279,11 +1312,42 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { + export interface HeaderBarButtonItemSpacing { + type: 'spacing'; + spacing: number; ++ flexible?: boolean | undefined; ++} ++ ++export interface HeaderBarButtonSearchBarPlacementItem { ++ type: 'searchBarPlacement'; ++ activatesSearchController?: boolean | undefined; ++ customView?: boolean | undefined; ++ index?: number | undefined; ++ width?: number | undefined; ++} ++ ++export interface HeaderBarButtonSearchFieldItem { ++ type: 'searchField'; ++ placeholder?: string | undefined; ++ width?: number | undefined; ++} ++ ++export interface HeaderBarButtonMailSearchToolbarItem { ++ type: 'mailSearchToolbar'; ++ composeButtonId?: string | undefined; ++ composeMenu?: HeaderBarButtonItemWithMenu['menu'] | undefined; ++ composeSystemImageName?: string | undefined; ++ filterButtonId?: string | undefined; ++ filterMenu?: HeaderBarButtonItemWithMenu['menu'] | undefined; ++ filterSystemImageName?: string | undefined; ++ onComposePress?: (() => void) | undefined; ++ onFilterPress?: (() => void) | undefined; ++ onSearchTextChange?: ((text: string) => void) | undefined; ++ placeholder?: string | undefined; ++ searchTextChangeId?: string | undefined; ++ useFallbackSearchField?: boolean | undefined; ++ width?: number | undefined; + } + + export type HeaderBarButtonItem = + | HeaderBarButtonItemWithAction + | HeaderBarButtonItemWithMenu ++ | HeaderBarButtonMailSearchToolbarItem ++ | HeaderBarButtonSearchFieldItem ++ | HeaderBarButtonSearchBarPlacementItem + | HeaderBarButtonItemSpacing; + + /** diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43540ffd62a..13e453e2582 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,30 +66,15 @@ overrides: packageExtensionsChecksum: sha256-FV3+2NW/9MFbunJaPsMxvO0LAzyfccoPtPiKoIXAwUU= patchedDependencies: - '@effect/vitest@4.0.0-beta.78': - hash: 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f - path: patches/@effect__vitest@4.0.0-beta.78.patch - '@expo/metro-config@56.0.14': - hash: 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 - path: patches/@expo%2Fmetro-config@56.0.14.patch - '@ff-labs/fff-node@0.9.4': - hash: 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 - path: patches/@ff-labs__fff-node@0.9.4.patch - '@pierre/diffs@1.3.0-beta.5': - hash: 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a - path: patches/@pierre%2Fdiffs@1.3.0-beta.5.patch - effect@4.0.0-beta.78: - hash: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 - path: patches/effect@4.0.0-beta.78.patch - expo-modules-jsi@56.0.10: - hash: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f - path: patches/expo-modules-jsi@56.0.10.patch - react-native-nitro-modules@0.35.9: - hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - path: patches/react-native-nitro-modules@0.35.9.patch - react-native-screens@4.25.2: - hash: 41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d - path: patches/react-native-screens@4.25.2.patch + '@effect/vitest@4.0.0-beta.78': 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f + '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 + '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 + '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a + effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 + expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f + expo-router@56.2.11: f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162 + react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 + react-native-screens@4.25.2: ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126 importers: @@ -198,10 +183,10 @@ importers: dependencies: '@callstack/liquid-glass': specifier: ^0.7.1 - version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@clerk/expo': specifier: 3.6.2 - version: 3.6.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 3.6.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@effect/atom-react': specifier: 4.0.0-beta.78 version: 4.0.0-beta.78(effect@4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5))(react@19.2.3)(scheduler@0.27.0) @@ -210,13 +195,13 @@ importers: version: 0.4.2 '@expo/metro-runtime': specifier: ~56.0.15 - version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/ui': specifier: ~56.0.18 - version: 56.0.18(19413efe5eaad64848598eedfe3a0fd3) + version: 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@legendapp/list': specifier: 3.2.0 - version: 3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@noble/curves': specifier: 'catalog:' version: 1.9.1 @@ -228,7 +213,7 @@ importers: version: 1.3.0-beta.5(patch_hash=7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a)(@shikijs/themes@4.2.0)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@react-native-menu/menu': specifier: ^2.0.0 - version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -249,7 +234,7 @@ importers: version: link:../../packages/contracts '@t3tools/mobile-markdown-text': specifier: file:./modules/t3-markdown-text - version: file:apps/mobile/modules/t3-markdown-text(a6e4bd1e9376530ea93dbb7d8a53d32d) + version: file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe) '@t3tools/mobile-review-diff-native': specifier: file:./modules/t3-review-diff version: file:apps/mobile/modules/t3-review-diff @@ -270,40 +255,40 @@ importers: version: 4.0.0-beta.78(patch_hash=c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5) expo: specifier: ~56.0.12 - version: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + version: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-asset: specifier: ~56.0.17 - version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + version: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-auth-session: specifier: ~56.0.14 - version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-build-properties: specifier: ~56.0.19 version: 56.0.19(expo@56.0.12) expo-camera: specifier: ~56.0.8 - version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-clipboard: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-constants: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) expo-dev-client: specifier: ~56.0.20 - version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-file-system: specifier: ~56.0.8 - version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-font: specifier: ~56.0.7 - version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-glass-effect: specifier: ~56.0.4 - version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: specifier: ~56.0.3 version: 56.0.3(expo@56.0.12) @@ -312,19 +297,19 @@ importers: version: 56.0.18(expo@56.0.12) expo-linking: specifier: ~56.0.14 - version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-network: specifier: ~56.0.5 version: 56.0.5(expo@56.0.12)(react@19.2.3) expo-notifications: specifier: ~56.0.18 - version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + version: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) expo-paste-input: specifier: ^0.1.15 - version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a) + version: 56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -333,16 +318,16 @@ importers: version: 56.0.10(expo@56.0.12)(typescript@6.0.3) expo-symbols: specifier: ~56.0.6 - version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-updates: specifier: ~56.0.19 - version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-web-browser: specifier: ~56.0.5 - version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + version: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-widgets: specifier: ~56.0.19 - version: 56.0.19(19413efe5eaad64848598eedfe3a0fd3) + version: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) punycode: specifier: ^2.3.1 version: 2.3.1 @@ -354,43 +339,43 @@ importers: version: 19.2.3(react@19.2.3) react-native: specifier: 0.85.3 - version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + version: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-gesture-handler: specifier: ~2.31.1 - version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-image-viewing: specifier: ^0.2.2 - version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-keyboard-controller: specifier: 1.21.6 - version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-markdown: specifier: ^0.5.0 - version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-nitro-modules: specifier: 0.35.9 - version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-reanimated: specifier: 4.3.1 - version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-safe-area-context: specifier: ~5.7.0 - version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 - version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-svg: specifier: 15.15.4 - version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-webview: specifier: ^13.16.1 - version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-worklets: specifier: 0.8.3 - version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) shiki: specifier: 4.2.0 version: 4.2.0 @@ -399,7 +384,7 @@ importers: version: 3.6.0 uniwind: specifier: ^1.6.2 - version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) + version: 1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0) devDependencies: '@effect/vitest': specifier: 4.0.0-beta.78 @@ -920,21 +905,25 @@ packages: resolution: {integrity: sha512-SRYfQcsXlOq+CD/FqkQBTSHbaD++w73GnnO+NUV9adLYrca3kfetRwWT1iguY1cNS0l34dCR3rlzCPq78vg1Jg==} cpu: [arm64] os: [linux] + libc: [musl] '@anthropic-ai/claude-agent-sdk-linux-arm64@0.3.170': resolution: {integrity: sha512-gLbaFqcGppFJQd4DLNV4IXoeahejT/p2/M8bSSvRDbla9GOsBr1AxV5XLRyBn1e7xFGozZIAIQr3+1chp7NJgQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@anthropic-ai/claude-agent-sdk-linux-x64-musl@0.3.170': resolution: {integrity: sha512-m4+I0qBEk7cxRKS+pL+eoWXbXTFOAo83fQ0tQvap4z/mDMm06IWJtEPoYTaMBwsp32GJWLkHWKbZSBCHZnp2DQ==} cpu: [x64] os: [linux] + libc: [musl] '@anthropic-ai/claude-agent-sdk-linux-x64@0.3.170': resolution: {integrity: sha512-Xl/m7TaSC3T5IDBdHrZQ9fCQYyDmPELN34CL+MoyPIf7uSmuZnjE9fUOqDh2Rv26JxWssi1M6X+BBvVuKd6Cpg==} cpu: [x64] os: [linux] + libc: [glibc] '@anthropic-ai/claude-agent-sdk-win32-arm64@0.3.170': resolution: {integrity: sha512-IG+8isJNNJKbnnhO7m+PGhfVCg+XoQ/MDxGde5eigFI0WsEfitjuWSWwx82bT9ghxI1aa6qNvI+UPgPcZuo5Fg==} @@ -986,24 +975,28 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@astrojs/compiler-binding-linux-arm64-musl@0.2.3': resolution: {integrity: sha512-O3e2CbN4yTsRguWYNnRd0p5YQ0H3fb7KpcR0W4R319q/gq5B1pJ7eqNbiO3b8g2AuiEcRTiUz5jeGT9j69cxOQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@astrojs/compiler-binding-linux-x64-gnu@0.2.3': resolution: {integrity: sha512-hbLBjXVp+96psMe7/7uqyrquGiULXANrq6REVxxPK/I5VzebZ7LHmSfykmByUbLyR1u+K6CTBKgvdQsK2L+2Xw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@astrojs/compiler-binding-linux-x64-musl@0.2.3': resolution: {integrity: sha512-vIiEvOwrJfHZMaTmqUCrFTIwMYL0+PD3Rvy7kFDQgERyx3zhaw8CPa01MCCqa+/sj344BGrXKZ6ti37SgNLMhw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@astrojs/compiler-binding-wasm32-wasi@0.2.3': resolution: {integrity: sha512-p9S2X8z/mUR2SMzAVJRFMCt8YaalKR+pjl2DgpdjzCQc6ww4bo8kiy54tgKqxZeNF5c+/2tCDTQIxVSm9V1FsA==} @@ -1592,21 +1585,25 @@ packages: resolution: {integrity: sha512-A/pWy8Jb/PhDYc2/JFuYh06gFJcsfBUBDl81YydGYBrL/Z4nItDfhNDNOibyeSN/lKKDRlycIHEIajjErk00sQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@bruits/satteri-linux-arm64-musl@0.9.3': resolution: {integrity: sha512-L6YxmyOSickzo4pE5WmZfNTJnjX0MtgKOsuwQfNZECTx9Ir5vl2B37EIwnxe2AybuPPHl+FqVQtthNDUdH4Vgg==} cpu: [arm64] os: [linux] + libc: [musl] '@bruits/satteri-linux-x64-gnu@0.9.3': resolution: {integrity: sha512-RgH6GPihg9Lzs2yHUsMjqiLxfLyOdmBty8sg9pBY9B4CBnvdOzvg8vklqN+C4qrEEdA9TwpbDpHr1AshLKyRpw==} cpu: [x64] os: [linux] + libc: [glibc] '@bruits/satteri-linux-x64-musl@0.9.3': resolution: {integrity: sha512-BeWhVORjNTIomePznUKiMbHZTqC0j7sMXZFsISmbX+po5d33KLkqBqKh6K332CHJ8KUmCWx16FfPjwsoysttQg==} cpu: [x64] os: [linux] + libc: [musl] '@bruits/satteri-wasm32-wasi@0.9.3': resolution: {integrity: sha512-dFNcOHKWV2cztCPnYTn7kZ9D7kNOt8N239z5ysFkNHLxJrfK7zaKIXQbfXYN32C+JoVFqAcTIOeWH2+VnsCOHg==} @@ -2592,21 +2589,25 @@ packages: resolution: {integrity: sha512-m5+8vA+1veaUUWonwva1WsU6m1HRm8CpYUzr06KDB65mewlmPbqz7+Fh7hjEfiD8C4mHVHe6RysULvAH1yhsdw==} cpu: [arm64] os: [linux] + libc: [glibc] '@ff-labs/fff-bin-linux-arm64-musl@0.9.4': resolution: {integrity: sha512-EMeWm7CSTVkizy4ZEzUkLDP024tVcbCUthduuIhekFQRDsiaAze0YboIylWb9HBHJCZlCCoZrWAl4nnJbsX7AA==} cpu: [arm64] os: [linux] + libc: [musl] '@ff-labs/fff-bin-linux-x64-gnu@0.9.4': resolution: {integrity: sha512-pglE0uLkhnlE6bStXqfgUjYTSj+2sVwXaPfoA0QksidAsQor6NRt8004mygzC9DPubgHq5B9QezPfEwigKaP9Q==} cpu: [x64] os: [linux] + libc: [glibc] '@ff-labs/fff-bin-linux-x64-musl@0.9.4': resolution: {integrity: sha512-VNKxgl8qs3aTfXViX7lqRK1aLu311h8dtBFqG4Scv+9Oi7WprybUp5L7IZ8sxKERaDAaiJMXHodXa1c90QdK8w==} cpu: [x64] os: [linux] + libc: [musl] '@ff-labs/fff-bin-win32-arm64@0.9.4': resolution: {integrity: sha512-uFEt0aNL54vQxq1ivjxRuo+thnhS4wLqa4INl4VXnXJUmwB42XXxD+gsj7vzhBLLx4cFf0aWgy/+TVDR8yjZtQ==} @@ -2693,89 +2694,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -3234,48 +3251,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-arm64-musl@0.55.0': resolution: {integrity: sha512-r8xlKJFcsRmn0H5jZrdORae6RX9jDBrZVvOoxF+bCQtampQJClv80aZEHsv+NsLsp2KCE5ql79O7DpPVzYWpXA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-ppc64-gnu@0.55.0': resolution: {integrity: sha512-GRKv/HXHcwIVld/WU61rF0g0R16hl5EJ+ScKdpjevT57lnLnagj/U2YUbXf2mT+2Pg1uCzWC+mvGicPV3CDdLQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-gnu@0.55.0': resolution: {integrity: sha512-rdv57enTiPtpSYRMKfAiEbQb0Puw5t9N7isVinDoo5qeLDScro2gznmZqSgSWbVZRzLisTeCTW8Qwgw0bOHv3A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-riscv64-musl@0.55.0': resolution: {integrity: sha512-7v1nNrlD43VY6+sYQ6efYyb3lE6QY182304PD/768ZxTjOmFd/3dQa3u/nGBUAXYdGSWOQc5N3PnS0QzUXyEIA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxfmt/binding-linux-s390x-gnu@0.55.0': resolution: {integrity: sha512-f4lJLUSPOgScjFl9LiflKCTocyNRwE25JmTMbN4XQdDjoZzEHjqf3wA3VESF1/csg7i8m7+EQLbrZyYDqe10UQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-gnu@0.55.0': resolution: {integrity: sha512-MihqiPziJNoWy4MqNSV+jVA1g+07iQDjZiR0vaCaDoPgFEiJpCMsxamktzLV07cEeQsSJ04vQaU4CzCQwIvtDA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxfmt/binding-linux-x64-musl@0.55.0': resolution: {integrity: sha512-Yqghym7KYAVjP9MmSrNZiDeerMuoejNjo0r3ox5H3GDKk8eAfl8VyJm9i+pWCLDCTnAbcTUMMN2ZKjUYXH1v3g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxfmt/binding-openharmony-arm64@0.55.0': resolution: {integrity: sha512-s5SDvVVSbyQl1V5UU3Yl12M+XLUQ3rl5SglNqgAA2K4PXUtQhyNSS00wivONPEnNo5W01rCou8WkDNyvI/RGHg==} @@ -3378,48 +3403,56 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-arm64-musl@1.70.0': resolution: {integrity: sha512-JiylyurlB0CLSedNtx1gzv3FvfWPF1h/2Y3BJszPLNt5XQFlBsH5ke0Jle3iJb3uqu5m2e7A/DwzpuCAHdiU+A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@oxlint/binding-linux-ppc64-gnu@1.70.0': resolution: {integrity: sha512-J8VPG7I3/HmgaU4u8pNU2kFx2+0U+vPLS1dXFxXOaR/2TQ0f8AC7DRz0SRGRI1bfphnX2hVYTTtLuhL4nYKL+Q==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-gnu@1.70.0': resolution: {integrity: sha512-N2+4lV2KLN+oXTIIIwmWDhwkrnvqf5oX7Hw0zPjk+RuIVgiBQSOlJWF7uQoFx2siEYX0ZQ5cfSbEAHm+J3t7Wg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-riscv64-musl@1.70.0': resolution: {integrity: sha512-1e2L7cFCvx9QDzq6NPP+0tABKb5z6nWHyddWTNKprEsjO9xNrAtPowuCGpjNXxkTdsMiZ4jc8YQ5SstZd4XK6g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [riscv64] os: [linux] + libc: [musl] '@oxlint/binding-linux-s390x-gnu@1.70.0': resolution: {integrity: sha512-Kwu/l/8GcYibCWA9m9N5pRXMIKVSsL/YbgpLzYkqDhWTiqdRfnNJ/+nqIKRKQiFbHWsdlHEhzMwruJK+qcEruA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-gnu@1.70.0': resolution: {integrity: sha512-tap04CsHYOl0nSAQJfPNIuBxqEPB2HnhQqwaOXLg1jnp2XfRo8Fa814dA4QC4zpvTWXCjAAaCY1W5LOORkEQuQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@oxlint/binding-linux-x64-musl@1.70.0': resolution: {integrity: sha512-hzJa/WgvtJpbBD9rgfy0qe+MjbxOXNUT0bfR1S6EQQzfTtBFA9xg5q8KSwRrQ2QfSS+TaP4j+4mVPQrfNc6UNg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@oxlint/binding-openharmony-arm64@1.70.0': resolution: {integrity: sha512-xbsaNSNzVSnaJACCUYr1HQMyY/Q/Q1LkePmHG3UvZPvGCYGNxrsZp9OmtA6ick8xH47ltRRbRrPCM1YXYcyC+A==} @@ -3906,108 +3939,126 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.0.1': resolution: {integrity: sha512-QoSx2EkyrrdZ6kcyE8stqZ62t0Yra8Fs5ia9lOxJrh6TMQJK7gQKmscdTHf7pOXKREKrVwOtJcQG3qVSfc866A==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-gnu@1.1.3': resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-arm64-musl@1.0.1': resolution: {integrity: sha512-uwNwFpwKeNiZawfAWBgg0VIztPTV3ihhh1vV334h9ivnNLorxnQMU6Fz8wG1Zb4Qh9LC1/MkcyT3YlDXG3Rsgg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-arm64-musl@1.1.3': resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-ppc64-gnu@1.0.1': resolution: {integrity: sha512-zY1bul7OWr7DFBiJ++wofXvnr8B45ce3QsQUhKrIhXsygAh7bTkwyeM1bi1a2g5C/yC/N8TZyGDEoMfm/l9mpg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-ppc64-gnu@1.1.3': resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.0.1': resolution: {integrity: sha512-0frlsT/f4Ft6I7SMESTKnF3cZsdicQn1dCMkF/jT9wDLE+gGoiQfv1nmT9e+s7s/fekvvy6tZM2jHvI2tkbJDQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-s390x-gnu@1.1.3': resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.0.1': resolution: {integrity: sha512-XABVmGp9Tg0WspTVvwduTc4fpqy6JnAUrSQe6OuyqD/03nI7r0O9OWUkMIwFrjKAIqolvqoA4ZrJppgwE0Gxmw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-gnu@1.1.3': resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-musl@1.0.1': resolution: {integrity: sha512-bV4fzswuzVcKD90o/VM6QqKxnxlDq0g2BISDLNVmxrnhpv1DDbyPhCIjYfvzYLV+MvkKKnQt2Q6AO86SEBULUQ==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-linux-x64-musl@1.1.3': resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} @@ -4144,66 +4195,79 @@ packages: resolution: {integrity: sha512-DxH0P3wxm+Yzs/p3zrk9dw1rURu8p0Nv5+MRK/L7OtnLNg5rLZraSBFZ8iUXOd9f2BlhJyEpIZUH/emjq4UJ4g==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.61.0': resolution: {integrity: sha512-T6ZvMNe84kAz6TBWHC7hGAoEtzP1LWYw/AqayGWEF6uISt3Abk/st06LqRD9THd7Xz3NxzurUpzAuEAUbZf+nw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.61.0': resolution: {integrity: sha512-q/4hzvQkDs8b4jIBab1pnLiiM0ayTZsN2amBFPDzuyZxjEd4wDwx0UJFYM3cOZzSf5Kw8fnWSprJzIBMkcR44Q==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.61.0': resolution: {integrity: sha512-vvYWX3akdEAY6km+9wAqFDnk6pQsbJKVnj7xawcvs/+fdlYBGp+U+Qq/lLfpIxYIZvZLHMAKD9HLdacSx/r3dw==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.61.0': resolution: {integrity: sha512-DePa5cqOxDP/Zp0VOXpeWaGew5iIv5DXp9NYbzkX5PFQyWVX9184WCTh3hvr/7lhXo8ZVlbFLkz8+o/q1dU6gA==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.61.0': resolution: {integrity: sha512-LV8aWMB8UChglMCEzs7RkN0GsH29RJaLLqwm9fCIjlqwxQTiWAqNcc7wjBkH31hV0PU/yVxGYvrYsgfea2qw6g==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.61.0': resolution: {integrity: sha512-QoNSnwQtaeNu5grdBbsL0tt1uyl5EnS8DA8Mr3nluMXbhdQNyhN+G4tBax7VCdxLKj8YJ0/4OO9Ho84jMnJtKA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.61.0': resolution: {integrity: sha512-/zZp5MKapIIApE8trN8qLGNSiRN9TUoaUZ1cmVu4XnVdd5LQLOXTtyi+vtfUbNnT3iyjzpPqYeKXmvJ+gJGYWw==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.61.0': resolution: {integrity: sha512-RbrzcD3aJ1k3UbtMRRBNwojdVVyXjuVAFTfn/xPa6EEl6GE9Sm/akPgFTb9aAC9pMKGJ6CtWxaGrqWcabH+ySg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.61.0': resolution: {integrity: sha512-ZF+onDsBso8PJf1XaG9lB+O9RnBpKGnY6OrzC4CSHrtC1jb6jWLTKK4bRqdoCXHd22gyr2hiYmEAm8Wns/BOCw==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.61.0': resolution: {integrity: sha512-Atk0aSIk5Zx2Wuh9dgRQgLP0Koc8hOeYpbWryMXyk8G8/HmPkwPPkMqIIDhrXHHYqfUzSJA/I7IWSBv8xSmRBA==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.61.0': resolution: {integrity: sha512-0uMOcf3eZ5K+K4cYHkdxShFMPlPXCOdfDFEFn9dNYAEEd2cVvmOfH7zFgRVoDgmtQ1m9k5q7qfrHzyMAubKYUA==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.61.0': resolution: {integrity: sha512-mvFtE4A/t/7hRJ7X8Ozmu8FsIkAUat2nzl12pgU337BRmq87AQUJztwHz2Zv5/tjo9/C95E66CK03SI/ToEDJw==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.61.0': resolution: {integrity: sha512-z9b9+aTxvt8n2rNltMPvyaUfB8NJ+CVyOrGK/MdIKHx7B+lXmZpm/XbRsU7Rpf3fRqJ2uS6mBJiJveCtq8LHDg==} @@ -4438,48 +4502,56 @@ packages: engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-gnu@4.3.0': resolution: {integrity: sha512-qTJHELX8jetjhRQHCLilkVLmybpzNQAtaI/gaoVoidn/ufbNDbAo8KlK2J+yPoc8wQxvDxCmh/5lr8nC1+lTbg==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.2.1': resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-arm64-musl@4.3.0': resolution: {integrity: sha512-Z6sukiQsngnWO+l39X4pPbiWT81IC+PLKF+PHxIlyZbGNb9MODfYlXEVlFvej5BOZInWX01kVyzeLvHsXhfczQ==} engines: {node: '>= 20'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.2.1': resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-gnu@4.3.0': resolution: {integrity: sha512-DRNdQRpSGzRGfARVuVkxvM8Q12nh19l4BF/G7zGA1oe+9wcC6saFBHTISrpIcKzhiXtSrlSrluCfvMuledoCTQ==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.2.1': resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-musl@4.3.0': resolution: {integrity: sha512-Z0IADbDo8bh6I7h2IQMx601AdXBLfFpEdUotft86evd/8ZPflZe9COPO8Q1vw+pfLWIUo9zN/JGZvwuAJqduqg==} engines: {node: '>= 20'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.2.1': resolution: {integrity: sha512-MGFB5cVPvshR85MTJkEvqDUnuNoysrsRxd6vnk1Lf2tbiqNlXpHYZqkqOQalydienEWOHHFyyuTSYRsLfxFJ2Q==} @@ -4952,24 +5024,28 @@ packages: engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [arm64] os: [linux] + libc: [glibc] '@voidzero-dev/vite-plus-linux-arm64-musl@0.2.1': resolution: {integrity: sha512-vUY7hYycZW0qEevpl7ImzZJFnOEKRYCaCOX4TBW0vk6MJZ+zj/xW7e0LOggzJcz2wbYAgLDqp5h+b8wV9dguDA==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [arm64] os: [linux] + libc: [musl] '@voidzero-dev/vite-plus-linux-x64-gnu@0.2.1': resolution: {integrity: sha512-tFxpToEaykBGxMQHp8M/qmr1yruRRED+c9gA1h9kmplqot04OxuqzRCWu/IiIvMJ0v3JFdOP3gqkyjXLLJhxIA==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [x64] os: [linux] + libc: [glibc] '@voidzero-dev/vite-plus-linux-x64-musl@0.2.1': resolution: {integrity: sha512-2scSS7wEbLO2758fqr1/bAULg7nLCFa5V8LO2b5w3g1CrTYdMTDt2WX1ghPesIi+70pYGydRbXo6iaaN43zfMg==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} cpu: [x64] os: [linux] + libc: [musl] '@voidzero-dev/vite-plus-win32-arm64-msvc@0.2.1': resolution: {integrity: sha512-3+5FJYhi9SqBszjngI2LBmvoiqEwxJWyQ5UsOUtNz6/d+yDrDw+tOgHLl4OKIh5aVNZeIGXzxvP6h24kcEqIyg==} @@ -5052,24 +5128,28 @@ packages: engines: {node: '>= 12'} cpu: [arm64] os: [linux] + libc: [glibc] '@yuuang/ffi-rs-linux-arm64-musl@1.3.2': resolution: {integrity: sha512-lejvOSqypPziQH5rzfkDlJ6e92qhWbDutE9ttOO6z5I2k83zoh9iZhZWhaXSU5VqgQpcshRkrbtXb9gy1ft5dA==} engines: {node: '>= 12'} cpu: [arm64] os: [linux] + libc: [musl] '@yuuang/ffi-rs-linux-x64-gnu@1.3.2': resolution: {integrity: sha512-s8VCFazaJKmgY2hgMTpWk4TtBY/zy5ovbaGgwyY0FvBD0YvyhcET4IrMsDJpHhFVTPCYfKZ1dN45clD/YiFp6g==} engines: {node: '>= 12'} cpu: [x64] os: [linux] + libc: [glibc] '@yuuang/ffi-rs-linux-x64-musl@1.3.2': resolution: {integrity: sha512-Ahr5chfKZKWUik20bEZRug+be57LZ2yYrtolyjSRoo7A4ZniBUHBZUNWm6TD6i0CJayqyxWeVk/XiaABD8bY0w==} engines: {node: '>= 12'} cpu: [x64] os: [linux] + libc: [glibc] '@yuuang/ffi-rs-win32-arm64-msvc@1.3.2': resolution: {integrity: sha512-yhpLcj0qel5VNlpzxPZfNmi7+rEX8444QHjUP6WWLxdRfqPllROu/Cp3OpkBpw3BLdxfcDhWkjWMD5QsJN0Pvg==} @@ -5160,7 +5240,7 @@ packages: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} alchemy@https://pkg.ing/alchemy/078ff00: - resolution: {tarball: https://pkg.ing/alchemy/078ff00} + resolution: {integrity: sha512-+yX8sLZZSS5EfHHiO5vKHK782VMo1MkcvMGaHeOLoLSGTFWMT+wRqU5CX+LOOPXyymuMWW9cydg8Hg3c9/u1bw==, tarball: https://pkg.ing/alchemy/078ff00} version: 2.0.0-beta.51 hasBin: true peerDependencies: @@ -7448,72 +7528,84 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.31.1: resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-gnu@1.32.0: resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.31.1: resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-arm64-musl@1.32.0: resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.31.1: resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-gnu@1.32.0: resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.31.1: resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-linux-x64-musl@1.32.0: resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} @@ -11144,10 +11236,10 @@ snapshots: '@bruits/satteri-win32-x64-msvc@0.9.3': optional: true - '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@capsizecss/unpack@4.0.1': dependencies: @@ -11251,23 +11343,23 @@ snapshots: electron-store: 8.2.0 react-dom: 19.2.6(react@19.2.6) - '@clerk/expo@3.6.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@clerk/expo@3.6.2(expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(expo-constants@56.0.18)(expo-crypto@56.0.4(expo@56.0.12))(expo-secure-store@56.0.4(expo@56.0.12))(expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: '@clerk/clerk-js': 6.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/react': 6.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3) '@clerk/shared': 4.22.0(react-dom@19.2.3(react@19.2.3))(react@19.2.3) base-64: 1.0.0 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-url-polyfill: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) tslib: 2.8.1 optionalDependencies: - expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-auth-session: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: 56.0.4(expo@56.0.12) expo-secure-store: 56.0.4(expo@56.0.12) - expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react-dom: 19.2.3(react@19.2.3) '@clerk/react@6.11.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': @@ -11850,7 +11942,7 @@ snapshots: '@expo-google-fonts/material-symbols@0.4.38': {} - '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': + '@expo/cli@56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6)': dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/config': 56.0.9(typescript@6.0.3) @@ -11860,7 +11952,7 @@ snapshots: '@expo/image-utils': 0.10.1(typescript@6.0.3) '@expo/inline-modules': 0.0.12(typescript@6.0.3) '@expo/json-file': 10.2.0 - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) '@expo/metro-file-map': 56.0.3 @@ -11885,7 +11977,7 @@ snapshots: connect: 3.7.0 debug: 4.4.3 dnssd-advertise: 1.1.4 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-server: 56.0.5 fetch-nodeshim: 0.4.10 getenv: 2.0.0 @@ -11911,8 +12003,8 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo-router: 56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' - '@expo/metro-runtime' @@ -11995,18 +12087,18 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/devtools@56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: chalk: 4.1.2 optionalDependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/dom-webview@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@expo/env@2.3.0': dependencies: @@ -12067,13 +12159,13 @@ snapshots: - supports-color - typescript - '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/log-box@56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) anser: 1.4.10 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) stacktrace-parser: 0.1.11 '@expo/metro-config@56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6)': @@ -12102,7 +12194,7 @@ snapshots: postcss: 8.5.15 resolve-from: 5.0.0 optionalDependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) transitivePeerDependencies: - bufferutil - supports-color @@ -12120,14 +12212,14 @@ snapshots: transitivePeerDependencies: - supports-color - '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@expo/metro-runtime@56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) anser: 1.4.10 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) stacktrace-parser: 0.1.11 whatwg-fetch: 3.6.20 optionalDependencies: @@ -12202,14 +12294,14 @@ snapshots: '@expo/router-server@56.0.14(@expo/metro-runtime@56.0.15)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo-server@56.0.5)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: debug: 4.4.3 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-server: 56.0.5 react: 19.2.3 optionalDependencies: - '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-router: 56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -12224,18 +12316,18 @@ snapshots: '@expo/sudo-prompt@9.3.2': {} - '@expo/ui@56.0.18(19413efe5eaad64848598eedfe3a0fd3)': + '@expo/ui@56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0)': dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: '@babel/core': 7.29.7 react-dom: 19.2.3(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@types/react' - '@types/react-dom' @@ -12498,13 +12590,13 @@ snapshots: dependencies: jsbi: 4.3.2 - '@legendapp/list@3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@legendapp/list@3.2.0(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 use-sync-external-store: 1.6.0(react@19.2.3) optionalDependencies: react-dom: 19.2.3(react@19.2.3) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@legendapp/list@3.2.0(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': dependencies: @@ -13327,15 +13419,15 @@ snapshots: prompts: 2.4.2 tinyexec: 1.2.4 - '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native-masked-view/masked-view@0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) '@react-native/assets-registry@0.85.3': {} @@ -13395,7 +13487,7 @@ snapshots: tinyglobby: 0.2.17 yargs: 17.7.2 - '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + '@react-native/community-cli-plugin@0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: '@react-native/dev-middleware': 0.85.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) debug: 4.4.3 @@ -13405,7 +13497,7 @@ snapshots: metro-core: 0.84.4 semver: 7.8.5 optionalDependencies: - '@react-native/metro-config': 0.85.3(@babel/core@7.29.7) + '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - supports-color @@ -13453,7 +13545,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@react-native/metro-config@0.85.3(@babel/core@7.29.7)': + '@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6)': dependencies: '@react-native/js-polyfills': 0.85.3 '@react-native/metro-babel-transformer': 0.85.3(@babel/core@7.29.7) @@ -13461,16 +13553,18 @@ snapshots: metro-runtime: 0.84.4 transitivePeerDependencies: - '@babel/core' + - bufferutil - supports-color + - utf-8-validate '@react-native/normalize-colors@0.85.3': {} - '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + '@react-native/virtualized-lists@0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: invariant: 2.2.4 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) optionalDependencies: '@types/react': 19.2.16 @@ -13855,15 +13949,15 @@ snapshots: dependencies: defer-to-connect: 2.0.1 - '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(a6e4bd1e9376530ea93dbb7d8a53d32d)': + '@t3tools/mobile-markdown-text@file:apps/mobile/modules/t3-markdown-text(ed3009b8f2424467288a00b38bef28fe)': dependencies: - expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) - expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-clipboard: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-haptics: 56.0.3(expo@56.0.12) - expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-nitro-markdown: 0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@t3tools/mobile-review-diff-native@file:apps/mobile/modules/t3-review-diff': {} @@ -14324,19 +14418,19 @@ snapshots: '@rolldown/plugin-babel': 0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.1.3) babel-plugin-react-compiler: 1.0.0 - '@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)))': + '@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9)': dependencies: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) - '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))) - vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9))(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9) + vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)))': + '@vitest/browser@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9)': dependencies: '@blazediff/core': 1.9.1 '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) @@ -14345,7 +14439,7 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9))(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -14978,8 +15072,8 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-widgets: 56.0.19(19413efe5eaad64848598eedfe3a0fd3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -15031,8 +15125,8 @@ snapshots: react-refresh: 0.14.2 optionalDependencies: '@babel/runtime': 7.29.7 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-widgets: 56.0.19(19413efe5eaad64848598eedfe3a0fd3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-widgets: 56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0) transitivePeerDependencies: - '@babel/core' - supports-color @@ -15929,29 +16023,29 @@ snapshots: expo-application@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + expo-asset@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-auth-session@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: expo-application: 56.0.3(expo@56.0.12) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-crypto: 56.0.4(expo@56.0.12) - expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-web-browser: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - expo - supports-color @@ -15959,119 +16053,119 @@ snapshots: expo-build-properties@56.0.19(expo@56.0.12): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) resolve-from: 5.0.0 semver: 7.8.5 - expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-camera@56.0.8(@types/emscripten@1.41.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: barcode-detector: 3.2.0(@types/emscripten@1.41.5) - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@types/emscripten' - expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-clipboard@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-constants@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/env': 2.3.0 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color expo-crypto@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-launcher: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-dev-menu-interface: 56.0.1(expo@56.0.12) expo-manifests: 56.0.4(expo@56.0.12) expo-updates-interface: 56.0.2(expo@56.0.12) transitivePeerDependencies: - react-native - expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-launcher@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: '@expo/schema-utils': 56.0.1 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-dev-menu: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) expo-manifests: 56.0.4(expo@56.0.12) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-dev-menu-interface@56.0.1(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-dev-menu@56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-dev-menu-interface: 56.0.1(expo@56.0.12) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-eas-client@56.0.1: {} - expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-file-system@56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-font@56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) fontfaceobserver: 2.3.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-glass-effect@56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-haptics@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-loader@56.0.3(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-picker@56.0.18(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-image-loader: 56.0.3(expo@56.0.12) expo-json-utils@56.0.0: {} expo-keep-awake@56.0.3(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-linking@56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - expo - supports-color expo-manifests@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-json-utils: 56.0.0 expo-modules-autolinking@56.0.16(typescript@6.0.3): @@ -16084,66 +16178,66 @@ snapshots: - supports-color - typescript - expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-modules-core@56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/expo-modules-macros-plugin': 0.2.2 - expo-modules-jsi: 56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-modules-jsi: 56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) optionalDependencies: - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-modules-jsi@56.0.10(patch_hash=9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) expo-network@56.0.5(expo@56.0.12)(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): + expo-notifications@56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3): dependencies: '@expo/image-utils': 0.10.1(typescript@6.0.3) abort-controller: 3.0.0 badgin: 1.2.3 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-application: 56.0.3(expo@56.0.12) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - supports-color - typescript - expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-paste-input@0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(9f7d882dd7941d8c79bb2c310ac5d36a): + expo-router@56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651): dependencies: - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/schema-utils': 56.0.1 - '@expo/ui': 56.0.18(19413efe5eaad64848598eedfe3a0fd3) + '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) '@radix-ui/react-slot': 1.2.4(@types/react@19.2.16)(react@19.2.3) '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@testing-library/jest-dom': 6.9.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) client-only: 0.0.1 color: 4.2.3 debug: 4.4.3 escape-string-regexp: 4.0.0 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-glass-effect: 56.0.4(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-linking: 56.0.14(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-server: 56.0.5 - expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 nanoid: 3.3.12 @@ -16151,10 +16245,10 @@ snapshots: react: 19.2.3 react-fast-compare: 3.2.2 react-is: 19.2.7 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-drawer-layout: 4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(patch_hash=41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -16162,8 +16256,8 @@ snapshots: vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) optionalDependencies: react-dom: 19.2.3(react@19.2.3) - react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - '@testing-library/dom' @@ -16175,7 +16269,7 @@ snapshots: expo-secure-store@56.0.4(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-server@56.0.5: {} @@ -16183,7 +16277,7 @@ snapshots: dependencies: '@expo/config-plugins': 56.0.8(typescript@6.0.3) '@expo/image-utils': 0.10.1(typescript@6.0.3) - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) xml2js: 0.6.0 transitivePeerDependencies: - supports-color @@ -16191,20 +16285,20 @@ snapshots: expo-structured-headers@56.0.0: {} - expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-symbols@56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo-google-fonts/material-symbols': 0.4.38 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) sf-symbols-typescript: 2.2.0 expo-updates-interface@56.0.2(expo@56.0.12): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) - expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + expo-updates@56.0.19(expo-dev-client@56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)))(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@expo/code-signing-certificates': 0.0.6 '@expo/plist': 0.7.0 @@ -16212,7 +16306,7 @@ snapshots: arg: 4.1.3 chalk: 4.1.2 debug: 4.4.3 - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) expo-eas-client: 56.0.1 expo-manifests: 56.0.4(expo@56.0.12) expo-structured-headers: 56.0.0 @@ -16222,25 +16316,25 @@ snapshots: ignore: 5.3.2 nullthrows: 1.1.1 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) resolve-from: 5.0.0 optionalDependencies: - expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-dev-client: 56.0.20(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) transitivePeerDependencies: - supports-color - expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + expo-web-browser@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-widgets@56.0.19(19413efe5eaad64848598eedfe3a0fd3): + expo-widgets@56.0.19(3cdc0dde9f93166d952f1e1bd0cb25c0): dependencies: '@expo/plist': 0.7.0 - '@expo/ui': 56.0.18(19413efe5eaad64848598eedfe3a0fd3) - expo: 56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/ui': 56.0.18(3cdc0dde9f93166d952f1e1bd0cb25c0) + expo: 56.0.12(8895228379997a2a064f9644cda56ed0) react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@babel/core' - '@types/react' @@ -16249,37 +16343,37 @@ snapshots: - react-native-reanimated - react-native-worklets - expo@56.0.12(@babel/core@7.29.7)(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-router@56.2.11)(expo-widgets@56.0.19)(react-dom@19.2.3(react@19.2.3))(react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6): + expo@56.0.12(8895228379997a2a064f9644cda56ed0): dependencies: '@babel/runtime': 7.29.7 - '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) + '@expo/cli': 56.1.16(@expo/dom-webview@56.0.5)(@expo/metro-runtime@56.0.15)(bufferutil@4.1.0)(expo-constants@56.0.18)(expo-font@56.0.7)(expo-router@56.2.11)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3)(utf-8-validate@6.0.6) '@expo/config': 56.0.9(typescript@6.0.3) '@expo/config-plugins': 56.0.9(typescript@6.0.3) - '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/devtools': 56.0.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/fingerprint': 0.19.4 '@expo/local-build-cache-provider': 56.0.8(typescript@6.0.3) - '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro': 56.0.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@expo/metro-config': 56.0.14(patch_hash=8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46)(bufferutil@4.1.0)(expo@56.0.12)(typescript@6.0.3)(utf-8-validate@6.0.6) '@ungap/structured-clone': 1.3.1 babel-preset-expo: 56.0.15(@babel/core@7.29.7)(@babel/runtime@7.29.7)(expo-widgets@56.0.19)(expo@56.0.12)(react-refresh@0.14.2) - expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) - expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-file-system: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) - expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-asset: 56.0.17(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(typescript@6.0.3) + expo-constants: 56.0.18(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-file-system: 56.0.8(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)) + expo-font: 56.0.7(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-keep-awake: 56.0.3(expo@56.0.12)(react@19.2.3) expo-modules-autolinking: 56.0.16(typescript@6.0.3) - expo-modules-core: 56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + expo-modules-core: 56.0.17(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) pretty-format: 29.7.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-refresh: 0.14.2 whatwg-url-minimum: 0.1.2 optionalDependencies: - '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/dom-webview': 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-dom: 19.2.3(react@19.2.3) - react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-webview: 13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) transitivePeerDependencies: - '@babel/core' - bufferutil @@ -18700,102 +18794,102 @@ snapshots: transitivePeerDependencies: - supports-color - react-native-drawer-layout@4.2.4(react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-drawer-layout@4.2.4(0e9729601f58a7a7ae26c76fe6017455): dependencies: color: 4.2.3 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) - react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@egjs/hammerjs': 2.0.17 '@types/react-test-renderer': 19.1.0 hoist-non-react-statics: 3.3.2 invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-image-viewing@0.2.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-is-edge-to-edge@1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-keyboard-controller@1.21.6(react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-nitro-markdown@0.5.8(react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-nitro-modules: 0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) optionalDependencies: - react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-svg: 15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-nitro-modules@0.35.9(patch_hash=825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-reanimated@4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-is-edge-to-edge: 1.3.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-worklets: 0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) semver: 7.8.5 - react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-safe-area-context@5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(patch_hash=41409f74fee097b94000313019b188603b99d9ce5666cba9c84de38fc216cd4d)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-shiki-engine@0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@shikijs/types': 4.3.0 '@shikijs/vscode-textmate': 10.0.2 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-svg@15.15.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: css-select: 5.2.2 css-tree: 1.1.3 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) warn-once: 0.1.1 - react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): + react-native-url-polyfill@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6)): dependencies: - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) whatwg-url-without-unicode: 8.0.0-3 - react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-webview@13.16.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: escape-string-regexp: 4.0.0 invariant: 2.2.4 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: '@babel/core': 7.29.7 '@babel/plugin-transform-arrow-functions': 7.29.7(@babel/core@7.29.7) @@ -18807,23 +18901,23 @@ snapshots: '@babel/plugin-transform-template-literals': 7.29.7(@babel/core@7.29.7) '@babel/plugin-transform-unicode-regex': 7.29.7(@babel/core@7.29.7) '@babel/preset-typescript': 7.29.7(@babel/core@7.29.7) - '@react-native/metro-config': 0.85.3(@babel/core@7.29.7) + '@react-native/metro-config': 0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6) convert-source-map: 2.0.0 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) semver: 7.8.5 transitivePeerDependencies: - supports-color - react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6): + react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6): dependencies: '@react-native/assets-registry': 0.85.3 '@react-native/codegen': 0.85.3(@babel/core@7.29.7) - '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@react-native/community-cli-plugin': 0.85.3(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bufferutil@4.1.0)(utf-8-validate@6.0.6) '@react-native/gradle-plugin': 0.85.3 '@react-native/js-polyfills': 0.85.3 '@react-native/normalize-colors': 0.85.3 - '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@react-native/virtualized-lists': 0.85.3(@types/react@19.2.16)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) abort-controller: 3.0.0 anser: 1.4.10 ansi-regex: 5.0.1 @@ -19874,14 +19968,14 @@ snapshots: universalify@2.0.1: {} - uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): + uniwind@1.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)(tailwindcss@4.3.0): dependencies: '@tailwindcss/node': 4.2.1 '@tailwindcss/oxide': 4.2.1 culori: 4.0.2 lightningcss: 1.30.1 react: 19.2.3 - react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) tailwindcss: 4.3.0 unpipe@1.0.0: {} @@ -20005,8 +20099,8 @@ snapshots: dependencies: '@oxc-project/types': 0.136.0 '@oxlint/plugins': 1.68.0 - '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))) - '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))) + '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9) + '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9) '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) '@vitest/pretty-format': 4.1.9 @@ -20019,7 +20113,7 @@ snapshots: oxlint: 1.70.0(oxlint-tsgolint@0.23.0)(vite-plus@0.2.1(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) oxlint-tsgolint: 0.23.0 vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9))(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) optionalDependencies: '@voidzero-dev/vite-plus-darwin-arm64': 0.2.1 '@voidzero-dev/vite-plus-darwin-x64': 0.2.1 @@ -20065,7 +20159,7 @@ snapshots: optionalDependencies: vite: '@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9))(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)): + vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) @@ -20089,7 +20183,7 @@ snapshots: why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.12.4 - '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))) + '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.1(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9) transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a85da0b620c..b566cc03b93 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -5,6 +5,20 @@ packages: - packages/* - scripts +allowBuilds: + browser-tabs-lock: set this to true or false + bufferutil: set this to true or false + core-js: set this to true or false + electron: set this to true or false + electron-winstaller: set this to true or false + esbuild: set this to true or false + msgpackr-extract: set this to true or false + msw: set this to true or false + node-pty: set this to true or false + sharp: set this to true or false + utf-8-validate: set this to true or false + workerd: set this to true or false + catalog: "@clerk/backend": 3.8.4 "@clerk/clerk-js": 6.22.0 @@ -96,6 +110,7 @@ patchedDependencies: "@pierre/diffs@1.3.0-beta.5": patches/@pierre%2Fdiffs@1.3.0-beta.5.patch effect@4.0.0-beta.78: patches/effect@4.0.0-beta.78.patch expo-modules-jsi@56.0.10: patches/expo-modules-jsi@56.0.10.patch + expo-router@56.2.11: patches/expo-router@56.2.11.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch react-native-screens@4.25.2: patches/react-native-screens@4.25.2.patch From 618506f135f7448ff57ba4063c7d68560fc53468 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 14:16:34 -0700 Subject: [PATCH 23/71] Group native iOS header controls --- apps/mobile/src/app/debug/rns-glass.tsx | 25 +- apps/mobile/src/features/home/HomeHeader.tsx | 7 +- apps/mobile/src/features/home/HomeScreen.tsx | 6 +- .../threads/ThreadNavigationSidebar.tsx | 7 +- .../features/threads/ThreadRouteScreen.tsx | 9 +- apps/mobile/src/lib/ios-native-chrome.ts | 10 - patches/expo-router@56.2.11.patch | 27 +-- patches/react-native-screens@4.25.2.patch | 217 ++++++++++++++---- pnpm-lock.yaml | 18 +- 9 files changed, 208 insertions(+), 118 deletions(-) delete mode 100644 apps/mobile/src/lib/ios-native-chrome.ts diff --git a/apps/mobile/src/app/debug/rns-glass.tsx b/apps/mobile/src/app/debug/rns-glass.tsx index c0079c83da6..3f16dc1e01a 100644 --- a/apps/mobile/src/app/debug/rns-glass.tsx +++ b/apps/mobile/src/app/debug/rns-glass.tsx @@ -238,8 +238,7 @@ function SidebarColumn(props: { const { colors } = props; const insets = useSafeAreaInsets(); const compactTopInset = 18; - const headerButtonTint = - colors.background === "#050507" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; + const headerButtonTint = colors.foreground; if (props.compact) { return ( @@ -435,8 +434,7 @@ function MailWideSidebarNativePane(props: { const { colors } = props; const insets = useSafeAreaInsets(); const headerInset = insets.top + 76; - const headerButtonTint = - colors.background === "#050507" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; + const headerButtonTint = colors.foreground; return ( @@ -490,9 +488,9 @@ function MailWideSidebarNativePane(props: { variant: "prominent", }, { - accessibilityLabel: "More sidebar options", - icon: { name: "ellipsis", type: "sfSymbol" }, - identifier: "rns-glass-ipad-more", + accessibilityLabel: "Open settings", + icon: { name: "gearshape", type: "sfSymbol" }, + identifier: "rns-glass-ipad-settings", onPress: () => {}, sharesBackground: true, tintColor: headerButtonTint, @@ -525,8 +523,7 @@ function MailWideDetailNativePane(props: { const { colors } = props; const insets = useSafeAreaInsets(); const headerInset = insets.top + 88; - const headerButtonTint = - colors.background === "#050507" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; + const headerButtonTint = colors.foreground; return ( @@ -648,14 +645,6 @@ function MailWideDetailNativePane(props: { }, ] as ComponentProps["headerLeftBarButtonItems"] } - headerRightBarButtonItems={ - [ - { - activatesSearchController: true, - type: "searchBarPlacement", - }, - ] as ComponentProps["headerRightBarButtonItems"] - } hideBackButton hideShadow={false} largeTitle={false} @@ -678,7 +667,7 @@ function MailWideDetailNativePane(props: { onChangeText={(event: NativeSyntheticEvent<{ readonly text?: string }>) => { props.onSearchQueryChange(event.nativeEvent.text ?? ""); }} - placement="automatic" + placement="integratedButton" placeholder="Search" textColor={colors.foreground} tintColor={colors.foreground} diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 8c565522d5b..6796a499cdb 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -5,11 +5,10 @@ import type { } from "@t3tools/contracts"; import { Stack } from "expo-router"; import { useCallback, useRef } from "react"; -import { Platform, useColorScheme } from "react-native"; +import { Platform } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; -import { iosNativeGlassButtonTint } from "../../lib/ios-native-chrome"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { @@ -40,9 +39,7 @@ export function HomeHeader(props: { readonly onStartNewTask: () => void; }) { const searchBarRef = useRef(null); - const colorScheme = useColorScheme(); const iconColor = useThemeColor("--color-icon"); - const headerButtonTint = iosNativeGlassButtonTint(colorScheme); const hasCustomListOptions = hasCustomHomeListOptions(props); const focusSearch = useCallback(() => { searchBarRef.current?.focus(); @@ -77,7 +74,7 @@ export function HomeHeader(props: { label: "", onPress: props.onOpenSettings, sharesBackground: true, - tintColor: headerButtonTint, + tintColor: iconColor, type: "button", variant: "prominent", width: 58, diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 95f8186dbe9..1681cba4bfe 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -14,7 +14,6 @@ import { Platform, Pressable, ScrollView, - useColorScheme, useWindowDimensions, View, } from "react-native"; @@ -30,7 +29,6 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; -import { iosNativeGlassButtonTint } from "../../lib/ios-native-chrome"; import { AppText as Text } from "../../components/AppText"; import { EmptyState } from "../../components/EmptyState"; @@ -383,8 +381,6 @@ export function HomeScreen(props: HomeScreenProps) { const accentColor = useThemeColor("--color-icon-muted"); const foregroundColor = useThemeColor("--color-foreground"); const screenColor = useThemeColor("--color-screen"); - const colorScheme = useColorScheme(); - const nativeHeaderButtonTint = iosNativeGlassButtonTint(colorScheme); const hasCustomListOptions = hasCustomHomeListOptions(props); const filterMenu = buildHomeListFilterMenu({ environments: props.environments, @@ -607,7 +603,7 @@ export function HomeScreen(props: HomeScreenProps) { identifier: "home-settings", onPress: props.onOpenSettings, sharesBackground: true, - tintColor: nativeHeaderButtonTint, + tintColor: foregroundColor, type: "button", variant: "prominent", width: 58, diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 28b67c2a4c9..b1cf0a80423 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -16,7 +16,6 @@ import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; -import { iosNativeGlassButtonTint } from "../../lib/ios-native-chrome"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; import { useWorkspaceState } from "../../state/workspace"; @@ -487,8 +486,6 @@ export function ThreadNavigationSidebar(props: { const filterIcon = hasCustomHomeListOptions(options) ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle"; - const nativeHeaderButtonTint = iosNativeGlassButtonTint(colorScheme); - if (usesNativeSidebarChrome) { const { Screen, ScreenStack, ScreenStackHeaderConfig } = require("react-native-screens") as typeof import("react-native-screens"); @@ -559,7 +556,7 @@ export function ThreadNavigationSidebar(props: { ], }, sharesBackground: true, - tintColor: nativeHeaderButtonTint, + tintColor: foregroundColor, type: "menu", variant: "prominent", width: 58, @@ -570,7 +567,7 @@ export function ThreadNavigationSidebar(props: { identifier: "thread-sidebar-settings", onPress: props.onOpenSettings, sharesBackground: true, - tintColor: nativeHeaderButtonTint, + tintColor: foregroundColor, type: "button", variant: "prominent", width: 58, diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 4ddf3567b73..57155cbba5d 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -643,16 +643,9 @@ function ThreadRouteContent( allowToolbarIntegration: true, hideNavigationBar: false, placeholder: "Search", + placement: "integratedButton", } : undefined, - unstable_headerRightItems: usesThreadSearchToolbar - ? () => [ - { - activatesSearchController: true, - type: "searchBarPlacement", - }, - ] - : undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> diff --git a/apps/mobile/src/lib/ios-native-chrome.ts b/apps/mobile/src/lib/ios-native-chrome.ts deleted file mode 100644 index e45b81e21fa..00000000000 --- a/apps/mobile/src/lib/ios-native-chrome.ts +++ /dev/null @@ -1,10 +0,0 @@ -export type NativeChromeColorScheme = "dark" | "light" | "unspecified" | null | undefined; - -/** - * Tint used for iOS 27 glass header buttons. UIKit still owns the actual - * material; this color only nudges the sampled glass toward Mail/Messages' - * button tone without baking custom blur into React views. - */ -export function iosNativeGlassButtonTint(colorScheme: NativeChromeColorScheme): string { - return colorScheme === "dark" ? "rgba(62,62,66,0.88)" : "rgba(255,255,255,0.82)"; -} diff --git a/patches/expo-router@56.2.11.patch b/patches/expo-router@56.2.11.patch index 8ec3076d74e..78a4dace79c 100644 --- a/patches/expo-router@56.2.11.patch +++ b/patches/expo-router@56.2.11.patch @@ -1,8 +1,5 @@ -diff --git a/android/src/main/java/expo/modules/router/ExpoRouterModule.kt b/android/src/main/java/expo/modules/router/ExpoRouterModule.kt -deleted file mode 100644 -index 369cd65886415cb894b858e38604dc59b5c6ba95..0000000000000000000000000000000000000000 diff --git a/build/react-navigation/native-stack/types.d.ts b/build/react-navigation/native-stack/types.d.ts -index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..ed5a88b308e022b2ef49456dfb21c3a6a8aeea4f 100644 +index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..93845c781609c1a46bf71f2c30ef81432fb56bc2 100644 --- a/build/react-navigation/native-stack/types.d.ts +++ b/build/react-navigation/native-stack/types.d.ts @@ -306,6 +306,22 @@ export type NativeStackNavigationOptions = { @@ -28,22 +25,16 @@ index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..ed5a88b308e022b2ef49456dfb21c3a6 /** * String or a function that returns a React Element to be used by the header. * Defaults to screen `title` or route name. -@@ -1129,13 +1145,14 @@ export type NativeStackHeaderItemCustom = { - */ - hidesSharedBackground?: boolean; - }; -+export type NativeStackHeaderRawRNSItem = NonNullable[number] | NonNullable[number]; - /** - * An item that can be displayed in the header. - * It can be a button, a menu, spacing, or a custom element. - * - * On iOS 26, when showing items on the right side of the header, +@@ -1115,7 +1131,8 @@ export type NativeStackHeaderItemCustom = { * if the items don't fit the available space, they will be collapsed into a menu automatically. * Items with `type: 'custom'` will not be included in this automatic collapsing behavior. */ -export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom; ++export type NativeStackHeaderRawRNSItem = NonNullable[number] | NonNullable[number]; +export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom | NativeStackHeaderRawRNSItem; export type NativeStackNavigatorProps = DefaultNavigatorOptions, NativeStackNavigationOptions, NativeStackNavigationEventMap, NativeStackNavigationProp> & StackRouterOptions & NativeStackNavigationConfig; + export type NativeStackDescriptor = Descriptor, RouteProp>; + export type NativeStackDescriptorMap = { diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts index 218c4291fdb57600a45d32fbcefe3707a520fe79..ce3bbadec2d52949d133401f1df01b95899aa5bf 100644 --- a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts @@ -57,10 +48,10 @@ index 218c4291fdb57600a45d32fbcefe3707a520fe79..ce3bbadec2d52949d133401f1df01b95 export {}; //# sourceMappingURL=useHeaderConfigProps.d.ts.map diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.js b/build/react-navigation/native-stack/views/useHeaderConfigProps.js -index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..6fb1840deedd52e1d788c6f93982da26d4a58efc 100644 +index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..ed9651d5bcce56594c6a1946b2cb6a8f11f80c7c 100644 --- a/build/react-navigation/native-stack/views/useHeaderConfigProps.js +++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.js -@@ -18,6 +18,9 @@ const processBarButtonItems = (items, colors, fonts) => { +@@ -22,6 +22,9 @@ const processBarButtonItems = (items, colors, fonts) => { } return item; } @@ -70,7 +61,7 @@ index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..6fb1840deedd52e1d788c6f93982da26 if (item.type === 'button' || item.type === 'menu') { if (item.type === 'menu' && item.menu == null) { throw new Error(`Menu item must have a 'menu' property defined: ${JSON.stringify(item)}`); -@@ -108,7 +108,7 @@ const getMenuItem = (item) => { +@@ -108,7 +111,7 @@ const getMenuItem = (item) => { subtitle: description, }; }; @@ -79,7 +70,7 @@ index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..6fb1840deedd52e1d788c6f93982da26 const { direction } = (0, native_1.useLocale)(); const { colors, fonts, dark } = (0, native_1.useTheme)(); const tintColor = headerTintColor ?? (react_native_1.Platform.OS === 'ios' ? colors.primary : colors.text); -@@ -270,6 +270,8 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac +@@ -270,6 +273,8 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac children, headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts), headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index 678d2264888..fb1b9556223 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -86,7 +86,7 @@ index 919b984edc9f91ee9ac26faf257d8a721e26457c..3f54fad0a22d37bb7542679ae9d14088 NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm -index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab660844998e 100644 +index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be58629a26 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -30,6 +30,20 @@ namespace react = facebook::react; @@ -110,7 +110,17 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 @interface RCTImageLoader (Private) - (id)imageCache; @end -@@ -81,6 +95,7 @@ static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +@@ -47,6 +61,9 @@ static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; + @end + + @interface RNSScreenStackHeaderConfig () ++#if !TARGET_OS_TV ++- (NSArray *)barButtonItemGroupsFromItems:(NSArray *)items; ++#endif + @end + + @implementation RNSScreenStackHeaderConfig { +@@ -81,6 +98,7 @@ static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; self.hidden = YES; _reactSubviews = [NSMutableArray new]; _backTitleVisible = YES; @@ -118,7 +128,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 _blurEffect = RNSBlurEffectStyleNone; } -@@ -496,6 +511,10 @@ RNS_IGNORE_SUPER_CALL_END +@@ -496,6 +514,10 @@ RNS_IGNORE_SUPER_CALL_END if (shouldHide) { navitem.title = config.title; @@ -129,7 +139,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. [navctr setNavigationBarHidden:YES animated:animated]; -@@ -512,11 +531,19 @@ RNS_IGNORE_SUPER_CALL_END +@@ -512,11 +534,19 @@ RNS_IGNORE_SUPER_CALL_END } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; @@ -149,17 +159,34 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV -@@ -638,9 +665,196 @@ RNS_IGNORE_SUPER_CALL_END +@@ -637,10 +667,229 @@ RNS_IGNORE_SUPER_CALL_END + // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title; - navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems +- navitem.leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems - withCurrentItems:navitem.leftBarButtonItems]; -+ withCurrentItems:navitem.leftBarButtonItems -+ navigationItem:navitem]; - navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems +- navitem.rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems - withCurrentItems:navitem.rightBarButtonItems]; -+ withCurrentItems:navitem.rightBarButtonItems -+ navigationItem:navitem]; ++ NSArray *leftBarButtonItems = [config barButtonItemsFromConfigs:config.headerLeftBarButtonItems ++ withCurrentItems:navitem.leftBarButtonItems ++ navigationItem:navitem]; ++ NSArray *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems ++ withCurrentItems:navitem.rightBarButtonItems ++ navigationItem:navitem]; ++#if !TARGET_OS_TV ++ if (@available(iOS 16.0, *)) { ++ navitem.leadingItemGroups = [config barButtonItemGroupsFromItems:leftBarButtonItems]; ++ navitem.trailingItemGroups = [config barButtonItemGroupsFromItems:rightBarButtonItems]; ++ navitem.leftBarButtonItems = nil; ++ navitem.rightBarButtonItems = nil; ++ } else { ++ navitem.leftBarButtonItems = leftBarButtonItems; ++ navitem.rightBarButtonItems = rightBarButtonItems; ++ } ++#else ++ navitem.leftBarButtonItems = leftBarButtonItems; ++ navitem.rightBarButtonItems = rightBarButtonItems; ++#endif + NSDictionary *mailSearchToolbarConfig = nil; + for (NSDictionary *toolbarConfig in config.headerToolbarItems) { + if (toolbarConfig[@"mailSearchToolbar"]) { @@ -366,7 +393,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items // (setting nav bar visibility should be done after `navitem.*BarButtonItems`). -@@ -773,6 +987,7 @@ RNS_IGNORE_SUPER_CALL_END +@@ -773,6 +1022,7 @@ RNS_IGNORE_SUPER_CALL_END - (NSArray *)barButtonItemsFromConfigs:(NSArray *> *)dicts withCurrentItems:(NSArray *)currentItems @@ -374,7 +401,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 { if (dicts.count == 0) { return currentItems; -@@ -781,7 +996,161 @@ RNS_IGNORE_SUPER_CALL_END +@@ -781,7 +1031,190 @@ RNS_IGNORE_SUPER_CALL_END [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; @@ -543,6 +570,18 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 + } + } + NSNumber *index = dict[@"index"]; ++#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ if (@available(iOS 26.0, *)) { ++ NSNumber *sharesBackground = dict[@"sharesBackground"]; ++ if (sharesBackground != nil) { ++ item.sharesBackground = sharesBackground.boolValue; ++ } ++ NSNumber *hidesSharedBackground = dict[@"hidesSharedBackground"]; ++ if (hidesSharedBackground != nil) { ++ item.hidesSharedBackground = hidesSharedBackground.boolValue; ++ } ++ } ++#endif + if (index != nil && index.integerValue < items.count) { + [items insertObject:item atIndex:index.integerValue]; + } else { @@ -554,7 +593,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict action:^(NSString *buttonId) { auto eventEmitter = std::static_pointer_cast( -@@ -809,11 +1178,15 @@ RNS_IGNORE_SUPER_CALL_END +@@ -809,11 +1242,15 @@ RNS_IGNORE_SUPER_CALL_END [items addObject:item]; } } else if (dict[@"spacing"]) { @@ -574,7 +613,55 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 NSNumber *index = dict[@"index"]; if (index.integerValue < items.count) { [items insertObject:item atIndex:index.integerValue]; -@@ -1013,6 +1386,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -825,6 +1262,47 @@ RNS_IGNORE_SUPER_CALL_END + return items; + } + ++#if !TARGET_OS_TV ++- (NSArray *)barButtonItemGroupsFromItems:(NSArray *)items ++{ ++ if (items.count == 0) { ++ return @[]; ++ } ++ ++ NSMutableArray *groups = [NSMutableArray array]; ++ NSMutableArray *sharedRun = [NSMutableArray array]; ++ ++ void (^flushSharedRun)(void) = ^{ ++ if (sharedRun.count == 0) { ++ return; ++ } ++ [groups addObject:[UIBarButtonItemGroup fixedGroupWithRepresentativeItem:nil ++ items:[sharedRun copy]]]; ++ [sharedRun removeAllObjects]; ++ }; ++ ++ for (UIBarButtonItem *item in items) { ++ BOOL shouldShareBackground = NO; ++#if RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ if (@available(iOS 26.0, *)) { ++ shouldShareBackground = item.sharesBackground && !item.hidesSharedBackground; ++ } ++#endif ++ ++ if (shouldShareBackground) { ++ [sharedRun addObject:item]; ++ continue; ++ } ++ ++ flushSharedRun(); ++ [groups addObject:[UIBarButtonItemGroup fixedGroupWithRepresentativeItem:nil items:@[ item ]]]; ++ } ++ ++ flushSharedRun(); ++ return groups; ++} ++#endif ++ + RNS_IGNORE_SUPER_CALL_BEGIN + - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex + { +@@ -1013,6 +1491,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); @@ -583,7 +670,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); } -@@ -1038,6 +1413,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1038,6 +1518,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; _backButtonDisplayMode = [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; @@ -591,7 +678,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..1e2517d0468f58147b199e3cc898ab66 if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; -@@ -1084,6 +1460,18 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1084,6 +1565,18 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) _headerRightBarButtonItems = array; } @@ -683,10 +770,10 @@ index 52c61c863c53c289569a741445ed08c0355d95ba..cb4d6be752f98e06dfd3b40453c2bd88 #pragma mark - Override diff --git a/lib/commonjs/components/ScreenStackHeaderConfig.js b/lib/commonjs/components/ScreenStackHeaderConfig.js -index 16b979bb3dfb41ff247403f1632c300a9a60d549..4c1b5a7491fb6555cdacb9fa5ddb473d7e9c4556 100644 +index 16b979bb3dfb41ff247403f1632c300a9a60d549..67f52f105966833884344553847f11d0b66e6a4e 100644 --- a/lib/commonjs/components/ScreenStackHeaderConfig.js +++ b/lib/commonjs/components/ScreenStackHeaderConfig.js -@@ -23,18 +23,35 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -23,18 +23,40 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ } = (0, _TopInsetApplicationContext.useTopInsetApplication)(!props.hidden, props.disableTopInsetApplication ?? false); const { headerLeftBarButtonItems, @@ -730,7 +817,7 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..4c1b5a7491fb6555cdacb9fa5ddb473d } : undefined; // Handle bar button menu item presses by deep-searching nested menus -@@ -56,7 +73,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -56,7 +78,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ }; // Check each bar-button item with a menu @@ -739,7 +826,7 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..4c1b5a7491fb6555cdacb9fa5ddb473d for (const item of allItems) { if (item && item.type === 'menu' && item.menu) { const action = findInMenu(item.menu, event.nativeEvent.menuId); -@@ -64,6 +81,15 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -64,6 +86,15 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ action.onPress(); return; } @@ -755,7 +842,7 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..4c1b5a7491fb6555cdacb9fa5ddb473d } } } : undefined; -@@ -71,6 +97,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -71,6 +102,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ userInterfaceStyle: props.experimental_userInterfaceStyle, headerLeftBarButtonItems: preparedHeaderLeftBarButtonItems, headerRightBarButtonItems: preparedHeaderRightBarButtonItems, @@ -764,7 +851,7 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..4c1b5a7491fb6555cdacb9fa5ddb473d onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, ref: ref, diff --git a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js -index ab93f62e4a7049d63c6681cecbd6cf8b1d07ce10..3a7196894cae4e13ec3357e178f181578407dd2a 100644 +index ab93f62e4a7049d63c6681cecbd6cf8b1d07ce10..b5ea122473b023f84eabdb1914aef09a28c6d525 100644 --- a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js +++ b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js @@ -41,10 +41,31 @@ const prepareMenu = (menu, index, side, path = '') => { @@ -831,10 +918,10 @@ index de25040bfec8587ca311f5a42661240640989419..bf73f41744e08bc60ffac09029419d19 function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } diff --git a/lib/module/components/ScreenStackHeaderConfig.js b/lib/module/components/ScreenStackHeaderConfig.js -index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..5b83738e58d03192452b03d06453292ece929b41 100644 +index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..b1dfe857e0cf06c75088203a7f4157643af479b5 100644 --- a/lib/module/components/ScreenStackHeaderConfig.js +++ b/lib/module/components/ScreenStackHeaderConfig.js -@@ -19,18 +19,35 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -19,18 +19,40 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref } = useTopInsetApplication(!props.hidden, props.disableTopInsetApplication ?? false); const { headerLeftBarButtonItems, @@ -878,7 +965,7 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..5b83738e58d03192452b03d06453292e } : undefined; // Handle bar button menu item presses by deep-searching nested menus -@@ -52,7 +69,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -52,7 +74,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref }; // Check each bar-button item with a menu @@ -887,7 +974,7 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..5b83738e58d03192452b03d06453292e for (const item of allItems) { if (item && item.type === 'menu' && item.menu) { const action = findInMenu(item.menu, event.nativeEvent.menuId); -@@ -60,6 +77,15 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -60,6 +82,15 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref action.onPress(); return; } @@ -903,7 +990,7 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..5b83738e58d03192452b03d06453292e } } } : undefined; -@@ -67,6 +93,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -67,6 +98,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref userInterfaceStyle: props.experimental_userInterfaceStyle, headerLeftBarButtonItems: preparedHeaderLeftBarButtonItems, headerRightBarButtonItems: preparedHeaderRightBarButtonItems, @@ -912,7 +999,7 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..5b83738e58d03192452b03d06453292e onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, ref: ref, diff --git a/lib/module/components/helpers/prepareHeaderBarButtonItems.js b/lib/module/components/helpers/prepareHeaderBarButtonItems.js -index 8a70ffd78617147418d628c03c580bb7ff9a8a72..749ca41df55394c264601c56dad2a59970ac0e4d 100644 +index 8a70ffd78617147418d628c03c580bb7ff9a8a72..8dbd0ba2ee61ee54d44cdf286a720ce26af01f26 100644 --- a/lib/module/components/helpers/prepareHeaderBarButtonItems.js +++ b/lib/module/components/helpers/prepareHeaderBarButtonItems.js @@ -35,10 +35,31 @@ const prepareMenu = (menu, index, side, path = '') => { @@ -949,7 +1036,7 @@ index 8a70ffd78617147418d628c03c580bb7ff9a8a72..749ca41df55394c264601c56dad2a599 if (item.icon?.type === 'imageSource') { imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/module/index.js b/lib/module/index.js -index 07caaf7f04c1d6484727b43b694ccf76b1988984..ba2e9c5e0c788a64db13733f403c9087567ac6a0 100644 +index 07caaf7f04c1d6484727b43b694ccf76b1988984..07fd7a57687797c5ae8f685ea70ccf983e262f63 100644 --- a/lib/module/index.js +++ b/lib/module/index.js @@ -43,4 +43,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; @@ -958,8 +1045,9 @@ index 07caaf7f04c1d6484727b43b694ccf76b1988984..ba2e9c5e0c788a64db13733f403c9087 export * from './components/tabs'; +export * from './components/gamma/scroll-view-marker'; //# sourceMappingURL=index.js.map +\ No newline at end of file diff --git a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts -index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..ec0e5ec08f0240155beaa3ada54bcda9ddf83415 100644 +index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..45f16ef96ee86402c68a1b90edb4a71de05773a9 100644 --- a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +++ b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts @@ -9,6 +9,7 @@ type OnPressHeaderBarButtonMenuItemEvent = Readonly<{ @@ -1006,7 +1094,7 @@ index 8e4480f901d5d557244643a32341d46e9a497e68..883c5f264301058cbcd9090bee2702ee //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts -index 3b384e03891e38e936f370372a682d73440e7ec2..9f643bfd4f99fbb36d2a8214fdbf8d207b4cb9ec 100644 +index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac4e6af1d8 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -10,6 +10,7 @@ export type SearchBarCommands = { @@ -1070,7 +1158,7 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9f643bfd4f99fbb36d2a8214fdbf8d20 /** * Allows for setting text color of the title. */ -@@ -1145,8 +1178,33 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { +@@ -1145,8 +1178,37 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { export interface HeaderBarButtonItemSpacing { type: 'spacing'; spacing: number; @@ -1109,8 +1197,57 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9f643bfd4f99fbb36d2a8214fdbf8d20 /** * Custom Screen Transition */ +diff --git a/node_modules/.bin/react-native b/node_modules/.bin/react-native +new file mode 100755 +index 0000000000000000000000000000000000000000..bdc1e9183f552f93e05d5d5501116bc9e3539219 +--- /dev/null ++++ b/node_modules/.bin/react-native +@@ -0,0 +1,43 @@ ++#!/bin/sh ++basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") ++basedir_win="$basedir" ++exe="" ++msys="" ++ ++case `uname -a` in ++ *CYGWIN*|*MINGW*|*MSYS*) ++ if command -v cygpath > /dev/null 2>&1; then ++ basedir_win=`cygpath -w "$basedir"` ++ fi ++ exe=".exe" ++ msys="true" ++ ;; ++ *WSL2*) ++ if command -v wslpath > /dev/null 2>&1; then ++ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" ++ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then ++ basedir_win="$basedir" ++ else ++ exe=".exe" ++ fi ++ fi ++ ;; ++esac ++ ++if [ -z "$NODE_PATH" ]; then ++ export NODE_PATH="/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/node_modules" ++else ++ export NODE_PATH="/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/node_modules:$NODE_PATH" ++fi ++if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then ++ exec "$basedir/node.exe" "$basedir_win/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" ++elif [ -x "$basedir/node" ]; then ++ exec "$basedir/node" "$basedir/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" ++elif command -v node >/dev/null 2>&1; then ++ exec node "$basedir/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" ++elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then ++ exec node.exe "$basedir_win/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" ++else ++ exec node "$basedir/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" ++fi ++# cmd-shim-target=/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js diff --git a/src/components/ScreenStackHeaderConfig.tsx b/src/components/ScreenStackHeaderConfig.tsx -index 421b3c2545426ae957271bd6515bcf827437541c..7adcf68a0f53f4b49bfb6ad77d6090c110862aea 100644 +index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461f5d4da86 100644 --- a/src/components/ScreenStackHeaderConfig.tsx +++ b/src/components/ScreenStackHeaderConfig.tsx @@ -39,7 +39,11 @@ export const ScreenStackHeaderConfig = React.forwardRef< @@ -1161,7 +1298,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..7adcf68a0f53f4b49bfb6ad77d6090c1 ); if ( pressedItem && -@@ -73,6 +85,19 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -73,6 +85,31 @@ export const ScreenStackHeaderConfig = React.forwardRef< ) { pressedItem.onPress(); } @@ -1193,7 +1330,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..7adcf68a0f53f4b49bfb6ad77d6090c1 } : undefined; -@@ -102,6 +127,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -102,6 +139,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< const allItems = [ ...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), @@ -1201,7 +1338,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..7adcf68a0f53f4b49bfb6ad77d6090c1 ]; for (const item of allItems) { if (item && item.type === 'menu' && item.menu) { -@@ -110,6 +136,17 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -110,6 +148,17 @@ export const ScreenStackHeaderConfig = React.forwardRef< action.onPress(); return; } @@ -1219,7 +1356,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..7adcf68a0f53f4b49bfb6ad77d6090c1 } } } -@@ -121,6 +158,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -121,6 +170,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< userInterfaceStyle={props.experimental_userInterfaceStyle} headerLeftBarButtonItems={preparedHeaderLeftBarButtonItems} headerRightBarButtonItems={preparedHeaderRightBarButtonItems} @@ -1324,7 +1461,7 @@ index a405aec437bd879d8be7a706d29e965d77658899..cd649bd7ab7de7b7131302e8be682ec6 +export * from './components/gamma/scroll-view-marker'; export type * from './components/shared/types'; diff --git a/src/types.tsx b/src/types.tsx -index 76a83f3acb6fd3f0af7f027798848b7124100286..7d6a81fecd759c393a2ba9bf797ea55e52721f32 100644 +index 76a83f3acb6fd3f0af7f027798848b7124100286..5d0e317fa26844989e8a80c4ffc4d703c22abd8a 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -26,6 +26,7 @@ export type SearchBarCommands = { @@ -1388,7 +1525,7 @@ index 76a83f3acb6fd3f0af7f027798848b7124100286..7d6a81fecd759c393a2ba9bf797ea55e /** * Allows for setting text color of the title. */ -@@ -1279,11 +1312,42 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { +@@ -1279,11 +1312,46 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { export interface HeaderBarButtonItemSpacing { type: 'spacing'; spacing: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 13e453e2582..8f158c1f951 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,9 +72,9 @@ patchedDependencies: '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f - expo-router@56.2.11: f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162 + expo-router@56.2.11: 7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - react-native-screens@4.25.2: ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126 + react-native-screens@4.25.2: 6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04 importers: @@ -309,7 +309,7 @@ importers: version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651) + version: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -363,7 +363,7 @@ importers: version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -12003,7 +12003,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651) + expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12301,7 +12301,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651) + expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16217,7 +16217,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(patch_hash=f4105a895c92dbd16cde508b889f9f91adfa9c8d3fa574d2291624e603b8b162)(9e3f7e083323b053f5523df8d80f7651): + expo-router@56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16248,7 +16248,7 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(patch_hash=ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -18855,7 +18855,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(patch_hash=ec394daa9cd5d6ab9af2e1f04e6c4718911cb1ea54211e1d7a5024e2e4080126)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) From 5a1138cf399d889a7920d88b66b3d29b57a9558d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 14:43:02 -0700 Subject: [PATCH 24/71] Polish native phone chrome --- apps/mobile/src/app/debug/rns-glass.tsx | 10 ++-- apps/mobile/src/features/home/HomeHeader.tsx | 2 - apps/mobile/src/features/home/HomeScreen.tsx | 2 - patches/react-native-screens@4.25.2.patch | 49 -------------------- pnpm-lock.yaml | 16 +++---- 5 files changed, 11 insertions(+), 68 deletions(-) diff --git a/apps/mobile/src/app/debug/rns-glass.tsx b/apps/mobile/src/app/debug/rns-glass.tsx index 3f16dc1e01a..657911af56b 100644 --- a/apps/mobile/src/app/debug/rns-glass.tsx +++ b/apps/mobile/src/app/debug/rns-glass.tsx @@ -238,7 +238,6 @@ function SidebarColumn(props: { const { colors } = props; const insets = useSafeAreaInsets(); const compactTopInset = 18; - const headerButtonTint = colors.foreground; if (props.compact) { return ( @@ -280,13 +279,11 @@ function SidebarColumn(props: { headerRightBarButtonItems={ [ { - accessibilityLabel: "More", - icon: { name: "ellipsis", type: "sfSymbol" }, - identifier: "rns-glass-more", + accessibilityLabel: "Open settings", + icon: { name: "gearshape", type: "sfSymbol" }, + identifier: "rns-glass-settings", onPress: () => {}, - tintColor: headerButtonTint, type: "button", - variant: "prominent", }, ] as ComponentProps["headerRightBarButtonItems"] } @@ -309,7 +306,6 @@ function SidebarColumn(props: { hideShadow={false} largeTitle={false} navigationItemStyle="editor" - subtitle="t3code · Ready" title="Threads" titleColor={colors.foreground} titleFontSize={18} diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 6796a499cdb..8d116365dbe 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -74,9 +74,7 @@ export function HomeHeader(props: { label: "", onPress: props.onOpenSettings, sharesBackground: true, - tintColor: iconColor, type: "button", - variant: "prominent", width: 58, }, ] diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 1681cba4bfe..22a23a915d4 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -603,9 +603,7 @@ export function HomeScreen(props: HomeScreenProps) { identifier: "home-settings", onPress: props.onOpenSettings, sharesBackground: true, - tintColor: foregroundColor, type: "button", - variant: "prominent", width: 58, }, ] as ComponentProps["headerRightBarButtonItems"] diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index fb1b9556223..4d4db90525c 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -1197,55 +1197,6 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac /** * Custom Screen Transition */ -diff --git a/node_modules/.bin/react-native b/node_modules/.bin/react-native -new file mode 100755 -index 0000000000000000000000000000000000000000..bdc1e9183f552f93e05d5d5501116bc9e3539219 ---- /dev/null -+++ b/node_modules/.bin/react-native -@@ -0,0 +1,43 @@ -+#!/bin/sh -+basedir=$(dirname "$(echo "$0" | sed -e 's,\\,/,g')") -+basedir_win="$basedir" -+exe="" -+msys="" -+ -+case `uname -a` in -+ *CYGWIN*|*MINGW*|*MSYS*) -+ if command -v cygpath > /dev/null 2>&1; then -+ basedir_win=`cygpath -w "$basedir"` -+ fi -+ exe=".exe" -+ msys="true" -+ ;; -+ *WSL2*) -+ if command -v wslpath > /dev/null 2>&1; then -+ basedir_win="$(wslpath -w "$basedir" 2> /dev/null)" -+ if [ $? -ne 0 ] || [ -z "$basedir_win" ]; then -+ basedir_win="$basedir" -+ else -+ exe=".exe" -+ fi -+ fi -+ ;; -+esac -+ -+if [ -z "$NODE_PATH" ]; then -+ export NODE_PATH="/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/node_modules" -+else -+ export NODE_PATH="/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules:/Users/julius/Developer/t3code/node_modules/.pnpm/node_modules:$NODE_PATH" -+fi -+if [ -n "$exe" ] && [ -x "$basedir/node.exe" ]; then -+ exec "$basedir/node.exe" "$basedir_win/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" -+elif [ -x "$basedir/node" ]; then -+ exec "$basedir/node" "$basedir/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" -+elif command -v node >/dev/null 2>&1; then -+ exec node "$basedir/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" -+elif [ -n "$exe" ] && command -v node.exe >/dev/null 2>&1; then -+ exec node.exe "$basedir_win/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" -+else -+ exec node "$basedir/../../../../../react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js" "$@" -+fi -+# cmd-shim-target=/Users/julius/Developer/t3code/node_modules/.pnpm/react-native@0.85.3_@babel+core@7.29.7_@react-native+metro-config@0.85.3_@babel+core@7._225d580d785074a01bfaff9454beedcb/node_modules/react-native/cli.js diff --git a/src/components/ScreenStackHeaderConfig.tsx b/src/components/ScreenStackHeaderConfig.tsx index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461f5d4da86 100644 --- a/src/components/ScreenStackHeaderConfig.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8f158c1f951..a37a603348e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,7 +74,7 @@ patchedDependencies: expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f expo-router@56.2.11: 7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - react-native-screens@4.25.2: 6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04 + react-native-screens@4.25.2: 0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970 importers: @@ -309,7 +309,7 @@ importers: version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9) + version: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -363,7 +363,7 @@ importers: version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -12003,7 +12003,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9) + expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12301,7 +12301,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9) + expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16217,7 +16217,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(6a31176c3f20e1d33712c4a708b7d2a9): + expo-router@56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16248,7 +16248,7 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(patch_hash=6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -18855,7 +18855,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(patch_hash=6f8431805b3a31bbf78a71a8ab7184f929cc9c444f44fe8a28ba1e6676c04b04)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) From ce72ed4eb82e6f42fdd79b401cad532cfd523f14 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 15:11:53 -0700 Subject: [PATCH 25/71] Restore native iPad thread search --- .../threads/ThreadNavigationSidebar.tsx | 22 +++++++++---------- .../features/threads/ThreadRouteScreen.tsx | 2 +- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index b1cf0a80423..d4d0c925ca6 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -490,6 +490,17 @@ export function ThreadNavigationSidebar(props: { const { Screen, ScreenStack, ScreenStackHeaderConfig } = require("react-native-screens") as typeof import("react-native-screens"); const nativeHeaderRightBarButtonItems = [ + { + accessibilityLabel: "Open settings", + icon: { name: "gearshape", type: "sfSymbol" }, + identifier: "thread-sidebar-settings", + onPress: props.onOpenSettings, + sharesBackground: true, + tintColor: foregroundColor, + type: "button", + variant: "prominent", + width: 58, + }, { accessibilityLabel: "Filter and sort threads", icon: { name: filterIcon, type: "sfSymbol" }, @@ -561,17 +572,6 @@ export function ThreadNavigationSidebar(props: { variant: "prominent", width: 58, }, - { - accessibilityLabel: "Open settings", - icon: { name: "gearshape", type: "sfSymbol" }, - identifier: "thread-sidebar-settings", - onPress: props.onOpenSettings, - sharesBackground: true, - tintColor: foregroundColor, - type: "button", - variant: "prominent", - width: 58, - }, ] as ComponentProps["headerRightBarButtonItems"]; return ( diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 57155cbba5d..58055ca8590 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -701,7 +701,7 @@ function ThreadRouteContent( projectScripts={selectedThreadProject?.scripts ?? []} terminalSessions={terminalMenuSessions} showDirectFileControl={layout.usesSplitView} - showSearchSlot={false} + showSearchSlot={layout.usesSplitView} onOpenTerminal={handleOpenTerminal} onOpenNewTerminal={handleOpenNewTerminal} onRunProjectScript={handleRunProjectScript} From a4472d0f49dcc8ca31259eacfb9764ca472a9ca2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 16:22:58 -0700 Subject: [PATCH 26/71] Wire native iPad thread toolbar --- .../features/threads/ThreadGitControls.tsx | 436 +++++++++++++----- .../features/threads/ThreadRouteScreen.tsx | 67 ++- patches/react-native-screens@4.25.2.patch | 148 ++++-- pnpm-lock.yaml | 8 +- 4 files changed, 456 insertions(+), 203 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 4e93a91e419..6a3231c0b31 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -11,6 +11,7 @@ import { resolveQuickAction, } from "@t3tools/client-runtime/state/vcs"; import { useLocalSearchParams, useRouter } from "expo-router"; +import type { NativeStackNavigationOptions } from "expo-router/build/react-navigation/native-stack/types"; import Stack from "expo-router/stack"; import { useCallback, useMemo } from "react"; import { Alert } from "react-native"; @@ -65,7 +66,16 @@ function compactMenuStatus(gitStatus: VcsStatusResult | null): string { return parts.join(" · "); } -export function ThreadGitControls(props: { +type HeaderRightItems = ReturnType< + NonNullable +>; +type QuickActionIcon = + | "arrow.down.circle" + | "arrow.up.right.circle" + | "checkmark.circle" + | "arrow.up.circle"; + +type ThreadGitControlsProps = { readonly auxiliaryPaneControl?: { readonly accessibilityLabel: string; readonly onPress: () => void; @@ -77,6 +87,7 @@ export function ThreadGitControls(props: { readonly canOpenFiles: boolean; readonly projectScripts: ReadonlyArray; readonly terminalSessions: ReadonlyArray; + readonly showActionControls?: boolean; readonly showDirectFileControl?: boolean; readonly showSearchSlot?: boolean; readonly onOpenFilesInspector?: () => void; @@ -86,7 +97,9 @@ export function ThreadGitControls(props: { readonly onRunProjectScript: (script: ProjectScript) => Promise; readonly onPull: () => Promise; readonly onRunAction: (input: GitActionRequestInput) => Promise; -}) { +}; + +function useThreadGitControlModel(props: ThreadGitControlsProps) { const router = useRouter(); const { environmentId, threadId } = useLocalSearchParams<{ environmentId: EnvironmentId; @@ -117,7 +130,7 @@ export function ThreadGitControls(props: { ? (quickAction.hint ?? "This action is unavailable.") : null; - const quickActionIcon = (() => { + const quickActionIcon: QuickActionIcon = (() => { if (quickAction.kind === "run_pull") return "arrow.down.circle"; if (quickAction.kind === "open_pr") return "arrow.up.right.circle"; if (quickAction.kind === "run_action") { @@ -189,9 +202,209 @@ export function ThreadGitControls(props: { } }, [onPull, openExistingPr, quickAction, runActionWithPrompt]); + const openFiles = useCallback(() => { + if (props.onOpenFilesInspector) { + props.onOpenFilesInspector(); + return; + } + router.push(buildThreadFilesNavigation({ environmentId, threadId })); + }, [environmentId, props.onOpenFilesInspector, router, threadId]); + + const openReview = useCallback(() => { + router.push(buildThreadReviewRoutePath({ environmentId, threadId })); + }, [environmentId, router, threadId]); + + const openGitInspector = useCallback(() => { + if (props.onOpenGitInspector) { + props.onOpenGitInspector(); + return; + } + router.push({ + pathname: "/threads/[environmentId]/[threadId]/git", + params: { environmentId, threadId }, + }); + }, [environmentId, props.onOpenGitInspector, router, threadId]); + + return { + currentBranchLabel, + isRepo, + openFiles, + openGitInspector, + openReview, + quickAction, + quickActionHint, + quickActionIcon, + runQuickAction, + }; +} + +export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): HeaderRightItems { + const model = useThreadGitControlModel(props); + + return useMemo( + () => + [ + { + accessibilityLabel: "Open terminal", + disabled: !props.canOpenTerminal, + icon: { name: "terminal", type: "sfSymbol" }, + identifier: "thread-right-terminal", + label: "Terminal", + menu: { + items: [ + ...props.projectScripts.map((script) => ({ + description: script.command, + icon: { name: projectScriptMenuIcon(script.icon), type: "sfSymbol" as const }, + label: projectScriptMenuLabel(script), + onPress: () => void props.onRunProjectScript(script), + type: "action" as const, + })), + ...(props.projectScripts.length === 0 + ? [ + { + description: "This project has no saved scripts yet", + disabled: true, + icon: { name: "play", type: "sfSymbol" as const }, + label: "No project scripts", + onPress: () => {}, + type: "action" as const, + }, + ] + : []), + ...props.terminalSessions.map((session) => ({ + description: [ + getTerminalStatusLabel({ + status: session.status, + hasRunningSubprocess: session.hasRunningSubprocess, + }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · "), + icon: { name: "terminal", type: "sfSymbol" as const }, + label: session.displayLabel, + onPress: () => props.onOpenTerminal(session.terminalId), + type: "action" as const, + })), + { + description: "Start another shell for this thread", + icon: { name: "plus", type: "sfSymbol" }, + label: "Open new terminal", + onPress: props.onOpenNewTerminal, + type: "action", + }, + ], + title: "Terminal", + }, + sharesBackground: true, + type: "menu", + variant: "prominent", + width: 58, + }, + { + accessibilityLabel: "Open files", + disabled: !props.canOpenFiles, + icon: { name: "folder", type: "sfSymbol" }, + identifier: "thread-right-files", + label: "Files", + onPress: model.openFiles, + sharesBackground: true, + type: "button", + variant: "prominent", + width: 58, + }, + { + accessibilityLabel: "Git actions", + icon: { name: "point.topleft.down.curvedto.point.bottomright.up", type: "sfSymbol" }, + identifier: "thread-right-git", + label: "Git", + menu: { + items: [ + { + description: compactMenuStatus(props.gitStatus), + disabled: true, + icon: { + name: "point.topleft.down.curvedto.point.bottomright.up", + type: "sfSymbol", + }, + label: compactMenuBranchLabel(model.currentBranchLabel), + onPress: () => {}, + type: "action", + }, + { + description: model.quickActionHint ?? undefined, + disabled: model.quickAction.disabled, + icon: { name: model.quickActionIcon, type: "sfSymbol" }, + label: model.quickAction.label, + onPress: () => void model.runQuickAction(), + type: "action", + }, + { + description: "Turn diffs and worktree changes", + disabled: !model.isRepo, + icon: { name: "text.bubble", type: "sfSymbol" }, + label: "Review changes", + onPress: model.openReview, + type: "action", + }, + { + description: "Browse this workspace", + disabled: !props.canOpenFiles, + icon: { name: "folder", type: "sfSymbol" }, + label: "Files", + onPress: model.openFiles, + type: "action", + }, + { + description: "Commit, files, branches", + icon: { name: "ellipsis.circle", type: "sfSymbol" }, + label: "More", + onPress: model.openGitInspector, + type: "action", + }, + ], + title: "Git", + }, + sharesBackground: true, + type: "menu", + variant: "prominent", + width: 58, + }, + ].toReversed() as HeaderRightItems, + [ + model.currentBranchLabel, + model.isRepo, + model.openFiles, + model.openGitInspector, + model.openReview, + model.quickAction.disabled, + model.quickAction.label, + model.quickActionHint, + model.quickActionIcon, + model.runQuickAction, + props.canOpenFiles, + props.canOpenTerminal, + props.gitStatus, + props.onOpenNewTerminal, + props.onOpenTerminal, + props.onRunProjectScript, + props.projectScripts, + props.terminalSessions, + ], + ); +} + +export function ThreadGitControls(props: ThreadGitControlsProps) { + const model = useThreadGitControlModel(props); + const showActionControls = props.showActionControls ?? true; + + if (!showActionControls && !props.showSearchSlot) { + return null; + } + return ( - {props.auxiliaryPaneControl ? ( + {showActionControls && props.auxiliaryPaneControl ? ( ) : null} - - {props.projectScripts.length > 0 ? ( - props.projectScripts.map((script) => ( + {showActionControls ? ( + <> + + {props.projectScripts.length > 0 ? ( + props.projectScripts.map((script) => ( + void props.onRunProjectScript(script)} + subtitle={script.command} + > + {projectScriptMenuLabel(script)} + + )) + ) : ( + {}} + subtitle="This project has no saved scripts yet" + > + No project scripts + + )} + {props.terminalSessions.map((session) => ( + props.onOpenTerminal(session.terminalId)} + subtitle={[ + getTerminalStatusLabel({ + status: session.status, + hasRunningSubprocess: session.hasRunningSubprocess, + }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · ")} + > + {session.displayLabel} + + ))} void props.onRunProjectScript(script)} - subtitle={script.command} + icon="plus" + onPress={props.onOpenNewTerminal} + subtitle="Start another shell for this thread" > - {projectScriptMenuLabel(script)} + Open new terminal - )) - ) : ( - {}} - subtitle="This project has no saved scripts yet" - > - No project scripts - - )} - {props.terminalSessions.map((session) => ( - props.onOpenTerminal(session.terminalId)} - subtitle={[ - getTerminalStatusLabel({ - status: session.status, - hasRunningSubprocess: session.hasRunningSubprocess, - }), - basename(session.cwd), - ] - .filter(Boolean) - .join(" · ")} - > - {session.displayLabel} - - ))} - - Open new terminal - - - {props.showDirectFileControl ? ( - { - if (props.onOpenFilesInspector) { - props.onOpenFilesInspector(); - return; - } - router.push(buildThreadFilesNavigation({ environmentId, threadId })); - }} - separateBackground - /> + + {props.showDirectFileControl ? ( + + ) : null} + + {}} + subtitle={compactMenuStatus(props.gitStatus)} + > + + {compactMenuBranchLabel(model.currentBranchLabel)} + + + void model.runQuickAction()} + subtitle={model.quickActionHint ?? undefined} + > + {model.quickAction.label} + + + Review changes + + + Files + + + More + + + ) : null} - - {}} - subtitle={compactMenuStatus(gitStatus)} - > - {compactMenuBranchLabel(currentBranchLabel)} - - void runQuickAction()} - subtitle={quickActionHint ?? undefined} - > - {quickAction.label} - - router.push(buildThreadReviewRoutePath({ environmentId, threadId }))} - subtitle="Turn diffs and worktree changes" - > - Review changes - - { - if (props.onOpenFilesInspector) { - props.onOpenFilesInspector(); - return; - } - router.push(buildThreadFilesNavigation({ environmentId, threadId })); - }} - subtitle="Browse this workspace" - > - Files - - { - if (props.onOpenGitInspector) { - props.onOpenGitInspector(); - return; - } - router.push({ - pathname: "/threads/[environmentId]/[threadId]/git", - params: { environmentId, threadId }, - }); - }} - subtitle="Commit, files, branches" - > - More - - {props.showSearchSlot ? ( <> diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 58055ca8590..2b120c8b86e 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -43,7 +43,7 @@ import { } from "../terminal/terminalLaunchContext"; import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen } from "./ThreadDetailScreen"; -import { ThreadGitControls } from "./ThreadGitControls"; +import { ThreadGitControls, useThreadGitRightHeaderItems } from "./ThreadGitControls"; import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -249,9 +249,6 @@ function ThreadRouteContent( } return inspectorSelection.mode; } - if (fileInspector.supported && selectedThreadCwd !== null) { - return "files"; - } return null; })(); useEffect(() => { @@ -596,6 +593,33 @@ function ThreadRouteContent( terminalMenuSessions, ], ); + const threadGitControlProps = { + auxiliaryPaneControl: + !layout.usesSplitView && fileInspector.supported && selectedThreadCwd !== null + ? { + accessibilityLabel: "Toggle inspector", + onPress: handleToggleInspector, + } + : undefined, + onOpenFilesInspector: + fileInspector.supported && selectedThreadCwd !== null ? handleOpenFilesInspector : undefined, + onOpenGitInspector: fileInspector.supported ? handleOpenGitInspector : undefined, + currentBranch: selectedThread?.branch ?? null, + gitStatus: gitStatus.data, + gitOperationLabel: gitState.gitOperationLabel, + canOpenTerminal: Boolean(selectedThreadProject?.workspaceRoot), + canOpenFiles: Boolean(selectedThreadProject?.workspaceRoot), + projectScripts: selectedThreadProject?.scripts ?? [], + terminalSessions: terminalMenuSessions, + showDirectFileControl: layout.usesSplitView, + showSearchSlot: false, + onOpenTerminal: handleOpenTerminal, + onOpenNewTerminal: handleOpenNewTerminal, + onRunProjectScript: handleRunProjectScript, + onPull: gitActions.onPullSelectedThreadBranch, + onRunAction: gitActions.onRunSelectedThreadGitAction, + }; + const threadRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); if (!environmentId || !threadId) { return ; @@ -646,6 +670,10 @@ function ThreadRouteContent( placement: "integratedButton", } : undefined, + unstable_headerRightItems: + layout.usesSplitView && Platform.OS === "ios" && !activeInspectorRenderer + ? () => threadRightHeaderItems + : undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> @@ -678,36 +706,7 @@ function ThreadRouteContent( ) : null} - + diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index 4d4db90525c..3c0d333c990 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -61,7 +61,7 @@ index 69d4d9a4704a7bef3fa207de2dbb2d5119846ccb..6192c4a23b1ce08cc2a085c5e7fb0f4a - (UIEdgeInsets)providerSafeAreaInsets diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h -index 919b984edc9f91ee9ac26faf257d8a721e26457c..3f54fad0a22d37bb7542679ae9d14088b14d07e3 100644 +index 919b984edc9f91ee9ac26faf257d8a721e26457c..5bb0cd6736ed6bc51db57e2a9326f758f22c51d8 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -21,6 +21,8 @@ @@ -73,7 +73,7 @@ index 919b984edc9f91ee9ac26faf257d8a721e26457c..3f54fad0a22d37bb7542679ae9d14088 @property (nonatomic, retain) NSString *titleFontFamily; @property (nonatomic, retain) NSNumber *titleFontSize; @property (nonatomic, retain) NSString *titleFontWeight; -@@ -45,9 +47,11 @@ NS_ASSUME_NONNULL_BEGIN +@@ -45,9 +47,12 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic) BOOL backButtonInCustomView; @property (nonatomic) UISemanticContentAttribute direction; @property (nonatomic) UINavigationItemBackButtonDisplayMode backButtonDisplayMode; @@ -81,12 +81,13 @@ index 919b984edc9f91ee9ac26faf257d8a721e26457c..3f54fad0a22d37bb7542679ae9d14088 @property (nonatomic) RNSBlurEffectStyle blurEffect; @property (nonatomic, copy, nullable) NSArray *> *headerRightBarButtonItems; @property (nonatomic, copy, nullable) NSArray *> *headerLeftBarButtonItems; ++@property (nonatomic, copy, nullable) NSArray *> *headerCenterBarButtonItems; +@property (nonatomic, copy, nullable) NSArray *> *headerToolbarItems; @property (nonatomic, readwrite) BOOL synchronousShadowStateUpdatesEnabled; NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm -index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be58629a26 100644 +index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f4f19bdb2 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -30,6 +30,20 @@ namespace react = facebook::react; @@ -159,7 +160,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV -@@ -637,10 +667,229 @@ RNS_IGNORE_SUPER_CALL_END +@@ -637,10 +667,235 @@ RNS_IGNORE_SUPER_CALL_END // This assignment should be done after `navitem.titleView = ...` assignment (iOS 16.0 bug). // See: https://github.com/software-mansion/react-native-screens/issues/1570 (comments) navitem.title = config.title; @@ -173,10 +174,16 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be + NSArray *rightBarButtonItems = [config barButtonItemsFromConfigs:config.headerRightBarButtonItems + withCurrentItems:navitem.rightBarButtonItems + navigationItem:navitem]; ++ NSArray *centerBarButtonItems = [config barButtonItemsFromConfigs:config.headerCenterBarButtonItems ++ withCurrentItems:@[] ++ navigationItem:navitem]; +#if !TARGET_OS_TV + if (@available(iOS 16.0, *)) { + navitem.leadingItemGroups = [config barButtonItemGroupsFromItems:leftBarButtonItems]; + navitem.trailingItemGroups = [config barButtonItemGroupsFromItems:rightBarButtonItems]; ++ if (@available(iOS 26.0, *)) { ++ navitem.centerItemGroups = [config barButtonItemGroupsFromItems:centerBarButtonItems]; ++ } + navitem.leftBarButtonItems = nil; + navitem.rightBarButtonItems = nil; + } else { @@ -393,7 +400,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items // (setting nav bar visibility should be done after `navitem.*BarButtonItems`). -@@ -773,6 +1022,7 @@ RNS_IGNORE_SUPER_CALL_END +@@ -773,6 +1028,7 @@ RNS_IGNORE_SUPER_CALL_END - (NSArray *)barButtonItemsFromConfigs:(NSArray *> *)dicts withCurrentItems:(NSArray *)currentItems @@ -401,7 +408,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be { if (dicts.count == 0) { return currentItems; -@@ -781,7 +1031,190 @@ RNS_IGNORE_SUPER_CALL_END +@@ -781,7 +1037,190 @@ RNS_IGNORE_SUPER_CALL_END [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; @@ -593,7 +600,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict action:^(NSString *buttonId) { auto eventEmitter = std::static_pointer_cast( -@@ -809,11 +1242,15 @@ RNS_IGNORE_SUPER_CALL_END +@@ -809,11 +1248,15 @@ RNS_IGNORE_SUPER_CALL_END [items addObject:item]; } } else if (dict[@"spacing"]) { @@ -613,7 +620,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be NSNumber *index = dict[@"index"]; if (index.integerValue < items.count) { [items insertObject:item atIndex:index.integerValue]; -@@ -825,6 +1262,47 @@ RNS_IGNORE_SUPER_CALL_END +@@ -825,6 +1268,47 @@ RNS_IGNORE_SUPER_CALL_END return items; } @@ -661,7 +668,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be RNS_IGNORE_SUPER_CALL_BEGIN - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex { -@@ -1013,6 +1491,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1013,6 +1497,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); @@ -670,7 +677,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); } -@@ -1038,6 +1518,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1038,6 +1524,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; _backButtonDisplayMode = [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; @@ -678,10 +685,22 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..ec3d02cf912fe0e617ed593e321177be if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; -@@ -1084,6 +1565,18 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1084,6 +1571,30 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) _headerRightBarButtonItems = array; } ++ if (newScreenProps.headerCenterBarButtonItems != oldScreenProps.headerCenterBarButtonItems) { ++ const auto &vec = newScreenProps.headerCenterBarButtonItems; ++ NSMutableArray *> *array = [NSMutableArray arrayWithCapacity:vec.size()]; ++ for (const auto &item : vec) { ++ NSDictionary *dict = [RNSConvert idFromFollyDynamic:item]; ++ if ([dict isKindOfClass:[NSDictionary class]]) { ++ [array addObject:dict]; ++ } ++ } ++ _headerCenterBarButtonItems = array; ++ } ++ + if (newScreenProps.headerToolbarItems != oldScreenProps.headerToolbarItems) { + const auto &vec = newScreenProps.headerToolbarItems; + NSMutableArray *> *array = [NSMutableArray arrayWithCapacity:vec.size()]; @@ -770,28 +789,30 @@ index 52c61c863c53c289569a741445ed08c0355d95ba..cb4d6be752f98e06dfd3b40453c2bd88 #pragma mark - Override diff --git a/lib/commonjs/components/ScreenStackHeaderConfig.js b/lib/commonjs/components/ScreenStackHeaderConfig.js -index 16b979bb3dfb41ff247403f1632c300a9a60d549..67f52f105966833884344553847f11d0b66e6a4e 100644 +index 16b979bb3dfb41ff247403f1632c300a9a60d549..ad1fdb273e09b344184a7013f4f1b038e84ed19d 100644 --- a/lib/commonjs/components/ScreenStackHeaderConfig.js +++ b/lib/commonjs/components/ScreenStackHeaderConfig.js -@@ -23,18 +23,40 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -23,18 +23,42 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ } = (0, _TopInsetApplicationContext.useTopInsetApplication)(!props.hidden, props.disableTopInsetApplication ?? false); const { headerLeftBarButtonItems, - headerRightBarButtonItems + headerRightBarButtonItems, ++ headerCenterBarButtonItems, + headerToolbarItems } = props; const preparedHeaderLeftBarButtonItems = headerLeftBarButtonItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerLeftBarButtonItems, 'left') : undefined; const preparedHeaderRightBarButtonItems = headerRightBarButtonItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerRightBarButtonItems, 'right') : undefined; - const hasHeaderBarButtonItems = _utils.isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length); ++ const preparedHeaderCenterBarButtonItems = headerCenterBarButtonItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerCenterBarButtonItems, 'right') : undefined; + const preparedHeaderToolbarItems = headerToolbarItems && _utils.isHeaderBarButtonsAvailableForCurrentPlatform ? (0, _prepareHeaderBarButtonItems.prepareHeaderBarButtonItems)(headerToolbarItems, 'right') : undefined; -+ const hasHeaderBarButtonItems = _utils.isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length || preparedHeaderToolbarItems?.length); ++ const hasHeaderBarButtonItems = _utils.isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length || preparedHeaderCenterBarButtonItems?.length || preparedHeaderToolbarItems?.length); // Handle bar button item presses const onPressHeaderBarButtonItem = hasHeaderBarButtonItems ? event => { - const pressedItem = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])].find(item => item && 'buttonId' in item && item.buttonId === event.nativeEvent.buttonId); + const buttonId = event.nativeEvent.buttonId; -+ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderCenterBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; + const pressedItem = allItems.find(item => item && 'buttonId' in item && item.buttonId === buttonId); if (pressedItem && pressedItem.type === 'button' && pressedItem.onPress) { pressedItem.onPress(); @@ -817,16 +838,16 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..67f52f105966833884344553847f11d0 } : undefined; // Handle bar button menu item presses by deep-searching nested menus -@@ -56,7 +78,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -56,7 +80,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ }; // Check each bar-button item with a menu - const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])]; -+ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderCenterBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; for (const item of allItems) { if (item && item.type === 'menu' && item.menu) { const action = findInMenu(item.menu, event.nativeEvent.menuId); -@@ -64,6 +86,15 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -64,6 +88,15 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ action.onPress(); return; } @@ -842,10 +863,11 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..67f52f105966833884344553847f11d0 } } } : undefined; -@@ -71,6 +102,7 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ +@@ -71,6 +104,8 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ userInterfaceStyle: props.experimental_userInterfaceStyle, headerLeftBarButtonItems: preparedHeaderLeftBarButtonItems, headerRightBarButtonItems: preparedHeaderRightBarButtonItems, ++ headerCenterBarButtonItems: preparedHeaderCenterBarButtonItems, + headerToolbarItems: preparedHeaderToolbarItems, onPressHeaderBarButtonItem: onPressHeaderBarButtonItem, onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, @@ -918,28 +940,30 @@ index de25040bfec8587ca311f5a42661240640989419..bf73f41744e08bc60ffac09029419d19 function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } diff --git a/lib/module/components/ScreenStackHeaderConfig.js b/lib/module/components/ScreenStackHeaderConfig.js -index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..b1dfe857e0cf06c75088203a7f4157643af479b5 100644 +index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..3d995490fac910dfcc21e81ae965e31f24717c51 100644 --- a/lib/module/components/ScreenStackHeaderConfig.js +++ b/lib/module/components/ScreenStackHeaderConfig.js -@@ -19,18 +19,40 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -19,18 +19,42 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref } = useTopInsetApplication(!props.hidden, props.disableTopInsetApplication ?? false); const { headerLeftBarButtonItems, - headerRightBarButtonItems + headerRightBarButtonItems, ++ headerCenterBarButtonItems, + headerToolbarItems } = props; const preparedHeaderLeftBarButtonItems = headerLeftBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerLeftBarButtonItems, 'left') : undefined; const preparedHeaderRightBarButtonItems = headerRightBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerRightBarButtonItems, 'right') : undefined; - const hasHeaderBarButtonItems = isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length); ++ const preparedHeaderCenterBarButtonItems = headerCenterBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerCenterBarButtonItems, 'right') : undefined; + const preparedHeaderToolbarItems = headerToolbarItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerToolbarItems, 'right') : undefined; -+ const hasHeaderBarButtonItems = isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length || preparedHeaderToolbarItems?.length); ++ const hasHeaderBarButtonItems = isHeaderBarButtonsAvailableForCurrentPlatform && (preparedHeaderLeftBarButtonItems?.length || preparedHeaderRightBarButtonItems?.length || preparedHeaderCenterBarButtonItems?.length || preparedHeaderToolbarItems?.length); // Handle bar button item presses const onPressHeaderBarButtonItem = hasHeaderBarButtonItems ? event => { - const pressedItem = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])].find(item => item && 'buttonId' in item && item.buttonId === event.nativeEvent.buttonId); + const buttonId = event.nativeEvent.buttonId; -+ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderCenterBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; + const pressedItem = allItems.find(item => item && 'buttonId' in item && item.buttonId === buttonId); if (pressedItem && pressedItem.type === 'button' && pressedItem.onPress) { pressedItem.onPress(); @@ -965,16 +989,16 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..b1dfe857e0cf06c75088203a7f415764 } : undefined; // Handle bar button menu item presses by deep-searching nested menus -@@ -52,7 +74,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -52,7 +76,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref }; // Check each bar-button item with a menu - const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? [])]; -+ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; ++ const allItems = [...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ...(preparedHeaderCenterBarButtonItems ?? []), ...(preparedHeaderToolbarItems ?? [])]; for (const item of allItems) { if (item && item.type === 'menu' && item.menu) { const action = findInMenu(item.menu, event.nativeEvent.menuId); -@@ -60,6 +82,15 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -60,6 +84,15 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref action.onPress(); return; } @@ -990,10 +1014,11 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..b1dfe857e0cf06c75088203a7f415764 } } } : undefined; -@@ -67,6 +98,7 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref +@@ -67,6 +100,8 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref userInterfaceStyle: props.experimental_userInterfaceStyle, headerLeftBarButtonItems: preparedHeaderLeftBarButtonItems, headerRightBarButtonItems: preparedHeaderRightBarButtonItems, ++ headerCenterBarButtonItems: preparedHeaderCenterBarButtonItems, + headerToolbarItems: preparedHeaderToolbarItems, onPressHeaderBarButtonItem: onPressHeaderBarButtonItem, onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, @@ -1047,7 +1072,7 @@ index 07caaf7f04c1d6484727b43b694ccf76b1988984..07fd7a57687797c5ae8f685ea70ccf98 //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts -index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..45f16ef96ee86402c68a1b90edb4a71de05773a9 100644 +index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..9e3d310dea3a36d79ae7f5078ccc792ab9ff9979 100644 --- a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +++ b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts @@ -9,6 +9,7 @@ type OnPressHeaderBarButtonMenuItemEvent = Readonly<{ @@ -1058,7 +1083,7 @@ index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..45f16ef96ee86402c68a1b90edb4a71d type BlurEffect = 'none' | 'extraLight' | 'light' | 'dark' | 'regular' | 'prominent' | 'systemUltraThinMaterial' | 'systemThinMaterial' | 'systemMaterial' | 'systemThickMaterial' | 'systemChromeMaterial' | 'systemUltraThinMaterialLight' | 'systemThinMaterialLight' | 'systemMaterialLight' | 'systemThickMaterialLight' | 'systemChromeMaterialLight' | 'systemUltraThinMaterialDark' | 'systemThinMaterialDark' | 'systemMaterialDark' | 'systemThickMaterialDark' | 'systemChromeMaterialDark'; type UserInterfaceStyle = 'unspecified' | 'light' | 'dark'; export interface NativeProps extends ViewProps { -@@ -32,18 +33,22 @@ export interface NativeProps extends ViewProps { +@@ -32,18 +33,23 @@ export interface NativeProps extends ViewProps { largeTitleColor?: ColorValue | undefined; translucent?: boolean | undefined; title?: string | undefined; @@ -1077,6 +1102,7 @@ index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..45f16ef96ee86402c68a1b90edb4a71d topInsetEnabled?: boolean | undefined; headerLeftBarButtonItems?: CT.UnsafeMixed[] | undefined; headerRightBarButtonItems?: CT.UnsafeMixed[] | undefined; ++ headerCenterBarButtonItems?: CT.UnsafeMixed[] | undefined; + headerToolbarItems?: CT.UnsafeMixed[] | undefined; onPressHeaderBarButtonItem?: CT.DirectEventHandler | undefined; onPressHeaderBarButtonMenuItem?: CT.DirectEventHandler | undefined; @@ -1094,7 +1120,7 @@ index 8e4480f901d5d557244643a32341d46e9a497e68..883c5f264301058cbcd9090bee2702ee //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts -index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac4e6af1d8 100644 +index 3b384e03891e38e936f370372a682d73440e7ec2..66fe089401975016d2fb91b13e135940adf516cb 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -10,6 +10,7 @@ export type SearchBarCommands = { @@ -1120,11 +1146,21 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac /** * Array of UIBarButtomItems to the left side of the header. * -@@ -643,6 +652,16 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { +@@ -643,6 +652,26 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { * @platform ios */ headerRightBarButtonItems?: HeaderBarButtonItem[] | undefined; + /** ++ * Array of UIBarButtonItems to the centered item group in modern iOS headers. ++ * ++ * This maps to `UINavigationItem.centerItemGroups` on iOS 26+ and lets ++ * document/editor-style chrome keep primary actions visually centered while ++ * leaving trailing search separate. ++ * ++ * @platform ios ++ */ ++ headerCenterBarButtonItems?: HeaderBarButtonItem[] | undefined; ++ /** + * Array of UIBarButtonItems to display in the native toolbar attached to + * the current screen's navigation controller. + * @@ -1137,7 +1173,7 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac /** * When set to true the header will be hidden while the parent Screen is on the top of the stack. The default value is false. */ -@@ -704,6 +723,20 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { +@@ -704,6 +733,20 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { * String that can be displayed in the header as a fallback for `headerTitle`. */ title?: string | undefined; @@ -1158,7 +1194,7 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac /** * Allows for setting text color of the title. */ -@@ -1145,8 +1178,37 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { +@@ -1145,8 +1188,37 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { export interface HeaderBarButtonItemSpacing { type: 'spacing'; spacing: number; @@ -1198,10 +1234,10 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..9834a915b66c1c5172a2e9aa423599ac * Custom Screen Transition */ diff --git a/src/components/ScreenStackHeaderConfig.tsx b/src/components/ScreenStackHeaderConfig.tsx -index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461f5d4da86 100644 +index 421b3c2545426ae957271bd6515bcf827437541c..0ca6f1d7f324eaf257891a3bbb0aab210aa30f6f 100644 --- a/src/components/ScreenStackHeaderConfig.tsx +++ b/src/components/ScreenStackHeaderConfig.tsx -@@ -39,7 +39,11 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -39,7 +39,12 @@ export const ScreenStackHeaderConfig = React.forwardRef< props.disableTopInsetApplication ?? false, ); @@ -1209,15 +1245,20 @@ index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461 + const { + headerLeftBarButtonItems, + headerRightBarButtonItems, ++ headerCenterBarButtonItems, + headerToolbarItems, + } = props; const preparedHeaderLeftBarButtonItems = headerLeftBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform -@@ -49,22 +53,30 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -49,22 +54,36 @@ export const ScreenStackHeaderConfig = React.forwardRef< headerRightBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ? prepareHeaderBarButtonItems(headerRightBarButtonItems, 'right') : undefined; ++ const preparedHeaderCenterBarButtonItems = ++ headerCenterBarButtonItems && isHeaderBarButtonsAvailableForCurrentPlatform ++ ? prepareHeaderBarButtonItems(headerCenterBarButtonItems, 'right') ++ : undefined; + const preparedHeaderToolbarItems = + headerToolbarItems && isHeaderBarButtonsAvailableForCurrentPlatform + ? prepareHeaderBarButtonItems(headerToolbarItems, 'right') @@ -1227,6 +1268,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461 (preparedHeaderLeftBarButtonItems?.length || - preparedHeaderRightBarButtonItems?.length); + preparedHeaderRightBarButtonItems?.length || ++ preparedHeaderCenterBarButtonItems?.length || + preparedHeaderToolbarItems?.length); // Handle bar button item presses @@ -1238,6 +1280,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461 ...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), - ].find( ++ ...(preparedHeaderCenterBarButtonItems ?? []), + ...(preparedHeaderToolbarItems ?? []), + ]; + const pressedItem = allItems.find( @@ -1249,7 +1292,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461 ); if ( pressedItem && -@@ -73,6 +85,31 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -73,6 +92,31 @@ export const ScreenStackHeaderConfig = React.forwardRef< ) { pressedItem.onPress(); } @@ -1281,15 +1324,16 @@ index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461 } : undefined; -@@ -102,6 +139,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -102,6 +146,8 @@ export const ScreenStackHeaderConfig = React.forwardRef< const allItems = [ ...(preparedHeaderLeftBarButtonItems ?? []), ...(preparedHeaderRightBarButtonItems ?? []), ++ ...(preparedHeaderCenterBarButtonItems ?? []), + ...(preparedHeaderToolbarItems ?? []), ]; for (const item of allItems) { if (item && item.type === 'menu' && item.menu) { -@@ -110,6 +148,17 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -110,6 +156,17 @@ export const ScreenStackHeaderConfig = React.forwardRef< action.onPress(); return; } @@ -1307,10 +1351,11 @@ index 421b3c2545426ae957271bd6515bcf827437541c..4566c1be1825ccaf487d4b64e96bf461 } } } -@@ -121,6 +170,7 @@ export const ScreenStackHeaderConfig = React.forwardRef< +@@ -121,6 +178,8 @@ export const ScreenStackHeaderConfig = React.forwardRef< userInterfaceStyle={props.experimental_userInterfaceStyle} headerLeftBarButtonItems={preparedHeaderLeftBarButtonItems} headerRightBarButtonItems={preparedHeaderRightBarButtonItems} ++ headerCenterBarButtonItems={preparedHeaderCenterBarButtonItems} + headerToolbarItems={preparedHeaderToolbarItems} onPressHeaderBarButtonItem={onPressHeaderBarButtonItem} onPressHeaderBarButtonMenuItem={onPressHeaderBarButtonMenuItem} @@ -1366,7 +1411,7 @@ index be2c24dfee77f5883b5ab1d7473be80933b5dc6f..0b038d2005b2d51bbb2ab1d7dba7eacc if (item.icon?.type === 'imageSource') { imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts -index ba2479de0f49c112470f78c5084059ddd9e2f3e8..811d41a20d1212a5d05f8cdf86d6b63755ed1ce1 100644 +index ba2479de0f49c112470f78c5084059ddd9e2f3e8..8677583a8f8daa4839340467466bfab8d73111ab 100644 --- a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts @@ -14,6 +14,7 @@ type OnPressHeaderBarButtonItemEvent = Readonly<{ buttonId: string }>; @@ -1393,10 +1438,11 @@ index ba2479de0f49c112470f78c5084059ddd9e2f3e8..811d41a20d1212a5d05f8cdf86d6b637 hideBackButton?: boolean | undefined; backButtonInCustomView?: boolean | undefined; blurEffect?: CT.WithDefault; -@@ -74,6 +78,7 @@ export interface NativeProps extends ViewProps { +@@ -74,6 +78,8 @@ export interface NativeProps extends ViewProps { topInsetEnabled?: boolean | undefined; headerLeftBarButtonItems?: CT.UnsafeMixed[] | undefined; headerRightBarButtonItems?: CT.UnsafeMixed[] | undefined; ++ headerCenterBarButtonItems?: CT.UnsafeMixed[] | undefined; + headerToolbarItems?: CT.UnsafeMixed[] | undefined; onPressHeaderBarButtonItem?: | CT.DirectEventHandler @@ -1412,7 +1458,7 @@ index a405aec437bd879d8be7a706d29e965d77658899..cd649bd7ab7de7b7131302e8be682ec6 +export * from './components/gamma/scroll-view-marker'; export type * from './components/shared/types'; diff --git a/src/types.tsx b/src/types.tsx -index 76a83f3acb6fd3f0af7f027798848b7124100286..5d0e317fa26844989e8a80c4ffc4d703c22abd8a 100644 +index 76a83f3acb6fd3f0af7f027798848b7124100286..bb7abb6e4705b8423b2bf89989fe4a8b38d751f7 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -26,6 +26,7 @@ export type SearchBarCommands = { @@ -1438,11 +1484,21 @@ index 76a83f3acb6fd3f0af7f027798848b7124100286..5d0e317fa26844989e8a80c4ffc4d703 /** * Array of UIBarButtomItems to the left side of the header. * -@@ -747,6 +756,16 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { +@@ -747,6 +756,26 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { * @platform ios */ headerRightBarButtonItems?: HeaderBarButtonItem[] | undefined; + /** ++ * Array of UIBarButtonItems to the centered item group in modern iOS headers. ++ * ++ * This maps to `UINavigationItem.centerItemGroups` on iOS 26+ and lets ++ * document/editor-style chrome keep primary actions visually centered while ++ * leaving trailing search separate. ++ * ++ * @platform ios ++ */ ++ headerCenterBarButtonItems?: HeaderBarButtonItem[] | undefined; ++ /** + * Array of UIBarButtonItems to display in the native toolbar attached to + * the current screen's navigation controller. + * @@ -1455,7 +1511,7 @@ index 76a83f3acb6fd3f0af7f027798848b7124100286..5d0e317fa26844989e8a80c4ffc4d703 /** * When set to true the header will be hidden while the parent Screen is on the top of the stack. The default value is false. */ -@@ -808,6 +827,20 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { +@@ -808,6 +837,20 @@ export interface ScreenStackHeaderConfigProps extends ViewProps { * String that can be displayed in the header as a fallback for `headerTitle`. */ title?: string | undefined; @@ -1476,7 +1532,7 @@ index 76a83f3acb6fd3f0af7f027798848b7124100286..5d0e317fa26844989e8a80c4ffc4d703 /** * Allows for setting text color of the title. */ -@@ -1279,11 +1312,46 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { +@@ -1279,11 +1322,46 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { export interface HeaderBarButtonItemSpacing { type: 'spacing'; spacing: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a37a603348e..87790cc10c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -74,7 +74,7 @@ patchedDependencies: expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f expo-router@56.2.11: 7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - react-native-screens@4.25.2: 0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970 + react-native-screens@4.25.2: 9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b importers: @@ -363,7 +363,7 @@ importers: version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16248,7 +16248,7 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(patch_hash=0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -18855,7 +18855,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(patch_hash=0d1f958f77cda20b499ec08b8b1261ddd21986a75f807815c4f93a5097f12970)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) From 59af9dce68d8cd9375d11f33bba666304b631ea4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 17:05:45 -0700 Subject: [PATCH 27/71] Recover stale mobile thread routes --- .../features/threads/ThreadGitControls.tsx | 15 ++++- apps/mobile/src/state/use-thread-selection.ts | 56 ++++++++++++++++++- patches/expo-router@56.2.11.patch | 39 ++++++++++--- pnpm-lock.yaml | 10 ++-- 4 files changed, 102 insertions(+), 18 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 6a3231c0b31..0c7930eaca7 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -66,7 +66,7 @@ function compactMenuStatus(gitStatus: VcsStatusResult | null): string { return parts.join(" · "); } -type HeaderRightItems = ReturnType< +type HeaderItems = ReturnType< NonNullable >; type QuickActionIcon = @@ -238,7 +238,7 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { }; } -export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): HeaderRightItems { +function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): HeaderItems { const model = useThreadGitControlModel(props); return useMemo( @@ -370,7 +370,7 @@ export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): Hea variant: "prominent", width: 58, }, - ].toReversed() as HeaderRightItems, + ] as HeaderItems, [ model.currentBranchLabel, model.isRepo, @@ -394,6 +394,15 @@ export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): Hea ); } +export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): HeaderItems { + const actionItems = useThreadGitHeaderActionItems(props); + return useMemo(() => actionItems.toReversed() as HeaderItems, [actionItems]); +} + +export function useThreadGitCenterHeaderItems(props: ThreadGitControlsProps): HeaderItems { + return useThreadGitHeaderActionItems(props); +} + export function ThreadGitControls(props: ThreadGitControlsProps) { const model = useThreadGitControlModel(props); const showActionControls = props.showActionControls ?? true; diff --git a/apps/mobile/src/state/use-thread-selection.ts b/apps/mobile/src/state/use-thread-selection.ts index 8f3a1c2d1cf..656a9613866 100644 --- a/apps/mobile/src/state/use-thread-selection.ts +++ b/apps/mobile/src/state/use-thread-selection.ts @@ -2,12 +2,16 @@ import { useGlobalSearchParams } from "expo-router"; import { createContext, createElement, use, useMemo, useRef, type ReactNode } from "react"; import { EnvironmentId, + type OrchestrationThread, ThreadId, type ScopedProjectRef, type ScopedThreadRef, } from "@t3tools/contracts"; +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import * as Option from "effect/Option"; import { useProject, useThreadShell } from "../state/entities"; +import { useEnvironmentThread } from "../state/threads"; import { useRemoteEnvironmentRuntime, useSavedRemoteConnection, @@ -21,6 +25,43 @@ function firstRouteParam(value: string | string[] | undefined): string | null { return value ?? null; } +function latestUserMessageAt(thread: OrchestrationThread): OrchestrationThread["updatedAt"] | null { + for (let index = thread.messages.length - 1; index >= 0; index -= 1) { + const message = thread.messages[index]; + if (message?.role === "user") { + return message.createdAt; + } + } + + return null; +} + +function threadDetailToShell( + environmentId: EnvironmentId, + thread: OrchestrationThread, +): EnvironmentThreadShell { + return { + environmentId, + id: thread.id, + projectId: thread.projectId, + title: thread.title, + modelSelection: thread.modelSelection, + runtimeMode: thread.runtimeMode, + interactionMode: thread.interactionMode, + branch: thread.branch, + worktreePath: thread.worktreePath, + latestTurn: thread.latestTurn, + createdAt: thread.createdAt, + updatedAt: thread.updatedAt, + archivedAt: thread.archivedAt, + session: thread.session, + latestUserMessageAt: latestUserMessageAt(thread), + hasPendingApprovals: false, + hasPendingUserInput: false, + hasActionableProposedPlan: false, + }; +} + function useResolvedThreadSelection() { const params = useGlobalSearchParams<{ environmentId?: string | string[]; @@ -43,7 +84,20 @@ function useResolvedThreadSelection() { lastRouteThreadRef.current = routeThreadRef; } const selectedThreadRef = routeThreadRef ?? lastRouteThreadRef.current; - const selectedThread = useThreadShell(selectedThreadRef); + const selectedThreadShell = useThreadShell(selectedThreadRef); + const selectedThreadDetailState = useEnvironmentThread( + selectedThreadRef?.environmentId ?? null, + selectedThreadRef?.threadId ?? null, + ); + const selectedThreadDetail = Option.getOrNull(selectedThreadDetailState.data); + const selectedThread = useMemo( + () => + selectedThreadShell ?? + (selectedThreadRef !== null && selectedThreadDetail !== null + ? threadDetailToShell(selectedThreadRef.environmentId, selectedThreadDetail) + : null), + [selectedThreadDetail, selectedThreadRef, selectedThreadShell], + ); const selectedProjectRef = useMemo( () => selectedThread === null diff --git a/patches/expo-router@56.2.11.patch b/patches/expo-router@56.2.11.patch index 78a4dace79c..a2ea0f13a3a 100644 --- a/patches/expo-router@56.2.11.patch +++ b/patches/expo-router@56.2.11.patch @@ -1,12 +1,21 @@ diff --git a/build/react-navigation/native-stack/types.d.ts b/build/react-navigation/native-stack/types.d.ts -index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..93845c781609c1a46bf71f2c30ef81432fb56bc2 100644 +index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..78929fe237b7f9d7808f8de2b844e822976e0d69 100644 --- a/build/react-navigation/native-stack/types.d.ts +++ b/build/react-navigation/native-stack/types.d.ts -@@ -306,6 +306,22 @@ export type NativeStackNavigationOptions = { +@@ -306,6 +306,31 @@ export type NativeStackNavigationOptions = { * @platform ios */ unstable_headerRightItems?: (props: NativeStackHeaderItemProps) => NativeStackHeaderItem[]; + /** ++ * Function which returns an array of items to display as the centered item ++ * group in modern iOS headers. ++ * ++ * This is an unstable API and might change in the future. ++ * ++ * @platform ios ++ */ ++ unstable_headerCenterItems?: (props: NativeStackHeaderItemProps) => NativeStackHeaderItem[]; ++ /** + * Function which returns an array of toolbar items for the current screen. + * + * This is an unstable API and might change in the future. @@ -25,18 +34,18 @@ index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..93845c781609c1a46bf71f2c30ef8143 /** * String or a function that returns a React Element to be used by the header. * Defaults to screen `title` or route name. -@@ -1115,7 +1131,8 @@ export type NativeStackHeaderItemCustom = { +@@ -1115,7 +1140,8 @@ export type NativeStackHeaderItemCustom = { * if the items don't fit the available space, they will be collapsed into a menu automatically. * Items with `type: 'custom'` will not be included in this automatic collapsing behavior. */ -export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom; -+export type NativeStackHeaderRawRNSItem = NonNullable[number] | NonNullable[number]; ++export type NativeStackHeaderRawRNSItem = NonNullable[number] | NonNullable[number] | NonNullable[number]; +export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom | NativeStackHeaderRawRNSItem; export type NativeStackNavigatorProps = DefaultNavigatorOptions, NativeStackNavigationOptions, NativeStackNavigationEventMap, NativeStackNavigationProp> & StackRouterOptions & NativeStackNavigationConfig; export type NativeStackDescriptor = Descriptor, RouteProp>; export type NativeStackDescriptorMap = { diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts -index 218c4291fdb57600a45d32fbcefe3707a520fe79..ce3bbadec2d52949d133401f1df01b95899aa5bf 100644 +index 218c4291fdb57600a45d32fbcefe3707a520fe79..5fdbb2754755a27589ad1555eae892b5fbee2c6a 100644 --- a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts +++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts @@ -10,6 +10,6 @@ type Props = NativeStackNavigationOptions & { @@ -44,11 +53,11 @@ index 218c4291fdb57600a45d32fbcefe3707a520fe79..ce3bbadec2d52949d133401f1df01b95 route: Route; }; -export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }: Props): ScreenStackHeaderConfigProps; -+export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }: Props): ScreenStackHeaderConfigProps; ++export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }: Props): ScreenStackHeaderConfigProps; export {}; //# sourceMappingURL=useHeaderConfigProps.d.ts.map diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.js b/build/react-navigation/native-stack/views/useHeaderConfigProps.js -index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..ed9651d5bcce56594c6a1946b2cb6a8f11f80c7c 100644 +index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..9d839442e00e136204630663f207a45c950dcd25 100644 --- a/build/react-navigation/native-stack/views/useHeaderConfigProps.js +++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.js @@ -22,6 +22,9 @@ const processBarButtonItems = (items, colors, fonts) => { @@ -66,14 +75,26 @@ index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..ed9651d5bcce56594c6a1946b2cb6a8f }; }; -function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }) { -+function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }) { ++function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }) { const { direction } = (0, native_1.useLocale)(); const { colors, fonts, dark } = (0, native_1.useTheme)(); const tintColor = headerTintColor ?? (react_native_1.Platform.OS === 'ios' ? colors.primary : colors.text); -@@ -270,6 +273,8 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac +@@ -216,6 +219,10 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac + tintColor, + canGoBack, + }); ++ const centerItems = headerCenterItems?.({ ++ tintColor, ++ canGoBack, ++ }); + if (rightItems) { + // iOS renders right items in reverse order + // So we need to reverse them here to match the order +@@ -270,6 +277,9 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac children, headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts), headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), ++ headerCenterBarButtonItems: processBarButtonItems(centerItems, colors, fonts), + headerToolbarItems, + navigationItemStyle, experimental_userInterfaceStyle: dark ? 'dark' : 'light', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 87790cc10c6..80d36c1cf98 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,7 @@ patchedDependencies: '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f - expo-router@56.2.11: 7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215 + expo-router@56.2.11: d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 react-native-screens@4.25.2: 9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b @@ -309,7 +309,7 @@ importers: version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177) + version: 56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -12003,7 +12003,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177) + expo-router: 56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12301,7 +12301,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177) + expo-router: 56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16217,7 +16217,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(patch_hash=7b915c5a27d2253e9770b92ebfcd8a9f85ebb849f07bdfa8d85af76ea0231215)(5cd69264394276d4731a5f991997a177): + expo-router@56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) From b5ef3187481f9fb6b0afbf6e2c67eef0867e2b8f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 17:40:20 -0700 Subject: [PATCH 28/71] Use native split thread header title --- .../features/threads/ThreadRouteScreen.tsx | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 2b120c8b86e..78a7e225cf5 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -43,7 +43,7 @@ import { } from "../terminal/terminalLaunchContext"; import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen } from "./ThreadDetailScreen"; -import { ThreadGitControls, useThreadGitRightHeaderItems } from "./ThreadGitControls"; +import { ThreadGitControls, useThreadGitCenterHeaderItems } from "./ThreadGitControls"; import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -619,7 +619,7 @@ function ThreadRouteContent( onPull: gitActions.onPullSelectedThreadBranch, onRunAction: gitActions.onRunSelectedThreadGitAction, }; - const threadRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); + const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); if (!environmentId || !threadId) { return ; @@ -653,6 +653,14 @@ function ThreadRouteContent( headerShadowVisible: false, }), headerTintColor: iconColor, + headerTitle: selectedThread.title, + headerTitleStyle: usesNativeHeaderGlass + ? { + fontSize: 17, + fontWeight: "800", + } + : undefined, + title: selectedThread.title, headerBackVisible: !layout.usesSplitView, headerBackTitle: "", scrollEdgeEffects: { @@ -670,22 +678,24 @@ function ThreadRouteContent( placement: "integratedButton", } : undefined, - unstable_headerRightItems: + unstable_headerCenterItems: layout.usesSplitView && Platform.OS === "ios" && !activeInspectorRenderer - ? () => threadRightHeaderItems + ? () => threadCenterHeaderItems : undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> - - - + {layout.usesSplitView ? null : ( + + + + )} Date: Mon, 29 Jun 2026 17:50:41 -0700 Subject: [PATCH 29/71] Align iPad detail header with Mail --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 78a7e225cf5..8f9af0135a1 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -653,14 +653,14 @@ function ThreadRouteContent( headerShadowVisible: false, }), headerTintColor: iconColor, - headerTitle: selectedThread.title, + headerTitle: layout.usesSplitView ? "" : selectedThread.title, headerTitleStyle: usesNativeHeaderGlass ? { fontSize: 17, fontWeight: "800", } : undefined, - title: selectedThread.title, + title: layout.usesSplitView ? "" : selectedThread.title, headerBackVisible: !layout.usesSplitView, headerBackTitle: "", scrollEdgeEffects: { From 89139e939450543d0fa4fc7989d16fc36ce0a5cd Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 18:28:10 -0700 Subject: [PATCH 30/71] Stabilize iPad thread header controls --- .../features/threads/ThreadRouteScreen.tsx | 88 +++++++++++++------ 1 file changed, 61 insertions(+), 27 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 8f9af0135a1..85698b824a3 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,4 +1,5 @@ import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; +import type { NativeStackNavigationOptions } from "expo-router/build/react-navigation/native-stack/types"; import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; import * as Option from "effect/Option"; import { EnvironmentId, ThreadId, type ProjectScript } from "@t3tools/contracts"; @@ -43,7 +44,7 @@ import { } from "../terminal/terminalLaunchContext"; import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen } from "./ThreadDetailScreen"; -import { ThreadGitControls, useThreadGitCenterHeaderItems } from "./ThreadGitControls"; +import { ThreadGitControls, useThreadGitRightHeaderItems } from "./ThreadGitControls"; import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -59,7 +60,6 @@ import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, } from "../layout/AdaptiveWorkspaceLayout"; -import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; import { ThreadFileNavigatorPane } from "../files/thread-file-navigator-pane"; import { ThreadInspectorContentStack, @@ -72,6 +72,10 @@ interface ThreadInspectorSelection { readonly mode: ThreadInspectorMode; } +type NativeHeaderItems = ReturnType< + NonNullable +>; + const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); function InspectorPaneRoleActivation() { @@ -213,8 +217,14 @@ function ThreadRouteContent( readonly selectedThreadDetailState: ReturnType; }, ) { - const { fileInspector, layout, panes, showAuxiliaryPane, toggleAuxiliaryPane } = - useAdaptiveWorkspaceLayout(); + const { + fileInspector, + layout, + panes, + showAuxiliaryPane, + toggleAuxiliaryPane, + togglePrimarySidebar, + } = useAdaptiveWorkspaceLayout(); const { connectionState } = useRemoteConnectionStatus(); const { onReconnectEnvironment } = useRemoteConnections(); const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = @@ -619,7 +629,48 @@ function ThreadRouteContent( onPull: gitActions.onPullSelectedThreadBranch, onRunAction: gitActions.onRunSelectedThreadGitAction, }; - const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); + const threadRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); + const splitLeftHeaderItems = useMemo( + () => [ + ...(props.onReturnToThread + ? [ + { + accessibilityLabel: "Return to chat", + icon: { name: "chevron.left", type: "sfSymbol" as const }, + identifier: "thread-left-return", + onPress: props.onReturnToThread, + sharesBackground: true, + type: "button" as const, + width: 58, + }, + ] + : []), + { + accessibilityLabel: panes.primarySidebarVisible + ? "Maximize content" + : "Show thread sidebar", + icon: { + name: panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left", + type: "sfSymbol" as const, + }, + identifier: "thread-left-sidebar", + onPress: togglePrimarySidebar, + sharesBackground: true, + type: "button" as const, + width: 58, + }, + { + accessibilityLabel: "New task", + icon: { name: "square.and.pencil", type: "sfSymbol" as const }, + identifier: "thread-left-new-task", + onPress: () => router.push("/new"), + sharesBackground: true, + type: "button" as const, + width: 58, + }, + ], + [panes.primarySidebarVisible, props.onReturnToThread, router, togglePrimarySidebar], + ); if (!environmentId || !threadId) { return ; @@ -678,9 +729,11 @@ function ThreadRouteContent( placement: "integratedButton", } : undefined, - unstable_headerCenterItems: - layout.usesSplitView && Platform.OS === "ios" && !activeInspectorRenderer - ? () => threadCenterHeaderItems + unstable_headerLeftItems: + layout.usesSplitView && Platform.OS === "ios" ? () => splitLeftHeaderItems : undefined, + unstable_headerRightItems: + layout.usesSplitView && Platform.OS === "ios" + ? () => threadRightHeaderItems : undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} @@ -697,25 +750,6 @@ function ThreadRouteContent( )} - router.push("/new")} - separateBackground - /> - } - > - {props.onReturnToThread ? ( - - ) : null} - - From 2f4fdfa03ec613d602974ef735882ad6fe1a26fa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 18:39:43 -0700 Subject: [PATCH 31/71] Align iPad thread controls with Mail --- .../features/threads/ThreadGitControls.tsx | 6 +++- .../features/threads/ThreadRouteScreen.tsx | 28 ++++++++++++------- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 0c7930eaca7..428b560717a 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -400,7 +400,11 @@ export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): Hea } export function useThreadGitCenterHeaderItems(props: ThreadGitControlsProps): HeaderItems { - return useThreadGitHeaderActionItems(props); + const actionItems = useThreadGitHeaderActionItems(props); + return useMemo( + () => [actionItems[1], actionItems[2], actionItems[0]].filter(Boolean) as HeaderItems, + [actionItems], + ); } export function ThreadGitControls(props: ThreadGitControlsProps) { diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 85698b824a3..79feffee7ee 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -44,7 +44,7 @@ import { } from "../terminal/terminalLaunchContext"; import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen } from "./ThreadDetailScreen"; -import { ThreadGitControls, useThreadGitRightHeaderItems } from "./ThreadGitControls"; +import { ThreadGitControls, useThreadGitCenterHeaderItems } from "./ThreadGitControls"; import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -629,7 +629,7 @@ function ThreadRouteContent( onPull: gitActions.onPullSelectedThreadBranch, onRunAction: gitActions.onRunSelectedThreadGitAction, }; - const threadRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); + const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); const splitLeftHeaderItems = useMemo( () => [ ...(props.onReturnToThread @@ -639,7 +639,7 @@ function ThreadRouteContent( icon: { name: "chevron.left", type: "sfSymbol" as const }, identifier: "thread-left-return", onPress: props.onReturnToThread, - sharesBackground: true, + sharesBackground: false, type: "button" as const, width: 58, }, @@ -655,7 +655,7 @@ function ThreadRouteContent( }, identifier: "thread-left-sidebar", onPress: togglePrimarySidebar, - sharesBackground: true, + sharesBackground: false, type: "button" as const, width: 58, }, @@ -664,12 +664,24 @@ function ThreadRouteContent( icon: { name: "square.and.pencil", type: "sfSymbol" as const }, identifier: "thread-left-new-task", onPress: () => router.push("/new"), - sharesBackground: true, + sharesBackground: false, type: "button" as const, width: 58, }, + { + flexible: true, + spacing: 0, + type: "spacing" as const, + }, + ...threadCenterHeaderItems, + ], + [ + panes.primarySidebarVisible, + props.onReturnToThread, + router, + threadCenterHeaderItems, + togglePrimarySidebar, ], - [panes.primarySidebarVisible, props.onReturnToThread, router, togglePrimarySidebar], ); if (!environmentId || !threadId) { @@ -731,10 +743,6 @@ function ThreadRouteContent( : undefined, unstable_headerLeftItems: layout.usesSplitView && Platform.OS === "ios" ? () => splitLeftHeaderItems : undefined, - unstable_headerRightItems: - layout.usesSplitView && Platform.OS === "ios" - ? () => threadRightHeaderItems - : undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> From 6c96dc7e9274ccb865ad0549aba6e623be8b2d05 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 19:02:21 -0700 Subject: [PATCH 32/71] Nudge iPad detail header controls inward --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 79feffee7ee..1314e33f31a 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -632,6 +632,12 @@ function ThreadRouteContent( const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); const splitLeftHeaderItems = useMemo( () => [ + { + // Match Mail's split-view detail toolbar: the first detail action sits + // inside the content pane, not flush against the sidebar divider. + spacing: 18, + type: "spacing" as const, + }, ...(props.onReturnToThread ? [ { From 29ffec87c0e933ce025190aa37d73c4c96ab41f4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 19:57:07 -0700 Subject: [PATCH 33/71] Hide split detail native title --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 1314e33f31a..2618e6a021b 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -722,7 +722,7 @@ function ThreadRouteContent( headerShadowVisible: false, }), headerTintColor: iconColor, - headerTitle: layout.usesSplitView ? "" : selectedThread.title, + headerTitle: layout.usesSplitView ? () => null : selectedThread.title, headerTitleStyle: usesNativeHeaderGlass ? { fontSize: 17, From 036e481ec0bd26b49d32f0dd3a1c12c4c5743112 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 22:12:22 -0700 Subject: [PATCH 34/71] Fix native iOS thread header integration --- .../features/threads/ThreadGitControls.tsx | 188 +++++++++--------- .../features/threads/ThreadRouteScreen.tsx | 22 +- patches/expo-router@56.2.11.patch | 27 ++- pnpm-lock.yaml | 44 ++-- 4 files changed, 151 insertions(+), 130 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 428b560717a..df170b24ca2 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -426,115 +426,111 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { /> ) : null} {showActionControls ? ( - <> - - {props.projectScripts.length > 0 ? ( - props.projectScripts.map((script) => ( - void props.onRunProjectScript(script)} - subtitle={script.command} - > - {projectScriptMenuLabel(script)} - - )) - ) : ( + + {props.projectScripts.length > 0 ? ( + props.projectScripts.map((script) => ( {}} - subtitle="This project has no saved scripts yet" + key={script.id} + icon={projectScriptMenuIcon(script.icon)} + onPress={() => void props.onRunProjectScript(script)} + subtitle={script.command} > - No project scripts + {projectScriptMenuLabel(script)} - )} - {props.terminalSessions.map((session) => ( - props.onOpenTerminal(session.terminalId)} - subtitle={[ - getTerminalStatusLabel({ - status: session.status, - hasRunningSubprocess: session.hasRunningSubprocess, - }), - basename(session.cwd), - ] - .filter(Boolean) - .join(" · ")} - > - {session.displayLabel} - - ))} + )) + ) : ( - Open new terminal - - - {props.showDirectFileControl ? ( - - ) : null} - - {}} - subtitle={compactMenuStatus(props.gitStatus)} + subtitle="This project has no saved scripts yet" > - - {compactMenuBranchLabel(model.currentBranchLabel)} - + No project scripts + )} + {props.terminalSessions.map((session) => ( void model.runQuickAction()} - subtitle={model.quickActionHint ?? undefined} + key={session.terminalId} + icon="terminal" + onPress={() => props.onOpenTerminal(session.terminalId)} + subtitle={[ + getTerminalStatusLabel({ + status: session.status, + hasRunningSubprocess: session.hasRunningSubprocess, + }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · ")} > - {model.quickAction.label} + {session.displayLabel} - - Review changes - - - Files - - - More - - - + ))} + + Open new terminal + + + ) : null} + {showActionControls && props.showDirectFileControl ? ( + ) : null} - {props.showSearchSlot ? ( - <> - - - + {showActionControls ? ( + + {}} + subtitle={compactMenuStatus(props.gitStatus)} + > + + {compactMenuBranchLabel(model.currentBranchLabel)} + + + void model.runQuickAction()} + subtitle={model.quickActionHint ?? undefined} + > + {model.quickAction.label} + + + Review changes + + + Files + + + More + + ) : null} + {props.showSearchSlot ? : null} + {props.showSearchSlot ? : null} ); } diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 2618e6a021b..d9b57608bb1 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -674,20 +674,8 @@ function ThreadRouteContent( type: "button" as const, width: 58, }, - { - flexible: true, - spacing: 0, - type: "spacing" as const, - }, - ...threadCenterHeaderItems, - ], - [ - panes.primarySidebarVisible, - props.onReturnToThread, - router, - threadCenterHeaderItems, - togglePrimarySidebar, ], + [panes.primarySidebarVisible, props.onReturnToThread, router, togglePrimarySidebar], ); if (!environmentId || !threadId) { @@ -749,11 +737,17 @@ function ThreadRouteContent( : undefined, unstable_headerLeftItems: layout.usesSplitView && Platform.OS === "ios" ? () => splitLeftHeaderItems : undefined, + unstable_headerCenterItems: + layout.usesSplitView && Platform.OS === "ios" + ? () => threadCenterHeaderItems + : undefined, + unstable_headerSubtitle: + usesNativeHeaderGlass && !layout.usesSplitView ? headerSubtitle : undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> - {layout.usesSplitView ? null : ( + {layout.usesSplitView || usesNativeHeaderGlass ? null : ( NativeStackHeaderItem[]; @@ -24,6 +24,14 @@ index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..78929fe237b7f9d7808f8de2b844e822 + */ + unstable_headerToolbarItems?: (props: NativeStackHeaderItemProps) => NonNullable; + /** ++ * String displayed below the compact navigation title on iOS 26+. ++ * ++ * This is an unstable API and might change in the future. ++ * ++ * @platform ios ++ */ ++ unstable_headerSubtitle?: ScreenStackHeaderConfigProps['subtitle']; ++ /** + * Native iOS navigation item style to apply to the header. + * + * This is an unstable API and might change in the future. @@ -34,7 +42,7 @@ index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..78929fe237b7f9d7808f8de2b844e822 /** * String or a function that returns a React Element to be used by the header. * Defaults to screen `title` or route name. -@@ -1115,7 +1140,8 @@ export type NativeStackHeaderItemCustom = { +@@ -1115,7 +1148,8 @@ export type NativeStackHeaderItemCustom = { * if the items don't fit the available space, they will be collapsed into a menu automatically. * Items with `type: 'custom'` will not be included in this automatic collapsing behavior. */ @@ -53,7 +61,7 @@ index 218c4291fdb57600a45d32fbcefe3707a520fe79..5fdbb2754755a27589ad1555eae892b5 route: Route; }; -export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }: Props): ScreenStackHeaderConfigProps; -+export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }: Props): ScreenStackHeaderConfigProps; ++export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_headerSubtitle: headerSubtitle, unstable_navigationItemStyle: navigationItemStyle, }: Props): ScreenStackHeaderConfigProps; export {}; //# sourceMappingURL=useHeaderConfigProps.d.ts.map diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.js b/build/react-navigation/native-stack/views/useHeaderConfigProps.js @@ -75,27 +83,32 @@ index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..9d839442e00e136204630663f207a45c }; }; -function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }) { -+function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_navigationItemStyle: navigationItemStyle, }) { ++function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_headerSubtitle: headerSubtitle, unstable_navigationItemStyle: navigationItemStyle, }) { const { direction } = (0, native_1.useLocale)(); const { colors, fonts, dark } = (0, native_1.useTheme)(); const tintColor = headerTintColor ?? (react_native_1.Platform.OS === 'ios' ? colors.primary : colors.text); -@@ -216,6 +219,10 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac +@@ -216,6 +219,14 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac tintColor, canGoBack, }); + const centerItems = headerCenterItems?.({ + tintColor, + canGoBack, ++ }); ++ const toolbarItems = headerToolbarItems?.({ ++ tintColor, ++ canGoBack, + }); if (rightItems) { // iOS renders right items in reverse order // So we need to reverse them here to match the order -@@ -270,6 +277,9 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac +@@ -270,6 +281,10 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac children, headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts), headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), ++ subtitle: headerSubtitle, + headerCenterBarButtonItems: processBarButtonItems(centerItems, colors, fonts), -+ headerToolbarItems, ++ headerToolbarItems: toolbarItems, + navigationItemStyle, experimental_userInterfaceStyle: dark ? 'dark' : 'light', }; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 80d36c1cf98..1b3a615d058 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,15 +66,33 @@ overrides: packageExtensionsChecksum: sha256-FV3+2NW/9MFbunJaPsMxvO0LAzyfccoPtPiKoIXAwUU= patchedDependencies: - '@effect/vitest@4.0.0-beta.78': 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f - '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 - '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 - '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a - effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 - expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f - expo-router@56.2.11: d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c - react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - react-native-screens@4.25.2: 9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b + '@effect/vitest@4.0.0-beta.78': + hash: 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f + path: patches/@effect__vitest@4.0.0-beta.78.patch + '@expo/metro-config@56.0.14': + hash: 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 + path: patches/@expo%2Fmetro-config@56.0.14.patch + '@ff-labs/fff-node@0.9.4': + hash: 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 + path: patches/@ff-labs__fff-node@0.9.4.patch + '@pierre/diffs@1.3.0-beta.5': + hash: 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a + path: patches/@pierre%2Fdiffs@1.3.0-beta.5.patch + effect@4.0.0-beta.78: + hash: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 + path: patches/effect@4.0.0-beta.78.patch + expo-modules-jsi@56.0.10: + hash: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f + path: patches/expo-modules-jsi@56.0.10.patch + expo-router@56.2.11: + hash: 3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8 + path: patches/expo-router@56.2.11.patch + react-native-nitro-modules@0.35.9: + hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 + path: patches/react-native-nitro-modules@0.35.9.patch + react-native-screens@4.25.2: + hash: 9a555061147ecd74107ca21a77697a0515cd6a9359abbe4baf8b44295565e61b + path: patches/react-native-screens@4.25.2.patch importers: @@ -309,7 +327,7 @@ importers: version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a) + version: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -12003,7 +12021,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a) + expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12301,7 +12319,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a) + expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16217,7 +16235,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(patch_hash=d63025d5ca8999d61e090361acb374aebe9917ae1da7c4d65c2cc44977bd844c)(708fd680682731bfd53a5282341dd27a): + expo-router@56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) From fdac1fbcd4f6fe87ff5499917a7d60a099aaab88 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 22:30:20 -0700 Subject: [PATCH 35/71] Use Expo Router native home toolbar --- apps/mobile/src/app/index.tsx | 33 +++---- apps/mobile/src/features/home/HomeHeader.tsx | 33 ++++--- apps/mobile/src/features/home/HomeScreen.tsx | 92 +------------------- 3 files changed, 36 insertions(+), 122 deletions(-) diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index cc1b9632d9f..aefb87f70af 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -2,7 +2,6 @@ import * as Arr from "effect/Array"; import * as Order from "effect/Order"; import { Stack, useRouter } from "expo-router"; import { useMemo, useState } from "react"; -import { Platform } from "react-native"; import { useProjects, useThreadShells } from "../state/entities"; import { useWorkspaceState } from "../state/workspace"; @@ -73,24 +72,20 @@ export default function HomeRouteScreen() { return ( <> - {Platform.OS === "ios" ? ( - - ) : ( - router.push("/settings")} - onProjectGroupingModeChange={setProjectGroupingMode} - onProjectSortOrderChange={setProjectSortOrder} - onSearchQueryChange={setSearchQuery} - onStartNewTask={() => router.push("/new")} - onThreadSortOrderChange={setThreadSortOrder} - /> - )} + router.push("/settings")} + onProjectGroupingModeChange={setProjectGroupingMode} + onProjectSortOrderChange={setProjectSortOrder} + onSearchQueryChange={setSearchQuery} + onStartNewTask={() => router.push("/new")} + onThreadSortOrderChange={setThreadSortOrder} + /> { - props.onSearchQueryChange(""); - }, - onChangeText: (event) => { - props.onSearchQueryChange(event.nativeEvent.text); - }, - }, + headerSearchBarOptions: + Platform.OS === "ios" + ? undefined + : { + ref: searchBarRef, + allowToolbarIntegration: true, + hideNavigationBar: false, + placeholder: "Search", + onCancelButtonPress: () => { + props.onSearchQueryChange(""); + }, + onChangeText: (event) => { + props.onSearchQueryChange(event.nativeEvent.text); + }, + }, }} /> diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 22a23a915d4..800ad5484d7 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -26,9 +26,7 @@ import Animated, { withTiming, } from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; import { AppText as Text } from "../../components/AppText"; import { EmptyState } from "../../components/EmptyState"; @@ -37,11 +35,7 @@ import type { WorkspaceState } from "../../state/workspaceModel"; import type { SavedRemoteConnection } from "../../lib/connection"; import { relativeTime } from "../../lib/time"; import { threadStatusTone } from "../threads/threadPresentation"; -import { - buildHomeListFilterMenu, - type HomeListFilterMenuEnvironment, -} from "./home-list-filter-menu"; -import { hasCustomHomeListOptions } from "./home-list-options"; +import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; import { ThreadSwipeable } from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; @@ -379,21 +373,6 @@ export function HomeScreen(props: HomeScreenProps) { const insets = useSafeAreaInsets(); const { height: windowHeight } = useWindowDimensions(); const accentColor = useThemeColor("--color-icon-muted"); - const foregroundColor = useThemeColor("--color-foreground"); - const screenColor = useThemeColor("--color-screen"); - const hasCustomListOptions = hasCustomHomeListOptions(props); - const filterMenu = buildHomeListFilterMenu({ - environments: props.environments, - selectedEnvironmentId: props.selectedEnvironmentId, - projectSortOrder: props.projectSortOrder, - threadSortOrder: props.threadSortOrder, - projectGroupingMode: props.projectGroupingMode, - onEnvironmentChange: props.onEnvironmentChange, - onProjectSortOrderChange: props.onProjectSortOrderChange, - onThreadSortOrderChange: props.onThreadSortOrderChange, - onProjectGroupingModeChange: props.onProjectGroupingModeChange, - onOpenSettings: props.onOpenSettings, - }); const toggleExpanded = useCallback((key: string) => { setExpandedProjects((prev) => { const next = new Set(prev); @@ -573,75 +552,6 @@ export function HomeScreen(props: HomeScreenProps) { ) : null; - if (Platform.OS === "ios") { - return ( - - - {scrollView} - {connectionStatus} - ["headerRightBarButtonItems"] - } - headerToolbarItems={ - [ - { - composeButtonId: "home-new-task", - composeSystemImageName: "square.and.pencil", - filterButtonId: "home-filter", - filterMenu, - filterSystemImageName: hasCustomListOptions - ? "line.3.horizontal.decrease.circle.fill" - : "line.3.horizontal.decrease", - onComposePress: props.onStartNewTask, - onSearchTextChange: props.onSearchQueryChange, - placeholder: "Search", - searchTextChangeId: "home-search-text", - type: "mailSearchToolbar", - useFallbackSearchField: true, - }, - ] as ComponentProps["headerToolbarItems"] - } - hideBackButton - hideShadow={false} - largeTitle={false} - navigationItemStyle="editor" - title="Threads" - titleColor={foregroundColor} - titleFontSize={18} - titleFontWeight="800" - translucent - /> - - - ); - } - return ( <> {scrollView} From 051160cfecbbc32ff8a78a4de2a43d72457f0017 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 22:51:23 -0700 Subject: [PATCH 36/71] Simplify iPhone thread native header --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index d9b57608bb1..55de057af69 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -741,8 +741,7 @@ function ThreadRouteContent( layout.usesSplitView && Platform.OS === "ios" ? () => threadCenterHeaderItems : undefined, - unstable_headerSubtitle: - usesNativeHeaderGlass && !layout.usesSplitView ? headerSubtitle : undefined, + unstable_headerSubtitle: undefined, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> From 9e52f110e2121908465d97c4b77a122009bf0d92 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:05:17 -0700 Subject: [PATCH 37/71] Use native glass header for terminal route --- .../terminal/ThreadTerminalRouteScreen.tsx | 118 ++++++++++++------ .../src/lib/native-scroll-edge-effect.test.ts | 6 +- 2 files changed, 85 insertions(+), 39 deletions(-) diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 8e9a47a58b5..387c2afe3e4 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -3,7 +3,7 @@ import { type KnownTerminalSession } from "@t3tools/client-runtime/state/termina import { SymbolView } from "expo-symbols"; import { Stack, useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Pressable, Text as RNText, View, useColorScheme } from "react-native"; +import { Platform, Pressable, Text as RNText, View, useColorScheme } from "react-native"; import { KeyboardController, KeyboardEvents, @@ -25,6 +25,7 @@ import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useWorkspaceState } from "../../state/workspace"; import { buildThreadTerminalNavigation } from "../../lib/routes"; +import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useAttachedTerminalSession, @@ -72,6 +73,7 @@ import { const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; const TERMINAL_ACCESSORY_HEIGHT = 52; +const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); type PendingModifier = "ctrl" | "meta"; type HostPlatform = "mac" | "linux" | "windows" | "unknown"; @@ -86,6 +88,47 @@ type TerminalToolbarAction = readonly modifier: PendingModifier; }; +function TerminalHeaderTitle(props: { + readonly topLine: string; + readonly bottomLine: string; + readonly foreground: string; + readonly mutedForeground: string; +}) { + return ( + + + {props.topLine} + + + {props.bottomLine} + + + ); +} + function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { return value[0] ?? null; @@ -404,6 +447,7 @@ export function ThreadTerminalRouteScreen() { ); const terminalTheme = getPierreTerminalTheme(appearanceScheme); + const usesNativeHeaderGlass = Platform.OS === "ios"; const pendingModifier = pendingModifierState.terminalId === terminalId ? pendingModifierState.value : null; const headerTitle = useMemo(() => { @@ -422,6 +466,22 @@ export function ThreadTerminalRouteScreen() { selectedThreadProject?.title, selectedThreadProject?.workspaceRoot, ]); + const renderTerminalHeaderTitle = useCallback( + () => ( + + ), + [ + headerTitle.bottomLine, + headerTitle.topLine, + terminalTheme.foreground, + terminalTheme.mutedForeground, + ], + ); const terminalToolbarActions = useMemo>(() => { const modifierActions: ReadonlyArray = hostPlatform === "mac" @@ -903,44 +963,30 @@ export function ThreadTerminalRouteScreen() { headerShown: true, headerBackButtonDisplayMode: "minimal", headerBackTitle: "", + headerTransparent: usesNativeHeaderGlass, headerShadowVisible: false, - headerStyle: { backgroundColor: terminalTheme.background }, + headerStyle: { + backgroundColor: usesNativeHeaderGlass ? "transparent" : terminalTheme.background, + }, headerTintColor: terminalTheme.foreground, headerTitleAlign: "center", - title: "", - headerTitle: () => ( - - - {headerTitle.topLine} - - - {headerTitle.bottomLine} - - - ), + headerTitle: usesNativeHeaderGlass ? headerTitle.topLine : renderTerminalHeaderTitle, + headerTitleStyle: usesNativeHeaderGlass + ? { + fontSize: 17, + fontWeight: "800", + } + : undefined, + scrollEdgeEffects: usesNativeHeaderGlass + ? { + top: TOP_SCROLL_EDGE_EFFECT, + bottom: "hidden", + left: "hidden", + right: "hidden", + } + : undefined, + title: headerTitle.topLine, + unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} /> diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts b/apps/mobile/src/lib/native-scroll-edge-effect.test.ts index 953ea5213d3..e2872a67c55 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts +++ b/apps/mobile/src/lib/native-scroll-edge-effect.test.ts @@ -7,9 +7,9 @@ describe("nativeTopScrollEdgeEffect", () => { expect(nativeTopScrollEdgeEffect("ios", "26.5")).toBe("automatic"); }); - it("uses the native hard treatment on iOS 27 and later", () => { - expect(nativeTopScrollEdgeEffect("ios", "27.0")).toBe("hard"); - expect(nativeTopScrollEdgeEffect("ios", 28)).toBe("hard"); + it("uses the softer native treatment on iOS 27 and later", () => { + expect(nativeTopScrollEdgeEffect("ios", "27.0")).toBe("soft"); + expect(nativeTopScrollEdgeEffect("ios", 28)).toBe("soft"); }); it("does not apply the iOS workaround to other platforms", () => { From 2c00d79902ebbcda9ecd3cf6cf7ac57fc8f49526 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:16:01 -0700 Subject: [PATCH 38/71] Use shared native scroll edge effect in thread surfaces --- apps/mobile/src/features/threads/ThreadFeed.tsx | 5 ++++- apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx | 4 +++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 745fc1770b8..9c661490253 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -25,6 +25,7 @@ import { ActivityIndicator, Image, Linking, + Platform, type LayoutChangeEvent, type NativeScrollEvent, type NativeSyntheticEvent, @@ -68,6 +69,7 @@ import { import { buildReviewParsedDiff } from "../review/reviewModel"; import { cn } from "../../lib/cn"; import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout"; +import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; import { buildThreadFilesNavigation } from "../../lib/routes"; import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; @@ -86,6 +88,7 @@ const MESSAGE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", }); +const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); function formatMessageTime(input: string): string { const timestamp = Date.parse(input); @@ -1499,7 +1502,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { From 0f3201e699a00ca64ca512e69825d1a5a9d5c3ae Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:32:06 -0700 Subject: [PATCH 39/71] Use native edge chrome on file and review routes --- .../src/features/files/ThreadFilesRouteScreen.tsx | 10 ++++++++++ apps/mobile/src/features/review/ReviewSheet.tsx | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 662bcb4dca6..a97443dfa2e 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -4,6 +4,7 @@ import { useLocalSearchParams, useRouter } from "expo-router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, + Platform, Pressable, ScrollView, Text as RNText, @@ -24,6 +25,7 @@ import { EmptyState } from "../../components/EmptyState"; import { LoadingScreen } from "../../components/LoadingScreen"; import { cn } from "../../lib/cn"; import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation"; +import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { buildThreadFilesNavigation, buildThreadRoutePath } from "../../lib/routes"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; @@ -58,6 +60,7 @@ import { import { useWorkspaceFileAssetUrl } from "./workspaceFileAssetUrl"; type FileViewMode = "preview" | "source"; +const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -579,6 +582,13 @@ export function ThreadFilesTreeScreen() { headerStyle: { backgroundColor: "transparent" }, headerShadowVisible: false, headerTitle: renderHeaderTitle, + scrollEdgeEffects: { + top: TOP_SCROLL_EDGE_EFFECT, + bottom: "hidden", + left: "hidden", + right: "hidden", + }, + unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, headerSearchBarOptions: { allowToolbarIntegration: true, autoCapitalize: "none", diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 82c7b5b8796..e62baf44e53 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -17,6 +17,7 @@ import { import { ActivityIndicator, FlatList, + Platform, Pressable, ScrollView, type NativeSyntheticEvent, @@ -31,6 +32,7 @@ import { AppText as Text } from "../../components/AppText"; import { environmentCatalog } from "../../connection/catalog"; import { useEnvironmentPresentation } from "../../state/presentation"; import { useAtomCommand } from "../../state/use-atom-command"; +import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; @@ -59,6 +61,8 @@ import { resolveReviewAvailability } from "./reviewAvailability"; import { resolveSelectedReviewFileId } from "./reviewPaneSelection"; import { buildReviewSectionMenu } from "./review-section-menu"; +const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); + const REVIEW_HEADER_SPACING = 0; const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { @@ -589,6 +593,13 @@ export function ReviewSheet() { backgroundColor: "transparent", }, headerTitle: renderHeaderTitle, + scrollEdgeEffects: { + top: TOP_SCROLL_EDGE_EFFECT, + bottom: "hidden", + left: "hidden", + right: "hidden", + }, + unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, }} /> From 3e4b357a86d47a24033f966a479f8ebb59913272 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:50:55 -0700 Subject: [PATCH 40/71] Add native sidebar status subtitle --- .../threads/ThreadNavigationSidebar.tsx | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 016cce0c64e..179bee9843d 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -20,6 +20,7 @@ import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; import { useWorkspaceState } from "../../state/workspace"; +import type { WorkspaceState } from "../../state/workspaceModel"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { @@ -48,6 +49,16 @@ const SIDEBAR_HEADER_WASH_OPACITY = { light: [0.46, 0.3, 0.08], } as const; +function sidebarConnectionStatusLabel(state: WorkspaceState): string { + if (state.networkStatus === "offline") return "Offline"; + if (state.connectionState === "connected") return "Ready"; + if (state.connectionState === "connecting") return "Connecting"; + if (state.connectionState === "reconnecting") return "Reconnecting"; + if (state.connectionState === "error") return "Error"; + if (!state.hasConnections) return "No environments"; + return "Not connected"; +} + const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly backgroundColor: ColorValue; readonly foregroundColor: ColorValue; @@ -278,6 +289,35 @@ export function ThreadNavigationSidebar(props: { [groups], ); const showsConnectionStatus = shouldShowWorkspaceConnectionStatus(catalogState); + const selectedThread = useMemo(() => { + if (props.selectedThreadKey === null) return null; + return ( + threads.find( + (thread) => scopedThreadKey(thread.environmentId, thread.id) === props.selectedThreadKey, + ) ?? null + ); + }, [props.selectedThreadKey, threads]); + const selectedProject = useMemo(() => { + if (selectedThread === null) return null; + return ( + projects.find( + (project) => + project.environmentId === selectedThread.environmentId && + project.id === selectedThread.projectId, + ) ?? null + ); + }, [projects, selectedThread]); + const selectedEnvironmentLabel = + options.selectedEnvironmentId === null + ? null + : (environments.find( + (environment) => environment.environmentId === options.selectedEnvironmentId, + )?.label ?? null); + const sidebarScopeLabel = + selectedProject?.title ?? + selectedEnvironmentLabel ?? + (projects.length === 1 ? projects[0]!.title : "All projects"); + const nativeSidebarSubtitle = `${sidebarScopeLabel} · ${sidebarConnectionStatusLabel(catalogState)}`; const listMenuActions = useMemo( () => [ { @@ -657,6 +697,7 @@ export function ThreadNavigationSidebar(props: { hideShadow={false} headerRightBarButtonItems={nativeHeaderRightBarButtonItems} navigationItemStyle="editor" + subtitle={nativeSidebarSubtitle} title="Threads" titleColor={foregroundColor} titleFontSize={17} From a0e0b6ef712cc297f22d222d251a7c3d152e305f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:01:16 -0700 Subject: [PATCH 41/71] Add split home compose toolbar button --- apps/mobile/src/app/index.tsx | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index aefb87f70af..fcee1344f89 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -64,7 +64,16 @@ export default function HomeRouteScreen() { headerTitle: "", }} /> - + router.push("/new")} + separateBackground + /> + } + /> ); From f5d4d7b63c3b6055a0cc3cd44de49b389a50b8b5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:18:38 -0700 Subject: [PATCH 42/71] Stop forcing native glass debug route --- apps/mobile/src/app/_layout.tsx | 52 +---- .../archive/ArchivedThreadsScreen.tsx | 186 ++++++++++++------ 2 files changed, 140 insertions(+), 98 deletions(-) diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx index 785bc0b441f..159bd509743 100644 --- a/apps/mobile/src/app/_layout.tsx +++ b/apps/mobile/src/app/_layout.tsx @@ -4,17 +4,10 @@ import { DMSans_700Bold, useFonts, } from "@expo-google-fonts/dm-sans"; -import { usePathname, useRouter } from "expo-router"; +import { usePathname } from "expo-router"; import Stack from "expo-router/stack"; -import { useCallback, useEffect } from "react"; -import { - LogBox, - Platform, - Settings, - StatusBar, - useColorScheme, - useWindowDimensions, -} from "react-native"; +import { useCallback } from "react"; +import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; @@ -39,16 +32,7 @@ import { deriveStableFormSheetDetent } from "../lib/layout"; import { useThemeColor } from "../lib/useThemeColor"; import { HardwareKeyboardCommandProvider } from "../features/keyboard/HardwareKeyboardCommandProvider"; -const debugLaunchRoute = - __DEV__ && Platform.OS === "ios" ? Settings.get("T3DebugRoute") : undefined; - -if (debugLaunchRoute !== "/debug/rns-glass") { - require("../../global.css"); -} - -if (debugLaunchRoute === "/debug/rns-glass") { - LogBox.ignoreAllLogs(true); -} +require("../../global.css"); function AppNavigator() { const pathname = usePathname(); @@ -64,24 +48,16 @@ function AppNavigator() { function AppNavigatorContent() { const pathname = usePathname(); - const isDebugRoute = pathname.startsWith("/debug/") || debugLaunchRoute === "/debug/rns-glass"; + const isDebugRoute = pathname.startsWith("/debug/"); if (isDebugRoute) { - return ; + return ; } - return ; + return ; } -function DebugNavigatorHost(props: { readonly pathname: string }) { - const router = useRouter(); - - useEffect(() => { - if (debugLaunchRoute === "/debug/rns-glass" && props.pathname !== "/debug/rns-glass") { - router.replace("/debug/rns-glass" as never); - } - }, [props.pathname, router]); - +function DebugNavigatorHost() { return ( <> @@ -99,22 +75,12 @@ function DebugNavigatorHost(props: { readonly pathname: string }) { ); } -function WorkspaceNavigatorHost(props: { readonly pathname: string }) { +function WorkspaceNavigatorHost() { const colorScheme = useColorScheme(); - const pathname = props.pathname; - const router = useRouter(); const statusBarBg = useThemeColor("--color-status-bar"); useAgentNotificationNavigation(); useThreadOutboxDrain(); - useEffect(() => { - if (!__DEV__ || Platform.OS !== "ios") return; - const debugRoute = Settings.get("T3DebugRoute"); - if (typeof debugRoute !== "string" || debugRoute.length === 0 || pathname === debugRoute) - return; - router.replace(debugRoute as never); - }, [pathname, router]); - return ( <> void; + readonly onRefresh: () => void; readonly onSearchQueryChange: (query: string) => void; readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void; }) { + const { width } = useWindowDimensions(); const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; + const usesNativeMailToolbar = Platform.OS === "ios" && width < 700; + const archiveFilterMenu = { + title: "Archived thread options", + items: [ + { + type: "submenu" as const, + title: "Environment", + items: [ + { + type: "action" as const, + title: "All environments", + state: props.selectedEnvironmentId === null ? ("on" as const) : ("off" as const), + onPress: () => props.onEnvironmentChange(null), + }, + ...props.environments.map((environment) => ({ + type: "action" as const, + title: environment.label, + state: + props.selectedEnvironmentId === environment.environmentId + ? ("on" as const) + : ("off" as const), + onPress: () => props.onEnvironmentChange(environment.environmentId), + })), + ], + }, + { + type: "submenu" as const, + title: "Sort by archived date", + items: [ + { + type: "action" as const, + title: "Newest first", + state: props.sortOrder === "newest" ? ("on" as const) : ("off" as const), + onPress: () => props.onSortOrderChange("newest"), + }, + { + type: "action" as const, + title: "Oldest first", + state: props.sortOrder === "oldest" ? ("on" as const) : ("off" as const), + onPress: () => props.onSortOrderChange("oldest"), + }, + ], + }, + ], + }; return ( <> { - props.onSearchQueryChange(event.nativeEvent.text); - }, - onCancelButtonPress: () => { - props.onSearchQueryChange(""); - }, - }, + headerTransparent: usesNativeMailToolbar, + headerStyle: usesNativeMailToolbar ? { backgroundColor: "transparent" } : undefined, + headerShadowVisible: usesNativeMailToolbar ? false : undefined, + unstable_navigationItemStyle: usesNativeMailToolbar ? "editor" : undefined, + unstable_headerToolbarItems: usesNativeMailToolbar + ? () => [ + { + composeButtonId: "archived-refresh", + composeSystemImageName: "arrow.clockwise", + filterMenu: archiveFilterMenu, + filterButtonId: "archived-filter", + filterSystemImageName: hasCustomFilter + ? "line.3.horizontal.decrease.circle.fill" + : "line.3.horizontal.decrease", + onComposePress: props.onRefresh, + onSearchTextChange: props.onSearchQueryChange, + placeholder: "Search", + searchTextChangeId: "archived-search-text", + type: "mailSearchToolbar", + useFallbackSearchField: true, + }, + ] + : undefined, + headerSearchBarOptions: usesNativeMailToolbar + ? undefined + : { + autoCapitalize: "none", + hideNavigationBar: false, + obscureBackground: false, + placeholder: "Search archived threads", + placement: "stacked", + onChangeText: (event) => { + props.onSearchQueryChange(event.nativeEvent.text); + }, + onCancelButtonPress: () => { + props.onSearchQueryChange(""); + }, + }, }} /> - - - - Environment - props.onEnvironmentChange(null)} - > - All environments - - {props.environments.map((environment) => ( + {usesNativeMailToolbar ? null : ( + + + + Environment props.onEnvironmentChange(environment.environmentId)} + isOn={props.selectedEnvironmentId === null} + onPress={() => props.onEnvironmentChange(null)} > - {environment.label} + All environments - ))} - + {props.environments.map((environment) => ( + props.onEnvironmentChange(environment.environmentId)} + > + {environment.label} + + ))} + - - Sort by archived date - props.onSortOrderChange("newest")} - > - Newest first - - props.onSortOrderChange("oldest")} - > - Oldest first - + + Sort by archived date + props.onSortOrderChange("newest")} + > + Newest first + + props.onSortOrderChange("oldest")} + > + Oldest first + + - - + + )} ); } @@ -361,6 +436,7 @@ export function ArchivedThreadsScreen(props: { Date: Tue, 30 Jun 2026 00:37:24 -0700 Subject: [PATCH 43/71] Match Mail sidebar selection on iPad --- .../features/threads/ThreadGitControls.tsx | 264 +++++++++--------- .../threads/ThreadNavigationSidebar.tsx | 41 ++- 2 files changed, 169 insertions(+), 136 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index df170b24ca2..a8ac116e30a 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -69,6 +69,12 @@ function compactMenuStatus(gitStatus: VcsStatusResult | null): string { type HeaderItems = ReturnType< NonNullable >; +type HeaderItem = HeaderItems[number]; +type ThreadGitHeaderActionItems = { + readonly terminal: HeaderItem; + readonly files: HeaderItem; + readonly git: HeaderItem; +}; type QuickActionIcon = | "arrow.down.circle" | "arrow.up.right.circle" @@ -238,139 +244,138 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { }; } -function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): HeaderItems { +function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGitHeaderActionItems { const model = useThreadGitControlModel(props); return useMemo( - () => - [ - { - accessibilityLabel: "Open terminal", - disabled: !props.canOpenTerminal, - icon: { name: "terminal", type: "sfSymbol" }, - identifier: "thread-right-terminal", - label: "Terminal", - menu: { - items: [ - ...props.projectScripts.map((script) => ({ - description: script.command, - icon: { name: projectScriptMenuIcon(script.icon), type: "sfSymbol" as const }, - label: projectScriptMenuLabel(script), - onPress: () => void props.onRunProjectScript(script), - type: "action" as const, - })), - ...(props.projectScripts.length === 0 - ? [ - { - description: "This project has no saved scripts yet", - disabled: true, - icon: { name: "play", type: "sfSymbol" as const }, - label: "No project scripts", - onPress: () => {}, - type: "action" as const, - }, - ] - : []), - ...props.terminalSessions.map((session) => ({ - description: [ - getTerminalStatusLabel({ - status: session.status, - hasRunningSubprocess: session.hasRunningSubprocess, - }), - basename(session.cwd), + () => ({ + terminal: { + accessibilityLabel: "Open terminal", + disabled: !props.canOpenTerminal, + icon: { name: "terminal", type: "sfSymbol" }, + identifier: "thread-right-terminal", + label: "Terminal", + menu: { + items: [ + ...props.projectScripts.map((script) => ({ + description: script.command, + icon: { name: projectScriptMenuIcon(script.icon), type: "sfSymbol" as const }, + label: projectScriptMenuLabel(script), + onPress: () => void props.onRunProjectScript(script), + type: "action" as const, + })), + ...(props.projectScripts.length === 0 + ? [ + { + description: "This project has no saved scripts yet", + disabled: true, + icon: { name: "play", type: "sfSymbol" as const }, + label: "No project scripts", + onPress: () => {}, + type: "action" as const, + }, ] - .filter(Boolean) - .join(" · "), - icon: { name: "terminal", type: "sfSymbol" as const }, - label: session.displayLabel, - onPress: () => props.onOpenTerminal(session.terminalId), - type: "action" as const, - })), - { - description: "Start another shell for this thread", - icon: { name: "plus", type: "sfSymbol" }, - label: "Open new terminal", - onPress: props.onOpenNewTerminal, - type: "action", - }, - ], - title: "Terminal", - }, - sharesBackground: true, - type: "menu", - variant: "prominent", - width: 58, - }, - { - accessibilityLabel: "Open files", - disabled: !props.canOpenFiles, - icon: { name: "folder", type: "sfSymbol" }, - identifier: "thread-right-files", - label: "Files", - onPress: model.openFiles, - sharesBackground: true, - type: "button", - variant: "prominent", - width: 58, + : []), + ...props.terminalSessions.map((session) => ({ + description: [ + getTerminalStatusLabel({ + status: session.status, + hasRunningSubprocess: session.hasRunningSubprocess, + }), + basename(session.cwd), + ] + .filter(Boolean) + .join(" · "), + icon: { name: "terminal", type: "sfSymbol" as const }, + label: session.displayLabel, + onPress: () => props.onOpenTerminal(session.terminalId), + type: "action" as const, + })), + { + description: "Start another shell for this thread", + icon: { name: "plus", type: "sfSymbol" }, + label: "Open new terminal", + onPress: props.onOpenNewTerminal, + type: "action", + }, + ], + title: "Terminal", }, - { - accessibilityLabel: "Git actions", - icon: { name: "point.topleft.down.curvedto.point.bottomright.up", type: "sfSymbol" }, - identifier: "thread-right-git", - label: "Git", - menu: { - items: [ - { - description: compactMenuStatus(props.gitStatus), - disabled: true, - icon: { - name: "point.topleft.down.curvedto.point.bottomright.up", - type: "sfSymbol", - }, - label: compactMenuBranchLabel(model.currentBranchLabel), - onPress: () => {}, - type: "action", - }, - { - description: model.quickActionHint ?? undefined, - disabled: model.quickAction.disabled, - icon: { name: model.quickActionIcon, type: "sfSymbol" }, - label: model.quickAction.label, - onPress: () => void model.runQuickAction(), - type: "action", - }, - { - description: "Turn diffs and worktree changes", - disabled: !model.isRepo, - icon: { name: "text.bubble", type: "sfSymbol" }, - label: "Review changes", - onPress: model.openReview, - type: "action", - }, - { - description: "Browse this workspace", - disabled: !props.canOpenFiles, - icon: { name: "folder", type: "sfSymbol" }, - label: "Files", - onPress: model.openFiles, - type: "action", + sharesBackground: true, + type: "menu", + variant: "prominent", + width: 58, + }, + files: { + accessibilityLabel: "Open files", + disabled: !props.canOpenFiles, + icon: { name: "folder", type: "sfSymbol" }, + identifier: "thread-right-files", + label: "Files", + onPress: model.openFiles, + sharesBackground: true, + type: "button", + variant: "prominent", + width: 58, + }, + git: { + accessibilityLabel: "Git actions", + icon: { name: "point.topleft.down.curvedto.point.bottomright.up", type: "sfSymbol" }, + identifier: "thread-right-git", + label: "Git", + menu: { + items: [ + { + description: compactMenuStatus(props.gitStatus), + disabled: true, + icon: { + name: "point.topleft.down.curvedto.point.bottomright.up", + type: "sfSymbol", }, - { - description: "Commit, files, branches", - icon: { name: "ellipsis.circle", type: "sfSymbol" }, - label: "More", - onPress: model.openGitInspector, - type: "action", - }, - ], - title: "Git", - }, - sharesBackground: true, - type: "menu", - variant: "prominent", - width: 58, + label: compactMenuBranchLabel(model.currentBranchLabel), + onPress: () => {}, + type: "action", + }, + { + description: model.quickActionHint ?? undefined, + disabled: model.quickAction.disabled, + icon: { name: model.quickActionIcon, type: "sfSymbol" }, + label: model.quickAction.label, + onPress: () => void model.runQuickAction(), + type: "action", + }, + { + description: "Turn diffs and worktree changes", + disabled: !model.isRepo, + icon: { name: "text.bubble", type: "sfSymbol" }, + label: "Review changes", + onPress: model.openReview, + type: "action", + }, + { + description: "Browse this workspace", + disabled: !props.canOpenFiles, + icon: { name: "folder", type: "sfSymbol" }, + label: "Files", + onPress: model.openFiles, + type: "action", + }, + { + description: "Commit, files, branches", + icon: { name: "ellipsis.circle", type: "sfSymbol" }, + label: "More", + onPress: model.openGitInspector, + type: "action", + }, + ], + title: "Git", }, - ] as HeaderItems, + sharesBackground: true, + type: "menu", + variant: "prominent", + width: 58, + }, + }), [ model.currentBranchLabel, model.isRepo, @@ -396,13 +401,16 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): HeaderIte export function useThreadGitRightHeaderItems(props: ThreadGitControlsProps): HeaderItems { const actionItems = useThreadGitHeaderActionItems(props); - return useMemo(() => actionItems.toReversed() as HeaderItems, [actionItems]); + return useMemo( + () => [actionItems.git, actionItems.files, actionItems.terminal] as HeaderItems, + [actionItems], + ); } export function useThreadGitCenterHeaderItems(props: ThreadGitControlsProps): HeaderItems { const actionItems = useThreadGitHeaderActionItems(props); return useMemo( - () => [actionItems[1], actionItems[2], actionItems[0]].filter(Boolean) as HeaderItems, + () => [actionItems.files, actionItems.git, actionItems.terminal] as HeaderItems, [actionItems], ); } diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 179bee9843d..1326b4290bb 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -72,6 +72,9 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly pressedBackgroundColor: ColorValue; readonly selected: boolean; readonly selectedBackgroundColor: ColorValue; + readonly selectedForegroundColor: ColorValue; + readonly selectedMutedColor: ColorValue; + readonly selectedPressedBackgroundColor: ColorValue; readonly simultaneousSwipeGesture?: ComponentProps< typeof ThreadSwipeable >["simultaneousWithExternalGesture"]; @@ -93,10 +96,18 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { pressedBackgroundColor, selected, selectedBackgroundColor, + selectedForegroundColor, + selectedMutedColor, + selectedPressedBackgroundColor, simultaneousSwipeGesture, thread, environmentLabel, } = props; + const effectiveForegroundColor = selected ? selectedForegroundColor : foregroundColor; + const effectiveMutedColor = selected ? selectedMutedColor : mutedColor; + const effectivePressedBackgroundColor = selected + ? selectedPressedBackgroundColor + : pressedBackgroundColor; const handleArchive = useCallback(() => { onArchiveThread(thread); }, [onArchiveThread, thread]); @@ -162,7 +173,8 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { style={({ pressed }) => [ styles.threadSelectionTarget, { - backgroundColor: pressed || hovered ? pressedBackgroundColor : "transparent", + backgroundColor: + pressed || hovered ? effectivePressedBackgroundColor : "transparent", cursor: "pointer", }, ]} @@ -171,7 +183,7 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { {thread.title} @@ -180,12 +192,12 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { {subtitle.join(" · ")} ) : null} - + {relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} @@ -199,10 +211,15 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { hitSlop={6} style={({ pressed }) => [ styles.moreButton, - { backgroundColor: pressed ? pressedBackgroundColor : "transparent" }, + { backgroundColor: pressed ? effectivePressedBackgroundColor : "transparent" }, ]} > - + @@ -420,8 +437,10 @@ export function ThreadNavigationSidebar(props: { const placeholderColor = useThemeColor("--color-placeholder"); const searchBackgroundColor = colorScheme === "dark" ? IOS_SEARCH_FILL_DARK : IOS_SEARCH_FILL_LIGHT; - const selectedBackgroundColor = - colorScheme === "dark" ? "rgba(255,255,255,0.12)" : "rgba(0,0,0,0.08)"; + const selectedBackgroundColor = useThemeColor("--color-user-bubble"); + const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground"); + const selectedMutedColor = useThemeColor("--color-user-bubble-foreground-muted"); + const selectedPressedBackgroundColor = "rgba(255,255,255,0.16)"; const pressedBackgroundColor = useThemeColor("--color-subtle"); const listThemeKey = `${colorScheme}:${String(backgroundColor)}:${String(selectedBackgroundColor)}`; const listExtraData = `${listThemeKey}:${props.selectedThreadKey ?? ""}`; @@ -501,6 +520,9 @@ export function ThreadNavigationSidebar(props: { pressedBackgroundColor={pressedBackgroundColor} selected={item.key === props.selectedThreadKey} selectedBackgroundColor={selectedBackgroundColor} + selectedForegroundColor={selectedForegroundColor} + selectedMutedColor={selectedMutedColor} + selectedPressedBackgroundColor={selectedPressedBackgroundColor} simultaneousSwipeGesture={sidebarScrollGesture} thread={thread} environmentLabel={savedConnectionsById[thread.environmentId]?.environmentLabel ?? null} @@ -521,6 +543,9 @@ export function ThreadNavigationSidebar(props: { props.width, savedConnectionsById, selectedBackgroundColor, + selectedForegroundColor, + selectedMutedColor, + selectedPressedBackgroundColor, listThemeKey, mutedColor, ], From 9083c716eab5b44e5338a7fefa80c555c861908d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:47:06 -0700 Subject: [PATCH 44/71] Hide thread search while inspector owns search --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 55de057af69..35fbf429071 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -330,7 +330,8 @@ function ThreadRouteContent( const secondaryFg = String(useThemeColor("--color-foreground-secondary")); const screenBackgroundColor = String(useThemeColor("--color-screen")); const usesNativeHeaderGlass = Platform.OS === "ios"; - const usesThreadSearchToolbar = Platform.OS === "ios" && layout.usesSplitView; + const usesThreadSearchToolbar = + Platform.OS === "ios" && layout.usesSplitView && inspectorMode === null; const focusThreadSearch = useCallback(() => { if (!usesThreadSearchToolbar) { return false; From af87ceced9b0db67421e6355d0d99161b27eb614 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:59:41 -0700 Subject: [PATCH 45/71] Add split sidebar control to terminal header --- .../terminal/ThreadTerminalRouteScreen.tsx | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 387c2afe3e4..5a878659a7c 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -34,6 +34,7 @@ import { import { useThreadSelection } from "../../state/use-thread-selection"; import { useSelectedThreadDetail } from "../../state/use-thread-detail"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; +import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; import { TerminalSurface } from "./NativeTerminalSurface"; import { getPierreTerminalTheme } from "./terminalTheme"; import { loadPreferences, savePreferencesPatch } from "../../lib/storage"; @@ -204,6 +205,7 @@ export function ThreadTerminalRouteScreen() { const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const { state: workspaceState } = useWorkspaceState(); + const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const params = useLocalSearchParams<{ environmentId?: string | string[]; threadId?: string | string[]; @@ -990,6 +992,19 @@ export function ThreadTerminalRouteScreen() { }} /> + {layout.usesSplitView ? ( + + + + ) : null} + {isEnvironmentReady ? ( From 2caf6dd1be805ace649ee7ad4cbbbeafa377f5d9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 01:10:37 -0700 Subject: [PATCH 46/71] Use Mail-style overflow controls on mobile --- apps/mobile/src/features/home/HomeHeader.tsx | 4 +--- apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index a3283d546b3..2dd65335ab3 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -71,13 +71,11 @@ export function HomeHeader(props: { ? () => [ { accessibilityLabel: "Open settings", - icon: { name: "gearshape", type: "sfSymbol" }, + icon: { name: "ellipsis", type: "sfSymbol" }, identifier: "home-settings", label: "", onPress: props.onOpenSettings, - sharesBackground: true, type: "button", - width: 58, }, ] : undefined, diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 1326b4290bb..b7c5cc6092e 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -559,7 +559,7 @@ export function ThreadNavigationSidebar(props: { const nativeHeaderRightBarButtonItems = [ { accessibilityLabel: "Open settings", - icon: { name: "gearshape", type: "sfSymbol" }, + icon: { name: "ellipsis", type: "sfSymbol" }, identifier: "thread-sidebar-settings", onPress: props.onOpenSettings, sharesBackground: true, From 27606a9c943d99d7d109f70fdb11964af8277ada Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 01:23:04 -0700 Subject: [PATCH 47/71] Use native iPad archive header search --- .../archive/ArchivedThreadsScreen.tsx | 34 ++++++++++++++----- 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 2ef7e848dd2..03b5bf834aa 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -64,7 +64,8 @@ function ArchivedThreadsHeader(props: { }) { const { width } = useWindowDimensions(); const hasCustomFilter = props.selectedEnvironmentId !== null || props.sortOrder !== "newest"; - const usesNativeMailToolbar = Platform.OS === "ios" && width < 700; + const usesNativeChrome = Platform.OS === "ios"; + const usesCompactMailToolbar = Platform.OS === "ios" && width < 700; const archiveFilterMenu = { title: "Archived thread options", items: [ @@ -115,11 +116,11 @@ function ArchivedThreadsHeader(props: { [ { composeButtonId: "archived-refresh", @@ -138,14 +139,21 @@ function ArchivedThreadsHeader(props: { }, ] : undefined, - headerSearchBarOptions: usesNativeMailToolbar + headerSearchBarOptions: usesCompactMailToolbar ? undefined : { + ...(usesNativeChrome + ? { + allowToolbarIntegration: true, + placement: "integratedButton" as const, + } + : { + placement: "stacked" as const, + }), autoCapitalize: "none", hideNavigationBar: false, obscureBackground: false, placeholder: "Search archived threads", - placement: "stacked", onChangeText: (event) => { props.onSearchQueryChange(event.nativeEvent.text); }, @@ -156,8 +164,16 @@ function ArchivedThreadsHeader(props: { }} /> - {usesNativeMailToolbar ? null : ( + {usesCompactMailToolbar ? null : ( + {usesNativeChrome ? ( + + ) : null} Date: Tue, 30 Jun 2026 01:39:40 -0700 Subject: [PATCH 48/71] Share archive row swipe behavior --- .../archive/ArchivedThreadsScreen.tsx | 321 ++++++++---------- 1 file changed, 142 insertions(+), 179 deletions(-) diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 03b5bf834aa..f66f5c423d8 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -4,10 +4,9 @@ import type { } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; -import * as Haptics from "expo-haptics"; import { Stack } from "expo-router"; import { SymbolView } from "expo-symbols"; -import { useCallback, useRef } from "react"; +import { useCallback, useMemo, useRef, type ComponentProps } from "react"; import { ActivityIndicator, Platform, @@ -17,9 +16,8 @@ import { useWindowDimensions, View, } from "react-native"; -import ReanimatedSwipeable, { - type SwipeableMethods, -} from "react-native-gesture-handler/ReanimatedSwipeable"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; @@ -27,11 +25,7 @@ import { EmptyState } from "../../components/EmptyState"; import { ProjectFavicon } from "../../components/ProjectFavicon"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; -import { - THREAD_SWIPE_ACTIONS_WIDTH, - THREAD_SWIPE_SPRING, - ThreadSwipeActions, -} from "../home/thread-swipe-actions"; +import { ThreadSwipeable } from "../home/thread-swipe-actions"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { @@ -259,26 +253,20 @@ function ArchivedThreadRow(props: { readonly onDelete: () => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; + readonly simultaneousSwipeGesture?: ComponentProps< + typeof ThreadSwipeable + >["simultaneousWithExternalGesture"]; readonly onUnarchive: () => void; readonly thread: EnvironmentThreadShell; }) { - const swipeableRef = useRef(null); - const fullSwipeArmedRef = useRef(false); const { width: windowWidth } = useWindowDimensions(); const cardColor = useThemeColor("--color-card"); const iconColor = useThemeColor("--color-icon-subtle"); const separatorColor = useThemeColor("--color-separator"); - const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, (windowWidth - 32) * 0.58); const timestamp = relativeTime(props.thread.archivedAt ?? props.thread.updatedAt); const subtitle = [props.environmentLabel, props.thread.branch].filter((part): part is string => Boolean(part), ); - const handleFullSwipeArmedChange = useCallback((armed: boolean) => { - if (armed && !fullSwipeArmedRef.current && process.env.EXPO_OS === "ios") { - void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); - } - fullSwipeArmedRef.current = armed; - }, []); const handleMenuAction = useCallback( (event: { nativeEvent: { event: string } }) => { if (event.nativeEvent.event === "unarchive") { @@ -291,114 +279,80 @@ function ArchivedThreadRow(props: { ); return ( - { - fullSwipeArmedRef.current = false; - if (swipeableRef.current) { - props.onSwipeableClose(swipeableRef.current); - } - }} - onSwipeableOpenStartDrag={() => { - if (swipeableRef.current) { - props.onSwipeableWillOpen(swipeableRef.current); - } - }} - onSwipeableWillOpen={() => { - const methods = swipeableRef.current; - if (!methods) return; - - props.onSwipeableWillOpen(methods); - if (fullSwipeArmedRef.current) { - fullSwipeArmedRef.current = false; - methods.close(); - props.onDelete(); - } + ( - - )} - rightThreshold={THREAD_SWIPE_ACTIONS_WIDTH * 0.42} + simultaneousWithExternalGesture={props.simultaneousSwipeGesture} + threadTitle={props.thread.title} > - - - - - - - - - {props.thread.title} - - - {timestamp} - + {() => ( + + + - {subtitle.length > 0 ? ( - - + + + - {subtitle.join(" · ")} + {props.thread.title} + + + {timestamp} - ) : null} - + {subtitle.length > 0 ? ( + + + + {subtitle.join(" · ")} + + + ) : null} + - - - - - - - + + + + + + + )} + ); } @@ -432,6 +386,7 @@ export function ArchivedThreadsScreen(props: { readonly onUnarchiveThread: (thread: EnvironmentThreadShell) => void; }) { const openSwipeableRef = useRef(null); + const archiveScrollGesture = useMemo(() => Gesture.Native(), []); const refreshTint = useThemeColor("--color-icon"); const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current && openSwipeableRef.current !== methods) { @@ -459,70 +414,78 @@ export function ArchivedThreadsScreen(props: { sortOrder={props.sortOrder} /> - openSwipeableRef.current?.close()} - refreshControl={ - - } - showsVerticalScrollIndicator={false} - > - {props.error ? : null} + + openSwipeableRef.current?.close()} + refreshControl={ + + } + showsVerticalScrollIndicator={false} + > + {props.error ? : null} - {isInitialLoad ? ( - - - Loading archive… - - ) : props.groups.length === 0 ? ( - - ) : ( - props.groups.map((group) => { - const environmentLabel = - props.environments.find( - (environment) => environment.environmentId === group.project.environmentId, - )?.label ?? null; + {isInitialLoad ? ( + + + Loading archive… + + ) : props.groups.length === 0 ? ( + + ) : ( + props.groups.map((group) => { + const environmentLabel = + props.environments.find( + (environment) => environment.environmentId === group.project.environmentId, + )?.label ?? null; - return ( - - - - {group.threads.map((thread, index) => ( - props.onDeleteThread(thread)} - onSwipeableClose={handleSwipeableClose} - onSwipeableWillOpen={handleSwipeableWillOpen} - onUnarchive={() => props.onUnarchiveThread(thread)} - thread={thread} - /> - ))} + return ( + + + + {group.threads.map((thread, index) => ( + props.onDeleteThread(thread)} + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + onUnarchive={() => props.onUnarchiveThread(thread)} + simultaneousSwipeGesture={archiveScrollGesture} + thread={thread} + /> + ))} + - - ); - }) - )} - + ); + }) + )} + + ); } From 1b7051a5d3d613387fd990ef9193cb94428bddbb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 01:59:32 -0700 Subject: [PATCH 49/71] Use native header search for file inspector --- .../files/thread-file-navigator-pane.tsx | 114 ++++++++++++++++-- 1 file changed, 102 insertions(+), 12 deletions(-) diff --git a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx index c11095b7254..58a77a7460d 100644 --- a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx +++ b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx @@ -1,9 +1,17 @@ import type { EnvironmentId, ProjectListEntriesResult } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; -import { useCallback, useState } from "react"; -import { Pressable, useColorScheme, View } from "react-native"; +import { useCallback, useMemo, useState, type ComponentProps } from "react"; +import { Platform, Pressable, useColorScheme, View, type NativeSyntheticEvent } from "react-native"; +import { + Screen, + ScreenStack, + ScreenStackHeaderConfig, + ScreenStackHeaderSearchBarView, + SearchBar, +} from "react-native-screens"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; +import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { projectEnvironment } from "../../state/projects"; import { useEnvironmentQuery } from "../../state/query"; @@ -22,6 +30,9 @@ export function ThreadFileNavigatorPane(props: { const colorScheme = useColorScheme(); const highlightTheme = colorScheme === "dark" ? "dark" : "light"; const iconColor = String(useThemeColor("--color-icon-muted")); + const foregroundColor = String(useThemeColor("--color-foreground")); + const sheetColor = String(useThemeColor("--color-sheet")); + const topScrollEdgeEffect = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); const entriesQuery = useEnvironmentQuery( projectEnvironment.listEntries({ environmentId: props.environmentId, @@ -40,6 +51,94 @@ export function ThreadFileNavigatorPane(props: { }, [highlightTheme, props.cwd, props.environmentId], ); + const nativeHeaderRightBarButtonItems = useMemo( + () => + [ + { + accessibilityLabel: "Refresh files", + icon: { name: "arrow.clockwise", type: "sfSymbol" as const }, + identifier: "thread-file-navigator-refresh", + onPress: entriesQuery.refresh, + sharesBackground: false, + tintColor: foregroundColor, + type: "button" as const, + width: 44, + }, + ] as ComponentProps["headerRightBarButtonItems"], + [entriesQuery.refresh, foregroundColor], + ); + + const fileTree = ( + + ); + + if (Platform.OS === "ios") { + return ( + + + + {fileTree} + + + { + setSearchQuery(""); + }} + onChangeText={(event: NativeSyntheticEvent<{ readonly text?: string }>) => { + setSearchQuery(event.nativeEvent.text ?? ""); + }} + placement="integratedButton" + placeholder="Search files" + textColor={foregroundColor} + tintColor={foregroundColor} + /> + + + + + + ); + } return ( @@ -75,16 +174,7 @@ export function ThreadFileNavigatorPane(props: { /> - + {fileTree} ); } From c11d0c68e750c7ab4a0d21cf809531b6012f7b53 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:08:45 -0700 Subject: [PATCH 50/71] Use native header for git inspector --- .../features/threads/git/GitOverviewSheet.tsx | 212 ++++++++++++------ 1 file changed, 141 insertions(+), 71 deletions(-) diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 075f0d916dd..e518d3e971e 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -7,12 +7,14 @@ import { import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useLocalSearchParams, useRouter } from "expo-router"; import { SymbolView } from "expo-symbols"; -import { useCallback, useEffect, useMemo } from "react"; -import { Alert, Pressable, ScrollView, View } from "react-native"; +import { useCallback, useEffect, useMemo, type ComponentProps } from "react"; +import { Alert, Platform, Pressable, ScrollView, View } from "react-native"; +import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../../lib/useThemeColor"; import { AppText as Text } from "../../../components/AppText"; +import { nativeTopScrollEdgeEffect } from "../../../lib/native-scroll-edge-effect"; import { tryOpenExternalUrl } from "../../../lib/openExternalUrl"; import { buildThreadReviewRoutePath } from "../../../lib/routes"; import { useEnvironmentQuery } from "../../../state/query"; @@ -23,6 +25,8 @@ import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-wo import { vcsEnvironment } from "../../../state/vcs"; import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents"; +const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); + export function GitOverviewSheet( props: { readonly headerInset?: number; @@ -44,6 +48,8 @@ export function GitOverviewSheet( const iconColor = useThemeColor("--color-icon"); const borderColor = useThemeColor("--color-border"); + const foregroundColor = String(useThemeColor("--color-foreground")); + const sheetColor = String(useThemeColor("--color-sheet")); const gitStatus = useEnvironmentQuery( selectedThread !== null && selectedThreadCwd !== null @@ -55,6 +61,7 @@ export function GitOverviewSheet( ); const currentBranchLabel = gitStatus.data?.refName ?? selectedThread?.branch ?? "Detached HEAD"; + const currentStatusSummary = statusSummary(gitStatus.data); const currentWorktreePath = selectedThreadWorktreePath; const gitOperationLabel = gitState.gitOperationLabel; const busy = gitOperationLabel !== null; @@ -160,6 +167,136 @@ export function GitOverviewSheet( [environmentId, openExistingPr, router, runActionWithPrompt, threadId], ); + const inspectorHeaderRightBarButtonItems = useMemo( + () => + [ + { + accessibilityLabel: "Refresh repository status", + disabled: busy, + icon: { name: "arrow.clockwise", type: "sfSymbol" as const }, + identifier: "git-overview-refresh", + onPress: () => { + void gitActions.refreshSelectedThreadGitStatus(); + }, + sharesBackground: false, + tintColor: foregroundColor, + type: "button" as const, + width: 44, + }, + ] as ComponentProps["headerRightBarButtonItems"], + [busy, foregroundColor, gitActions], + ); + + const content = ( + + + {sheetMenuItems.map(({ item, disabledReason }, index) => ( + + {index > 0 ? ( + + ) : null} + void onPressMenuItem(item)} + /> + + ))} + {(gitStatus.data?.behindCount ?? 0) > 0 ? ( + <> + + void gitActions.onPullSelectedThreadBranch()} + /> + + ) : null} + + router.push(buildThreadReviewRoutePath({ environmentId, threadId }))} + /> + + + router.push({ + pathname: "/threads/[environmentId]/[threadId]/git/branches", + params: { environmentId, threadId }, + }) + } + /> + + + {currentWorktreePath ? : null} + + ); + + if (isInspector && Platform.OS === "ios") { + return ( + + + + {content} + + + + + ); + } + return ( - {statusSummary(gitStatus.data)} + {currentStatusSummary} - - - {sheetMenuItems.map(({ item, disabledReason }, index) => ( - - {index > 0 ? ( - - ) : null} - void onPressMenuItem(item)} - /> - - ))} - {(gitStatus.data?.behindCount ?? 0) > 0 ? ( - <> - - void gitActions.onPullSelectedThreadBranch()} - /> - - ) : null} - - router.push(buildThreadReviewRoutePath({ environmentId, threadId }))} - /> - - - router.push({ - pathname: "/threads/[environmentId]/[threadId]/git/branches", - params: { environmentId, threadId }, - }) - } - /> - - - {currentWorktreePath ? : null} - + {content} ); } From c1f87cc48beb186673dcea5506e9de9c835299f8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:15:56 -0700 Subject: [PATCH 51/71] Show native search in iPad thread header --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 35fbf429071..4a1b0ccc8b1 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -623,7 +623,7 @@ function ThreadRouteContent( projectScripts: selectedThreadProject?.scripts ?? [], terminalSessions: terminalMenuSessions, showDirectFileControl: layout.usesSplitView, - showSearchSlot: false, + showSearchSlot: usesThreadSearchToolbar, onOpenTerminal: handleOpenTerminal, onOpenNewTerminal: handleOpenNewTerminal, onRunProjectScript: handleRunProjectScript, From 035eb3b2e65cbcbea3779dd2bfd1b1d2e271b37b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:30:55 -0700 Subject: [PATCH 52/71] Centralize native Mail search toolbar config --- .../archive/ArchivedThreadsScreen.tsx | 7 +++--- apps/mobile/src/features/home/HomeHeader.tsx | 7 +++--- .../layout/native-mail-search-toolbar.ts | 24 +++++++++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 apps/mobile/src/features/layout/native-mail-search-toolbar.ts diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index f66f5c423d8..a245b58faa8 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -26,6 +26,7 @@ import { ProjectFavicon } from "../../components/ProjectFavicon"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; +import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; export interface ArchivedThreadsHeaderEnvironment { @@ -116,7 +117,7 @@ function ArchivedThreadsHeader(props: { unstable_navigationItemStyle: usesNativeChrome ? "editor" : undefined, unstable_headerToolbarItems: usesCompactMailToolbar ? () => [ - { + createNativeMailSearchToolbarItem({ composeButtonId: "archived-refresh", composeSystemImageName: "arrow.clockwise", filterMenu: archiveFilterMenu, @@ -128,9 +129,7 @@ function ArchivedThreadsHeader(props: { onSearchTextChange: props.onSearchQueryChange, placeholder: "Search", searchTextChangeId: "archived-search-text", - type: "mailSearchToolbar", - useFallbackSearchField: true, - }, + }), ] : undefined, headerSearchBarOptions: usesCompactMailToolbar diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 2dd65335ab3..b2a62f853cf 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -10,6 +10,7 @@ import type { SearchBarCommands } from "react-native-screens"; import { useThemeColor } from "../../lib/useThemeColor"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { buildHomeListFilterMenu, @@ -82,7 +83,7 @@ export function HomeHeader(props: { unstable_headerToolbarItems: Platform.OS === "ios" ? () => [ - { + createNativeMailSearchToolbarItem({ composeButtonId: "home-new-task", composeSystemImageName: "square.and.pencil", filterMenu, @@ -94,9 +95,7 @@ export function HomeHeader(props: { onSearchTextChange: props.onSearchQueryChange, placeholder: "Search", searchTextChangeId: "home-search-text", - type: "mailSearchToolbar", - useFallbackSearchField: true, - }, + }), ] : undefined, headerSearchBarOptions: diff --git a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts new file mode 100644 index 00000000000..5cc12957c5f --- /dev/null +++ b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts @@ -0,0 +1,24 @@ +import type { HeaderBarButtonMailSearchToolbarItem } from "react-native-screens"; + +type NativeMailSearchToolbarInput = Omit< + HeaderBarButtonMailSearchToolbarItem, + "type" | "useFallbackSearchField" +>; + +/** + * Builds the patched react-native-screens Mail-style bottom search toolbar. + * + * Keeping this behind an app-level helper makes the iOS-only RNS patch an + * explicit layout primitive instead of a per-screen object literal. Android can + * keep using Expo Router toolbar primitives without depending on this helper. + */ +export function createNativeMailSearchToolbarItem( + input: NativeMailSearchToolbarInput, +): HeaderBarButtonMailSearchToolbarItem { + return { + placeholder: "Search", + ...input, + type: "mailSearchToolbar", + useFallbackSearchField: true, + }; +} From 7cddaba1da0aaf899f90b7e8923919f729c329da Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:39:21 -0700 Subject: [PATCH 53/71] Tune selected sidebar status pill contrast --- .../src/features/threads/ThreadNavigationSidebar.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index b7c5cc6092e..fdec322d0e2 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -141,6 +141,13 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { Boolean(part), ); const statusTone = threadStatusTone(thread); + const effectiveStatusTone = selected + ? { + ...statusTone, + pillClassName: "bg-white/20", + textClassName: "text-white", + } + : statusTone; return ( - + Date: Tue, 30 Jun 2026 03:14:16 -0700 Subject: [PATCH 54/71] Use native Mail toolbar for file search --- .../features/files/ThreadFilesRouteScreen.tsx | 91 ++++++++++++------- patches/react-native-screens@4.25.2.patch | 89 ++++++++++-------- pnpm-lock.yaml | 8 +- 3 files changed, 114 insertions(+), 74 deletions(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index a97443dfa2e..4ed2c148e83 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -39,6 +39,7 @@ import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, } from "../layout/AdaptiveWorkspaceLayout"; +import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; import { ReviewHighlighterProvider } from "../review/ReviewHighlighterProvider"; import { ThreadRouteScreen } from "../threads/ThreadRouteScreen"; @@ -572,6 +573,8 @@ export function ThreadFilesTreeScreen() { ); } + const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView; + return ( { - setSearchQuery(event.nativeEvent.text); - }, - onCancelButtonPress: () => { - setSearchQuery(""); - }, - }, + unstable_headerToolbarItems: usesCompactMailToolbar + ? () => [ + createNativeMailSearchToolbarItem({ + composeButtonId: "files-refresh", + composeSystemImageName: "arrow.clockwise", + onComposePress: entriesQuery.refresh, + onSearchTextChange: setSearchQuery, + placeholder: "Search files", + searchTextChangeId: "files-search-text", + }), + ] + : undefined, + headerSearchBarOptions: usesCompactMailToolbar + ? undefined + : { + allowToolbarIntegration: true, + autoCapitalize: "none", + hideNavigationBar: false, + placeholder: "Search files", + onChangeText: (event) => { + setSearchQuery(event.nativeEvent.text); + }, + onCancelButtonPress: () => { + setSearchQuery(""); + }, + }, }} /> - - {layout.usesSplitView ? ( - - ) : null} - - - - - + {layout.usesSplitView || !usesCompactMailToolbar ? ( + + {layout.usesSplitView ? ( + + ) : null} + {!usesCompactMailToolbar ? ( + + ) : null} + + ) : null} + {usesCompactMailToolbar ? null : ( + + + + )} Date: Tue, 30 Jun 2026 03:32:09 -0700 Subject: [PATCH 55/71] Align inspector toolbar placement on iPad --- .../features/files/ThreadFilesRouteScreen.tsx | 38 ++++++------- .../src/features/review/ReviewSheet.tsx | 27 ++++----- patches/react-native-screens@4.25.2.patch | 56 +++++++++---------- pnpm-lock.yaml | 8 +-- 4 files changed, 64 insertions(+), 65 deletions(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 4ed2c148e83..c6b9d33ee56 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -620,27 +620,25 @@ export function ThreadFilesTreeScreen() { }, }} /> - {layout.usesSplitView || !usesCompactMailToolbar ? ( + {layout.usesSplitView ? ( + + + + ) : null} + {!usesCompactMailToolbar ? ( - {layout.usesSplitView ? ( - - ) : null} - {!usesCompactMailToolbar ? ( - - ) : null} + ) : null} {usesCompactMailToolbar ? null : ( diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index e62baf44e53..eac42a9d04c 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -603,20 +603,21 @@ export function ReviewSheet() { }} /> - {layout.usesSplitView || showSectionToolbar || panes.supportsAuxiliaryPane ? ( + {layout.usesSplitView ? ( + + + + ) : null} + + {showSectionToolbar || panes.supportsAuxiliaryPane ? ( - {layout.usesSplitView ? ( - - ) : null} {panes.supportsAuxiliaryPane ? ( *)barButtonItemsFromConfigs:(NSArray *> *)dicts withCurrentItems:(NSArray *)currentItems @@ -418,7 +418,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f { if (dicts.count == 0) { return currentItems; -@@ -781,7 +1037,190 @@ RNS_IGNORE_SUPER_CALL_END +@@ -781,7 +1047,197 @@ RNS_IGNORE_SUPER_CALL_END [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; @@ -617,7 +617,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict action:^(NSString *buttonId) { auto eventEmitter = std::static_pointer_cast( -@@ -809,11 +1248,15 @@ RNS_IGNORE_SUPER_CALL_END +@@ -809,11 +1265,15 @@ RNS_IGNORE_SUPER_CALL_END [items addObject:item]; } } else if (dict[@"spacing"]) { @@ -637,7 +637,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f NSNumber *index = dict[@"index"]; if (index.integerValue < items.count) { [items insertObject:item atIndex:index.integerValue]; -@@ -825,6 +1268,47 @@ RNS_IGNORE_SUPER_CALL_END +@@ -825,6 +1285,47 @@ RNS_IGNORE_SUPER_CALL_END return items; } @@ -685,7 +685,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f RNS_IGNORE_SUPER_CALL_BEGIN - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex { -@@ -1013,6 +1497,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1013,6 +1514,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); @@ -694,7 +694,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); } -@@ -1038,6 +1524,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1038,6 +1541,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; _backButtonDisplayMode = [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; @@ -702,7 +702,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; -@@ -1084,6 +1571,30 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1084,6 +1588,30 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) _headerRightBarButtonItems = array; } @@ -734,7 +734,7 @@ index 5970e3e3a624b9498b8dedfc16831df03a274d0c..99a8bce383d46e0452eea857e84c7a2f if (needsNavigationControllerLayout) { diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h -index 4badf565055086e814808b7ef3bcefc5f89e8af4..7703df97d1a4572e301acf50ca8cba6d29ff5666 100644 +index 4badf56..7703df9 100644 --- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h +++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.h @@ -1,10 +1,11 @@ @@ -751,7 +751,7 @@ index 4badf565055086e814808b7ef3bcefc5f89e8af4..7703df97d1a4572e301acf50ca8cba6d @end diff --git a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm -index 52c61c863c53c289569a741445ed08c0355d95ba..cb4d6be752f98e06dfd3b40453c2bd881886ad88 100644 +index 52c61c8..cb4d6be 100644 --- a/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm +++ b/ios/gamma/scroll-view-marker/RNSScrollViewMarkerComponentView.mm @@ -2,6 +2,7 @@ @@ -806,7 +806,7 @@ index 52c61c863c53c289569a741445ed08c0355d95ba..cb4d6be752f98e06dfd3b40453c2bd88 #pragma mark - Override diff --git a/lib/commonjs/components/ScreenStackHeaderConfig.js b/lib/commonjs/components/ScreenStackHeaderConfig.js -index 16b979bb3dfb41ff247403f1632c300a9a60d549..ad1fdb273e09b344184a7013f4f1b038e84ed19d 100644 +index 16b979b..01415d4 100644 --- a/lib/commonjs/components/ScreenStackHeaderConfig.js +++ b/lib/commonjs/components/ScreenStackHeaderConfig.js @@ -23,18 +23,42 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ @@ -890,7 +890,7 @@ index 16b979bb3dfb41ff247403f1632c300a9a60d549..ad1fdb273e09b344184a7013f4f1b038 onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, ref: ref, diff --git a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js -index ab93f62e4a7049d63c6681cecbd6cf8b1d07ce10..b5ea122473b023f84eabdb1914aef09a28c6d525 100644 +index ab93f62..b5ea122 100644 --- a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js +++ b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js @@ -41,10 +41,31 @@ const prepareMenu = (menu, index, side, path = '') => { @@ -927,7 +927,7 @@ index ab93f62e4a7049d63c6681cecbd6cf8b1d07ce10..b5ea122473b023f84eabdb1914aef09a if (item.icon?.type === 'imageSource') { imageSource = _reactNative.Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/commonjs/index.js b/lib/commonjs/index.js -index de25040bfec8587ca311f5a42661240640989419..bf73f41744e08bc60ffac09029419d19a94425ee 100644 +index de25040..bf73f41 100644 --- a/lib/commonjs/index.js +++ b/lib/commonjs/index.js @@ -202,6 +202,7 @@ var _utils = require("./utils"); @@ -957,7 +957,7 @@ index de25040bfec8587ca311f5a42661240640989419..bf73f41744e08bc60ffac09029419d19 function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && {}.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } diff --git a/lib/module/components/ScreenStackHeaderConfig.js b/lib/module/components/ScreenStackHeaderConfig.js -index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..3d995490fac910dfcc21e81ae965e31f24717c51 100644 +index cf15f36..dd66167 100644 --- a/lib/module/components/ScreenStackHeaderConfig.js +++ b/lib/module/components/ScreenStackHeaderConfig.js @@ -19,18 +19,42 @@ export const ScreenStackHeaderConfig = /*#__PURE__*/React.forwardRef((props, ref @@ -1041,7 +1041,7 @@ index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..3d995490fac910dfcc21e81ae965e31f onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, ref: ref, diff --git a/lib/module/components/helpers/prepareHeaderBarButtonItems.js b/lib/module/components/helpers/prepareHeaderBarButtonItems.js -index 8a70ffd78617147418d628c03c580bb7ff9a8a72..8dbd0ba2ee61ee54d44cdf286a720ce26af01f26 100644 +index 8a70ffd..8dbd0ba 100644 --- a/lib/module/components/helpers/prepareHeaderBarButtonItems.js +++ b/lib/module/components/helpers/prepareHeaderBarButtonItems.js @@ -35,10 +35,31 @@ const prepareMenu = (menu, index, side, path = '') => { @@ -1078,7 +1078,7 @@ index 8a70ffd78617147418d628c03c580bb7ff9a8a72..8dbd0ba2ee61ee54d44cdf286a720ce2 if (item.icon?.type === 'imageSource') { imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/module/index.js b/lib/module/index.js -index 07caaf7f04c1d6484727b43b694ccf76b1988984..07fd7a57687797c5ae8f685ea70ccf983e262f63 100644 +index 07caaf7..07fd7a5 100644 --- a/lib/module/index.js +++ b/lib/module/index.js @@ -43,4 +43,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; @@ -1089,7 +1089,7 @@ index 07caaf7f04c1d6484727b43b694ccf76b1988984..07fd7a57687797c5ae8f685ea70ccf98 //# sourceMappingURL=index.js.map \ No newline at end of file diff --git a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts -index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..9e3d310dea3a36d79ae7f5078ccc792ab9ff9979 100644 +index b7568ec..41ca1f2 100644 --- a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +++ b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts @@ -9,6 +9,7 @@ type OnPressHeaderBarButtonMenuItemEvent = Readonly<{ @@ -1125,7 +1125,7 @@ index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..9e3d310dea3a36d79ae7f5078ccc792a onPressHeaderBarButtonMenuItem?: CT.DirectEventHandler | undefined; synchronousShadowStateUpdatesEnabled?: CT.WithDefault; diff --git a/lib/typescript/index.d.ts b/lib/typescript/index.d.ts -index 8e4480f901d5d557244643a32341d46e9a497e68..883c5f264301058cbcd9090bee2702ee02346161 100644 +index 8e4480f..883c5f2 100644 --- a/lib/typescript/index.d.ts +++ b/lib/typescript/index.d.ts @@ -32,5 +32,6 @@ export { default as useTransitionProgress } from './useTransitionProgress'; @@ -1137,7 +1137,7 @@ index 8e4480f901d5d557244643a32341d46e9a497e68..883c5f264301058cbcd9090bee2702ee //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts -index 3b384e03891e38e936f370372a682d73440e7ec2..66fe089401975016d2fb91b13e135940adf516cb 100644 +index 3b384e0..a77ef61 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -10,6 +10,7 @@ export type SearchBarCommands = { @@ -1251,7 +1251,7 @@ index 3b384e03891e38e936f370372a682d73440e7ec2..66fe089401975016d2fb91b13e135940 * Custom Screen Transition */ diff --git a/src/components/ScreenStackHeaderConfig.tsx b/src/components/ScreenStackHeaderConfig.tsx -index 421b3c2545426ae957271bd6515bcf827437541c..0ca6f1d7f324eaf257891a3bbb0aab210aa30f6f 100644 +index 421b3c2..0ca6f1d 100644 --- a/src/components/ScreenStackHeaderConfig.tsx +++ b/src/components/ScreenStackHeaderConfig.tsx @@ -39,7 +39,12 @@ export const ScreenStackHeaderConfig = React.forwardRef< @@ -1378,7 +1378,7 @@ index 421b3c2545426ae957271bd6515bcf827437541c..0ca6f1d7f324eaf257891a3bbb0aab21 onPressHeaderBarButtonMenuItem={onPressHeaderBarButtonMenuItem} ref={ref} diff --git a/src/components/helpers/prepareHeaderBarButtonItems.ts b/src/components/helpers/prepareHeaderBarButtonItems.ts -index be2c24dfee77f5883b5ab1d7473be80933b5dc6f..0b038d2005b2d51bbb2ab1d7dba7eaccda83d42f 100644 +index be2c24d..0b038d2 100644 --- a/src/components/helpers/prepareHeaderBarButtonItems.ts +++ b/src/components/helpers/prepareHeaderBarButtonItems.ts @@ -50,13 +50,43 @@ const prepareMenu = ( @@ -1428,7 +1428,7 @@ index be2c24dfee77f5883b5ab1d7473be80933b5dc6f..0b038d2005b2d51bbb2ab1d7dba7eacc if (item.icon?.type === 'imageSource') { imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts -index ba2479de0f49c112470f78c5084059ddd9e2f3e8..8677583a8f8daa4839340467466bfab8d73111ab 100644 +index ba2479d..8677583 100644 --- a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts @@ -14,6 +14,7 @@ type OnPressHeaderBarButtonItemEvent = Readonly<{ buttonId: string }>; @@ -1465,7 +1465,7 @@ index ba2479de0f49c112470f78c5084059ddd9e2f3e8..8677583a8f8daa4839340467466bfab8 | CT.DirectEventHandler | undefined; diff --git a/src/index.tsx b/src/index.tsx -index a405aec437bd879d8be7a706d29e965d77658899..cd649bd7ab7de7b7131302e8be682ec6d0081a9e 100644 +index a405aec..cd649bd 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -66,4 +66,5 @@ export { default as useTransitionProgress } from './useTransitionProgress'; @@ -1475,7 +1475,7 @@ index a405aec437bd879d8be7a706d29e965d77658899..cd649bd7ab7de7b7131302e8be682ec6 +export * from './components/gamma/scroll-view-marker'; export type * from './components/shared/types'; diff --git a/src/types.tsx b/src/types.tsx -index 76a83f3acb6fd3f0af7f027798848b7124100286..bb7abb6e4705b8423b2bf89989fe4a8b38d751f7 100644 +index 76a83f3..bb7abb6 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -26,6 +26,7 @@ export type SearchBarCommands = { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a1aeaaa0101..1dce182822b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -91,7 +91,7 @@ patchedDependencies: hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 path: patches/react-native-nitro-modules@0.35.9.patch react-native-screens@4.25.2: - hash: fdfee13625b106d658de40cceb9947a4a1f658e55faa4ad2192304763050dcb9 + hash: d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb path: patches/react-native-screens@4.25.2.patch importers: @@ -381,7 +381,7 @@ importers: version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=fdfee13625b106d658de40cceb9947a4a1f658e55faa4ad2192304763050dcb9)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16266,7 +16266,7 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(patch_hash=fdfee13625b106d658de40cceb9947a4a1f658e55faa4ad2192304763050dcb9)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -18873,7 +18873,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(patch_hash=fdfee13625b106d658de40cceb9947a4a1f658e55faa4ad2192304763050dcb9)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) From 8fb62e1ba31e5c08b6b372bfbefa36577db32af3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 03:43:37 -0700 Subject: [PATCH 56/71] Use native glass header for file previews --- .../features/files/ThreadFilesRouteScreen.tsx | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index c6b9d33ee56..bc5da63e75f 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -665,6 +665,7 @@ export function ThreadFileScreen() { useAdaptiveWorkspacePaneRole("inspector"); const router = useRouter(); const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); + const iconColor = useThemeColor("--color-icon"); const params = useLocalSearchParams<{ line?: string | string[]; path?: string | string[]; @@ -758,7 +759,21 @@ export function ThreadFileScreen() { {fileInspector.supported ? ( From dfbfc25a8fcd272322c7b40a16bcbbc4dda9da17 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:05:42 -0700 Subject: [PATCH 57/71] Centralize native header scroll edge effects --- .../features/files/ThreadFilesRouteScreen.tsx | 18 ++++-------------- .../files/thread-file-navigator-pane.tsx | 11 +++-------- .../mobile/src/features/review/ReviewSheet.tsx | 11 +++-------- .../terminal/ThreadTerminalRouteScreen.tsx | 13 +++---------- .../mobile/src/features/threads/ThreadFeed.tsx | 14 +++----------- .../threads/ThreadNavigationSidebar.tsx | 11 +++-------- .../src/features/threads/ThreadRouteScreen.tsx | 11 +++-------- .../features/threads/git/GitOverviewSheet.tsx | 11 +++-------- .../src/lib/native-scroll-edge-effect.test.ts | 16 +++++++++++++++- .../src/lib/native-scroll-edge-effect.ts | 18 ++++++++++++++++++ 10 files changed, 58 insertions(+), 76 deletions(-) diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index bc5da63e75f..2fa912243d7 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -25,7 +25,7 @@ import { EmptyState } from "../../components/EmptyState"; import { LoadingScreen } from "../../components/LoadingScreen"; import { cn } from "../../lib/cn"; import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { buildThreadFilesNavigation, buildThreadRoutePath } from "../../lib/routes"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; @@ -61,7 +61,7 @@ import { import { useWorkspaceFileAssetUrl } from "./workspaceFileAssetUrl"; type FileViewMode = "preview" | "source"; -const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -585,12 +585,7 @@ export function ThreadFilesTreeScreen() { headerStyle: { backgroundColor: "transparent" }, headerShadowVisible: false, headerTitle: renderHeaderTitle, - scrollEdgeEffects: { - top: TOP_SCROLL_EDGE_EFFECT, - bottom: "hidden", - left: "hidden", - right: "hidden", - }, + scrollEdgeEffects: HEADER_SCROLL_EDGE_EFFECTS, unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, unstable_headerToolbarItems: usesCompactMailToolbar ? () => [ @@ -765,12 +760,7 @@ export function ThreadFileScreen() { headerShadowVisible: false, headerStyle: { backgroundColor: "transparent" }, headerTintColor: iconColor, - scrollEdgeEffects: { - top: TOP_SCROLL_EDGE_EFFECT, - bottom: "hidden", - left: "hidden", - right: "hidden", - }, + scrollEdgeEffects: HEADER_SCROLL_EDGE_EFFECTS, title: basename(relativePath), unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, }} diff --git a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx index 58a77a7460d..5af3e6b6063 100644 --- a/apps/mobile/src/features/files/thread-file-navigator-pane.tsx +++ b/apps/mobile/src/features/files/thread-file-navigator-pane.tsx @@ -11,7 +11,7 @@ import { } from "react-native-screens"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { projectEnvironment } from "../../state/projects"; import { useEnvironmentQuery } from "../../state/query"; @@ -32,7 +32,7 @@ export function ThreadFileNavigatorPane(props: { const iconColor = String(useThemeColor("--color-icon-muted")); const foregroundColor = String(useThemeColor("--color-foreground")); const sheetColor = String(useThemeColor("--color-sheet")); - const topScrollEdgeEffect = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); + const headerScrollEdgeEffects = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); const entriesQuery = useEnvironmentQuery( projectEnvironment.listEntries({ environmentId: props.environmentId, @@ -90,12 +90,7 @@ export function ThreadFileNavigatorPane(props: { enabled isNativeStack screenId="thread-file-navigator-native" - scrollEdgeEffects={{ - bottom: "hidden", - left: "hidden", - right: "hidden", - top: topScrollEdgeEffect, - }} + scrollEdgeEffects={headerScrollEdgeEffects} style={{ backgroundColor: sheetColor, flex: 1 }} > {fileTree} diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index eac42a9d04c..0199d693c82 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -32,7 +32,7 @@ import { AppText as Text } from "../../components/AppText"; import { environmentCatalog } from "../../connection/catalog"; import { useEnvironmentPresentation } from "../../state/presentation"; import { useAtomCommand } from "../../state/use-atom-command"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; @@ -61,7 +61,7 @@ import { resolveReviewAvailability } from "./reviewAvailability"; import { resolveSelectedReviewFileId } from "./reviewPaneSelection"; import { buildReviewSectionMenu } from "./review-section-menu"; -const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); const REVIEW_HEADER_SPACING = 0; @@ -593,12 +593,7 @@ export function ReviewSheet() { backgroundColor: "transparent", }, headerTitle: renderHeaderTitle, - scrollEdgeEffects: { - top: TOP_SCROLL_EDGE_EFFECT, - bottom: "hidden", - left: "hidden", - right: "hidden", - }, + scrollEdgeEffects: HEADER_SCROLL_EDGE_EFFECTS, unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, }} /> diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 5a878659a7c..e386098d7bb 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -25,7 +25,7 @@ import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useWorkspaceState } from "../../state/workspace"; import { buildThreadTerminalNavigation } from "../../lib/routes"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useAttachedTerminalSession, @@ -74,7 +74,7 @@ import { const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; const TERMINAL_ACCESSORY_HEIGHT = 52; -const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); type PendingModifier = "ctrl" | "meta"; type HostPlatform = "mac" | "linux" | "windows" | "unknown"; @@ -979,14 +979,7 @@ export function ThreadTerminalRouteScreen() { fontWeight: "800", } : undefined, - scrollEdgeEffects: usesNativeHeaderGlass - ? { - top: TOP_SCROLL_EDGE_EFFECT, - bottom: "hidden", - left: "hidden", - right: "hidden", - } - : undefined, + scrollEdgeEffects: usesNativeHeaderGlass ? HEADER_SCROLL_EDGE_EFFECTS : undefined, title: headerTitle.topLine, unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, }} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 9c661490253..e28ac945fb0 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -69,7 +69,7 @@ import { import { buildReviewParsedDiff } from "../review/reviewModel"; import { cn } from "../../lib/cn"; import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { buildThreadFilesNavigation } from "../../lib/routes"; import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; @@ -88,7 +88,7 @@ const MESSAGE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", }); -const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); function formatMessageTime(input: string): string { const timestamp = Date.parse(input); @@ -1499,15 +1499,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { return ( <> - + diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 4a1b0ccc8b1..640233bfab1 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -22,7 +22,7 @@ import { import { scopedThreadKey } from "../../lib/scopedEntities"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { connectionTone } from "../connection/connectionTone"; -import { nativeTopScrollEdgeEffect } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useRemoteConnections, @@ -76,7 +76,7 @@ type NativeHeaderItems = ReturnType< NonNullable >; -const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); function InspectorPaneRoleActivation() { useAdaptiveWorkspacePaneRole("inspector"); @@ -721,12 +721,7 @@ function ThreadRouteContent( title: layout.usesSplitView ? "" : selectedThread.title, headerBackVisible: !layout.usesSplitView, headerBackTitle: "", - scrollEdgeEffects: { - top: TOP_SCROLL_EDGE_EFFECT, - bottom: "hidden", - left: "hidden", - right: "hidden", - }, + scrollEdgeEffects: HEADER_SCROLL_EDGE_EFFECTS, headerSearchBarOptions: usesThreadSearchToolbar ? { ref: threadSearchBarRef, diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index e518d3e971e..1974f7a1869 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -14,7 +14,7 @@ import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../../lib/useThemeColor"; import { AppText as Text } from "../../../components/AppText"; -import { nativeTopScrollEdgeEffect } from "../../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../../lib/native-scroll-edge-effect"; import { tryOpenExternalUrl } from "../../../lib/openExternalUrl"; import { buildThreadReviewRoutePath } from "../../../lib/routes"; import { useEnvironmentQuery } from "../../../state/query"; @@ -25,7 +25,7 @@ import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-wo import { vcsEnvironment } from "../../../state/vcs"; import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSheetComponents"; -const TOP_SCROLL_EDGE_EFFECT = nativeTopScrollEdgeEffect(Platform.OS, Platform.Version); +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); export function GitOverviewSheet( props: { @@ -268,12 +268,7 @@ export function GitOverviewSheet( enabled isNativeStack screenId="thread-git-inspector-native" - scrollEdgeEffects={{ - bottom: "hidden", - left: "hidden", - right: "hidden", - top: TOP_SCROLL_EDGE_EFFECT, - }} + scrollEdgeEffects={HEADER_SCROLL_EDGE_EFFECTS} style={{ backgroundColor: sheetColor, flex: 1 }} > {content} diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts b/apps/mobile/src/lib/native-scroll-edge-effect.test.ts index e2872a67c55..f110fad905c 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts +++ b/apps/mobile/src/lib/native-scroll-edge-effect.test.ts @@ -1,6 +1,9 @@ import { describe, expect, it } from "vite-plus/test"; -import { nativeTopScrollEdgeEffect } from "./native-scroll-edge-effect"; +import { + nativeHeaderScrollEdgeEffects, + nativeTopScrollEdgeEffect, +} from "./native-scroll-edge-effect"; describe("nativeTopScrollEdgeEffect", () => { it("keeps the automatic native treatment on iOS 26", () => { @@ -16,3 +19,14 @@ describe("nativeTopScrollEdgeEffect", () => { expect(nativeTopScrollEdgeEffect("android", 27)).toBe("automatic"); }); }); + +describe("nativeHeaderScrollEdgeEffects", () => { + it("keeps non-top header edges hidden while applying the platform top effect", () => { + expect(nativeHeaderScrollEdgeEffects("ios", "27.0")).toEqual({ + top: "soft", + bottom: "hidden", + left: "hidden", + right: "hidden", + }); + }); +}); diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.ts b/apps/mobile/src/lib/native-scroll-edge-effect.ts index fd19a7f33db..aacabcdd22c 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.ts +++ b/apps/mobile/src/lib/native-scroll-edge-effect.ts @@ -1,4 +1,10 @@ export type NativeTopScrollEdgeEffect = "automatic" | "soft"; +export type NativeHeaderScrollEdgeEffects = { + readonly top: NativeTopScrollEdgeEffect; + readonly bottom: "hidden"; + readonly left: "hidden"; + readonly right: "hidden"; +}; function majorVersion(version: number | string): number { if (typeof version === "number") { @@ -20,3 +26,15 @@ export function nativeTopScrollEdgeEffect( ): NativeTopScrollEdgeEffect { return os === "ios" && majorVersion(version) >= 27 ? "soft" : "automatic"; } + +export function nativeHeaderScrollEdgeEffects( + os: string, + version: number | string, +): NativeHeaderScrollEdgeEffects { + return { + top: nativeTopScrollEdgeEffect(os, version), + bottom: "hidden", + left: "hidden", + right: "hidden", + }; +} From c77e6af72a925164a37a534e3f8a22a8d6e6345d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:11:46 -0700 Subject: [PATCH 58/71] Use native scroll edge effects on list headers --- apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx | 4 ++++ apps/mobile/src/features/home/HomeHeader.tsx | 3 +++ 2 files changed, 7 insertions(+) diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index a245b58faa8..292828c4345 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -23,12 +23,15 @@ import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { EmptyState } from "../../components/EmptyState"; import { ProjectFavicon } from "../../components/ProjectFavicon"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import type { ArchivedThreadGroup, ArchivedThreadSortOrder } from "./archivedThreadList"; +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); + export interface ArchivedThreadsHeaderEnvironment { readonly environmentId: EnvironmentId; readonly label: string; @@ -114,6 +117,7 @@ function ArchivedThreadsHeader(props: { headerTransparent: usesNativeChrome, headerStyle: usesNativeChrome ? { backgroundColor: "transparent" } : undefined, headerShadowVisible: usesNativeChrome ? false : undefined, + scrollEdgeEffects: usesNativeChrome ? HEADER_SCROLL_EDGE_EFFECTS : undefined, unstable_navigationItemStyle: usesNativeChrome ? "editor" : undefined, unstable_headerToolbarItems: usesCompactMailToolbar ? () => [ diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index b2a62f853cf..2953768cb38 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -8,6 +8,7 @@ import { useCallback, useRef } from "react"; import { Platform } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; +import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; @@ -24,6 +25,7 @@ import { } from "./home-list-options"; export type HomeHeaderEnvironment = HomeListFilterMenuEnvironment; +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); export function HomeHeader(props: { readonly environments: ReadonlyArray; @@ -59,6 +61,7 @@ export function HomeHeader(props: { headerLargeTitle: false, headerStyle: { backgroundColor: "transparent" }, headerTintColor: iconColor, + scrollEdgeEffects: Platform.OS === "ios" ? HEADER_SCROLL_EDGE_EFFECTS : undefined, headerBackVisible: false, headerBackTitle: "", headerTitle: "Threads", From 92404ecfd8504e9d00df286591a5f11cf94a8ff4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:23:06 -0700 Subject: [PATCH 59/71] Use native new task sheet controls --- apps/mobile/src/app/new/_layout.tsx | 25 ++----------------------- 1 file changed, 2 insertions(+), 23 deletions(-) diff --git a/apps/mobile/src/app/new/_layout.tsx b/apps/mobile/src/app/new/_layout.tsx index 3848a1a0ca4..78720ac686e 100644 --- a/apps/mobile/src/app/new/_layout.tsx +++ b/apps/mobile/src/app/new/_layout.tsx @@ -1,10 +1,7 @@ -import { useRouter } from "expo-router"; import Stack from "expo-router/stack"; -import { SymbolView } from "expo-symbols"; -import { Pressable } from "react-native"; +import { Platform } from "react-native"; import { useResolveClassNames } from "uniwind"; -import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout"; import { NewTaskFlowProvider } from "../../features/threads/new-task-flow-provider"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -12,25 +9,7 @@ export const unstable_settings = { anchor: "index", }; -function NewTaskCloseButton() { - const router = useRouter(); - const tintColor = useThemeColor("--color-foreground"); - - return ( - router.dismiss()} - > - - - ); -} - export default function NewTaskLayout() { - const { layout } = useAdaptiveWorkspaceLayout(); const sheetStyle = useResolveClassNames("bg-sheet"); const sheetBg = useThemeColor("--color-sheet"); const headerTint = useThemeColor("--color-foreground"); @@ -46,7 +25,7 @@ export default function NewTaskLayout() { headerStyle: { backgroundColor: sheetBg }, headerTintColor: headerTint, headerTitleStyle: { fontFamily: "DMSans_700Bold" }, - headerRight: layout.usesSplitView ? NewTaskCloseButton : undefined, + unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, }} > From a838270400c3b27b1fb6646b12c83dc26ae932b9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:40:23 -0700 Subject: [PATCH 60/71] Use compact native search in iPad thread header --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 640233bfab1..e84b478cab8 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -623,7 +623,7 @@ function ThreadRouteContent( projectScripts: selectedThreadProject?.scripts ?? [], terminalSessions: terminalMenuSessions, showDirectFileControl: layout.usesSplitView, - showSearchSlot: usesThreadSearchToolbar, + showSearchSlot: false, onOpenTerminal: handleOpenTerminal, onOpenNewTerminal: handleOpenNewTerminal, onRunProjectScript: handleRunProjectScript, From 258a36a7f0164267d752e1e6ebf27800813433a3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:50:22 -0700 Subject: [PATCH 61/71] Flatten mobile not found link style --- apps/mobile/src/app/+not-found.tsx | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/apps/mobile/src/app/+not-found.tsx b/apps/mobile/src/app/+not-found.tsx index d11155f8602..ceee5e4126e 100644 --- a/apps/mobile/src/app/+not-found.tsx +++ b/apps/mobile/src/app/+not-found.tsx @@ -7,6 +7,14 @@ import { AppText as Text } from "../components/AppText"; export default function NotFoundRoute() { const screenBgStyle = StyleSheet.flatten(useResolveClassNames("bg-screen")); const primaryBgStyle = StyleSheet.flatten(useResolveClassNames("bg-primary")); + const returnHomeButtonStyle = StyleSheet.flatten([ + { + borderRadius: 999, + paddingHorizontal: 20, + paddingVertical: 14, + }, + primaryBgStyle, + ]); return ( - + Return home From 5bfd2161be38dbcf00c30e5432c1c4880adb6be6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:58:51 -0700 Subject: [PATCH 62/71] Remove stale thread toolbar search slot --- apps/mobile/src/features/threads/ThreadGitControls.tsx | 5 +---- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index a8ac116e30a..cc00da1c301 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -95,7 +95,6 @@ type ThreadGitControlsProps = { readonly terminalSessions: ReadonlyArray; readonly showActionControls?: boolean; readonly showDirectFileControl?: boolean; - readonly showSearchSlot?: boolean; readonly onOpenFilesInspector?: () => void; readonly onOpenGitInspector?: () => void; readonly onOpenTerminal: (terminalId?: string | null) => void; @@ -419,7 +418,7 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { const model = useThreadGitControlModel(props); const showActionControls = props.showActionControls ?? true; - if (!showActionControls && !props.showSearchSlot) { + if (!showActionControls) { return null; } @@ -537,8 +536,6 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { ) : null} - {props.showSearchSlot ? : null} - {props.showSearchSlot ? : null} ); } diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index e84b478cab8..d51ad60b265 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -623,7 +623,6 @@ function ThreadRouteContent( projectScripts: selectedThreadProject?.scripts ?? [], terminalSessions: terminalMenuSessions, showDirectFileControl: layout.usesSplitView, - showSearchSlot: false, onOpenTerminal: handleOpenTerminal, onOpenNewTerminal: handleOpenNewTerminal, onRunProjectScript: handleRunProjectScript, From a930a44212951eed1a5073e02590a165db373680 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 07:39:55 -0700 Subject: [PATCH 63/71] Polish native mobile Mail-style chrome --- apps/mobile/src/features/home/HomeHeader.tsx | 7 +- apps/mobile/src/features/home/HomeScreen.tsx | 27 +++-- .../layout/native-glass-header-items.ts | 31 ++++++ .../threads/ThreadNavigationSidebar.tsx | 19 ++-- .../features/threads/ThreadRouteScreen.tsx | 13 +-- patches/react-native-screens@4.25.2.patch | 102 +++++++++++++++++- pnpm-lock.yaml | 50 +++------ 7 files changed, 182 insertions(+), 67 deletions(-) create mode 100644 apps/mobile/src/features/layout/native-glass-header-items.ts diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 2953768cb38..e68e07c683d 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -11,6 +11,7 @@ import type { SearchBarCommands } from "react-native-screens"; import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; +import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { @@ -73,14 +74,14 @@ export function HomeHeader(props: { unstable_headerRightItems: Platform.OS === "ios" ? () => [ - { + withNativeGlassHeaderItem({ accessibilityLabel: "Open settings", - icon: { name: "ellipsis", type: "sfSymbol" }, + icon: { name: "ellipsis", type: "sfSymbol" } as const, identifier: "home-settings", label: "", onPress: props.onOpenSettings, type: "button", - }, + }), ] : undefined, unstable_headerToolbarItems: diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 800ad5484d7..6be719944e1 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -435,6 +435,16 @@ export function HomeScreen(props: HomeScreenProps) { <> {Platform.OS === "ios" ? null : } + {shouldShowConnectionStatus && Platform.OS === "ios" ? ( + + + + ) : null} + {!hasAnyThreads ? ( ); - const connectionStatus = shouldShowConnectionStatus ? ( - - - - ) : null; + const connectionStatus = + shouldShowConnectionStatus && Platform.OS !== "ios" ? ( + + + + ) : null; return ( <> diff --git a/apps/mobile/src/features/layout/native-glass-header-items.ts b/apps/mobile/src/features/layout/native-glass-header-items.ts new file mode 100644 index 00000000000..9ec2eb420b8 --- /dev/null +++ b/apps/mobile/src/features/layout/native-glass-header-items.ts @@ -0,0 +1,31 @@ +type NativeGlassHeaderItem = { + readonly type: "button" | "menu"; + readonly glassEffect?: boolean; + readonly sharesBackground?: boolean; + readonly variant?: "plain" | "done" | "prominent"; + readonly width?: number; +}; + +export const NATIVE_GLASS_HEADER_BUTTON_WIDTH = 58; + +/** + * iOS 26/27 Mail-style header controls need the native glass button + * configuration when they are not part of a larger toolbar. Keep this + * centralized so Expo Router screens and plain react-native-screens demos + * don't drift apart. + */ +export function withNativeGlassHeaderItem( + item: T, + options: { + readonly sharesBackground?: boolean; + readonly width?: number; + } = {}, +): T { + return { + ...item, + glassEffect: item.glassEffect ?? true, + sharesBackground: options.sharesBackground ?? item.sharesBackground ?? true, + variant: item.variant ?? "prominent", + width: options.width ?? item.width ?? NATIVE_GLASS_HEADER_BUTTON_WIDTH, + } as T; +} diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 964fbcb0c3a..e134ba2e64e 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -35,6 +35,7 @@ import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { useThreadListActions } from "../home/useThreadListActions"; import { WorkspaceConnectionStatus } from "../home/WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "../home/workspace-connection-status"; +import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { SidebarHeaderActions } from "./sidebar-header-actions"; import { SidebarFilterButton } from "./sidebar-filter-button"; import { threadStatusTone } from "./threadPresentation"; @@ -564,20 +565,17 @@ export function ThreadNavigationSidebar(props: { const { Screen, ScreenStack, ScreenStackHeaderConfig } = require("react-native-screens") as typeof import("react-native-screens"); const nativeHeaderRightBarButtonItems = [ - { + withNativeGlassHeaderItem({ accessibilityLabel: "Open settings", - icon: { name: "ellipsis", type: "sfSymbol" }, + icon: { name: "ellipsis", type: "sfSymbol" } as const, identifier: "thread-sidebar-settings", onPress: props.onOpenSettings, - sharesBackground: true, tintColor: foregroundColor, type: "button", - variant: "prominent", - width: 58, - }, - { + }), + withNativeGlassHeaderItem({ accessibilityLabel: "Filter and sort threads", - icon: { name: filterIcon, type: "sfSymbol" }, + icon: { name: filterIcon, type: "sfSymbol" } as const, identifier: "thread-sidebar-filter", menu: { title: "Thread list options", @@ -640,12 +638,9 @@ export function ThreadNavigationSidebar(props: { }, ], }, - sharesBackground: true, tintColor: foregroundColor, type: "menu", - variant: "prominent", - width: 58, - }, + }), ] as ComponentProps["headerRightBarButtonItems"]; return ( diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index d51ad60b265..f42bd3d3b46 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -60,6 +60,7 @@ import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, } from "../layout/AdaptiveWorkspaceLayout"; +import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { ThreadFileNavigatorPane } from "../files/thread-file-navigator-pane"; import { ThreadInspectorContentStack, @@ -640,7 +641,7 @@ function ThreadRouteContent( }, ...(props.onReturnToThread ? [ - { + withNativeGlassHeaderItem({ accessibilityLabel: "Return to chat", icon: { name: "chevron.left", type: "sfSymbol" as const }, identifier: "thread-left-return", @@ -648,10 +649,10 @@ function ThreadRouteContent( sharesBackground: false, type: "button" as const, width: 58, - }, + }), ] : []), - { + withNativeGlassHeaderItem({ accessibilityLabel: panes.primarySidebarVisible ? "Maximize content" : "Show thread sidebar", @@ -664,8 +665,8 @@ function ThreadRouteContent( sharesBackground: false, type: "button" as const, width: 58, - }, - { + }), + withNativeGlassHeaderItem({ accessibilityLabel: "New task", icon: { name: "square.and.pencil", type: "sfSymbol" as const }, identifier: "thread-left-new-task", @@ -673,7 +674,7 @@ function ThreadRouteContent( sharesBackground: false, type: "button" as const, width: 58, - }, + }), ], [panes.primarySidebarVisible, props.onReturnToThread, router, togglePrimarySidebar], ); diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index 8b775d72606..21b8effc574 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -11,6 +11,69 @@ index ea5325e..acd2fd7 100644 + imageLoader:(RCTImageLoader *)imageLoader; + @end +diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm +index 0eb1f09..73298f1 100644 +--- a/ios/RNSBarButtonItem.mm ++++ b/ios/RNSBarButtonItem.mm +@@ -110,6 +110,58 @@ static UIMenuOptions RNSMakeUIMenuOptionsFromConfig(NSDictionary *config); + if (dict[@"accessibilityHint"]) { + self.accessibilityHint = dict[@"accessibilityHint"]; + } ++#if !TARGET_OS_TV && RNS_IPHONE_OS_VERSION_AVAILABLE(26_0) ++ NSNumber *glassEffectNum = dict[@"glassEffect"]; ++ if (@available(iOS 26.0, *)) { ++ if ([glassEffectNum boolValue]) { ++ UIButtonConfiguration *configuration = [UIButtonConfiguration glassButtonConfiguration]; ++ configuration.cornerStyle = UIButtonConfigurationCornerStyleCapsule; ++ UIBackgroundConfiguration *background = configuration.background; ++ background.strokeWidth = 0.0; ++ background.strokeColor = UIColor.clearColor; ++ configuration.background = background; ++ configuration.contentInsets = NSDirectionalEdgeInsetsZero; ++ if (title != nil) { ++ configuration.title = title; ++ } ++ ++ UIButton *button = [UIButton buttonWithConfiguration:configuration primaryAction:nil]; ++ button.translatesAutoresizingMaskIntoConstraints = NO; ++ button.enabled = self.enabled; ++ button.accessibilityLabel = self.accessibilityLabel; ++ button.accessibilityHint = self.accessibilityHint; ++ ++ CGFloat resolvedWidth = width != nil ? width.doubleValue : 56.0; ++ [button.widthAnchor constraintEqualToConstant:resolvedWidth].active = YES; ++ [button.heightAnchor constraintEqualToConstant:50.0].active = YES; ++ ++ [[self class] resolveImageFromConfig:dict ++ imageLoader:imageLoader ++ completionBlock:^(UIImage *img) { ++ UIButtonConfiguration *updatedConfiguration = button.configuration; ++ updatedConfiguration.image = img; ++ button.configuration = updatedConfiguration; ++ }]; ++ ++ NSDictionary *menu = dict[@"menu"]; ++ if (menu) { ++ button.menu = [[self class] initUIMenuWithDict:menu menuAction:menuAction imageLoader:imageLoader]; ++ button.showsMenuAsPrimaryAction = YES; ++ } else { ++ NSString *buttonId = dict[@"buttonId"]; ++ if (buttonId && action) { ++ UIAction *pressAction = [UIAction actionWithHandler:^(__kindof UIAction *_Nonnull sender) { ++ action(buttonId); ++ }]; ++ [button addAction:pressAction forControlEvents:UIControlEventTouchUpInside]; ++ } ++ } ++ ++ self.customView = button; ++ return self; ++ } ++ } ++#endif + + #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 + if (@available(tvOS 17.0, *)) { diff --git a/ios/RNSScreen.mm b/ios/RNSScreen.mm index 69d4d9a..6192c4a 100644 --- a/ios/RNSScreen.mm @@ -1137,7 +1200,7 @@ index 8e4480f..883c5f2 100644 //# sourceMappingURL=index.d.ts.map \ No newline at end of file diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts -index 3b384e0..a77ef61 100644 +index 3b384e0..1e3a966 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -10,6 +10,7 @@ export type SearchBarCommands = { @@ -1211,7 +1274,19 @@ index 3b384e0..a77ef61 100644 /** * Allows for setting text color of the title. */ -@@ -1145,8 +1188,37 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { +@@ -1001,6 +1044,11 @@ interface SharedHeaderBarButtonItem { + * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/style-swift.property + */ + variant?: 'plain' | 'done' | 'prominent' | undefined; ++ /** ++ * Render the item with UIButtonConfiguration.glassButtonConfiguration. ++ * Only available from iOS 26.0 and later. ++ */ ++ glassEffect?: boolean | undefined; + /** + * The tint color to apply to the item. + * +@@ -1145,8 +1193,37 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { export interface HeaderBarButtonItemSpacing { type: 'spacing'; spacing: number; @@ -1250,6 +1325,13 @@ index 3b384e0..a77ef61 100644 /** * Custom Screen Transition */ +@@ -1192,4 +1269,4 @@ export interface GestureProviderProps extends GestureProps { + gestureDetectorBridge: React.MutableRefObject; + } + export {}; +-//# sourceMappingURL=types.d.ts.map +\ No newline at end of file ++//# sourceMappingURL=types.d.ts.map diff --git a/src/components/ScreenStackHeaderConfig.tsx b/src/components/ScreenStackHeaderConfig.tsx index 421b3c2..0ca6f1d 100644 --- a/src/components/ScreenStackHeaderConfig.tsx @@ -1475,7 +1557,7 @@ index a405aec..cd649bd 100644 +export * from './components/gamma/scroll-view-marker'; export type * from './components/shared/types'; diff --git a/src/types.tsx b/src/types.tsx -index 76a83f3..bb7abb6 100644 +index 76a83f3..9e4499f 100644 --- a/src/types.tsx +++ b/src/types.tsx @@ -26,6 +26,7 @@ export type SearchBarCommands = { @@ -1549,7 +1631,19 @@ index 76a83f3..bb7abb6 100644 /** * Allows for setting text color of the title. */ -@@ -1279,11 +1322,46 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { +@@ -1125,6 +1168,11 @@ interface SharedHeaderBarButtonItem { + * Read more: https://developer.apple.com/documentation/uikit/uibarbuttonitem/style-swift.property + */ + variant?: 'plain' | 'done' | 'prominent' | undefined; ++ /** ++ * Render the item with UIButtonConfiguration.glassButtonConfiguration. ++ * Only available from iOS 26.0 and later. ++ */ ++ glassEffect?: boolean | undefined; + /** + * The tint color to apply to the item. + * +@@ -1279,11 +1327,46 @@ export interface HeaderBarButtonItemWithMenu extends SharedHeaderBarButtonItem { export interface HeaderBarButtonItemSpacing { type: 'spacing'; spacing: number; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1dce182822b..5a62d88ec2d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,33 +66,15 @@ overrides: packageExtensionsChecksum: sha256-FV3+2NW/9MFbunJaPsMxvO0LAzyfccoPtPiKoIXAwUU= patchedDependencies: - '@effect/vitest@4.0.0-beta.78': - hash: 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f - path: patches/@effect__vitest@4.0.0-beta.78.patch - '@expo/metro-config@56.0.14': - hash: 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 - path: patches/@expo%2Fmetro-config@56.0.14.patch - '@ff-labs/fff-node@0.9.4': - hash: 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 - path: patches/@ff-labs__fff-node@0.9.4.patch - '@pierre/diffs@1.3.0-beta.5': - hash: 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a - path: patches/@pierre%2Fdiffs@1.3.0-beta.5.patch - effect@4.0.0-beta.78: - hash: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 - path: patches/effect@4.0.0-beta.78.patch - expo-modules-jsi@56.0.10: - hash: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f - path: patches/expo-modules-jsi@56.0.10.patch - expo-router@56.2.11: - hash: 3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8 - path: patches/expo-router@56.2.11.patch - react-native-nitro-modules@0.35.9: - hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - path: patches/react-native-nitro-modules@0.35.9.patch - react-native-screens@4.25.2: - hash: d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb - path: patches/react-native-screens@4.25.2.patch + '@effect/vitest@4.0.0-beta.78': 42b87cc47e70d74e62496e7a8261b3fd298ecad4464d209348ab04b96f853a5f + '@expo/metro-config@56.0.14': 8cb08b5bb7051ed9d2dbe46a2c293c5a1e17f1bd6ddf30de27909e18c921ff46 + '@ff-labs/fff-node@0.9.4': 2b16019ce7ab61aec6478dd02f79ef468cc1d5c51e9d00764f7d2ab8167210c8 + '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a + effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 + expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f + expo-router@56.2.11: 3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8 + react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 + react-native-screens@4.25.2: 914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7 importers: @@ -327,7 +309,7 @@ importers: version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) expo-router: specifier: ~56.2.11 - version: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a) + version: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -381,7 +363,7 @@ importers: version: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-screens: specifier: 4.25.2 - version: 4.25.2(patch_hash=d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + version: 4.25.2(patch_hash=914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-shiki-engine: specifier: ^0.3.12 version: 0.3.12(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -12021,7 +12003,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a) + expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12319,7 +12301,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a) + expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16235,7 +16217,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(708fd680682731bfd53a5282341dd27a): + expo-router@56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16266,7 +16248,7 @@ snapshots: react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) react-native-drawer-layout: 4.2.4(0e9729601f58a7a7ae26c76fe6017455) react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - react-native-screens: 4.25.2(patch_hash=d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) server-only: 0.0.1 sf-symbols-typescript: 2.2.0 shallowequal: 1.1.0 @@ -18873,7 +18855,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - react-native-screens@4.25.2(patch_hash=d5b1dfed010c3e2ea92399b0a83d9ba903443f4e900184f9bdefbb22cafa33cb)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): + react-native-screens@4.25.2(patch_hash=914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: react: 19.2.3 react-freeze: 1.0.4(react@19.2.3) From 3158e1dc12a2629fa06c3e968ed4e4bee00e8ca8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 07:57:44 -0700 Subject: [PATCH 64/71] Group iPad detail header controls --- apps/mobile/src/features/threads/ThreadRouteScreen.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index f42bd3d3b46..0fa74fdd9e2 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -646,9 +646,7 @@ function ThreadRouteContent( icon: { name: "chevron.left", type: "sfSymbol" as const }, identifier: "thread-left-return", onPress: props.onReturnToThread, - sharesBackground: false, type: "button" as const, - width: 58, }), ] : []), @@ -662,18 +660,14 @@ function ThreadRouteContent( }, identifier: "thread-left-sidebar", onPress: togglePrimarySidebar, - sharesBackground: false, type: "button" as const, - width: 58, }), withNativeGlassHeaderItem({ accessibilityLabel: "New task", icon: { name: "square.and.pencil", type: "sfSymbol" as const }, identifier: "thread-left-new-task", onPress: () => router.push("/new"), - sharesBackground: false, type: "button" as const, - width: 58, }), ], [panes.primarySidebarVisible, props.onReturnToThread, router, togglePrimarySidebar], From 856fc61e65a84239550971e4d0aa68324f925df5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 08:26:38 -0700 Subject: [PATCH 65/71] Tune native mobile glass toolbar grouping --- apps/mobile/src/app/index.tsx | 1 - apps/mobile/src/features/home/HomeHeader.tsx | 2 ++ apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx | 1 - 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/app/index.tsx index fcee1344f89..993458fee02 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/app/index.tsx @@ -70,7 +70,6 @@ export default function HomeRouteScreen() { accessibilityLabel="Start new task" icon="square.and.pencil" onPress={() => router.push("/new")} - separateBackground /> } /> diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index e68e07c683d..87c9575e452 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -76,11 +76,13 @@ export function HomeHeader(props: { ? () => [ withNativeGlassHeaderItem({ accessibilityLabel: "Open settings", + glassEffect: false, icon: { name: "ellipsis", type: "sfSymbol" } as const, identifier: "home-settings", label: "", onPress: props.onOpenSettings, type: "button", + variant: "plain", }), ] : undefined, diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx index 2e9027fb636..c38304428d3 100644 --- a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx +++ b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx @@ -24,7 +24,6 @@ export function WorkspaceSidebarToolbar( } icon={panes.primarySidebarVisible ? "arrow.up.left.and.arrow.down.right" : "sidebar.left"} onPress={togglePrimarySidebar} - separateBackground /> {props.afterSidebarButton} From c656e071e45f9679c95b3f403aa9062d69157245 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 08:44:41 -0700 Subject: [PATCH 66/71] Wire iPad header search to thread sidebar --- .../features/layout/AdaptiveWorkspaceLayout.tsx | 11 +++++++++++ .../threads/ThreadNavigationSidebar.tsx | 17 +++++++++-------- .../src/features/threads/ThreadRouteScreen.tsx | 9 +++++++++ 3 files changed, 29 insertions(+), 8 deletions(-) diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index d331ca3f806..4cd73883a24 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -43,7 +43,9 @@ interface AdaptiveWorkspaceContextValue { readonly layout: Layout; readonly panes: WorkspacePaneLayout; readonly fileInspector: FileInspectorPaneLayout; + readonly primarySidebarSearchQuery: string; readonly activateAuxiliaryPaneRole: (role: WorkspaceAuxiliaryPaneRole) => () => void; + readonly setPrimarySidebarSearchQuery: (query: string) => void; readonly showAuxiliaryPane: (role: WorkspaceAuxiliaryPaneRole) => void; readonly toggleAuxiliaryPane: () => void; readonly togglePrimarySidebar: () => void; @@ -65,7 +67,9 @@ const AdaptiveWorkspaceContext = createContext({ layout: compactLayout, panes: compactPanes, fileInspector: compactFileInspector, + primarySidebarSearchQuery: "", activateAuxiliaryPaneRole: () => () => undefined, + setPrimarySidebarSearchQuery: () => undefined, showAuxiliaryPane: () => undefined, toggleAuxiliaryPane: () => undefined, togglePrimarySidebar: () => undefined, @@ -102,6 +106,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const [fileInspectorPreferredWidth, setFileInspectorPreferredWidth] = useState( null, ); + const [primarySidebarSearchQuery, setPrimarySidebarSearchQuery] = useState(""); const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = useState(null); const params = useGlobalSearchParams<{ @@ -238,7 +243,9 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) layout, panes, fileInspector, + primarySidebarSearchQuery, activateAuxiliaryPaneRole, + setPrimarySidebarSearchQuery, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar, @@ -249,7 +256,9 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) fileInspector, layout, panes, + primarySidebarSearchQuery, showAuxiliaryPane, + setPrimarySidebarSearchQuery, setAuxiliaryPaneWidth, toggleAuxiliaryPane, togglePrimarySidebar, @@ -329,7 +338,9 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) selectedThreadKey={selectedThreadKey} onOpenSettings={handleOpenSettings} onSelectThread={handleSelectThread} + onSearchQueryChange={setPrimarySidebarSearchQuery} onStartNewTask={handleStartNewTask} + searchQuery={primarySidebarSearchQuery} /> ) : null} diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index e134ba2e64e..911a2d2f6b3 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -249,9 +249,11 @@ export function ThreadNavigationSidebar(props: { readonly visible: boolean; readonly selectedThreadKey: string | null; readonly onOpenSettings: () => void; + readonly onSearchQueryChange: (query: string) => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; readonly onStartNewTask: () => void; readonly onRequestVisibility: () => void; + readonly searchQuery: string; }) { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; @@ -260,7 +262,6 @@ export function ThreadNavigationSidebar(props: { const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); - const [searchQuery, setSearchQuery] = useState(""); const [headerIsOverContent, setHeaderIsOverContent] = useState(false); const searchInputRef = useRef(null); const openSwipeableRef = useRef(null); @@ -294,12 +295,12 @@ export function ThreadNavigationSidebar(props: { projects, threads, environmentId: options.selectedEnvironmentId, - searchQuery, + searchQuery: props.searchQuery, projectSortOrder: options.projectSortOrder, threadSortOrder: options.threadSortOrder, projectGroupingMode: options.projectGroupingMode, }), - [options, projects, searchQuery, threads], + [options, projects, props.searchQuery, threads], ); const listItems = useMemo>( () => @@ -451,7 +452,7 @@ export function ThreadNavigationSidebar(props: { const selectedPressedBackgroundColor = "rgba(255,255,255,0.16)"; const pressedBackgroundColor = useThemeColor("--color-subtle"); const listThemeKey = `${colorScheme}:${String(backgroundColor)}:${String(selectedBackgroundColor)}`; - const listExtraData = `${listThemeKey}:${props.selectedThreadKey ?? ""}`; + const listExtraData = `${listThemeKey}:${props.selectedThreadKey ?? ""}:${props.searchQuery}`; const headerFadeColor = String(backgroundColor); const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; const usesNativeSidebarChrome = Platform.OS === "ios"; @@ -703,7 +704,7 @@ export function ThreadNavigationSidebar(props: { {catalogState.isLoadingConnections ? "Loading threads…" - : searchQuery.trim().length > 0 + : props.searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} @@ -772,7 +773,7 @@ export function ThreadNavigationSidebar(props: { {catalogState.isLoadingConnections ? "Loading threads…" - : searchQuery.trim().length > 0 + : props.searchQuery.trim().length > 0 ? "No matching threads" : "No threads yet"} @@ -857,12 +858,12 @@ export function ThreadNavigationSidebar(props: { autoCapitalize="none" autoCorrect={false} clearButtonMode="while-editing" - onChangeText={setSearchQuery} + onChangeText={props.onSearchQueryChange} placeholder="Search" placeholderTextColor={placeholderColor} returnKeyType="search" style={[styles.searchInput, { color: foregroundColor }]} - value={searchQuery} + value={props.searchQuery} /> diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 0fa74fdd9e2..94b2d758ac8 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -222,6 +222,7 @@ function ThreadRouteContent( fileInspector, layout, panes, + setPrimarySidebarSearchQuery, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar, @@ -720,7 +721,15 @@ function ThreadRouteContent( ? { ref: threadSearchBarRef, allowToolbarIntegration: true, + autoCapitalize: "none", hideNavigationBar: false, + obscureBackground: false, + onCancelButtonPress: () => { + setPrimarySidebarSearchQuery(""); + }, + onChangeText: (event) => { + setPrimarySidebarSearchQuery(event.nativeEvent.text); + }, placeholder: "Search", placement: "integratedButton", } From a423e3c4b1c6a20fe4155498511dae5e5c5bcc50 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 09:46:59 -0700 Subject: [PATCH 67/71] Move thread detail chrome to direct RNS --- apps/mobile/src/features/home/HomeHeader.tsx | 2 - .../layout/native-glass-header-items.ts | 6 +- .../features/threads/ThreadDetailScreen.tsx | 6 +- .../src/features/threads/ThreadFeed.tsx | 10 +- .../features/threads/ThreadGitControls.tsx | 3 - .../features/threads/ThreadRouteScreen.tsx | 252 +++++++++++++----- .../src/lib/native-scroll-edge-effect.test.ts | 8 +- .../src/lib/native-scroll-edge-effect.ts | 25 +- 8 files changed, 213 insertions(+), 99 deletions(-) diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 87c9575e452..e68e07c683d 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -76,13 +76,11 @@ export function HomeHeader(props: { ? () => [ withNativeGlassHeaderItem({ accessibilityLabel: "Open settings", - glassEffect: false, icon: { name: "ellipsis", type: "sfSymbol" } as const, identifier: "home-settings", label: "", onPress: props.onOpenSettings, type: "button", - variant: "plain", }), ] : undefined, diff --git a/apps/mobile/src/features/layout/native-glass-header-items.ts b/apps/mobile/src/features/layout/native-glass-header-items.ts index 9ec2eb420b8..997f3bc1016 100644 --- a/apps/mobile/src/features/layout/native-glass-header-items.ts +++ b/apps/mobile/src/features/layout/native-glass-header-items.ts @@ -6,8 +6,6 @@ type NativeGlassHeaderItem = { readonly width?: number; }; -export const NATIVE_GLASS_HEADER_BUTTON_WIDTH = 58; - /** * iOS 26/27 Mail-style header controls need the native glass button * configuration when they are not part of a larger toolbar. Keep this @@ -26,6 +24,8 @@ export function withNativeGlassHeaderItem( glassEffect: item.glassEffect ?? true, sharesBackground: options.sharesBackground ?? item.sharesBackground ?? true, variant: item.variant ?? "prominent", - width: options.width ?? item.width ?? NATIVE_GLASS_HEADER_BUTTON_WIDTH, + ...(options.width !== undefined || item.width !== undefined + ? { width: options.width ?? item.width } + : {}), } as T; } diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 15aea05dd2c..942dbd279bb 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -69,6 +69,7 @@ export interface ThreadDetailScreenProps { readonly selectedThreadQueueCount: number; readonly serverConfig: T3ServerConfig | null; readonly layoutVariant?: LayoutVariant; + readonly nativeHeaderContentTopInset?: number; readonly usesAutomaticContentInsets?: boolean; readonly onHeaderMaterialVisibilityChange?: (visible: boolean) => void; readonly onOpenDrawer: () => void; @@ -237,6 +238,9 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const layoutVariant = props.layoutVariant ?? "compact"; const isSplitLayout = layoutVariant === "split"; const contentMaxWidth = isSplitLayout ? CHAT_CONTENT_MAX_WIDTH : undefined; + const nativeHeaderContentTopInset = + props.nativeHeaderContentTopInset ?? + Math.max(headerHeight, insets.top + (isSplitLayout ? 88 : 92)); const selectedInstanceId = props.selectedThread.modelSelection.instanceId; useStreamingHaptics(props.selectedThread.id, props.selectedThreadFeed); const selectedProviderSkills = useMemo( @@ -385,7 +389,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread freeze={freeze} anchorMessageId={anchorMessageId} contentInsetEndAdjustment={contentInsetEndAdjustment} - contentTopInset={props.usesAutomaticContentInsets ? headerHeight : 0} + contentTopInset={props.usesAutomaticContentInsets ? nativeHeaderContentTopInset : 0} contentBottomInset={estimatedOverlayHeight} contentMaxWidth={contentMaxWidth} layoutVariant={layoutVariant} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index e28ac945fb0..6547d3781f5 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1165,6 +1165,8 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { const insets = useSafeAreaInsets(); const topContentInset = props.contentTopInset ?? insets.top + 44; const bottomContentInset = props.contentBottomInset ?? 18; + const usesNativeAutomaticInsets = + props.usesAutomaticContentInsets === true && Platform.OS === "ios"; const iconSubtleColor = useThemeColor("--color-icon-subtle"); const userBubbleColor = useThemeColor("--color-user-bubble"); @@ -1504,9 +1506,9 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ref={props.listRef} key={props.threadId} style={{ flex: 1 }} - contentInsetAdjustmentBehavior="never" - automaticallyAdjustsScrollIndicatorInsets={false} - {...(props.usesAutomaticContentInsets + contentInsetAdjustmentBehavior={usesNativeAutomaticInsets ? "automatic" : "never"} + automaticallyAdjustsScrollIndicatorInsets={usesNativeAutomaticInsets} + {...(usesNativeAutomaticInsets ? { contentInset: { top: topContentInset, left: 0, right: 0, bottom: 0 }, scrollIndicatorInsets: { top: 0, left: 0, right: 0, bottom: 0 }, @@ -1543,7 +1545,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { onScroll={handleScroll} scrollEventThrottle={16} ListHeaderComponent={ - props.usesAutomaticContentInsets ? null : + usesNativeAutomaticInsets ? null : } contentContainerStyle={{ paddingTop: 12, diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index cc00da1c301..2c117c4f6f6 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -303,7 +303,6 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit sharesBackground: true, type: "menu", variant: "prominent", - width: 58, }, files: { accessibilityLabel: "Open files", @@ -315,7 +314,6 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit sharesBackground: true, type: "button", variant: "prominent", - width: 58, }, git: { accessibilityLabel: "Git actions", @@ -372,7 +370,6 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit sharesBackground: true, type: "menu", variant: "prominent", - width: 58, }, }), [ diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 94b2d758ac8..ca2a9eac4c6 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,11 +1,33 @@ import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; import type { NativeStackNavigationOptions } from "expo-router/build/react-navigation/native-stack/types"; -import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"; +import { + useCallback, + useEffect, + useMemo, + useRef, + useState, + type ComponentProps, + type ReactNode, +} from "react"; import * as Option from "effect/Option"; import { EnvironmentId, ThreadId, type ProjectScript } from "@t3tools/contracts"; import { projectScriptCwd, projectScriptRuntimeEnv } from "@t3tools/shared/projectScripts"; -import { Platform, Pressable, ScrollView, Text as RNText, View } from "react-native"; -import type { SearchBarCommands } from "react-native-screens"; +import { + Platform, + Pressable, + ScrollView, + Text as RNText, + View, + type NativeSyntheticEvent, +} from "react-native"; +import { + Screen, + ScreenStack, + ScreenStackHeaderConfig, + ScreenStackHeaderSearchBarView, + SearchBar, + type SearchBarCommands, +} from "react-native-screens"; import { useWorkspaceState } from "../../state/workspace"; import { useThemeColor } from "../../lib/useThemeColor"; import { useEnvironmentQuery } from "../../state/query"; @@ -44,7 +66,11 @@ import { } from "../terminal/terminalLaunchContext"; import { terminalDebugLog } from "../terminal/terminalDebugLog"; import { ThreadDetailScreen } from "./ThreadDetailScreen"; -import { ThreadGitControls, useThreadGitCenterHeaderItems } from "./ThreadGitControls"; +import { + ThreadGitControls, + useThreadGitCenterHeaderItems, + useThreadGitRightHeaderItems, +} from "./ThreadGitControls"; import { GitOverviewSheet } from "./git/GitOverviewSheet"; import { ThreadNavigationDrawer } from "./ThreadNavigationDrawer"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -76,9 +102,14 @@ interface ThreadInspectorSelection { type NativeHeaderItems = ReturnType< NonNullable >; +type RnsHeaderItems = ComponentProps["headerRightBarButtonItems"]; const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); +function asRnsHeaderItems(items: NativeHeaderItems | undefined): RnsHeaderItems { + return items as unknown as RnsHeaderItems; +} + function InspectorPaneRoleActivation() { useAdaptiveWorkspacePaneRole("inspector"); return null; @@ -632,6 +663,25 @@ function ThreadRouteContent( onRunAction: gitActions.onRunSelectedThreadGitAction, }; const threadCenterHeaderItems = useThreadGitCenterHeaderItems(threadGitControlProps); + const compactRightHeaderItems = useThreadGitRightHeaderItems(threadGitControlProps); + const compactLeftHeaderItems = useMemo( + () => [ + withNativeGlassHeaderItem({ + accessibilityLabel: "Back to threads", + icon: { name: "chevron.left", type: "sfSymbol" as const }, + identifier: "thread-compact-back", + onPress: () => { + if (router.canGoBack()) { + router.back(); + return; + } + router.replace("/"); + }, + type: "button" as const, + }), + ], + [router], + ); const splitLeftHeaderItems = useMemo( () => [ { @@ -690,6 +740,137 @@ function ThreadRouteContent( connectionState: routeConnectionState, }); const serverConfig = routeEnvironmentRuntime?.serverConfig ?? null; + const renderThreadRouteBody = (showActionControls: boolean) => ( + <> + + + + + + + + + {layout.usesSplitView ? null : ( + setDrawerVisible(false)} + onSelectThread={(thread) => { + router.replace(buildThreadRoutePath(thread)); + }} + onStartNewTask={() => router.push("/new")} + /> + )} + + + + ); + + if (usesNativeHeaderGlass) { + return ( + <> + {activeInspectorRenderer ? : null} + + + + {renderThreadRouteBody(false)} + + {usesThreadSearchToolbar ? ( + + { + setPrimarySidebarSearchQuery(""); + }} + onChangeText={(event: NativeSyntheticEvent<{ readonly text?: string }>) => { + setPrimarySidebarSearchQuery(event.nativeEvent.text ?? ""); + }} + placement="integratedButton" + placeholder="Search" + textColor={foregroundColor} + tintColor={iconColor} + /> + + ) : null} + + + + + ); + } return ( <> @@ -756,68 +937,7 @@ function ThreadRouteContent( )} - - - - - - - - - {layout.usesSplitView ? null : ( - setDrawerVisible(false)} - onSelectThread={(thread) => { - router.replace(buildThreadRoutePath(thread)); - }} - onStartNewTask={() => router.push("/new")} - /> - )} - - + {renderThreadRouteBody(!layout.usesSplitView)} ); } diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts b/apps/mobile/src/lib/native-scroll-edge-effect.test.ts index f110fad905c..e8719827f31 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts +++ b/apps/mobile/src/lib/native-scroll-edge-effect.test.ts @@ -10,9 +10,9 @@ describe("nativeTopScrollEdgeEffect", () => { expect(nativeTopScrollEdgeEffect("ios", "26.5")).toBe("automatic"); }); - it("uses the softer native treatment on iOS 27 and later", () => { - expect(nativeTopScrollEdgeEffect("ios", "27.0")).toBe("soft"); - expect(nativeTopScrollEdgeEffect("ios", 28)).toBe("soft"); + it("keeps UIKit automatic scroll-edge sampling on iOS 27 and later", () => { + expect(nativeTopScrollEdgeEffect("ios", "27.0")).toBe("automatic"); + expect(nativeTopScrollEdgeEffect("ios", 28)).toBe("automatic"); }); it("does not apply the iOS workaround to other platforms", () => { @@ -23,7 +23,7 @@ describe("nativeTopScrollEdgeEffect", () => { describe("nativeHeaderScrollEdgeEffects", () => { it("keeps non-top header edges hidden while applying the platform top effect", () => { expect(nativeHeaderScrollEdgeEffects("ios", "27.0")).toEqual({ - top: "soft", + top: "automatic", bottom: "hidden", left: "hidden", right: "hidden", diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.ts b/apps/mobile/src/lib/native-scroll-edge-effect.ts index aacabcdd22c..867a4a9ccda 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.ts +++ b/apps/mobile/src/lib/native-scroll-edge-effect.ts @@ -6,25 +6,18 @@ export type NativeHeaderScrollEdgeEffects = { readonly right: "hidden"; }; -function majorVersion(version: number | string): number { - if (typeof version === "number") { - return Math.trunc(version); - } - - const parsed = Number.parseInt(version, 10); - return Number.isNaN(parsed) ? 0 : parsed; -} - -/** - * iOS 27's system apps use a soft scroll-edge treatment for Messages-style - * chrome. Avoid the `hard` style here: it adds the dividing line that makes the - * header feel custom and heavier than Messages/Mail. - */ export function nativeTopScrollEdgeEffect( os: string, - version: number | string, + _version: number | string, ): NativeTopScrollEdgeEffect { - return os === "ios" && majorVersion(version) >= 27 ? "soft" : "automatic"; + if (os !== "ios") { + return "automatic"; + } + + // The standalone RNS/Mail spike that matched Messages/GitHub used UIKit's + // automatic scroll-edge behavior. Forcing `soft` on iOS 27 makes production + // look like a local overlay instead of sampling the app content edge-to-edge. + return "automatic"; } export function nativeHeaderScrollEdgeEffects( From f6df16ab1ebf0bbcc718e84a514da2ffd70b2948 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 11:41:13 -0700 Subject: [PATCH 68/71] Replace Expo Router with native stack navigation --- apps/mobile/app.config.ts | 1 - apps/mobile/index.ts | 6 +- apps/mobile/package.json | 4 +- apps/mobile/src/App.tsx | 542 ++++++++++++++++++ apps/mobile/src/app/_layout.tsx | 206 ------- apps/mobile/src/app/connections/_layout.tsx | 29 - apps/mobile/src/app/new/_layout.tsx | 52 -- apps/mobile/src/app/settings/_layout.tsx | 62 -- .../[environmentId]/[threadId]/_layout.tsx | 113 ---- .../[threadId]/git/_layout.tsx | 58 -- .../[environmentId]/[threadId]/git/review.tsx | 18 - .../agent-awareness/notificationNavigation.ts | 2 +- .../archive/ArchivedThreadsRouteScreen.tsx | 2 +- .../archive/ArchivedThreadsScreen.tsx | 2 +- .../features/files/ThreadFilesRouteScreen.tsx | 4 +- apps/mobile/src/features/home/HomeHeader.tsx | 2 +- .../HardwareKeyboardCommandProvider.tsx | 2 +- .../layout/AdaptiveWorkspaceLayout.tsx | 7 +- .../layout/workspace-sidebar-toolbar.tsx | 2 +- .../features/projects/AddProjectScreen.tsx | 2 +- .../review/ReviewCommentComposerSheet.tsx | 2 +- .../src/features/review/ReviewSheet.tsx | 6 +- .../useReviewCommentSelectionController.ts | 2 +- .../terminal/ThreadTerminalRouteScreen.tsx | 2 +- .../features/threads/NewTaskDraftScreen.tsx | 2 +- .../features/threads/ThreadDetailScreen.tsx | 2 +- .../src/features/threads/ThreadFeed.tsx | 2 +- .../features/threads/ThreadGitControls.tsx | 15 +- .../threads/ThreadNavigationSidebar.tsx | 2 +- .../features/threads/ThreadRouteScreen.tsx | 7 +- .../features/threads/git/GitBranchesSheet.tsx | 2 +- .../features/threads/git/GitCommitSheet.tsx | 2 +- .../features/threads/git/GitConfirmSheet.tsx | 2 +- .../features/threads/git/GitOverviewSheet.tsx | 2 +- apps/mobile/src/lib/routes.ts | 2 +- apps/mobile/src/navigation/route-model.ts | 375 ++++++++++++ apps/mobile/src/navigation/router.tsx | 536 +++++++++++++++++ .../src/{app => screens}/+not-found.tsx | 2 +- .../{app => screens}/connections/index.tsx | 2 +- .../src/{app => screens}/connections/new.tsx | 2 +- .../src/{app => screens}/debug/rns-glass.tsx | 2 +- apps/mobile/src/{app => screens}/index.tsx | 2 +- .../new/add-project/destination.tsx | 0 .../new/add-project/index.tsx | 0 .../new/add-project/local.tsx | 0 .../new/add-project/repository.tsx | 2 +- .../mobile/src/{app => screens}/new/draft.tsx | 2 +- .../mobile/src/{app => screens}/new/index.tsx | 2 +- .../src/{app => screens}/settings/archive.tsx | 0 .../src/{app => screens}/settings/auth.tsx | 2 +- .../settings/environment-new.tsx | 0 .../settings/environments.tsx | 2 +- .../src/{app => screens}/settings/index.tsx | 2 +- .../{app => screens}/settings/waitlist.tsx | 2 +- .../[threadId]/files/[...path].tsx | 0 .../[threadId]/files/index.tsx | 0 .../[threadId]/git-confirm.tsx | 0 .../[threadId]/git/branches.tsx | 0 .../[environmentId]/[threadId]/git/commit.tsx | 0 .../[environmentId]/[threadId]/git/index.tsx | 0 .../[environmentId]/[threadId]/index.tsx | 0 .../[threadId]/review-comment.tsx | 0 .../[environmentId]/[threadId]/review.tsx | 0 .../[environmentId]/[threadId]/terminal.tsx | 0 apps/mobile/src/state/use-thread-selection.ts | 2 +- patches/expo-router@56.2.11.patch | 115 ---- pnpm-lock.yaml | 153 ++++- pnpm-workspace.yaml | 1 - 68 files changed, 1648 insertions(+), 726 deletions(-) create mode 100644 apps/mobile/src/App.tsx delete mode 100644 apps/mobile/src/app/_layout.tsx delete mode 100644 apps/mobile/src/app/connections/_layout.tsx delete mode 100644 apps/mobile/src/app/new/_layout.tsx delete mode 100644 apps/mobile/src/app/settings/_layout.tsx delete mode 100644 apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx delete mode 100644 apps/mobile/src/app/threads/[environmentId]/[threadId]/git/_layout.tsx delete mode 100644 apps/mobile/src/app/threads/[environmentId]/[threadId]/git/review.tsx create mode 100644 apps/mobile/src/navigation/route-model.ts create mode 100644 apps/mobile/src/navigation/router.tsx rename apps/mobile/src/{app => screens}/+not-found.tsx (96%) rename apps/mobile/src/{app => screens}/connections/index.tsx (98%) rename apps/mobile/src/{app => screens}/connections/new.tsx (99%) rename apps/mobile/src/{app => screens}/debug/rns-glass.tsx (99%) rename apps/mobile/src/{app => screens}/index.tsx (98%) rename apps/mobile/src/{app => screens}/new/add-project/destination.tsx (100%) rename apps/mobile/src/{app => screens}/new/add-project/index.tsx (100%) rename apps/mobile/src/{app => screens}/new/add-project/local.tsx (100%) rename apps/mobile/src/{app => screens}/new/add-project/repository.tsx (90%) rename apps/mobile/src/{app => screens}/new/draft.tsx (91%) rename apps/mobile/src/{app => screens}/new/index.tsx (99%) rename apps/mobile/src/{app => screens}/settings/archive.tsx (100%) rename apps/mobile/src/{app => screens}/settings/auth.tsx (93%) rename apps/mobile/src/{app => screens}/settings/environment-new.tsx (100%) rename apps/mobile/src/{app => screens}/settings/environments.tsx (99%) rename apps/mobile/src/{app => screens}/settings/index.tsx (99%) rename apps/mobile/src/{app => screens}/settings/waitlist.tsx (94%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/files/[...path].tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/files/index.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/git-confirm.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/git/branches.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/git/commit.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/git/index.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/index.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/review-comment.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/review.tsx (100%) rename apps/mobile/src/{app => screens}/threads/[environmentId]/[threadId]/terminal.tsx (100%) delete mode 100644 patches/expo-router@56.2.11.patch diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index 1dd884aeb8d..b6c3b4dd3da 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -109,7 +109,6 @@ const config: ExpoConfig = { favicon: "./assets/favicon.png", }, plugins: [ - "expo-router", "expo-font", "expo-secure-store", ["@clerk/expo", { theme: "./clerk-theme.json" }], diff --git a/apps/mobile/index.ts b/apps/mobile/index.ts index 642e1b77be9..febabbfe399 100644 --- a/apps/mobile/index.ts +++ b/apps/mobile/index.ts @@ -1,2 +1,6 @@ import "react-native-gesture-handler"; -import "expo-router/entry"; +import { registerRootComponent } from "expo"; + +import App from "./src/App"; + +registerRootComponent(App); diff --git a/apps/mobile/package.json b/apps/mobile/package.json index c9e8051ede3..4212c221696 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -50,6 +50,9 @@ "@noble/hashes": "catalog:", "@pierre/diffs": "catalog:", "@react-native-menu/menu": "^2.0.0", + "@react-navigation/elements": "2.9.26", + "@react-navigation/native": "7.3.4", + "@react-navigation/native-stack": "7.17.6", "@shikijs/core": "4.2.0", "@shikijs/engine-javascript": "4.2.0", "@shikijs/langs": "4.2.0", @@ -81,7 +84,6 @@ "expo-network": "~56.0.5", "expo-notifications": "~56.0.18", "expo-paste-input": "^0.1.15", - "expo-router": "~56.2.11", "expo-secure-store": "~56.0.4", "expo-splash-screen": "~56.0.10", "expo-symbols": "~56.0.6", diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx new file mode 100644 index 00000000000..543a9f64e84 --- /dev/null +++ b/apps/mobile/src/App.tsx @@ -0,0 +1,542 @@ +import { + DMSans_400Regular, + DMSans_500Medium, + DMSans_700Bold, + useFonts, +} from "@expo-google-fonts/dm-sans"; +import { NavigationContainer, type NavigationContainerRef } from "@react-navigation/native"; +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import * as Linking from "expo-linking"; +import { use, useCallback, useMemo, useRef, useState, type ReactNode } from "react"; +import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; +import { GestureHandlerRootView } from "react-native-gesture-handler"; +import { KeyboardProvider } from "react-native-keyboard-controller"; +import { SafeAreaProvider } from "react-native-safe-area-context"; +import { useResolveClassNames } from "uniwind"; + +import { RegistryContext } from "@effect/atom-react"; +import { LoadingScreen } from "./components/LoadingScreen"; +import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; +import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; +import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider"; +import { + ClerkSettingsSheetDetentProvider, + useClerkSettingsSheetDetent, +} from "./features/cloud/ClerkSettingsSheetDetent"; +import { + AdaptiveWorkspaceLayout, + useAdaptiveWorkspaceLayout, +} from "./features/layout/AdaptiveWorkspaceLayout"; +import { deriveStableFormSheetDetent } from "./lib/layout"; +import { useThemeColor } from "./lib/useThemeColor"; +import { appAtomRegistry } from "./state/atom-registry"; +import { ThreadSelectionProvider } from "./state/use-thread-selection"; +import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; +import HomeRouteScreen from "./screens/index"; +import SettingsRouteScreen from "./screens/settings/index"; +import SettingsEnvironmentsRouteScreen from "./screens/settings/environments"; +import SettingsAuthRouteScreen from "./screens/settings/auth"; +import SettingsWaitlistRouteScreen from "./screens/settings/waitlist"; +import ConnectionsRouteScreen from "./screens/connections/index"; +import ConnectionsNewRouteScreen from "./screens/connections/new"; +import NewTaskRoute from "./screens/new/index"; +import AddProjectRoute from "./screens/new/add-project/index"; +import AddProjectRepositoryRoute from "./screens/new/add-project/repository"; +import AddProjectDestinationRoute from "./screens/new/add-project/destination"; +import AddProjectLocalRoute from "./screens/new/add-project/local"; +import NewTaskDraftRoute from "./screens/new/draft"; +import RnsGlassDebugRoute from "./screens/debug/rns-glass"; +import NotFoundRoute from "./screens/+not-found"; +import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen"; +import { ReviewHighlighterProvider } from "./features/review/ReviewHighlighterProvider"; +import { ReviewCommentComposerSheet } from "./features/review/ReviewCommentComposerSheet"; +import { ReviewSheet } from "./features/review/ReviewSheet"; +import { ThreadTerminalRouteScreen } from "./features/terminal/ThreadTerminalRouteScreen"; +import { ThreadRouteScreen } from "./features/threads/ThreadRouteScreen"; +import { GitBranchesSheet } from "./features/threads/git/GitBranchesSheet"; +import { GitCommitSheet } from "./features/threads/git/GitCommitSheet"; +import { GitConfirmSheet } from "./features/threads/git/GitConfirmSheet"; +import { GitOverviewSheet } from "./features/threads/git/GitOverviewSheet"; +import { HardwareKeyboardCommandProvider } from "./features/keyboard/HardwareKeyboardCommandProvider"; +import { + AppNavigationContext, + createRouter, + pathAndParamsFromCurrentRoute, +} from "./navigation/router"; +import { + getFocusedRoute, + type AppStackParamList, + type RouteParams, +} from "./navigation/route-model"; + +import "../global.css"; + +const RootStack = createNativeStackNavigator(); + +const linking = { + prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], + config: { + screens: { + Home: "", + DebugRnsGlass: "debug/rns-glass", + Settings: "settings", + SettingsEnvironments: "settings/environments", + SettingsEnvironmentNew: "settings/environment-new", + SettingsArchive: "settings/archive", + SettingsAuth: "settings/auth", + SettingsWaitlist: "settings/waitlist", + Connections: "connections", + ConnectionsNew: "connections/new", + NewTask: "new", + AddProject: "new/add-project", + AddProjectRepository: "new/add-project/repository", + AddProjectDestination: "new/add-project/destination", + AddProjectLocal: "new/add-project/local", + NewTaskDraft: "new/draft", + Thread: "threads/:environmentId/:threadId", + ThreadTerminal: "threads/:environmentId/:threadId/terminal", + ThreadReview: "threads/:environmentId/:threadId/review", + ThreadReviewComment: "threads/:environmentId/:threadId/review-comment", + ThreadFiles: "threads/:environmentId/:threadId/files", + ThreadFile: "threads/:environmentId/:threadId/files/:path*", + GitOverview: "threads/:environmentId/:threadId/git", + GitCommit: "threads/:environmentId/:threadId/git/commit", + GitBranches: "threads/:environmentId/:threadId/git/branches", + GitConfirm: "threads/:environmentId/:threadId/git-confirm", + NotFound: "*", + }, + }, +}; + +function ThreadSelectionRoute(props: { readonly children: ReactNode }) { + return {props.children}; +} + +function ThreadRoute() { + return ( + + + + ); +} + +function ThreadTerminalRoute() { + return ( + + + + ); +} + +function ThreadFilesRoute() { + return ( + + + + ); +} + +function ThreadFileRoute() { + return ( + + + + ); +} + +function ThreadReviewRoute() { + return ( + + + + + + ); +} + +function ThreadReviewCommentRoute() { + return ( + + + + ); +} + +function GitOverviewRoute() { + return ( + + + + ); +} + +function GitCommitRoute() { + return ( + + + + ); +} + +function GitBranchesRoute() { + return ( + + + + ); +} + +function GitConfirmRoute() { + return ( + + + + ); +} + +function AppNavigationProvider(props: { readonly children: ReactNode }) { + const navigationRef = useRef>(null); + const [pathname, setPathname] = useState("/"); + const [params, setParams] = useState({}); + const router = useMemo(() => createRouter(navigationRef), []); + const contextValue = useMemo( + () => ({ + navigationRef, + pathname, + params, + router, + }), + [params, pathname, router], + ); + const syncState = useCallback(() => { + const route = getFocusedRoute(navigationRef.current?.getRootState()); + if (!route) { + setPathname("/"); + setParams({}); + return; + } + + const current = pathAndParamsFromCurrentRoute(route); + setPathname(current.pathname); + setParams(current.params); + }, []); + + return ( + + + {props.children} + + + ); +} + +function AppNavigator() { + const pathname = usePathnameFromContext(); + const expandedSettingsRouteIsActive = + pathname === "/settings/archive" || pathname === "/settings/auth"; + + return ( + + + + ); +} + +function usePathnameFromContext() { + const context = use(AppNavigationContext); + if (context === null) { + return "/"; + } + return context.pathname; +} + +function AppNavigatorContent() { + const pathname = usePathnameFromContext(); + const isDebugRoute = pathname.startsWith("/debug/"); + + if (isDebugRoute) { + return ; + } + + return ; +} + +function DebugNavigatorHost() { + return ( + <> + + + + + + + + ); +} + +function WorkspaceNavigatorHost() { + const colorScheme = useColorScheme(); + const statusBarBg = useThemeColor("--color-status-bar"); + useAgentNotificationNavigation(); + useThreadOutboxDrain(); + + return ( + <> + + + + + + ); +} + +function WorkspaceNavigator() { + const { collapse, isExpanded } = useClerkSettingsSheetDetent(); + const { layout } = useAdaptiveWorkspaceLayout(); + const { height } = useWindowDimensions(); + const sheetStyle = useResolveClassNames("bg-sheet"); + + const handleSettingsTransitionEnd = useCallback( + (event: { data: { closing: boolean } }) => { + if (event.data.closing) { + collapse(); + } + }, + [collapse], + ); + + const connectionSheetScreenOptions = { + contentStyle: sheetStyle, + gestureEnabled: true, + headerShown: false, + presentation: "formSheet" as const, + sheetAllowedDetents: [0.55, 0.7], + sheetGrabberVisible: true, + }; + const settingsScreenOptions = layout.usesSplitView + ? { + animation: "none" as const, + contentStyle: sheetStyle, + gestureEnabled: false, + headerShown: false, + presentation: "card" as const, + } + : { + ...connectionSheetScreenOptions, + sheetAllowedDetents: isExpanded ? [0.92] : [0.7], + }; + const newTaskScreenOptions = { + contentStyle: sheetStyle, + gestureEnabled: true, + headerShown: false, + presentation: "formSheet" as const, + sheetAllowedDetents: [layout.usesSplitView ? deriveStableFormSheetDetent(height) : 0.92], + sheetGrabberVisible: !layout.usesSplitView, + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} + +export default function App() { + const [fontsLoaded] = useFonts({ + DMSans_400Regular, + DMSans_500Medium, + DMSans_700Bold, + }); + + return ( + + + + + + + + {fontsLoaded ? ( + + ) : ( + + )} + + + + + + + + ); +} diff --git a/apps/mobile/src/app/_layout.tsx b/apps/mobile/src/app/_layout.tsx deleted file mode 100644 index 159bd509743..00000000000 --- a/apps/mobile/src/app/_layout.tsx +++ /dev/null @@ -1,206 +0,0 @@ -import { - DMSans_400Regular, - DMSans_500Medium, - DMSans_700Bold, - useFonts, -} from "@expo-google-fonts/dm-sans"; -import { usePathname } from "expo-router"; -import Stack from "expo-router/stack"; -import { useCallback } from "react"; -import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; -import { GestureHandlerRootView } from "react-native-gesture-handler"; -import { KeyboardProvider } from "react-native-keyboard-controller"; -import { SafeAreaProvider } from "react-native-safe-area-context"; -import { useResolveClassNames } from "uniwind"; - -import { LoadingScreen } from "../components/LoadingScreen"; - -import { useThreadOutboxDrain } from "../state/use-thread-outbox-drain"; -import { RegistryContext } from "@effect/atom-react"; -import { appAtomRegistry } from "../state/atom-registry"; -import { CloudAuthProvider } from "../features/cloud/CloudAuthProvider"; -import { - ClerkSettingsSheetDetentProvider, - useClerkSettingsSheetDetent, -} from "../features/cloud/ClerkSettingsSheetDetent"; -import { useAgentNotificationNavigation } from "../features/agent-awareness/notificationNavigation"; -import { - AdaptiveWorkspaceLayout, - useAdaptiveWorkspaceLayout, -} from "../features/layout/AdaptiveWorkspaceLayout"; -import { deriveStableFormSheetDetent } from "../lib/layout"; -import { useThemeColor } from "../lib/useThemeColor"; -import { HardwareKeyboardCommandProvider } from "../features/keyboard/HardwareKeyboardCommandProvider"; - -require("../../global.css"); - -function AppNavigator() { - const pathname = usePathname(); - const expandedSettingsRouteIsActive = - pathname === "/settings/archive" || pathname === "/settings/auth"; - - return ( - - - - ); -} - -function AppNavigatorContent() { - const pathname = usePathname(); - const isDebugRoute = pathname.startsWith("/debug/"); - - if (isDebugRoute) { - return ; - } - - return ; -} - -function DebugNavigatorHost() { - return ( - <> - - - - - - ); -} - -function WorkspaceNavigatorHost() { - const colorScheme = useColorScheme(); - const statusBarBg = useThemeColor("--color-status-bar"); - useAgentNotificationNavigation(); - useThreadOutboxDrain(); - - return ( - <> - - - - - - ); -} - -function WorkspaceNavigator() { - const { collapse, isExpanded } = useClerkSettingsSheetDetent(); - const { layout } = useAdaptiveWorkspaceLayout(); - const { height } = useWindowDimensions(); - const sheetStyle = useResolveClassNames("bg-sheet"); - - const handleSettingsTransitionEnd = useCallback( - (event: { data: { closing: boolean } }) => { - if (event.data.closing) { - collapse(); - } - }, - [collapse], - ); - - const connectionSheetScreenOptions = { - contentStyle: sheetStyle, - gestureEnabled: true, - headerShown: false, - presentation: "formSheet" as const, - sheetAllowedDetents: [0.55, 0.7], - sheetGrabberVisible: true, - }; - const settingsScreenOptions = layout.usesSplitView - ? { - animation: "none" as const, - contentStyle: sheetStyle, - gestureEnabled: false, - headerShown: false, - presentation: "card" as const, - } - : { - ...connectionSheetScreenOptions, - sheetAllowedDetents: isExpanded ? [0.92] : [0.7], - }; - const newTaskScreenOptions = { - contentStyle: sheetStyle, - gestureEnabled: true, - headerShown: false, - presentation: "formSheet" as const, - sheetAllowedDetents: [layout.usesSplitView ? deriveStableFormSheetDetent(height) : 0.92], - sheetGrabberVisible: !layout.usesSplitView, - }; - - return ( - - - - - - - - - ); -} - -export default function RootLayout() { - const [fontsLoaded] = useFonts({ - DMSans_400Regular, - DMSans_500Medium, - DMSans_700Bold, - }); - return ( - - - - - - - {fontsLoaded ? ( - - ) : ( - - )} - - - - - - - ); -} diff --git a/apps/mobile/src/app/connections/_layout.tsx b/apps/mobile/src/app/connections/_layout.tsx deleted file mode 100644 index 902b53cb15a..00000000000 --- a/apps/mobile/src/app/connections/_layout.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import Stack from "expo-router/stack"; -import { useResolveClassNames } from "uniwind"; -import { useThemeColor } from "../../lib/useThemeColor"; - -export const unstable_settings = { - anchor: "index", -}; - -export default function ConnectionsLayout() { - const contentStyle = useResolveClassNames("bg-sheet"); - const connSheetBg = useThemeColor("--color-sheet"); - const headerTint = useThemeColor("--color-foreground"); - - return ( - - - - - ); -} diff --git a/apps/mobile/src/app/new/_layout.tsx b/apps/mobile/src/app/new/_layout.tsx deleted file mode 100644 index 78720ac686e..00000000000 --- a/apps/mobile/src/app/new/_layout.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import Stack from "expo-router/stack"; -import { Platform } from "react-native"; -import { useResolveClassNames } from "uniwind"; - -import { NewTaskFlowProvider } from "../../features/threads/new-task-flow-provider"; -import { useThemeColor } from "../../lib/useThemeColor"; - -export const unstable_settings = { - anchor: "index", -}; - -export default function NewTaskLayout() { - const sheetStyle = useResolveClassNames("bg-sheet"); - const sheetBg = useThemeColor("--color-sheet"); - const headerTint = useThemeColor("--color-foreground"); - - return ( - - - - - - - - - - - ); -} diff --git a/apps/mobile/src/app/settings/_layout.tsx b/apps/mobile/src/app/settings/_layout.tsx deleted file mode 100644 index 2607c2cd1f1..00000000000 --- a/apps/mobile/src/app/settings/_layout.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import Stack from "expo-router/stack"; -import { useCallback } from "react"; -import { useResolveClassNames } from "uniwind"; - -import { useClerkSettingsSheetDetent } from "../../features/cloud/ClerkSettingsSheetDetent"; -import { useThemeColor } from "../../lib/useThemeColor"; - -export const unstable_settings = { - anchor: "index", -}; - -export default function SettingsLayout() { - const { collapse } = useClerkSettingsSheetDetent(); - const contentStyle = useResolveClassNames("bg-sheet"); - const sheetBg = useThemeColor("--color-sheet"); - const headerTint = useThemeColor("--color-foreground"); - const handleExpandedRouteTransitionEnd = useCallback( - (event: { data: { closing: boolean } }) => { - if (event.data.closing) { - collapse(); - } - }, - [collapse], - ); - - return ( - - - - - - - - - ); -} diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx deleted file mode 100644 index d69e42310bd..00000000000 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/_layout.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import Stack from "expo-router/stack"; -import { StyleSheet } from "react-native"; -import { useResolveClassNames } from "uniwind"; -import { useAdaptiveWorkspaceLayout } from "../../../../features/layout/AdaptiveWorkspaceLayout"; -import { ThreadSelectionProvider } from "../../../../state/use-thread-selection"; - -export default function ThreadLayout() { - const { fileInspector } = useAdaptiveWorkspaceLayout(); - const sheetStyle = StyleSheet.flatten(useResolveClassNames("bg-sheet")); - const headerBg = { - backgroundColor: (sheetStyle as { backgroundColor?: string })?.backgroundColor, - }; - - return ( - - - - - - - - - - - - - ); -} diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/_layout.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/_layout.tsx deleted file mode 100644 index 15b10c295f3..00000000000 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/_layout.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import Stack from "expo-router/stack"; -import { StyleSheet } from "react-native"; -import { useResolveClassNames } from "uniwind"; - -export const unstable_settings = { - anchor: "index", -}; - -export default function GitSheetLayout() { - const sheetStyle = StyleSheet.flatten(useResolveClassNames("bg-sheet")); - const headerBg = { - backgroundColor: (sheetStyle as { backgroundColor?: string })?.backgroundColor, - }; - - return ( - - - - - - - ); -} diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/review.tsx b/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/review.tsx deleted file mode 100644 index 8ca686c1469..00000000000 --- a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/review.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { Redirect, useLocalSearchParams } from "expo-router"; -import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; - -export default function ReviewRoute() { - const { environmentId, threadId } = useLocalSearchParams<{ - environmentId: EnvironmentId; - threadId: ThreadId; - }>(); - - return ( - - ); -} diff --git a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts index 18bb93d723e..03aacf2ca30 100644 --- a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts +++ b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts @@ -1,6 +1,6 @@ import { useEffect, useRef } from "react"; import * as Notifications from "expo-notifications"; -import { useRouter } from "expo-router"; +import { useRouter } from "../../navigation/router"; import { routeAgentNotificationResponseOnce } from "./notificationPayload"; import { consumeLastAgentNotificationResponse } from "./notificationResponseConsumer"; diff --git a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx index d560f8db9fa..8253481bf26 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx @@ -1,7 +1,7 @@ import type { EnvironmentId } from "@t3tools/contracts"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; -import { useFocusEffect } from "expo-router"; +import { useFocusEffect } from "../../navigation/router"; import { useCallback, useMemo, useState } from "react"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 292828c4345..354da264946 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -4,7 +4,7 @@ import type { } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; -import { Stack } from "expo-router"; +import { Stack } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import { useCallback, useMemo, useRef, type ComponentProps } from "react"; import { diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 2fa912243d7..083ba56d7a4 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -1,6 +1,6 @@ -import Stack from "expo-router/stack"; +import Stack from "../../navigation/router"; import { SymbolView } from "expo-symbols"; -import { useLocalSearchParams, useRouter } from "expo-router"; +import { useLocalSearchParams, useRouter } from "../../navigation/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index e68e07c683d..287c40c85bf 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -3,7 +3,7 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import { Stack } from "expo-router"; +import { Stack } from "../../navigation/router"; import { useCallback, useRef } from "react"; import { Platform } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; diff --git a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx index 4db0449ee4d..b026e8c6461 100644 --- a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx +++ b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx @@ -1,4 +1,4 @@ -import { usePathname, useRouter } from "expo-router"; +import { usePathname, useRouter } from "../../navigation/router"; import { useCallback, useMemo, useSyncExternalStore, type PropsWithChildren } from "react"; import { diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 4cd73883a24..bcb69a0c93e 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,6 +1,11 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useFocusEffect, useGlobalSearchParams, usePathname, useRouter } from "expo-router"; +import { + useFocusEffect, + useGlobalSearchParams, + usePathname, + useRouter, +} from "../../navigation/router"; import { createContext, use, diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx index c38304428d3..3db7c343ccc 100644 --- a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx +++ b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx @@ -1,4 +1,4 @@ -import { Stack } from "expo-router"; +import { Stack } from "../../navigation/router"; import type { ReactNode } from "react"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index fa1f635de8d..db00a9d2028 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -22,7 +22,7 @@ import { isFilesystemBrowseQuery, } from "@t3tools/client-runtime/state/projects"; import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts"; -import { useLocalSearchParams, useRouter } from "expo-router"; +import { useLocalSearchParams, useRouter } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index d35c48e8a9b..862949db3b4 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -1,4 +1,4 @@ -import { useLocalSearchParams, useRouter } from "expo-router"; +import { useLocalSearchParams, useRouter } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import { TextInputWrapper } from "expo-paste-input"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 0199d693c82..fc78caf5c53 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -1,7 +1,7 @@ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useLocalSearchParams } from "expo-router"; -import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; -import Stack from "expo-router/stack"; +import { useLocalSearchParams } from "../../navigation/router"; +import { useHeaderHeight } from "@react-navigation/elements"; +import Stack from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import { memo, diff --git a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts index ae1dc6a0575..30549bfc7a0 100644 --- a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts +++ b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { NativeSyntheticEvent } from "react-native"; -import { useRouter } from "expo-router"; +import { useRouter } from "../../navigation/router"; import * as Arr from "effect/Array"; import { pipe } from "effect/Function"; import * as Result from "effect/Result"; diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index e386098d7bb..6e2c3866671 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -1,7 +1,7 @@ import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; import { SymbolView } from "expo-symbols"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { Stack, useLocalSearchParams, useRouter } from "../../navigation/router"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Platform, Pressable, Text as RNText, View, useColorScheme } from "react-native"; import { diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index ce24198f5e2..60a5cac1622 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -1,4 +1,4 @@ -import { Stack, useRouter } from "expo-router"; +import { Stack, useRouter } from "../../navigation/router"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { Alert, InteractionManager, View, useColorScheme } from "react-native"; import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller"; diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index 942dbd279bb..cc69d5552aa 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -15,7 +15,7 @@ import type { } from "@t3tools/contracts"; import { formatElapsed } from "@t3tools/shared/orchestrationTiming"; import * as Haptics from "expo-haptics"; -import { useHeaderHeight } from "expo-router/build/react-navigation/elements"; +import { useHeaderHeight } from "@react-navigation/elements"; import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { View, type GestureResponderEvent } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 6547d3781f5..89a977e8d7f 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -4,7 +4,7 @@ import { type LegendListRef } from "@legendapp/list/react-native"; import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts"; import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; import { SymbolView } from "expo-symbols"; -import { useRouter } from "expo-router"; +import { useRouter } from "../../navigation/router"; import { memo, useCallback, diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 2c117c4f6f6..022f55947a0 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -10,9 +10,8 @@ import { requiresDefaultBranchConfirmation, resolveQuickAction, } from "@t3tools/client-runtime/state/vcs"; -import { useLocalSearchParams, useRouter } from "expo-router"; -import type { NativeStackNavigationOptions } from "expo-router/build/react-navigation/native-stack/types"; -import Stack from "expo-router/stack"; +import { useLocalSearchParams, useRouter } from "../../navigation/router"; +import Stack from "../../navigation/router"; import { useCallback, useMemo } from "react"; import { Alert } from "react-native"; import { buildThreadFilesNavigation, buildThreadReviewRoutePath } from "../../lib/routes"; @@ -66,10 +65,8 @@ function compactMenuStatus(gitStatus: VcsStatusResult | null): string { return parts.join(" · "); } -type HeaderItems = ReturnType< - NonNullable ->; -type HeaderItem = HeaderItems[number]; +type HeaderItem = Record; +type HeaderItems = HeaderItem[]; type ThreadGitHeaderActionItems = { readonly terminal: HeaderItem; readonly files: HeaderItem; @@ -330,7 +327,7 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit type: "sfSymbol", }, label: compactMenuBranchLabel(model.currentBranchLabel), - onPress: () => {}, + onPress: (): void => {}, type: "action", }, { @@ -338,7 +335,7 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit disabled: model.quickAction.disabled, icon: { name: model.quickActionIcon, type: "sfSymbol" }, label: model.quickAction.label, - onPress: () => void model.runQuickAction(), + onPress: (): void => void model.runQuickAction(), type: "action", }, { diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 911a2d2f6b3..47642b37286 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -2,7 +2,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell import { LegendList } from "@legendapp/list/react-native"; import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "expo-symbols"; -import { useRouter } from "expo-router"; +import { useRouter } from "../../navigation/router"; import { memo, useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; import { Platform, Pressable, StyleSheet, TextInput, View, useColorScheme } from "react-native"; diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index ca2a9eac4c6..e8f5ba82d85 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,5 +1,4 @@ -import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "expo-router"; -import type { NativeStackNavigationOptions } from "expo-router/build/react-navigation/native-stack/types"; +import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "../../navigation/router"; import { useCallback, useEffect, @@ -99,9 +98,7 @@ interface ThreadInspectorSelection { readonly mode: ThreadInspectorMode; } -type NativeHeaderItems = ReturnType< - NonNullable ->; +type NativeHeaderItems = ReadonlyArray>; type RnsHeaderItems = ComponentProps["headerRightBarButtonItems"]; const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx index e27136702f2..da62cf856c9 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx @@ -1,5 +1,5 @@ import { sanitizeFeatureBranchName } from "@t3tools/shared/git"; -import { useRouter } from "expo-router"; +import { useRouter } from "../../../navigation/router"; import { useState } from "react"; import { Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index 76e0daf5f0a..5f9dd278b74 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "expo-router"; +import { useRouter } from "../../../navigation/router"; import { useCallback, useState } from "react"; import { Pressable, ScrollView, View, useColorScheme } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index 4272c31d2c1..bc0021cc9e5 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -2,7 +2,7 @@ import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime/st import { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; import * as Arr from "effect/Array"; import * as Result from "effect/Result"; -import { useLocalSearchParams, useRouter } from "expo-router"; +import { useLocalSearchParams, useRouter } from "../../../navigation/router"; import { useCallback, useMemo } from "react"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 1974f7a1869..c246c9b302b 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -5,7 +5,7 @@ import { requiresDefaultBranchConfirmation, } from "@t3tools/client-runtime/state/vcs"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useLocalSearchParams, useRouter } from "expo-router"; +import { useLocalSearchParams, useRouter } from "../../../navigation/router"; import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, type ComponentProps } from "react"; import { Alert, Platform, Pressable, ScrollView, View } from "react-native"; diff --git a/apps/mobile/src/lib/routes.ts b/apps/mobile/src/lib/routes.ts index 3a33e2ee0f9..a0ac64e68a2 100644 --- a/apps/mobile/src/lib/routes.ts +++ b/apps/mobile/src/lib/routes.ts @@ -1,4 +1,4 @@ -import type { Href, useRouter } from "expo-router"; +import type { Href, useRouter } from "../navigation/router"; import { type EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; diff --git a/apps/mobile/src/navigation/route-model.ts b/apps/mobile/src/navigation/route-model.ts new file mode 100644 index 00000000000..3da7df17649 --- /dev/null +++ b/apps/mobile/src/navigation/route-model.ts @@ -0,0 +1,375 @@ +import type { NavigationState, PartialState, Route } from "@react-navigation/native"; + +export type RouteParams = Record; + +export type AppRouteName = + | "Home" + | "Settings" + | "SettingsEnvironments" + | "SettingsEnvironmentNew" + | "SettingsArchive" + | "SettingsAuth" + | "SettingsWaitlist" + | "Connections" + | "ConnectionsNew" + | "NewTask" + | "AddProject" + | "AddProjectRepository" + | "AddProjectDestination" + | "AddProjectLocal" + | "NewTaskDraft" + | "Thread" + | "ThreadTerminal" + | "ThreadReview" + | "ThreadReviewComment" + | "ThreadFiles" + | "ThreadFile" + | "GitOverview" + | "GitCommit" + | "GitBranches" + | "GitConfirm" + | "DebugRnsGlass" + | "NotFound"; + +export type AppStackParamList = { + Home: undefined; + Settings: undefined; + SettingsEnvironments: undefined; + SettingsEnvironmentNew: RouteParams | undefined; + SettingsArchive: undefined; + SettingsAuth: undefined; + SettingsWaitlist: undefined; + Connections: undefined; + ConnectionsNew: RouteParams | undefined; + NewTask: undefined; + AddProject: RouteParams | undefined; + AddProjectRepository: RouteParams | undefined; + AddProjectDestination: RouteParams | undefined; + AddProjectLocal: RouteParams | undefined; + NewTaskDraft: RouteParams | undefined; + Thread: RouteParams; + ThreadTerminal: RouteParams; + ThreadReview: RouteParams; + ThreadReviewComment: RouteParams; + ThreadFiles: RouteParams; + ThreadFile: RouteParams; + GitOverview: RouteParams; + GitCommit: RouteParams; + GitBranches: RouteParams; + GitConfirm: RouteParams; + DebugRnsGlass: undefined; + NotFound: RouteParams | undefined; +}; + +export type AppHref = + | string + | { + readonly pathname: string; + readonly params?: RouteParams; + }; + +export type AppNavigationTarget = { + readonly name: AppRouteName; + readonly params?: RouteParams; +}; + +export type AppFocusedRoute = Pick, "name" | "params">; + +function normalizeParamValue(value: unknown): string | string[] | undefined { + if (value === undefined || value === null) { + return undefined; + } + if (Array.isArray(value)) { + return value.map((item) => String(item)); + } + return String(value); +} + +export function normalizeRouteParams( + params: Record | undefined, +): RouteParams | undefined { + if (!params) { + return undefined; + } + + const normalized: RouteParams = {}; + for (const [key, value] of Object.entries(params)) { + const normalizedValue = normalizeParamValue(value); + if (normalizedValue !== undefined) { + normalized[key] = normalizedValue; + } + } + return normalized; +} + +function withParams(name: AppRouteName, params?: Record): AppNavigationTarget { + return { name, params: normalizeRouteParams(params) }; +} + +function splitPathAndQuery(path: string): { + readonly pathname: string; + readonly query: RouteParams | undefined; +} { + const queryStart = path.indexOf("?"); + if (queryStart === -1) { + return { pathname: path, query: undefined }; + } + + const pathname = path.slice(0, queryStart); + const query = new URLSearchParams(path.slice(queryStart + 1)); + const params: RouteParams = {}; + for (const [key, value] of query.entries()) { + const existing = params[key]; + if (Array.isArray(existing)) { + existing.push(value); + } else if (existing !== undefined) { + params[key] = [existing, value]; + } else { + params[key] = value; + } + } + return { pathname, query: Object.keys(params).length > 0 ? params : undefined }; +} + +function pathSegments(pathname: string): string[] { + return pathname + .replace(/^\/+|\/+$/g, "") + .split("/") + .filter(Boolean) + .map((segment) => decodeURIComponent(segment)); +} + +function mergeParams( + base: Record | undefined, + extra: Record | undefined, +): RouteParams | undefined { + return normalizeRouteParams({ ...base, ...extra }); +} + +export function resolveNavigationTarget(href: AppHref): AppNavigationTarget { + if (typeof href !== "string") { + return resolveNavigationObject(href.pathname, href.params); + } + + const { pathname, query } = splitPathAndQuery(href); + const target = resolveNavigationPath(pathname); + return { name: target.name, params: mergeParams(target.params, query) }; +} + +function resolveNavigationObject( + pathname: string, + params: RouteParams | undefined, +): AppNavigationTarget { + switch (pathname) { + case "/threads/[environmentId]/[threadId]/terminal": + return withParams("ThreadTerminal", params); + case "/threads/[environmentId]/[threadId]/files": + return withParams("ThreadFiles", params); + case "/threads/[environmentId]/[threadId]/files/[...path]": + return withParams("ThreadFile", params); + case "/threads/[environmentId]/[threadId]/review": + return withParams("ThreadReview", params); + case "/threads/[environmentId]/[threadId]/review-comment": + return withParams("ThreadReviewComment", params); + case "/threads/[environmentId]/[threadId]/git": + return withParams("GitOverview", params); + case "/threads/[environmentId]/[threadId]/git/commit": + return withParams("GitCommit", params); + case "/threads/[environmentId]/[threadId]/git/branches": + return withParams("GitBranches", params); + case "/threads/[environmentId]/[threadId]/git-confirm": + return withParams("GitConfirm", params); + case "/new/draft": + return withParams("NewTaskDraft", params); + case "/new/add-project/repository": + return withParams("AddProjectRepository", params); + case "/new/add-project/destination": + return withParams("AddProjectDestination", params); + case "/new/add-project/local": + return withParams("AddProjectLocal", params); + default: { + const target = resolveNavigationPath(pathname); + return { ...target, params: mergeParams(target.params, params) }; + } + } +} + +export function resolveNavigationPath(pathname: string): AppNavigationTarget { + const segments = pathSegments(pathname); + + if (segments.length === 0) return { name: "Home" }; + if (segments[0] === "debug" && segments[1] === "rns-glass") return { name: "DebugRnsGlass" }; + + if (segments[0] === "settings") { + switch (segments[1]) { + case undefined: + return { name: "Settings" }; + case "environments": + return { name: "SettingsEnvironments" }; + case "environment-new": + return { name: "SettingsEnvironmentNew" }; + case "archive": + return { name: "SettingsArchive" }; + case "auth": + return { name: "SettingsAuth" }; + case "waitlist": + return { name: "SettingsWaitlist" }; + default: + return withParams("NotFound", { pathname }); + } + } + + if (segments[0] === "connections") { + return segments[1] === "new" ? { name: "ConnectionsNew" } : { name: "Connections" }; + } + + if (segments[0] === "new") { + if (segments[1] === "add-project") { + switch (segments[2]) { + case undefined: + return { name: "AddProject" }; + case "repository": + return { name: "AddProjectRepository" }; + case "destination": + return { name: "AddProjectDestination" }; + case "local": + return { name: "AddProjectLocal" }; + default: + return withParams("NotFound", { pathname }); + } + } + return segments[1] === "draft" ? { name: "NewTaskDraft" } : { name: "NewTask" }; + } + + if (segments[0] === "threads" && segments[1] && segments[2]) { + const baseParams = { environmentId: segments[1], threadId: segments[2] }; + switch (segments[3]) { + case undefined: + return withParams("Thread", baseParams); + case "terminal": + return withParams("ThreadTerminal", baseParams); + case "review": + return withParams("ThreadReview", baseParams); + case "review-comment": + return withParams("ThreadReviewComment", baseParams); + case "git": + if (segments[4] === "commit") return withParams("GitCommit", baseParams); + if (segments[4] === "branches") return withParams("GitBranches", baseParams); + if (segments[4] === "review") return withParams("ThreadReview", baseParams); + return withParams("GitOverview", baseParams); + case "git-confirm": + return withParams("GitConfirm", baseParams); + case "files": + if (segments.length > 4) { + return withParams("ThreadFile", { ...baseParams, path: segments.slice(4) }); + } + return withParams("ThreadFiles", baseParams); + default: + return withParams("NotFound", { pathname }); + } + } + + return withParams("NotFound", { pathname }); +} + +export function buildPathFromRoute(route: AppFocusedRoute): string { + const params = (route.params ?? {}) as RouteParams; + const environmentId = firstParam(params.environmentId); + const threadId = firstParam(params.threadId); + const path = params.path; + const pathSegmentsValue = Array.isArray(path) ? path : path ? [path] : []; + const query = new URLSearchParams(); + for (const [key, value] of Object.entries(params)) { + if (key === "environmentId" || key === "threadId" || key === "path") continue; + if (Array.isArray(value)) { + for (const item of value) query.append(key, item); + } else if (value !== undefined) { + query.set(key, value); + } + } + const querySuffix = query.size > 0 ? `?${query.toString()}` : ""; + + switch (route.name as AppRouteName) { + case "Home": + return "/"; + case "Settings": + return "/settings"; + case "SettingsEnvironments": + return "/settings/environments"; + case "SettingsEnvironmentNew": + return `/settings/environment-new${querySuffix}`; + case "SettingsArchive": + return "/settings/archive"; + case "SettingsAuth": + return "/settings/auth"; + case "SettingsWaitlist": + return "/settings/waitlist"; + case "Connections": + return "/connections"; + case "ConnectionsNew": + return `/connections/new${querySuffix}`; + case "NewTask": + return "/new"; + case "AddProject": + return "/new/add-project"; + case "AddProjectRepository": + return `/new/add-project/repository${querySuffix}`; + case "AddProjectDestination": + return `/new/add-project/destination${querySuffix}`; + case "AddProjectLocal": + return `/new/add-project/local${querySuffix}`; + case "NewTaskDraft": + return `/new/draft${querySuffix}`; + case "Thread": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}`; + case "ThreadTerminal": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/terminal${querySuffix}`; + case "ThreadReview": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/review`; + case "ThreadReviewComment": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/review-comment${querySuffix}`; + case "ThreadFiles": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/files${querySuffix}`; + case "ThreadFile": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/files/${pathSegmentsValue.map(encodeURIComponent).join("/")}${querySuffix}`; + case "GitOverview": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/git${querySuffix}`; + case "GitCommit": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/git/commit${querySuffix}`; + case "GitBranches": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/git/branches${querySuffix}`; + case "GitConfirm": + return `/threads/${encodeURIComponent(environmentId ?? "")}/${encodeURIComponent(threadId ?? "")}/git-confirm${querySuffix}`; + case "DebugRnsGlass": + return "/debug/rns-glass"; + case "NotFound": + return firstParam(params.pathname) ?? "/not-found"; + } +} + +export function buildPathFromState( + state: NavigationState | PartialState | undefined, +): string { + const route = getFocusedRoute(state); + return route ? buildPathFromRoute(route) : "/"; +} + +export function getFocusedRoute( + state: NavigationState | PartialState | undefined, +): AppFocusedRoute | undefined { + if (!state || state.routes.length === 0) { + return undefined; + } + + const index = state.index ?? 0; + const route = state.routes[index]; + if (!route) { + return undefined; + } + + return getFocusedRoute(route.state as PartialState | undefined) ?? route; +} + +export function firstParam(value: string | string[] | undefined): string | undefined { + return Array.isArray(value) ? value[0] : value; +} diff --git a/apps/mobile/src/navigation/router.tsx b/apps/mobile/src/navigation/router.tsx new file mode 100644 index 00000000000..f97e5344663 --- /dev/null +++ b/apps/mobile/src/navigation/router.tsx @@ -0,0 +1,536 @@ +import { + CommonActions, + StackActions, + useFocusEffect, + useNavigation, + useRoute, + type NavigationContainerRef, + type ParamListBase, +} from "@react-navigation/native"; +import type { ColorValue } from "react-native"; +import type { + NativeStackHeaderItem, + NativeStackHeaderItemMenu, + NativeStackNavigationOptions, + NativeStackNavigationProp, +} from "@react-navigation/native-stack"; +import { + Children, + cloneElement, + createContext, + createElement, + Fragment, + isValidElement, + use, + useEffect, + useMemo, + type ReactElement, + type ReactNode, +} from "react"; + +import { + buildPathFromRoute, + resolveNavigationTarget, + type AppHref, + type AppStackParamList, + type AppFocusedRoute, + type RouteParams, +} from "./route-model"; + +export { useFocusEffect }; +export type Href = AppHref; +type CompatNativeStackNavigationOptions = Omit< + NativeStackNavigationOptions, + "headerTintColor" | "unstable_headerLeftItems" | "unstable_headerRightItems" +> & { + readonly headerTintColor?: string | ColorValue; + readonly unstable_headerCenterItems?: unknown; + readonly unstable_headerLeftItems?: unknown; + readonly unstable_headerRightItems?: unknown; + readonly unstable_headerSubtitle?: unknown; + readonly unstable_headerToolbarItems?: unknown; + readonly unstable_navigationItemStyle?: unknown; +}; + +export type { CompatNativeStackNavigationOptions as NativeStackNavigationOptions }; + +type RouterLike = { + readonly push: (href: AppHref) => void; + readonly replace: (href: AppHref) => void; + readonly back: () => void; + readonly dismiss: () => void; + readonly dismissAll: () => void; + readonly canGoBack: () => boolean; + readonly setParams: (params: RouteParams) => void; +}; + +type NavigationContextValue = { + readonly navigationRef: React.RefObject | null>; + readonly pathname: string; + readonly params: RouteParams; + readonly router: RouterLike; +}; + +export const AppNavigationContext = createContext(null); + +export function useRouter(): RouterLike { + const context = use(AppNavigationContext); + if (context === null) { + throw new Error("useRouter must be used within AppNavigationProvider"); + } + return context.router; +} + +export function usePathname(): string { + const context = use(AppNavigationContext); + if (context === null) { + throw new Error("usePathname must be used within AppNavigationProvider"); + } + return context.pathname; +} + +export function useLocalSearchParams(): T { + const route = useRoute(); + return (route.params ?? {}) as RouteParams as T; +} + +export function useGlobalSearchParams(): T { + const context = use(AppNavigationContext); + if (context === null) { + throw new Error("useGlobalSearchParams must be used within AppNavigationProvider"); + } + return context.params as T; +} + +export function createRouter( + navigationRef: React.RefObject | null>, +): RouterLike { + const navigateWithAction = (href: AppHref, action: "push" | "replace" | "navigate"): void => { + const target = resolveNavigationTarget(href); + const navigation = navigationRef.current; + if (!navigation) { + return; + } + + if (action === "push") { + navigation.dispatch(StackActions.push(target.name, target.params)); + return; + } + if (action === "replace") { + navigation.dispatch(StackActions.replace(target.name, target.params)); + return; + } + navigation.dispatch( + CommonActions.navigate({ + name: target.name, + params: target.params, + }), + ); + }; + + return { + push: (href) => navigateWithAction(href, "push"), + replace: (href) => navigateWithAction(href, "replace"), + back: () => { + const navigation = navigationRef.current; + if (!navigation) return; + if (navigation.canGoBack()) { + navigation.goBack(); + return; + } + navigation.dispatch(StackActions.replace("Home")); + }, + dismiss: () => { + const navigation = navigationRef.current; + if (!navigation) return; + if (navigation.canGoBack()) { + navigation.goBack(); + return; + } + navigation.dispatch(StackActions.replace("Home")); + }, + dismissAll: () => { + const navigation = navigationRef.current; + if (!navigation) return; + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: "Home" }], + }), + ); + }, + canGoBack: () => navigationRef.current?.canGoBack() ?? false, + setParams: (params) => { + navigationRef.current?.dispatch(CommonActions.setParams(params)); + }, + }; +} + +export function deriveNavigationContextValue(input: { + readonly navigationRef: React.RefObject | null>; + readonly pathname: string; + readonly params: RouteParams; +}): NavigationContextValue { + return { + navigationRef: input.navigationRef, + pathname: input.pathname, + params: input.params, + router: createRouter(input.navigationRef), + }; +} + +function useNativeStackNavigation(): NativeStackNavigationProp | null { + try { + return useNavigation>(); + } catch { + return null; + } +} + +function normalizeScreenOptions( + options: CompatNativeStackNavigationOptions | undefined, +): NativeStackNavigationOptions | undefined { + if (!options) { + return options; + } + + const normalized = { ...options } as NativeStackNavigationOptions & { + unstable_navigationItemStyle?: unknown; + unstable_headerCenterItems?: unknown; + unstable_headerSubtitle?: unknown; + unstable_headerToolbarItems?: unknown; + }; + + delete normalized.unstable_navigationItemStyle; + delete normalized.unstable_headerCenterItems; + delete normalized.unstable_headerSubtitle; + delete normalized.unstable_headerToolbarItems; + + if (normalized.headerTintColor !== undefined) { + normalized.headerTintColor = String(normalized.headerTintColor); + } + + return normalized as NativeStackNavigationOptions; +} + +function StackScreen(props: { + readonly options?: CompatNativeStackNavigationOptions; + readonly listeners?: Record void>; + readonly name?: string; +}) { + const navigation = useNativeStackNavigation(); + const normalizedOptions = useMemo(() => normalizeScreenOptions(props.options), [props.options]); + + useEffect(() => { + if (!navigation || !normalizedOptions) { + return; + } + navigation.setOptions(normalizedOptions); + }, [navigation, normalizedOptions]); + + useEffect(() => { + if (!navigation || !props.listeners) { + return; + } + const subscriptions = Object.entries(props.listeners).map(([eventName, listener]) => + navigation.addListener(eventName as never, listener as never), + ); + return () => { + for (const unsubscribe of subscriptions) { + unsubscribe(); + } + }; + }, [navigation, props.listeners]); + + return null; +} + +function labelFromChildren(children: ReactNode): string { + const parts: string[] = []; + Children.forEach(children, (child) => { + if (typeof child === "string" || typeof child === "number") { + parts.push(String(child)); + } else if (isValidElement<{ children?: ReactNode }>(child)) { + parts.push(labelFromChildren(child.props.children)); + } + }); + return parts.join(""); +} + +type NativeStackHeaderIcon = NonNullable< + Extract["icon"] +>; + +function iconFromProp(icon: unknown): NativeStackHeaderIcon | undefined { + if (typeof icon !== "string") { + return undefined; + } + return { type: "sfSymbol", name: icon as never }; +} + +type ToolbarElementProps = Record & { readonly children?: ReactNode }; + +function elementTypeName(element: ReactElement): string | undefined { + const type = element.type; + if (typeof type === "function") { + return (type as { displayName?: string; name?: string }).displayName ?? type.name; + } + return undefined; +} + +function convertMenuAction( + element: ReactElement, +): NativeStackHeaderItemMenu["menu"]["items"][number] | null { + const typeName = elementTypeName(element); + if (typeName === "ToolbarMenuAction") { + const label = labelFromChildren(element.props.children); + return { + type: "action", + label, + description: typeof element.props.subtitle === "string" ? element.props.subtitle : undefined, + disabled: Boolean(element.props.disabled), + icon: iconFromProp(element.props.icon), + onPress: + typeof element.props.onPress === "function" + ? (element.props.onPress as () => void) + : () => undefined, + state: element.props.isOn === true ? "on" : undefined, + destructive: Boolean(element.props.destructive), + discoverabilityLabel: + typeof element.props.discoverabilityLabel === "string" + ? element.props.discoverabilityLabel + : undefined, + }; + } + + if (typeName === "ToolbarMenu") { + return { + type: "submenu", + label: + typeof element.props.title === "string" + ? element.props.title + : labelFromChildren(element.props.children), + icon: iconFromProp(element.props.icon), + inline: Boolean(element.props.inline), + items: collectMenuItems(element.props.children), + }; + } + + return null; +} + +function collectMenuItems(children: ReactNode): NativeStackHeaderItemMenu["menu"]["items"] { + const items: NativeStackHeaderItemMenu["menu"]["items"] = []; + Children.forEach(children, (child) => { + if (!isValidElement(child)) { + return; + } + const item = convertMenuAction(child); + if (item) { + items.push(item); + return; + } + items.push(...collectMenuItems(child.props.children)); + }); + return items; +} + +function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { + if (!isValidElement(child)) { + return null; + } + + const typeName = elementTypeName(child); + if (typeName === "ToolbarButton") { + return { + type: "button", + label: "", + accessibilityLabel: + typeof child.props.accessibilityLabel === "string" + ? child.props.accessibilityLabel + : undefined, + disabled: Boolean(child.props.disabled), + icon: iconFromProp(child.props.icon), + onPress: + typeof child.props.onPress === "function" + ? (child.props.onPress as () => void) + : () => undefined, + sharesBackground: !child.props.separateBackground, + variant: "prominent", + }; + } + + if (typeName === "ToolbarMenu") { + return { + type: "menu", + label: typeof child.props.title === "string" ? child.props.title : "", + accessibilityLabel: + typeof child.props.accessibilityLabel === "string" + ? child.props.accessibilityLabel + : undefined, + disabled: Boolean(child.props.disabled), + icon: iconFromProp(child.props.icon), + menu: { + title: typeof child.props.title === "string" ? child.props.title : undefined, + items: collectMenuItems(child.props.children), + }, + sharesBackground: !child.props.separateBackground, + variant: "prominent", + }; + } + + if (typeName === "ToolbarSpacer") { + return { + type: "spacing", + spacing: typeof child.props.width === "number" ? child.props.width : 8, + }; + } + + return null; +} + +function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { + const items: NativeStackHeaderItem[] = []; + Children.forEach(children, (child) => { + const item = convertToolbarChild(child); + if (item) { + items.push(item); + } + }); + return items; +} + +function Toolbar(props: { + readonly placement?: "left" | "right" | "bottom"; + readonly children?: ReactNode; +}) { + const navigation = useNativeStackNavigation(); + const items = useMemo(() => collectToolbarItems(props.children), [props.children]); + + useEffect(() => { + if (!navigation || props.placement === "bottom" || items.length === 0) { + return; + } + if (props.placement === "left") { + navigation.setOptions({ unstable_headerLeftItems: () => items }); + return; + } + navigation.setOptions({ unstable_headerRightItems: () => items }); + }, [items, navigation, props.placement]); + + return null; +} + +function ToolbarButton(_props: { + readonly accessibilityLabel?: string; + readonly disabled?: boolean; + readonly icon?: string; + readonly onPress?: () => void; + readonly separateBackground?: boolean; +}) { + return null; +} +ToolbarButton.displayName = "ToolbarButton"; + +function ToolbarMenu(_props: { + readonly accessibilityLabel?: string; + readonly children?: ReactNode; + readonly disabled?: boolean; + readonly icon?: string; + readonly inline?: boolean; + readonly separateBackground?: boolean; + readonly title?: string; +}) { + return null; +} +ToolbarMenu.displayName = "ToolbarMenu"; + +function ToolbarMenuAction(_props: { + readonly children?: ReactNode; + readonly destructive?: boolean; + readonly disabled?: boolean; + readonly discoverabilityLabel?: string; + readonly icon?: string; + readonly isOn?: boolean; + readonly onPress?: () => void; + readonly subtitle?: string; +}) { + return null; +} +ToolbarMenuAction.displayName = "ToolbarMenuAction"; + +function ToolbarLabel(_props: { readonly children?: ReactNode }) { + return null; +} +ToolbarLabel.displayName = "ToolbarLabel"; + +function ToolbarSpacer(_props: { readonly sharesBackground?: boolean; readonly width?: number }) { + return null; +} +ToolbarSpacer.displayName = "ToolbarSpacer"; + +function ToolbarSearchBarSlot() { + return null; +} +ToolbarSearchBarSlot.displayName = "ToolbarSearchBarSlot"; + +function StackScreenTitle(_props: { readonly asChild?: boolean; readonly children?: ReactNode }) { + return null; +} + +StackScreen.Title = StackScreenTitle; + +export const Stack = Object.assign( + function StackCompat(props: { readonly children?: ReactNode }) { + return createElement(Fragment, null, props.children); + }, + { + Screen: StackScreen, + Toolbar: Object.assign(Toolbar, { + Button: ToolbarButton, + Label: ToolbarLabel, + Menu: Object.assign(ToolbarMenu, { + Action: ToolbarMenuAction, + }), + MenuAction: ToolbarMenuAction, + SearchBarSlot: ToolbarSearchBarSlot, + Spacer: ToolbarSpacer, + }), + }, +); + +export default Stack; + +export function Redirect(props: { readonly href: AppHref }) { + const router = useRouter(); + useEffect(() => { + router.replace(props.href); + }, [props.href, router]); + return null; +} + +export function Link(props: { + readonly href: AppHref; + readonly asChild?: boolean; + readonly children?: ReactNode; +}) { + const router = useRouter(); + if (props.asChild && isValidElement<{ onPress?: () => void }>(props.children)) { + return cloneElement(props.children, { + onPress: () => router.push(props.href), + }); + } + return null; +} + +export function pathAndParamsFromCurrentRoute(route: { + readonly name: string; + readonly params?: object; +}): { readonly pathname: string; readonly params: RouteParams } { + return { + pathname: buildPathFromRoute(route as AppFocusedRoute), + params: (route.params ?? {}) as RouteParams, + }; +} + +export type Router = RouterLike; diff --git a/apps/mobile/src/app/+not-found.tsx b/apps/mobile/src/screens/+not-found.tsx similarity index 96% rename from apps/mobile/src/app/+not-found.tsx rename to apps/mobile/src/screens/+not-found.tsx index ceee5e4126e..94e24a78577 100644 --- a/apps/mobile/src/app/+not-found.tsx +++ b/apps/mobile/src/screens/+not-found.tsx @@ -1,4 +1,4 @@ -import { Link } from "expo-router"; +import { Link } from "../navigation/router"; import { Pressable, ScrollView, StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; diff --git a/apps/mobile/src/app/connections/index.tsx b/apps/mobile/src/screens/connections/index.tsx similarity index 98% rename from apps/mobile/src/app/connections/index.tsx rename to apps/mobile/src/screens/connections/index.tsx index 12a06996447..d6f5f7b2822 100644 --- a/apps/mobile/src/app/connections/index.tsx +++ b/apps/mobile/src/screens/connections/index.tsx @@ -1,4 +1,4 @@ -import { Stack, useRouter } from "expo-router"; +import { Stack, useRouter } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import type { EnvironmentId } from "@t3tools/contracts"; import { useCallback, useState } from "react"; diff --git a/apps/mobile/src/app/connections/new.tsx b/apps/mobile/src/screens/connections/new.tsx similarity index 99% rename from apps/mobile/src/app/connections/new.tsx rename to apps/mobile/src/screens/connections/new.tsx index ca9693dbb19..3ab81830d9b 100644 --- a/apps/mobile/src/app/connections/new.tsx +++ b/apps/mobile/src/screens/connections/new.tsx @@ -1,5 +1,5 @@ import { CameraView, useCameraPermissions } from "expo-camera"; -import { Stack, useLocalSearchParams, useRouter } from "expo-router"; +import { Stack, useLocalSearchParams, useRouter } from "../../navigation/router"; import { AsyncResult } from "effect/unstable/reactivity"; import { useCallback, useEffect, useState } from "react"; import { Alert, ScrollView, View } from "react-native"; diff --git a/apps/mobile/src/app/debug/rns-glass.tsx b/apps/mobile/src/screens/debug/rns-glass.tsx similarity index 99% rename from apps/mobile/src/app/debug/rns-glass.tsx rename to apps/mobile/src/screens/debug/rns-glass.tsx index 657911af56b..c6cb3a8bfe5 100644 --- a/apps/mobile/src/app/debug/rns-glass.tsx +++ b/apps/mobile/src/screens/debug/rns-glass.tsx @@ -1,4 +1,4 @@ -import { Stack } from "expo-router"; +import { Stack } from "../../navigation/router"; import { useEffect, useRef, useState, type ComponentProps } from "react"; import { Platform, diff --git a/apps/mobile/src/app/index.tsx b/apps/mobile/src/screens/index.tsx similarity index 98% rename from apps/mobile/src/app/index.tsx rename to apps/mobile/src/screens/index.tsx index 993458fee02..30cb75bd5b8 100644 --- a/apps/mobile/src/app/index.tsx +++ b/apps/mobile/src/screens/index.tsx @@ -1,6 +1,6 @@ import * as Arr from "effect/Array"; import * as Order from "effect/Order"; -import { Stack, useRouter } from "expo-router"; +import { Stack, useRouter } from "../navigation/router"; import { useMemo, useState } from "react"; import { useProjects, useThreadShells } from "../state/entities"; diff --git a/apps/mobile/src/app/new/add-project/destination.tsx b/apps/mobile/src/screens/new/add-project/destination.tsx similarity index 100% rename from apps/mobile/src/app/new/add-project/destination.tsx rename to apps/mobile/src/screens/new/add-project/destination.tsx diff --git a/apps/mobile/src/app/new/add-project/index.tsx b/apps/mobile/src/screens/new/add-project/index.tsx similarity index 100% rename from apps/mobile/src/app/new/add-project/index.tsx rename to apps/mobile/src/screens/new/add-project/index.tsx diff --git a/apps/mobile/src/app/new/add-project/local.tsx b/apps/mobile/src/screens/new/add-project/local.tsx similarity index 100% rename from apps/mobile/src/app/new/add-project/local.tsx rename to apps/mobile/src/screens/new/add-project/local.tsx diff --git a/apps/mobile/src/app/new/add-project/repository.tsx b/apps/mobile/src/screens/new/add-project/repository.tsx similarity index 90% rename from apps/mobile/src/app/new/add-project/repository.tsx rename to apps/mobile/src/screens/new/add-project/repository.tsx index 7bf23a4955a..be3140937c7 100644 --- a/apps/mobile/src/app/new/add-project/repository.tsx +++ b/apps/mobile/src/screens/new/add-project/repository.tsx @@ -1,4 +1,4 @@ -import { Stack, useLocalSearchParams } from "expo-router"; +import { Stack, useLocalSearchParams } from "../../../navigation/router"; import { addProjectRemoteSourceLabel } from "@t3tools/client-runtime/operations/projects"; import { AddProjectRepositoryScreen } from "../../../features/projects/AddProjectScreen"; diff --git a/apps/mobile/src/app/new/draft.tsx b/apps/mobile/src/screens/new/draft.tsx similarity index 91% rename from apps/mobile/src/app/new/draft.tsx rename to apps/mobile/src/screens/new/draft.tsx index 3adc7c53f92..f4cb57935c7 100644 --- a/apps/mobile/src/app/new/draft.tsx +++ b/apps/mobile/src/screens/new/draft.tsx @@ -1,4 +1,4 @@ -import { Stack, useLocalSearchParams } from "expo-router"; +import { Stack, useLocalSearchParams } from "../../navigation/router"; import { NewTaskDraftScreen } from "../../features/threads/NewTaskDraftScreen"; diff --git a/apps/mobile/src/app/new/index.tsx b/apps/mobile/src/screens/new/index.tsx similarity index 99% rename from apps/mobile/src/app/new/index.tsx rename to apps/mobile/src/screens/new/index.tsx index 4e5339e7de2..82ab63fb4c6 100644 --- a/apps/mobile/src/app/new/index.tsx +++ b/apps/mobile/src/screens/new/index.tsx @@ -1,4 +1,4 @@ -import { Link, Stack, useRouter } from "expo-router"; +import { Link, Stack, useRouter } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; import { useMemo } from "react"; diff --git a/apps/mobile/src/app/settings/archive.tsx b/apps/mobile/src/screens/settings/archive.tsx similarity index 100% rename from apps/mobile/src/app/settings/archive.tsx rename to apps/mobile/src/screens/settings/archive.tsx diff --git a/apps/mobile/src/app/settings/auth.tsx b/apps/mobile/src/screens/settings/auth.tsx similarity index 93% rename from apps/mobile/src/app/settings/auth.tsx rename to apps/mobile/src/screens/settings/auth.tsx index de33207ccda..984e7fbb0d1 100644 --- a/apps/mobile/src/app/settings/auth.tsx +++ b/apps/mobile/src/screens/settings/auth.tsx @@ -1,6 +1,6 @@ import { useAuth } from "@clerk/expo"; import { AuthView, UserProfileView } from "@clerk/expo/native"; -import { Redirect, Stack } from "expo-router"; +import { Redirect, Stack } from "../../navigation/router"; import { View } from "react-native"; import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; diff --git a/apps/mobile/src/app/settings/environment-new.tsx b/apps/mobile/src/screens/settings/environment-new.tsx similarity index 100% rename from apps/mobile/src/app/settings/environment-new.tsx rename to apps/mobile/src/screens/settings/environment-new.tsx diff --git a/apps/mobile/src/app/settings/environments.tsx b/apps/mobile/src/screens/settings/environments.tsx similarity index 99% rename from apps/mobile/src/app/settings/environments.tsx rename to apps/mobile/src/screens/settings/environments.tsx index 8f65c630a54..9584be950c9 100644 --- a/apps/mobile/src/app/settings/environments.tsx +++ b/apps/mobile/src/screens/settings/environments.tsx @@ -1,5 +1,5 @@ import { useAuth } from "@clerk/expo"; -import { Stack, useRouter } from "expo-router"; +import { Stack, useRouter } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import { connectionStatusText, diff --git a/apps/mobile/src/app/settings/index.tsx b/apps/mobile/src/screens/settings/index.tsx similarity index 99% rename from apps/mobile/src/app/settings/index.tsx rename to apps/mobile/src/screens/settings/index.tsx index 76b90108163..0e4003a4994 100644 --- a/apps/mobile/src/app/settings/index.tsx +++ b/apps/mobile/src/screens/settings/index.tsx @@ -1,6 +1,6 @@ import { useAuth, useUser } from "@clerk/expo"; import * as Notifications from "expo-notifications"; -import { Link, Stack, useRouter } from "expo-router"; +import { Link, Stack, useRouter } from "../../navigation/router"; import { SymbolView } from "expo-symbols"; import * as Effect from "effect/Effect"; import { useCallback, useEffect, useMemo, useState } from "react"; diff --git a/apps/mobile/src/app/settings/waitlist.tsx b/apps/mobile/src/screens/settings/waitlist.tsx similarity index 94% rename from apps/mobile/src/app/settings/waitlist.tsx rename to apps/mobile/src/screens/settings/waitlist.tsx index 3fc2fad2695..952d119d82f 100644 --- a/apps/mobile/src/app/settings/waitlist.tsx +++ b/apps/mobile/src/screens/settings/waitlist.tsx @@ -1,5 +1,5 @@ import { useAuth } from "@clerk/expo"; -import { Redirect, Stack, useFocusEffect, useRouter } from "expo-router"; +import { Redirect, Stack, useFocusEffect, useRouter } from "../../navigation/router"; import { useCallback } from "react"; import { ScrollView } from "react-native"; diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/files/[...path].tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/[...path].tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/files/[...path].tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/[...path].tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/files/index.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/index.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/files/index.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/index.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git-confirm.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git-confirm.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/git-confirm.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/git-confirm.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/branches.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/branches.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/git/branches.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/branches.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/commit.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/commit.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/git/commit.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/commit.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/git/index.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/index.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/git/index.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/index.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/index.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/index.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/index.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/index.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/review-comment.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review-comment.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/review-comment.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/review-comment.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/review.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/review.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/review.tsx diff --git a/apps/mobile/src/app/threads/[environmentId]/[threadId]/terminal.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/terminal.tsx similarity index 100% rename from apps/mobile/src/app/threads/[environmentId]/[threadId]/terminal.tsx rename to apps/mobile/src/screens/threads/[environmentId]/[threadId]/terminal.tsx diff --git a/apps/mobile/src/state/use-thread-selection.ts b/apps/mobile/src/state/use-thread-selection.ts index 656a9613866..c5eaf5743dd 100644 --- a/apps/mobile/src/state/use-thread-selection.ts +++ b/apps/mobile/src/state/use-thread-selection.ts @@ -1,4 +1,4 @@ -import { useGlobalSearchParams } from "expo-router"; +import { useGlobalSearchParams } from "../navigation/router"; import { createContext, createElement, use, useMemo, useRef, type ReactNode } from "react"; import { EnvironmentId, diff --git a/patches/expo-router@56.2.11.patch b/patches/expo-router@56.2.11.patch deleted file mode 100644 index 9564867cbf7..00000000000 --- a/patches/expo-router@56.2.11.patch +++ /dev/null @@ -1,115 +0,0 @@ -diff --git a/build/react-navigation/native-stack/types.d.ts b/build/react-navigation/native-stack/types.d.ts -index 6de73e0c33cedbaaaffdcb092ba21fc9ff71862e..78929fe237b7f9d7808f8de2b844e822976e0d69 100644 ---- a/build/react-navigation/native-stack/types.d.ts -+++ b/build/react-navigation/native-stack/types.d.ts -@@ -306,6 +306,39 @@ export type NativeStackNavigationOptions = { - * @platform ios - */ - unstable_headerRightItems?: (props: NativeStackHeaderItemProps) => NativeStackHeaderItem[]; -+ /** -+ * Function which returns an array of items to display as the centered item -+ * group in modern iOS headers. -+ * -+ * This is an unstable API and might change in the future. -+ * -+ * @platform ios -+ */ -+ unstable_headerCenterItems?: (props: NativeStackHeaderItemProps) => NativeStackHeaderItem[]; -+ /** -+ * Function which returns an array of toolbar items for the current screen. -+ * -+ * This is an unstable API and might change in the future. -+ * -+ * @platform ios -+ */ -+ unstable_headerToolbarItems?: (props: NativeStackHeaderItemProps) => NonNullable; -+ /** -+ * String displayed below the compact navigation title on iOS 26+. -+ * -+ * This is an unstable API and might change in the future. -+ * -+ * @platform ios -+ */ -+ unstable_headerSubtitle?: ScreenStackHeaderConfigProps['subtitle']; -+ /** -+ * Native iOS navigation item style to apply to the header. -+ * -+ * This is an unstable API and might change in the future. -+ * -+ * @platform ios -+ */ -+ unstable_navigationItemStyle?: ScreenStackHeaderConfigProps['navigationItemStyle']; - /** - * String or a function that returns a React Element to be used by the header. - * Defaults to screen `title` or route name. -@@ -1115,7 +1148,8 @@ export type NativeStackHeaderItemCustom = { - * if the items don't fit the available space, they will be collapsed into a menu automatically. - * Items with `type: 'custom'` will not be included in this automatic collapsing behavior. - */ --export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom; -+export type NativeStackHeaderRawRNSItem = NonNullable[number] | NonNullable[number] | NonNullable[number]; -+export type NativeStackHeaderItem = NativeStackHeaderItemButton | NativeStackHeaderItemMenu | NativeStackHeaderItemSpacing | NativeStackHeaderItemCustom | NativeStackHeaderRawRNSItem; - export type NativeStackNavigatorProps = DefaultNavigatorOptions, NativeStackNavigationOptions, NativeStackNavigationEventMap, NativeStackNavigationProp> & StackRouterOptions & NativeStackNavigationConfig; - export type NativeStackDescriptor = Descriptor, RouteProp>; - export type NativeStackDescriptorMap = { -diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts -index 218c4291fdb57600a45d32fbcefe3707a520fe79..5fdbb2754755a27589ad1555eae892b5fbee2c6a 100644 ---- a/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts -+++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.d.ts -@@ -10,6 +10,6 @@ type Props = NativeStackNavigationOptions & { - } | undefined; - route: Route; - }; --export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }: Props): ScreenStackHeaderConfigProps; -+export declare function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_headerSubtitle: headerSubtitle, unstable_navigationItemStyle: navigationItemStyle, }: Props): ScreenStackHeaderConfigProps; - export {}; - //# sourceMappingURL=useHeaderConfigProps.d.ts.map -diff --git a/build/react-navigation/native-stack/views/useHeaderConfigProps.js b/build/react-navigation/native-stack/views/useHeaderConfigProps.js -index 2de6d160cedffb4ffb124adf7cd10979ec6ee778..9d839442e00e136204630663f207a45c950dcd25 100644 ---- a/build/react-navigation/native-stack/views/useHeaderConfigProps.js -+++ b/build/react-navigation/native-stack/views/useHeaderConfigProps.js -@@ -22,6 +22,9 @@ const processBarButtonItems = (items, colors, fonts) => { - } - return item; - } -+ if (item.type === 'searchField' || item.type === 'searchBarPlacement' || item.type === 'mailSearchToolbar') { -+ return item; -+ } - if (item.type === 'button' || item.type === 'menu') { - if (item.type === 'menu' && item.menu == null) { - throw new Error(`Menu item must have a 'menu' property defined: ${JSON.stringify(item)}`); -@@ -108,7 +111,7 @@ const getMenuItem = (item) => { - subtitle: description, - }; - }; --function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, }) { -+function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBackButtonDisplayMode, headerBackButtonMenuEnabled, headerBackTitle, headerBackTitleStyle, headerBackVisible, headerShadowVisible, headerLargeStyle, headerLargeTitle: headerLargeTitleDeprecated, headerLargeTitleEnabled = headerLargeTitleDeprecated, headerLargeTitleShadowVisible, headerLargeTitleStyle, headerBackground, headerLeft, headerRight, headerShown, headerStyle, headerBlurEffect, headerTintColor, headerTitle, headerTitleAlign, headerTitleStyle, headerTransparent, headerSearchBarOptions, headerTopInsetEnabled, headerBack, route, title, unstable_headerLeftItems: headerLeftItems, unstable_headerRightItems: headerRightItems, unstable_headerCenterItems: headerCenterItems, unstable_headerToolbarItems: headerToolbarItems, unstable_headerSubtitle: headerSubtitle, unstable_navigationItemStyle: navigationItemStyle, }) { - const { direction } = (0, native_1.useLocale)(); - const { colors, fonts, dark } = (0, native_1.useTheme)(); - const tintColor = headerTintColor ?? (react_native_1.Platform.OS === 'ios' ? colors.primary : colors.text); -@@ -216,6 +219,14 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac - tintColor, - canGoBack, - }); -+ const centerItems = headerCenterItems?.({ -+ tintColor, -+ canGoBack, -+ }); -+ const toolbarItems = headerToolbarItems?.({ -+ tintColor, -+ canGoBack, -+ }); - if (rightItems) { - // iOS renders right items in reverse order - // So we need to reverse them here to match the order -@@ -270,6 +281,10 @@ function useHeaderConfigProps({ headerBackIcon, headerBackImageSource, headerBac - children, - headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts), - headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), -+ subtitle: headerSubtitle, -+ headerCenterBarButtonItems: processBarButtonItems(centerItems, colors, fonts), -+ headerToolbarItems: toolbarItems, -+ navigationItemStyle, - experimental_userInterfaceStyle: dark ? 'dark' : 'light', - }; - } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5a62d88ec2d..151d801a139 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -72,7 +72,6 @@ patchedDependencies: '@pierre/diffs@1.3.0-beta.5': 7cb6da88544119adda056b2f46f43956f99326227732da0b345081e285a6c53a effect@4.0.0-beta.78: c502bc684210b707dfceb87d8fe6ad6843395af6e19cfc02cd65854898bde2c5 expo-modules-jsi@56.0.10: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f - expo-router@56.2.11: 3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8 react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 react-native-screens@4.25.2: 914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7 @@ -214,6 +213,15 @@ importers: '@react-native-menu/menu': specifier: ^2.0.0 version: 2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@react-navigation/elements': + specifier: 2.9.26 + version: 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) + '@react-navigation/native': + specifier: 7.3.4 + version: 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + '@react-navigation/native-stack': + specifier: 7.17.6 + version: 7.17.6(dc1fa3b83a3d36b06fc35a0865f3d516) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -307,9 +315,6 @@ importers: expo-paste-input: specifier: ^0.1.15 version: 0.1.15(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: - specifier: ~56.2.11 - version: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3) expo-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -3844,6 +3849,41 @@ packages: '@types/react': optional: true + '@react-navigation/core@7.21.2': + resolution: {integrity: sha512-EsJhQnsL5NuIhXsWF7URijS5hd2AaJUREdq1fOIowlyNNQBA3jCnkGU0DkW7+eI40cQ+GXH8DQw+ci7TefLIxQ==} + peerDependencies: + react: '>= 18.2.0' + + '@react-navigation/elements@2.9.26': + resolution: {integrity: sha512-EaHSJf42MjnH5VFL+KZfQIyqJvdQWK9Z27J7WUSuIVX5NXlR925sPmhWf540DX9gD1ny8BfUGPZAuw/PO4tK2w==} + peerDependencies: + '@react-native-masked-view/masked-view': '>= 0.2.0' + '@react-navigation/native': ^7.3.4 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + peerDependenciesMeta: + '@react-native-masked-view/masked-view': + optional: true + + '@react-navigation/native-stack@7.17.6': + resolution: {integrity: sha512-CnNPnlSGnrxPyHguDp/xDg5w9STMmIoI6IFZe7cpvEeD0GF/LTzvZ0gmq+hPJTm2/KIrnuPx34emCofzpht45g==} + peerDependencies: + '@react-navigation/native': ^7.3.4 + react: '>= 18.2.0' + react-native: '*' + react-native-safe-area-context: '>= 4.0.0' + react-native-screens: '>= 4.0.0' + + '@react-navigation/native@7.3.4': + resolution: {integrity: sha512-zyAqFLeZuaakerMMnhYqBeGAzJ+WFQUTgezP3FxSlXNwFq5XxnjP28vi2XHGqm4zg4pGWls9Ay4JKshIHUOpwA==} + peerDependencies: + react: '>= 18.2.0' + react-native: '*' + + '@react-navigation/routers@7.6.0': + resolution: {integrity: sha512-lblhDXfS75jLc7G2K7BZGM+7cjqQXk13X/MA4fq/12r62zM+fBhhreLzYflSitrDDXFRJpSvJXy0ziiGU04Xow==} + '@rolldown/binding-android-arm64@1.0.0-rc.17': resolution: {integrity: sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -9326,6 +9366,9 @@ packages: standard-navigation@0.0.5: resolution: {integrity: sha512-YAmzwAiiQVocZxO/VGPFiQHcu5pKiz09QIGC0MK6aRMoa3E0QkoTQgcqJr7ZZ3OMiNhu4DkaGElFI5htjOIDbw==} + standard-navigation@0.0.7: + resolution: {integrity: sha512-NCGLCNyuXrFOkGHxdNZFnpsehGtiq1oXbPhKl7ZuxFO5J//H2evqqOchmD4YwEUJnkjO4kH9Xp4hQX6hdAYCKQ==} + standardwebhooks@1.0.0: resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} @@ -10268,7 +10311,8 @@ snapshots: 7zip-bin@5.2.0: {} - '@adobe/css-tools@4.5.0': {} + '@adobe/css-tools@4.5.0': + optional: true '@alcalzone/ansi-tokenize@0.2.5': dependencies: @@ -12003,7 +12047,7 @@ snapshots: ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) zod: 3.25.76 optionalDependencies: - expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3) + expo-router: 56.2.11(58010f146fb71a08f4c76f0e25e5e6b3) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) transitivePeerDependencies: - '@expo/dom-webview' @@ -12301,7 +12345,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) - expo-router: 56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3) + expo-router: 56.2.11(58010f146fb71a08f4c76f0e25e5e6b3) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -13220,6 +13264,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.16)(react@19.2.3)': dependencies: @@ -13260,6 +13305,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@types/react': 19.2.16 + optional: true '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: @@ -13343,6 +13389,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true '@radix-ui/react-slot@1.2.3(@types/react@19.2.16)(react@19.2.3)': dependencies: @@ -13357,6 +13404,7 @@ snapshots: react: 19.2.3 optionalDependencies: '@types/react': 19.2.16 + optional: true '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': dependencies: @@ -13373,6 +13421,7 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 '@types/react-dom': 19.2.3(@types/react@19.2.16) + optional: true '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.16)(react@19.2.3)': dependencies: @@ -13423,6 +13472,7 @@ snapshots: dependencies: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + optional: true '@react-native-menu/menu@2.0.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: @@ -13568,6 +13618,59 @@ snapshots: optionalDependencies: '@types/react': 19.2.16 + '@react-navigation/core@7.21.2(react@19.2.3)': + dependencies: + '@react-navigation/routers': 7.6.0 + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.12 + query-string: 7.1.3 + react: 19.2.3 + react-is: 19.2.7 + use-latest-callback: 0.2.6(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + + '@react-navigation/elements@2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4)': + dependencies: + '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + color: 4.2.3 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + use-latest-callback: 0.2.6(react@19.2.3) + use-sync-external-store: 1.6.0(react@19.2.3) + optionalDependencies: + '@react-native-masked-view/masked-view': 0.3.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + + '@react-navigation/native-stack@7.17.6(dc1fa3b83a3d36b06fc35a0865f3d516)': + dependencies: + '@react-navigation/elements': 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) + '@react-navigation/native': 7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + color: 4.2.3 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + react-native-safe-area-context: 5.7.0(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + react-native-screens: 4.25.2(patch_hash=914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) + sf-symbols-typescript: 2.2.0 + warn-once: 0.1.1 + transitivePeerDependencies: + - '@react-native-masked-view/masked-view' + + '@react-navigation/native@7.3.4(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': + dependencies: + '@react-navigation/core': 7.21.2(react@19.2.3) + escape-string-regexp: 4.0.0 + fast-deep-equal: 3.1.3 + nanoid: 3.3.12 + react: 19.2.3 + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) + standard-navigation: 0.0.7 + use-latest-callback: 0.2.6(react@19.2.3) + + '@react-navigation/routers@7.6.0': + dependencies: + nanoid: 3.3.12 + '@rolldown/binding-android-arm64@1.0.0-rc.17': optional: true @@ -14213,6 +14316,7 @@ snapshots: dom-accessibility-api: 0.6.3 picocolors: 1.1.1 redent: 3.0.0 + optional: true '@testing-library/user-event@14.6.1(@testing-library/dom@10.4.1)': dependencies: @@ -15402,7 +15506,8 @@ snapshots: cli-width@4.1.0: {} - client-only@0.0.1: {} + client-only@0.0.1: + optional: true cliui@8.0.1: dependencies: @@ -15594,7 +15699,8 @@ snapshots: css-what@6.2.2: {} - css.escape@1.5.1: {} + css.escape@1.5.1: + optional: true csso@5.0.5: dependencies: @@ -15717,7 +15823,8 @@ snapshots: dom-accessibility-api@0.5.16: {} - dom-accessibility-api@0.6.3: {} + dom-accessibility-api@0.6.3: + optional: true dom-serializer@2.0.0: dependencies: @@ -16217,7 +16324,7 @@ snapshots: react: 19.2.3 react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6) - expo-router@56.2.11(patch_hash=3c26210132f023a81982922c80746cb1008e8ec3b699ee6d054d687a9a24e3f8)(58010f146fb71a08f4c76f0e25e5e6b3): + expo-router@56.2.11(58010f146fb71a08f4c76f0e25e5e6b3): dependencies: '@expo/log-box': 56.0.13(@expo/dom-webview@56.0.5)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) '@expo/metro-runtime': 56.0.15(@expo/log-box@56.0.13)(expo@56.0.12)(react-dom@19.2.3(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -16266,6 +16373,7 @@ snapshots: - expo-font - react-native-worklets - supports-color + optional: true expo-secure-store@56.0.4(expo@56.0.12): dependencies: @@ -16974,7 +17082,8 @@ snapshots: immediate@3.0.6: {} - indent-string@4.0.0: {} + indent-string@4.0.0: + optional: true indent-string@5.0.0: {} @@ -18086,7 +18195,8 @@ snapshots: mimic-response@3.1.0: {} - min-indent@1.0.1: {} + min-indent@1.0.1: + optional: true minimatch@10.2.5: dependencies: @@ -18755,7 +18865,8 @@ snapshots: dependencies: react: 19.2.6 - react-fast-compare@3.2.2: {} + react-fast-compare@3.2.2: + optional: true react-freeze@1.0.4(react@19.2.3): dependencies: @@ -18802,6 +18913,7 @@ snapshots: react-native-gesture-handler: 2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) react-native-reanimated: 4.3.1(react-native-worklets@0.8.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3))(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) use-latest-callback: 0.2.6(react@19.2.3) + optional: true react-native-gesture-handler@2.31.2(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3): dependencies: @@ -19016,6 +19128,7 @@ snapshots: dependencies: indent-string: 4.0.0 strip-indent: 3.0.0 + optional: true redis-errors@1.2.0: {} @@ -19447,7 +19560,8 @@ snapshots: transitivePeerDependencies: - supports-color - server-only@0.0.1: {} + server-only@0.0.1: + optional: true setimmediate@1.0.5: {} @@ -19455,7 +19569,8 @@ snapshots: sf-symbols-typescript@2.2.0: {} - shallowequal@1.1.0: {} + shallowequal@1.1.0: + optional: true sharp@0.34.5: dependencies: @@ -19619,7 +19734,10 @@ snapshots: standard-as-callback@2.1.0: {} - standard-navigation@0.0.5: {} + standard-navigation@0.0.5: + optional: true + + standard-navigation@0.0.7: {} standardwebhooks@1.0.0: dependencies: @@ -19683,6 +19801,7 @@ snapshots: strip-indent@3.0.0: dependencies: min-indent: 1.0.1 + optional: true strnum@2.3.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b566cc03b93..f3b8d15cd72 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -110,7 +110,6 @@ patchedDependencies: "@pierre/diffs@1.3.0-beta.5": patches/@pierre%2Fdiffs@1.3.0-beta.5.patch effect@4.0.0-beta.78: patches/effect@4.0.0-beta.78.patch expo-modules-jsi@56.0.10: patches/expo-modules-jsi@56.0.10.patch - expo-router@56.2.11: patches/expo-router@56.2.11.patch react-native-nitro-modules@0.35.9: patches/react-native-nitro-modules@0.35.9.patch react-native-screens@4.25.2: patches/react-native-screens@4.25.2.patch From ee203ed5182ad05afed96f64d913e9ae528d0c44 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 16:12:14 -0700 Subject: [PATCH 69/71] Structure mobile navigation around native stack --- apps/mobile/src/App.tsx | 510 +----------------- .../agent-awareness/notificationNavigation.ts | 4 +- .../archive/ArchivedThreadsRouteScreen.tsx | 2 +- .../archive/ArchivedThreadsScreen.tsx | 53 +- .../features/files/ThreadFilesRouteScreen.tsx | 51 +- apps/mobile/src/features/home/HomeHeader.tsx | 89 +-- .../HardwareKeyboardCommandProvider.tsx | 6 +- .../layout/AdaptiveWorkspaceLayout.tsx | 14 +- .../layout/native-glass-header-items.ts | 2 +- .../layout/native-mail-search-toolbar.ts | 3 +- .../layout/workspace-sidebar-toolbar.tsx | 8 +- .../features/projects/AddProjectScreen.tsx | 34 +- .../review/ReviewCommentComposerSheet.tsx | 6 +- .../src/features/review/ReviewSheet.tsx | 65 +-- .../useReviewCommentSelectionController.ts | 6 +- .../terminal/ThreadTerminalRouteScreen.tsx | 61 ++- .../features/threads/NewTaskDraftScreen.tsx | 8 +- .../src/features/threads/ThreadFeed.tsx | 4 +- .../features/threads/ThreadGitControls.tsx | 90 ++-- .../threads/ThreadNavigationSidebar.tsx | 4 +- .../features/threads/ThreadRouteScreen.tsx | 21 +- .../features/threads/git/GitBranchesSheet.tsx | 4 +- .../features/threads/git/GitCommitSheet.tsx | 4 +- .../features/threads/git/GitConfirmSheet.tsx | 6 +- .../features/threads/git/GitOverviewSheet.tsx | 12 +- apps/mobile/src/lib/routes.test.ts | 4 +- apps/mobile/src/lib/routes.ts | 20 +- apps/mobile/src/navigation/RootNavigator.tsx | 405 ++++++++++++++ apps/mobile/src/navigation/app-navigation.tsx | 176 ++++++ apps/mobile/src/navigation/linking.ts | 39 ++ .../{router.tsx => native-stack-header.tsx} | 265 +++------ apps/mobile/src/navigation/route-model.ts | 4 + apps/mobile/src/screens/+not-found.tsx | 6 +- apps/mobile/src/screens/connections/index.tsx | 16 +- apps/mobile/src/screens/connections/new.tsx | 19 +- apps/mobile/src/screens/debug/rns-glass.tsx | 6 +- apps/mobile/src/screens/index.tsx | 12 +- .../screens/new/add-project/repository.tsx | 6 +- apps/mobile/src/screens/new/draft.tsx | 6 +- apps/mobile/src/screens/new/index.tsx | 25 +- apps/mobile/src/screens/settings/archive.tsx | 3 - apps/mobile/src/screens/settings/auth.tsx | 6 +- .../src/screens/settings/environment-new.tsx | 1 - .../src/screens/settings/environments.tsx | 16 +- apps/mobile/src/screens/settings/index.tsx | 25 +- apps/mobile/src/screens/settings/waitlist.tsx | 13 +- .../[threadId]/files/[...path].tsx | 5 - .../[threadId]/files/index.tsx | 5 - .../[threadId]/git-confirm.tsx | 5 - .../[threadId]/git/branches.tsx | 5 - .../[environmentId]/[threadId]/git/commit.tsx | 5 - .../[environmentId]/[threadId]/git/index.tsx | 5 - .../[environmentId]/[threadId]/index.tsx | 5 - .../[threadId]/review-comment.tsx | 5 - .../[environmentId]/[threadId]/review.tsx | 10 - .../[environmentId]/[threadId]/terminal.tsx | 5 - apps/mobile/src/state/use-thread-selection.ts | 4 +- 57 files changed, 1092 insertions(+), 1107 deletions(-) create mode 100644 apps/mobile/src/navigation/RootNavigator.tsx create mode 100644 apps/mobile/src/navigation/app-navigation.tsx create mode 100644 apps/mobile/src/navigation/linking.ts rename apps/mobile/src/navigation/{router.tsx => native-stack-header.tsx} (59%) delete mode 100644 apps/mobile/src/screens/settings/archive.tsx delete mode 100644 apps/mobile/src/screens/settings/environment-new.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/[...path].tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/index.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/git-confirm.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/branches.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/commit.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/index.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/index.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/review-comment.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/review.tsx delete mode 100644 apps/mobile/src/screens/threads/[environmentId]/[threadId]/terminal.tsx diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 543a9f64e84..dc221fc05be 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -4,513 +4,19 @@ import { DMSans_700Bold, useFonts, } from "@expo-google-fonts/dm-sans"; -import { NavigationContainer, type NavigationContainerRef } from "@react-navigation/native"; -import { createNativeStackNavigator } from "@react-navigation/native-stack"; -import * as Linking from "expo-linking"; -import { use, useCallback, useMemo, useRef, useState, type ReactNode } from "react"; -import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; import { GestureHandlerRootView } from "react-native-gesture-handler"; import { KeyboardProvider } from "react-native-keyboard-controller"; import { SafeAreaProvider } from "react-native-safe-area-context"; -import { useResolveClassNames } from "uniwind"; import { RegistryContext } from "@effect/atom-react"; import { LoadingScreen } from "./components/LoadingScreen"; -import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; -import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider"; -import { - ClerkSettingsSheetDetentProvider, - useClerkSettingsSheetDetent, -} from "./features/cloud/ClerkSettingsSheetDetent"; -import { - AdaptiveWorkspaceLayout, - useAdaptiveWorkspaceLayout, -} from "./features/layout/AdaptiveWorkspaceLayout"; -import { deriveStableFormSheetDetent } from "./lib/layout"; -import { useThemeColor } from "./lib/useThemeColor"; +import { AppNavigationProvider } from "./navigation/app-navigation"; +import { RootNavigator } from "./navigation/RootNavigator"; import { appAtomRegistry } from "./state/atom-registry"; -import { ThreadSelectionProvider } from "./state/use-thread-selection"; -import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; -import HomeRouteScreen from "./screens/index"; -import SettingsRouteScreen from "./screens/settings/index"; -import SettingsEnvironmentsRouteScreen from "./screens/settings/environments"; -import SettingsAuthRouteScreen from "./screens/settings/auth"; -import SettingsWaitlistRouteScreen from "./screens/settings/waitlist"; -import ConnectionsRouteScreen from "./screens/connections/index"; -import ConnectionsNewRouteScreen from "./screens/connections/new"; -import NewTaskRoute from "./screens/new/index"; -import AddProjectRoute from "./screens/new/add-project/index"; -import AddProjectRepositoryRoute from "./screens/new/add-project/repository"; -import AddProjectDestinationRoute from "./screens/new/add-project/destination"; -import AddProjectLocalRoute from "./screens/new/add-project/local"; -import NewTaskDraftRoute from "./screens/new/draft"; -import RnsGlassDebugRoute from "./screens/debug/rns-glass"; -import NotFoundRoute from "./screens/+not-found"; -import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen"; -import { ReviewHighlighterProvider } from "./features/review/ReviewHighlighterProvider"; -import { ReviewCommentComposerSheet } from "./features/review/ReviewCommentComposerSheet"; -import { ReviewSheet } from "./features/review/ReviewSheet"; -import { ThreadTerminalRouteScreen } from "./features/terminal/ThreadTerminalRouteScreen"; -import { ThreadRouteScreen } from "./features/threads/ThreadRouteScreen"; -import { GitBranchesSheet } from "./features/threads/git/GitBranchesSheet"; -import { GitCommitSheet } from "./features/threads/git/GitCommitSheet"; -import { GitConfirmSheet } from "./features/threads/git/GitConfirmSheet"; -import { GitOverviewSheet } from "./features/threads/git/GitOverviewSheet"; -import { HardwareKeyboardCommandProvider } from "./features/keyboard/HardwareKeyboardCommandProvider"; -import { - AppNavigationContext, - createRouter, - pathAndParamsFromCurrentRoute, -} from "./navigation/router"; -import { - getFocusedRoute, - type AppStackParamList, - type RouteParams, -} from "./navigation/route-model"; import "../global.css"; -const RootStack = createNativeStackNavigator(); - -const linking = { - prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], - config: { - screens: { - Home: "", - DebugRnsGlass: "debug/rns-glass", - Settings: "settings", - SettingsEnvironments: "settings/environments", - SettingsEnvironmentNew: "settings/environment-new", - SettingsArchive: "settings/archive", - SettingsAuth: "settings/auth", - SettingsWaitlist: "settings/waitlist", - Connections: "connections", - ConnectionsNew: "connections/new", - NewTask: "new", - AddProject: "new/add-project", - AddProjectRepository: "new/add-project/repository", - AddProjectDestination: "new/add-project/destination", - AddProjectLocal: "new/add-project/local", - NewTaskDraft: "new/draft", - Thread: "threads/:environmentId/:threadId", - ThreadTerminal: "threads/:environmentId/:threadId/terminal", - ThreadReview: "threads/:environmentId/:threadId/review", - ThreadReviewComment: "threads/:environmentId/:threadId/review-comment", - ThreadFiles: "threads/:environmentId/:threadId/files", - ThreadFile: "threads/:environmentId/:threadId/files/:path*", - GitOverview: "threads/:environmentId/:threadId/git", - GitCommit: "threads/:environmentId/:threadId/git/commit", - GitBranches: "threads/:environmentId/:threadId/git/branches", - GitConfirm: "threads/:environmentId/:threadId/git-confirm", - NotFound: "*", - }, - }, -}; - -function ThreadSelectionRoute(props: { readonly children: ReactNode }) { - return {props.children}; -} - -function ThreadRoute() { - return ( - - - - ); -} - -function ThreadTerminalRoute() { - return ( - - - - ); -} - -function ThreadFilesRoute() { - return ( - - - - ); -} - -function ThreadFileRoute() { - return ( - - - - ); -} - -function ThreadReviewRoute() { - return ( - - - - - - ); -} - -function ThreadReviewCommentRoute() { - return ( - - - - ); -} - -function GitOverviewRoute() { - return ( - - - - ); -} - -function GitCommitRoute() { - return ( - - - - ); -} - -function GitBranchesRoute() { - return ( - - - - ); -} - -function GitConfirmRoute() { - return ( - - - - ); -} - -function AppNavigationProvider(props: { readonly children: ReactNode }) { - const navigationRef = useRef>(null); - const [pathname, setPathname] = useState("/"); - const [params, setParams] = useState({}); - const router = useMemo(() => createRouter(navigationRef), []); - const contextValue = useMemo( - () => ({ - navigationRef, - pathname, - params, - router, - }), - [params, pathname, router], - ); - const syncState = useCallback(() => { - const route = getFocusedRoute(navigationRef.current?.getRootState()); - if (!route) { - setPathname("/"); - setParams({}); - return; - } - - const current = pathAndParamsFromCurrentRoute(route); - setPathname(current.pathname); - setParams(current.params); - }, []); - - return ( - - - {props.children} - - - ); -} - -function AppNavigator() { - const pathname = usePathnameFromContext(); - const expandedSettingsRouteIsActive = - pathname === "/settings/archive" || pathname === "/settings/auth"; - - return ( - - - - ); -} - -function usePathnameFromContext() { - const context = use(AppNavigationContext); - if (context === null) { - return "/"; - } - return context.pathname; -} - -function AppNavigatorContent() { - const pathname = usePathnameFromContext(); - const isDebugRoute = pathname.startsWith("/debug/"); - - if (isDebugRoute) { - return ; - } - - return ; -} - -function DebugNavigatorHost() { - return ( - <> - - - - - - - - ); -} - -function WorkspaceNavigatorHost() { - const colorScheme = useColorScheme(); - const statusBarBg = useThemeColor("--color-status-bar"); - useAgentNotificationNavigation(); - useThreadOutboxDrain(); - - return ( - <> - - - - - - ); -} - -function WorkspaceNavigator() { - const { collapse, isExpanded } = useClerkSettingsSheetDetent(); - const { layout } = useAdaptiveWorkspaceLayout(); - const { height } = useWindowDimensions(); - const sheetStyle = useResolveClassNames("bg-sheet"); - - const handleSettingsTransitionEnd = useCallback( - (event: { data: { closing: boolean } }) => { - if (event.data.closing) { - collapse(); - } - }, - [collapse], - ); - - const connectionSheetScreenOptions = { - contentStyle: sheetStyle, - gestureEnabled: true, - headerShown: false, - presentation: "formSheet" as const, - sheetAllowedDetents: [0.55, 0.7], - sheetGrabberVisible: true, - }; - const settingsScreenOptions = layout.usesSplitView - ? { - animation: "none" as const, - contentStyle: sheetStyle, - gestureEnabled: false, - headerShown: false, - presentation: "card" as const, - } - : { - ...connectionSheetScreenOptions, - sheetAllowedDetents: isExpanded ? [0.92] : [0.7], - }; - const newTaskScreenOptions = { - contentStyle: sheetStyle, - gestureEnabled: true, - headerShown: false, - presentation: "formSheet" as const, - sheetAllowedDetents: [layout.usesSplitView ? deriveStableFormSheetDetent(height) : 0.92], - sheetGrabberVisible: !layout.usesSplitView, - }; - - return ( - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - ); -} - export default function App() { const [fontsLoaded] = useFonts({ DMSans_400Regular, @@ -525,13 +31,11 @@ export default function App() { - - {fontsLoaded ? ( - - ) : ( - - )} - + {fontsLoaded ? ( + + ) : ( + + )} diff --git a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts index 03aacf2ca30..2662566e281 100644 --- a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts +++ b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts @@ -1,12 +1,12 @@ import { useEffect, useRef } from "react"; import * as Notifications from "expo-notifications"; -import { useRouter } from "../../navigation/router"; +import { useAppNavigation } from "../../navigation/native-stack-header"; import { routeAgentNotificationResponseOnce } from "./notificationPayload"; import { consumeLastAgentNotificationResponse } from "./notificationResponseConsumer"; export function useAgentNotificationNavigation(): void { - const router = useRouter(); + const router = useAppNavigation(); const handledResponseIds = useRef(new Set()); useEffect(() => { diff --git a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx index 8253481bf26..8c56962b101 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx @@ -1,7 +1,7 @@ import type { EnvironmentId } from "@t3tools/contracts"; import * as Arr from "effect/Array"; import * as Order from "effect/Order"; -import { useFocusEffect } from "../../navigation/router"; +import { useFocusEffect } from "../../navigation/native-stack-header"; import { useCallback, useMemo, useState } from "react"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index 354da264946..f8e50eb4dbb 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -4,7 +4,10 @@ import type { } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; -import { Stack } from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, +} from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import { useCallback, useMemo, useRef, type ComponentProps } from "react"; import { @@ -111,7 +114,7 @@ function ArchivedThreadsHeader(props: { return ( <> - {usesCompactMailToolbar ? null : ( - + {usesNativeChrome ? ( - ) : null} - - - Environment - + Environment + props.onEnvironmentChange(null)} > - All environments - + All environments + {props.environments.map((environment) => ( - props.onEnvironmentChange(environment.environmentId)} > - {environment.label} - + {environment.label} + ))} - + - - Sort by archived date - + Sort by archived date + props.onSortOrderChange("newest")} > - Newest first - - Newest first + + props.onSortOrderChange("oldest")} > - Oldest first - - - - + Oldest first + + + + )} ); diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 083ba56d7a4..b6bf434b6ba 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -1,6 +1,9 @@ -import Stack from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, +} from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; -import { useLocalSearchParams, useRouter } from "../../navigation/router"; +import { useRouteParams, useAppNavigation } from "../../navigation/native-stack-header"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { ActivityIndicator, @@ -355,7 +358,7 @@ function FileContent(props: { } function useThreadFilesWorkspace() { - const params = useLocalSearchParams<{ + const params = useRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); @@ -385,7 +388,7 @@ function useThreadFilesWorkspace() { function FilesUnavailable() { return ( - + - {layout.usesSplitView ? ( - - + - + ) : null} {!usesCompactMailToolbar ? ( - - + - + ) : null} {usesCompactMailToolbar ? null : ( - - - + + + )} (); @@ -744,7 +747,7 @@ export function ThreadFileScreen() { if (relativePath === null) { return ( - + ); @@ -753,7 +756,7 @@ export function ThreadFileScreen() { return ( - {fileInspector.supported ? ( - { @@ -776,9 +779,9 @@ export function ThreadFileScreen() { /> ) : null} - + {fileInspector.supported ? ( - ) : null} - { @@ -798,7 +801,7 @@ export function ThreadFileScreen() { fileQuery.refresh(); }} /> - + renderInspector(0) : undefined} > diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 287c40c85bf..043c4c3d070 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -3,7 +3,10 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import { Stack } from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, +} from "../../navigation/native-stack-header"; import { useCallback, useRef } from "react"; import { Platform } from "react-native"; import type { SearchBarCommands } from "react-native-screens"; @@ -54,7 +57,7 @@ export function HomeHeader(props: { return ( <> - {Platform.OS === "ios" ? null : ( - - + - + )} {Platform.OS === "ios" ? null : ( - - + - - Settings - + + Settings + - - Environment - + Environment + props.onEnvironmentChange(null)} subtitle="Show threads from every environment" > - All environments - + All environments + {props.environments.map((environment) => ( - props.onEnvironmentChange(environment.environmentId)} > - {environment.label} - + {environment.label} + ))} - + - - Sort projects + + Sort projects {PROJECT_SORT_OPTIONS.map((option) => ( - props.onProjectSortOrderChange(option.value)} > - {option.label} - + {option.label} + ))} - + - - Sort threads + + Sort threads {THREAD_SORT_OPTIONS.map((option) => ( - props.onThreadSortOrderChange(option.value)} > - {option.label} - + {option.label} + ))} - + - - Group projects + + Group projects {PROJECT_GROUPING_OPTIONS.map((option) => ( - props.onProjectGroupingModeChange(option.value)} subtitle={option.subtitle} > - {option.label} - + {option.label} + ))} - - - - - - + + + + + - + )} ); diff --git a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx index b026e8c6461..ddf79b8cc2b 100644 --- a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx +++ b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx @@ -1,4 +1,4 @@ -import { usePathname, useRouter } from "../../navigation/router"; +import { useCurrentPathname, useAppNavigation } from "../../navigation/native-stack-header"; import { useCallback, useMemo, useSyncExternalStore, type PropsWithChildren } from "react"; import { @@ -18,8 +18,8 @@ import { } from "./hardwareKeyboardCommands"; export function HardwareKeyboardCommandProvider({ children }: PropsWithChildren) { - const pathname = usePathname(); - const router = useRouter(); + const pathname = useCurrentPathname(); + const router = useAppNavigation(); const registrationVersion = useSyncExternalStore( subscribeToHardwareKeyboardCommandRegistrations, getHardwareKeyboardCommandRegistrationVersion, diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index bcb69a0c93e..a575926d7d8 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -2,10 +2,10 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useFocusEffect, - useGlobalSearchParams, - usePathname, - useRouter, -} from "../../navigation/router"; + useCurrentRouteParams, + useCurrentPathname, + useAppNavigation, +} from "../../navigation/native-stack-header"; import { createContext, use, @@ -99,8 +99,8 @@ export function useAdaptiveWorkspacePaneRole(role: WorkspaceAuxiliaryPaneRole) { export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) { const { width, height } = useWindowDimensions(); - const pathname = usePathname(); - const router = useRouter(); + const pathname = useCurrentPathname(); + const router = useAppNavigation(); const activeRoleOwner = useRef(null); const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); @@ -114,7 +114,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const [primarySidebarSearchQuery, setPrimarySidebarSearchQuery] = useState(""); const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = useState(null); - const params = useGlobalSearchParams<{ + const params = useCurrentRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); diff --git a/apps/mobile/src/features/layout/native-glass-header-items.ts b/apps/mobile/src/features/layout/native-glass-header-items.ts index 997f3bc1016..ec88051eaaa 100644 --- a/apps/mobile/src/features/layout/native-glass-header-items.ts +++ b/apps/mobile/src/features/layout/native-glass-header-items.ts @@ -9,7 +9,7 @@ type NativeGlassHeaderItem = { /** * iOS 26/27 Mail-style header controls need the native glass button * configuration when they are not part of a larger toolbar. Keep this - * centralized so Expo Router screens and plain react-native-screens demos + * centralized so native-stack screens and plain react-native-screens demos * don't drift apart. */ export function withNativeGlassHeaderItem( diff --git a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts index 5cc12957c5f..820e1222243 100644 --- a/apps/mobile/src/features/layout/native-mail-search-toolbar.ts +++ b/apps/mobile/src/features/layout/native-mail-search-toolbar.ts @@ -10,7 +10,8 @@ type NativeMailSearchToolbarInput = Omit< * * Keeping this behind an app-level helper makes the iOS-only RNS patch an * explicit layout primitive instead of a per-screen object literal. Android can - * keep using Expo Router toolbar primitives without depending on this helper. + * keep using platform-specific header/search primitives without depending on + * this helper. */ export function createNativeMailSearchToolbarItem( input: NativeMailSearchToolbarInput, diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx index 3db7c343ccc..b25f59f5b4a 100644 --- a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx +++ b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx @@ -1,4 +1,4 @@ -import { Stack } from "../../navigation/router"; +import { NativeHeaderToolbar } from "../../navigation/native-stack-header"; import type { ReactNode } from "react"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; @@ -16,9 +16,9 @@ export function WorkspaceSidebarToolbar( } return ( - + {props.children} - {props.afterSidebarButton} - + ); } diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index db00a9d2028..a1aaf7df938 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -22,7 +22,7 @@ import { isFilesystemBrowseQuery, } from "@t3tools/client-runtime/state/projects"; import { CommandId, type EnvironmentId, ProjectId } from "@t3tools/contracts"; -import { useLocalSearchParams, useRouter } from "../../navigation/router"; +import { useRouteParams, useAppNavigation } from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; @@ -250,8 +250,8 @@ function useSelectedEnvironment(): { readonly selectedEnvironment: EnvironmentOption | null; readonly setSelectedEnvironmentId: (environmentId: EnvironmentId) => void; } { - const router = useRouter(); - const params = useLocalSearchParams<{ environmentId?: string }>(); + const router = useAppNavigation(); + const params = useRouteParams<{ environmentId?: string }>(); const environmentOptions = useEnvironmentOptions(); const requestedEnvironmentId = stringParam(params.environmentId) as EnvironmentId | null; const selectedEnvironment = @@ -269,7 +269,7 @@ function useSelectedEnvironment(): { } function EmptyEnvironmentState() { - const router = useRouter(); + const router = useAppNavigation(); return ( @@ -294,7 +294,7 @@ function SourceControlRow(props: { readonly hint: string; readonly isFirst: boolean; }) { - const router = useRouter(); + const router = useAppNavigation(); const iconColor = useThemeColor("--color-icon"); const title = props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`; @@ -323,7 +323,7 @@ function SourceControlRow(props: { isFirst={props.isFirst} onPress={() => router.push({ - pathname: "/new/add-project/repository", + name: "AddProjectRepository", params: { environmentId: props.selectedEnvironmentId, source: props.source, @@ -335,7 +335,7 @@ function SourceControlRow(props: { } export function AddProjectSourceScreen() { - const router = useRouter(); + const router = useAppNavigation(); const accentColor = useThemeColor("--color-icon-muted"); const iconColor = useThemeColor("--color-icon"); const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } = @@ -410,7 +410,7 @@ export function AddProjectSourceScreen() { isFirst onPress={() => router.push({ - pathname: "/new/add-project/local", + name: "AddProjectLocal", params: { environmentId: selectedEnvironment.environmentId }, }) } @@ -440,7 +440,7 @@ export function AddProjectSourceScreen() { } function useCreateProject(environment: EnvironmentOption | null) { - const router = useRouter(); + const router = useAppNavigation(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); const projects = useProjects(); @@ -456,7 +456,7 @@ function useCreateProject(environment: EnvironmentOption | null) { if (existing) { Alert.alert("Project already exists", existing.title); router.replace({ - pathname: "/new/draft", + name: "NewTaskDraft", params: { environmentId: existing.environmentId, projectId: existing.id, @@ -481,7 +481,7 @@ function useCreateProject(environment: EnvironmentOption | null) { return result; } router.replace({ - pathname: "/new/draft", + name: "NewTaskDraft", params: { environmentId: environment.environmentId, projectId, @@ -495,7 +495,7 @@ function useCreateProject(environment: EnvironmentOption | null) { } function useEnvironmentFromParam(): EnvironmentOption | null { - const params = useLocalSearchParams<{ environmentId?: string }>(); + const params = useRouteParams<{ environmentId?: string }>(); const environmentOptions = useEnvironmentOptions(); const environmentId = stringParam(params.environmentId) as EnvironmentId | null; return ( @@ -509,8 +509,8 @@ export function AddProjectRepositoryScreen() { const lookupRepositoryQuery = useAtomQueryRunner(sourceControlEnvironment.repository, { reportFailure: false, }); - const router = useRouter(); - const params = useLocalSearchParams<{ environmentId?: string; source?: string }>(); + const router = useAppNavigation(); + const params = useRouteParams<{ environmentId?: string; source?: string }>(); const environment = useEnvironmentFromParam(); const source = sourceFromParam(params.source); const [repositoryInput, setRepositoryInput] = useState(""); @@ -525,7 +525,7 @@ export function AddProjectRepositoryScreen() { if (!provider) { const remoteUrl = repositoryInput.trim(); router.push({ - pathname: "/new/add-project/destination", + name: "AddProjectDestination", params: { environmentId: environment.environmentId, source, @@ -549,7 +549,7 @@ export function AddProjectRepositoryScreen() { } else { const repository = result.value; router.push({ - pathname: "/new/add-project/destination", + name: "AddProjectDestination", params: { environmentId: environment.environmentId, source, @@ -749,7 +749,7 @@ export function AddProjectDestinationScreen() { const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, { reportFailure: false, }); - const params = useLocalSearchParams<{ + const params = useRouteParams<{ environmentId?: string; remoteUrl?: string; repositoryTitle?: string; diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index 862949db3b4..3503a150214 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -1,4 +1,4 @@ -import { useLocalSearchParams, useRouter } from "../../navigation/router"; +import { useRouteParams, useAppNavigation } from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import { TextInputWrapper } from "expo-paste-input"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; @@ -41,13 +41,13 @@ import { const REVIEW_COMMENT_PREVIEW_MAX_LINES = 5; export function ReviewCommentComposerSheet() { - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); const iconTint = String(useThemeColor("--color-icon")); const target = useReviewCommentTarget(); - const { environmentId, threadId } = useLocalSearchParams<{ + const { environmentId, threadId } = useRouteParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index fc78caf5c53..4b897951540 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -1,7 +1,10 @@ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useLocalSearchParams } from "../../navigation/router"; +import { useRouteParams } from "../../navigation/native-stack-header"; import { useHeaderHeight } from "@react-navigation/elements"; -import Stack from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, +} from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import { memo, @@ -395,7 +398,7 @@ export function ReviewSheet() { const headerForeground = String(useThemeColor("--color-foreground")); const headerMuted = String(useThemeColor("--color-foreground-muted")); const headerIcon = String(useThemeColor("--color-icon")); - const { environmentId, threadId } = useLocalSearchParams<{ + const { environmentId, threadId } = useRouteParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); @@ -584,7 +587,7 @@ export function ReviewSheet() { return ( <> - {layout.usesSplitView ? ( - - + - + ) : null} {showSectionToolbar || panes.supportsAuxiliaryPane ? ( - + {panes.supportsAuxiliaryPane ? ( - ) : null} {showSectionToolbar ? ( - - - + + { @@ -635,9 +638,9 @@ export function ReviewSheet() { } }} > - Working tree - - Working tree + + { @@ -646,9 +649,9 @@ export function ReviewSheet() { } }} > - Branch changes - - Branch changes + + { @@ -657,24 +660,24 @@ export function ReviewSheet() { } }} > - Latest turn - + Latest turn + {sectionMenu.turns.length > 0 ? ( - + {sectionMenu.turns.map((section) => ( - selectSection(section.id)} subtitle={section.subtitle ?? undefined} > - {section.title} - + {section.title} + ))} - + ) : null} - - + void refreshSelectedSection()} subtitle="Reload current diff" > - Refresh - - + Refresh + + ) : null} - + ) : null} diff --git a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts index 30549bfc7a0..557ce16b411 100644 --- a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts +++ b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { NativeSyntheticEvent } from "react-native"; -import { useRouter } from "../../navigation/router"; +import { useAppNavigation } from "../../navigation/native-stack-header"; import * as Arr from "effect/Array"; import { pipe } from "effect/Function"; import * as Result from "effect/Result"; @@ -34,7 +34,7 @@ export function useReviewCommentSelectionController(input: { readonly nativeReviewDiffData: NativeReviewDiffData; }) { const { environmentId, nativeReviewDiffData, selectedSection, threadId } = input; - const { push } = useRouter(); + const { push } = useAppNavigation(); const activeCommentTarget = useReviewCommentTarget(); const [pendingNativeCommentSelection, setPendingNativeCommentSelection] = useState(null); @@ -45,7 +45,7 @@ export function useReviewCommentSelectionController(input: { } push({ - pathname: "/threads/[environmentId]/[threadId]/review-comment", + name: "ThreadReviewComment", params: { environmentId, threadId }, }); }, [environmentId, push, threadId]); diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 6e2c3866671..6f62d85c016 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -1,7 +1,12 @@ import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; import { SymbolView } from "expo-symbols"; -import { Stack, useLocalSearchParams, useRouter } from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, + useRouteParams, + useAppNavigation, +} from "../../navigation/native-stack-header"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Platform, Pressable, Text as RNText, View, useColorScheme } from "react-native"; import { @@ -198,7 +203,7 @@ function pickRunningTerminalSessionForBootstrap( } export function ThreadTerminalRouteScreen() { - const router = useRouter(); + const router = useAppNavigation(); const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); const clearTerminal = useAtomCommand(terminalEnvironment.clear, "terminal clear"); @@ -206,7 +211,7 @@ export function ThreadTerminalRouteScreen() { const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const { state: workspaceState } = useWorkspaceState(); const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); - const params = useLocalSearchParams<{ + const params = useRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; terminalId?: string | string[]; @@ -960,7 +965,7 @@ export function ThreadTerminalRouteScreen() { return ( <> - {layout.usesSplitView ? ( - - + - + ) : null} {isEnvironmentReady ? ( - - - + + + {getTerminalStatusLabel({ status: terminal.status, hasRunningSubprocess: terminal.hasRunningSubprocess, })} - - - Text size - + + Text size + - {`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} - - {`A- ${Math.max(MIN_TERMINAL_FONT_SIZE, fontSize - TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} + + = MAX_TERMINAL_FONT_SIZE} discoverabilityLabel="Increase terminal text size" onPress={handleIncreaseFontSize} > - {`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} - - + {`A+ ${Math.min(MAX_TERMINAL_FONT_SIZE, fontSize + TERMINAL_FONT_SIZE_STEP).toFixed(1)} pt`} + + {terminalMenuSessions.map((session) => ( - handleSelectTerminal(session.terminalId)} @@ -1036,18 +1041,18 @@ export function ThreadTerminalRouteScreen() { .filter(Boolean) .join(" · ")} > - {session.displayLabel} - + {session.displayLabel} + ))} - - Open new terminal - - - + Open new terminal + + + ) : null} diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 60a5cac1622..53cb3f4cefa 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -1,4 +1,4 @@ -import { Stack, useRouter } from "../../navigation/router"; +import { NativeStackScreenOptions, useAppNavigation } from "../../navigation/native-stack-header"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { Alert, InteractionManager, View, useColorScheme } from "react-native"; import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller"; @@ -58,7 +58,7 @@ export function NewTaskDraftScreen(props: { const projects = useProjects(); const createProjectThread = useCreateProjectThread(); const flow = useNewTaskFlow(); - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const isKeyboardVisible = useKeyboardState((state) => state.isVisible); @@ -435,14 +435,14 @@ export function NewTaskDraftScreen(props: { if (!selectedProject) { return ( - + ); } return ( - + diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 89a977e8d7f..3b52ae35596 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -4,7 +4,7 @@ import { type LegendListRef } from "@legendapp/list/react-native"; import type { EnvironmentId, MessageId, ThreadId, TurnId } from "@t3tools/contracts"; import { CHAT_LIST_ANCHOR_OFFSET, resolveChatListAnchoredEndSpace } from "@t3tools/shared/chatList"; import { SymbolView } from "expo-symbols"; -import { useRouter } from "../../navigation/router"; +import { useAppNavigation } from "../../navigation/native-stack-header"; import { memo, useCallback, @@ -1125,7 +1125,7 @@ function ThreadFeedPlaceholder(props: { } export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { - const router = useRouter(); + const router = useAppNavigation(); const copyFeedbackTimeoutRef = useRef | null>(null); const foldSettleFrameRef = useRef(null); const foldSettleSecondFrameRef = useRef(null); diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index 022f55947a0..ee34a4f3d99 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -10,8 +10,8 @@ import { requiresDefaultBranchConfirmation, resolveQuickAction, } from "@t3tools/client-runtime/state/vcs"; -import { useLocalSearchParams, useRouter } from "../../navigation/router"; -import Stack from "../../navigation/router"; +import { useRouteParams, useAppNavigation } from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar } from "../../navigation/native-stack-header"; import { useCallback, useMemo } from "react"; import { Alert } from "react-native"; import { buildThreadFilesNavigation, buildThreadReviewRoutePath } from "../../lib/routes"; @@ -102,8 +102,8 @@ type ThreadGitControlsProps = { }; function useThreadGitControlModel(props: ThreadGitControlsProps) { - const router = useRouter(); - const { environmentId, threadId } = useLocalSearchParams<{ + const router = useAppNavigation(); + const { environmentId, threadId } = useRouteParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); @@ -171,7 +171,7 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { requiresDefaultBranchConfirmation(input.action, isDefaultRef) ) { router.push({ - pathname: "/threads/[environmentId]/[threadId]/git-confirm", + name: "GitConfirm", params: { environmentId, threadId, @@ -222,7 +222,7 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { return; } router.push({ - pathname: "/threads/[environmentId]/[threadId]/git", + name: "GitOverview", params: { environmentId, threadId }, }); }, [environmentId, props.onOpenGitInspector, router, threadId]); @@ -417,9 +417,9 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { } return ( - + {showActionControls && props.auxiliaryPaneControl ? ( - ) : null} {showActionControls ? ( - + {props.projectScripts.length > 0 ? ( props.projectScripts.map((script) => ( - void props.onRunProjectScript(script)} subtitle={script.command} > - {projectScriptMenuLabel(script)} - + + {projectScriptMenuLabel(script)} + + )) ) : ( - {}} subtitle="This project has no saved scripts yet" > - No project scripts - + No project scripts + )} {props.terminalSessions.map((session) => ( - props.onOpenTerminal(session.terminalId)} @@ -464,20 +470,20 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { .filter(Boolean) .join(" · ")} > - {session.displayLabel} - + {session.displayLabel} + ))} - - Open new terminal - - + Open new terminal + + ) : null} {showActionControls && props.showDirectFileControl ? ( - ) : null} {showActionControls ? ( - - + {}} subtitle={compactMenuStatus(props.gitStatus)} > - + {compactMenuBranchLabel(model.currentBranchLabel)} - - - + + void model.runQuickAction()} subtitle={model.quickActionHint ?? undefined} > - {model.quickAction.label} - - {model.quickAction.label} + + - Review changes - - Review changes + + - Files - - Files + + - More - - + More + + ) : null} - + ); } diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 47642b37286..893beb57b49 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -2,7 +2,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell import { LegendList } from "@legendapp/list/react-native"; import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "expo-symbols"; -import { useRouter } from "../../navigation/router"; +import { useAppNavigation } from "../../navigation/native-stack-header"; import { memo, useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; import { Platform, Pressable, StyleSheet, TextInput, View, useColorScheme } from "react-native"; @@ -257,7 +257,7 @@ export function ThreadNavigationSidebar(props: { }) { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; - const router = useRouter(); + const router = useAppNavigation(); const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index e8f5ba82d85..559874f9560 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,4 +1,9 @@ -import { Stack, useFocusEffect, useLocalSearchParams, useRouter } from "../../navigation/router"; +import { + NativeStackScreenOptions, + useFocusEffect, + useRouteParams, + useAppNavigation, +} from "../../navigation/native-stack-header"; import { useCallback, useEffect, @@ -153,7 +158,7 @@ export function ThreadRouteScreen(props: ThreadRouteScreenProps = {}) { const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { selectedThread } = useThreadSelection(); - const params = useLocalSearchParams<{ + const params = useRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); @@ -268,8 +273,8 @@ function ThreadRouteContent( const gitActions = useSelectedThreadGitActions(); const requests = useSelectedThreadRequests(); const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, "thread interrupt"); - const router = useRouter(); - const params = useLocalSearchParams<{ + const router = useAppNavigation(); + const params = useRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); @@ -808,7 +813,7 @@ function ThreadRouteContent( return ( <> {activeInspectorRenderer ? : null} - + {activeInspectorRenderer ? : null} - {layout.usesSplitView || usesNativeHeaderGlass ? null : ( - + - + )} {renderThreadRouteBody(!layout.usesSplitView)} diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx index da62cf856c9..aed634b8d4a 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx @@ -1,5 +1,5 @@ import { sanitizeFeatureBranchName } from "@t3tools/shared/git"; -import { useRouter } from "../../../navigation/router"; +import { useAppNavigation } from "../../../navigation/native-stack-header"; import { useState } from "react"; import { Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -15,7 +15,7 @@ import { vcsEnvironment } from "../../../state/vcs"; import { SheetActionButton } from "./gitSheetComponents"; export function GitBranchesSheet() { - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const { selectedThread } = useThreadSelection(); const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index 5f9dd278b74..db0784ad4f2 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -1,4 +1,4 @@ -import { useRouter } from "../../../navigation/router"; +import { useAppNavigation } from "../../../navigation/native-stack-header"; import { useCallback, useState } from "react"; import { Pressable, ScrollView, View, useColorScheme } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -14,7 +14,7 @@ import { vcsEnvironment } from "../../../state/vcs"; import { SheetActionButton } from "./gitSheetComponents"; export function GitCommitSheet() { - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const isDarkMode = useColorScheme() === "dark"; const { selectedThread } = useThreadSelection(); diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index bc0021cc9e5..e240d8c0602 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -2,7 +2,7 @@ import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime/st import { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; import * as Arr from "effect/Array"; import * as Result from "effect/Result"; -import { useLocalSearchParams, useRouter } from "../../../navigation/router"; +import { useRouteParams, useAppNavigation } from "../../../navigation/native-stack-header"; import { useCallback, useMemo } from "react"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -13,12 +13,12 @@ import { useSelectedThreadGitState } from "../../../state/use-selected-thread-gi import { SheetActionButton } from "./gitSheetComponents"; export function GitConfirmSheet() { - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); - const params = useLocalSearchParams<{ + const params = useRouteParams<{ confirmAction?: string; branchName?: string; includesCommit?: string; diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index c246c9b302b..20e04b54df1 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -5,7 +5,7 @@ import { requiresDefaultBranchConfirmation, } from "@t3tools/client-runtime/state/vcs"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useLocalSearchParams, useRouter } from "../../../navigation/router"; +import { useRouteParams, useAppNavigation } from "../../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, type ComponentProps } from "react"; import { Alert, Platform, Pressable, ScrollView, View } from "react-native"; @@ -33,11 +33,11 @@ export function GitOverviewSheet( readonly presentation?: "sheet" | "inspector"; } = {}, ) { - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const presentation = props.presentation ?? "sheet"; const isInspector = presentation === "inspector"; - const { environmentId, threadId } = useLocalSearchParams<{ + const { environmentId, threadId } = useRouteParams<{ environmentId: EnvironmentId; threadId: ThreadId; }>(); @@ -120,7 +120,7 @@ export function GitOverviewSheet( requiresDefaultBranchConfirmation(input.action, isDefaultRef) ) { router.push({ - pathname: "/threads/[environmentId]/[threadId]/git-confirm", + name: "GitConfirm", params: { environmentId, threadId, @@ -151,7 +151,7 @@ export function GitOverviewSheet( } if (item.dialogAction === "commit") { router.push({ - pathname: "/threads/[environmentId]/[threadId]/git/commit", + name: "GitCommit", params: { environmentId, threadId }, }); return; @@ -248,7 +248,7 @@ export function GitOverviewSheet( disabled={busy || !isRepo} onPress={() => router.push({ - pathname: "/threads/[environmentId]/[threadId]/git/branches", + name: "GitBranches", params: { environmentId, threadId }, }) } diff --git a/apps/mobile/src/lib/routes.test.ts b/apps/mobile/src/lib/routes.test.ts index 773de9d84f7..dffe212457a 100644 --- a/apps/mobile/src/lib/routes.test.ts +++ b/apps/mobile/src/lib/routes.test.ts @@ -23,7 +23,7 @@ describe("thread file routes", () => { it("builds typed navigation params for a file and source line", () => { expect(buildThreadFilesNavigation(thread, "src/main.ts", 42)).toEqual({ - pathname: "/threads/[environmentId]/[threadId]/files/[...path]", + name: "ThreadFile", params: { environmentId: "environment-1", threadId: "thread-1", @@ -35,7 +35,7 @@ describe("thread file routes", () => { it("targets the files index when no file path is provided", () => { expect(buildThreadFilesNavigation(thread)).toEqual({ - pathname: "/threads/[environmentId]/[threadId]/files", + name: "ThreadFiles", params: { environmentId: "environment-1", threadId: "thread-1", diff --git a/apps/mobile/src/lib/routes.ts b/apps/mobile/src/lib/routes.ts index a0ac64e68a2..2cd9f243f12 100644 --- a/apps/mobile/src/lib/routes.ts +++ b/apps/mobile/src/lib/routes.ts @@ -1,10 +1,11 @@ -import type { Href, useRouter } from "../navigation/router"; import { type EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import type { AppNavigation } from "../navigation/app-navigation"; +import type { AppNavigationTarget } from "../navigation/route-model"; import type { SelectedThreadRef } from "../state/remote-runtime-types"; -type Router = ReturnType; +type Router = AppNavigation; type ThreadRouteInput = | Pick @@ -65,15 +66,10 @@ export function buildThreadTerminalRoutePath( return `${basePath}?terminalId=${encodeURIComponent(terminalId)}`; } -/** - * Prefer this over {@link buildThreadTerminalRoutePath} with `router.push(string)` — Expo Router - * often does not merge query strings into `useLocalSearchParams`, which breaks terminal bootstrap - * (`requestedTerminalId` stays null while the UI assumes `default`). - */ export function buildThreadTerminalNavigation( input: ThreadRouteInput | PlainThreadRouteInput, terminalId?: string | null, -): Href { +): AppNavigationTarget { const environmentId = String(input.environmentId); const threadId = String("threadId" in input ? input.threadId : input.id); @@ -87,7 +83,7 @@ export function buildThreadTerminalNavigation( } return { - pathname: "/threads/[environmentId]/[threadId]/terminal", + name: "ThreadTerminal", params, }; } @@ -96,14 +92,14 @@ export function buildThreadFilesNavigation( input: ThreadRouteInput | PlainThreadRouteInput, relativePath?: string | null, line?: number | null, -): Href { +): AppNavigationTarget { const environmentId = String(input.environmentId); const threadId = String("threadId" in input ? input.threadId : input.id); const path = relativePath?.split("/").filter((segment) => segment.length > 0) ?? []; if (path.length === 0) { return { - pathname: "/threads/[environmentId]/[threadId]/files", + name: "ThreadFiles", params: { environmentId, threadId }, }; } @@ -119,7 +115,7 @@ export function buildThreadFilesNavigation( } return { - pathname: "/threads/[environmentId]/[threadId]/files/[...path]", + name: "ThreadFile", params, }; } diff --git a/apps/mobile/src/navigation/RootNavigator.tsx b/apps/mobile/src/navigation/RootNavigator.tsx new file mode 100644 index 00000000000..484275d7f0e --- /dev/null +++ b/apps/mobile/src/navigation/RootNavigator.tsx @@ -0,0 +1,405 @@ +import { createNativeStackNavigator } from "@react-navigation/native-stack"; +import { useCallback, type ReactNode } from "react"; +import { StatusBar, useColorScheme, useWindowDimensions } from "react-native"; +import { useResolveClassNames } from "uniwind"; + +import { ArchivedThreadsRouteScreen } from "../features/archive/ArchivedThreadsRouteScreen"; +import { useAgentNotificationNavigation } from "../features/agent-awareness/notificationNavigation"; +import { + ClerkSettingsSheetDetentProvider, + useClerkSettingsSheetDetent, +} from "../features/cloud/ClerkSettingsSheetDetent"; +import { + AdaptiveWorkspaceLayout, + useAdaptiveWorkspaceLayout, +} from "../features/layout/AdaptiveWorkspaceLayout"; +import { ThreadFilesTreeScreen, ThreadFileScreen } from "../features/files/ThreadFilesRouteScreen"; +import { HardwareKeyboardCommandProvider } from "../features/keyboard/HardwareKeyboardCommandProvider"; +import { ReviewCommentComposerSheet } from "../features/review/ReviewCommentComposerSheet"; +import { ReviewHighlighterProvider } from "../features/review/ReviewHighlighterProvider"; +import { ReviewSheet } from "../features/review/ReviewSheet"; +import { ThreadTerminalRouteScreen } from "../features/terminal/ThreadTerminalRouteScreen"; +import { ThreadRouteScreen } from "../features/threads/ThreadRouteScreen"; +import { GitBranchesSheet } from "../features/threads/git/GitBranchesSheet"; +import { GitCommitSheet } from "../features/threads/git/GitCommitSheet"; +import { GitConfirmSheet } from "../features/threads/git/GitConfirmSheet"; +import { GitOverviewSheet } from "../features/threads/git/GitOverviewSheet"; +import { deriveStableFormSheetDetent } from "../lib/layout"; +import { useThemeColor } from "../lib/useThemeColor"; +import NotFoundRoute from "../screens/+not-found"; +import ConnectionsRouteScreen from "../screens/connections"; +import ConnectionsNewRouteScreen from "../screens/connections/new"; +import RnsGlassDebugRoute from "../screens/debug/rns-glass"; +import HomeRouteScreen from "../screens"; +import AddProjectRoute from "../screens/new/add-project"; +import AddProjectDestinationRoute from "../screens/new/add-project/destination"; +import AddProjectLocalRoute from "../screens/new/add-project/local"; +import AddProjectRepositoryRoute from "../screens/new/add-project/repository"; +import NewTaskDraftRoute from "../screens/new/draft"; +import NewTaskRoute from "../screens/new"; +import SettingsAuthRouteScreen from "../screens/settings/auth"; +import SettingsEnvironmentsRouteScreen from "../screens/settings/environments"; +import SettingsRouteScreen from "../screens/settings"; +import SettingsWaitlistRouteScreen from "../screens/settings/waitlist"; +import { ThreadSelectionProvider } from "../state/use-thread-selection"; +import { useThreadOutboxDrain } from "../state/use-thread-outbox-drain"; +import { useCurrentPathname } from "./app-navigation"; +import type { AppStackParamList } from "./route-model"; + +const RootStack = createNativeStackNavigator(); + +function ThreadSelectionRoute(props: { readonly children: ReactNode }) { + return {props.children}; +} + +function ThreadRoute() { + return ( + + + + ); +} + +function ThreadTerminalRoute() { + return ( + + + + ); +} + +function ThreadFilesRoute() { + return ( + + + + ); +} + +function ThreadFileRoute() { + return ( + + + + ); +} + +function ThreadReviewRoute() { + return ( + + + + + + ); +} + +function ThreadReviewCommentRoute() { + return ( + + + + ); +} + +function GitOverviewRoute() { + return ( + + + + ); +} + +function GitCommitRoute() { + return ( + + + + ); +} + +function GitBranchesRoute() { + return ( + + + + ); +} + +function GitConfirmRoute() { + return ( + + + + ); +} + +export function RootNavigator() { + const pathname = useCurrentPathname(); + const expandedSettingsRouteIsActive = + pathname === "/settings/archive" || pathname === "/settings/auth"; + + return ( + + + + + + ); +} + +function RootNavigatorContent() { + const pathname = useCurrentPathname(); + const isDebugRoute = pathname.startsWith("/debug/"); + + if (isDebugRoute) { + return ; + } + + return ; +} + +function DebugNavigatorHost() { + return ( + <> + + + + + + + + ); +} + +function WorkspaceNavigatorHost() { + const colorScheme = useColorScheme(); + const statusBarBg = useThemeColor("--color-status-bar"); + useAgentNotificationNavigation(); + useThreadOutboxDrain(); + + return ( + <> + + + + + + ); +} + +function WorkspaceNavigator() { + const { collapse, isExpanded } = useClerkSettingsSheetDetent(); + const { layout } = useAdaptiveWorkspaceLayout(); + const { height } = useWindowDimensions(); + const sheetStyle = useResolveClassNames("bg-sheet"); + + const handleSettingsTransitionEnd = useCallback( + (event: { data: { closing: boolean } }) => { + if (event.data.closing) { + collapse(); + } + }, + [collapse], + ); + + const connectionSheetScreenOptions = { + contentStyle: sheetStyle, + gestureEnabled: true, + headerShown: false, + presentation: "formSheet" as const, + sheetAllowedDetents: [0.55, 0.7], + sheetGrabberVisible: true, + }; + const settingsScreenOptions = layout.usesSplitView + ? { + animation: "none" as const, + contentStyle: sheetStyle, + gestureEnabled: false, + headerShown: false, + presentation: "card" as const, + } + : { + ...connectionSheetScreenOptions, + sheetAllowedDetents: isExpanded ? [0.92] : [0.7], + }; + const newTaskScreenOptions = { + contentStyle: sheetStyle, + gestureEnabled: true, + headerShown: false, + presentation: "formSheet" as const, + sheetAllowedDetents: [layout.usesSplitView ? deriveStableFormSheetDetent(height) : 0.92], + sheetGrabberVisible: !layout.usesSplitView, + }; + + return ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/apps/mobile/src/navigation/app-navigation.tsx b/apps/mobile/src/navigation/app-navigation.tsx new file mode 100644 index 00000000000..6bc4a7e2d57 --- /dev/null +++ b/apps/mobile/src/navigation/app-navigation.tsx @@ -0,0 +1,176 @@ +import { + CommonActions, + NavigationContainer, + StackActions, + type NavigationContainerRef, +} from "@react-navigation/native"; +import { createContext, use, useCallback, useMemo, useRef, useState, type ReactNode } from "react"; + +import { appLinking } from "./linking"; +import { + buildPathFromRoute, + getFocusedRoute, + resolveNavigationTarget, + type AppFocusedRoute, + type AppHref, + type AppStackParamList, + type RouteParams, +} from "./route-model"; + +export type AppNavigation = { + readonly push: (href: AppHref) => void; + readonly replace: (href: AppHref) => void; + readonly back: () => void; + readonly dismiss: () => void; + readonly dismissAll: () => void; + readonly canGoBack: () => boolean; + readonly setParams: (params: RouteParams) => void; +}; + +type AppNavigationContextValue = { + readonly navigationRef: React.RefObject | null>; + readonly pathname: string; + readonly params: RouteParams; + readonly navigation: AppNavigation; +}; + +export const AppNavigationContext = createContext(null); + +export function useAppNavigation(): AppNavigation { + const context = use(AppNavigationContext); + if (context === null) { + throw new Error("useAppNavigation must be used within AppNavigationProvider"); + } + return context.navigation; +} + +export function useCurrentPathname(): string { + const context = use(AppNavigationContext); + if (context === null) { + throw new Error("useCurrentPathname must be used within AppNavigationProvider"); + } + return context.pathname; +} + +export function useCurrentRouteParams(): T { + const context = use(AppNavigationContext); + if (context === null) { + throw new Error("useCurrentRouteParams must be used within AppNavigationProvider"); + } + return context.params as T; +} + +export function createAppNavigation( + navigationRef: React.RefObject | null>, +): AppNavigation { + const navigateWithAction = (href: AppHref, action: "push" | "replace" | "navigate"): void => { + const target = resolveNavigationTarget(href); + const navigation = navigationRef.current; + if (!navigation) { + return; + } + + if (action === "push") { + navigation.dispatch(StackActions.push(target.name, target.params)); + return; + } + if (action === "replace") { + navigation.dispatch(StackActions.replace(target.name, target.params)); + return; + } + navigation.dispatch( + CommonActions.navigate({ + name: target.name, + params: target.params, + }), + ); + }; + + return { + push: (href) => navigateWithAction(href, "push"), + replace: (href) => navigateWithAction(href, "replace"), + back: () => { + const navigation = navigationRef.current; + if (!navigation) return; + if (navigation.canGoBack()) { + navigation.goBack(); + return; + } + navigation.dispatch(StackActions.replace("Home")); + }, + dismiss: () => { + const navigation = navigationRef.current; + if (!navigation) return; + if (navigation.canGoBack()) { + navigation.goBack(); + return; + } + navigation.dispatch(StackActions.replace("Home")); + }, + dismissAll: () => { + const navigation = navigationRef.current; + if (!navigation) return; + navigation.dispatch( + CommonActions.reset({ + index: 0, + routes: [{ name: "Home" }], + }), + ); + }, + canGoBack: () => navigationRef.current?.canGoBack() ?? false, + setParams: (params) => { + navigationRef.current?.dispatch(CommonActions.setParams(params)); + }, + }; +} + +export function AppNavigationProvider(props: { readonly children: ReactNode }) { + const navigationRef = useRef>(null); + const [pathname, setPathname] = useState("/"); + const [params, setParams] = useState({}); + const navigation = useMemo(() => createAppNavigation(navigationRef), []); + const contextValue = useMemo( + () => ({ + navigationRef, + pathname, + params, + navigation, + }), + [navigation, params, pathname], + ); + const syncState = useCallback(() => { + const route = getFocusedRoute(navigationRef.current?.getRootState()); + if (!route) { + setPathname("/"); + setParams({}); + return; + } + + const current = pathAndParamsFromCurrentRoute(route); + setPathname(current.pathname); + setParams(current.params); + }, []); + + return ( + + + {props.children} + + + ); +} + +export function pathAndParamsFromCurrentRoute(route: AppFocusedRoute): { + readonly pathname: string; + readonly params: RouteParams; +} { + return { + pathname: buildPathFromRoute(route), + params: (route.params ?? {}) as RouteParams, + }; +} diff --git a/apps/mobile/src/navigation/linking.ts b/apps/mobile/src/navigation/linking.ts new file mode 100644 index 00000000000..55ecafd7cf0 --- /dev/null +++ b/apps/mobile/src/navigation/linking.ts @@ -0,0 +1,39 @@ +import type { LinkingOptions } from "@react-navigation/native"; +import * as Linking from "expo-linking"; + +import type { AppStackParamList } from "./route-model"; + +export const appLinking: LinkingOptions = { + prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], + config: { + screens: { + Home: "", + DebugRnsGlass: "debug/rns-glass", + Settings: "settings", + SettingsEnvironments: "settings/environments", + SettingsEnvironmentNew: "settings/environment-new", + SettingsArchive: "settings/archive", + SettingsAuth: "settings/auth", + SettingsWaitlist: "settings/waitlist", + Connections: "connections", + ConnectionsNew: "connections/new", + NewTask: "new", + AddProject: "new/add-project", + AddProjectRepository: "new/add-project/repository", + AddProjectDestination: "new/add-project/destination", + AddProjectLocal: "new/add-project/local", + NewTaskDraft: "new/draft", + Thread: "threads/:environmentId/:threadId", + ThreadTerminal: "threads/:environmentId/:threadId/terminal", + ThreadReview: "threads/:environmentId/:threadId/review", + ThreadReviewComment: "threads/:environmentId/:threadId/review-comment", + ThreadFiles: "threads/:environmentId/:threadId/files", + ThreadFile: "threads/:environmentId/:threadId/files/:path*", + GitOverview: "threads/:environmentId/:threadId/git", + GitCommit: "threads/:environmentId/:threadId/git/commit", + GitBranches: "threads/:environmentId/:threadId/git/branches", + GitConfirm: "threads/:environmentId/:threadId/git-confirm", + NotFound: "*", + }, + }, +}; diff --git a/apps/mobile/src/navigation/router.tsx b/apps/mobile/src/navigation/native-stack-header.tsx similarity index 59% rename from apps/mobile/src/navigation/router.tsx rename to apps/mobile/src/navigation/native-stack-header.tsx index f97e5344663..611d6ec1c26 100644 --- a/apps/mobile/src/navigation/router.tsx +++ b/apps/mobile/src/navigation/native-stack-header.tsx @@ -1,13 +1,9 @@ import { - CommonActions, - StackActions, useFocusEffect, useNavigation, useRoute, - type NavigationContainerRef, type ParamListBase, } from "@react-navigation/native"; -import type { ColorValue } from "react-native"; import type { NativeStackHeaderItem, NativeStackHeaderItemMenu, @@ -17,29 +13,24 @@ import type { import { Children, cloneElement, - createContext, createElement, Fragment, isValidElement, - use, useEffect, useMemo, type ReactElement, type ReactNode, } from "react"; +import type { ColorValue } from "react-native"; -import { - buildPathFromRoute, - resolveNavigationTarget, - type AppHref, - type AppStackParamList, - type AppFocusedRoute, - type RouteParams, -} from "./route-model"; +import { useAppNavigation } from "./app-navigation"; +import type { AppHref, RouteParams } from "./route-model"; export { useFocusEffect }; -export type Href = AppHref; -type CompatNativeStackNavigationOptions = Omit< +export { useAppNavigation, useCurrentPathname, useCurrentRouteParams } from "./app-navigation"; +export type { AppHref }; + +export type AppNativeStackNavigationOptions = Omit< NativeStackNavigationOptions, "headerTintColor" | "unstable_headerLeftItems" | "unstable_headerRightItems" > & { @@ -52,133 +43,11 @@ type CompatNativeStackNavigationOptions = Omit< readonly unstable_navigationItemStyle?: unknown; }; -export type { CompatNativeStackNavigationOptions as NativeStackNavigationOptions }; - -type RouterLike = { - readonly push: (href: AppHref) => void; - readonly replace: (href: AppHref) => void; - readonly back: () => void; - readonly dismiss: () => void; - readonly dismissAll: () => void; - readonly canGoBack: () => boolean; - readonly setParams: (params: RouteParams) => void; -}; - -type NavigationContextValue = { - readonly navigationRef: React.RefObject | null>; - readonly pathname: string; - readonly params: RouteParams; - readonly router: RouterLike; -}; - -export const AppNavigationContext = createContext(null); - -export function useRouter(): RouterLike { - const context = use(AppNavigationContext); - if (context === null) { - throw new Error("useRouter must be used within AppNavigationProvider"); - } - return context.router; -} - -export function usePathname(): string { - const context = use(AppNavigationContext); - if (context === null) { - throw new Error("usePathname must be used within AppNavigationProvider"); - } - return context.pathname; -} - -export function useLocalSearchParams(): T { +export function useRouteParams(): T { const route = useRoute(); return (route.params ?? {}) as RouteParams as T; } -export function useGlobalSearchParams(): T { - const context = use(AppNavigationContext); - if (context === null) { - throw new Error("useGlobalSearchParams must be used within AppNavigationProvider"); - } - return context.params as T; -} - -export function createRouter( - navigationRef: React.RefObject | null>, -): RouterLike { - const navigateWithAction = (href: AppHref, action: "push" | "replace" | "navigate"): void => { - const target = resolveNavigationTarget(href); - const navigation = navigationRef.current; - if (!navigation) { - return; - } - - if (action === "push") { - navigation.dispatch(StackActions.push(target.name, target.params)); - return; - } - if (action === "replace") { - navigation.dispatch(StackActions.replace(target.name, target.params)); - return; - } - navigation.dispatch( - CommonActions.navigate({ - name: target.name, - params: target.params, - }), - ); - }; - - return { - push: (href) => navigateWithAction(href, "push"), - replace: (href) => navigateWithAction(href, "replace"), - back: () => { - const navigation = navigationRef.current; - if (!navigation) return; - if (navigation.canGoBack()) { - navigation.goBack(); - return; - } - navigation.dispatch(StackActions.replace("Home")); - }, - dismiss: () => { - const navigation = navigationRef.current; - if (!navigation) return; - if (navigation.canGoBack()) { - navigation.goBack(); - return; - } - navigation.dispatch(StackActions.replace("Home")); - }, - dismissAll: () => { - const navigation = navigationRef.current; - if (!navigation) return; - navigation.dispatch( - CommonActions.reset({ - index: 0, - routes: [{ name: "Home" }], - }), - ); - }, - canGoBack: () => navigationRef.current?.canGoBack() ?? false, - setParams: (params) => { - navigationRef.current?.dispatch(CommonActions.setParams(params)); - }, - }; -} - -export function deriveNavigationContextValue(input: { - readonly navigationRef: React.RefObject | null>; - readonly pathname: string; - readonly params: RouteParams; -}): NavigationContextValue { - return { - navigationRef: input.navigationRef, - pathname: input.pathname, - params: input.params, - router: createRouter(input.navigationRef), - }; -} - function useNativeStackNavigation(): NativeStackNavigationProp | null { try { return useNavigation>(); @@ -188,7 +57,7 @@ function useNativeStackNavigation(): NativeStackNavigationProp | } function normalizeScreenOptions( - options: CompatNativeStackNavigationOptions | undefined, + options: AppNativeStackNavigationOptions | undefined, ): NativeStackNavigationOptions | undefined { if (!options) { return options; @@ -213,8 +82,8 @@ function normalizeScreenOptions( return normalized as NativeStackNavigationOptions; } -function StackScreen(props: { - readonly options?: CompatNativeStackNavigationOptions; +export function NativeStackScreenOptions(props: { + readonly options?: AppNativeStackNavigationOptions; readonly listeners?: Record void>; readonly name?: string; }) { @@ -282,7 +151,7 @@ function convertMenuAction( element: ReactElement, ): NativeStackHeaderItemMenu["menu"]["items"][number] | null { const typeName = elementTypeName(element); - if (typeName === "ToolbarMenuAction") { + if (typeName === "NativeHeaderToolbarMenuAction") { const label = labelFromChildren(element.props.children); return { type: "action", @@ -303,7 +172,7 @@ function convertMenuAction( }; } - if (typeName === "ToolbarMenu") { + if (typeName === "NativeHeaderToolbarMenu") { return { type: "submenu", label: @@ -341,7 +210,7 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { } const typeName = elementTypeName(child); - if (typeName === "ToolbarButton") { + if (typeName === "NativeHeaderToolbarButton") { return { type: "button", label: "", @@ -360,7 +229,7 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { }; } - if (typeName === "ToolbarMenu") { + if (typeName === "NativeHeaderToolbarMenu") { return { type: "menu", label: typeof child.props.title === "string" ? child.props.title : "", @@ -379,7 +248,7 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { }; } - if (typeName === "ToolbarSpacer") { + if (typeName === "NativeHeaderToolbarSpacer") { return { type: "spacing", spacing: typeof child.props.width === "number" ? child.props.width : 8, @@ -400,7 +269,7 @@ function collectToolbarItems(children: ReactNode): NativeStackHeaderItem[] { return items; } -function Toolbar(props: { +function NativeHeaderToolbarRoot(props: { readonly placement?: "left" | "right" | "bottom"; readonly children?: ReactNode; }) { @@ -421,7 +290,7 @@ function Toolbar(props: { return null; } -function ToolbarButton(_props: { +function NativeHeaderToolbarButton(_props: { readonly accessibilityLabel?: string; readonly disabled?: boolean; readonly icon?: string; @@ -430,9 +299,9 @@ function ToolbarButton(_props: { }) { return null; } -ToolbarButton.displayName = "ToolbarButton"; +NativeHeaderToolbarButton.displayName = "NativeHeaderToolbarButton"; -function ToolbarMenu(_props: { +function NativeHeaderToolbarMenu(_props: { readonly accessibilityLabel?: string; readonly children?: ReactNode; readonly disabled?: boolean; @@ -443,9 +312,9 @@ function ToolbarMenu(_props: { }) { return null; } -ToolbarMenu.displayName = "ToolbarMenu"; +NativeHeaderToolbarMenu.displayName = "NativeHeaderToolbarMenu"; -function ToolbarMenuAction(_props: { +function NativeHeaderToolbarMenuAction(_props: { readonly children?: ReactNode; readonly destructive?: boolean; readonly disabled?: boolean; @@ -457,80 +326,70 @@ function ToolbarMenuAction(_props: { }) { return null; } -ToolbarMenuAction.displayName = "ToolbarMenuAction"; +NativeHeaderToolbarMenuAction.displayName = "NativeHeaderToolbarMenuAction"; -function ToolbarLabel(_props: { readonly children?: ReactNode }) { +function NativeHeaderToolbarLabel(_props: { readonly children?: ReactNode }) { return null; } -ToolbarLabel.displayName = "ToolbarLabel"; +NativeHeaderToolbarLabel.displayName = "NativeHeaderToolbarLabel"; -function ToolbarSpacer(_props: { readonly sharesBackground?: boolean; readonly width?: number }) { +function NativeHeaderToolbarSpacer(_props: { + readonly sharesBackground?: boolean; + readonly width?: number; +}) { return null; } -ToolbarSpacer.displayName = "ToolbarSpacer"; +NativeHeaderToolbarSpacer.displayName = "NativeHeaderToolbarSpacer"; -function ToolbarSearchBarSlot() { +function NativeHeaderToolbarSearchBarSlot() { return null; } -ToolbarSearchBarSlot.displayName = "ToolbarSearchBarSlot"; - -function StackScreenTitle(_props: { readonly asChild?: boolean; readonly children?: ReactNode }) { +NativeHeaderToolbarSearchBarSlot.displayName = "NativeHeaderToolbarSearchBarSlot"; + +export const NativeHeaderToolbar = Object.assign(NativeHeaderToolbarRoot, { + Button: NativeHeaderToolbarButton, + Label: NativeHeaderToolbarLabel, + Menu: Object.assign(NativeHeaderToolbarMenu, { + Action: NativeHeaderToolbarMenuAction, + }), + MenuAction: NativeHeaderToolbarMenuAction, + SearchBarSlot: NativeHeaderToolbarSearchBarSlot, + Spacer: NativeHeaderToolbarSpacer, +}); + +function NativeStackScreenTitle(_props: { + readonly asChild?: boolean; + readonly children?: ReactNode; +}) { return null; } -StackScreen.Title = StackScreenTitle; - -export const Stack = Object.assign( - function StackCompat(props: { readonly children?: ReactNode }) { - return createElement(Fragment, null, props.children); - }, - { - Screen: StackScreen, - Toolbar: Object.assign(Toolbar, { - Button: ToolbarButton, - Label: ToolbarLabel, - Menu: Object.assign(ToolbarMenu, { - Action: ToolbarMenuAction, - }), - MenuAction: ToolbarMenuAction, - SearchBarSlot: ToolbarSearchBarSlot, - Spacer: ToolbarSpacer, - }), - }, -); - -export default Stack; - -export function Redirect(props: { readonly href: AppHref }) { - const router = useRouter(); +NativeStackScreenOptions.Title = NativeStackScreenTitle; + +export function NavigateTo(props: { readonly href: AppHref }) { + const navigation = useAppNavigation(); useEffect(() => { - router.replace(props.href); - }, [props.href, router]); + navigation.replace(props.href); + }, [navigation, props.href]); return null; } -export function Link(props: { +export function NavigationLink(props: { readonly href: AppHref; readonly asChild?: boolean; readonly children?: ReactNode; }) { - const router = useRouter(); + const navigation = useAppNavigation(); if (props.asChild && isValidElement<{ onPress?: () => void }>(props.children)) { return cloneElement(props.children, { - onPress: () => router.push(props.href), + onPress: () => navigation.push(props.href), }); } return null; } -export function pathAndParamsFromCurrentRoute(route: { - readonly name: string; - readonly params?: object; -}): { readonly pathname: string; readonly params: RouteParams } { - return { - pathname: buildPathFromRoute(route as AppFocusedRoute), - params: (route.params ?? {}) as RouteParams, - }; -} - -export type Router = RouterLike; +export const NativeStackFragment = function NativeStackFragment(props: { + readonly children?: ReactNode; +}) { + return createElement(Fragment, null, props.children); +}; diff --git a/apps/mobile/src/navigation/route-model.ts b/apps/mobile/src/navigation/route-model.ts index 3da7df17649..e11e741e203 100644 --- a/apps/mobile/src/navigation/route-model.ts +++ b/apps/mobile/src/navigation/route-model.ts @@ -63,6 +63,7 @@ export type AppStackParamList = { export type AppHref = | string + | AppNavigationTarget | { readonly pathname: string; readonly params?: RouteParams; @@ -148,6 +149,9 @@ function mergeParams( export function resolveNavigationTarget(href: AppHref): AppNavigationTarget { if (typeof href !== "string") { + if ("name" in href) { + return withParams(href.name, href.params); + } return resolveNavigationObject(href.pathname, href.params); } diff --git a/apps/mobile/src/screens/+not-found.tsx b/apps/mobile/src/screens/+not-found.tsx index 94e24a78577..3d9f502e557 100644 --- a/apps/mobile/src/screens/+not-found.tsx +++ b/apps/mobile/src/screens/+not-found.tsx @@ -1,4 +1,4 @@ -import { Link } from "../navigation/router"; +import { NavigationLink } from "../navigation/native-stack-header"; import { Pressable, ScrollView, StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; @@ -32,11 +32,11 @@ export default function NotFoundRoute() { Route not found - + Return home - + ); } diff --git a/apps/mobile/src/screens/connections/index.tsx b/apps/mobile/src/screens/connections/index.tsx index d6f5f7b2822..e459a141ccd 100644 --- a/apps/mobile/src/screens/connections/index.tsx +++ b/apps/mobile/src/screens/connections/index.tsx @@ -1,4 +1,8 @@ -import { Stack, useRouter } from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, + useAppNavigation, +} from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import type { EnvironmentId } from "@t3tools/contracts"; import { useCallback, useState } from "react"; @@ -18,7 +22,7 @@ export default function ConnectionsRouteScreen() { onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const hasEnvironments = connectedEnvironments.length > 0; const [expandedId, setExpandedId] = useState(null); @@ -31,18 +35,18 @@ export default function ConnectionsRouteScreen() { return ( - - - + router.push("/connections/new")} separateBackground /> - + (); + const router = useAppNavigation(); + const params = useRouteParams<{ mode?: string }>(); const insets = useSafeAreaInsets(); const [hostInput, setHostInput] = useState(""); const [codeInput, setCodeInput] = useState(""); @@ -124,13 +129,13 @@ export default function ConnectionsNewRouteScreen() { return ( - - - + { if (showScanner) { @@ -141,7 +146,7 @@ export default function ConnectionsNewRouteScreen() { }} separateBackground /> - + - + {usesWideSplit ? ( - router.push("/new")} diff --git a/apps/mobile/src/screens/new/add-project/repository.tsx b/apps/mobile/src/screens/new/add-project/repository.tsx index be3140937c7..c085bcb94b3 100644 --- a/apps/mobile/src/screens/new/add-project/repository.tsx +++ b/apps/mobile/src/screens/new/add-project/repository.tsx @@ -1,10 +1,10 @@ -import { Stack, useLocalSearchParams } from "../../../navigation/router"; +import { NativeStackScreenOptions, useRouteParams } from "../../../navigation/native-stack-header"; import { addProjectRemoteSourceLabel } from "@t3tools/client-runtime/operations/projects"; import { AddProjectRepositoryScreen } from "../../../features/projects/AddProjectScreen"; export default function AddProjectRepositoryRoute() { - const params = useLocalSearchParams<{ source?: string | string[] }>(); + const params = useRouteParams<{ source?: string | string[] }>(); const source = Array.isArray(params.source) ? params.source[0] : params.source; const title = source === "github" || @@ -16,7 +16,7 @@ export default function AddProjectRepositoryRoute() { return ( <> - + ); diff --git a/apps/mobile/src/screens/new/draft.tsx b/apps/mobile/src/screens/new/draft.tsx index f4cb57935c7..b32d601596d 100644 --- a/apps/mobile/src/screens/new/draft.tsx +++ b/apps/mobile/src/screens/new/draft.tsx @@ -1,9 +1,9 @@ -import { Stack, useLocalSearchParams } from "../../navigation/router"; +import { NativeStackScreenOptions, useRouteParams } from "../../navigation/native-stack-header"; import { NewTaskDraftScreen } from "../../features/threads/NewTaskDraftScreen"; export default function NewTaskDraftRoute() { - const params = useLocalSearchParams<{ + const params = useRouteParams<{ environmentId?: string | string[]; projectId?: string | string[]; title?: string | string[]; @@ -11,7 +11,7 @@ export default function NewTaskDraftRoute() { return ( <> - - - + + {layout.usesSplitView ? ( - router.dismiss()} separateBackground /> ) : null} - router.push("/new/add-project")} separateBackground /> - + - + ); })} diff --git a/apps/mobile/src/screens/settings/archive.tsx b/apps/mobile/src/screens/settings/archive.tsx deleted file mode 100644 index 2b900afbbce..00000000000 --- a/apps/mobile/src/screens/settings/archive.tsx +++ /dev/null @@ -1,3 +0,0 @@ -import { ArchivedThreadsRouteScreen } from "../../features/archive/ArchivedThreadsRouteScreen"; - -export default ArchivedThreadsRouteScreen; diff --git a/apps/mobile/src/screens/settings/auth.tsx b/apps/mobile/src/screens/settings/auth.tsx index 984e7fbb0d1..0d5a1c04c2b 100644 --- a/apps/mobile/src/screens/settings/auth.tsx +++ b/apps/mobile/src/screens/settings/auth.tsx @@ -1,6 +1,6 @@ import { useAuth } from "@clerk/expo"; import { AuthView, UserProfileView } from "@clerk/expo/native"; -import { Redirect, Stack } from "../../navigation/router"; +import { NavigateTo, NativeStackScreenOptions } from "../../navigation/native-stack-header"; import { View } from "react-native"; import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; @@ -9,7 +9,7 @@ export default function SettingsAuthRouteScreen() { return hasCloudPublicConfig() ? ( ) : ( - + ); } @@ -18,7 +18,7 @@ function ConfiguredSettingsAuthRouteScreen() { return ( <> - + {isLoaded ? ( isSignedIn ? ( diff --git a/apps/mobile/src/screens/settings/environment-new.tsx b/apps/mobile/src/screens/settings/environment-new.tsx deleted file mode 100644 index 3fb3e8d3a04..00000000000 --- a/apps/mobile/src/screens/settings/environment-new.tsx +++ /dev/null @@ -1 +0,0 @@ -export { default } from "../connections/new"; diff --git a/apps/mobile/src/screens/settings/environments.tsx b/apps/mobile/src/screens/settings/environments.tsx index 9584be950c9..0af15c027fe 100644 --- a/apps/mobile/src/screens/settings/environments.tsx +++ b/apps/mobile/src/screens/settings/environments.tsx @@ -1,5 +1,9 @@ import { useAuth } from "@clerk/expo"; -import { Stack, useRouter } from "../../navigation/router"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, + useAppNavigation, +} from "../../navigation/native-stack-header"; import { SymbolView } from "expo-symbols"; import { connectionStatusText, @@ -41,7 +45,7 @@ export default function SettingsEnvironmentsRouteScreen() { onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); - const router = useRouter(); + const router = useAppNavigation(); const insets = useSafeAreaInsets(); const { localEnvironments, connectedCloudEnvironments } = splitEnvironmentSections({ connectedEnvironments, @@ -57,18 +61,18 @@ export default function SettingsEnvironmentsRouteScreen() { return ( - - - + router.push("/settings/environment-new")} separateBackground /> - + {layout.usesSplitView ? ( - - + router.back()} separateBackground /> - + ) : null} {hasCloudPublicConfig() ? : } @@ -64,7 +69,7 @@ function LocalSettingsRouteScreen() { return ( - + - + + {content} - + ); } diff --git a/apps/mobile/src/screens/settings/waitlist.tsx b/apps/mobile/src/screens/settings/waitlist.tsx index 952d119d82f..d83e3c966de 100644 --- a/apps/mobile/src/screens/settings/waitlist.tsx +++ b/apps/mobile/src/screens/settings/waitlist.tsx @@ -1,5 +1,10 @@ import { useAuth } from "@clerk/expo"; -import { Redirect, Stack, useFocusEffect, useRouter } from "../../navigation/router"; +import { + NavigateTo, + NativeStackScreenOptions, + useFocusEffect, + useAppNavigation, +} from "../../navigation/native-stack-header"; import { useCallback } from "react"; import { ScrollView } from "react-native"; @@ -11,14 +16,14 @@ export default function SettingsWaitlistRouteScreen() { return hasCloudPublicConfig() ? ( ) : ( - + ); } function ConfiguredSettingsWaitlistRouteScreen() { const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { expand } = useClerkSettingsSheetDetent(); - const router = useRouter(); + const router = useAppNavigation(); useFocusEffect( useCallback(() => { @@ -30,7 +35,7 @@ function ConfiguredSettingsWaitlistRouteScreen() { return ( <> - + ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/index.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/index.tsx deleted file mode 100644 index b67630dbf06..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/files/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ThreadFilesTreeScreen } from "../../../../../features/files/ThreadFilesRouteScreen"; - -export default function ThreadFilesIndexRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git-confirm.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git-confirm.tsx deleted file mode 100644 index 3773ce55b2e..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git-confirm.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GitConfirmSheet } from "../../../../features/threads/git/GitConfirmSheet"; - -export default function GitConfirmRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/branches.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/branches.tsx deleted file mode 100644 index 955e4febd7a..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/branches.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GitBranchesSheet } from "../../../../../features/threads/git/GitBranchesSheet"; - -export default function GitBranchesRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/commit.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/commit.tsx deleted file mode 100644 index 4510f81a8c9..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/commit.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GitCommitSheet } from "../../../../../features/threads/git/GitCommitSheet"; - -export default function GitCommitRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/index.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/index.tsx deleted file mode 100644 index dd099d80d24..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/git/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { GitOverviewSheet } from "../../../../../features/threads/git/GitOverviewSheet"; - -export default function GitRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/index.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/index.tsx deleted file mode 100644 index 4586ee93ca0..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ThreadRouteScreen } from "../../../../features/threads/ThreadRouteScreen"; - -export default function ThreadRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review-comment.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review-comment.tsx deleted file mode 100644 index 281ec341079..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review-comment.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ReviewCommentComposerSheet } from "../../../../features/review/ReviewCommentComposerSheet"; - -export default function ReviewCommentRoute() { - return ; -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review.tsx deleted file mode 100644 index ee993ee7c87..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/review.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { ReviewHighlighterProvider } from "../../../../features/review/ReviewHighlighterProvider"; -import { ReviewSheet } from "../../../../features/review/ReviewSheet"; - -export default function ReviewRoute() { - return ( - - - - ); -} diff --git a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/terminal.tsx b/apps/mobile/src/screens/threads/[environmentId]/[threadId]/terminal.tsx deleted file mode 100644 index e209a180a42..00000000000 --- a/apps/mobile/src/screens/threads/[environmentId]/[threadId]/terminal.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { ThreadTerminalRouteScreen } from "../../../../features/terminal/ThreadTerminalRouteScreen"; - -export default function ThreadTerminalRoute() { - return ; -} diff --git a/apps/mobile/src/state/use-thread-selection.ts b/apps/mobile/src/state/use-thread-selection.ts index c5eaf5743dd..bc077f4c93a 100644 --- a/apps/mobile/src/state/use-thread-selection.ts +++ b/apps/mobile/src/state/use-thread-selection.ts @@ -1,4 +1,4 @@ -import { useGlobalSearchParams } from "../navigation/router"; +import { useCurrentRouteParams } from "../navigation/native-stack-header"; import { createContext, createElement, use, useMemo, useRef, type ReactNode } from "react"; import { EnvironmentId, @@ -63,7 +63,7 @@ function threadDetailToShell( } function useResolvedThreadSelection() { - const params = useGlobalSearchParams<{ + const params = useCurrentRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; }>(); From 10ca14a362a3a0db516e43f75f4d4f4335b4268d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 17:32:34 -0700 Subject: [PATCH 70/71] Tighten mobile navigation targets --- .../agent-awareness/notificationNavigation.ts | 6 +- .../features/files/ThreadFilesRouteScreen.tsx | 26 +-- .../HardwareKeyboardCommandProvider.tsx | 25 +-- .../layout/AdaptiveWorkspaceLayout.tsx | 31 ++-- .../features/projects/AddProjectScreen.tsx | 81 ++++----- .../review/ReviewCommentComposerSheet.tsx | 6 +- .../useReviewCommentSelectionController.ts | 10 +- .../terminal/ThreadTerminalRouteScreen.tsx | 16 +- .../features/threads/NewTaskDraftScreen.tsx | 10 +- .../src/features/threads/ThreadFeed.tsx | 6 +- .../features/threads/ThreadGitControls.tsx | 50 +++--- .../threads/ThreadNavigationSidebar.tsx | 7 +- .../features/threads/ThreadRouteScreen.tsx | 47 ++--- .../features/threads/git/GitBranchesSheet.tsx | 8 +- .../features/threads/git/GitCommitSheet.tsx | 6 +- .../features/threads/git/GitConfirmSheet.tsx | 17 +- .../features/threads/git/GitOverviewSheet.tsx | 53 +++--- apps/mobile/src/lib/routes.test.ts | 67 ++++++- apps/mobile/src/lib/routes.ts | 165 +++++++++++++++++- apps/mobile/src/navigation/app-navigation.tsx | 13 +- .../src/navigation/native-stack-header.tsx | 8 +- apps/mobile/src/navigation/route-model.ts | 57 +----- apps/mobile/src/screens/+not-found.tsx | 3 +- apps/mobile/src/screens/connections/index.tsx | 5 +- apps/mobile/src/screens/connections/new.tsx | 6 +- apps/mobile/src/screens/index.tsx | 26 +-- apps/mobile/src/screens/new/index.tsx | 28 +-- apps/mobile/src/screens/settings/auth.tsx | 3 +- .../src/screens/settings/environments.tsx | 5 +- apps/mobile/src/screens/settings/index.tsx | 30 ++-- apps/mobile/src/screens/settings/waitlist.tsx | 11 +- 31 files changed, 523 insertions(+), 309 deletions(-) diff --git a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts index 2662566e281..15ff1e09a83 100644 --- a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts +++ b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts @@ -6,7 +6,7 @@ import { routeAgentNotificationResponseOnce } from "./notificationPayload"; import { consumeLastAgentNotificationResponse } from "./notificationResponseConsumer"; export function useAgentNotificationNavigation(): void { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const handledResponseIds = useRef(new Set()); useEffect(() => { @@ -14,7 +14,7 @@ export function useAgentNotificationNavigation(): void { routeAgentNotificationResponseOnce({ handledResponseIds: handledResponseIds.current, response, - navigate: (deepLink) => router.push(deepLink as never), + navigate: (deepLink) => navigation.push(deepLink as never), }); }; @@ -28,5 +28,5 @@ export function useAgentNotificationNavigation(): void { return () => { subscription.remove(); }; - }, [router]); + }, [navigation]); } diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index b6bf434b6ba..8a1aa1614d1 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -30,7 +30,7 @@ import { cn } from "../../lib/cn"; import { resolveFileSelectionNavigationAction } from "../../lib/adaptive-navigation"; import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; -import { buildThreadFilesNavigation, buildThreadRoutePath } from "../../lib/routes"; +import { buildThreadFilesNavigation, threadNavigation } from "../../lib/routes"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadSelection } from "../../state/use-thread-selection"; @@ -468,7 +468,7 @@ function FilesToolbarBottomFade() { export function ThreadFilesTreeScreen() { useAdaptiveWorkspacePaneRole("inspector"); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const { fileInspector, layout, panes, showAuxiliaryPane, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const [searchQuery, setSearchQuery] = useState(""); @@ -486,14 +486,14 @@ export function ThreadFilesTreeScreen() { ); const entriesData = entriesQuery.data as ProjectListEntriesResult | null; const handleReturnToThread = useCallback(() => { - if (router.canGoBack()) { - router.back(); + if (navigation.canGoBack()) { + navigation.back(); return; } if (environmentId !== null && threadId !== null) { - router.replace(buildThreadRoutePath({ environmentId, threadId })); + navigation.replace(threadNavigation({ environmentId, threadId })); } - }, [environmentId, router, threadId]); + }, [environmentId, navigation, threadId]); const handleSelectFile = useCallback( (path: string) => { @@ -505,12 +505,12 @@ export function ThreadFilesTreeScreen() { hasPersistentFileInspector: fileInspector.supported, }); if (navigationAction === "replace") { - router.replace(destination); + navigation.replace(destination); return; } - router.push(destination); + navigation.push(destination); }, - [environmentId, fileInspector.supported, router, threadId], + [environmentId, fileInspector.supported, navigation, threadId], ); const renderInspector = useCallback( (headerInset: number) => @@ -661,7 +661,7 @@ export function ThreadFilesTreeScreen() { export function ThreadFileScreen() { useAdaptiveWorkspacePaneRole("inspector"); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); const iconColor = useThemeColor("--color-icon"); const params = useRouteParams<{ @@ -714,12 +714,12 @@ export function ThreadFileScreen() { // We are already on the catch-all file route. Updating its params keeps // the current native screen mounted while replacing the selected file in // place, avoiding an RNSScreen snapshot/unmount for every tree click. - router.setParams({ + navigation.setParams({ line: undefined, path: path.split("/").filter(Boolean), }); }, - [router], + [navigation], ); const renderInspector = useCallback( (headerInset: number) => @@ -774,7 +774,7 @@ export function ThreadFileScreen() { accessibilityLabel="Return to chat" icon="chevron.left" onPress={() => { - router.replace(buildThreadRoutePath({ environmentId, threadId })); + navigation.replace(threadNavigation({ environmentId, threadId })); }} /> ) : null} diff --git a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx index ddf79b8cc2b..132a14cd873 100644 --- a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx +++ b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx @@ -2,10 +2,11 @@ import { useCurrentPathname, useAppNavigation } from "../../navigation/native-st import { useCallback, useMemo, useSyncExternalStore, type PropsWithChildren } from "react"; import { - buildThreadFilesRoutePath, - buildThreadReviewRoutePath, - buildThreadTerminalRoutePath, + buildThreadFilesNavigation, + buildThreadReviewNavigation, + buildThreadTerminalNavigation, dismissRoute, + newTaskNavigation, } from "../../lib/routes"; import { T3KeyboardCommands } from "../../native/T3KeyboardCommands"; import { @@ -19,7 +20,7 @@ import { export function HardwareKeyboardCommandProvider({ children }: PropsWithChildren) { const pathname = useCurrentPathname(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const registrationVersion = useSyncExternalStore( subscribeToHardwareKeyboardCommandRegistrations, getHardwareKeyboardCommandRegistrationVersion, @@ -28,41 +29,41 @@ export function HardwareKeyboardCommandProvider({ children }: PropsWithChildren) const enabledCommands = useMemo(() => { const commands = new Set(getRegisteredHardwareKeyboardCommands()); commands.add("newTask"); - if (pathname !== "/" || router.canGoBack()) commands.add("back"); + if (pathname !== "/" || navigation.canGoBack()) commands.add("back"); if (parseActiveThreadPath(pathname)) { commands.add("files"); commands.add("terminal"); commands.add("review"); } return [...commands]; - }, [pathname, registrationVersion, router]); + }, [pathname, registrationVersion, navigation]); const onCommand = useCallback( (command: HardwareKeyboardCommand) => { if (dispatchHardwareKeyboardCommand(command)) return; if (command === "newTask") { - router.push("/new"); + navigation.push(newTaskNavigation()); return; } if (command === "back") { - dismissRoute(router); + dismissRoute(navigation); return; } const thread = parseActiveThreadPath(pathname); if (!thread) return; if (command === "files" && !/\/files(?:\/|$)/.test(pathname)) { - router.push(buildThreadFilesRoutePath(thread)); + navigation.push(buildThreadFilesNavigation(thread)); } if (command === "terminal" && !/\/terminal(?:\/|$)/.test(pathname)) { - router.push(buildThreadTerminalRoutePath(thread)); + navigation.push(buildThreadTerminalNavigation(thread)); } if (command === "review" && !/\/review(?:\/|$)/.test(pathname)) { - router.push(buildThreadReviewRoutePath(thread)); + navigation.push(buildThreadReviewNavigation(thread)); } }, - [pathname, router], + [pathname, navigation], ); return ( diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index a575926d7d8..6c2ae88b683 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -35,7 +35,12 @@ import { type WorkspacePaneLayout, } from "../../lib/layout"; import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation"; -import { buildThreadFilesNavigation, buildThreadRoutePath } from "../../lib/routes"; +import { + buildThreadFilesNavigation, + newTaskNavigation, + settingsNavigation, + threadNavigation, +} from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { parseActiveThreadPath, @@ -100,7 +105,7 @@ export function useAdaptiveWorkspacePaneRole(role: WorkspaceAuxiliaryPaneRole) { export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) { const { width, height } = useWindowDimensions(); const pathname = useCurrentPathname(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const activeRoleOwner = useRef(null); const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); @@ -222,9 +227,9 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) if (/\/files(?:\/|$)/.test(pathname)) { return true; } - router.replace(buildThreadFilesNavigation(activeThread)); + navigation.replace(buildThreadFilesNavigation(activeThread)); return true; - }, [fileInspector.supported, layout.usesSplitView, pathname, router, showAuxiliaryPane]); + }, [fileInspector.supported, layout.usesSplitView, pathname, navigation, showAuxiliaryPane]); useHardwareKeyboardCommand("files", handleOpenFilesCommand); const toggleAuxiliaryPane = useCallback(() => { if (auxiliaryPaneRole === "inspector") { @@ -271,11 +276,11 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) ); const handleOpenSettings = useCallback(() => { - router.push("/settings"); - }, [router]); + navigation.push(settingsNavigation()); + }, [navigation]); const handleStartNewTask = useCallback(() => { - router.push("/new"); - }, [router]); + navigation.push(newTaskNavigation()); + }, [navigation]); const renderedSidebarWidth = useSharedValue( panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0, @@ -295,7 +300,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { - const destination = buildThreadRoutePath(thread); + const destination = threadNavigation(thread); const navigationAction = resolveThreadSelectionNavigationAction({ usesSplitView: layout.usesSplitView, pathname, @@ -306,7 +311,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) return; } setFileInspectorPreferredVisible(false); - router.setParams({ + navigation.setParams({ environmentId: String(thread.environmentId), threadId: String(thread.id), }); @@ -314,12 +319,12 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) } if (navigationAction === "replace") { setFileInspectorPreferredVisible(false); - router.replace(destination); + navigation.replace(destination); return; } - router.push(destination); + navigation.push(destination); }, - [layout.usesSplitView, pathname, router, selectedThreadKey], + [layout.usesSplitView, pathname, navigation, selectedThreadKey], ); return ( diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index a1aaf7df938..4b70aa9bbdf 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -40,6 +40,13 @@ import { sourceControlEnvironment } from "../../state/sourceControl"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; import { ErrorBanner } from "../../components/ErrorBanner"; import { SourceControlIcon } from "../../components/SourceControlIcon"; +import { + addProjectDestinationNavigation, + addProjectLocalNavigation, + addProjectRepositoryNavigation, + connectionsNewNavigation, + newTaskDraftNavigation, +} from "../../lib/routes"; import { useThemeColor } from "../../lib/useThemeColor"; import { uuidv4 } from "../../lib/uuid"; import { useAtomCommand } from "../../state/use-atom-command"; @@ -250,7 +257,7 @@ function useSelectedEnvironment(): { readonly selectedEnvironment: EnvironmentOption | null; readonly setSelectedEnvironmentId: (environmentId: EnvironmentId) => void; } { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const params = useRouteParams<{ environmentId?: string }>(); const environmentOptions = useEnvironmentOptions(); const requestedEnvironmentId = stringParam(params.environmentId) as EnvironmentId | null; @@ -264,12 +271,12 @@ function useSelectedEnvironment(): { return { environmentOptions, selectedEnvironment, - setSelectedEnvironmentId: (environmentId) => router.setParams({ environmentId }), + setSelectedEnvironmentId: (environmentId) => navigation.setParams({ environmentId }), }; } function EmptyEnvironmentState() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); return ( @@ -278,7 +285,7 @@ function EmptyEnvironmentState() { Add an environment before adding a project. router.replace("/connections/new")} + onPress={() => navigation.replace(connectionsNewNavigation())} className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70" > Add environment @@ -294,7 +301,7 @@ function SourceControlRow(props: { readonly hint: string; readonly isFirst: boolean; }) { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const iconColor = useThemeColor("--color-icon"); const title = props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`; @@ -322,20 +329,19 @@ function SourceControlRow(props: { icon={icon} isFirst={props.isFirst} onPress={() => - router.push({ - name: "AddProjectRepository", - params: { + navigation.push( + addProjectRepositoryNavigation({ environmentId: props.selectedEnvironmentId, source: props.source, - }, - }) + }), + ) } /> ); } export function AddProjectSourceScreen() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const accentColor = useThemeColor("--color-icon-muted"); const iconColor = useThemeColor("--color-icon"); const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } = @@ -409,10 +415,11 @@ export function AddProjectSourceScreen() { } isFirst onPress={() => - router.push({ - name: "AddProjectLocal", - params: { environmentId: selectedEnvironment.environmentId }, - }) + navigation.push( + addProjectLocalNavigation({ + environmentId: selectedEnvironment.environmentId, + }), + ) } /> {(["url", ...sortAddProjectProviderSources(readiness)] as AddProjectRemoteSource[]).map( @@ -440,7 +447,7 @@ export function AddProjectSourceScreen() { } function useCreateProject(environment: EnvironmentOption | null) { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); const projects = useProjects(); @@ -455,14 +462,13 @@ function useCreateProject(environment: EnvironmentOption | null) { }); if (existing) { Alert.alert("Project already exists", existing.title); - router.replace({ - name: "NewTaskDraft", - params: { + navigation.replace( + newTaskDraftNavigation({ environmentId: existing.environmentId, projectId: existing.id, title: existing.title, - }, - }); + }), + ); return; } @@ -480,17 +486,16 @@ function useCreateProject(environment: EnvironmentOption | null) { if (AsyncResult.isFailure(result)) { return result; } - router.replace({ - name: "NewTaskDraft", - params: { + navigation.replace( + newTaskDraftNavigation({ environmentId: environment.environmentId, projectId, title: inferProjectTitleFromPath(workspaceRoot), - }, - }); + }), + ); return result; }, - [createProject, environment, projects, router], + [createProject, environment, projects, navigation], ); } @@ -509,7 +514,7 @@ export function AddProjectRepositoryScreen() { const lookupRepositoryQuery = useAtomQueryRunner(sourceControlEnvironment.repository, { reportFailure: false, }); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const params = useRouteParams<{ environmentId?: string; source?: string }>(); const environment = useEnvironmentFromParam(); const source = sourceFromParam(params.source); @@ -524,15 +529,14 @@ export function AddProjectRepositoryScreen() { const provider = addProjectRemoteSourceProvider(source); if (!provider) { const remoteUrl = repositoryInput.trim(); - router.push({ - name: "AddProjectDestination", - params: { + navigation.push( + addProjectDestinationNavigation({ environmentId: environment.environmentId, source, remoteUrl, repositoryTitle: remoteUrl, - }, - }); + }), + ); setIsSubmitting(false); return; } @@ -548,18 +552,17 @@ export function AddProjectRepositoryScreen() { setError(errorMessage(Cause.squash(result.cause))); } else { const repository = result.value; - router.push({ - name: "AddProjectDestination", - params: { + navigation.push( + addProjectDestinationNavigation({ environmentId: environment.environmentId, source, remoteUrl: repository.sshUrl, repositoryTitle: repository.nameWithOwner, - }, - }); + }), + ); } setIsSubmitting(false); - }, [environment, isSubmitting, lookupRepositoryQuery, repositoryInput, router, source]); + }, [environment, isSubmitting, lookupRepositoryQuery, repositoryInput, navigation, source]); return ( diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index 3503a150214..3383b25c697 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -41,7 +41,7 @@ import { const REVIEW_COMMENT_PREVIEW_MAX_LINES = 5; export function ReviewCommentComposerSheet() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); @@ -84,8 +84,8 @@ export function ReviewCommentComposerSheet() { const previewViewportWidth = Math.max(width - 40, 280); const dismissComposer = useCallback(() => { clearReviewCommentTarget(); - router.dismiss(); - }, [router]); + navigation.dismiss(); + }, [navigation]); const handleNativePaste = useNativePaste((uris) => { void (async () => { try { diff --git a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts index 557ce16b411..e7d9e790de9 100644 --- a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts +++ b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts @@ -6,6 +6,7 @@ import { pipe } from "effect/Function"; import * as Result from "effect/Result"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { buildThreadReviewCommentNavigation } from "../../lib/routes"; import { buildReviewCommentTarget, @@ -34,7 +35,7 @@ export function useReviewCommentSelectionController(input: { readonly nativeReviewDiffData: NativeReviewDiffData; }) { const { environmentId, nativeReviewDiffData, selectedSection, threadId } = input; - const { push } = useAppNavigation(); + const navigation = useAppNavigation(); const activeCommentTarget = useReviewCommentTarget(); const [pendingNativeCommentSelection, setPendingNativeCommentSelection] = useState(null); @@ -44,11 +45,8 @@ export function useReviewCommentSelectionController(input: { return; } - push({ - name: "ThreadReviewComment", - params: { environmentId, threadId }, - }); - }, [environmentId, push, threadId]); + navigation.push(buildThreadReviewCommentNavigation({ environmentId, threadId })); + }, [environmentId, navigation, threadId]); const selectedRowIds = useMemo(() => { if ( diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 6f62d85c016..033270b4cc4 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -203,7 +203,7 @@ function pickRunningTerminalSessionForBootstrap( } export function ThreadTerminalRouteScreen() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); const clearTerminal = useAtomCommand(terminalEnvironment.clear, "terminal clear"); @@ -613,8 +613,10 @@ export function ThreadTerminalRouteScreen() { if (!shouldRedirectToRunningTerminal || !selectedThread || !runningSession) { return; } - router.replace(buildThreadTerminalNavigation(selectedThread, runningSession.target.terminalId)); - }, [router, runningSession, selectedThread, shouldRedirectToRunningTerminal]); + navigation.replace( + buildThreadTerminalNavigation(selectedThread, runningSession.target.terminalId), + ); + }, [navigation, runningSession, selectedThread, shouldRedirectToRunningTerminal]); useEffect(() => { const initialInput = pendingLaunch?.initialInput; @@ -839,9 +841,9 @@ export function ThreadTerminalRouteScreen() { return; } - router.replace(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); + navigation.replace(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); }, - [router, selectedThread, terminalId], + [navigation, selectedThread, terminalId], ); const handleOpenNewTerminal = useCallback(() => { @@ -849,7 +851,7 @@ export function ThreadTerminalRouteScreen() { return; } - router.replace( + navigation.replace( buildThreadTerminalNavigation( selectedThread, nextOpenTerminalId({ @@ -858,7 +860,7 @@ export function ThreadTerminalRouteScreen() { }), ), ); - }, [router, selectedThread, terminalId, terminalMenuSessions]); + }, [navigation, selectedThread, terminalId, terminalMenuSessions]); const adjustFontSize = useCallback((delta: number) => { setTimeout(() => { diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 53cb3f4cefa..55faf2228f7 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -29,7 +29,7 @@ import { providerOptionsConfigurationLabel, resolveProviderOptionDescriptors, } from "../../lib/providerOptions"; -import { buildThreadRoutePath } from "../../lib/routes"; +import { newTaskNavigation, threadNavigation } from "../../lib/routes"; import { scopedProjectKey } from "../../lib/scopedEntities"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { getComposerDraftSnapshot } from "../../state/use-composer-drafts"; @@ -58,7 +58,7 @@ export function NewTaskDraftScreen(props: { const projects = useProjects(); const createProjectThread = useCreateProjectThread(); const flow = useNewTaskFlow(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const isKeyboardVisible = useKeyboardState((state) => state.isVisible); @@ -101,13 +101,13 @@ export function NewTaskDraftScreen(props: { return; } - router.replace("/new"); + navigation.replace(newTaskNavigation()); }, [ logicalProjects, projects, props.initialProjectRef?.environmentId, props.initialProjectRef?.projectId, - router, + navigation, selectedProject, setProject, ]); @@ -429,7 +429,7 @@ export function NewTaskDraftScreen(props: { flow.setPrompt(""); flow.clearAttachments(); - router.replace(buildThreadRoutePath(result.value)); + navigation.replace(threadNavigation(result.value)); } if (!selectedProject) { diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 3b52ae35596..fbc6c25de5f 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1125,7 +1125,7 @@ function ThreadFeedPlaceholder(props: { } export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const copyFeedbackTimeoutRef = useRef | null>(null); const foldSettleFrameRef = useRef(null); const foldSettleSecondFrameRef = useRef(null); @@ -1180,7 +1180,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ); if (relativePath) { void Haptics.selectionAsync(); - router.push( + navigation.push( buildThreadFilesNavigation( { environmentId: props.environmentId, threadId: props.threadId }, relativePath, @@ -1195,7 +1195,7 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { void Linking.openURL(presentation.href); } }, - [props.environmentId, props.threadId, props.workspaceRoot, router], + [props.environmentId, props.threadId, props.workspaceRoot, navigation], ); const markdownStyles = useMarkdownStyles(onMarkdownLinkPress); const reviewCommentColors = useReviewCommentColors(); diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index ee34a4f3d99..c28d55d914c 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -14,7 +14,12 @@ import { useRouteParams, useAppNavigation } from "../../navigation/native-stack- import { NativeHeaderToolbar } from "../../navigation/native-stack-header"; import { useCallback, useMemo } from "react"; import { Alert } from "react-native"; -import { buildThreadFilesNavigation, buildThreadReviewRoutePath } from "../../lib/routes"; +import { + buildGitConfirmNavigation, + buildGitOverviewNavigation, + buildThreadFilesNavigation, + buildThreadReviewNavigation, +} from "../../lib/routes"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { basename, @@ -102,7 +107,7 @@ type ThreadGitControlsProps = { }; function useThreadGitControlModel(props: ThreadGitControlsProps) { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const { environmentId, threadId } = useRouteParams<{ environmentId: EnvironmentId; threadId: ThreadId; @@ -170,24 +175,24 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { !input.featureBranch && requiresDefaultBranchConfirmation(input.action, isDefaultRef) ) { - router.push({ - name: "GitConfirm", - params: { - environmentId, - threadId, - confirmAction: confirmableAction, - branchName, - includesCommit: String( - input.action === "commit_push" || input.action === "commit_push_pr", - ), - }, - }); + navigation.push( + buildGitConfirmNavigation( + { environmentId, threadId }, + { + confirmAction: confirmableAction, + branchName, + includesCommit: String( + input.action === "commit_push" || input.action === "commit_push_pr", + ), + }, + ), + ); return; } await onRunAction(input); }, - [environmentId, gitStatus, isDefaultRef, onRunAction, router, threadId], + [environmentId, gitStatus, isDefaultRef, onRunAction, navigation, threadId], ); const runQuickAction = useCallback(async () => { @@ -209,23 +214,20 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { props.onOpenFilesInspector(); return; } - router.push(buildThreadFilesNavigation({ environmentId, threadId })); - }, [environmentId, props.onOpenFilesInspector, router, threadId]); + navigation.push(buildThreadFilesNavigation({ environmentId, threadId })); + }, [environmentId, props.onOpenFilesInspector, navigation, threadId]); const openReview = useCallback(() => { - router.push(buildThreadReviewRoutePath({ environmentId, threadId })); - }, [environmentId, router, threadId]); + navigation.push(buildThreadReviewNavigation({ environmentId, threadId })); + }, [environmentId, navigation, threadId]); const openGitInspector = useCallback(() => { if (props.onOpenGitInspector) { props.onOpenGitInspector(); return; } - router.push({ - name: "GitOverview", - params: { environmentId, threadId }, - }); - }, [environmentId, props.onOpenGitInspector, router, threadId]); + navigation.push(buildGitOverviewNavigation({ environmentId, threadId })); + }, [environmentId, props.onOpenGitInspector, navigation, threadId]); return { currentBranchLabel, diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 893beb57b49..1cf8d448dfe 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -15,6 +15,7 @@ import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; +import { settingsEnvironmentsNavigation } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -257,7 +258,7 @@ export function ThreadNavigationSidebar(props: { }) { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; - const router = useAppNavigation(); + const navigation = useAppNavigation(); const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); @@ -693,7 +694,7 @@ export function ThreadNavigationSidebar(props: { showsConnectionStatus ? ( router.push("/settings/environments")} + onPress={() => navigation.push(settingsEnvironmentsNavigation())} state={catalogState} variant="sidebar" /> @@ -870,7 +871,7 @@ export function ThreadNavigationSidebar(props: { {showsConnectionStatus ? ( router.push("/settings/environments")} + onPress={() => navigation.push(settingsEnvironmentsNavigation())} state={catalogState} variant="sidebar" /> diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 559874f9560..2d0c37a71f3 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -42,8 +42,11 @@ import { EmptyState } from "../../components/EmptyState"; import { LoadingScreen } from "../../components/LoadingScreen"; import { buildThreadFilesNavigation, - buildThreadRoutePath, buildThreadTerminalNavigation, + connectionsNavigation, + homeNavigation, + newTaskNavigation, + threadNavigation, } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; @@ -273,7 +276,7 @@ function ThreadRouteContent( const gitActions = useSelectedThreadGitActions(); const requests = useSelectedThreadRequests(); const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, "thread interrupt"); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const params = useRouteParams<{ environmentId?: string | string[]; threadId?: string | string[]; @@ -476,14 +479,14 @@ function ThreadRouteContent( if (selectedThread === null) { return; } - const navigation = buildThreadFilesNavigation(selectedThread, path); + const destination = buildThreadFilesNavigation(selectedThread, path); if (fileInspector.supported) { - router.replace(navigation); + navigation.replace(destination); return; } - router.push(navigation); + navigation.push(destination); }, - [fileInspector.supported, router, selectedThread], + [fileInspector.supported, navigation, selectedThread], ); const GitInspector = useCallback( () => , @@ -519,8 +522,8 @@ function ThreadRouteContent( const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; const handleOpenConnectionEditor = useCallback(() => { - void router.push("/connections"); - }, [router]); + void navigation.push(connectionsNavigation()); + }, [navigation]); const handleStopThread = useCallback(() => { if ( !selectedThread || @@ -552,9 +555,9 @@ function ThreadRouteContent( return; } - void router.push(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); + void navigation.push(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); }, - [router, selectedThread, selectedThreadProject?.workspaceRoot], + [navigation, selectedThread, selectedThreadProject?.workspaceRoot], ); const handleOpenNewTerminal = useCallback(() => { @@ -571,8 +574,8 @@ function ThreadRouteContent( const nextId = nextOpenTerminalId({ listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), }); - void router.push(buildThreadTerminalNavigation(selectedThread, nextId)); - }, [router, selectedThread, selectedThreadProject?.workspaceRoot, terminalMenuSessions]); + void navigation.push(buildThreadTerminalNavigation(selectedThread, nextId)); + }, [navigation, selectedThread, selectedThreadProject?.workspaceRoot, terminalMenuSessions]); const handleRunProjectScript = useCallback( async (script: ProjectScript) => { @@ -629,10 +632,10 @@ function ThreadRouteContent( worktreePath: preferredWorktreePath, }); - void router.push(buildThreadTerminalNavigation(selectedThread, targetTerminalId)); + void navigation.push(buildThreadTerminalNavigation(selectedThread, targetTerminalId)); }, [ - router, + navigation, selectedThread, selectedThreadDetailWorktreePath, selectedThreadProject, @@ -673,16 +676,16 @@ function ThreadRouteContent( icon: { name: "chevron.left", type: "sfSymbol" as const }, identifier: "thread-compact-back", onPress: () => { - if (router.canGoBack()) { - router.back(); + if (navigation.canGoBack()) { + navigation.back(); return; } - router.replace("/"); + navigation.replace(homeNavigation()); }, type: "button" as const, }), ], - [router], + [navigation], ); const splitLeftHeaderItems = useMemo( () => [ @@ -719,11 +722,11 @@ function ThreadRouteContent( accessibilityLabel: "New task", icon: { name: "square.and.pencil", type: "sfSymbol" as const }, identifier: "thread-left-new-task", - onPress: () => router.push("/new"), + onPress: () => navigation.push(newTaskNavigation()), type: "button" as const, }), ], - [panes.primarySidebarVisible, props.onReturnToThread, router, togglePrimarySidebar], + [panes.primarySidebarVisible, props.onReturnToThread, navigation, togglePrimarySidebar], ); if (!environmentId || !threadId) { @@ -799,9 +802,9 @@ function ThreadRouteContent( selectedThreadKey={selectedThreadKey} onClose={() => setDrawerVisible(false)} onSelectThread={(thread) => { - router.replace(buildThreadRoutePath(thread)); + navigation.replace(threadNavigation(thread)); }} - onStartNewTask={() => router.push("/new")} + onStartNewTask={() => navigation.push(newTaskNavigation())} /> )} diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx index aed634b8d4a..ec5e2283ccf 100644 --- a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx @@ -15,7 +15,7 @@ import { vcsEnvironment } from "../../../state/vcs"; import { SheetActionButton } from "./gitSheetComponents"; export function GitBranchesSheet() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const { selectedThread } = useThreadSelection(); const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); @@ -96,7 +96,7 @@ export function GitBranchesSheet() { if (branch.length === 0) return; void gitActions.onCreateSelectedThreadBranch(branch).then(() => { setNewBranchName(""); - router.dismiss(); + navigation.dismiss(); }); }} /> @@ -146,7 +146,7 @@ export function GitBranchesSheet() { if (baseBranch.length === 0 || newBranch.length === 0) return; void gitActions.onCreateSelectedThreadWorktree({ baseBranch, newBranch }).then(() => { setWorktreeBranchName(""); - router.dismiss(); + navigation.dismiss(); }); }} /> @@ -188,7 +188,7 @@ export function GitBranchesSheet() { }} onPress={() => { void gitActions.onCheckoutSelectedThreadBranch(branch.name).then(() => { - router.dismiss(); + navigation.dismiss(); }); }} > diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index db0784ad4f2..7a093a565fc 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -14,7 +14,7 @@ import { vcsEnvironment } from "../../../state/vcs"; import { SheetActionButton } from "./gitSheetComponents"; export function GitCommitSheet() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const isDarkMode = useColorScheme() === "dark"; const { selectedThread } = useThreadSelection(); @@ -55,7 +55,7 @@ export function GitCommitSheet() { const runCommitAction = useCallback( async (featureBranch: boolean) => { const commitMessage = dialogCommitMessage.trim(); - router.dismiss(); + navigation.dismiss(); await gitActions.onRunSelectedThreadGitAction({ action: "commit", featureBranch, @@ -63,7 +63,7 @@ export function GitCommitSheet() { ...(!allSelected ? { filePaths: selectedFiles.map((file) => file.path) } : {}), }); }, - [allSelected, dialogCommitMessage, gitActions, router, selectedFiles], + [allSelected, dialogCommitMessage, gitActions, navigation, selectedFiles], ); return ( diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index e240d8c0602..df2a7514fdf 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -13,7 +13,7 @@ import { useSelectedThreadGitState } from "../../../state/use-selected-thread-gi import { SheetActionButton } from "./gitSheetComponents"; export function GitConfirmSheet() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); @@ -49,17 +49,17 @@ export function GitConfirmSheet() { const continuePendingAction = useCallback(async () => { if (!confirmAction) return; - router.dismissAll(); + navigation.dismissAll(); await gitActions.onRunSelectedThreadGitAction({ action: confirmAction, ...(params.commitMessage ? { commitMessage: params.commitMessage } : {}), ...(params.filePaths ? { filePaths: params.filePaths.split(",") } : {}), }); - }, [confirmAction, gitActions, params, router]); + }, [confirmAction, gitActions, params, navigation]); const movePendingActionToFeatureBranch = useCallback(async () => { if (!confirmAction) return; - router.dismissAll(); + navigation.dismissAll(); if (includesCommit) { await gitActions.onRunSelectedThreadGitAction({ @@ -82,7 +82,14 @@ export function GitConfirmSheet() { ); await gitActions.onCreateSelectedThreadBranch(newBranchName); await gitActions.onRunSelectedThreadGitAction({ action: confirmAction }); - }, [confirmAction, gitActions, gitState.selectedThreadBranches, includesCommit, params, router]); + }, [ + confirmAction, + gitActions, + gitState.selectedThreadBranches, + includesCommit, + params, + navigation, + ]); return ( diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 20e04b54df1..5e0c1c4a3fe 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -16,7 +16,12 @@ import { useThemeColor } from "../../../lib/useThemeColor"; import { AppText as Text } from "../../../components/AppText"; import { nativeHeaderScrollEdgeEffects } from "../../../lib/native-scroll-edge-effect"; import { tryOpenExternalUrl } from "../../../lib/openExternalUrl"; -import { buildThreadReviewRoutePath } from "../../../lib/routes"; +import { + buildGitBranchesNavigation, + buildGitCommitNavigation, + buildGitConfirmNavigation, + buildThreadReviewNavigation, +} from "../../../lib/routes"; import { useEnvironmentQuery } from "../../../state/query"; import { useThreadSelection } from "../../../state/use-thread-selection"; import { useSelectedThreadGitActions } from "../../../state/use-selected-thread-git-actions"; @@ -33,7 +38,7 @@ export function GitOverviewSheet( readonly presentation?: "sheet" | "inspector"; } = {}, ) { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const presentation = props.presentation ?? "sheet"; const isInspector = presentation === "inspector"; @@ -119,27 +124,27 @@ export function GitOverviewSheet( !input.featureBranch && requiresDefaultBranchConfirmation(input.action, isDefaultRef) ) { - router.push({ - name: "GitConfirm", - params: { - environmentId, - threadId, - confirmAction: confirmableAction, - branchName, - includesCommit: String( - input.action === "commit_push" || input.action === "commit_push_pr", - ), - }, - }); + navigation.push( + buildGitConfirmNavigation( + { environmentId, threadId }, + { + confirmAction: confirmableAction, + branchName, + includesCommit: String( + input.action === "commit_push" || input.action === "commit_push_pr", + ), + }, + ), + ); return; } if (!isInspector) { - router.dismiss(); + navigation.dismiss(); } await gitActions.onRunSelectedThreadGitAction(input); }, - [environmentId, gitActions, gitStatus.data, isDefaultRef, isInspector, router, threadId], + [environmentId, gitActions, gitStatus.data, isDefaultRef, isInspector, navigation, threadId], ); const onPressMenuItem = useCallback( @@ -150,10 +155,7 @@ export function GitOverviewSheet( return; } if (item.dialogAction === "commit") { - router.push({ - name: "GitCommit", - params: { environmentId, threadId }, - }); + navigation.push(buildGitCommitNavigation({ environmentId, threadId })); return; } if (item.dialogAction === "push") { @@ -164,7 +166,7 @@ export function GitOverviewSheet( await runActionWithPrompt({ action: "create_pr" }); } }, - [environmentId, openExistingPr, router, runActionWithPrompt, threadId], + [environmentId, openExistingPr, navigation, runActionWithPrompt, threadId], ); const inspectorHeaderRightBarButtonItems = useMemo( @@ -238,7 +240,7 @@ export function GitOverviewSheet( title="Review changes" subtitle="Inspect turn diffs, worktree changes, and base branch diff" disabled={busy || !isRepo} - onPress={() => router.push(buildThreadReviewRoutePath({ environmentId, threadId }))} + onPress={() => navigation.push(buildThreadReviewNavigation({ environmentId, threadId }))} /> - router.push({ - name: "GitBranches", - params: { environmentId, threadId }, - }) - } + onPress={() => navigation.push(buildGitBranchesNavigation({ environmentId, threadId }))} /> diff --git a/apps/mobile/src/lib/routes.test.ts b/apps/mobile/src/lib/routes.test.ts index dffe212457a..aad266bab07 100644 --- a/apps/mobile/src/lib/routes.test.ts +++ b/apps/mobile/src/lib/routes.test.ts @@ -1,7 +1,14 @@ import { describe, expect, it } from "vite-plus/test"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { buildThreadFilesNavigation, buildThreadFilesRoutePath } from "./routes"; +import { + buildGitConfirmNavigation, + buildThreadFilesNavigation, + buildThreadFilesRoutePath, + buildThreadReviewCommentNavigation, + newTaskDraftNavigation, + threadNavigation, +} from "./routes"; const thread = { environmentId: EnvironmentId.make("environment-1"), @@ -43,3 +50,61 @@ describe("thread file routes", () => { }); }); }); + +describe("named navigation targets", () => { + it("builds thread params without string route templates", () => { + expect(threadNavigation(thread)).toEqual({ + name: "Thread", + params: { + environmentId: "environment-1", + threadId: "thread-1", + }, + }); + }); + + it("builds review comment params without string route templates", () => { + expect(buildThreadReviewCommentNavigation(thread)).toEqual({ + name: "ThreadReviewComment", + params: { + environmentId: "environment-1", + threadId: "thread-1", + }, + }); + }); + + it("builds git confirmation params with action metadata", () => { + expect( + buildGitConfirmNavigation(thread, { + branchName: "main", + confirmAction: "push", + includesCommit: "false", + }), + ).toEqual({ + name: "GitConfirm", + params: { + environmentId: "environment-1", + threadId: "thread-1", + branchName: "main", + confirmAction: "push", + includesCommit: "false", + }, + }); + }); + + it("builds new task draft params in one place", () => { + expect( + newTaskDraftNavigation({ + environmentId: "environment-1", + projectId: "project-1", + title: "Project", + }), + ).toEqual({ + name: "NewTaskDraft", + params: { + environmentId: "environment-1", + projectId: "project-1", + title: "Project", + }, + }); + }); +}); diff --git a/apps/mobile/src/lib/routes.ts b/apps/mobile/src/lib/routes.ts index 2cd9f243f12..37c358b8670 100644 --- a/apps/mobile/src/lib/routes.ts +++ b/apps/mobile/src/lib/routes.ts @@ -5,7 +5,7 @@ import type { AppNavigation } from "../navigation/app-navigation"; import type { AppNavigationTarget } from "../navigation/route-model"; import type { SelectedThreadRef } from "../state/remote-runtime-types"; -type Router = AppNavigation; +type Navigation = AppNavigation; type ThreadRouteInput = | Pick @@ -27,12 +27,157 @@ export function buildThreadRoutePath(input: ThreadRouteInput | PlainThreadRouteI return `/threads/${encodeURIComponent(environmentId)}/${encodeURIComponent(threadId)}`; } +export function homeNavigation(): AppNavigationTarget { + return { name: "Home" }; +} + +export function settingsNavigation(): AppNavigationTarget { + return { name: "Settings" }; +} + +export function settingsEnvironmentsNavigation(): AppNavigationTarget { + return { name: "SettingsEnvironments" }; +} + +export function settingsEnvironmentNewNavigation(): AppNavigationTarget { + return { name: "SettingsEnvironmentNew" }; +} + +export function settingsAuthNavigation(): AppNavigationTarget { + return { name: "SettingsAuth" }; +} + +export function settingsArchiveNavigation(): AppNavigationTarget { + return { name: "SettingsArchive" }; +} + +export function settingsWaitlistNavigation(): AppNavigationTarget { + return { name: "SettingsWaitlist" }; +} + +export function connectionsNavigation(): AppNavigationTarget { + return { name: "Connections" }; +} + +export function connectionsNewNavigation(): AppNavigationTarget { + return { name: "ConnectionsNew" }; +} + +export function newTaskNavigation(): AppNavigationTarget { + return { name: "NewTask" }; +} + +export function addProjectNavigation(): AppNavigationTarget { + return { name: "AddProject" }; +} + +export function addProjectRepositoryNavigation(params: { + readonly environmentId?: string; + readonly source?: string; +}): AppNavigationTarget { + return { name: "AddProjectRepository", params }; +} + +export function addProjectLocalNavigation(params: { + readonly environmentId?: string; +}): AppNavigationTarget { + return { name: "AddProjectLocal", params }; +} + +export function addProjectDestinationNavigation(params: { + readonly environmentId?: string; + readonly source?: string; + readonly remoteUrl?: string; + readonly repositoryTitle?: string; +}): AppNavigationTarget { + return { name: "AddProjectDestination", params }; +} + +export function newTaskDraftNavigation(params: { + readonly environmentId: string; + readonly projectId: string; + readonly title: string; +}): AppNavigationTarget { + return { name: "NewTaskDraft", params }; +} + +export function threadNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, +): AppNavigationTarget { + return { + name: "Thread", + params: threadRouteParams(input), + }; +} + export function buildThreadReviewRoutePath( input: ThreadRouteInput | PlainThreadRouteInput, ): string { return `${buildThreadRoutePath(input)}/review`; } +export function buildThreadReviewNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, +): AppNavigationTarget { + return { + name: "ThreadReview", + params: threadRouteParams(input), + }; +} + +export function buildThreadReviewCommentNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, +): AppNavigationTarget { + return { + name: "ThreadReviewComment", + params: threadRouteParams(input), + }; +} + +export function buildGitOverviewNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, +): AppNavigationTarget { + return { + name: "GitOverview", + params: threadRouteParams(input), + }; +} + +export function buildGitCommitNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, +): AppNavigationTarget { + return { + name: "GitCommit", + params: threadRouteParams(input), + }; +} + +export function buildGitBranchesNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, +): AppNavigationTarget { + return { + name: "GitBranches", + params: threadRouteParams(input), + }; +} + +export function buildGitConfirmNavigation( + input: ThreadRouteInput | PlainThreadRouteInput, + params: { + readonly confirmAction: string; + readonly branchName: string; + readonly includesCommit: string; + }, +): AppNavigationTarget { + return { + name: "GitConfirm", + params: { + ...threadRouteParams(input), + ...params, + }, + }; +} + export function buildThreadFilesRoutePath( input: ThreadRouteInput | PlainThreadRouteInput, relativePath?: string | null, @@ -120,11 +265,21 @@ export function buildThreadFilesNavigation( }; } -export function dismissRoute(router: Router) { - if (router.canGoBack()) { - router.back(); +export function dismissRoute(navigation: Navigation) { + if (navigation.canGoBack()) { + navigation.back(); return; } - router.replace("/"); + navigation.replace(homeNavigation()); +} + +function threadRouteParams(input: ThreadRouteInput | PlainThreadRouteInput): { + readonly environmentId: string; + readonly threadId: string; +} { + return { + environmentId: String(input.environmentId), + threadId: String("threadId" in input ? input.threadId : input.id), + }; } diff --git a/apps/mobile/src/navigation/app-navigation.tsx b/apps/mobile/src/navigation/app-navigation.tsx index 6bc4a7e2d57..512c9600819 100644 --- a/apps/mobile/src/navigation/app-navigation.tsx +++ b/apps/mobile/src/navigation/app-navigation.tsx @@ -12,14 +12,14 @@ import { getFocusedRoute, resolveNavigationTarget, type AppFocusedRoute, - type AppHref, + type AppNavigationInput, type AppStackParamList, type RouteParams, } from "./route-model"; export type AppNavigation = { - readonly push: (href: AppHref) => void; - readonly replace: (href: AppHref) => void; + readonly push: (target: AppNavigationInput) => void; + readonly replace: (target: AppNavigationInput) => void; readonly back: () => void; readonly dismiss: () => void; readonly dismissAll: () => void; @@ -63,8 +63,11 @@ export function useCurrentRouteParams(): T export function createAppNavigation( navigationRef: React.RefObject | null>, ): AppNavigation { - const navigateWithAction = (href: AppHref, action: "push" | "replace" | "navigate"): void => { - const target = resolveNavigationTarget(href); + const navigateWithAction = ( + input: AppNavigationInput, + action: "push" | "replace" | "navigate", + ): void => { + const target = resolveNavigationTarget(input); const navigation = navigationRef.current; if (!navigation) { return; diff --git a/apps/mobile/src/navigation/native-stack-header.tsx b/apps/mobile/src/navigation/native-stack-header.tsx index 611d6ec1c26..64ad7404bd7 100644 --- a/apps/mobile/src/navigation/native-stack-header.tsx +++ b/apps/mobile/src/navigation/native-stack-header.tsx @@ -24,11 +24,11 @@ import { import type { ColorValue } from "react-native"; import { useAppNavigation } from "./app-navigation"; -import type { AppHref, RouteParams } from "./route-model"; +import type { AppNavigationInput, RouteParams } from "./route-model"; export { useFocusEffect }; export { useAppNavigation, useCurrentPathname, useCurrentRouteParams } from "./app-navigation"; -export type { AppHref }; +export type { AppNavigationInput }; export type AppNativeStackNavigationOptions = Omit< NativeStackNavigationOptions, @@ -366,7 +366,7 @@ function NativeStackScreenTitle(_props: { NativeStackScreenOptions.Title = NativeStackScreenTitle; -export function NavigateTo(props: { readonly href: AppHref }) { +export function NavigateTo(props: { readonly href: AppNavigationInput }) { const navigation = useAppNavigation(); useEffect(() => { navigation.replace(props.href); @@ -375,7 +375,7 @@ export function NavigateTo(props: { readonly href: AppHref }) { } export function NavigationLink(props: { - readonly href: AppHref; + readonly href: AppNavigationInput; readonly asChild?: boolean; readonly children?: ReactNode; }) { diff --git a/apps/mobile/src/navigation/route-model.ts b/apps/mobile/src/navigation/route-model.ts index e11e741e203..72fbd7bba56 100644 --- a/apps/mobile/src/navigation/route-model.ts +++ b/apps/mobile/src/navigation/route-model.ts @@ -61,13 +61,7 @@ export type AppStackParamList = { NotFound: RouteParams | undefined; }; -export type AppHref = - | string - | AppNavigationTarget - | { - readonly pathname: string; - readonly params?: RouteParams; - }; +export type AppNavigationInput = string | AppNavigationTarget; export type AppNavigationTarget = { readonly name: AppRouteName; @@ -147,57 +141,16 @@ function mergeParams( return normalizeRouteParams({ ...base, ...extra }); } -export function resolveNavigationTarget(href: AppHref): AppNavigationTarget { - if (typeof href !== "string") { - if ("name" in href) { - return withParams(href.name, href.params); - } - return resolveNavigationObject(href.pathname, href.params); +export function resolveNavigationTarget(input: AppNavigationInput): AppNavigationTarget { + if (typeof input !== "string") { + return withParams(input.name, input.params); } - const { pathname, query } = splitPathAndQuery(href); + const { pathname, query } = splitPathAndQuery(input); const target = resolveNavigationPath(pathname); return { name: target.name, params: mergeParams(target.params, query) }; } -function resolveNavigationObject( - pathname: string, - params: RouteParams | undefined, -): AppNavigationTarget { - switch (pathname) { - case "/threads/[environmentId]/[threadId]/terminal": - return withParams("ThreadTerminal", params); - case "/threads/[environmentId]/[threadId]/files": - return withParams("ThreadFiles", params); - case "/threads/[environmentId]/[threadId]/files/[...path]": - return withParams("ThreadFile", params); - case "/threads/[environmentId]/[threadId]/review": - return withParams("ThreadReview", params); - case "/threads/[environmentId]/[threadId]/review-comment": - return withParams("ThreadReviewComment", params); - case "/threads/[environmentId]/[threadId]/git": - return withParams("GitOverview", params); - case "/threads/[environmentId]/[threadId]/git/commit": - return withParams("GitCommit", params); - case "/threads/[environmentId]/[threadId]/git/branches": - return withParams("GitBranches", params); - case "/threads/[environmentId]/[threadId]/git-confirm": - return withParams("GitConfirm", params); - case "/new/draft": - return withParams("NewTaskDraft", params); - case "/new/add-project/repository": - return withParams("AddProjectRepository", params); - case "/new/add-project/destination": - return withParams("AddProjectDestination", params); - case "/new/add-project/local": - return withParams("AddProjectLocal", params); - default: { - const target = resolveNavigationPath(pathname); - return { ...target, params: mergeParams(target.params, params) }; - } - } -} - export function resolveNavigationPath(pathname: string): AppNavigationTarget { const segments = pathSegments(pathname); diff --git a/apps/mobile/src/screens/+not-found.tsx b/apps/mobile/src/screens/+not-found.tsx index 3d9f502e557..7dc664b8c14 100644 --- a/apps/mobile/src/screens/+not-found.tsx +++ b/apps/mobile/src/screens/+not-found.tsx @@ -3,6 +3,7 @@ import { Pressable, ScrollView, StyleSheet } from "react-native"; import { useResolveClassNames } from "uniwind"; import { AppText as Text } from "../components/AppText"; +import { homeNavigation } from "../lib/routes"; export default function NotFoundRoute() { const screenBgStyle = StyleSheet.flatten(useResolveClassNames("bg-screen")); @@ -32,7 +33,7 @@ export default function NotFoundRoute() { Route not found - + Return home diff --git a/apps/mobile/src/screens/connections/index.tsx b/apps/mobile/src/screens/connections/index.tsx index e459a141ccd..15084bfc8af 100644 --- a/apps/mobile/src/screens/connections/index.tsx +++ b/apps/mobile/src/screens/connections/index.tsx @@ -12,6 +12,7 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text } from "../../components/AppText"; import { cn } from "../../lib/cn"; +import { connectionsNewNavigation } from "../../lib/routes"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; import { ConnectionEnvironmentRow } from "../../features/connection/ConnectionEnvironmentRow"; @@ -22,7 +23,7 @@ export default function ConnectionsRouteScreen() { onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const hasEnvironments = connectedEnvironments.length > 0; const [expandedId, setExpandedId] = useState(null); @@ -43,7 +44,7 @@ export default function ConnectionsRouteScreen() { router.push("/connections/new")} + onPress={() => navigation.push(connectionsNewNavigation())} separateBackground /> diff --git a/apps/mobile/src/screens/connections/new.tsx b/apps/mobile/src/screens/connections/new.tsx index 1673a392ae9..a08fd9b164c 100644 --- a/apps/mobile/src/screens/connections/new.tsx +++ b/apps/mobile/src/screens/connections/new.tsx @@ -26,7 +26,7 @@ export default function ConnectionsNewRouteScreen() { onConnectPress, pairingConnectionError, } = useRemoteConnections(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const params = useRouteParams<{ mode?: string }>(); const insets = useSafeAreaInsets(); const [hostInput, setHostInput] = useState(""); @@ -121,11 +121,11 @@ export default function ConnectionsNewRouteScreen() { onChangeConnectionPairingUrl(pairingUrl); const result = await onConnectPress(pairingUrl); if (AsyncResult.isSuccess(result)) { - dismissRoute(router); + dismissRoute(navigation); } else { setIsSubmitting(false); } - }, [codeInput, hostInput, onChangeConnectionPairingUrl, onConnectPress, router]); + }, [codeInput, hostInput, onChangeConnectionPairingUrl, onConnectPress, navigation]); return ( diff --git a/apps/mobile/src/screens/index.tsx b/apps/mobile/src/screens/index.tsx index 56605d123f7..0eca8a62bbf 100644 --- a/apps/mobile/src/screens/index.tsx +++ b/apps/mobile/src/screens/index.tsx @@ -9,7 +9,13 @@ import { useMemo, useState } from "react"; import { useProjects, useThreadShells } from "../state/entities"; import { useWorkspaceState } from "../state/workspace"; -import { buildThreadRoutePath } from "../lib/routes"; +import { + connectionsNewNavigation, + newTaskNavigation, + settingsEnvironmentsNavigation, + settingsNavigation, + threadNavigation, +} from "../lib/routes"; import { useSavedRemoteConnections } from "../state/use-remote-environment-registry"; import { HomeScreen } from "../features/home/HomeScreen"; import { HomeHeader } from "../features/home/HomeHeader"; @@ -27,7 +33,7 @@ export default function HomeRouteScreen() { const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const [searchQuery, setSearchQuery] = useState(""); const { archiveThread, confirmDeleteThread } = useThreadListActions(); const environments = useMemo( @@ -73,7 +79,7 @@ export default function HomeRouteScreen() { router.push("/new")} + onPress={() => navigation.push(newTaskNavigation())} /> } /> @@ -91,30 +97,30 @@ export default function HomeRouteScreen() { threadSortOrder={listOptions.threadSortOrder} projectGroupingMode={listOptions.projectGroupingMode} onEnvironmentChange={setSelectedEnvironmentId} - onOpenSettings={() => router.push("/settings")} + onOpenSettings={() => navigation.push(settingsNavigation())} onProjectGroupingModeChange={setProjectGroupingMode} onProjectSortOrderChange={setProjectSortOrder} onSearchQueryChange={setSearchQuery} - onStartNewTask={() => router.push("/new")} + onStartNewTask={() => navigation.push(newTaskNavigation())} onThreadSortOrderChange={setThreadSortOrder} /> router.push("/connections/new")} + onAddConnection={() => navigation.push(connectionsNewNavigation())} onArchiveThread={archiveThread} onDeleteThread={confirmDeleteThread} onEnvironmentChange={setSelectedEnvironmentId} - onOpenEnvironments={() => router.push("/settings/environments")} - onOpenSettings={() => router.push("/settings")} + onOpenEnvironments={() => navigation.push(settingsEnvironmentsNavigation())} + onOpenSettings={() => navigation.push(settingsNavigation())} onProjectGroupingModeChange={setProjectGroupingMode} onProjectSortOrderChange={setProjectSortOrder} onSearchQueryChange={setSearchQuery} onSelectThread={(thread) => { - router.push(buildThreadRoutePath(thread)); + navigation.push(threadNavigation(thread)); }} - onStartNewTask={() => router.push("/new")} + onStartNewTask={() => navigation.push(newTaskNavigation())} onThreadSortOrderChange={setThreadSortOrder} projectGroupingMode={listOptions.projectGroupingMode} projects={projects} diff --git a/apps/mobile/src/screens/new/index.tsx b/apps/mobile/src/screens/new/index.tsx index 75e23e2d91e..64760e9d4ef 100644 --- a/apps/mobile/src/screens/new/index.tsx +++ b/apps/mobile/src/screens/new/index.tsx @@ -17,6 +17,11 @@ import { useProjects, useThreadShells } from "../../state/entities"; import type { WorkspaceState } from "../../state/workspaceModel"; import { useWorkspaceState } from "../../state/workspace"; import { groupProjectsByRepository } from "../../lib/repositoryGroups"; +import { + addProjectNavigation, + connectionsNewNavigation, + newTaskDraftNavigation, +} from "../../lib/routes"; import { useAdaptiveWorkspaceLayout } from "../../features/layout/AdaptiveWorkspaceLayout"; function deriveProjectEmptyState(catalogState: WorkspaceState): { @@ -78,7 +83,7 @@ export default function NewTaskRoute() { const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const { layout } = useAdaptiveWorkspaceLayout(); const insets = useSafeAreaInsets(); const chevronColor = useThemeColor("--color-chevron"); @@ -121,13 +126,13 @@ export default function NewTaskRoute() { router.dismiss()} + onPress={() => navigation.dismiss()} separateBackground /> ) : null} router.push("/new/add-project")} + onPress={() => navigation.push(addProjectNavigation())} separateBackground /> @@ -153,7 +158,7 @@ export default function NewTaskRoute() { {!catalogState.hasReadyEnvironment ? ( router.push("/connections/new")} + onPress={() => navigation.push(connectionsNewNavigation())} > Add environment @@ -162,7 +167,7 @@ export default function NewTaskRoute() { ) : ( router.push("/new/add-project")} + onPress={() => navigation.push(addProjectNavigation())} > Add new project @@ -179,14 +184,11 @@ export default function NewTaskRoute() { return ( ) : ( - + ); } diff --git a/apps/mobile/src/screens/settings/environments.tsx b/apps/mobile/src/screens/settings/environments.tsx index 0af15c027fe..3bfb01c827c 100644 --- a/apps/mobile/src/screens/settings/environments.tsx +++ b/apps/mobile/src/screens/settings/environments.tsx @@ -34,6 +34,7 @@ import { ConnectionStatusDot } from "../../features/connection/ConnectionStatusD import { splitEnvironmentSections } from "../../features/connection/environmentSections"; import { cn } from "../../lib/cn"; import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic"; +import { settingsEnvironmentNewNavigation } from "../../lib/routes"; import { useThemeColor } from "../../lib/useThemeColor"; import type { ConnectedEnvironmentSummary } from "../../state/remote-runtime-types"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; @@ -45,7 +46,7 @@ export default function SettingsEnvironmentsRouteScreen() { onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); const insets = useSafeAreaInsets(); const { localEnvironments, connectedCloudEnvironments } = splitEnvironmentSections({ connectedEnvironments, @@ -69,7 +70,7 @@ export default function SettingsEnvironmentsRouteScreen() { router.push("/settings/environment-new")} + onPress={() => navigation.push(settingsEnvironmentNewNavigation())} separateBackground /> diff --git a/apps/mobile/src/screens/settings/index.tsx b/apps/mobile/src/screens/settings/index.tsx index 23a82f1e869..c2c1bae8bc4 100644 --- a/apps/mobile/src/screens/settings/index.tsx +++ b/apps/mobile/src/screens/settings/index.tsx @@ -35,13 +35,19 @@ import { WorkspaceSidebarToolbar } from "../../features/layout/workspace-sidebar import { runtime } from "../../lib/runtime"; import { loadPreferences } from "../../lib/storage"; import { useThemeColor } from "../../lib/useThemeColor"; +import { + settingsArchiveNavigation, + settingsAuthNavigation, + settingsEnvironmentsNavigation, + settingsWaitlistNavigation, +} from "../../lib/routes"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported"; type LiveActivityStatus = "checking" | "enabled" | "disabled" | "signed-out" | "linking"; export default function SettingsRouteScreen() { - const router = useAppNavigation(); + const navigation = useAppNavigation(); const { layout } = useAdaptiveWorkspaceLayout(); return ( @@ -52,7 +58,7 @@ export default function SettingsRouteScreen() { router.back()} + onPress={() => navigation.back()} separateBackground /> @@ -86,7 +92,7 @@ function LocalSettingsRouteScreen() { icon="desktopcomputer" label="Environments" value={`${environmentCount}`} - href="/settings/environments" + href={settingsEnvironmentsNavigation()} /> @@ -100,7 +106,7 @@ function LocalSettingsRouteScreen() { function ConfiguredSettingsRouteScreen() { const insets = useSafeAreaInsets(); - const { push } = useAppNavigation(); + const navigation = useAppNavigation(); const { expand: expandClerkSheet } = useClerkSettingsSheetDetent(); const { getToken, isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { user } = useUser(); @@ -211,10 +217,10 @@ function ConfiguredSettingsRouteScreen() { "Live Activity updates require approved T3 Cloud access so relay can deliver updates to this device.", [ { text: "Cancel", style: "cancel" }, - { text: "Continue", onPress: () => push("/settings/waitlist") }, + { text: "Continue", onPress: () => navigation.push(settingsWaitlistNavigation()) }, ], ); - }, [push]); + }, [navigation]); const linkEnvironments = useCallback(async () => { if (!isSignedIn) { @@ -341,12 +347,12 @@ function ConfiguredSettingsRouteScreen() { const openAccount = useCallback(() => { if (!isLoaded) return; if (!isSignedIn) { - push("/settings/waitlist"); + navigation.push(settingsWaitlistNavigation()); return; } expandClerkSheet(); - push("/settings/auth"); - }, [expandClerkSheet, isLoaded, isSignedIn, push]); + navigation.push(settingsAuthNavigation()); + }, [expandClerkSheet, isLoaded, isSignedIn, navigation]); return ( @@ -381,7 +387,7 @@ function ConfiguredSettingsRouteScreen() { icon="desktopcomputer" label="Environments" value={`${environmentCount}`} - href="/settings/environments" + href={settingsEnvironmentsNavigation()} /> - + ); } @@ -458,7 +464,7 @@ function SettingsRow(props: { readonly icon: SymbolName; readonly label: string; readonly value?: string; - readonly href?: "/settings/archive" | "/settings/environments"; + readonly href?: ComponentProps["href"]; readonly onPress?: () => void; }) { const icon = useThemeColor("--color-icon"); diff --git a/apps/mobile/src/screens/settings/waitlist.tsx b/apps/mobile/src/screens/settings/waitlist.tsx index d83e3c966de..057ea5eabe1 100644 --- a/apps/mobile/src/screens/settings/waitlist.tsx +++ b/apps/mobile/src/screens/settings/waitlist.tsx @@ -11,26 +11,27 @@ import { ScrollView } from "react-native"; import { CloudWaitlistEnrollment } from "../../features/cloud/CloudWaitlistEnrollment"; import { useClerkSettingsSheetDetent } from "../../features/cloud/ClerkSettingsSheetDetent"; import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; +import { settingsAuthNavigation, settingsNavigation } from "../../lib/routes"; export default function SettingsWaitlistRouteScreen() { return hasCloudPublicConfig() ? ( ) : ( - + ); } function ConfiguredSettingsWaitlistRouteScreen() { const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { expand } = useClerkSettingsSheetDetent(); - const router = useAppNavigation(); + const navigation = useAppNavigation(); useFocusEffect( useCallback(() => { if (isLoaded && isSignedIn) { - router.replace("/settings"); + navigation.replace(settingsNavigation()); } - }, [isLoaded, isSignedIn, router]), + }, [isLoaded, isSignedIn, navigation]), ); return ( @@ -51,7 +52,7 @@ function ConfiguredSettingsWaitlistRouteScreen() { { expand(); - router.push("/settings/auth"); + navigation.push(settingsAuthNavigation()); }} /> From 77544333e03973a81fb2ab70bfd40580614f40d9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 8 Jul 2026 04:05:22 -0700 Subject: [PATCH 71/71] Improve mobile settings navigation and cached connection state - Nest settings routes for form-sheet navigation - Persist server config and VCS refs across web and mobile - Add mobile header actions for empty connection state --- app.json | 5 + apps/mobile/src/connection/storage.ts | 207 +++++++++++++++++- apps/mobile/src/features/home/HomeHeader.tsx | 8 +- .../threads/ThreadNavigationSidebar.tsx | 159 +++++++------- apps/mobile/src/navigation/RootNavigator.tsx | 59 +++-- apps/mobile/src/navigation/app-navigation.tsx | 27 +++ apps/mobile/src/navigation/linking.ts | 17 +- apps/mobile/src/navigation/route-model.ts | 32 ++- apps/web/src/connection/storage.ts | 119 +++++++++- .../src/connection/registry.test.ts | 4 + .../src/platform/persistence.ts | 30 +++ .../client-runtime/src/state/server.test.ts | 104 ++++++++- packages/client-runtime/src/state/server.ts | 134 +++++++++++- .../src/state/shell-sync.test.ts | 4 + .../src/state/threads-sync.test.ts | 4 + packages/client-runtime/src/state/vcs.test.ts | 73 ++++++ packages/client-runtime/src/state/vcs.ts | 155 +++++++++++-- 17 files changed, 983 insertions(+), 158 deletions(-) create mode 100644 app.json create mode 100644 packages/client-runtime/src/state/vcs.test.ts diff --git a/app.json b/app.json new file mode 100644 index 00000000000..87f6cb8b6e3 --- /dev/null +++ b/app.json @@ -0,0 +1,5 @@ +{ + "ios": { + "bundleIdentifier": "com.juliusmarminge.-t3toolsmonorepo" + } +} diff --git a/apps/mobile/src/connection/storage.ts b/apps/mobile/src/connection/storage.ts index 276ea3c5c08..0ded4a7416c 100644 --- a/apps/mobile/src/connection/storage.ts +++ b/apps/mobile/src/connection/storage.ts @@ -18,7 +18,9 @@ import { EnvironmentId, OrchestrationThread, OrchestrationShellSnapshot, + ServerConfig, ThreadId, + VcsListRefsResult, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -34,6 +36,10 @@ const SHELL_SNAPSHOT_CACHE_DIRECTORY = "connection-shell-snapshots"; const LEGACY_SHELL_SNAPSHOT_CACHE_DIRECTORY = "shell-snapshots"; const THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION = 1; const THREAD_SNAPSHOT_CACHE_DIRECTORY = "connection-thread-snapshots"; +const SERVER_CONFIG_CACHE_SCHEMA_VERSION = 1; +const SERVER_CONFIG_CACHE_DIRECTORY = "connection-server-configs"; +const VCS_REFS_CACHE_SCHEMA_VERSION = 1; +const VCS_REFS_CACHE_DIRECTORY = "connection-vcs-refs"; const StoredShellSnapshot = Schema.Struct({ schemaVersion: Schema.Literal(SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION), @@ -48,6 +54,19 @@ const StoredThreadSnapshot = Schema.Struct({ thread: OrchestrationThread, }); +const StoredServerConfig = Schema.Struct({ + schemaVersion: Schema.Literal(SERVER_CONFIG_CACHE_SCHEMA_VERSION), + environmentId: EnvironmentId, + config: ServerConfig, +}); + +const StoredVcsRefs = Schema.Struct({ + schemaVersion: Schema.Literal(VCS_REFS_CACHE_SCHEMA_VERSION), + environmentId: EnvironmentId, + cwd: Schema.String, + refs: VcsListRefsResult, +}); + const LegacyStoredShellSnapshot = Schema.Struct({ schemaVersion: Schema.Literal(1), environmentId: EnvironmentId, @@ -55,6 +74,16 @@ const LegacyStoredShellSnapshot = Schema.Struct({ snapshot: OrchestrationShellSnapshot, }); +const decodeStoredShellSnapshot = Schema.decodeUnknownResult(StoredShellSnapshot); +const encodeStoredShellSnapshot = Schema.encodeUnknownResult(StoredShellSnapshot); +const decodeStoredThreadSnapshot = Schema.decodeUnknownResult(StoredThreadSnapshot); +const encodeStoredThreadSnapshot = Schema.encodeUnknownResult(StoredThreadSnapshot); +const decodeStoredServerConfig = Schema.decodeUnknownResult(StoredServerConfig); +const encodeStoredServerConfig = Schema.encodeUnknownResult(StoredServerConfig); +const decodeStoredVcsRefs = Schema.decodeUnknownResult(StoredVcsRefs); +const encodeStoredVcsRefs = Schema.encodeUnknownResult(StoredVcsRefs); +const decodeLegacyStoredShellSnapshot = Schema.decodeUnknownResult(LegacyStoredShellSnapshot); + function catalogError(operation: string, cause: unknown) { return new ConnectionTransientError({ reason: "remote-unavailable", @@ -69,6 +98,10 @@ function shellPersistenceError( | "load-thread" | "save-thread" | "remove-thread" + | "load-server-config" + | "save-server-config" + | "load-vcs-refs" + | "save-vcs-refs" | "clear-environment", cause: unknown, ) { @@ -82,6 +115,14 @@ function threadSnapshotFileName(threadId: ThreadId): string { return `${encodeURIComponent(threadId)}.json`; } +function environmentCacheFileName(environmentId: EnvironmentId): string { + return `${encodeURIComponent(environmentId)}.json`; +} + +function vcsRefsFileName(cwd: string): string { + return `${encodeURIComponent(cwd)}.json`; +} + const threadSnapshotDirectory = Effect.fn("mobile.connectionStorage.threadSnapshotDirectory")( function* ( environmentId: EnvironmentId, @@ -146,9 +187,54 @@ const secureCatalogStorage: SecureCatalogStorage = { }; function shellSnapshotFileName(environmentId: EnvironmentId): string { - return `${encodeURIComponent(environmentId)}.json`; + return environmentCacheFileName(environmentId); } +const serverConfigFile = Effect.fn("mobile.connectionStorage.serverConfigFile")(function* ( + environmentId: EnvironmentId, + operation: "load-server-config" | "save-server-config" | "clear-environment", +) { + return yield* Effect.tryPromise({ + try: async () => { + const { Directory, File, Paths } = await import("expo-file-system"); + const directory = new Directory(Paths.document, SERVER_CONFIG_CACHE_DIRECTORY); + directory.create({ idempotent: true, intermediates: true }); + return new File(directory, environmentCacheFileName(environmentId)); + }, + catch: (cause) => shellPersistenceError(operation, cause), + }); +}); + +const vcsRefsDirectory = Effect.fn("mobile.connectionStorage.vcsRefsDirectory")(function* ( + environmentId: EnvironmentId, + operation: "load-vcs-refs" | "save-vcs-refs" | "clear-environment", +) { + return yield* Effect.tryPromise({ + try: async () => { + const { Directory, Paths } = await import("expo-file-system"); + const directory = new Directory( + Paths.document, + VCS_REFS_CACHE_DIRECTORY, + encodeURIComponent(environmentId), + ); + if (operation !== "clear-environment") { + directory.create({ idempotent: true, intermediates: true }); + } + return directory; + }, + catch: (cause) => shellPersistenceError(operation, cause), + }); +}); + +const vcsRefsFile = Effect.fn("mobile.connectionStorage.vcsRefsFile")(function* ( + environmentId: EnvironmentId, + cwd: string, + operation: "load-vcs-refs" | "save-vcs-refs", +) { + const { File } = yield* Effect.promise(() => import("expo-file-system")); + return new File(yield* vcsRefsDirectory(environmentId, operation), vcsRefsFileName(cwd)); +}); + const shellSnapshotFileInDirectory = Effect.fn( "mobile.connectionStorage.shellSnapshotFileInDirectory", )(function* ( @@ -289,9 +375,9 @@ export const connectionStorageLayer = Layer.effectContext( try: () => JSON.parse(raw) as unknown, catch: (cause) => shellPersistenceError("load-shell", cause), }); - const stored = yield* Effect.fromResult( - Schema.decodeUnknownResult(StoredShellSnapshot)(parsed), - ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause))); + const stored = yield* Effect.fromResult(decodeStoredShellSnapshot(parsed)).pipe( + Effect.mapError((cause) => shellPersistenceError("load-shell", cause)), + ); return stored.environmentId === environmentId ? Option.some(stored.snapshot) : Option.none(); @@ -310,7 +396,7 @@ export const connectionStorageLayer = Layer.effectContext( catch: (cause) => shellPersistenceError("load-shell", cause), }); const legacyStored = yield* Effect.fromResult( - Schema.decodeUnknownResult(LegacyStoredShellSnapshot)(legacyParsed), + decodeLegacyStoredShellSnapshot(legacyParsed), ).pipe(Effect.mapError((cause) => shellPersistenceError("load-shell", cause))); return legacyStored.environmentId === environmentId ? Option.some(legacyStored.snapshot) @@ -324,9 +410,9 @@ export const connectionStorageLayer = Layer.effectContext( environmentId, snapshot, } as const; - const encoded = yield* Effect.fromResult( - Schema.encodeUnknownResult(StoredShellSnapshot)(stored), - ).pipe(Effect.mapError((cause) => shellPersistenceError("save-shell", cause))); + const encoded = yield* Effect.fromResult(encodeStoredShellSnapshot(stored)).pipe( + Effect.mapError((cause) => shellPersistenceError("save-shell", cause)), + ); yield* Effect.try({ try: () => { if (!file.exists) { @@ -337,6 +423,47 @@ export const connectionStorageLayer = Layer.effectContext( catch: (cause) => shellPersistenceError("save-shell", cause), }); }), + loadServerConfig: (environmentId) => + Effect.gen(function* () { + const file = yield* serverConfigFile(environmentId, "load-server-config"); + if (!file.exists) { + return Option.none(); + } + const raw = yield* Effect.tryPromise({ + try: () => file.text(), + catch: (cause) => shellPersistenceError("load-server-config", cause), + }); + const parsed = yield* Effect.try({ + try: () => JSON.parse(raw) as unknown, + catch: (cause) => shellPersistenceError("load-server-config", cause), + }); + const stored = yield* Effect.fromResult(decodeStoredServerConfig(parsed)).pipe( + Effect.mapError((cause) => shellPersistenceError("load-server-config", cause)), + ); + return stored.environmentId === environmentId + ? Option.some(stored.config) + : Option.none(); + }), + saveServerConfig: (environmentId, config) => + Effect.gen(function* () { + const file = yield* serverConfigFile(environmentId, "save-server-config"); + const encoded = yield* Effect.fromResult( + encodeStoredServerConfig({ + schemaVersion: SERVER_CONFIG_CACHE_SCHEMA_VERSION, + environmentId, + config, + }), + ).pipe(Effect.mapError((cause) => shellPersistenceError("save-server-config", cause))); + yield* Effect.try({ + try: () => { + if (!file.exists) { + file.create({ intermediates: true, overwrite: true }); + } + file.write(JSON.stringify(encoded)); + }, + catch: (cause) => shellPersistenceError("save-server-config", cause), + }); + }), loadThread: (environmentId, threadId) => Effect.gen(function* () { const file = yield* threadSnapshotFile(environmentId, threadId, "load-thread"); @@ -351,9 +478,9 @@ export const connectionStorageLayer = Layer.effectContext( try: () => JSON.parse(raw) as unknown, catch: (cause) => shellPersistenceError("load-thread", cause), }); - const stored = yield* Effect.fromResult( - Schema.decodeUnknownResult(StoredThreadSnapshot)(parsed), - ).pipe(Effect.mapError((cause) => shellPersistenceError("load-thread", cause))); + const stored = yield* Effect.fromResult(decodeStoredThreadSnapshot(parsed)).pipe( + Effect.mapError((cause) => shellPersistenceError("load-thread", cause)), + ); return stored.environmentId === environmentId && stored.threadId === threadId ? Option.some(stored.thread) : Option.none(); @@ -362,7 +489,7 @@ export const connectionStorageLayer = Layer.effectContext( Effect.gen(function* () { const file = yield* threadSnapshotFile(environmentId, thread.id, "save-thread"); const encoded = yield* Effect.fromResult( - Schema.encodeUnknownResult(StoredThreadSnapshot)({ + encodeStoredThreadSnapshot({ schemaVersion: THREAD_SNAPSHOT_CACHE_SCHEMA_VERSION, environmentId, threadId: thread.id, @@ -379,6 +506,48 @@ export const connectionStorageLayer = Layer.effectContext( catch: (cause) => shellPersistenceError("save-thread", cause), }); }), + loadVcsRefs: (environmentId, cwd) => + Effect.gen(function* () { + const file = yield* vcsRefsFile(environmentId, cwd, "load-vcs-refs"); + if (!file.exists) { + return Option.none(); + } + const raw = yield* Effect.tryPromise({ + try: () => file.text(), + catch: (cause) => shellPersistenceError("load-vcs-refs", cause), + }); + const parsed = yield* Effect.try({ + try: () => JSON.parse(raw) as unknown, + catch: (cause) => shellPersistenceError("load-vcs-refs", cause), + }); + const stored = yield* Effect.fromResult(decodeStoredVcsRefs(parsed)).pipe( + Effect.mapError((cause) => shellPersistenceError("load-vcs-refs", cause)), + ); + return stored.environmentId === environmentId && stored.cwd === cwd + ? Option.some(stored.refs) + : Option.none(); + }), + saveVcsRefs: (environmentId, cwd, refs) => + Effect.gen(function* () { + const file = yield* vcsRefsFile(environmentId, cwd, "save-vcs-refs"); + const encoded = yield* Effect.fromResult( + encodeStoredVcsRefs({ + schemaVersion: VCS_REFS_CACHE_SCHEMA_VERSION, + environmentId, + cwd, + refs, + }), + ).pipe(Effect.mapError((cause) => shellPersistenceError("save-vcs-refs", cause))); + yield* Effect.try({ + try: () => { + if (!file.exists) { + file.create({ intermediates: true, overwrite: true }); + } + file.write(JSON.stringify(encoded)); + }, + catch: (cause) => shellPersistenceError("save-vcs-refs", cause), + }); + }), removeThread: (environmentId, threadId) => Effect.gen(function* () { const file = yield* threadSnapshotFile(environmentId, threadId, "remove-thread"); @@ -408,6 +577,13 @@ export const connectionStorageLayer = Layer.effectContext( catch: (cause) => shellPersistenceError("clear-environment", cause), }); } + const configFile = yield* serverConfigFile(environmentId, "clear-environment"); + if (configFile.exists) { + yield* Effect.try({ + try: () => configFile.delete(), + catch: (cause) => shellPersistenceError("clear-environment", cause), + }); + } const threadDirectory = yield* threadSnapshotDirectory( environmentId, "clear-environment", @@ -418,6 +594,13 @@ export const connectionStorageLayer = Layer.effectContext( catch: (cause) => shellPersistenceError("clear-environment", cause), }); } + const refsDirectory = yield* vcsRefsDirectory(environmentId, "clear-environment"); + if (refsDirectory.exists) { + yield* Effect.try({ + try: () => refsDirectory.delete(), + catch: (cause) => shellPersistenceError("clear-environment", cause), + }); + } }), }); diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 043c4c3d070..39d6a664bd3 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -14,7 +14,6 @@ import type { SearchBarCommands } from "react-native-screens"; import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useThemeColor } from "../../lib/useThemeColor"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; -import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import type { HomeProjectSortOrder } from "./homeThreadList"; import { @@ -77,14 +76,17 @@ export function HomeHeader(props: { unstable_headerRightItems: Platform.OS === "ios" ? () => [ - withNativeGlassHeaderItem({ + { accessibilityLabel: "Open settings", + glassEffect: false, icon: { name: "ellipsis", type: "sfSymbol" } as const, identifier: "home-settings", label: "", onPress: props.onOpenSettings, + sharesBackground: false, type: "button", - }), + variant: "plain", + }, ] : undefined, unstable_headerToolbarItems: diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 1cf8d448dfe..755bef03adc 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -15,7 +15,7 @@ import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; -import { settingsEnvironmentsNavigation } from "../../lib/routes"; +import { connectionsNewNavigation, settingsEnvironmentsNavigation } from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -566,83 +566,92 @@ export function ThreadNavigationSidebar(props: { if (usesNativeSidebarChrome) { const { Screen, ScreenStack, ScreenStackHeaderConfig } = require("react-native-screens") as typeof import("react-native-screens"); - const nativeHeaderRightBarButtonItems = [ - withNativeGlassHeaderItem({ - accessibilityLabel: "Open settings", - icon: { name: "ellipsis", type: "sfSymbol" } as const, - identifier: "thread-sidebar-settings", - onPress: props.onOpenSettings, - tintColor: foregroundColor, - type: "button", - }), - withNativeGlassHeaderItem({ - accessibilityLabel: "Filter and sort threads", - icon: { name: filterIcon, type: "sfSymbol" } as const, - identifier: "thread-sidebar-filter", - menu: { - title: "Thread list options", - items: [ - { - type: "submenu", - title: "Environment", - items: [ - { - onPress: () => setSelectedEnvironmentId(null), - state: options.selectedEnvironmentId === null ? "on" : "off", - subtitle: "Show threads from every environment", - title: "All environments", - type: "action", - }, - ...environments.map((environment) => ({ - onPress: () => setSelectedEnvironmentId(environment.environmentId), - state: - options.selectedEnvironmentId === environment.environmentId - ? ("on" as const) - : ("off" as const), - title: environment.label, - type: "action" as const, - })), - ], - }, - { - type: "submenu", - title: "Sort projects", - items: PROJECT_SORT_OPTIONS.map((option) => ({ - onPress: () => setProjectSortOrder(option.value), - state: - options.projectSortOrder === option.value ? ("on" as const) : ("off" as const), - title: option.label, - type: "action" as const, - })), - }, - { - type: "submenu", - title: "Sort threads", - items: THREAD_SORT_OPTIONS.map((option) => ({ - onPress: () => setThreadSortOrder(option.value), - state: - options.threadSortOrder === option.value ? ("on" as const) : ("off" as const), - title: option.label, - type: "action" as const, - })), - }, - { - type: "submenu", - title: "Group projects", - items: PROJECT_GROUPING_OPTIONS.map((option) => ({ - onPress: () => setProjectGroupingMode(option.value), + const settingsHeaderItem = withNativeGlassHeaderItem({ + accessibilityLabel: "Open settings", + icon: { name: "ellipsis", type: "sfSymbol" } as const, + identifier: "thread-sidebar-settings", + onPress: props.onOpenSettings, + tintColor: foregroundColor, + type: "button", + }); + const addEnvironmentHeaderItem = withNativeGlassHeaderItem({ + accessibilityLabel: "Add environment", + icon: { name: "plus", type: "sfSymbol" } as const, + identifier: "thread-sidebar-add-environment", + onPress: () => navigation.push(connectionsNewNavigation()), + tintColor: foregroundColor, + type: "button", + }); + const filterHeaderItem = withNativeGlassHeaderItem({ + accessibilityLabel: "Filter and sort threads", + icon: { name: filterIcon, type: "sfSymbol" } as const, + identifier: "thread-sidebar-filter", + menu: { + title: "Thread list options", + items: [ + { + type: "submenu", + title: "Environment", + items: [ + { + onPress: () => setSelectedEnvironmentId(null), + state: options.selectedEnvironmentId === null ? "on" : "off", + subtitle: "Show threads from every environment", + title: "All environments", + type: "action", + }, + ...environments.map((environment) => ({ + onPress: () => setSelectedEnvironmentId(environment.environmentId), state: - options.projectGroupingMode === option.value ? ("on" as const) : ("off" as const), - subtitle: option.subtitle, - title: option.label, + options.selectedEnvironmentId === environment.environmentId + ? ("on" as const) + : ("off" as const), + title: environment.label, type: "action" as const, })), - }, - ], - }, - tintColor: foregroundColor, - type: "menu", - }), + ], + }, + { + type: "submenu", + title: "Sort projects", + items: PROJECT_SORT_OPTIONS.map((option) => ({ + onPress: () => setProjectSortOrder(option.value), + state: options.projectSortOrder === option.value ? ("on" as const) : ("off" as const), + title: option.label, + type: "action" as const, + })), + }, + { + type: "submenu", + title: "Sort threads", + items: THREAD_SORT_OPTIONS.map((option) => ({ + onPress: () => setThreadSortOrder(option.value), + state: options.threadSortOrder === option.value ? ("on" as const) : ("off" as const), + title: option.label, + type: "action" as const, + })), + }, + { + type: "submenu", + title: "Group projects", + items: PROJECT_GROUPING_OPTIONS.map((option) => ({ + onPress: () => setProjectGroupingMode(option.value), + state: + options.projectGroupingMode === option.value ? ("on" as const) : ("off" as const), + subtitle: option.subtitle, + title: option.label, + type: "action" as const, + })), + }, + ], + }, + tintColor: foregroundColor, + type: "menu", + }); + const nativeHeaderRightBarButtonItems = [ + ...(!catalogState.hasConnections ? [addEnvironmentHeaderItem] : []), + settingsHeaderItem, + ...(catalogState.hasConnections ? [filterHeaderItem] : []), ] as ComponentProps["headerRightBarButtonItems"]; return ( diff --git a/apps/mobile/src/navigation/RootNavigator.tsx b/apps/mobile/src/navigation/RootNavigator.tsx index 484275d7f0e..dd7d4055166 100644 --- a/apps/mobile/src/navigation/RootNavigator.tsx +++ b/apps/mobile/src/navigation/RootNavigator.tsx @@ -44,9 +44,10 @@ import SettingsWaitlistRouteScreen from "../screens/settings/waitlist"; import { ThreadSelectionProvider } from "../state/use-thread-selection"; import { useThreadOutboxDrain } from "../state/use-thread-outbox-drain"; import { useCurrentPathname } from "./app-navigation"; -import type { AppStackParamList } from "./route-model"; +import type { AppStackParamList, SettingsStackParamList } from "./route-model"; const RootStack = createNativeStackNavigator(); +const SettingsStack = createNativeStackNavigator(); function ThreadSelectionRoute(props: { readonly children: ReactNode }) { return {props.children}; @@ -210,7 +211,7 @@ function WorkspaceNavigator() { const connectionSheetScreenOptions = { contentStyle: sheetStyle, gestureEnabled: true, - headerShown: false, + headerShown: true, presentation: "formSheet" as const, sheetAllowedDetents: [0.55, 0.7], sheetGrabberVisible: true, @@ -225,6 +226,7 @@ function WorkspaceNavigator() { } : { ...connectionSheetScreenOptions, + headerShown: false, sheetAllowedDetents: isExpanded ? [0.92] : [0.7], }; const newTaskScreenOptions = { @@ -250,37 +252,10 @@ function WorkspaceNavigator() { /> - - - - - ); } + +function SettingsNavigator() { + const sheetStyle = useResolveClassNames("bg-sheet"); + + return ( + + + + + + + + + ); +} diff --git a/apps/mobile/src/navigation/app-navigation.tsx b/apps/mobile/src/navigation/app-navigation.tsx index 512c9600819..5756bbfba3e 100644 --- a/apps/mobile/src/navigation/app-navigation.tsx +++ b/apps/mobile/src/navigation/app-navigation.tsx @@ -13,8 +13,10 @@ import { resolveNavigationTarget, type AppFocusedRoute, type AppNavigationInput, + type AppRouteName, type AppStackParamList, type RouteParams, + type SettingsStackRouteName, } from "./route-model"; export type AppNavigation = { @@ -36,6 +38,18 @@ type AppNavigationContextValue = { export const AppNavigationContext = createContext(null); +const SETTINGS_CHILD_ROUTES = new Set([ + "SettingsEnvironments", + "SettingsEnvironmentNew", + "SettingsArchive", + "SettingsAuth", + "SettingsWaitlist", +]); + +function isSettingsChildRoute(name: AppRouteName): name is SettingsStackRouteName { + return SETTINGS_CHILD_ROUTES.has(name); +} + export function useAppNavigation(): AppNavigation { const context = use(AppNavigationContext); if (context === null) { @@ -73,6 +87,19 @@ export function createAppNavigation( return; } + if (isSettingsChildRoute(target.name)) { + navigation.dispatch( + CommonActions.navigate({ + name: "Settings", + params: { + screen: target.name, + params: target.params, + }, + }), + ); + return; + } + if (action === "push") { navigation.dispatch(StackActions.push(target.name, target.params)); return; diff --git a/apps/mobile/src/navigation/linking.ts b/apps/mobile/src/navigation/linking.ts index 55ecafd7cf0..32bf3b584d3 100644 --- a/apps/mobile/src/navigation/linking.ts +++ b/apps/mobile/src/navigation/linking.ts @@ -9,12 +9,17 @@ export const appLinking: LinkingOptions = { screens: { Home: "", DebugRnsGlass: "debug/rns-glass", - Settings: "settings", - SettingsEnvironments: "settings/environments", - SettingsEnvironmentNew: "settings/environment-new", - SettingsArchive: "settings/archive", - SettingsAuth: "settings/auth", - SettingsWaitlist: "settings/waitlist", + Settings: { + path: "settings", + screens: { + SettingsIndex: "", + SettingsEnvironments: "environments", + SettingsEnvironmentNew: "environment-new", + SettingsArchive: "archive", + SettingsAuth: "auth", + SettingsWaitlist: "waitlist", + }, + }, Connections: "connections", ConnectionsNew: "connections/new", NewTask: "new", diff --git a/apps/mobile/src/navigation/route-model.ts b/apps/mobile/src/navigation/route-model.ts index 72fbd7bba56..dc174826ab9 100644 --- a/apps/mobile/src/navigation/route-model.ts +++ b/apps/mobile/src/navigation/route-model.ts @@ -1,15 +1,16 @@ -import type { NavigationState, PartialState, Route } from "@react-navigation/native"; +import type { + NavigationState, + NavigatorScreenParams, + PartialState, + Route, +} from "@react-navigation/native"; export type RouteParams = Record; export type AppRouteName = | "Home" | "Settings" - | "SettingsEnvironments" - | "SettingsEnvironmentNew" - | "SettingsArchive" - | "SettingsAuth" - | "SettingsWaitlist" + | SettingsStackRouteName | "Connections" | "ConnectionsNew" | "NewTask" @@ -31,14 +32,26 @@ export type AppRouteName = | "DebugRnsGlass" | "NotFound"; -export type AppStackParamList = { - Home: undefined; - Settings: undefined; +export type SettingsStackRouteName = + | "SettingsIndex" + | "SettingsEnvironments" + | "SettingsEnvironmentNew" + | "SettingsArchive" + | "SettingsAuth" + | "SettingsWaitlist"; + +export type SettingsStackParamList = { + SettingsIndex: undefined; SettingsEnvironments: undefined; SettingsEnvironmentNew: RouteParams | undefined; SettingsArchive: undefined; SettingsAuth: undefined; SettingsWaitlist: undefined; +}; + +export type AppStackParamList = { + Home: undefined; + Settings: NavigatorScreenParams | undefined; Connections: undefined; ConnectionsNew: RouteParams | undefined; NewTask: undefined; @@ -250,6 +263,7 @@ export function buildPathFromRoute(route: AppFocusedRoute): string { case "Home": return "/"; case "Settings": + case "SettingsIndex": return "/settings"; case "SettingsEnvironments": return "/settings/environments"; diff --git a/apps/web/src/connection/storage.ts b/apps/web/src/connection/storage.ts index d118a428ed7..5362ff2ba82 100644 --- a/apps/web/src/connection/storage.ts +++ b/apps/web/src/connection/storage.ts @@ -21,7 +21,9 @@ import { EnvironmentId, OrchestrationShellSnapshot, OrchestrationThread, + ServerConfig, ThreadId, + VcsListRefsResult, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -32,10 +34,12 @@ import * as Schema from "effect/Schema"; import * as Semaphore from "effect/Semaphore"; const DATABASE_NAME = "t3code:connection-runtime"; -const DATABASE_VERSION = 2; +const DATABASE_VERSION = 4; const CATALOG_STORE_NAME = "catalog"; const SHELL_STORE_NAME = "shell"; const THREAD_STORE_NAME = "thread"; +const SERVER_CONFIG_STORE_NAME = "server-config"; +const VCS_REFS_STORE_NAME = "vcs-refs"; const CATALOG_KEY = "document"; const SHELL_SNAPSHOT_CACHE_SCHEMA_VERSION = 1; @@ -52,6 +56,19 @@ const StoredThreadSnapshot = Schema.Struct({ thread: OrchestrationThread, }); const StoredThreadSnapshotJson = Schema.fromJsonString(StoredThreadSnapshot); +const StoredServerConfig = Schema.Struct({ + schemaVersion: Schema.Literal(1), + environmentId: EnvironmentId, + config: ServerConfig, +}); +const StoredServerConfigJson = Schema.fromJsonString(StoredServerConfig); +const StoredVcsRefs = Schema.Struct({ + schemaVersion: Schema.Literal(1), + environmentId: EnvironmentId, + cwd: Schema.String, + refs: VcsListRefsResult, +}); +const StoredVcsRefsJson = Schema.fromJsonString(StoredVcsRefs); const ConnectionCatalogDocumentJson = Schema.fromJsonString(ConnectionCatalogDocument); const decodeConnectionCatalogDocument = Schema.decodeUnknownEffect(ConnectionCatalogDocumentJson); const encodeConnectionCatalogDocument = Schema.encodeEffect(ConnectionCatalogDocumentJson); @@ -59,6 +76,10 @@ const decodeStoredShellSnapshot = Schema.decodeUnknownEffect(StoredShellSnapshot const encodeStoredShellSnapshot = Schema.encodeEffect(StoredShellSnapshotJson); const decodeStoredThreadSnapshot = Schema.decodeUnknownEffect(StoredThreadSnapshotJson); const encodeStoredThreadSnapshot = Schema.encodeEffect(StoredThreadSnapshotJson); +const decodeStoredServerConfig = Schema.decodeUnknownEffect(StoredServerConfigJson); +const encodeStoredServerConfig = Schema.encodeEffect(StoredServerConfigJson); +const decodeStoredVcsRefs = Schema.decodeUnknownEffect(StoredVcsRefsJson); +const encodeStoredVcsRefs = Schema.encodeEffect(StoredVcsRefsJson); function catalogError(operation: string, cause: unknown) { return new ConnectionTransientError({ @@ -77,6 +98,10 @@ function persistenceError( | "load-thread" | "save-thread" | "remove-thread" + | "load-server-config" + | "save-server-config" + | "load-vcs-refs" + | "save-vcs-refs" | "clear-environment", cause: unknown, ) { @@ -105,6 +130,12 @@ const openDatabase = Effect.fn("web.connectionStorage.openDatabase")(function* ( if (!request.result.objectStoreNames.contains(THREAD_STORE_NAME)) { request.result.createObjectStore(THREAD_STORE_NAME); } + if (!request.result.objectStoreNames.contains(SERVER_CONFIG_STORE_NAME)) { + request.result.createObjectStore(SERVER_CONFIG_STORE_NAME); + } + if (!request.result.objectStoreNames.contains(VCS_REFS_STORE_NAME)) { + request.result.createObjectStore(VCS_REFS_STORE_NAME); + } }); request.addEventListener("error", () => { resume(Effect.fail(catalogError("open", request.error ?? "Unknown IndexedDB error"))); @@ -194,6 +225,10 @@ function threadCacheKey(environmentId: EnvironmentId, threadId: ThreadId) { return `${environmentId}:${threadId}`; } +function vcsRefsCacheKey(environmentId: EnvironmentId, cwd: string) { + return `${environmentId}:${cwd}`; +} + const decodeCatalog = Effect.fn("web.connectionStorage.decodeCatalog")(function* (raw: string) { return yield* decodeConnectionCatalogDocument(raw).pipe( Effect.mapError((cause) => catalogError("decode", cause)), @@ -459,6 +494,40 @@ export const connectionStorageLayer = Layer.effectContext( : persistenceError("save-shell", cause), ), ), + loadServerConfig: (environmentId) => + readDatabaseValue(database, SERVER_CONFIG_STORE_NAME, environmentId).pipe( + Effect.flatMap((raw) => { + if (typeof raw !== "string") { + return Effect.succeed(Option.none()); + } + return decodeStoredServerConfig(raw).pipe( + Effect.mapError((cause) => persistenceError("load-server-config", cause)), + Effect.map((stored) => + stored.environmentId === environmentId ? Option.some(stored.config) : Option.none(), + ), + ); + }), + Effect.mapError((cause) => + cause._tag === "ConnectionPersistenceError" + ? cause + : persistenceError("load-server-config", cause), + ), + ), + saveServerConfig: (environmentId, config) => + Effect.gen(function* () { + const encoded = yield* encodeStoredServerConfig({ + schemaVersion: 1, + environmentId, + config, + }).pipe(Effect.mapError((cause) => persistenceError("save-server-config", cause))); + yield* writeDatabaseValue(database, SERVER_CONFIG_STORE_NAME, environmentId, encoded); + }).pipe( + Effect.mapError((cause) => + cause._tag === "ConnectionPersistenceError" + ? cause + : persistenceError("save-server-config", cause), + ), + ), loadThread: (environmentId, threadId) => readDatabaseValue( database, @@ -505,6 +574,48 @@ export const connectionStorageLayer = Layer.effectContext( : persistenceError("save-thread", cause), ), ), + loadVcsRefs: (environmentId, cwd) => + readDatabaseValue(database, VCS_REFS_STORE_NAME, vcsRefsCacheKey(environmentId, cwd)).pipe( + Effect.flatMap((raw) => { + if (typeof raw !== "string") { + return Effect.succeed(Option.none()); + } + return decodeStoredVcsRefs(raw).pipe( + Effect.mapError((cause) => persistenceError("load-vcs-refs", cause)), + Effect.map((stored) => + stored.environmentId === environmentId && stored.cwd === cwd + ? Option.some(stored.refs) + : Option.none(), + ), + ); + }), + Effect.mapError((cause) => + cause._tag === "ConnectionPersistenceError" + ? cause + : persistenceError("load-vcs-refs", cause), + ), + ), + saveVcsRefs: (environmentId, cwd, refs) => + Effect.gen(function* () { + const encoded = yield* encodeStoredVcsRefs({ + schemaVersion: 1, + environmentId, + cwd, + refs, + }).pipe(Effect.mapError((cause) => persistenceError("save-vcs-refs", cause))); + yield* writeDatabaseValue( + database, + VCS_REFS_STORE_NAME, + vcsRefsCacheKey(environmentId, cwd), + encoded, + ); + }).pipe( + Effect.mapError((cause) => + cause._tag === "ConnectionPersistenceError" + ? cause + : persistenceError("save-vcs-refs", cause), + ), + ), removeThread: (environmentId, threadId) => removeDatabaseValue( database, @@ -520,6 +631,12 @@ export const connectionStorageLayer = Layer.effectContext( THREAD_STORE_NAME, IDBKeyRange.bound(`${environmentId}:`, `${environmentId}:\uffff`), ), + removeDatabaseValue(database, SERVER_CONFIG_STORE_NAME, environmentId), + removeDatabaseValuesInRange( + database, + VCS_REFS_STORE_NAME, + IDBKeyRange.bound(`${environmentId}:`, `${environmentId}:\uffff`), + ), ], { concurrency: "unbounded", discard: true }, ).pipe(Effect.mapError((cause) => persistenceError("clear-environment", cause))), diff --git a/packages/client-runtime/src/connection/registry.test.ts b/packages/client-runtime/src/connection/registry.test.ts index 885ba4cb781..c75cc2fad4b 100644 --- a/packages/client-runtime/src/connection/registry.test.ts +++ b/packages/client-runtime/src/connection/registry.test.ts @@ -247,6 +247,10 @@ const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* ( loadThread: (_environmentId, _threadId) => Effect.succeed(Option.none()), saveThread: (_environmentId, _thread) => Effect.void, removeThread: (_environmentId, _threadId) => Effect.void, + loadServerConfig: () => Effect.succeed(Option.none()), + saveServerConfig: () => Effect.void, + loadVcsRefs: () => Effect.succeed(Option.none()), + saveVcsRefs: () => Effect.void, clear: (environmentId) => Ref.update(shellCache, (current) => { const next = new Map(current); diff --git a/packages/client-runtime/src/platform/persistence.ts b/packages/client-runtime/src/platform/persistence.ts index 71664bf4601..6515059655e 100644 --- a/packages/client-runtime/src/platform/persistence.ts +++ b/packages/client-runtime/src/platform/persistence.ts @@ -2,7 +2,9 @@ import { type EnvironmentId, type OrchestrationThread, type OrchestrationShellSnapshot, + type ServerConfig, type ThreadId, + type VcsListRefsResult, } from "@t3tools/contracts"; import * as Context from "effect/Context"; import * as Effect from "effect/Effect"; @@ -24,6 +26,10 @@ export class ConnectionPersistenceError extends Schema.TaggedErrorClass Effect.Effect; + /** + * The last complete server configuration. This deliberately includes provider + * metadata so offline task creation can still offer the models a user last saw. + */ + readonly loadServerConfig: ( + environmentId: EnvironmentId, + ) => Effect.Effect, ConnectionPersistenceError>; + readonly saveServerConfig: ( + environmentId: EnvironmentId, + config: ServerConfig, + ) => Effect.Effect; + /** + * The unfiltered branch list for a workspace. Query-specific lists are not + * cached because they are incomplete and unsafe to present as a full picker. + */ + readonly loadVcsRefs: ( + environmentId: EnvironmentId, + cwd: string, + ) => Effect.Effect, ConnectionPersistenceError>; + readonly saveVcsRefs: ( + environmentId: EnvironmentId, + cwd: string, + refs: VcsListRefsResult, + ) => Effect.Effect; readonly clear: ( environmentId: EnvironmentId, ) => Effect.Effect; diff --git a/packages/client-runtime/src/state/server.test.ts b/packages/client-runtime/src/state/server.test.ts index 4b9564e031c..d600c736834 100644 --- a/packages/client-runtime/src/state/server.test.ts +++ b/packages/client-runtime/src/state/server.test.ts @@ -1,8 +1,31 @@ -import { type ServerConfig, type ServerLifecycleWelcomePayload } from "@t3tools/contracts"; +import { + EnvironmentId, + type ServerConfig, + type ServerConfigStreamEvent, + type ServerLifecycleWelcomePayload, + WS_METHODS, +} from "@t3tools/contracts"; import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Stream from "effect/Stream"; +import * as SubscriptionRef from "effect/SubscriptionRef"; -import { applyServerConfigProjection, projectServerWelcome } from "./server.ts"; +import { + AVAILABLE_CONNECTION_STATE, + PrimaryConnectionTarget, + type PreparedConnection, +} from "../connection/model.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; +import * as Persistence from "../platform/persistence.ts"; +import type { WsRpcProtocolClient } from "../rpc/protocol.ts"; +import type { RpcSession } from "../rpc/session.ts"; +import { + applyServerConfigProjection, + makeEnvironmentServerConfigState, + projectServerWelcome, +} from "./server.ts"; const CONFIG = { availableEditors: [], @@ -14,6 +37,23 @@ const CONFIG = { settings: {}, } as unknown as ServerConfig; +const TARGET = new PrimaryConnectionTarget({ + environmentId: EnvironmentId.make("environment-1"), + label: "Test environment", + httpBaseUrl: "https://environment.example.test", + wsBaseUrl: "wss://environment.example.test", +}); + +function session(client: WsRpcProtocolClient): RpcSession { + return { + client, + initialConfig: Effect.succeed(CONFIG), + ready: Effect.void, + probe: Effect.void, + closed: Effect.never, + }; +} + describe("server state projection", () => { it("applies every config category to the projected snapshot", () => { const snapshot = applyServerConfigProjection(Option.none(), { @@ -51,4 +91,64 @@ describe("server state projection", () => { expect(Option.getOrThrow(afterReady)).toBe(welcome); expect(emitted).toEqual([]); }); + + it.effect("starts from cached configuration and persists the live projection", () => + Effect.gen(function* () { + const events = yield* Queue.unbounded(); + const client = { + [WS_METHODS.subscribeServerConfig]: () => Stream.fromQueue(events), + } as unknown as WsRpcProtocolClient; + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ + target: TARGET, + state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), + session: yield* SubscriptionRef.make(Option.some(session(client))), + prepared: yield* SubscriptionRef.make(Option.none()), + connect: Effect.void, + disconnect: Effect.void, + retryNow: Effect.void, + } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); + const savedConfigs = yield* Queue.unbounded(); + const cache = Persistence.EnvironmentCacheStore.of({ + loadShell: () => Effect.succeed(Option.none()), + saveShell: () => Effect.void, + loadThread: () => Effect.succeed(Option.none()), + saveThread: () => Effect.void, + removeThread: () => Effect.void, + loadServerConfig: () => Effect.succeed(Option.some(CONFIG)), + saveServerConfig: (_environmentId, config) => Queue.offer(savedConfigs, config), + loadVcsRefs: () => Effect.succeed(Option.none()), + saveVcsRefs: () => Effect.void, + clear: () => Effect.void, + }); + + yield* Effect.scoped( + Effect.gen(function* () { + const state = yield* makeEnvironmentServerConfigState().pipe( + Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), + Effect.provideService(Persistence.EnvironmentCacheStore, cache), + ); + expect(Option.getOrThrow(yield* SubscriptionRef.get(state)).config).toBe(CONFIG); + + const providers: ServerConfig["providers"] = []; + yield* Queue.offer(events, { + version: 1, + type: "providerStatuses", + payload: { providers }, + }); + const projected = yield* SubscriptionRef.changes(state).pipe( + Stream.filter((value) => + Option.match(value, { + onNone: () => false, + onSome: (projection) => projection.latestEvent.type === "providerStatuses", + }), + ), + Stream.runHead, + ); + expect(Option.getOrThrow(Option.getOrThrow(projected)).config.providers).toBe(providers); + }), + ); + + expect((yield* Queue.take(savedConfigs)).providers).toEqual([]); + }), + ); }); diff --git a/packages/client-runtime/src/state/server.ts b/packages/client-runtime/src/state/server.ts index eb784183793..200e2d587af 100644 --- a/packages/client-runtime/src/state/server.ts +++ b/packages/client-runtime/src/state/server.ts @@ -5,8 +5,12 @@ import { type ServerLifecycleWelcomePayload, WS_METHODS, } from "@t3tools/contracts"; +import * as Effect from "effect/Effect"; import * as Option from "effect/Option"; +import * as Queue from "effect/Queue"; +import * as Result from "effect/Result"; import * as Stream from "effect/Stream"; +import * as SubscriptionRef from "effect/SubscriptionRef"; import { AsyncResult, Atom } from "effect/unstable/reactivity"; import { @@ -16,6 +20,11 @@ import { createEnvironmentRpcSubscriptionAtomFamily, } from "./runtime.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; +import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import { safeErrorLogAttributes } from "../errors/safeLog.ts"; +import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import { subscribe, type EnvironmentRpcInput } from "../rpc/client.ts"; +import { followStreamInEnvironment } from "./runtime.ts"; export interface ServerConfigProjection { readonly config: ServerConfig; @@ -68,6 +77,111 @@ export function projectServerConfig( return [next, Option.toArray(next)]; } +const cachedConfigSnapshotEvent = (config: ServerConfig): ServerConfigStreamEvent => ({ + version: 1, + type: "snapshot", + config, +}); + +/** + * Keeps a complete server configuration available during reconnects. Server + * config carries the provider/model catalogue used by task creation, so it is + * useful—and safe—to retain after a transport session ends. + */ +export const makeEnvironmentServerConfigState = Effect.fn("EnvironmentServerConfigState.make")( + function* () { + const supervisor = yield* EnvironmentSupervisor; + const cache = yield* EnvironmentCacheStore; + const environmentId = supervisor.target.environmentId; + const cachedConfig = yield* cache.loadServerConfig(environmentId).pipe( + Effect.catch((error) => + Effect.logWarning("Could not load cached server configuration.").pipe( + Effect.annotateLogs({ + environmentId, + ...safeErrorLogAttributes(error), + }), + Effect.as(Option.none()), + ), + ), + ); + const state = yield* SubscriptionRef.make>( + Option.map(cachedConfig, (config) => ({ + config, + latestEvent: cachedConfigSnapshotEvent(config), + })), + ); + const persistence = yield* Queue.sliding(1); + + const persist = Effect.fn("EnvironmentServerConfigState.persist")(function* ( + config: ServerConfig, + ) { + yield* cache.saveServerConfig(environmentId, config).pipe( + Effect.catch((error) => + Effect.logWarning("Could not persist cached server configuration.").pipe( + Effect.annotateLogs({ + environmentId, + ...safeErrorLogAttributes(error), + }), + ), + ), + ); + }); + + yield* Stream.fromQueue(persistence).pipe( + Stream.debounce("500 millis"), + Stream.runForEach(persist), + Effect.forkScoped, + ); + + yield* subscribe(WS_METHODS.subscribeServerConfig, {}).pipe( + Stream.runForEach((event) => + Effect.gen(function* () { + const next = applyServerConfigProjection(yield* SubscriptionRef.get(state), event); + if (Option.isNone(next)) { + return; + } + yield* SubscriptionRef.set(state, next); + yield* Queue.offer(persistence, next.value.config); + }), + ), + Effect.forkScoped, + ); + + yield* Effect.addFinalizer(() => + SubscriptionRef.get(state).pipe( + Effect.flatMap( + Option.match({ + onNone: () => Effect.void, + onSome: (projection) => persist(projection.config), + }), + ), + ), + ); + + return state; + }, +); + +export function serverConfigStateChanges(environmentId: EnvironmentId) { + return followStreamInEnvironment( + environmentId, + Stream.unwrap( + makeEnvironmentServerConfigState().pipe( + Effect.map((state) => + SubscriptionRef.changes(state).pipe( + Stream.filterMap((projection) => + Option.match(projection, { + onNone: () => Result.failVoid, + onSome: (value) => Result.succeed(value), + }), + ), + ), + ), + ), + ), + ); +} + export function projectServerWelcome( current: Option.Option, event: { @@ -86,7 +200,7 @@ export function projectServerWelcome( } export function createServerEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, options: { readonly initialConfigValueAtom: ( environmentId: EnvironmentId, @@ -98,12 +212,18 @@ export function createServerEnvironmentAtoms( mode: "serial" as const, key: ({ environmentId }: { readonly environmentId: string }) => environmentId, }; - const configProjection = createEnvironmentRpcSubscriptionAtomFamily(runtime, { - label: "environment-data:server:config-projection", - tag: WS_METHODS.subscribeServerConfig, - transform: (stream) => - stream.pipe(Stream.mapAccum(Option.none, projectServerConfig)), - }); + const configProjectionFamily = Atom.family((environmentId: EnvironmentId) => + runtime + .atom(serverConfigStateChanges(environmentId)) + .pipe( + Atom.setIdleTTL(5 * 60_000), + Atom.withLabel(`environment-data:server:config-projection:${environmentId}`), + ), + ); + const configProjection = (target: { + readonly environmentId: EnvironmentId; + readonly input: EnvironmentRpcInput; + }) => configProjectionFamily(target.environmentId); const emptyConfigAtom = Atom.make(null).pipe( Atom.withLabel("environment-data:server:config:empty"), ); diff --git a/packages/client-runtime/src/state/shell-sync.test.ts b/packages/client-runtime/src/state/shell-sync.test.ts index 2eab7214225..3865d2fe888 100644 --- a/packages/client-runtime/src/state/shell-sync.test.ts +++ b/packages/client-runtime/src/state/shell-sync.test.ts @@ -72,6 +72,10 @@ describe("environment shell synchronization", () => { loadThread: () => Effect.succeed(Option.none()), saveThread: () => Effect.void, removeThread: () => Effect.void, + loadServerConfig: () => Effect.succeed(Option.none()), + saveServerConfig: () => Effect.void, + loadVcsRefs: () => Effect.succeed(Option.none()), + saveVcsRefs: () => Effect.void, clear: () => Effect.void, }); const shellState = yield* makeEnvironmentShellState().pipe( diff --git a/packages/client-runtime/src/state/threads-sync.test.ts b/packages/client-runtime/src/state/threads-sync.test.ts index 3a5a8b69630..5b4fe5da2d3 100644 --- a/packages/client-runtime/src/state/threads-sync.test.ts +++ b/packages/client-runtime/src/state/threads-sync.test.ts @@ -140,6 +140,10 @@ const makeHarness = Effect.fn("TestEnvironmentThreads.makeHarness")(function* (o Ref.update(savedThreads, (current) => [...current, thread]), removeThread: (_environmentId, threadId) => Ref.update(removedThreads, (current) => [...current, threadId]), + loadServerConfig: () => Effect.succeed(Option.none()), + saveServerConfig: () => Effect.void, + loadVcsRefs: () => Effect.succeed(Option.none()), + saveVcsRefs: () => Effect.void, clear: () => Effect.void, }); const threadState = yield* makeEnvironmentThreadState(THREAD_ID).pipe( diff --git a/packages/client-runtime/src/state/vcs.test.ts b/packages/client-runtime/src/state/vcs.test.ts new file mode 100644 index 00000000000..212882b541e --- /dev/null +++ b/packages/client-runtime/src/state/vcs.test.ts @@ -0,0 +1,73 @@ +import { EnvironmentId, type VcsListRefsResult } from "@t3tools/contracts"; +import { describe, expect, it } from "@effect/vitest"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as SubscriptionRef from "effect/SubscriptionRef"; + +import { + AVAILABLE_CONNECTION_STATE, + PrimaryConnectionTarget, + type PreparedConnection, +} from "../connection/model.ts"; +import * as EnvironmentSupervisor from "../connection/supervisor.ts"; +import * as Persistence from "../platform/persistence.ts"; +import type { RpcSession } from "../rpc/session.ts"; +import { makeCachedVcsRefsState } from "./vcs.ts"; + +const TARGET = new PrimaryConnectionTarget({ + environmentId: EnvironmentId.make("environment-1"), + label: "Test environment", + httpBaseUrl: "https://environment.example.test", + wsBaseUrl: "wss://environment.example.test", +}); + +const CACHED_REFS: VcsListRefsResult = { + refs: [ + { + name: "main", + current: true, + isDefault: true, + worktreePath: "/repo", + }, + ], + isRepo: true, + hasPrimaryRemote: true, + nextCursor: null, + totalCount: 1, +}; + +describe("cached VCS refs", () => { + it.effect("loads an unfiltered branch list without a connection", () => + Effect.scoped( + Effect.gen(function* () { + const supervisor = EnvironmentSupervisor.EnvironmentSupervisor.of({ + target: TARGET, + state: yield* SubscriptionRef.make(AVAILABLE_CONNECTION_STATE), + session: yield* SubscriptionRef.make(Option.none()), + prepared: yield* SubscriptionRef.make(Option.none()), + connect: Effect.void, + disconnect: Effect.void, + retryNow: Effect.void, + } satisfies EnvironmentSupervisor.EnvironmentSupervisor["Service"]); + const cache = Persistence.EnvironmentCacheStore.of({ + loadShell: () => Effect.succeed(Option.none()), + saveShell: () => Effect.void, + loadThread: () => Effect.succeed(Option.none()), + saveThread: () => Effect.void, + removeThread: () => Effect.void, + loadServerConfig: () => Effect.succeed(Option.none()), + saveServerConfig: () => Effect.void, + loadVcsRefs: () => Effect.succeed(Option.some(CACHED_REFS)), + saveVcsRefs: () => Effect.void, + clear: () => Effect.void, + }); + const state = yield* makeCachedVcsRefsState({ cwd: "/repo", limit: 100 }).pipe( + Effect.provideService(EnvironmentSupervisor.EnvironmentSupervisor, supervisor), + Effect.provideService(Persistence.EnvironmentCacheStore, cache), + ); + + expect(Option.getOrThrow(yield* SubscriptionRef.get(state))).toEqual(CACHED_REFS); + }), + ), + ); +}); diff --git a/packages/client-runtime/src/state/vcs.ts b/packages/client-runtime/src/state/vcs.ts index 846d0d50609..e88fab9830e 100644 --- a/packages/client-runtime/src/state/vcs.ts +++ b/packages/client-runtime/src/state/vcs.ts @@ -1,26 +1,155 @@ -import { type VcsStatusResult, WS_METHODS } from "@t3tools/contracts"; +import { + type EnvironmentId, + type VcsListRefsInput, + type VcsListRefsResult, + type VcsStatusResult, + WS_METHODS, +} from "@t3tools/contracts"; import { applyGitStatusStreamEvent } from "@t3tools/shared/git"; +import * as Effect from "effect/Effect"; +import * as Option from "effect/Option"; +import * as Result from "effect/Result"; import * as Stream from "effect/Stream"; +import * as SubscriptionRef from "effect/SubscriptionRef"; import { Atom } from "effect/unstable/reactivity"; -import { - createEnvironmentRpcCommand, - createEnvironmentRpcQueryAtomFamily, - createEnvironmentSubscriptionAtomFamily, -} from "./runtime.ts"; +import { createEnvironmentRpcCommand, createEnvironmentSubscriptionAtomFamily } from "./runtime.ts"; import type { EnvironmentRegistry } from "../connection/registry.ts"; -import { subscribe, type EnvironmentRpcInput } from "../rpc/client.ts"; +import { EnvironmentSupervisor } from "../connection/supervisor.ts"; +import { safeErrorLogAttributes } from "../errors/safeLog.ts"; +import { EnvironmentCacheStore } from "../platform/persistence.ts"; +import { request, subscribe, type EnvironmentRpcInput } from "../rpc/client.ts"; +import { followStreamInEnvironment } from "./runtime.ts"; import { vcsCommandConcurrency, vcsCommandScheduler } from "./vcsCommandScheduler.ts"; +const OFFLINE_BRANCH_LIST_LIMIT = 100; + +function canUseVcsRefsCache(input: VcsListRefsInput): boolean { + return ( + input.query === undefined && + input.cursor === undefined && + input.includeMatchingRemoteRefs === undefined && + input.refKind === undefined && + input.limit === OFFLINE_BRANCH_LIST_LIMIT + ); +} + +/** + * Retains the last unfiltered branch-list response for the new-task picker. + * Filtered or paginated lists intentionally stay live-only: treating a + * partial result as a complete offline list would make branch selection + * misleading. + */ +export const makeCachedVcsRefsState = Effect.fn("CachedVcsRefsState.make")(function* ( + input: VcsListRefsInput, +) { + const supervisor = yield* EnvironmentSupervisor; + const cache = yield* EnvironmentCacheStore; + const environmentId = supervisor.target.environmentId; + const useCache = canUseVcsRefsCache(input); + const cached = useCache + ? yield* cache.loadVcsRefs(environmentId, input.cwd).pipe( + Effect.catch((error) => + Effect.logWarning("Could not load cached Git refs.").pipe( + Effect.annotateLogs({ + environmentId, + cwd: input.cwd, + ...safeErrorLogAttributes(error), + }), + Effect.as(Option.none()), + ), + ), + ) + : Option.none(); + const state = yield* SubscriptionRef.make(cached); + + const refresh = Effect.fn("CachedVcsRefsState.refresh")(function* () { + const refs = yield* request(WS_METHODS.vcsListRefs, input); + yield* SubscriptionRef.set(state, Option.some(refs)); + if (!useCache) { + return; + } + yield* cache.saveVcsRefs(environmentId, input.cwd, refs).pipe( + Effect.catch((error) => + Effect.logWarning("Could not persist cached Git refs.").pipe( + Effect.annotateLogs({ + environmentId, + cwd: input.cwd, + ...safeErrorLogAttributes(error), + }), + ), + ), + ); + }); + + yield* Stream.concat( + Stream.fromEffect(SubscriptionRef.get(supervisor.state)), + SubscriptionRef.changes(supervisor.state), + ).pipe( + Stream.filterMap((connection) => + connection.phase === "connected" ? Result.succeed(connection.generation) : Result.failVoid, + ), + Stream.changes, + Stream.runForEach(() => + refresh().pipe( + Effect.catch((error) => + Effect.logWarning("Could not refresh Git refs.").pipe( + Effect.annotateLogs({ + environmentId, + cwd: input.cwd, + ...safeErrorLogAttributes(error), + }), + ), + ), + ), + ), + Effect.forkScoped, + ); + + return state; +}); + +export function cachedVcsRefsChanges(environmentId: EnvironmentId, input: VcsListRefsInput) { + return followStreamInEnvironment( + environmentId, + Stream.unwrap( + makeCachedVcsRefsState(input).pipe( + Effect.map((state) => + SubscriptionRef.changes(state).pipe( + Stream.filterMap((result) => + Option.match(result, { + onNone: () => Result.failVoid, + onSome: (value) => Result.succeed(value), + }), + ), + ), + ), + ), + ), + ); +} + export function createVcsEnvironmentAtoms( - runtime: Atom.AtomRuntime, + runtime: Atom.AtomRuntime, ) { - return { - listRefs: createEnvironmentRpcQueryAtomFamily(runtime, { - label: "environment-data:vcs:list-refs", - tag: WS_METHODS.vcsListRefs, - staleTimeMs: 5_000, + const listRefsByEnvironment = Atom.family((environmentId: EnvironmentId) => + Atom.family((inputKey: string) => { + const input = JSON.parse(inputKey) as VcsListRefsInput; + return runtime + .atom(cachedVcsRefsChanges(environmentId, input)) + .pipe( + Atom.setIdleTTL(5 * 60_000), + Atom.withLabel(`environment-data:vcs:list-refs:${environmentId}:${inputKey}`), + ); }), + ); + const listRefs = (target: { + readonly environmentId: EnvironmentId; + readonly input: VcsListRefsInput; + }) => listRefsByEnvironment(target.environmentId)(JSON.stringify(target.input)); + + return { + listRefs, status: createEnvironmentSubscriptionAtomFamily(runtime, { label: "environment-data:vcs:status", subscribe: (input: EnvironmentRpcInput) =>