From ab4c4549666aded057724d6e1f1ca0f9d836fed3 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 22 Jun 2026 20:44:02 -0700 Subject: [PATCH 01/81] 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 e309c09eaeb9da584a170ad160c44d8a049a108e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 23 Jun 2026 22:08:57 -0700 Subject: [PATCH 02/81] 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 4a2f33f6265ad7c00ae1d91f0e53c01ae75b990c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 23 Jun 2026 22:47:55 -0700 Subject: [PATCH 03/81] 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 a2c6b39a9220e02198927a8e2d0f41f1bf1b95f4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Wed, 24 Jun 2026 09:25:28 -0700 Subject: [PATCH 04/81] 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 bee588a6e34df7ef314f2b648b65788e5b9dc097 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 25 Jun 2026 22:43:21 -0700 Subject: [PATCH 05/81] 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 50f3501b9120083132f68d0298c5e12cce55dfa9 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 17:25:24 -0700 Subject: [PATCH 06/81] 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 9cde29cb661..03c033d1fef 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 @@ -2387,8 +2393,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: '*' @@ -2406,9 +2412,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==} @@ -2437,18 +2449,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==} @@ -2456,16 +2468,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: @@ -2475,10 +2487,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: '*' @@ -2500,8 +2512,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==} @@ -2511,15 +2523,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 @@ -2546,8 +2558,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: '*' @@ -2566,8 +2578,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==} @@ -5456,6 +5470,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==} @@ -6384,26 +6413,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: '*' @@ -6413,15 +6442,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: '*' @@ -6431,13 +6460,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: '*' @@ -6447,8 +6476,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: '*' @@ -6456,14 +6485,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: '*' @@ -6486,8 +6515,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: '*' @@ -6500,8 +6529,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: '*' @@ -6511,12 +6540,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: '*' @@ -6525,8 +6554,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: '*' @@ -6536,8 +6565,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: '*' @@ -6550,15 +6579,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: '*' @@ -6587,8 +6616,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: @@ -6599,8 +6628,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: '*' @@ -6612,8 +6641,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: '*' @@ -6630,15 +6659,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': '*' @@ -8737,8 +8766,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: '*' @@ -9306,6 +9335,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==} @@ -11323,23 +11355,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)': @@ -11922,29 +11954,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 @@ -11957,8 +11989,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 @@ -11983,7 +12015,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' @@ -12021,11 +12053,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) @@ -12053,9 +12106,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) @@ -12067,9 +12120,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 @@ -12098,9 +12151,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 @@ -12118,16 +12171,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 @@ -12149,12 +12202,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 @@ -12172,11 +12224,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) @@ -12225,16 +12277,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: @@ -12251,17 +12303,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 @@ -12276,9 +12328,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 @@ -12292,7 +12344,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: @@ -13907,12 +13961,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) @@ -14983,7 +15037,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 @@ -15030,8 +15084,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 @@ -15926,28 +16033,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) @@ -15955,112 +16062,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) @@ -16068,12 +16175,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 @@ -16083,51 +16190,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) @@ -16137,12 +16244,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 @@ -16157,6 +16264,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) @@ -16171,17 +16279,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 @@ -16189,20 +16297,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 @@ -16210,11 +16318,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 @@ -16223,20 +16331,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: @@ -16247,35 +16355,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: @@ -18726,7 +18834,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) @@ -19523,6 +19631,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 ef20a23d83a..3ac87245556 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -85,7 +85,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:" @@ -105,10 +105,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 6a75b97563624f8d40d5a9281abf8cd7fd60efd6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 17:55:40 -0700 Subject: [PATCH 07/81] 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 03c033d1fef..575fea2437a 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)) @@ -1656,43 +1656,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 @@ -1705,11 +1705,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' @@ -1741,15 +1741,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 @@ -11248,10 +11248,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: @@ -11280,18 +11280,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 @@ -11306,9 +11306,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 @@ -11323,72 +11323,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 @@ -11398,7 +11398,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 @@ -11954,7 +11954,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) @@ -11964,7 +11964,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 @@ -11989,7 +11989,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 @@ -12015,8 +12015,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' @@ -12099,18 +12099,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: @@ -12171,13 +12171,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)': @@ -12206,7 +12206,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 @@ -12224,14 +12224,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: @@ -12306,14 +12306,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 @@ -12328,18 +12328,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' @@ -12602,13 +12602,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: @@ -13431,15 +13431,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': {} @@ -13499,7 +13499,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 @@ -13509,7 +13509,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 @@ -13557,7 +13557,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) @@ -13565,18 +13565,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 @@ -13961,15 +13959,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': {} @@ -15084,8 +15082,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 @@ -15137,8 +15135,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 @@ -16035,29 +16033,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 @@ -16065,119 +16063,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): @@ -16190,66 +16188,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 @@ -16257,10 +16255,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 @@ -16268,8 +16266,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' @@ -16281,7 +16279,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: {} @@ -16289,7 +16287,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 @@ -16297,20 +16295,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 @@ -16318,7 +16316,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 @@ -16328,25 +16326,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' @@ -16355,37 +16353,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 @@ -18806,102 +18804,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) @@ -18913,23 +18911,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 @@ -19980,14 +19978,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 3ac87245556..4878b4790ff 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,13 +20,13 @@ supportedArchitectures: - glibc 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 816bc4e29fff43f1e56477cc4ceb885a036b7da2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 18:43:31 -0700 Subject: [PATCH 08/81] 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 58eb4b1b753453b535f38da6326cbb61cada8c25 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Fri, 26 Jun 2026 22:01:12 -0700 Subject: [PATCH 09/81] 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 43e8ee8e624672415ec9ac898aa2bee392a03da0 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:15:16 -0700 Subject: [PATCH 10/81] 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 575fea2437a..4e1ae68d38d 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) @@ -12015,7 +12018,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' @@ -12313,7 +12316,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 @@ -16227,7 +16230,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) @@ -16258,7 +16261,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 @@ -18865,7 +18868,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 4878b4790ff..43c03137157 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -111,6 +111,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 f10eca4eebecc33b4485f588855aee448a5166e5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:23:12 -0700 Subject: [PATCH 11/81] 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 c5b567fcb7a7660efd875bd93063cf1d23fb8d15 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:27:13 -0700 Subject: [PATCH 12/81] 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 e218c047e7f4fe5c7a5dbfbfa2971affabc7aaeb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 11:51:58 -0700 Subject: [PATCH 13/81] 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 103410bcc889b50108e743c0a0a015cfbe7cf70e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:12:33 -0700 Subject: [PATCH 14/81] 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 ae2e81fb8cf810ed914b0d3e7b71d65b9b52a0d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:17:54 -0700 Subject: [PATCH 15/81] 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 7494eba7e465841082c058dfe8259aeedfcb93f7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:21:31 -0700 Subject: [PATCH 16/81] 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 91722d4982701c3bacf3911fe0b38080aad46e17 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:24:47 -0700 Subject: [PATCH 17/81] 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 be87e6c2054b988bb205a2d6ccd4c8677fa409ba Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:30:36 -0700 Subject: [PATCH 18/81] 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 66b381608aad6082fd303a0292378e5ee4d7bde4 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:35:47 -0700 Subject: [PATCH 19/81] 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 3b22639e165f9325d776496d3b5496fab06808ec Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:44:54 -0700 Subject: [PATCH 20/81] 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 3369bb960b02e85dec874572a589d5fd61ebb58c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Sat, 27 Jun 2026 12:54:20 -0700 Subject: [PATCH 21/81] 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 67f04944268f6a125c2aac1a6323f6c1f3c33f21 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 13:10:36 -0700 Subject: [PATCH 22/81] 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 | 563 ++++--- pnpm-workspace.yaml | 1 + 22 files changed, 4451 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 4e1ae68d38d..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 @@ -5255,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: @@ -11251,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: @@ -11358,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)': @@ -11957,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) @@ -11967,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 @@ -11992,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 @@ -12018,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' @@ -12102,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: @@ -12174,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)': @@ -12209,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 @@ -12227,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: @@ -12309,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 @@ -12331,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' @@ -12605,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: @@ -13434,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': {} @@ -13502,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 @@ -13512,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 @@ -13560,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) @@ -13568,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 @@ -13962,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': {} @@ -14431,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)) @@ -14452,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 @@ -15085,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 @@ -15138,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 @@ -16036,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 @@ -16066,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): @@ -16191,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 @@ -16258,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 @@ -16269,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' @@ -16282,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: {} @@ -16290,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 @@ -16298,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 @@ -16319,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 @@ -16329,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' @@ -16356,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 @@ -18807,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) @@ -18914,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 @@ -19981,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: {} @@ -20112,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 @@ -20126,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 @@ -20172,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)) @@ -20196,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 43c03137157..f5aa1de55d9 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -110,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 175523ceac97e814398a9925d3ae8f3a135a8883 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 14:16:34 -0700 Subject: [PATCH 23/81] 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 1d7cf9e538515cb8848eefedddc236d367271c4c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 14:43:02 -0700 Subject: [PATCH 24/81] 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 02aa919ac8f9b8c2d080a90b472d266b643995df Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 15:11:53 -0700 Subject: [PATCH 25/81] 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 e9fa6e401a5a65a152761bc9ab3871930ec18158 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 16:22:58 -0700 Subject: [PATCH 26/81] 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 a54fb4994f30b15b98e6607d33793f3f2f1ac7d7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 17:05:45 -0700 Subject: [PATCH 27/81] 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 16b9c78a6ac705087cd652ce90eb19c93679ea43 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 17:40:20 -0700 Subject: [PATCH 28/81] 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/81] 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 8218569fb944e6733c689ba02efe5afd971b3d01 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 18:28:10 -0700 Subject: [PATCH 30/81] 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 734d879933bf2c24dd4692d53a3a3ebe2631f122 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 18:39:43 -0700 Subject: [PATCH 31/81] 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 71b5d487588bcf0db6c8d12f91a4d76450653463 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 19:02:21 -0700 Subject: [PATCH 32/81] 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 9741b6d013b299d419fa974afa08e48d2822b3ed Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 19:57:07 -0700 Subject: [PATCH 33/81] 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 f01988e24ce167cce376fa2a53c12f25eacc3ca5 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 22:12:22 -0700 Subject: [PATCH 34/81] 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 dcaea07ee3d8b4daaad4f64fee91bd5c0cbb0b15 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 22:30:20 -0700 Subject: [PATCH 35/81] 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 433155f4c2de65983f0b29667d7a26eab8c4f33b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 22:51:23 -0700 Subject: [PATCH 36/81] 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 1de372b9c7922c4881908ab5cf49ad9f3dffe2ec Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:05:17 -0700 Subject: [PATCH 37/81] 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 3daa71d3971577edcb7aa13645a82ffd333e477e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:16:01 -0700 Subject: [PATCH 38/81] 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 5f29190085791ab1bd82494a04047e8f42d56ca8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:32:06 -0700 Subject: [PATCH 39/81] 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 7ff1fd0ce624a4c439f92bfe4bd79ad1c1ff714d Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Mon, 29 Jun 2026 23:50:55 -0700 Subject: [PATCH 40/81] 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 f849eae5db595657f6d43c5e5b937c7fd2905280 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:01:16 -0700 Subject: [PATCH 41/81] 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 e4faac57e5935ed5eea939c3768298d6dcf1e63b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:18:38 -0700 Subject: [PATCH 42/81] 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/81] 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 f6745a4080d70ac73b9eba61e825ea9611e752e2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:47:06 -0700 Subject: [PATCH 44/81] 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 cb90c54dc30300394d183ab176e632b50892cf62 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 00:59:41 -0700 Subject: [PATCH 45/81] 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 54eec5cd023657355ece71449af59984950f37fa Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 01:10:37 -0700 Subject: [PATCH 46/81] 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 96b4f8bfa2031c71332276f1ef465694b901bf5f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 01:23:04 -0700 Subject: [PATCH 47/81] 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/81] 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 90babc915e2864c1f51d0511ae21e2a0ef64223b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 01:59:32 -0700 Subject: [PATCH 49/81] 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 f0b2fc8d6f4623cc8d25f997f701d502b834db5b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:08:45 -0700 Subject: [PATCH 50/81] 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 7e4dab7cd38c8f6d1c77818cac4d22bd850c01c6 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:15:56 -0700 Subject: [PATCH 51/81] 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 fe1341a45286b33600e2043dd0a66026c2c13e51 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:30:55 -0700 Subject: [PATCH 52/81] 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 cbc606f10c7f596e7c222695824dfe123c9cb4ee Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 02:39:21 -0700 Subject: [PATCH 53/81] 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/81] 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/81] 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 c8f6d4be39568410cddf8fbbe73f71d20fb85057 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 03:43:37 -0700 Subject: [PATCH 56/81] 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 35c1127f4b152ea03a2040a51c31bd7735142b1e Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:05:42 -0700 Subject: [PATCH 57/81] 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 af7583c1121a91376c4ed93d957a1550fd0b6fcb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:11:46 -0700 Subject: [PATCH 58/81] 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 662dd403fcc5c9dfe5850d0a9c68ef5175b5cd67 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:23:06 -0700 Subject: [PATCH 59/81] 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 e29c04b037bbd2da0f2c1626b809a5622fda3846 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:40:23 -0700 Subject: [PATCH 60/81] 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 b6ac481a89ba09930e81574bbd40b839070ef087 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:50:22 -0700 Subject: [PATCH 61/81] 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 e095a58acb68160edd9a2e20fe01fbba57554497 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 04:58:51 -0700 Subject: [PATCH 62/81] 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 73f503898d596dca0d96e42e389f5dbfd764f94b Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 07:39:55 -0700 Subject: [PATCH 63/81] 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 434b9092ee77524e7ebcc7c7618bb7b2c4520d8f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 07:57:44 -0700 Subject: [PATCH 64/81] 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 22827d6274e8cf82e7347bc32729cf1ceea23f3c Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 08:26:38 -0700 Subject: [PATCH 65/81] 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 b1247e1051e88e5634acf1f31ce3da1af4e4c0bb Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 08:44:41 -0700 Subject: [PATCH 66/81] 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 45970f222a014ac077b7c5cdf19f39c290c631be Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 09:46:59 -0700 Subject: [PATCH 67/81] 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 16fd7da4b4e9e5024d967436a7fe80bbc1ad60a8 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 11:41:13 -0700 Subject: [PATCH 68/81] 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 f5aa1de55d9..43c03137157 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 52b59fcd2d3aca4f09b3d6d87359237360534a12 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 16:12:14 -0700 Subject: [PATCH 69/81] 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 5df0b2cbaa800379bdea9e9dc6baa399a1c11327 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Tue, 30 Jun 2026 17:32:34 -0700 Subject: [PATCH 70/81] 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 d43ed9f68db410ef2b3ba0737f60cd1515a0efa1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 01:02:45 -0700 Subject: [PATCH 71/81] Migrate mobile navigation to adaptive native stacks - Rework mobile routes around a static native stack layout - Add iPad-friendly sheet and header behavior across core screens - Update DPoP hashing and review diff pull-to-refresh support --- apps/mobile/app.config.ts | 4 + apps/mobile/index.ts | 7 +- .../ios/T3ReviewDiffModule.swift | 7 +- .../t3-review-diff/ios/T3ReviewDiffView.swift | 20 + apps/mobile/src/App.tsx | 43 +- apps/mobile/src/Stack.tsx | 421 +++++++ apps/mobile/src/components/AppText.tsx | 5 +- apps/mobile/src/components/EmptyState.tsx | 22 + .../agent-awareness/notificationNavigation.ts | 8 +- .../archive/ArchivedThreadsRouteScreen.tsx | 2 +- .../archive/ArchivedThreadsScreen.tsx | 16 +- apps/mobile/src/features/cloud/dpop.ts | 50 +- .../connection/ConnectionsNewRouteScreen.tsx} | 35 +- .../connection/ConnectionsRouteScreen.tsx} | 21 +- .../features/diffs/nativeReviewDiffSurface.ts | 2 + .../features/files/FileMarkdownPreview.tsx | 34 +- .../src/features/files/FileTreeBrowser.tsx | 99 +- .../src/features/files/SourceFileSurface.tsx | 28 +- .../features/files/ThreadFilesRouteScreen.tsx | 445 ++----- .../files/thread-file-navigator-pane.tsx | 2 +- apps/mobile/src/features/home/HomeHeader.tsx | 23 +- .../home/HomeRouteScreen.tsx} | 81 +- apps/mobile/src/features/home/HomeScreen.tsx | 81 +- .../HardwareKeyboardCommandProvider.tsx | 31 +- .../layout/AdaptiveWorkspaceLayout.tsx | 71 +- .../layout/native-glass-header-items.ts | 20 +- .../layout/workspace-sidebar-toolbar.tsx | 2 +- .../projects/AddProjectDestinationRoute.tsx | 15 + .../projects/AddProjectLocalRoute.tsx | 12 + .../projects/AddProjectRepositoryRoute.tsx | 31 + .../features/projects/AddProjectScreen.tsx | 113 +- .../projects/AddProjectSourceRoute.tsx | 5 + .../review/ReviewCommentComposerSheet.tsx | 18 +- .../src/features/review/ReviewSheet.tsx | 224 +--- .../review/nativeReviewDiffAdapter.ts | 12 +- .../useReviewCommentSelectionController.ts | 11 +- .../settings/SettingsAuthRouteScreen.tsx} | 23 +- .../SettingsEnvironmentsRouteScreen.tsx} | 33 +- .../settings/SettingsRouteScreen.tsx} | 109 +- .../settings/SettingsWaitlistRouteScreen.tsx} | 37 +- .../terminal/ThreadTerminalRouteScreen.tsx | 79 +- .../threads/NewTaskDraftRouteScreen.tsx} | 19 +- .../features/threads/NewTaskDraftScreen.tsx | 15 +- .../threads/NewTaskRouteScreen.tsx} | 108 +- .../src/features/threads/ThreadComposer.tsx | 31 +- .../features/threads/ThreadDetailScreen.tsx | 28 +- .../src/features/threads/ThreadFeed.tsx | 59 +- .../features/threads/ThreadGitControls.tsx | 88 +- .../threads/ThreadNavigationSidebar.tsx | 19 +- .../features/threads/ThreadRouteScreen.tsx | 303 ++--- .../features/threads/git/GitBranchesSheet.tsx | 17 +- .../features/threads/git/GitCommitSheet.tsx | 13 +- .../features/threads/git/GitConfirmSheet.tsx | 34 +- .../features/threads/git/GitOverviewSheet.tsx | 180 ++- .../threads/threadContentPresentation.ts | 8 +- apps/mobile/src/lib/routes.test.ts | 110 -- apps/mobile/src/lib/routes.ts | 285 ----- .../StackHeader.tsx} | 108 +- .../scrollEdgeEffects.test.ts} | 5 +- .../scrollEdgeEffects.ts} | 4 + apps/mobile/src/navigation/RootNavigator.tsx | 405 ------ apps/mobile/src/navigation/app-navigation.tsx | 179 --- apps/mobile/src/navigation/linking.ts | 39 - apps/mobile/src/navigation/route-model.ts | 332 ----- apps/mobile/src/screens/+not-found.tsx | 43 - apps/mobile/src/screens/debug/rns-glass.tsx | 1099 ----------------- .../screens/new/add-project/destination.tsx | 5 - .../src/screens/new/add-project/index.tsx | 5 - .../src/screens/new/add-project/local.tsx | 5 - .../screens/new/add-project/repository.tsx | 23 - apps/mobile/src/state/use-thread-selection.ts | 35 +- .../client-runtime/src/state/threadDetail.ts | 2 +- .../client-runtime/src/state/threadState.ts | 16 + packages/client-runtime/src/state/threads.ts | 20 +- packages/shared/src/dpop.ts | 31 +- ...act-navigation%2Fnative-stack@7.17.6.patch | 100 ++ patches/react-native-screens@4.25.2.patch | 267 +--- pnpm-lock.yaml | 702 +++++------ pnpm-workspace.yaml | 1 + 79 files changed, 2278 insertions(+), 4767 deletions(-) create mode 100644 apps/mobile/src/Stack.tsx rename apps/mobile/src/{screens/connections/new.tsx => features/connection/ConnectionsNewRouteScreen.tsx} (90%) rename apps/mobile/src/{screens/connections/index.tsx => features/connection/ConnectionsRouteScreen.tsx} (85%) rename apps/mobile/src/{screens/index.tsx => features/home/HomeRouteScreen.tsx} (56%) create mode 100644 apps/mobile/src/features/projects/AddProjectDestinationRoute.tsx create mode 100644 apps/mobile/src/features/projects/AddProjectLocalRoute.tsx create mode 100644 apps/mobile/src/features/projects/AddProjectRepositoryRoute.tsx create mode 100644 apps/mobile/src/features/projects/AddProjectSourceRoute.tsx rename apps/mobile/src/{screens/settings/auth.tsx => features/settings/SettingsAuthRouteScreen.tsx} (55%) rename apps/mobile/src/{screens/settings/environments.tsx => features/settings/SettingsEnvironmentsRouteScreen.tsx} (94%) rename apps/mobile/src/{screens/settings/index.tsx => features/settings/SettingsRouteScreen.tsx} (85%) rename apps/mobile/src/{screens/settings/waitlist.tsx => features/settings/SettingsWaitlistRouteScreen.tsx} (52%) rename apps/mobile/src/{screens/new/draft.tsx => features/threads/NewTaskDraftRouteScreen.tsx} (50%) rename apps/mobile/src/{screens/new/index.tsx => features/threads/NewTaskRouteScreen.tsx} (69%) delete mode 100644 apps/mobile/src/lib/routes.test.ts delete mode 100644 apps/mobile/src/lib/routes.ts rename apps/mobile/src/{navigation/native-stack-header.tsx => native/StackHeader.tsx} (82%) rename apps/mobile/src/{lib/native-scroll-edge-effect.test.ts => native/scrollEdgeEffects.test.ts} (89%) rename apps/mobile/src/{lib/native-scroll-edge-effect.ts => native/scrollEdgeEffects.ts} (82%) delete mode 100644 apps/mobile/src/navigation/RootNavigator.tsx delete mode 100644 apps/mobile/src/navigation/app-navigation.tsx delete mode 100644 apps/mobile/src/navigation/linking.ts delete mode 100644 apps/mobile/src/navigation/route-model.ts delete mode 100644 apps/mobile/src/screens/+not-found.tsx delete mode 100644 apps/mobile/src/screens/debug/rns-glass.tsx delete mode 100644 apps/mobile/src/screens/new/add-project/destination.tsx delete mode 100644 apps/mobile/src/screens/new/add-project/index.tsx delete mode 100644 apps/mobile/src/screens/new/add-project/local.tsx delete mode 100644 apps/mobile/src/screens/new/add-project/repository.tsx create mode 100644 packages/client-runtime/src/state/threadState.ts create mode 100644 patches/@react-navigation%2Fnative-stack@7.17.6.patch diff --git a/apps/mobile/app.config.ts b/apps/mobile/app.config.ts index b6c3b4dd3da..192153316b0 100644 --- a/apps/mobile/app.config.ts +++ b/apps/mobile/app.config.ts @@ -81,6 +81,10 @@ const config: ExpoConfig = { icon: variant.iosIcon, supportsTablet: true, bundleIdentifier: variant.iosBundleIdentifier, + // Pin code signing to the T3 Tools team so non-interactive `expo run:ios` + // does not fall back to a personal team (which cannot sign app groups, + // Sign in with Apple, or push notification entitlements). + appleTeamId: "ARK85ZXQ4Z", associatedDomains: [ `applinks:${variant.relyingParty}`, `webcredentials:${variant.relyingParty}`, diff --git a/apps/mobile/index.ts b/apps/mobile/index.ts index febabbfe399..979dc75d5f1 100644 --- a/apps/mobile/index.ts +++ b/apps/mobile/index.ts @@ -1,6 +1,11 @@ -import "react-native-gesture-handler"; import { registerRootComponent } from "expo"; +import "react-native-gesture-handler"; +import { featureFlags } from "react-native-screens"; import App from "./src/App"; +// Required for react-native-screens' iOS FormSheet sizing fix when a nested +// native stack is rendered inside a non-fitToContents formSheet. +featureFlags.experiment.synchronousScreenUpdatesEnabled = true; + registerRootComponent(App); diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift index 346588b798f..96a1785f9fc 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffModule.swift @@ -53,13 +53,18 @@ public class T3ReviewDiffModule: Module { view.setInitialRowIndex(initialRowIndex) } + Prop("refreshing") { (view: T3ReviewDiffView, refreshing: Bool) in + view.setRefreshing(refreshing) + } + Events( "onDebug", "onVisibleFileChange", "onToggleFile", "onToggleViewedFile", "onPressLine", - "onToggleComment" + "onToggleComment", + "onPullToRefresh" ) AsyncFunction("scrollToFile") { (view: T3ReviewDiffView, fileId: String, animated: Bool) in diff --git a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift index 387ca365e39..ef2157d4355 100644 --- a/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift +++ b/apps/mobile/modules/t3-review-diff/ios/T3ReviewDiffView.swift @@ -343,6 +343,9 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { let onToggleViewedFile = EventDispatcher() let onPressLine = EventDispatcher() let onToggleComment = EventDispatcher() + let onPullToRefresh = EventDispatcher() + + private let pullRefreshControl = UIRefreshControl() public required init(appContext: AppContext? = nil) { super.init(appContext: appContext) @@ -357,6 +360,8 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { scrollView.showsVerticalScrollIndicator = true scrollView.showsHorizontalScrollIndicator = false scrollView.backgroundColor = contentView.theme.background + pullRefreshControl.addTarget(self, action: #selector(handlePullToRefresh), for: .valueChanged) + scrollView.refreshControl = pullRefreshControl addSubview(scrollView) contentView.backgroundColor = contentView.theme.background @@ -802,6 +807,21 @@ public final class T3ReviewDiffView: ExpoView, UIScrollViewDelegate { setVerticalContentOffset(0, animated: animated) } + @objc private func handlePullToRefresh() { + onPullToRefresh([:]) + } + + /// Driven from JS: set to false once the reload completes to dismiss the spinner. + func setRefreshing(_ refreshing: Bool) { + if refreshing { + if !pullRefreshControl.isRefreshing { + pullRefreshControl.beginRefreshing() + } + } else if pullRefreshControl.isRefreshing { + pullRefreshControl.endRefreshing() + } + } + private func applyStyle() { contentView.style = ReviewDiffNativeStyle .resolve(stylePayload) diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index dc221fc05be..3c597f8040f 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -4,25 +4,41 @@ import { DMSans_700Bold, useFonts, } from "@expo-google-fonts/dm-sans"; +import * as Linking from "expo-linking"; +import * as SplashScreen from "expo-splash-screen"; +import { StatusBar, useColorScheme } 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 { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigation/native"; import { RegistryContext } from "@effect/atom-react"; -import { LoadingScreen } from "./components/LoadingScreen"; +import { useEffect } from "react"; import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider"; -import { AppNavigationProvider } from "./navigation/app-navigation"; -import { RootNavigator } from "./navigation/RootNavigator"; +import { RootStack } from "./Stack"; import { appAtomRegistry } from "./state/atom-registry"; +import { useThemeColor } from "./lib/useThemeColor"; import "../global.css"; +const appLinking = { + prefixes: [Linking.createURL("/"), "t3code://", "t3code-dev://", "t3code-preview://"], +}; + +const Navigation = createStaticNavigation(RootStack); + export default function App() { const [fontsLoaded] = useFonts({ DMSans_400Regular, DMSans_500Medium, DMSans_700Bold, }); + const colorScheme = useColorScheme(); + const statusBarBg = useThemeColor("--color-status-bar"); + + useEffect(() => { + if (fontsLoaded) SplashScreen.hide(); + }, [fontsLoaded]); return ( @@ -30,13 +46,20 @@ export default function App() { - - {fontsLoaded ? ( - - ) : ( - - )} - + + {/* The navigation theme drives the NATIVE header appearance: native-stack + forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without + this, React Navigation defaults to its light theme and every native + header (glass buttons, title, materials) is forced light even when + the system is in dark mode. */} + diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx new file mode 100644 index 00000000000..8086f5eef04 --- /dev/null +++ b/apps/mobile/src/Stack.tsx @@ -0,0 +1,421 @@ +import { + createPathConfigForStaticNavigation, + getPathFromState, + NavigationState, + StackActions, + useNavigation, +} from "@react-navigation/native"; +import { + createNativeStackNavigator, + createNativeStackScreen, + type NativeStackNavigationOptions, +} from "@react-navigation/native-stack"; +import { DynamicColorIOS, Platform, Pressable, ScrollView, StyleSheet } from "react-native"; +import { useResolveClassNames } from "uniwind"; + +import { AppText as Text } from "./components/AppText"; +import { ArchivedThreadsRouteScreen } from "./features/archive/ArchivedThreadsRouteScreen"; +import { useAgentNotificationNavigation } from "./features/agent-awareness/notificationNavigation"; +import { ClerkSettingsSheetDetentProvider } from "./features/cloud/ClerkSettingsSheetDetent"; +import { ThreadFilesTreeScreen, ThreadFileScreen } from "./features/files/ThreadFilesRouteScreen"; +import { AdaptiveWorkspaceLayout } from "./features/layout/AdaptiveWorkspaceLayout"; +import { HardwareKeyboardCommandProvider } from "./features/keyboard/HardwareKeyboardCommandProvider"; +import { ReviewCommentComposerSheet } from "./features/review/ReviewCommentComposerSheet"; +import { ReviewSheet } from "./features/review/ReviewSheet"; +import { ThreadTerminalRouteScreen } from "./features/terminal/ThreadTerminalRouteScreen"; +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 { ThreadRouteScreen } from "./features/threads/ThreadRouteScreen"; +import { ConnectionsRouteScreen } from "./features/connection/ConnectionsRouteScreen"; +import { ConnectionsNewRouteScreen } from "./features/connection/ConnectionsNewRouteScreen"; +import { HomeRouteScreen } from "./features/home/HomeRouteScreen"; +import { AddProjectDestinationRoute } from "./features/projects/AddProjectDestinationRoute"; +import { AddProjectLocalRoute } from "./features/projects/AddProjectLocalRoute"; +import { AddProjectRepositoryRoute } from "./features/projects/AddProjectRepositoryRoute"; +import { AddProjectSourceRoute } from "./features/projects/AddProjectSourceRoute"; +import { NewTaskDraftRouteScreen } from "./features/threads/NewTaskDraftRouteScreen"; +import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider"; +import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen"; +import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; +import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; +import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen"; +import { SettingsWaitlistRouteScreen } from "./features/settings/SettingsWaitlistRouteScreen"; +import { nativeHeaderScrollEdgeEffects } from "./native/StackHeader"; +import { useThreadOutboxDrain } from "./state/use-thread-outbox-drain"; + +const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); + +// Matches --color-sheet in global.css (light/dark). DynamicColorIOS lets the header +// background stay STATIC config while still adapting to appearance changes. +const SHEET_BACKGROUND_COLOR = + Platform.OS === "ios" + ? DynamicColorIOS({ light: "rgba(242, 242, 247, 0.98)", dark: "rgba(14, 14, 14, 0.98)" }) + : undefined; + +type AppScreenOptions = NativeStackNavigationOptions & { + readonly unstable_navigationItemStyle?: "editor"; +}; + +// Shared header presets. Screens only override genuinely dynamic values (titles, +// subtitles, toolbar items, search callbacks) via NativeStackScreenOptions. +// +// GLASS: transparent header over the screen's primary scroll view, with the iOS 26 +// scroll-edge blur sampling the content (Home, Thread, Files tree, settings sheet). +const GLASS_HEADER_OPTIONS: AppScreenOptions = { + headerBackButtonDisplayMode: "minimal", + headerBackTitle: "", + headerLargeTitle: false, + headerShadowVisible: false, + headerShown: true, + headerStyle: Platform.OS === "ios" ? { backgroundColor: "transparent" } : undefined, + headerTitleStyle: { fontSize: 18, fontWeight: "800" }, + headerTransparent: Platform.OS === "ios", + scrollEdgeEffects: Platform.OS === "ios" ? HEADER_SCROLL_EDGE_EFFECTS : undefined, + unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, +}; + +// SOLID: opaque sheet-colored header for surfaces whose content scrolls internally +// (file viewer, terminal, review) — there is nothing for glass to sample there. +const SOLID_HEADER_OPTIONS: AppScreenOptions = { + headerBackButtonDisplayMode: "minimal", + headerBackTitle: "", + headerLargeTitle: false, + headerShadowVisible: false, + headerShown: true, + headerStyle: + SHEET_BACKGROUND_COLOR !== undefined + ? // native-stack types this as `string`, but the native side accepts any + // ColorValue including DynamicColorIOS. + { backgroundColor: SHEET_BACKGROUND_COLOR as unknown as string } + : undefined, + headerTitleStyle: { fontSize: 18, fontWeight: "800" }, + headerTransparent: false, + unstable_navigationItemStyle: Platform.OS === "ios" ? "editor" : undefined, +}; + +// Solid header variant for screens inside sheets (centered title, no editor style). +const SHEET_SOLID_HEADER_OPTIONS: AppScreenOptions = { + ...SOLID_HEADER_OPTIONS, + unstable_navigationItemStyle: undefined, +}; + +const SettingsSheetStack = createNativeStackNavigator({ + initialRouteName: "Settings", + screenOptions: { + ...GLASS_HEADER_OPTIONS, + // Sheets read better with the iOS-default centered title (no editor style). + unstable_navigationItemStyle: undefined, + }, + screens: { + Settings: createNativeStackScreen({ + screen: SettingsRouteScreen, + linking: "", + options: { + title: "Settings", + }, + }), + SettingsEnvironments: createNativeStackScreen({ + screen: SettingsEnvironmentsRouteScreen, + linking: "environments", + options: { + title: "Environments", + }, + }), + SettingsEnvironmentNew: createNativeStackScreen({ + screen: ConnectionsNewRouteScreen, + linking: "environment-new", + options: { + title: "Add Environment", + }, + }), + SettingsArchive: createNativeStackScreen({ + screen: ArchivedThreadsRouteScreen, + linking: "archive", + options: { + title: "Archived Threads", + }, + }), + SettingsAuth: createNativeStackScreen({ + screen: SettingsAuthRouteScreen, + linking: "auth", + options: { + title: "Sign in", + }, + }), + SettingsWaitlist: createNativeStackScreen({ + screen: SettingsWaitlistRouteScreen, + linking: "waitlist", + options: { + title: "Join the waitlist", + }, + }), + }, +}); + +// Thread routes live FLAT in the root stack (not in a nested navigator). A nested +// stack means a second UINavigationController with its own UINavigationBar, which +// breaks iOS 26's shared-header morphing between Home and Thread (each pair inside +// one bar morphs; across two bars the whole screen slides). Flat linking paths keep +// the same deep-link URLs the nested config produced. +const THREAD_LINKING_PREFIX = "threads/:environmentId/:threadId"; + +// New-task / add-project flow: nested navigator inside the formSheet (Settings-sheet +// pattern — a plain formSheet screen cannot render a stack header; the header and +// in-sheet pushes come from this nested stack). +const NewTaskSheetStack = createNativeStackNavigator({ + initialRouteName: "NewTask", + screenOptions: { + ...GLASS_HEADER_OPTIONS, + // Sheets read better with the iOS-default centered title (no editor style). + unstable_navigationItemStyle: undefined, + }, + screens: { + NewTask: createNativeStackScreen({ + screen: NewTaskRouteScreen, + linking: "", + options: { + title: "Choose project", + }, + }), + NewTaskDraft: createNativeStackScreen({ + screen: NewTaskDraftRouteScreen, + linking: "draft", + // The draft composer has no scroll view for glass to sample; a solid + // header also lays the content out below the bar (no manual inset). + options: SHEET_SOLID_HEADER_OPTIONS, + }), + AddProject: createNativeStackScreen({ + screen: AddProjectSourceRoute, + linking: "add-project", + options: { + title: "Add Project", + }, + }), + AddProjectRepository: createNativeStackScreen({ + screen: AddProjectRepositoryRoute, + linking: "add-project/repository", + }), + AddProjectDestination: createNativeStackScreen({ + screen: AddProjectDestinationRoute, + linking: "add-project/destination", + }), + AddProjectLocal: createNativeStackScreen({ + screen: AddProjectLocalRoute, + linking: "add-project/local", + }), + }, +}); + +function RootStackLayout(props: { + readonly children: React.ReactNode; + readonly state: NavigationState; +}) { + useAgentNotificationNavigation(); + useThreadOutboxDrain(); + const path = getPathFromState(props.state, navigationPathConfig); + const pathname = path.startsWith("/") ? path : `/${path}`; + + return ( + + + {props.children} + + + ); +} + +function NotFoundScreen() { + const navigation = useNavigation(); + 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 ( + + + Route not found + + navigation.dispatch(StackActions.replace("Home"))} + > + Return home + + + ); +} + +export const RootStack = createNativeStackNavigator({ + initialRouteName: "Home", + layout: RootStackLayout, + screenOptions: { + headerShown: false, + }, + screens: { + Home: createNativeStackScreen({ + screen: HomeRouteScreen, + linking: "", + options: { + ...GLASS_HEADER_OPTIONS, + contentStyle: { backgroundColor: "transparent" }, + headerBackVisible: false, + title: "Threads", + }, + }), + Thread: createNativeStackScreen({ + screen: ThreadRouteScreen, + linking: THREAD_LINKING_PREFIX, + options: GLASS_HEADER_OPTIONS, + }), + ThreadTerminal: createNativeStackScreen({ + screen: ThreadTerminalRouteScreen, + linking: `${THREAD_LINKING_PREFIX}/terminal`, + options: SOLID_HEADER_OPTIONS, + }), + ThreadReview: createNativeStackScreen({ + screen: ReviewSheet, + linking: `${THREAD_LINKING_PREFIX}/review`, + options: SOLID_HEADER_OPTIONS, + }), + ThreadReviewComment: createNativeStackScreen({ + screen: ReviewCommentComposerSheet, + linking: `${THREAD_LINKING_PREFIX}/review-comment`, + options: { + presentation: "formSheet", + sheetAllowedDetents: [0.55, 0.92], + sheetGrabberVisible: true, + }, + }), + ThreadFiles: createNativeStackScreen({ + screen: ThreadFilesTreeScreen, + linking: `${THREAD_LINKING_PREFIX}/files`, + options: { + ...GLASS_HEADER_OPTIONS, + contentStyle: + SHEET_BACKGROUND_COLOR !== undefined + ? { backgroundColor: SHEET_BACKGROUND_COLOR } + : undefined, + title: "Files", + }, + }), + ThreadFile: createNativeStackScreen({ + screen: ThreadFileScreen, + linking: `${THREAD_LINKING_PREFIX}/files/:path*`, + options: SOLID_HEADER_OPTIONS, + }), + GitOverview: createNativeStackScreen({ + screen: GitOverviewSheet, + linking: `${THREAD_LINKING_PREFIX}/git`, + options: { + presentation: "formSheet", + sheetAllowedDetents: [0.55, 0.92], + sheetGrabberVisible: true, + }, + }), + GitCommit: createNativeStackScreen({ + screen: GitCommitSheet, + linking: `${THREAD_LINKING_PREFIX}/git/commit`, + options: { + presentation: "formSheet", + sheetAllowedDetents: [0.55, 0.92], + sheetGrabberVisible: true, + }, + }), + GitBranches: createNativeStackScreen({ + screen: GitBranchesSheet, + linking: `${THREAD_LINKING_PREFIX}/git/branches`, + options: { + presentation: "formSheet", + sheetAllowedDetents: [0.55, 0.92], + sheetGrabberVisible: true, + }, + }), + GitConfirm: createNativeStackScreen({ + screen: GitConfirmSheet, + linking: `${THREAD_LINKING_PREFIX}/git-confirm`, + options: { + presentation: "formSheet", + sheetAllowedDetents: [0.45, 0.7], + sheetGrabberVisible: true, + }, + }), + SettingsSheet: createNativeStackScreen({ + screen: SettingsSheetStack, + linking: "settings", + options: { + gestureEnabled: true, + headerShown: false, + presentation: "formSheet", + sheetAllowedDetents: [0.7, 0.92], + sheetGrabberVisible: true, + }, + }), + Connections: createNativeStackScreen({ + screen: ConnectionsRouteScreen, + linking: "connections", + options: { + title: "Environments", + presentation: "formSheet", + sheetAllowedDetents: [0.55, 0.7], + sheetGrabberVisible: true, + }, + }), + ConnectionsNew: createNativeStackScreen({ + screen: ConnectionsNewRouteScreen, + linking: "connections/new", + options: { + presentation: "formSheet", + sheetAllowedDetents: [0.55, 0.7], + sheetGrabberVisible: true, + }, + }), + NewTaskSheet: createNativeStackScreen({ + screen: NewTaskSheetStack, + linking: "new", + // The whole new-task flow (choose project → draft → add project) shares + // draft state via NewTaskFlowProvider. The expo-router era mounted it in + // app/new/_layout.tsx; this layout wrapper is the native-stack equivalent. + layout: ({ children }) => {children}, + options: { + gestureEnabled: true, + headerShown: false, + presentation: "formSheet", + sheetAllowedDetents: [0.92], + sheetGrabberVisible: true, + }, + }), + NotFound: createNativeStackScreen({ + screen: NotFoundScreen, + linking: "*", + }), + }, +}); +type RootStackType = typeof RootStack; + +const navigationPathConfig = { + screens: createPathConfigForStaticNavigation(RootStack) ?? {}, +}; + +declare module "@react-navigation/native" { + interface RootNavigator extends RootStackType {} +} diff --git a/apps/mobile/src/components/AppText.tsx b/apps/mobile/src/components/AppText.tsx index d98a8573e6c..33bffb1f8cd 100644 --- a/apps/mobile/src/components/AppText.tsx +++ b/apps/mobile/src/components/AppText.tsx @@ -1,4 +1,3 @@ -import type { Ref } from "react"; import { Text as RNText, TextInput as RNTextInput, @@ -21,7 +20,7 @@ export function AppText({ className, ...props }: AppTextProps) { export type AppTextInputProps = RNTextInputProps & { readonly className?: string; - readonly ref?: Ref; + readonly ref?: React.Ref; }; /** @@ -40,7 +39,7 @@ export function AppTextInput({ void; + readonly variant?: "card" | "plain"; }) { + if (props.variant === "plain") { + return ( + + {props.title} + + {props.detail} + + {props.actionLabel && props.onAction ? ( + + + {props.actionLabel} + + + ) : null} + + ); + } + return ( {props.title} diff --git a/apps/mobile/src/features/agent-awareness/notificationNavigation.ts b/apps/mobile/src/features/agent-awareness/notificationNavigation.ts index 15ff1e09a83..e9ca9246b9a 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 { useAppNavigation } from "../../navigation/native-stack-header"; +import { useLinkTo } from "@react-navigation/native"; import { routeAgentNotificationResponseOnce } from "./notificationPayload"; import { consumeLastAgentNotificationResponse } from "./notificationResponseConsumer"; export function useAgentNotificationNavigation(): void { - const navigation = useAppNavigation(); + const linkTo = useLinkTo(); const handledResponseIds = useRef(new Set()); useEffect(() => { @@ -14,7 +14,7 @@ export function useAgentNotificationNavigation(): void { routeAgentNotificationResponseOnce({ handledResponseIds: handledResponseIds.current, response, - navigate: (deepLink) => navigation.push(deepLink as never), + navigate: linkTo, }); }; @@ -28,5 +28,5 @@ export function useAgentNotificationNavigation(): void { return () => { subscription.remove(); }; - }, [navigation]); + }, [linkTo]); } diff --git a/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsRouteScreen.tsx index 8c56962b101..c2381ef2580 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/native-stack-header"; +import { useFocusEffect } from "@react-navigation/native"; 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 f8e50eb4dbb..d6b93cf0d89 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -4,10 +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 { - NativeHeaderToolbar, - NativeStackScreenOptions, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { SymbolView } from "expo-symbols"; import { useCallback, useMemo, useRef, type ComponentProps } from "react"; import { @@ -26,15 +23,12 @@ 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,14 +108,10 @@ function ArchivedThreadsHeader(props: { return ( <> + {/* Static header config (glass preset + title) lives in Stack.tsx; only + dynamic toolbar/search wiring is set here. */} [ createNativeMailSearchToolbarItem({ diff --git a/apps/mobile/src/features/cloud/dpop.ts b/apps/mobile/src/features/cloud/dpop.ts index 0bd4b7ff1bd..eabf0dd79b6 100644 --- a/apps/mobile/src/features/cloud/dpop.ts +++ b/apps/mobile/src/features/cloud/dpop.ts @@ -8,12 +8,7 @@ import * as Schema from "effect/Schema"; import * as ExpoCrypto from "expo-crypto"; import * as SecureStore from "expo-secure-store"; import { p256 } from "@noble/curves/nist"; -import { - computeDpopAccessTokenHash, - computeDpopJwkThumbprint, - DpopPublicJwk, - normalizeDpopHtu, -} from "@t3tools/shared/dpop"; +import { DpopPublicJwk, normalizeDpopHtu } from "@t3tools/shared/dpopCommon"; import * as Layer from "effect/Layer"; export class CloudDpopError extends Data.TaggedError("CloudDpopError")<{ @@ -107,6 +102,40 @@ function sha256Digest( ); } +function base64UrlSha256( + data: Uint8Array, + message: string, +): Effect.Effect { + return sha256Digest(data, message).pipe(Effect.map(Encoding.encodeBase64Url)); +} + +function dpopThumbprintInput(jwk: DpopPublicJwk): string { + return JSON.stringify({ + crv: jwk.crv, + kty: jwk.kty, + x: jwk.x, + y: jwk.y, + }); +} + +function computeDpopJwkThumbprintEffect( + jwk: DpopPublicJwk, +): Effect.Effect { + return base64UrlSha256( + new TextEncoder().encode(dpopThumbprintInput(jwk)), + "Could not hash the DPoP public key thumbprint.", + ); +} + +function computeDpopAccessTokenHashEffect( + accessToken: string, +): Effect.Effect { + return base64UrlSha256( + new TextEncoder().encode(accessToken), + "Could not hash the DPoP access token.", + ); +} + function secureRandomBytes( byteCount: number, message: string, @@ -153,7 +182,7 @@ export function generateDpopProofKeyPair(): Effect.Effect< try: () => publicJwkFromUncompressedPublicKey(p256.getPublicKey(privateKey, false)), catch: cloudDpopError("Generated DPoP public key is invalid."), }); - const thumbprint = computeDpopJwkThumbprint(publicJwk); + const thumbprint = yield* computeDpopJwkThumbprintEffect(publicJwk); return { privateJwk: privateJwkFromPrivateKey(privateKey, publicJwk), publicJwk, @@ -189,9 +218,10 @@ export function loadOrCreateDpopProofKeyPair(): Effect.Effect< }, catch: cloudDpopError("Stored DPoP proof key is invalid."), }); + const thumbprint = yield* computeDpopJwkThumbprintEffect(restored.publicJwk); return { ...restored, - thumbprint: computeDpopJwkThumbprint(restored.publicJwk), + thumbprint, }; } const generated = yield* generateDpopProofKeyPair(); @@ -243,7 +273,9 @@ export function createDpopProof(input: { Effect.map(Encoding.encodeBase64Url), Effect.mapError(cloudDpopError("Could not encode DPoP proof header.")), ); - const ath = input.accessToken ? computeDpopAccessTokenHash(input.accessToken) : null; + const ath = input.accessToken + ? yield* computeDpopAccessTokenHashEffect(input.accessToken) + : null; const payload = yield* encodeDpopJwtPayloadJson({ htm: input.method.toUpperCase(), htu, diff --git a/apps/mobile/src/screens/connections/new.tsx b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx similarity index 90% rename from apps/mobile/src/screens/connections/new.tsx rename to apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx index a08fd9b164c..7fe43755697 100644 --- a/apps/mobile/src/screens/connections/new.tsx +++ b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx @@ -1,10 +1,6 @@ import { CameraView, useCameraPermissions } from "expo-camera"; -import { - NativeHeaderToolbar, - NativeStackScreenOptions, - useRouteParams, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; +import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { AsyncResult } from "effect/unstable/reactivity"; import { useCallback, useEffect, useState } from "react"; import { Alert, ScrollView, View } from "react-native"; @@ -13,21 +9,26 @@ import { useThemeColor } from "../../lib/useThemeColor"; import { AppText as Text, AppTextInput as TextInput } from "../../components/AppText"; import { ErrorBanner } from "../../components/ErrorBanner"; -import { dismissRoute } from "../../lib/routes"; -import { ConnectionSheetButton } from "../../features/connection/ConnectionSheetButton"; -import { extractPairingUrlFromQrPayload } from "../../features/connection/pairing"; +import { ConnectionSheetButton } from "./ConnectionSheetButton"; +import { extractPairingUrlFromQrPayload } from "./pairing"; import { useRemoteConnections } from "../../state/use-remote-environment-registry"; -import { buildPairingUrl, parsePairingUrl } from "../../features/connection/pairing"; +import { buildPairingUrl, parsePairingUrl } from "./pairing"; -export default function ConnectionsNewRouteScreen() { +type ConnectionsNewRouteParams = { + readonly mode?: string; +}; + +export function ConnectionsNewRouteScreen({ + route, +}: StaticScreenProps) { const { connectionPairingUrl, onChangeConnectionPairingUrl, onConnectPress, pairingConnectionError, } = useRemoteConnections(); - const navigation = useAppNavigation(); - const params = useRouteParams<{ mode?: string }>(); + const navigation = useNavigation(); + const params = route.params ?? {}; const insets = useSafeAreaInsets(); const [hostInput, setHostInput] = useState(""); const [codeInput, setCodeInput] = useState(""); @@ -36,6 +37,7 @@ export default function ConnectionsNewRouteScreen() { const [cameraPermission, requestCameraPermission] = useCameraPermissions(); const [scannerLocked, setScannerLocked] = useState(false); + const headerIconColor = useThemeColor("--color-icon"); const placeholderColor = useThemeColor("--color-placeholder"); const connectDisabled = isSubmitting || hostInput.trim().length === 0; @@ -121,7 +123,11 @@ export default function ConnectionsNewRouteScreen() { onChangeConnectionPairingUrl(pairingUrl); const result = await onConnectPress(pairingUrl); if (AsyncResult.isSuccess(result)) { - dismissRoute(navigation); + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.dispatch(StackActions.replace("Home")); + } } else { setIsSubmitting(false); } @@ -145,6 +151,7 @@ export default function ConnectionsNewRouteScreen() { } }} separateBackground + tintColor={headerIconColor} /> diff --git a/apps/mobile/src/screens/connections/index.tsx b/apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx similarity index 85% rename from apps/mobile/src/screens/connections/index.tsx rename to apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx index 15084bfc8af..90a24d88287 100644 --- a/apps/mobile/src/screens/connections/index.tsx +++ b/apps/mobile/src/features/connection/ConnectionsRouteScreen.tsx @@ -1,8 +1,5 @@ -import { - NativeHeaderToolbar, - NativeStackScreenOptions, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar } from "../../native/StackHeader"; +import { useNavigation } from "@react-navigation/native"; import { SymbolView } from "expo-symbols"; import type { EnvironmentId } from "@t3tools/contracts"; import { useCallback, useState } from "react"; @@ -12,18 +9,17 @@ 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"; +import { ConnectionEnvironmentRow } from "./ConnectionEnvironmentRow"; -export default function ConnectionsRouteScreen() { +export function ConnectionsRouteScreen() { const { connectedEnvironments, onReconnectEnvironment, onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const hasEnvironments = connectedEnvironments.length > 0; const [expandedId, setExpandedId] = useState(null); @@ -36,15 +32,10 @@ export default function ConnectionsRouteScreen() { return ( - navigation.push(connectionsNewNavigation())} + onPress={() => navigation.navigate("ConnectionsNew")} separateBackground /> diff --git a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts index 9ac85b27157..551e577b294 100644 --- a/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts +++ b/apps/mobile/src/features/diffs/nativeReviewDiffSurface.ts @@ -122,7 +122,9 @@ export interface NativeReviewDiffViewProps extends ViewProps { readonly rowHeight: number; readonly contentWidth: number; readonly initialRowIndex?: number; + readonly refreshing?: boolean; readonly nativeViewRef?: Ref; + readonly onPullToRefresh?: (event: NativeSyntheticEvent>) => void; readonly onDebug?: (event: NativeSyntheticEvent>) => void; readonly onVisibleFileChange?: ( event: NativeSyntheticEvent<{ readonly fileId?: string | null }>, diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx index ce762ab184e..e7579dfb155 100644 --- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx +++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx @@ -1,11 +1,11 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useMemo, useState } from "react"; import { Markdown, type CustomRenderers, type NodeStyleOverrides, type PartialMarkdownTheme, } from "react-native-nitro-markdown"; -import { ScrollView, Text as NativeText, View } from "react-native"; +import { RefreshControl, ScrollView, Text as NativeText, View } from "react-native"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; @@ -142,14 +142,40 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { ]); } -export function FileMarkdownPreview(props: { readonly markdown: string }) { +export function FileMarkdownPreview(props: { + readonly markdown: string; + readonly onRefresh?: () => Promise | void; +}) { + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + const handlePullToRefresh = useCallback(async () => { + if (!props.onRefresh) { + return; + } + setIsPullRefreshing(true); + try { + await props.onRefresh(); + } finally { + setIsPullRefreshing(false); + } + }, [props.onRefresh]); const styles = useMarkdownPreviewStyles(); const onLinkPress = useCallback((href: string) => { void tryOpenExternalUrl(href, "markdown-link"); }, []); return ( - + void handlePullToRefresh()} + /> + ) : undefined + } + > {hasNativeSelectableMarkdownText() ? ( (null); + const insets = useSafeAreaInsets(); + // Native transparent-header height ≈ safe-area top + nav bar (~44). Matches the + // observed adjustedContentInset bottom (~102) seen in the native trace. + const headerInset = Platform.OS === "ios" ? insets.top + 44 : 0; const iconColor = String(useThemeColor("--color-icon-muted")); const { onPreviewFile, onSelectFile, selectedPath: controlledSelectedPath } = props; const controlledSelectedPathRef = useRef(controlledSelectedPath); @@ -218,47 +230,54 @@ export function FileTreeBrowser(props: { [expandedPaths, handleSelectFile, iconColor, onPreviewFile, selectedPath, toggleDirectory], ); + if (props.error && props.entries.length === 0) { + return ( + + Files unavailable + {props.error} + + ); + } + + // SPIKE: render the FlatList as the screen's DIRECT content (no wrapping View), and + // mirror the Home ScrollView exactly — `contentInsetAdjustmentBehavior: "automatic"` + // with NO manual contentInset. iOS only applies the nav-bar top inset + scroll-edge + // blur to a scroll view in the screen's primary position; a scroll view buried in + // flex-1 Views is ignored, which is why the tree rendered under the header with no blur. return ( - - {props.error && props.entries.length === 0 ? ( + item.node.path} + contentInsetAdjustmentBehavior={Platform.OS === "ios" ? "automatic" : "never"} + scrollIndicatorInsets={ + Platform.OS === "ios" ? { top: headerInset, left: 0, right: 0, bottom: 0 } : undefined + } + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="handled" + initialNumToRender={FILE_TREE_INITIAL_RENDER_COUNT} + maxToRenderPerBatch={FILE_TREE_RENDER_BATCH_SIZE} + updateCellsBatchingPeriod={16} + windowSize={5} + contentContainerStyle={{ paddingTop: 8, paddingBottom: 8 }} + refreshControl={} + renderItem={renderItem} + ListEmptyComponent={ - Files unavailable - {props.error} + {props.isPending ? ( + + ) : ( + <> + No files found + + {props.searchQuery.trim().length > 0 + ? "Try a different search." + : "The workspace file index is empty."} + + + )} - ) : ( - item.node.path} - 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={renderItem} - ListEmptyComponent={ - - {props.isPending ? ( - - ) : ( - <> - No files found - - {props.searchQuery.trim().length > 0 - ? "Try a different search." - : "The workspace file index is empty."} - - - )} - - } - /> - )} - + } + /> ); } diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index d1644ae2a81..7e4e8160c96 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -1,7 +1,7 @@ import { useAtomValue } from "@effect/atom-react"; import { AsyncResult } from "effect/unstable/reactivity"; import type { ComponentType } from "react"; -import { memo, useCallback, useEffect, useMemo, useRef } from "react"; +import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { FlatList, ScrollView, Text as NativeText, useColorScheme, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; @@ -37,6 +37,8 @@ interface SourceFileSurfaceProps { readonly contents: string; readonly path: string; readonly initialLine?: number | null; + /** Enables native pull-to-refresh on the source surface. */ + readonly onRefresh?: () => Promise | void; } type SourceHighlightStatus = "highlighting" | "ready" | "error"; @@ -155,8 +157,20 @@ function NativeSourceFileSurface( readonly NativeView: ComponentType; }, ) { - const { NativeView } = props; + const { NativeView, onRefresh } = props; const { rowsJson, status, targetIndex, theme, tokens } = useSourceFileModel(props); + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + const handlePullToRefresh = useCallback(async () => { + if (!onRefresh) { + return; + } + setIsPullRefreshing(true); + try { + await onRefresh(); + } finally { + setIsPullRefreshing(false); + } + }, [onRefresh]); const tokensJson = useMemo(() => JSON.stringify(buildNativeSourceTokens(tokens)), [tokens]); const selectedRowIdsJson = useMemo( () => JSON.stringify(targetIndex === null ? [] : [nativeSourceRowId(targetIndex)]), @@ -165,7 +179,7 @@ function NativeSourceFileSurface( const themeJson = useMemo(() => JSON.stringify(createNativeReviewDiffTheme(theme)), [theme]); return ( - + void handlePullToRefresh(), + } + : {})} /> ); @@ -213,7 +233,7 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { ); return ( - + void; -}) { - const iconColor = String( - useThemeColor(props.active ? "--color-primary-foreground" : "--color-icon-muted"), - ); - - return ( - - - - {props.label} - - - ); -} - -function BreadcrumbFade(props: { readonly color: string; readonly side: "left" | "right" }) { - const gradientId = `file-breadcrumb-${props.side}-fade`; - const isLeft = props.side === "left"; - - return ( - - - - - - - - - - - - ); -} - -function FileBreadcrumbs(props: { readonly projectName: string; readonly relativePath: string }) { - const iconColor = String(useThemeColor("--color-icon-muted")); - const cardColor = String(useThemeColor("--color-card")); - const scrollMetrics = useRef({ contentWidth: 0, offsetX: 0, viewportWidth: 0 }); - const [fadeVisibility, setFadeVisibility] = useState({ left: false, right: false }); - const breadcrumbs = useMemo( - () => fileBreadcrumbs(props.projectName, props.relativePath), - [props.projectName, props.relativePath], - ); - const updateFadeVisibility = useCallback( - (metrics: Partial<(typeof scrollMetrics)["current"]>) => { - Object.assign(scrollMetrics.current, metrics); - const { contentWidth, offsetX, viewportWidth } = scrollMetrics.current; - const maxOffset = Math.max(0, contentWidth - viewportWidth); - const next = { - left: maxOffset > 1 && offsetX > 1, - right: maxOffset > 1 && offsetX < maxOffset - 1, - }; - - setFadeVisibility((current) => - current.left === next.left && current.right === next.right ? current : next, - ); - }, - [], - ); - - return ( - - { - updateFadeVisibility({ contentWidth }); - }} - onLayout={(event) => { - updateFadeVisibility({ viewportWidth: event.nativeEvent.layout.width }); - }} - onScroll={(event) => { - updateFadeVisibility({ offsetX: event.nativeEvent.contentOffset.x }); - }} - scrollEventThrottle={16} - > - - {breadcrumbs.map((crumb, index) => ( - - {index > 0 ? ( - - ) : null} - - {crumb.label} - - - ))} - - - {fadeVisibility.left ? : null} - {fadeVisibility.right ? : null} - - ); -} - -function FilePreviewHeader(props: { - readonly activeMode: FileViewMode; - readonly showModeSelector: boolean; - readonly externalPreviewUri?: string | null; - readonly projectName: string; - readonly relativePath: string; - readonly onSetMode: (mode: FileViewMode) => void; -}) { - const iconColor = String(useThemeColor("--color-icon-muted")); - - return ( - - - - - - {props.showModeSelector ? ( - - props.onSetMode("preview")} - /> - props.onSetMode("source")} - /> - {props.externalPreviewUri !== undefined ? ( - { - if (typeof props.externalPreviewUri === "string") { - void tryOpenExternalUrl(props.externalPreviewUri, "file-preview"); - } - }} - > - - - ) : null} - - ) : null} - - ); -} - function FileContent(props: { readonly activeMode: FileViewMode; readonly previewUri: string | null; @@ -294,6 +95,7 @@ function FileContent(props: { readonly relativePath: string; readonly initialLine: number | null; readonly truncated: boolean; + readonly onRefresh?: () => Promise | void; }) { const isMarkdown = isMarkdownPreviewFile(props.relativePath); const isBrowserFile = isBrowserPreviewFile(props.relativePath); @@ -317,7 +119,7 @@ function FileContent(props: { if (props.fileError && props.fileContents === null) { return ( - + ); @@ -325,7 +127,7 @@ function FileContent(props: { if (props.fileContents === null) { return ( - + Loading file... @@ -333,7 +135,7 @@ function FileContent(props: { } return ( - + {props.truncated ? ( @@ -345,23 +147,35 @@ function FileContent(props: { ) : null} {props.activeMode === "preview" && isMarkdown ? ( - + ) : ( )} ); } -function useThreadFilesWorkspace() { - const params = useRouteParams<{ - environmentId?: string | string[]; - threadId?: string | string[]; - }>(); +type ThreadFilesRouteScreenProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; +}>; + +type ThreadFileRouteScreenProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; + readonly path: string[]; + readonly line?: string; +}>; + +function useThreadFilesWorkspace(params: { + readonly environmentId?: string | string[]; + readonly threadId?: string | string[]; +}) { const routeEnvironmentId = firstRouteParam(params.environmentId); const routeThreadId = firstRouteParam(params.threadId); const { selectedThread, selectedThreadProject } = useThreadSelection(); @@ -397,40 +211,6 @@ function FilesUnavailable() { ); } -function FilesHeaderTitle(props: { readonly projectName: string }) { - const foregroundColor = String(useThemeColor("--color-foreground")); - const secondaryForegroundColor = String(useThemeColor("--color-foreground-secondary")); - - return ( - - - Files - - - {props.projectName} - - - ); -} - function FilesToolbarBottomFade() { const sheetColor = String(useThemeColor("--color-sheet")); @@ -466,15 +246,17 @@ function FilesToolbarBottomFade() { ); } -export function ThreadFilesTreeScreen() { +export function ThreadFilesTreeScreen(props: ThreadFilesRouteScreenProps) { useAdaptiveWorkspacePaneRole("inspector"); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const { fileInspector, layout, panes, showAuxiliaryPane, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); const [searchQuery, setSearchQuery] = useState(""); const colorScheme = useColorScheme(); const highlightTheme = colorScheme === "dark" ? "dark" : "light"; - const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(); + const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace( + props.route.params, + ); const revealedInspectorRef = useRef(false); const entriesQuery = useEnvironmentQuery( environmentId !== null && cwd !== null && !fileInspector.supported @@ -487,11 +269,16 @@ export function ThreadFilesTreeScreen() { const entriesData = entriesQuery.data as ProjectListEntriesResult | null; const handleReturnToThread = useCallback(() => { if (navigation.canGoBack()) { - navigation.back(); + navigation.goBack(); return; } if (environmentId !== null && threadId !== null) { - navigation.replace(threadNavigation({ environmentId, threadId })); + navigation.dispatch( + StackActions.replace("Thread", { + environmentId: String(environmentId), + threadId: String(threadId), + }), + ); } }, [environmentId, navigation, threadId]); @@ -500,15 +287,19 @@ export function ThreadFilesTreeScreen() { if (environmentId === null || threadId === null) { return; } - const destination = buildThreadFilesNavigation({ environmentId, threadId }, path); + const params = { + environmentId: String(environmentId), + threadId: String(threadId), + path: path.split("/").filter((segment) => segment.length > 0), + }; const navigationAction = resolveFileSelectionNavigationAction({ hasPersistentFileInspector: fileInspector.supported, }); if (navigationAction === "replace") { - navigation.replace(destination); + navigation.dispatch(StackActions.replace("ThreadFile", params)); return; } - navigation.push(destination); + navigation.navigate("ThreadFile", params); }, [environmentId, fileInspector.supported, navigation, threadId], ); @@ -540,10 +331,6 @@ export function ThreadFilesTreeScreen() { }, [cwd, environmentId, highlightTheme], ); - const renderHeaderTitle = useCallback( - () => , - [projectName], - ); useEffect(() => { if (fileInspector.supported && cwd !== null && !revealedInspectorRef.current) { revealedInspectorRef.current = true; @@ -557,6 +344,7 @@ export function ThreadFilesTreeScreen() { ); } @@ -572,6 +360,7 @@ export function ThreadFilesTreeScreen() { ); } @@ -579,23 +368,17 @@ export function ThreadFilesTreeScreen() { const usesCompactMailToolbar = Platform.OS === "ios" && !layout.usesSplitView; return ( - + <> + {/* Static header config (glass preset, title, contentStyle) lives in Stack.tsx. + Only genuinely dynamic options are set here. */} 0 ? projectName : undefined, + // No refresh button: the list already supports pull-to-refresh. unstable_headerToolbarItems: usesCompactMailToolbar ? () => [ createNativeMailSearchToolbarItem({ - composeButtonId: "files-refresh", - composeSystemImageName: "arrow.clockwise", - onComposePress: entriesQuery.refresh, onSearchTextChange: setSearchQuery, placeholder: "Search files", searchTextChangeId: "files-search-text", @@ -630,15 +413,6 @@ export function ThreadFilesTreeScreen() { /> ) : null} - {!usesCompactMailToolbar ? ( - - - - ) : null} {usesCompactMailToolbar ? null : ( @@ -655,22 +429,21 @@ export function ThreadFilesTreeScreen() { onSelectFile={handleSelectFile} /> - + ); } -export function ThreadFileScreen() { +export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { useAdaptiveWorkspacePaneRole("inspector"); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const { fileInspector, panes, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); const iconColor = useThemeColor("--color-icon"); - const params = useRouteParams<{ - line?: string | string[]; - path?: string | string[]; - }>(); + const params = props.route.params; const relativePath = normalizeRoutePath(params.path); const targetLine = normalizeRouteLine(firstRouteParam(params.line)); - const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace(); + const { cwd, environmentId, projectName, selectedThread, threadId } = useThreadFilesWorkspace( + props.route.params, + ); const [modeOverride, setModeOverride] = useState<{ readonly path: string; readonly mode: FileViewMode; @@ -711,15 +484,13 @@ export function ThreadFileScreen() { 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. - navigation.setParams({ - line: undefined, + navigation.navigate("ThreadFile", { + environmentId: String(environmentId), + threadId: String(threadId), path: path.split("/").filter(Boolean), }); }, - [navigation], + [environmentId, navigation, threadId], ); const renderInspector = useCallback( (headerInset: number) => @@ -753,19 +524,22 @@ export function ThreadFileScreen() { ); } + const parentDir = relativePath.split("/").slice(0, -1).join("/"); + const headerSubtitle = [projectName, parentDir].filter(Boolean).join(" · "); + return ( 0 ? headerSubtitle : undefined, }} /> @@ -774,7 +548,12 @@ export function ThreadFileScreen() { accessibilityLabel="Return to chat" icon="chevron.left" onPress={() => { - navigation.replace(threadNavigation({ environmentId, threadId })); + navigation.dispatch( + StackActions.replace("Thread", { + environmentId: String(environmentId), + threadId: String(threadId), + }), + ); }} /> ) : null} @@ -790,31 +569,56 @@ export function ThreadFileScreen() { separateBackground /> ) : null} - { - if (resolvedActiveMode === "preview" && (isBrowserFile || isImageFile)) { - setPreviewRevision((current) => current + 1); - return; - } - fileQuery.refresh(); - }} - /> + + {canPreview && !isImageFile ? ( + + setModeOverride({ path: relativePath, mode: "preview" })} + > + Preview + + setModeOverride({ path: relativePath, mode: "source" })} + > + Source + + + ) : null} + copyTextWithHaptic(relativePath)} + > + Copy path + + {isBrowserFile && typeof assetPreviewUri === "string" ? ( + { + void tryOpenExternalUrl(assetPreviewUri, "file-preview"); + }} + > + Open in Safari + + ) : null} + {resolvedActiveMode === "preview" && (isBrowserFile || isImageFile) ? ( + { + setPreviewRevision((current) => current + 1); + }} + > + Refresh + + ) : null} + renderInspector(0) : undefined} > - { - setModeOverride({ path: relativePath, mode }); - }} - /> fileQuery.refresh()} /> 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 5af3e6b6063..f0fa6920c27 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 { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; import { useThemeColor } from "../../lib/useThemeColor"; import { projectEnvironment } from "../../state/projects"; import { useEnvironmentQuery } from "../../state/query"; diff --git a/apps/mobile/src/features/home/HomeHeader.tsx b/apps/mobile/src/features/home/HomeHeader.tsx index 043c4c3d070..652dc02fccb 100644 --- a/apps/mobile/src/features/home/HomeHeader.tsx +++ b/apps/mobile/src/features/home/HomeHeader.tsx @@ -3,15 +3,12 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import { - NativeHeaderToolbar, - NativeStackScreenOptions, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; 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 { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; import { useThemeColor } from "../../lib/useThemeColor"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; @@ -59,21 +56,9 @@ export function HomeHeader(props: { <> [ diff --git a/apps/mobile/src/screens/index.tsx b/apps/mobile/src/features/home/HomeRouteScreen.tsx similarity index 56% rename from apps/mobile/src/screens/index.tsx rename to apps/mobile/src/features/home/HomeRouteScreen.tsx index 0eca8a62bbf..81920ff8232 100644 --- a/apps/mobile/src/screens/index.tsx +++ b/apps/mobile/src/features/home/HomeRouteScreen.tsx @@ -1,39 +1,24 @@ import * as Arr from "effect/Array"; import * as Order from "effect/Order"; -import { - NativeHeaderToolbar, - NativeStackScreenOptions, - useAppNavigation, -} from "../navigation/native-stack-header"; +import { useNavigation } from "@react-navigation/native"; import { useMemo, useState } from "react"; -import { useProjects, useThreadShells } from "../state/entities"; -import { useWorkspaceState } from "../state/workspace"; -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"; -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"; +import { useProjects, useThreadShells } from "../../state/entities"; +import { useWorkspaceState } from "../../state/workspace"; +import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; +import { HomeScreen } from "./HomeScreen"; +import { HomeHeader } from "./HomeHeader"; +import { useHomeListOptions } from "./home-list-options"; +import { useThreadListActions } from "./useThreadListActions"; /* ─── Route screen ───────────────────────────────────────────────────── */ -export default function HomeRouteScreen() { - const { layout } = useAdaptiveWorkspaceLayout(); +export function HomeRouteScreen() { const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const [searchQuery, setSearchQuery] = useState(""); const { archiveThread, confirmDeleteThread } = useThreadListActions(); const environments = useMemo( @@ -63,31 +48,6 @@ export default function HomeRouteScreen() { } = useHomeListOptions(availableEnvironmentIds); const selectedEnvironmentId = listOptions.selectedEnvironmentId; - if (layout.usesSplitView) { - return ( - <> - - navigation.push(newTaskNavigation())} - /> - } - /> - - - ); - } - return ( <> navigation.push(settingsNavigation())} + onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })} onProjectGroupingModeChange={setProjectGroupingMode} onProjectSortOrderChange={setProjectSortOrder} onSearchQueryChange={setSearchQuery} - onStartNewTask={() => navigation.push(newTaskNavigation())} + onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} onThreadSortOrderChange={setThreadSortOrder} /> navigation.push(connectionsNewNavigation())} + onAddConnection={() => + navigation.navigate("SettingsSheet", { screen: "SettingsEnvironmentNew" }) + } onArchiveThread={archiveThread} onDeleteThread={confirmDeleteThread} onEnvironmentChange={setSelectedEnvironmentId} - onOpenEnvironments={() => navigation.push(settingsEnvironmentsNavigation())} - onOpenSettings={() => navigation.push(settingsNavigation())} + onOpenEnvironments={() => + navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" }) + } + onOpenSettings={() => navigation.navigate("SettingsSheet", { screen: "Settings" })} onProjectGroupingModeChange={setProjectGroupingMode} onProjectSortOrderChange={setProjectSortOrder} onSearchQueryChange={setSearchQuery} onSelectThread={(thread) => { - navigation.push(threadNavigation(thread)); + navigation.navigate("Thread", { + environmentId: thread.environmentId, + threadId: thread.id, + }); }} - onStartNewTask={() => navigation.push(newTaskNavigation())} + onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} onThreadSortOrderChange={setThreadSortOrder} projectGroupingMode={listOptions.projectGroupingMode} projects={projects} diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 6be719944e1..2aa7b0a7c53 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -86,7 +86,6 @@ function statusColors(thread: EnvironmentThreadShell): { bg: string; fg: string } 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) { @@ -185,12 +184,13 @@ function ProjectGroupLabel(props: { readonly title: string; readonly totalThreadCount: number; readonly isExpanded: boolean; + readonly isFirst: boolean; readonly onToggleExpand: () => void; }) { const hiddenCount = props.totalThreadCount - COLLAPSED_THREAD_LIMIT; return ( - + >(() => new Set()); const openSwipeableRef = useRef(null); const insets = useSafeAreaInsets(); - const { height: windowHeight } = useWindowDimensions(); const accentColor = useThemeColor("--color-icon-muted"); const toggleExpanded = useCallback((key: string) => { setExpandedProjects((prev) => { @@ -430,6 +429,43 @@ export function HomeScreen(props: HomeScreenProps) { catalogState: props.catalogState, projectCount: props.projects.length, }); + const connectionStatus = + shouldShowConnectionStatus && Platform.OS !== "ios" ? ( + + + + ) : null; + + if (!hasAnyThreads) { + return ( + + + + {emptyState.loading ? ( + + + + ) : null} + + {connectionStatus} + + ); + } const listContent = ( <> @@ -445,21 +481,7 @@ export function HomeScreen(props: HomeScreenProps) { ) : null} - {!hasAnyThreads ? ( - - - {emptyState.loading ? ( - - - - ) : null} - - ) : !hasResults && hasSearchQuery ? ( + {!hasResults && hasSearchQuery ? ( ) : !hasResults && selectedEnvironmentLabel ? ( ) : ( - projectGroups.map((group) => { + projectGroups.map((group, groupIndex) => { const isExpanded = expandedProjects.has(group.key); const visibleThreads = isExpanded ? group.threads @@ -485,6 +507,7 @@ export function HomeScreen(props: HomeScreenProps) { > toggleExpanded(group.key)} project={group.representative} title={group.title} @@ -526,6 +549,7 @@ export function HomeScreen(props: HomeScreenProps) { const scrollView = ( ); - const connectionStatus = - shouldShowConnectionStatus && Platform.OS !== "ios" ? ( - - - - ) : null; - return ( <> {scrollView} diff --git a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx index 132a14cd873..96a4a63e901 100644 --- a/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx +++ b/apps/mobile/src/features/keyboard/HardwareKeyboardCommandProvider.tsx @@ -1,13 +1,6 @@ -import { useCurrentPathname, useAppNavigation } from "../../navigation/native-stack-header"; +import { StackActions, useNavigation } from "@react-navigation/native"; import { useCallback, useMemo, useSyncExternalStore, type PropsWithChildren } from "react"; -import { - buildThreadFilesNavigation, - buildThreadReviewNavigation, - buildThreadTerminalNavigation, - dismissRoute, - newTaskNavigation, -} from "../../lib/routes"; import { T3KeyboardCommands } from "../../native/T3KeyboardCommands"; import { dispatchHardwareKeyboardCommand, @@ -18,9 +11,11 @@ import { type HardwareKeyboardCommand, } from "./hardwareKeyboardCommands"; -export function HardwareKeyboardCommandProvider({ children }: PropsWithChildren) { - const pathname = useCurrentPathname(); - const navigation = useAppNavigation(); +export function HardwareKeyboardCommandProvider({ + children, + pathname, +}: PropsWithChildren<{ readonly pathname: string }>) { + const navigation = useNavigation(); const registrationVersion = useSyncExternalStore( subscribeToHardwareKeyboardCommandRegistrations, getHardwareKeyboardCommandRegistrationVersion, @@ -43,24 +38,28 @@ export function HardwareKeyboardCommandProvider({ children }: PropsWithChildren) if (dispatchHardwareKeyboardCommand(command)) return; if (command === "newTask") { - navigation.push(newTaskNavigation()); + navigation.navigate("NewTaskSheet", { screen: "NewTask" }); return; } if (command === "back") { - dismissRoute(navigation); + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.dispatch(StackActions.replace("Home")); + } return; } const thread = parseActiveThreadPath(pathname); if (!thread) return; if (command === "files" && !/\/files(?:\/|$)/.test(pathname)) { - navigation.push(buildThreadFilesNavigation(thread)); + navigation.navigate("ThreadFiles", thread); } if (command === "terminal" && !/\/terminal(?:\/|$)/.test(pathname)) { - navigation.push(buildThreadTerminalNavigation(thread)); + navigation.navigate("ThreadTerminal", thread); } if (command === "review" && !/\/review(?:\/|$)/.test(pathname)) { - navigation.push(buildThreadReviewNavigation(thread)); + navigation.navigate("ThreadReview", thread); } }, [pathname, navigation], diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 6c2ae88b683..7977e13d067 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,11 +1,7 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { - useFocusEffect, - useCurrentRouteParams, - useCurrentPathname, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { useFocusEffect } from "@react-navigation/native"; +import { StackActions, useNavigation } from "@react-navigation/native"; import { createContext, use, @@ -35,12 +31,6 @@ import { type WorkspacePaneLayout, } from "../../lib/layout"; import { resolveThreadSelectionNavigationAction } from "../../lib/adaptive-navigation"; -import { - buildThreadFilesNavigation, - newTaskNavigation, - settingsNavigation, - threadNavigation, -} from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { parseActiveThreadPath, @@ -86,10 +76,6 @@ const AdaptiveWorkspaceContext = createContext({ setAuxiliaryPaneWidth: () => undefined, }); -function firstRouteParam(value: string | string[] | undefined): string | null { - return Array.isArray(value) ? (value[0] ?? null) : (value ?? null); -} - export function useAdaptiveWorkspaceLayout(): AdaptiveWorkspaceContextValue { return use(AdaptiveWorkspaceContext); } @@ -102,10 +88,13 @@ export function useAdaptiveWorkspacePaneRole(role: WorkspaceAuxiliaryPaneRole) { ); } -export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) { +export function AdaptiveWorkspaceLayout(props: { + readonly children: ReactNode; + readonly pathname: string; +}) { const { width, height } = useWindowDimensions(); - const pathname = useCurrentPathname(); - const navigation = useAppNavigation(); + const pathname = props.pathname; + const navigation = useNavigation(); const activeRoleOwner = useRef(null); const [primarySidebarPreferredVisible, setPrimarySidebarPreferredVisible] = useState(true); const [supplementaryPanePreferredVisible, setSupplementaryPanePreferredVisible] = useState(true); @@ -119,12 +108,9 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const [primarySidebarSearchQuery, setPrimarySidebarSearchQuery] = useState(""); const [focusedAuxiliaryPaneRole, setFocusedAuxiliaryPaneRole] = useState(null); - const params = useCurrentRouteParams<{ - environmentId?: string | string[]; - threadId?: string | string[]; - }>(); const baseLayout = useMemo(() => deriveLayout({ width, height }), [height, width]); const layout = baseLayout; + const shouldRenderPrimarySidebar = layout.usesSplitView && pathname !== "/"; const fileInspector = useMemo( () => deriveFileInspectorPaneLayout({ @@ -132,9 +118,17 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) viewportWidth: width, preferredWidth: fileInspectorPreferredWidth ?? undefined, reservedLeadingWidth: - layout.usesSplitView && primarySidebarPreferredVisible ? (layout.listPaneWidth ?? 0) : 0, + shouldRenderPrimarySidebar && primarySidebarPreferredVisible + ? (layout.listPaneWidth ?? 0) + : 0, }), - [fileInspectorPreferredWidth, layout, primarySidebarPreferredVisible, width], + [ + fileInspectorPreferredWidth, + layout, + primarySidebarPreferredVisible, + shouldRenderPrimarySidebar, + width, + ], ); const auxiliaryPaneRole: WorkspaceAuxiliaryPaneRole = focusedAuxiliaryPaneRole ?? (/\/files(?:\/|$)/.test(pathname) ? "inspector" : "supplementary"); @@ -165,8 +159,9 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) width, ], ); - const environmentId = firstRouteParam(params.environmentId); - const threadId = firstRouteParam(params.threadId); + const activeThread = parseActiveThreadPath(pathname); + const environmentId = activeThread?.environmentId ?? null; + const threadId = activeThread?.threadId ?? null; const selectedThreadKey = useMemo(() => { if (environmentId === null || threadId === null) { return null; @@ -227,7 +222,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) if (/\/files(?:\/|$)/.test(pathname)) { return true; } - navigation.replace(buildThreadFilesNavigation(activeThread)); + navigation.navigate("ThreadFiles", activeThread); return true; }, [fileInspector.supported, layout.usesSplitView, pathname, navigation, showAuxiliaryPane]); useHardwareKeyboardCommand("files", handleOpenFilesCommand); @@ -276,10 +271,10 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) ); const handleOpenSettings = useCallback(() => { - navigation.push(settingsNavigation()); + navigation.navigate("SettingsSheet", { screen: "Settings" }); }, [navigation]); const handleStartNewTask = useCallback(() => { - navigation.push(newTaskNavigation()); + navigation.navigate("NewTaskSheet", { screen: "NewTask" }); }, [navigation]); const renderedSidebarWidth = useSharedValue( @@ -300,7 +295,10 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { - const destination = threadNavigation(thread); + const params = { + environmentId: String(thread.environmentId), + threadId: String(thread.id), + }; const navigationAction = resolveThreadSelectionNavigationAction({ usesSplitView: layout.usesSplitView, pathname, @@ -311,18 +309,15 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) return; } setFileInspectorPreferredVisible(false); - navigation.setParams({ - environmentId: String(thread.environmentId), - threadId: String(thread.id), - }); + navigation.navigate("Thread", params); return; } if (navigationAction === "replace") { setFileInspectorPreferredVisible(false); - navigation.replace(destination); + navigation.dispatch(StackActions.replace("Thread", params)); return; } - navigation.push(destination); + navigation.navigate("Thread", params); }, [layout.usesSplitView, pathname, navigation, selectedThreadKey], ); @@ -331,7 +326,7 @@ export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode }) - {layout.usesSplitView && layout.listPaneWidth !== null ? ( + {shouldRenderPrimarySidebar && layout.listPaneWidth !== null ? ( ( item: T, options: { + readonly hidesSharedBackground?: boolean; readonly sharesBackground?: boolean; readonly width?: number; } = {}, ): T { + const sharesBackground = options.sharesBackground ?? item.sharesBackground ?? true; return { ...item, - glassEffect: item.glassEffect ?? true, - sharesBackground: options.sharesBackground ?? item.sharesBackground ?? true, - variant: item.variant ?? "prominent", - ...(options.width !== undefined || item.width !== undefined - ? { width: options.width ?? item.width } - : {}), + glassEffect: item.glassEffect ?? false, + hidesSharedBackground: options.hidesSharedBackground ?? item.hidesSharedBackground ?? false, + sharesBackground, + variant: item.variant ?? "plain", + width: options.width ?? item.width, } as T; } diff --git a/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx b/apps/mobile/src/features/layout/workspace-sidebar-toolbar.tsx index b25f59f5b4a..ad53d75323c 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 { NativeHeaderToolbar } from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar } from "../../native/StackHeader"; import type { ReactNode } from "react"; import { useAdaptiveWorkspaceLayout } from "./AdaptiveWorkspaceLayout"; diff --git a/apps/mobile/src/features/projects/AddProjectDestinationRoute.tsx b/apps/mobile/src/features/projects/AddProjectDestinationRoute.tsx new file mode 100644 index 00000000000..04e2e236bea --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectDestinationRoute.tsx @@ -0,0 +1,15 @@ +import type { StaticScreenProps } from "@react-navigation/native"; +import { AddProjectDestinationScreen } from "./AddProjectScreen"; + +type AddProjectDestinationRouteParams = { + readonly environmentId?: string | string[]; + readonly source?: string | string[]; + readonly remoteUrl?: string | string[]; + readonly repositoryTitle?: string | string[]; +}; + +export function AddProjectDestinationRoute({ + route, +}: StaticScreenProps) { + return ; +} diff --git a/apps/mobile/src/features/projects/AddProjectLocalRoute.tsx b/apps/mobile/src/features/projects/AddProjectLocalRoute.tsx new file mode 100644 index 00000000000..abe7fb6d6c6 --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectLocalRoute.tsx @@ -0,0 +1,12 @@ +import type { StaticScreenProps } from "@react-navigation/native"; +import { AddProjectLocalFolderScreen } from "./AddProjectScreen"; + +type AddProjectLocalRouteParams = { + readonly environmentId?: string | string[]; +}; + +export function AddProjectLocalRoute({ + route, +}: StaticScreenProps) { + return ; +} diff --git a/apps/mobile/src/features/projects/AddProjectRepositoryRoute.tsx b/apps/mobile/src/features/projects/AddProjectRepositoryRoute.tsx new file mode 100644 index 00000000000..cdf52022a44 --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectRepositoryRoute.tsx @@ -0,0 +1,31 @@ +import type { StaticScreenProps } from "@react-navigation/native"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; +import { addProjectRemoteSourceLabel } from "@t3tools/client-runtime/operations/projects"; + +import { AddProjectRepositoryScreen } from "./AddProjectScreen"; + +type AddProjectRepositoryRouteParams = { + readonly environmentId?: string | string[]; + readonly source?: string | string[]; +}; + +export function AddProjectRepositoryRoute({ + route, +}: StaticScreenProps) { + const params = route.params ?? {}; + const source = Array.isArray(params.source) ? params.source[0] : params.source; + const title = + source === "github" || + source === "gitlab" || + source === "bitbucket" || + source === "azure-devops" + ? addProjectRemoteSourceLabel(source) + : "Git URL"; + + return ( + <> + + + + ); +} diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index 4b70aa9bbdf..af730307ab4 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 { useRouteParams, useAppNavigation } from "../../navigation/native-stack-header"; +import { StackActions, useNavigation } from "@react-navigation/native"; import { SymbolView } from "expo-symbols"; import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react"; import { ActivityIndicator, Alert, Pressable, ScrollView, View } from "react-native"; @@ -40,13 +40,6 @@ 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"; @@ -257,26 +250,22 @@ function useSelectedEnvironment(): { readonly selectedEnvironment: EnvironmentOption | null; readonly setSelectedEnvironmentId: (environmentId: EnvironmentId) => void; } { - const navigation = useAppNavigation(); - const params = useRouteParams<{ environmentId?: string }>(); + const [selectedEnvironmentId, setSelectedEnvironmentId] = useState(null); const environmentOptions = useEnvironmentOptions(); - const requestedEnvironmentId = stringParam(params.environmentId) as EnvironmentId | null; const selectedEnvironment = - environmentOptions.find( - (environment) => environment.environmentId === requestedEnvironmentId, - ) ?? + environmentOptions.find((environment) => environment.environmentId === selectedEnvironmentId) ?? environmentOptions[0] ?? null; return { environmentOptions, selectedEnvironment, - setSelectedEnvironmentId: (environmentId) => navigation.setParams({ environmentId }), + setSelectedEnvironmentId, }; } function EmptyEnvironmentState() { - const navigation = useAppNavigation(); + const navigation = useNavigation(); return ( @@ -285,7 +274,7 @@ function EmptyEnvironmentState() { Add an environment before adding a project. navigation.replace(connectionsNewNavigation())} + onPress={() => navigation.dispatch(StackActions.replace("ConnectionsNew"))} className="mt-1 rounded-full bg-primary px-4 py-2.5 active:opacity-70" > Add environment @@ -301,7 +290,7 @@ function SourceControlRow(props: { readonly hint: string; readonly isFirst: boolean; }) { - const navigation = useAppNavigation(); + const navigation = useNavigation(); const iconColor = useThemeColor("--color-icon"); const title = props.source === "url" ? "Git URL" : `${addProjectRemoteSourceLabel(props.source)} repository`; @@ -329,19 +318,20 @@ function SourceControlRow(props: { icon={icon} isFirst={props.isFirst} onPress={() => - navigation.push( - addProjectRepositoryNavigation({ + navigation.navigate("NewTaskSheet", { + screen: "AddProjectRepository", + params: { environmentId: props.selectedEnvironmentId, source: props.source, - }), - ) + }, + }) } /> ); } export function AddProjectSourceScreen() { - const navigation = useAppNavigation(); + const navigation = useNavigation(); const accentColor = useThemeColor("--color-icon-muted"); const iconColor = useThemeColor("--color-icon"); const { environmentOptions, selectedEnvironment, setSelectedEnvironmentId } = @@ -415,11 +405,12 @@ export function AddProjectSourceScreen() { } isFirst onPress={() => - navigation.push( - addProjectLocalNavigation({ + navigation.navigate("NewTaskSheet", { + screen: "AddProjectLocal", + params: { environmentId: selectedEnvironment.environmentId, - }), - ) + }, + }) } /> {(["url", ...sortAddProjectProviderSources(readiness)] as AddProjectRemoteSource[]).map( @@ -447,7 +438,7 @@ export function AddProjectSourceScreen() { } function useCreateProject(environment: EnvironmentOption | null) { - const navigation = useAppNavigation(); + const navigation = useNavigation(); const createProject = useAtomCommand(projectEnvironment.create, { reportFailure: false }); const projects = useProjects(); @@ -462,8 +453,8 @@ function useCreateProject(environment: EnvironmentOption | null) { }); if (existing) { Alert.alert("Project already exists", existing.title); - navigation.replace( - newTaskDraftNavigation({ + navigation.dispatch( + StackActions.replace("NewTaskDraft", { environmentId: existing.environmentId, projectId: existing.id, title: existing.title, @@ -486,8 +477,8 @@ function useCreateProject(environment: EnvironmentOption | null) { if (AsyncResult.isFailure(result)) { return result; } - navigation.replace( - newTaskDraftNavigation({ + navigation.dispatch( + StackActions.replace("NewTaskDraft", { environmentId: environment.environmentId, projectId, title: inferProjectTitleFromPath(workspaceRoot), @@ -499,10 +490,11 @@ function useCreateProject(environment: EnvironmentOption | null) { ); } -function useEnvironmentFromParam(): EnvironmentOption | null { - const params = useRouteParams<{ environmentId?: string }>(); +function useEnvironmentFromParam( + environmentIdParam: string | string[] | undefined, +): EnvironmentOption | null { const environmentOptions = useEnvironmentOptions(); - const environmentId = stringParam(params.environmentId) as EnvironmentId | null; + const environmentId = stringParam(environmentIdParam) as EnvironmentId | null; return ( environmentOptions.find((environment) => environment.environmentId === environmentId) ?? environmentOptions[0] ?? @@ -510,14 +502,16 @@ function useEnvironmentFromParam(): EnvironmentOption | null { ); } -export function AddProjectRepositoryScreen() { +export function AddProjectRepositoryScreen(props: { + readonly environmentId?: string | string[]; + readonly source?: string | string[]; +}) { const lookupRepositoryQuery = useAtomQueryRunner(sourceControlEnvironment.repository, { reportFailure: false, }); - const navigation = useAppNavigation(); - const params = useRouteParams<{ environmentId?: string; source?: string }>(); - const environment = useEnvironmentFromParam(); - const source = sourceFromParam(params.source); + const navigation = useNavigation(); + const environment = useEnvironmentFromParam(props.environmentId); + const source = sourceFromParam(props.source); const [repositoryInput, setRepositoryInput] = useState(""); const [isSubmitting, setIsSubmitting] = useState(false); const [error, setError] = useState(null); @@ -529,14 +523,15 @@ export function AddProjectRepositoryScreen() { const provider = addProjectRemoteSourceProvider(source); if (!provider) { const remoteUrl = repositoryInput.trim(); - navigation.push( - addProjectDestinationNavigation({ + navigation.navigate("NewTaskSheet", { + screen: "AddProjectDestination", + params: { environmentId: environment.environmentId, source, remoteUrl, repositoryTitle: remoteUrl, - }), - ); + }, + }); setIsSubmitting(false); return; } @@ -552,14 +547,15 @@ export function AddProjectRepositoryScreen() { setError(errorMessage(Cause.squash(result.cause))); } else { const repository = result.value; - navigation.push( - addProjectDestinationNavigation({ + navigation.navigate("NewTaskSheet", { + screen: "AddProjectDestination", + params: { environmentId: environment.environmentId, source, remoteUrl: repository.sshUrl, repositoryTitle: repository.nameWithOwner, - }), - ); + }, + }); } setIsSubmitting(false); }, [environment, isSubmitting, lookupRepositoryQuery, repositoryInput, navigation, source]); @@ -684,8 +680,8 @@ function FolderBrowser(props: { ); } -export function AddProjectLocalFolderScreen() { - const environment = useEnvironmentFromParam(); +export function AddProjectLocalFolderScreen(props: { readonly environmentId?: string | string[] }) { + const environment = useEnvironmentFromParam(props.environmentId); const createProject = useCreateProject(environment); const [pathInput, setPathInput] = useState(() => getAddProjectInitialQuery(environment?.baseDirectory), @@ -748,19 +744,18 @@ export function AddProjectLocalFolderScreen() { ); } -export function AddProjectDestinationScreen() { +export function AddProjectDestinationScreen(props: { + readonly environmentId?: string | string[]; + readonly remoteUrl?: string | string[]; + readonly repositoryTitle?: string | string[]; +}) { const cloneRepository = useAtomCommand(sourceControlEnvironment.cloneRepository, { reportFailure: false, }); - const params = useRouteParams<{ - environmentId?: string; - remoteUrl?: string; - repositoryTitle?: string; - }>(); - const environment = useEnvironmentFromParam(); + const environment = useEnvironmentFromParam(props.environmentId); const createProject = useCreateProject(environment); - const remoteUrl = stringParam(params.remoteUrl); - const repositoryTitle = stringParam(params.repositoryTitle); + const remoteUrl = stringParam(props.remoteUrl); + const repositoryTitle = stringParam(props.repositoryTitle); const [pathInput, setPathInput] = useState(() => getAddProjectInitialQuery(environment?.baseDirectory), ); diff --git a/apps/mobile/src/features/projects/AddProjectSourceRoute.tsx b/apps/mobile/src/features/projects/AddProjectSourceRoute.tsx new file mode 100644 index 00000000000..2dc85606be7 --- /dev/null +++ b/apps/mobile/src/features/projects/AddProjectSourceRoute.tsx @@ -0,0 +1,5 @@ +import { AddProjectSourceScreen } from "./AddProjectScreen"; + +export function AddProjectSourceRoute() { + return ; +} diff --git a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx index 3383b25c697..207c5d0170a 100644 --- a/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx +++ b/apps/mobile/src/features/review/ReviewCommentComposerSheet.tsx @@ -1,4 +1,4 @@ -import { useRouteParams, useAppNavigation } from "../../navigation/native-stack-header"; +import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { SymbolView } from "expo-symbols"; import { TextInputWrapper } from "expo-paste-input"; import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; @@ -40,17 +40,19 @@ import { const REVIEW_COMMENT_PREVIEW_MAX_LINES = 5; -export function ReviewCommentComposerSheet() { - const navigation = useAppNavigation(); +type ReviewCommentComposerSheetProps = StaticScreenProps<{ + readonly environmentId: EnvironmentId; + readonly threadId: ThreadId; +}>; + +export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProps) { + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const { width } = useWindowDimensions(); const colorScheme = useColorScheme(); const iconTint = String(useThemeColor("--color-icon")); const target = useReviewCommentTarget(); - const { environmentId, threadId } = useRouteParams<{ - environmentId: EnvironmentId; - threadId: ThreadId; - }>(); + const { environmentId, threadId } = props.route.params; const [commentText, setCommentText] = useState(""); const [highlightedLinesById, setHighlightedLinesById] = useState< Record> @@ -84,7 +86,7 @@ export function ReviewCommentComposerSheet() { const previewViewportWidth = Math.max(width - 40, 280); const dismissComposer = useCallback(() => { clearReviewCommentTarget(); - navigation.dismiss(); + navigation.goBack(); }, [navigation]); const handleNativePaste = useNativePaste((uris) => { void (async () => { diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 4b897951540..28f4dc06f67 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -1,10 +1,6 @@ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useRouteParams } from "../../navigation/native-stack-header"; -import { useHeaderHeight } from "@react-navigation/elements"; -import { - NativeHeaderToolbar, - NativeStackScreenOptions, -} from "../../navigation/native-stack-header"; +import type { StaticScreenProps } from "@react-navigation/native"; +import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { SymbolView } from "expo-symbols"; import { memo, @@ -24,7 +20,6 @@ import { Pressable, ScrollView, type NativeSyntheticEvent, - Text as NativeText, StyleSheet, useColorScheme, View, @@ -35,9 +30,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 { 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"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; import { AdaptiveInspectorLayout } from "../layout/adaptive-inspector-layout"; @@ -64,8 +57,6 @@ import { resolveReviewAvailability } from "./reviewAvailability"; import { resolveSelectedReviewFileId } from "./reviewPaneSelection"; import { buildReviewSectionMenu } from "./review-section-menu"; -const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); - const REVIEW_HEADER_SPACING = 0; const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string }) { @@ -290,143 +281,39 @@ function ReviewFileNavigator({ ); } -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} - - )} - - - ); -} +type ReviewSheetProps = StaticScreenProps<{ + readonly environmentId: EnvironmentId; + readonly threadId: ThreadId; +}>; -export function ReviewSheet() { +export function ReviewSheet(props: ReviewSheetProps) { 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")); const headerIcon = String(useThemeColor("--color-icon")); - const { environmentId, threadId } = useRouteParams<{ - environmentId: EnvironmentId; - threadId: ThreadId; - }>(); + const { environmentId, threadId } = props.route.params; const environment = useEnvironmentPresentation(environmentId); const retryEnvironment = useAtomCommand(environmentCatalog.retryNow, "environment retry"); const isEnvironmentReady = environment.presentation?.connection.phase === "connected"; const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); const reviewCache = useReviewCacheForThread({ environmentId, threadId }); const selectedTheme = colorScheme === "dark" ? "dark" : "light"; - const topContentInset = headerHeight; + // With a solid (non-overlay) header the content lays out below the header + // natively, so no manual top inset is needed. + const topContentInset = 0; useEffect(() => { showAuxiliaryPane("inspector"); }, [environmentId, showAuxiliaryPane, threadId]); - const { - error, - loadingGitDiffs, - loadingTurnIds, - reviewSections, - selectedSection, - refreshSelectedSection, - selectSection, - } = useReviewSections({ - enabled: isEnvironmentReady, - environmentId, - threadId, - reviewCache, - }); + const { error, reviewSections, selectedSection, refreshSelectedSection, selectSection } = + useReviewSections({ + enabled: isEnvironmentReady, + environmentId, + threadId, + reviewCache, + }); useReviewDiffPrewarming({ threadKey: reviewCache.threadKey, sections: reviewSections, @@ -440,6 +327,16 @@ export function ReviewSheet() { }); const NativeReviewDiffView = resolveNativeReviewDiffView()!; const nativeReviewDiffViewRef = useRef(null); + // Native pull-to-refresh on the diff surface (replaces the old Refresh menu item). + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + const handlePullToRefresh = useCallback(async () => { + setIsPullRefreshing(true); + try { + await refreshSelectedSection(); + } finally { + setIsPullRefreshing(false); + } + }, [refreshSelectedSection]); const reviewFileNavigatorRef = useRef(null); const reviewFiles = parsedDiff.kind === "files" ? parsedDiff.files : []; const fileVisibility = useReviewFileVisibility({ @@ -499,12 +396,13 @@ export function ReviewSheet() { ), - [handleSelectFile, headerHeight, nativeReviewDiffData.files, selectedSection?.id], + [handleSelectFile, nativeReviewDiffData.files, selectedSection?.id], ); const handleNativeToggleFile = useCallback( @@ -564,40 +462,29 @@ export function ReviewSheet() { return <>{children}; }, [error, parsedDiffNotice]); - const renderHeaderTitle = useCallback( - () => ( - - ), - [ - headerDiffSummary.additions, - headerDiffSummary.deletions, - headerForeground, - headerMuted, - pendingReviewCommentCount, - selectedSection?.title, - ], - ); + const headerSubtitle = [ + headerDiffSummary.additions, + headerDiffSummary.deletions, + pendingReviewCommentCount > 0 + ? `${pendingReviewCommentCount} comment${pendingReviewCommentCount === 1 ? "" : "s"}` + : null, + ] + .filter(Boolean) + .join(" · "); + const headerTitleText = selectedSection?.title ?? "Review changes"; return ( <> 0 ? headerSubtitle : undefined, }} /> @@ -627,7 +514,7 @@ export function ReviewSheet() { /> ) : null} {showSectionToolbar ? ( - + ) : null} - void refreshSelectedSection()} - subtitle="Reload current diff" - > - Refresh - ) : null} @@ -726,6 +602,8 @@ export function ReviewSheet() { void handlePullToRefresh()} style={StyleSheet.absoluteFill} appearanceScheme={selectedTheme} collapsedFileIdsJson={nativeBridge.collapsedFileIdsJson} diff --git a/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts b/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts index a38e0019369..a5b5f2de2f0 100644 --- a/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts +++ b/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts @@ -110,10 +110,12 @@ export function createNativeReviewDiffTheme( if (scheme === "dark") { return { - background: terminalTheme.background, + // Match the app surface (--color-sheet) so code views blend with the rest of + // the app instead of using a distinct code-editor background. + background: "#0e0e0e", text: terminalTheme.foreground, mutedText: terminalTheme.mutedForeground, - headerBackground: terminalTheme.background, + headerBackground: "#0e0e0e", border: terminalTheme.border, hunkBackground: "#071f28", hunkText: terminalBlue ?? "#009fff", @@ -127,10 +129,12 @@ export function createNativeReviewDiffTheme( } return { - background: "#ffffff", + // Match the app surface (--color-sheet) so code views blend with the rest of the + // app instead of using a distinct code-editor background. + background: "#f2f2f7", text: "#070707", mutedText: terminalTheme.mutedForeground, - headerBackground: "#ffffff", + headerBackground: "#f2f2f7", border: terminalTheme.border, hunkBackground: "#e0f2ff", hunkText: terminalBlue ?? "#009fff", diff --git a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts index e7d9e790de9..923369a5e9e 100644 --- a/apps/mobile/src/features/review/useReviewCommentSelectionController.ts +++ b/apps/mobile/src/features/review/useReviewCommentSelectionController.ts @@ -1,13 +1,11 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import type { NativeSyntheticEvent } from "react-native"; -import { useAppNavigation } from "../../navigation/native-stack-header"; +import { useNavigation } from "@react-navigation/native"; import * as Arr from "effect/Array"; 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, clearReviewCommentTarget, @@ -35,7 +33,7 @@ export function useReviewCommentSelectionController(input: { readonly nativeReviewDiffData: NativeReviewDiffData; }) { const { environmentId, nativeReviewDiffData, selectedSection, threadId } = input; - const navigation = useAppNavigation(); + const navigation = useNavigation(); const activeCommentTarget = useReviewCommentTarget(); const [pendingNativeCommentSelection, setPendingNativeCommentSelection] = useState(null); @@ -45,7 +43,10 @@ export function useReviewCommentSelectionController(input: { return; } - navigation.push(buildThreadReviewCommentNavigation({ environmentId, threadId })); + navigation.navigate("ThreadReviewComment", { + environmentId, + threadId, + }); }, [environmentId, navigation, threadId]); const selectedRowIds = useMemo(() => { diff --git a/apps/mobile/src/screens/settings/auth.tsx b/apps/mobile/src/features/settings/SettingsAuthRouteScreen.tsx similarity index 55% rename from apps/mobile/src/screens/settings/auth.tsx rename to apps/mobile/src/features/settings/SettingsAuthRouteScreen.tsx index c1199d9a336..f6f92f5fc84 100644 --- a/apps/mobile/src/screens/settings/auth.tsx +++ b/apps/mobile/src/features/settings/SettingsAuthRouteScreen.tsx @@ -1,17 +1,22 @@ import { useAuth } from "@clerk/expo"; import { AuthView, UserProfileView } from "@clerk/expo/native"; -import { NavigateTo, NativeStackScreenOptions } from "../../navigation/native-stack-header"; +import { StackActions, useNavigation } from "@react-navigation/native"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; +import { useEffect } from "react"; import { View } from "react-native"; -import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; -import { settingsNavigation } from "../../lib/routes"; +import { hasCloudPublicConfig } from "../cloud/publicConfig"; -export default function SettingsAuthRouteScreen() { - return hasCloudPublicConfig() ? ( - - ) : ( - - ); +export function SettingsAuthRouteScreen() { + const navigation = useNavigation(); + + useEffect(() => { + if (!hasCloudPublicConfig()) { + navigation.dispatch(StackActions.replace("Settings")); + } + }, [navigation]); + + return hasCloudPublicConfig() ? : null; } function ConfiguredSettingsAuthRouteScreen() { diff --git a/apps/mobile/src/screens/settings/environments.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx similarity index 94% rename from apps/mobile/src/screens/settings/environments.tsx rename to apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx index 3bfb01c827c..e252098de37 100644 --- a/apps/mobile/src/screens/settings/environments.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx @@ -1,9 +1,6 @@ import { useAuth } from "@clerk/expo"; -import { - NativeHeaderToolbar, - NativeStackScreenOptions, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar } from "../../native/StackHeader"; +import { useNavigation } from "@react-navigation/native"; import { SymbolView } from "expo-symbols"; import { connectionStatusText, @@ -26,27 +23,26 @@ import { AppText as Text } from "../../components/AppText"; import { type RelayEnvironmentView, useConnectionController, -} from "../../features/connection/useConnectionController"; -import { hasCloudPublicConfig } from "../../features/cloud/publicConfig"; -import { availableCloudEnvironmentPresentation } from "../../features/cloud/cloudEnvironmentPresentation"; -import { ConnectionEnvironmentRow } from "../../features/connection/ConnectionEnvironmentRow"; -import { ConnectionStatusDot } from "../../features/connection/ConnectionStatusDot"; -import { splitEnvironmentSections } from "../../features/connection/environmentSections"; +} from "../connection/useConnectionController"; +import { hasCloudPublicConfig } from "../cloud/publicConfig"; +import { availableCloudEnvironmentPresentation } from "../cloud/cloudEnvironmentPresentation"; +import { ConnectionEnvironmentRow } from "../connection/ConnectionEnvironmentRow"; +import { ConnectionStatusDot } from "../connection/ConnectionStatusDot"; +import { splitEnvironmentSections } from "../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"; -export default function SettingsEnvironmentsRouteScreen() { +export function SettingsEnvironmentsRouteScreen() { const { connectedEnvironments, onReconnectEnvironment, onRemoveEnvironmentPress, onUpdateEnvironment, } = useRemoteConnections(); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const { localEnvironments, connectedCloudEnvironments } = splitEnvironmentSections({ connectedEnvironments, @@ -55,6 +51,7 @@ export default function SettingsEnvironmentsRouteScreen() { const hasLocalEnvironments = localEnvironments.length > 0; const [expandedId, setExpandedId] = useState(null); const accentColor = useThemeColor("--color-icon-muted"); + const headerIconColor = useThemeColor("--color-icon"); const handleToggle = useCallback((environmentId: EnvironmentId) => { setExpandedId((prev) => (prev === environmentId ? null : environmentId)); @@ -62,16 +59,12 @@ export default function SettingsEnvironmentsRouteScreen() { return ( - navigation.push(settingsEnvironmentNewNavigation())} + onPress={() => navigation.navigate("SettingsSheet", { screen: "SettingsEnvironmentNew" })} separateBackground + tintColor={headerIconColor} /> - {layout.usesSplitView ? ( - - navigation.back()} - separateBackground - /> - - ) : null} + [ + withNativeGlassHeaderItem({ + accessibilityLabel: "Close settings", + icon: { name: "xmark", type: "sfSymbol" } as const, + identifier: "settings-close", + label: "", + onPress: () => navigation.goBack(), + type: "button", + }), + ] + : undefined, + }} + /> {hasCloudPublicConfig() ? : } ); @@ -75,7 +68,6 @@ function LocalSettingsRouteScreen() { return ( - @@ -106,7 +98,7 @@ function LocalSettingsRouteScreen() { function ConfiguredSettingsRouteScreen() { const insets = useSafeAreaInsets(); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const { expand: expandClerkSheet } = useClerkSettingsSheetDetent(); const { getToken, isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { user } = useUser(); @@ -217,7 +209,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: () => navigation.push(settingsWaitlistNavigation()) }, + { + text: "Continue", + onPress: () => navigation.navigate("SettingsSheet", { screen: "SettingsWaitlist" }), + }, ], ); }, [navigation]); @@ -347,16 +342,15 @@ function ConfiguredSettingsRouteScreen() { const openAccount = useCallback(() => { if (!isLoaded) return; if (!isSignedIn) { - navigation.push(settingsWaitlistNavigation()); + navigation.navigate("SettingsSheet", { screen: "SettingsWaitlist" }); return; } expandClerkSheet(); - navigation.push(settingsAuthNavigation()); + navigation.navigate("SettingsSheet", { screen: "SettingsAuth" }); }, [expandClerkSheet, isLoaded, isSignedIn, navigation]); return ( - - + ); } @@ -464,9 +458,10 @@ function SettingsRow(props: { readonly icon: SymbolName; readonly label: string; readonly value?: string; - readonly href?: ComponentProps["href"]; + readonly target?: "SettingsEnvironments" | "SettingsArchive"; readonly onPress?: () => void; }) { + const navigation = useNavigation(); const icon = useThemeColor("--color-icon"); const chevron = useThemeColor("--color-chevron"); const content = ( @@ -499,13 +494,21 @@ function SettingsRow(props: { ); - if (props.href) { + const target = props.target; + if (target) { return ( - - - {content} - - + + navigation.navigate("SettingsSheet", { + screen: target, + }) + } + > + {content} + ); } diff --git a/apps/mobile/src/screens/settings/waitlist.tsx b/apps/mobile/src/features/settings/SettingsWaitlistRouteScreen.tsx similarity index 52% rename from apps/mobile/src/screens/settings/waitlist.tsx rename to apps/mobile/src/features/settings/SettingsWaitlistRouteScreen.tsx index 057ea5eabe1..f5182307ebb 100644 --- a/apps/mobile/src/screens/settings/waitlist.tsx +++ b/apps/mobile/src/features/settings/SettingsWaitlistRouteScreen.tsx @@ -1,42 +1,41 @@ import { useAuth } from "@clerk/expo"; -import { - NavigateTo, - NativeStackScreenOptions, - useFocusEffect, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { StackActions, useFocusEffect, useNavigation } from "@react-navigation/native"; import { useCallback } from "react"; 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"; +import { CloudWaitlistEnrollment } from "../cloud/CloudWaitlistEnrollment"; +import { useClerkSettingsSheetDetent } from "../cloud/ClerkSettingsSheetDetent"; +import { hasCloudPublicConfig } from "../cloud/publicConfig"; -export default function SettingsWaitlistRouteScreen() { - return hasCloudPublicConfig() ? ( - - ) : ( - +export function SettingsWaitlistRouteScreen() { + const navigation = useNavigation(); + + useFocusEffect( + useCallback(() => { + if (!hasCloudPublicConfig()) { + navigation.dispatch(StackActions.replace("Settings")); + } + }, [navigation]), ); + + return hasCloudPublicConfig() ? : null; } function ConfiguredSettingsWaitlistRouteScreen() { const { isLoaded, isSignedIn } = useAuth({ treatPendingAsSignedOut: false }); const { expand } = useClerkSettingsSheetDetent(); - const navigation = useAppNavigation(); + const navigation = useNavigation(); useFocusEffect( useCallback(() => { if (isLoaded && isSignedIn) { - navigation.replace(settingsNavigation()); + navigation.dispatch(StackActions.replace("Settings")); } }, [isLoaded, isSignedIn, navigation]), ); return ( <> - { expand(); - navigation.push(settingsAuthNavigation()); + navigation.navigate("SettingsSheet", { screen: "SettingsAuth" }); }} /> diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 033270b4cc4..11d64968f13 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -1,12 +1,8 @@ import { DEFAULT_TERMINAL_ID, EnvironmentId, ThreadId } from "@t3tools/contracts"; import { type KnownTerminalSession } from "@t3tools/client-runtime/state/terminal"; import { SymbolView } from "expo-symbols"; -import { - NativeHeaderToolbar, - NativeStackScreenOptions, - useRouteParams, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; +import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { Platform, Pressable, Text as RNText, View, useColorScheme } from "react-native"; import { @@ -29,8 +25,6 @@ import { useEnvironmentPresentation } from "../../state/presentation"; import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useWorkspaceState } from "../../state/workspace"; -import { buildThreadTerminalNavigation } from "../../lib/routes"; -import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useAttachedTerminalSession, @@ -79,7 +73,6 @@ import { const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; const TERMINAL_ACCESSORY_HEIGHT = 52; -const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); type PendingModifier = "ctrl" | "meta"; type HostPlatform = "mac" | "linux" | "windows" | "unknown"; @@ -202,8 +195,14 @@ function pickRunningTerminalSessionForBootstrap( ); } -export function ThreadTerminalRouteScreen() { - const navigation = useAppNavigation(); +type ThreadTerminalRouteScreenProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; + readonly terminalId?: string; +}>; + +export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) { + const navigation = useNavigation(); const writeTerminal = useAtomCommand(terminalEnvironment.write, "terminal write"); const resizeTerminal = useAtomCommand(terminalEnvironment.resize, "terminal resize"); const clearTerminal = useAtomCommand(terminalEnvironment.clear, "terminal clear"); @@ -211,11 +210,7 @@ export function ThreadTerminalRouteScreen() { const appearanceScheme = useColorScheme() === "light" ? "light" : "dark"; const { state: workspaceState } = useWorkspaceState(); const { layout, panes, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); - const params = useRouteParams<{ - environmentId?: string | string[]; - threadId?: string | string[]; - terminalId?: string | string[]; - }>(); + const params = props.route.params; const { selectedThread, selectedThreadProject, selectedEnvironmentConnection } = useThreadSelection(); const selectedThreadDetail = useSelectedThreadDetail(); @@ -613,8 +608,12 @@ export function ThreadTerminalRouteScreen() { if (!shouldRedirectToRunningTerminal || !selectedThread || !runningSession) { return; } - navigation.replace( - buildThreadTerminalNavigation(selectedThread, runningSession.target.terminalId), + navigation.dispatch( + StackActions.replace("ThreadTerminal", { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + terminalId: runningSession.target.terminalId, + }), ); }, [navigation, runningSession, selectedThread, shouldRedirectToRunningTerminal]); @@ -841,7 +840,13 @@ export function ThreadTerminalRouteScreen() { return; } - navigation.replace(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); + navigation.dispatch( + StackActions.replace("ThreadTerminal", { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + terminalId: nextTerminalId, + }), + ); }, [navigation, selectedThread, terminalId], ); @@ -851,14 +856,15 @@ export function ThreadTerminalRouteScreen() { return; } - navigation.replace( - buildThreadTerminalNavigation( - selectedThread, - nextOpenTerminalId({ + navigation.dispatch( + StackActions.replace("ThreadTerminal", { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + terminalId: nextOpenTerminalId({ listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), activeRouteTerminalId: terminalId, }), - ), + }), ); }, [navigation, selectedThread, terminalId, terminalMenuSessions]); @@ -969,26 +975,17 @@ export function ThreadTerminalRouteScreen() { <> 0 + ? headerTitle.bottomLine + : undefined, }} /> diff --git a/apps/mobile/src/screens/new/draft.tsx b/apps/mobile/src/features/threads/NewTaskDraftRouteScreen.tsx similarity index 50% rename from apps/mobile/src/screens/new/draft.tsx rename to apps/mobile/src/features/threads/NewTaskDraftRouteScreen.tsx index b32d601596d..e821a9d1bc2 100644 --- a/apps/mobile/src/screens/new/draft.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftRouteScreen.tsx @@ -1,13 +1,16 @@ -import { NativeStackScreenOptions, useRouteParams } from "../../navigation/native-stack-header"; +import type { StaticScreenProps } from "@react-navigation/native"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; -import { NewTaskDraftScreen } from "../../features/threads/NewTaskDraftScreen"; +import { NewTaskDraftScreen } from "./NewTaskDraftScreen"; -export default function NewTaskDraftRoute() { - const params = useRouteParams<{ - environmentId?: string | string[]; - projectId?: string | string[]; - title?: string | string[]; - }>(); +type NewTaskDraftRouteParams = { + readonly environmentId?: string | string[]; + readonly projectId?: string | string[]; + readonly title?: string | string[]; +}; + +export function NewTaskDraftRouteScreen({ route }: StaticScreenProps) { + const params = route.params ?? {}; return ( <> diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 55faf2228f7..482f223a80e 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -1,4 +1,5 @@ -import { NativeStackScreenOptions, useAppNavigation } from "../../navigation/native-stack-header"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; +import { StackActions, useNavigation } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { Alert, InteractionManager, View, useColorScheme } from "react-native"; import { KeyboardAvoidingView, useKeyboardState } from "react-native-keyboard-controller"; @@ -29,7 +30,6 @@ import { providerOptionsConfigurationLabel, resolveProviderOptionDescriptors, } from "../../lib/providerOptions"; -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 navigation = useAppNavigation(); + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const isKeyboardVisible = useKeyboardState((state) => state.isVisible); @@ -101,7 +101,7 @@ export function NewTaskDraftScreen(props: { return; } - navigation.replace(newTaskNavigation()); + navigation.dispatch(StackActions.replace("NewTask")); }, [ logicalProjects, projects, @@ -429,7 +429,12 @@ export function NewTaskDraftScreen(props: { flow.setPrompt(""); flow.clearAttachments(); - navigation.replace(threadNavigation(result.value)); + navigation.dispatch( + StackActions.replace("Thread", { + environmentId: String(result.value.environmentId), + threadId: String(result.value.threadId), + }), + ); } if (!selectedProject) { diff --git a/apps/mobile/src/screens/new/index.tsx b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx similarity index 69% rename from apps/mobile/src/screens/new/index.tsx rename to apps/mobile/src/features/threads/NewTaskRouteScreen.tsx index 64760e9d4ef..c4f7a54dec7 100644 --- a/apps/mobile/src/screens/new/index.tsx +++ b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx @@ -1,9 +1,5 @@ -import { - NavigationLink, - NativeHeaderToolbar, - NativeStackScreenOptions, - useAppNavigation, -} from "../../navigation/native-stack-header"; +import { NativeHeaderToolbar } from "../../native/StackHeader"; +import { useNavigation } from "@react-navigation/native"; import { SymbolView } from "expo-symbols"; import type { EnvironmentId, ProjectId } from "@t3tools/contracts"; import { useMemo } from "react"; @@ -17,12 +13,7 @@ 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"; +import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; function deriveProjectEmptyState(catalogState: WorkspaceState): { readonly title: string; @@ -79,11 +70,11 @@ function deriveProjectEmptyState(catalogState: WorkspaceState): { }; } -export default function NewTaskRoute() { +export function NewTaskRouteScreen() { const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); - const navigation = useAppNavigation(); + const navigation = useNavigation(); const { layout } = useAdaptiveWorkspaceLayout(); const insets = useSafeAreaInsets(); const chevronColor = useThemeColor("--color-chevron"); @@ -120,24 +111,24 @@ export default function NewTaskRoute() { return ( - {layout.usesSplitView ? ( navigation.dismiss()} + onPress={() => navigation.goBack()} separateBackground /> ) : null} navigation.push(addProjectNavigation())} + onPress={() => navigation.navigate("NewTaskSheet", { screen: "AddProject" })} separateBackground /> navigation.push(connectionsNewNavigation())} + onPress={() => navigation.navigate("ConnectionsNew")} > Add environment @@ -167,7 +158,7 @@ export default function NewTaskRoute() { ) : ( navigation.push(addProjectNavigation())} + onPress={() => navigation.navigate("NewTaskSheet", { screen: "AddProject" })} > Add new project @@ -182,49 +173,50 @@ export default function NewTaskRoute() { const isLast = index === items.length - 1; return ( - + navigation.navigate("NewTaskSheet", { + screen: "NewTaskDraft", + params: { + environmentId: item.environmentId, + projectId: item.id, + title: item.title, + }, + }) + } + style={{ + paddingHorizontal: 16, + paddingVertical: 14, + borderTopWidth: isFirst ? 0 : 1, + borderTopColor: borderSubtleColor, + borderTopLeftRadius: isFirst ? 24 : 0, + borderTopRightRadius: isFirst ? 24 : 0, + borderBottomLeftRadius: isLast ? 24 : 0, + borderBottomRightRadius: isLast ? 24 : 0, + }} > - - - - - - - {item.title} - - + + - - + + {item.title} + + + + ); })} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index 6f576df5c3b..a0b493153e5 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -82,6 +82,12 @@ export interface ThreadComposerProps { readonly connectionState: RemoteClientConnectionState; readonly connectionError: string | null; readonly environmentLabel: string | null; + /** + * Message sync phase for the selected thread (drives the status pill): + * "loading" = first fetch, nothing to show yet; "syncing" = cached messages + * are on screen while they reconcile with the server. + */ + readonly threadSyncPhase?: "loading" | "syncing" | null; readonly selectedThread: OrchestrationThreadShell; readonly serverConfig: T3ServerConfig | null; readonly queueCount: number; @@ -141,11 +147,17 @@ function ComposerSurface(props: { ); } +type ComposerStatusPillState = { + readonly kind: "unavailable" | "reconnecting" | "syncing"; + readonly label: string; +}; + function composerConnectionStatus(input: { readonly connectionError: string | null; readonly connectionState: RemoteClientConnectionState; readonly environmentLabel: string | null; -}): { readonly kind: "unavailable" | "reconnecting"; readonly label: string } | null { + readonly threadSyncPhase?: "loading" | "syncing" | null; +}): ComposerStatusPillState | null { const environmentLabel = input.environmentLabel ?? "Environment"; switch (input.connectionState) { @@ -170,15 +182,27 @@ function composerConnectionStatus(input: { case "available": return { kind: "unavailable", label: `${environmentLabel} is not connected` }; case "connected": + break; + } + + // Connected: the pill is the single loading/sync indicator. One stable + // label per open — "Loading" when starting from scratch, "Syncing" when + // cached messages are already visible. + switch (input.threadSyncPhase) { + case "loading": + return { kind: "syncing", label: "Loading messages..." }; + case "syncing": + return { kind: "syncing", label: "Syncing messages..." }; + default: return null; } } const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill(props: { readonly onPress: () => void; - readonly status: { readonly kind: "unavailable" | "reconnecting"; readonly label: string }; + readonly status: ComposerStatusPillState; }) { - const isReconnecting = props.status.kind === "reconnecting"; + const isReconnecting = props.status.kind !== "unavailable"; return ( @@ -257,6 +281,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer connectionError: props.connectionError, connectionState: props.connectionState, environmentLabel: props.environmentLabel, + threadSyncPhase: props.threadSyncPhase, }); const toolbarFadeOpaque = isDarkMode ? "rgba(0,0,0,0.95)" : "rgba(255,255,255,0.95)"; const toolbarFadeTransparent = isDarkMode ? "rgba(0,0,0,0)" : "rgba(255,255,255,0)"; diff --git a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx index cc69d5552aa..81c3b9d0076 100644 --- a/apps/mobile/src/features/threads/ThreadDetailScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadDetailScreen.tsx @@ -1,4 +1,5 @@ import { type EnvironmentConnectionPhase } from "@t3tools/client-runtime/connection"; +import type { EnvironmentThreadStatus } from "@t3tools/client-runtime/state/threads"; import { useKeyboardChatComposerInset, useKeyboardScrollToEnd } from "@legendapp/list/keyboard"; import type { LegendListRef } from "@legendapp/list/react-native"; import type { @@ -15,7 +16,6 @@ import type { } from "@t3tools/contracts"; import { formatElapsed } from "@t3tools/shared/orchestrationTiming"; import * as Haptics from "expo-haptics"; -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"; @@ -62,6 +62,8 @@ export interface ThreadDetailScreenProps { readonly draftMessage: string; readonly draftAttachments: ReadonlyArray; readonly connectionStateLabel: EnvironmentConnectionPhase; + /** Message sync status for the selected thread (drives the composer status pill). */ + readonly threadSyncStatus?: EnvironmentThreadStatus; readonly activeThreadBusy: boolean; readonly environmentId: EnvironmentId; readonly projectWorkspaceRoot: string | null; @@ -69,7 +71,6 @@ 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; @@ -210,7 +211,6 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const { onOpenDrawer } = props; const insets = useSafeAreaInsets(); - const headerHeight = useHeaderHeight(); const agentLabel = `${props.selectedThread.modelSelection.instanceId} agent`; const selectedThreadKey = scopedThreadKey(props.environmentId, props.selectedThread.id); const composerEditorRef = useRef(null); @@ -223,6 +223,22 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread const [anchorMessageId, setAnchorMessageId] = useState(null); const composerBottomInset = composerExpanded ? 0 : Math.max(insets.bottom, 12); const contentPresentationKind = props.contentPresentation.kind; + // The raw sync status enters "synchronizing" on every full fetch, cached or + // not. Whether messages are already on screen decides the pill label: no + // data yet → "Loading messages", cached data reconciling → "Syncing". + const threadSyncPhase = (() => { + switch (props.threadSyncStatus) { + case "empty": + case "cached": + case "synchronizing": + if (contentPresentationKind === "ready") { + return "syncing" as const; + } + return contentPresentationKind === "loading" ? ("loading" as const) : null; + default: + return null; + } + })(); const selectedThreadFeed = props.selectedThreadFeed; const composerChrome = composerExpanded ? COMPOSER_EXPANDED_CHROME : COMPOSER_COLLAPSED_CHROME; const composerOverlapHeight = composerChrome + composerBottomInset; @@ -238,9 +254,6 @@ 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( @@ -389,7 +402,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread freeze={freeze} anchorMessageId={anchorMessageId} contentInsetEndAdjustment={contentInsetEndAdjustment} - contentTopInset={props.usesAutomaticContentInsets ? nativeHeaderContentTopInset : 0} + contentTopInset={0} contentBottomInset={estimatedOverlayHeight} contentMaxWidth={contentMaxWidth} layoutVariant={layoutVariant} @@ -451,6 +464,7 @@ export const ThreadDetailScreen = memo(function ThreadDetailScreen(props: Thread connectionState={props.connectionStateLabel} connectionError={props.connectionError} environmentLabel={props.environmentLabel} + threadSyncPhase={threadSyncPhase} selectedThread={props.selectedThread} serverConfig={props.serverConfig} queueCount={props.selectedThreadQueueCount} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index fbc6c25de5f..4821a49234c 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 { useAppNavigation } from "../../navigation/native-stack-header"; +import { useNavigation } from "@react-navigation/native"; import { memo, useCallback, @@ -39,7 +39,6 @@ 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"; @@ -69,8 +68,6 @@ import { import { buildReviewParsedDiff } from "../review/reviewModel"; import { cn } from "../../lib/cn"; import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout"; -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"; import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links"; @@ -88,8 +85,6 @@ const MESSAGE_TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour: "numeric", minute: "2-digit", }); -const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); - function formatMessageTime(input: string): string { const timestamp = Date.parse(input); if (Number.isNaN(timestamp)) { @@ -1097,7 +1092,6 @@ function ThreadFeedPlaceholder(props: { readonly bottomInset: number; readonly detail: string; readonly horizontalPadding: number; - readonly loading?: boolean; readonly title: string; readonly topInset: number; }) { @@ -1114,7 +1108,6 @@ function ThreadFeedPlaceholder(props: { }} > - {props.loading ? : null} {props.title} {props.detail} @@ -1125,7 +1118,7 @@ function ThreadFeedPlaceholder(props: { } export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { - const navigation = useAppNavigation(); + const navigation = useNavigation(); const copyFeedbackTimeoutRef = useRef | null>(null); const foldSettleFrameRef = useRef(null); const foldSettleSecondFrameRef = useRef(null); @@ -1180,13 +1173,12 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ); if (relativePath) { void Haptics.selectionAsync(); - navigation.push( - buildThreadFilesNavigation( - { environmentId: props.environmentId, threadId: props.threadId }, - relativePath, - presentation.line, - ), - ); + navigation.navigate("ThreadFile", { + environmentId: String(props.environmentId), + threadId: String(props.threadId), + path: relativePath.split("/").filter((segment) => segment.length > 0), + ...(presentation.line ? { line: String(presentation.line) } : {}), + }); } return; } @@ -1473,19 +1465,6 @@ export const ThreadFeed = memo(function ThreadFeed(props: ThreadFeedProps) { ], ); - if (props.contentPresentation.kind === "loading") { - return ( - - ); - } - if (props.contentPresentation.kind === "unavailable") { return ( - + - - {props.feed.length === 0 ? ( + + {props.feed.length === 0 && props.contentPresentation.kind === "ready" ? ( void; @@ -107,11 +103,9 @@ type ThreadGitControlsProps = { }; function useThreadGitControlModel(props: ThreadGitControlsProps) { - const navigation = useAppNavigation(); - const { environmentId, threadId } = useRouteParams<{ - environmentId: EnvironmentId; - threadId: ThreadId; - }>(); + const navigation = useNavigation(); + const environmentId = props.environmentId; + const threadId = props.threadId; const { gitStatus, gitOperationLabel, onPull, onRunAction } = props; const currentBranchLabel = gitStatus?.refName ?? props.currentBranch ?? "Detached HEAD"; @@ -175,18 +169,15 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { !input.featureBranch && requiresDefaultBranchConfirmation(input.action, isDefaultRef) ) { - navigation.push( - buildGitConfirmNavigation( - { environmentId, threadId }, - { - confirmAction: confirmableAction, - branchName, - includesCommit: String( - input.action === "commit_push" || input.action === "commit_push_pr", - ), - }, + navigation.navigate("GitConfirm", { + environmentId: String(environmentId), + threadId: String(threadId), + confirmAction: confirmableAction, + branchName, + includesCommit: String( + input.action === "commit_push" || input.action === "commit_push_pr", ), - ); + }); return; } @@ -214,11 +205,17 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { props.onOpenFilesInspector(); return; } - navigation.push(buildThreadFilesNavigation({ environmentId, threadId })); + navigation.navigate("ThreadFiles", { + environmentId: String(environmentId), + threadId: String(threadId), + }); }, [environmentId, props.onOpenFilesInspector, navigation, threadId]); const openReview = useCallback(() => { - navigation.push(buildThreadReviewNavigation({ environmentId, threadId })); + navigation.navigate("ThreadReview", { + environmentId: EnvironmentId.make(String(environmentId)), + threadId: ThreadId.make(String(threadId)), + }); }, [environmentId, navigation, threadId]); const openGitInspector = useCallback(() => { @@ -226,7 +223,10 @@ function useThreadGitControlModel(props: ThreadGitControlsProps) { props.onOpenGitInspector(); return; } - navigation.push(buildGitOverviewNavigation({ environmentId, threadId })); + navigation.navigate("GitOverview", { + environmentId: String(environmentId), + threadId: String(threadId), + }); }, [environmentId, props.onOpenGitInspector, navigation, threadId]); return { @@ -301,7 +301,7 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit }, sharesBackground: true, type: "menu", - variant: "prominent", + variant: "plain", }, files: { accessibilityLabel: "Open files", @@ -312,7 +312,7 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit onPress: model.openFiles, sharesBackground: true, type: "button", - variant: "prominent", + variant: "plain", }, git: { accessibilityLabel: "Git actions", @@ -348,17 +348,9 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit 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" }, + icon: { name: "ellipsis", type: "sfSymbol" }, label: "More", onPress: model.openGitInspector, type: "action", @@ -368,7 +360,7 @@ function useThreadGitHeaderActionItems(props: ThreadGitControlsProps): ThreadGit }, sharesBackground: true, type: "menu", - variant: "prominent", + variant: "plain", }, }), [ @@ -522,15 +514,7 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { Review changes - Files - - diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 1cf8d448dfe..812a5829904 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 { useAppNavigation } from "../../navigation/native-stack-header"; +import { useNavigation } from "@react-navigation/native"; 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"; @@ -14,8 +14,7 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; 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 { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -258,7 +257,7 @@ export function ThreadNavigationSidebar(props: { }) { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; - const navigation = useAppNavigation(); + const navigation = useNavigation(); const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); @@ -694,7 +693,11 @@ export function ThreadNavigationSidebar(props: { showsConnectionStatus ? ( navigation.push(settingsEnvironmentsNavigation())} + onPress={() => + navigation.navigate("SettingsSheet", { + screen: "SettingsEnvironments", + }) + } state={catalogState} variant="sidebar" /> @@ -871,7 +874,11 @@ export function ThreadNavigationSidebar(props: { {showsConnectionStatus ? ( navigation.push(settingsEnvironmentsNavigation())} + onPress={() => + navigation.navigate("SettingsSheet", { + screen: "SettingsEnvironments", + }) + } state={catalogState} variant="sidebar" /> diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 2d0c37a71f3..119e9b378a6 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -1,37 +1,16 @@ +import { NativeStackScreenOptions } from "../../native/StackHeader"; import { - NativeStackScreenOptions, + StackActions, useFocusEffect, - useRouteParams, - useAppNavigation, -} from "../../navigation/native-stack-header"; -import { - useCallback, - useEffect, - useMemo, - useRef, - useState, - type ComponentProps, - type ReactNode, -} from "react"; + useNavigation, + type StaticScreenProps, +} from "@react-navigation/native"; +import { useCallback, useEffect, useMemo, useRef, useState, 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, - type NativeSyntheticEvent, -} from "react-native"; -import { - Screen, - ScreenStack, - ScreenStackHeaderConfig, - ScreenStackHeaderSearchBarView, - SearchBar, - type SearchBarCommands, -} from "react-native-screens"; +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"; @@ -40,18 +19,9 @@ import { vcsEnvironment } from "../../state/vcs"; import { EmptyState } from "../../components/EmptyState"; import { LoadingScreen } from "../../components/LoadingScreen"; -import { - buildThreadFilesNavigation, - buildThreadTerminalNavigation, - connectionsNavigation, - homeNavigation, - newTaskNavigation, - threadNavigation, -} from "../../lib/routes"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { connectionTone } from "../connection/connectionTone"; -import { nativeHeaderScrollEdgeEffects } from "../../lib/native-scroll-edge-effect"; import { useRemoteConnections, @@ -107,13 +77,6 @@ interface ThreadInspectorSelection { } type NativeHeaderItems = ReadonlyArray>; -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"); @@ -132,7 +95,12 @@ function OpeningThreadLoadingScreen() { return ; } -interface ThreadRouteScreenProps { +type ThreadRouteScreenRouteProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; +}>; + +interface ThreadRouteScreenProps extends ThreadRouteScreenRouteProps { readonly onReturnToThread?: () => void; readonly renderInspector?: (headerInset: number) => ReactNode; } @@ -157,14 +125,11 @@ function ThreadUnavailableScreen() { ); } -export function ThreadRouteScreen(props: ThreadRouteScreenProps = {}) { +export function ThreadRouteScreen(props: ThreadRouteScreenProps) { const { state: workspaceState } = useWorkspaceState(); const { connectionState } = useRemoteConnectionStatus(); const { selectedThread } = useThreadSelection(); - const params = useRouteParams<{ - environmentId?: string | string[]; - threadId?: string | string[]; - }>(); + const params = props.route.params; const environmentIdRaw = firstRouteParam(params.environmentId); const threadIdRaw = firstRouteParam(params.threadId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; @@ -180,19 +145,16 @@ export function ThreadRouteScreen(props: ThreadRouteScreenProps = {}) { ? 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 ; } + // Render the full thread chrome (header, feed, composer) as soon as the + // thread SHELL is known — no blocking on message detail. The feed shows a + // loading placeholder while messages fetch, and the composer's connection + // pill reports connecting/reconnecting/syncing status. if (selectedThread !== null && selectedThreadKey === routeThreadKey) { - if (!hasThreadDetail && !hasTerminalDetailState) { - return ; - } return ; } @@ -208,47 +170,6 @@ export function ThreadRouteScreen(props: ThreadRouteScreenProps = {}) { return ; } -function ThreadHeaderTitle(props: { - readonly foregroundColor: string; - readonly secondaryForegroundColor: string; - readonly subtitle: string; - readonly title: string; -}) { - return ( - { - // TODO: trigger rename modal - }} - > - - {props.title} - - - {props.subtitle} - - - ); -} - function ThreadRouteContent( props: ThreadRouteScreenProps & { readonly selectedThreadDetailState: ReturnType; @@ -276,11 +197,8 @@ function ThreadRouteContent( const gitActions = useSelectedThreadGitActions(); const requests = useSelectedThreadRequests(); const interruptThreadTurn = useAtomCommand(threadEnvironment.interruptTurn, "thread interrupt"); - const navigation = useAppNavigation(); - const params = useRouteParams<{ - environmentId?: string | string[]; - threadId?: string | string[]; - }>(); + const navigation = useNavigation(); + const params = props.route.params; const [drawerVisible, setDrawerVisible] = useState(false); const environmentIdRaw = firstRouteParam(params.environmentId); const environmentId = environmentIdRaw ? EnvironmentId.make(environmentIdRaw) : null; @@ -363,10 +281,7 @@ function ThreadRouteContent( ); /* ─── Native header theming ──────────────────────────────────────── */ - 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 usesNativeHeaderGlass = Platform.OS === "ios"; const usesThreadSearchToolbar = Platform.OS === "ios" && layout.usesSplitView && inspectorMode === null; @@ -479,18 +394,28 @@ function ThreadRouteContent( if (selectedThread === null) { return; } - const destination = buildThreadFilesNavigation(selectedThread, path); + const params = { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + path: path.split("/").filter((segment) => segment.length > 0), + }; if (fileInspector.supported) { - navigation.replace(destination); + navigation.navigate("ThreadFile", params); return; } - navigation.push(destination); + navigation.navigate("ThreadFile", params); }, [fileInspector.supported, navigation, selectedThread], ); const GitInspector = useCallback( - () => , - [], + () => ( + + ), + [props.route.params], ); const FilesInspector = useCallback( () => @@ -522,7 +447,7 @@ function ThreadRouteContent( const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; const handleOpenConnectionEditor = useCallback(() => { - void navigation.push(connectionsNavigation()); + void navigation.navigate("Connections"); }, [navigation]); const handleStopThread = useCallback(() => { if ( @@ -555,7 +480,11 @@ function ThreadRouteContent( return; } - void navigation.push(buildThreadTerminalNavigation(selectedThread, nextTerminalId)); + void navigation.navigate("ThreadTerminal", { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + ...(nextTerminalId ? { terminalId: nextTerminalId } : {}), + }); }, [navigation, selectedThread, selectedThreadProject?.workspaceRoot], ); @@ -574,7 +503,11 @@ function ThreadRouteContent( const nextId = nextOpenTerminalId({ listedTerminalIds: terminalMenuSessions.map((session) => session.terminalId), }); - void navigation.push(buildThreadTerminalNavigation(selectedThread, nextId)); + void navigation.navigate("ThreadTerminal", { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + terminalId: nextId, + }); }, [navigation, selectedThread, selectedThreadProject?.workspaceRoot, terminalMenuSessions]); const handleRunProjectScript = useCallback( @@ -632,7 +565,11 @@ function ThreadRouteContent( worktreePath: preferredWorktreePath, }); - void navigation.push(buildThreadTerminalNavigation(selectedThread, targetTerminalId)); + void navigation.navigate("ThreadTerminal", { + environmentId: String(selectedThread.environmentId), + threadId: String(selectedThread.id), + terminalId: targetTerminalId, + }); }, [ navigation, @@ -643,6 +580,8 @@ function ThreadRouteContent( ], ); const threadGitControlProps = { + environmentId: environmentIdRaw ?? "", + threadId: threadId ?? "", auxiliaryPaneControl: !layout.usesSplitView && fileInspector.supported && selectedThreadCwd !== null ? { @@ -669,24 +608,6 @@ function ThreadRouteContent( }; 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 (navigation.canGoBack()) { - navigation.back(); - return; - } - navigation.replace(homeNavigation()); - }, - type: "button" as const, - }), - ], - [navigation], - ); const splitLeftHeaderItems = useMemo( () => [ { @@ -722,7 +643,7 @@ function ThreadRouteContent( accessibilityLabel: "New task", icon: { name: "square.and.pencil", type: "sfSymbol" as const }, identifier: "thread-left-new-task", - onPress: () => navigation.push(newTaskNavigation()), + onPress: () => navigation.navigate("NewTaskSheet", { screen: "NewTask" }), type: "button" as const, }), ], @@ -770,6 +691,7 @@ function ThreadRouteContent( draftMessage={composer.draftMessage} draftAttachments={composer.draftAttachments} connectionStateLabel={routeConnectionState} + threadSyncStatus={selectedThreadDetailState.status} activeThreadBusy={composer.activeThreadBusy} environmentId={selectedThread.environmentId} projectWorkspaceRoot={selectedThreadProject?.workspaceRoot ?? null} @@ -802,9 +724,14 @@ function ThreadRouteContent( selectedThreadKey={selectedThreadKey} onClose={() => setDrawerVisible(false)} onSelectThread={(thread) => { - navigation.replace(threadNavigation(thread)); + navigation.dispatch( + StackActions.replace("Thread", { + environmentId: String(thread.environmentId), + threadId: String(thread.id), + }), + ); }} - onStartNewTask={() => navigation.push(newTaskNavigation())} + onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} /> )} @@ -812,86 +739,11 @@ function ThreadRouteContent( ); - 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 ( <> {activeInspectorRenderer ? : null} null : selectedThread.title, headerTitleStyle: usesNativeHeaderGlass ? { @@ -901,8 +753,6 @@ function ThreadRouteContent( : undefined, title: layout.usesSplitView ? "" : selectedThread.title, headerBackVisible: !layout.usesSplitView, - headerBackTitle: "", - scrollEdgeEffects: HEADER_SCROLL_EDGE_EFFECTS, headerSearchBarOptions: usesThreadSearchToolbar ? { ref: threadSearchBarRef, @@ -920,29 +770,24 @@ function ThreadRouteContent( placement: "integratedButton", } : undefined, + // Compact uses the NATIVE back button (Thread lives flat in the root + // stack now, so a real previous route exists); only split view needs + // custom left items. unstable_headerLeftItems: - layout.usesSplitView && Platform.OS === "ios" ? () => splitLeftHeaderItems : undefined, + Platform.OS === "ios" && layout.usesSplitView ? () => splitLeftHeaderItems : undefined, unstable_headerCenterItems: layout.usesSplitView && Platform.OS === "ios" ? () => threadCenterHeaderItems : undefined, - unstable_headerSubtitle: undefined, - unstable_navigationItemStyle: usesNativeHeaderGlass ? "editor" : undefined, + unstable_headerRightItems: + !layout.usesSplitView && Platform.OS === "ios" + ? () => compactRightHeaderItems + : undefined, + unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined, }} /> - {layout.usesSplitView || usesNativeHeaderGlass ? null : ( - - - - )} - - {renderThreadRouteBody(!layout.usesSplitView)} + {renderThreadRouteBody(!layout.usesSplitView && !usesNativeHeaderGlass)} ); } diff --git a/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx b/apps/mobile/src/features/threads/git/GitBranchesSheet.tsx index ec5e2283ccf..2acf641753d 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 { useAppNavigation } from "../../../navigation/native-stack-header"; +import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useState } from "react"; import { Pressable, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -14,8 +14,13 @@ import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-wo import { vcsEnvironment } from "../../../state/vcs"; import { SheetActionButton } from "./gitSheetComponents"; -export function GitBranchesSheet() { - const navigation = useAppNavigation(); +type GitBranchesSheetProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; +}>; + +export function GitBranchesSheet(_props: GitBranchesSheetProps) { + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const { selectedThread } = useThreadSelection(); const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); @@ -96,7 +101,7 @@ export function GitBranchesSheet() { if (branch.length === 0) return; void gitActions.onCreateSelectedThreadBranch(branch).then(() => { setNewBranchName(""); - navigation.dismiss(); + navigation.goBack(); }); }} /> @@ -146,7 +151,7 @@ export function GitBranchesSheet() { if (baseBranch.length === 0 || newBranch.length === 0) return; void gitActions.onCreateSelectedThreadWorktree({ baseBranch, newBranch }).then(() => { setWorktreeBranchName(""); - navigation.dismiss(); + navigation.goBack(); }); }} /> @@ -188,7 +193,7 @@ export function GitBranchesSheet() { }} onPress={() => { void gitActions.onCheckoutSelectedThreadBranch(branch.name).then(() => { - navigation.dismiss(); + navigation.goBack(); }); }} > diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index 7a093a565fc..a2620300be7 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -1,4 +1,4 @@ -import { useAppNavigation } from "../../../navigation/native-stack-header"; +import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useState } from "react"; import { Pressable, ScrollView, View, useColorScheme } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -13,8 +13,13 @@ import { useSelectedThreadWorktree } from "../../../state/use-selected-thread-wo import { vcsEnvironment } from "../../../state/vcs"; import { SheetActionButton } from "./gitSheetComponents"; -export function GitCommitSheet() { - const navigation = useAppNavigation(); +type GitCommitSheetProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; +}>; + +export function GitCommitSheet(_props: GitCommitSheetProps) { + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const isDarkMode = useColorScheme() === "dark"; const { selectedThread } = useThreadSelection(); @@ -55,7 +60,7 @@ export function GitCommitSheet() { const runCommitAction = useCallback( async (featureBranch: boolean) => { const commitMessage = dialogCommitMessage.trim(); - navigation.dismiss(); + navigation.goBack(); await gitActions.onRunSelectedThreadGitAction({ action: "commit", featureBranch, diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index df2a7514fdf..7a9199ff9f2 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 { useRouteParams, useAppNavigation } from "../../../navigation/native-stack-header"; +import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useMemo } from "react"; import { View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; @@ -12,19 +12,23 @@ import { useSelectedThreadGitActions } from "../../../state/use-selected-thread- import { useSelectedThreadGitState } from "../../../state/use-selected-thread-git-state"; import { SheetActionButton } from "./gitSheetComponents"; -export function GitConfirmSheet() { - const navigation = useAppNavigation(); +type GitConfirmSheetProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; + readonly confirmAction?: string; + readonly branchName?: string; + readonly includesCommit?: string; + readonly commitMessage?: string; + readonly filePaths?: string; +}>; + +export function GitConfirmSheet(props: GitConfirmSheetProps) { + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const gitState = useSelectedThreadGitState(); const gitActions = useSelectedThreadGitActions(); - const params = useRouteParams<{ - confirmAction?: string; - branchName?: string; - includesCommit?: string; - commitMessage?: string; - filePaths?: string; - }>(); + const params = props.route.params; const confirmAction = params.confirmAction as | "push" @@ -34,6 +38,8 @@ export function GitConfirmSheet() { | undefined; const branchName = params.branchName ?? ""; const includesCommit = params.includesCommit === "true"; + const environmentId = params.environmentId ?? ""; + const threadId = params.threadId ?? ""; const copy = useMemo( () => @@ -49,17 +55,17 @@ export function GitConfirmSheet() { const continuePendingAction = useCallback(async () => { if (!confirmAction) return; - navigation.dismissAll(); + navigation.dispatch(StackActions.replace("Thread", { environmentId, threadId })); await gitActions.onRunSelectedThreadGitAction({ action: confirmAction, ...(params.commitMessage ? { commitMessage: params.commitMessage } : {}), ...(params.filePaths ? { filePaths: params.filePaths.split(",") } : {}), }); - }, [confirmAction, gitActions, params, navigation]); + }, [confirmAction, environmentId, gitActions, params, navigation, threadId]); const movePendingActionToFeatureBranch = useCallback(async () => { if (!confirmAction) return; - navigation.dismissAll(); + navigation.dispatch(StackActions.replace("Thread", { environmentId, threadId })); if (includesCommit) { await gitActions.onRunSelectedThreadGitAction({ @@ -89,6 +95,8 @@ export function GitConfirmSheet() { includesCommit, params, navigation, + environmentId, + threadId, ]); return ( diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 5e0c1c4a3fe..7a652cb1b22 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -4,24 +4,18 @@ import { getGitActionDisabledReason, requiresDefaultBranchConfirmation, } from "@t3tools/client-runtime/state/vcs"; -import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import { useRouteParams, useAppNavigation } from "../../../navigation/native-stack-header"; +import { EnvironmentId, ThreadId } from "@t3tools/contracts"; +import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { SymbolView } from "expo-symbols"; -import { useCallback, useEffect, useMemo, type ComponentProps } from "react"; -import { Alert, Platform, Pressable, ScrollView, View } from "react-native"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import { Alert, Platform, Pressable, RefreshControl, 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 { nativeHeaderScrollEdgeEffects } from "../../../lib/native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects } from "../../../native/StackHeader"; import { tryOpenExternalUrl } from "../../../lib/openExternalUrl"; -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"; @@ -32,20 +26,21 @@ import { MetaCard, SheetListRow, menuItemIconName, statusSummary } from "./gitSh const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); -export function GitOverviewSheet( - props: { - readonly headerInset?: number; - readonly presentation?: "sheet" | "inspector"; - } = {}, -) { - const navigation = useAppNavigation(); +type GitOverviewSheetProps = StaticScreenProps<{ + readonly environmentId: string; + readonly threadId: string; +}> & { + readonly headerInset?: number; + readonly presentation?: "sheet" | "inspector"; +}; + +export function GitOverviewSheet(props: GitOverviewSheetProps) { + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const presentation = props.presentation ?? "sheet"; const isInspector = presentation === "inspector"; - const { environmentId, threadId } = useRouteParams<{ - environmentId: EnvironmentId; - threadId: ThreadId; - }>(); + const environmentId = EnvironmentId.make(props.route.params.environmentId); + const threadId = ThreadId.make(props.route.params.threadId); const { selectedThread } = useThreadSelection(); const { selectedThreadCwd, selectedThreadWorktreePath } = useSelectedThreadWorktree(); const gitState = useSelectedThreadGitState(); @@ -124,23 +119,20 @@ export function GitOverviewSheet( !input.featureBranch && requiresDefaultBranchConfirmation(input.action, isDefaultRef) ) { - navigation.push( - buildGitConfirmNavigation( - { environmentId, threadId }, - { - confirmAction: confirmableAction, - branchName, - includesCommit: String( - input.action === "commit_push" || input.action === "commit_push_pr", - ), - }, + navigation.navigate("GitConfirm", { + environmentId: String(environmentId), + threadId: String(threadId), + confirmAction: confirmableAction, + branchName, + includesCommit: String( + input.action === "commit_push" || input.action === "commit_push_pr", ), - ); + }); return; } if (!isInspector) { - navigation.dismiss(); + navigation.goBack(); } await gitActions.onRunSelectedThreadGitAction(input); }, @@ -155,7 +147,10 @@ export function GitOverviewSheet( return; } if (item.dialogAction === "commit") { - navigation.push(buildGitCommitNavigation({ environmentId, threadId })); + navigation.navigate("GitCommit", { + environmentId: String(environmentId), + threadId: String(threadId), + }); return; } if (item.dialogAction === "push") { @@ -169,37 +164,59 @@ export function GitOverviewSheet( [environmentId, openExistingPr, navigation, 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], + // Status facts live on the relevant rows instead of crowding the header + // subtitle: files changed → Commit, ahead → Push, PR → View PR, behind → Pull. + const rowStatusDetail = useCallback( + (item: (typeof menuItems)[number]): string | undefined => { + const status = gitStatus.data; + if (status == null) { + return undefined; + } + if (item.dialogAction === "commit" && status.hasWorkingTreeChanges) { + const fileCount = status.workingTree?.files.length ?? 0; + return `${fileCount} file${fileCount === 1 ? "" : "s"} changed`; + } + if (item.dialogAction === "push" && (status.aheadCount ?? 0) > 0) { + const ahead = status.aheadCount ?? 0; + return `${ahead} commit${ahead === 1 ? "" : "s"} ahead`; + } + if (item.kind === "open_pr" && status.pr?.number != null) { + return `PR #${status.pr.number} ${status.pr.state ?? "open"}`; + } + return undefined; + }, + [gitStatus.data, menuItems], ); + const behindCount = gitStatus.data?.behindCount ?? 0; + + // Deterministic pull-to-refresh state. Tying RefreshControl to the query's + // isPending flag left the spinner stuck (the status query reports pending + // during quiet background refreshes too). + const [isPullRefreshing, setIsPullRefreshing] = useState(false); + const handlePullRefresh = useCallback(async () => { + setIsPullRefreshing(true); + try { + await gitActions.refreshSelectedThreadGitStatus(); + } finally { + setIsPullRefreshing(false); + } + }, [gitActions]); + const content = ( void handlePullRefresh()} /> + } > void onPressMenuItem(item)} /> ))} - {(gitStatus.data?.behindCount ?? 0) > 0 ? ( + {behindCount > 0 ? ( <> void gitActions.onPullSelectedThreadBranch()} /> @@ -240,7 +257,7 @@ export function GitOverviewSheet( title="Review changes" subtitle="Inspect turn diffs, worktree changes, and base branch diff" disabled={busy || !isRepo} - onPress={() => navigation.push(buildThreadReviewNavigation({ environmentId, threadId }))} + onPress={() => navigation.navigate("ThreadReview", { environmentId, threadId })} /> navigation.push(buildGitBranchesNavigation({ environmentId, threadId }))} + onPress={() => + navigation.navigate("GitBranches", { + environmentId: String(environmentId), + threadId: String(threadId), + }) + } /> @@ -272,11 +294,9 @@ export function GitOverviewSheet( + + + {content} + + + + + ); + } + return ( { - it("includes an optional source line in string routes", () => { - expect(buildThreadFilesRoutePath(thread, "src/main.ts", 42)).toBe( - "/threads/environment-1/thread-1/files/src/main.ts?line=42", - ); - }); - - it("encodes each file path segment without encoding separators", () => { - expect(buildThreadFilesRoutePath(thread, "docs/My File#1.md")).toBe( - "/threads/environment-1/thread-1/files/docs/My%20File%231.md", - ); - }); - - it("builds typed navigation params for a file and source line", () => { - expect(buildThreadFilesNavigation(thread, "src/main.ts", 42)).toEqual({ - name: "ThreadFile", - params: { - environmentId: "environment-1", - threadId: "thread-1", - path: ["src", "main.ts"], - line: "42", - }, - }); - }); - - it("targets the files index when no file path is provided", () => { - expect(buildThreadFilesNavigation(thread)).toEqual({ - name: "ThreadFiles", - params: { - environmentId: "environment-1", - threadId: "thread-1", - }, - }); - }); -}); - -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 deleted file mode 100644 index 37c358b8670..00000000000 --- a/apps/mobile/src/lib/routes.ts +++ /dev/null @@ -1,285 +0,0 @@ -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 Navigation = AppNavigation; - -type ThreadRouteInput = - | Pick - | Pick; -type PlainThreadRouteInput = - | { - environmentId: EnvironmentId; - threadId: ThreadId; - } - | { - environmentId: EnvironmentId; - id: ThreadId; - }; - -export function buildThreadRoutePath(input: ThreadRouteInput | PlainThreadRouteInput): string { - const environmentId = input.environmentId; - const threadId = "threadId" in input ? input.threadId : input.id; - - 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, - line?: number | null, -): string { - const basePath = `${buildThreadRoutePath(input)}/files`; - if (!relativePath) { - return basePath; - } - - const pathSegments = relativePath.split("/").filter((segment) => segment.length > 0); - if (pathSegments.length === 0) { - return basePath; - } - - const encodedPath = pathSegments.map(encodeURIComponent).join("/"); - const lineParam = - Number.isFinite(line) && Number(line) > 0 ? `?line=${Math.floor(Number(line))}` : ""; - return `${basePath}/${encodedPath}${lineParam}`; -} - -export function buildThreadTerminalRoutePath( - input: ThreadRouteInput | PlainThreadRouteInput, - terminalId?: string | null, -): string { - const basePath = `${buildThreadRoutePath(input)}/terminal`; - if (!terminalId) { - return basePath; - } - - return `${basePath}?terminalId=${encodeURIComponent(terminalId)}`; -} - -export function buildThreadTerminalNavigation( - input: ThreadRouteInput | PlainThreadRouteInput, - terminalId?: string | null, -): AppNavigationTarget { - const environmentId = String(input.environmentId); - const threadId = String("threadId" in input ? input.threadId : input.id); - - const params: { environmentId: string; threadId: string; terminalId?: string } = { - environmentId, - threadId, - }; - - if (terminalId != null && terminalId !== "") { - params.terminalId = terminalId; - } - - return { - name: "ThreadTerminal", - params, - }; -} - -export function buildThreadFilesNavigation( - input: ThreadRouteInput | PlainThreadRouteInput, - relativePath?: string | null, - line?: number | null, -): 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 { - name: "ThreadFiles", - params: { environmentId, threadId }, - }; - } - - const params: { - environmentId: string; - threadId: string; - path: string[]; - line?: string; - } = { environmentId, threadId, path }; - if (Number.isFinite(line) && Number(line) > 0) { - params.line = String(Math.floor(Number(line))); - } - - return { - name: "ThreadFile", - params, - }; -} - -export function dismissRoute(navigation: Navigation) { - if (navigation.canGoBack()) { - navigation.back(); - return; - } - - 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/native-stack-header.tsx b/apps/mobile/src/native/StackHeader.tsx similarity index 82% rename from apps/mobile/src/navigation/native-stack-header.tsx rename to apps/mobile/src/native/StackHeader.tsx index 64ad7404bd7..e2708757995 100644 --- a/apps/mobile/src/navigation/native-stack-header.tsx +++ b/apps/mobile/src/native/StackHeader.tsx @@ -1,9 +1,4 @@ -import { - useFocusEffect, - useNavigation, - useRoute, - type ParamListBase, -} from "@react-navigation/native"; +import { useNavigation, type ParamListBase } from "@react-navigation/native"; import type { NativeStackHeaderItem, NativeStackHeaderItemMenu, @@ -12,23 +7,21 @@ import type { } from "@react-navigation/native-stack"; import { Children, - cloneElement, - createElement, - Fragment, isValidElement, useEffect, + useLayoutEffect, useMemo, type ReactElement, type ReactNode, } from "react"; import type { ColorValue } from "react-native"; -import { useAppNavigation } from "./app-navigation"; -import type { AppNavigationInput, RouteParams } from "./route-model"; - -export { useFocusEffect }; -export { useAppNavigation, useCurrentPathname, useCurrentRouteParams } from "./app-navigation"; -export type { AppNavigationInput }; +export { + nativeHeaderScrollEdgeEffects, + nativeTopScrollEdgeEffect, + type NativeHeaderScrollEdgeEffects, + type NativeTopScrollEdgeEffect, +} from "./scrollEdgeEffects"; export type AppNativeStackNavigationOptions = Omit< NativeStackNavigationOptions, @@ -43,17 +36,8 @@ export type AppNativeStackNavigationOptions = Omit< readonly unstable_navigationItemStyle?: unknown; }; -export function useRouteParams(): T { - const route = useRoute(); - return (route.params ?? {}) as RouteParams as T; -} - function useNativeStackNavigation(): NativeStackNavigationProp | null { - try { - return useNavigation>(); - } catch { - return null; - } + return useNavigation>(); } function normalizeScreenOptions( @@ -70,11 +54,6 @@ function normalizeScreenOptions( 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); } @@ -90,7 +69,7 @@ export function NativeStackScreenOptions(props: { const navigation = useNativeStackNavigation(); const normalizedOptions = useMemo(() => normalizeScreenOptions(props.options), [props.options]); - useEffect(() => { + useLayoutEffect(() => { if (!navigation || !normalizedOptions) { return; } @@ -129,6 +108,9 @@ function labelFromChildren(children: ReactNode): string { type NativeStackHeaderIcon = NonNullable< Extract["icon"] >; +type NativeStackOptionsWithToolbar = NativeStackNavigationOptions & { + unstable_headerToolbarItems?: () => NativeStackHeaderItem[]; +}; function iconFromProp(icon: unknown): NativeStackHeaderIcon | undefined { if (typeof icon !== "string") { @@ -225,7 +207,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { ? (child.props.onPress as () => void) : () => undefined, sharesBackground: !child.props.separateBackground, - variant: "prominent", + tintColor: child.props.tintColor as ColorValue | undefined, + variant: "plain", }; } @@ -244,7 +227,8 @@ function convertToolbarChild(child: ReactNode): NativeStackHeaderItem | null { items: collectMenuItems(child.props.children), }, sharesBackground: !child.props.separateBackground, - variant: "prominent", + tintColor: child.props.tintColor as ColorValue | undefined, + variant: "plain", }; } @@ -277,14 +261,29 @@ function NativeHeaderToolbarRoot(props: { const items = useMemo(() => collectToolbarItems(props.children), [props.children]); useEffect(() => { - if (!navigation || props.placement === "bottom" || items.length === 0) { + if (!navigation) { return; } + if (props.placement === "bottom") { + navigation.setOptions({ + unstable_headerToolbarItems: () => items, + } as NativeStackOptionsWithToolbar); + return () => { + navigation.setOptions({ + unstable_headerToolbarItems: () => [], + } as NativeStackOptionsWithToolbar); + }; + } if (props.placement === "left") { navigation.setOptions({ unstable_headerLeftItems: () => items }); - return; + return () => { + navigation.setOptions({ unstable_headerLeftItems: () => [] }); + }; } navigation.setOptions({ unstable_headerRightItems: () => items }); + return () => { + navigation.setOptions({ unstable_headerRightItems: () => [] }); + }; }, [items, navigation, props.placement]); return null; @@ -296,6 +295,7 @@ function NativeHeaderToolbarButton(_props: { readonly icon?: string; readonly onPress?: () => void; readonly separateBackground?: boolean; + readonly tintColor?: ColorValue; }) { return null; } @@ -308,6 +308,7 @@ function NativeHeaderToolbarMenu(_props: { readonly icon?: string; readonly inline?: boolean; readonly separateBackground?: boolean; + readonly tintColor?: ColorValue; readonly title?: string; }) { return null; @@ -356,40 +357,3 @@ export const NativeHeaderToolbar = Object.assign(NativeHeaderToolbarRoot, { SearchBarSlot: NativeHeaderToolbarSearchBarSlot, Spacer: NativeHeaderToolbarSpacer, }); - -function NativeStackScreenTitle(_props: { - readonly asChild?: boolean; - readonly children?: ReactNode; -}) { - return null; -} - -NativeStackScreenOptions.Title = NativeStackScreenTitle; - -export function NavigateTo(props: { readonly href: AppNavigationInput }) { - const navigation = useAppNavigation(); - useEffect(() => { - navigation.replace(props.href); - }, [navigation, props.href]); - return null; -} - -export function NavigationLink(props: { - readonly href: AppNavigationInput; - readonly asChild?: boolean; - readonly children?: ReactNode; -}) { - const navigation = useAppNavigation(); - if (props.asChild && isValidElement<{ onPress?: () => void }>(props.children)) { - return cloneElement(props.children, { - onPress: () => navigation.push(props.href), - }); - } - return null; -} - -export const NativeStackFragment = function NativeStackFragment(props: { - readonly children?: ReactNode; -}) { - return createElement(Fragment, null, props.children); -}; diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts b/apps/mobile/src/native/scrollEdgeEffects.test.ts similarity index 89% rename from apps/mobile/src/lib/native-scroll-edge-effect.test.ts rename to apps/mobile/src/native/scrollEdgeEffects.test.ts index e8719827f31..cf7e590671c 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.test.ts +++ b/apps/mobile/src/native/scrollEdgeEffects.test.ts @@ -1,9 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { - nativeHeaderScrollEdgeEffects, - nativeTopScrollEdgeEffect, -} from "./native-scroll-edge-effect"; +import { nativeHeaderScrollEdgeEffects, nativeTopScrollEdgeEffect } from "./scrollEdgeEffects"; describe("nativeTopScrollEdgeEffect", () => { it("keeps the automatic native treatment on iOS 26", () => { diff --git a/apps/mobile/src/lib/native-scroll-edge-effect.ts b/apps/mobile/src/native/scrollEdgeEffects.ts similarity index 82% rename from apps/mobile/src/lib/native-scroll-edge-effect.ts rename to apps/mobile/src/native/scrollEdgeEffects.ts index 867a4a9ccda..cb6f74295de 100644 --- a/apps/mobile/src/lib/native-scroll-edge-effect.ts +++ b/apps/mobile/src/native/scrollEdgeEffects.ts @@ -1,3 +1,7 @@ +// Pure helpers for native header scroll-edge effects. Kept free of +// react-native / react-navigation imports so they stay unit-testable in node +// (those packages ship untranspiled Flow syntax). + export type NativeTopScrollEdgeEffect = "automatic" | "soft"; export type NativeHeaderScrollEdgeEffects = { readonly top: NativeTopScrollEdgeEffect; diff --git a/apps/mobile/src/navigation/RootNavigator.tsx b/apps/mobile/src/navigation/RootNavigator.tsx deleted file mode 100644 index 484275d7f0e..00000000000 --- a/apps/mobile/src/navigation/RootNavigator.tsx +++ /dev/null @@ -1,405 +0,0 @@ -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 deleted file mode 100644 index 512c9600819..00000000000 --- a/apps/mobile/src/navigation/app-navigation.tsx +++ /dev/null @@ -1,179 +0,0 @@ -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 AppNavigationInput, - type AppStackParamList, - type RouteParams, -} from "./route-model"; - -export type AppNavigation = { - readonly push: (target: AppNavigationInput) => void; - readonly replace: (target: AppNavigationInput) => 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 = ( - input: AppNavigationInput, - action: "push" | "replace" | "navigate", - ): void => { - const target = resolveNavigationTarget(input); - 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 deleted file mode 100644 index 55ecafd7cf0..00000000000 --- a/apps/mobile/src/navigation/linking.ts +++ /dev/null @@ -1,39 +0,0 @@ -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/route-model.ts b/apps/mobile/src/navigation/route-model.ts deleted file mode 100644 index 72fbd7bba56..00000000000 --- a/apps/mobile/src/navigation/route-model.ts +++ /dev/null @@ -1,332 +0,0 @@ -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 AppNavigationInput = string | AppNavigationTarget; - -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(input: AppNavigationInput): AppNavigationTarget { - if (typeof input !== "string") { - return withParams(input.name, input.params); - } - - const { pathname, query } = splitPathAndQuery(input); - const target = resolveNavigationPath(pathname); - return { name: target.name, params: mergeParams(target.params, query) }; -} - -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/screens/+not-found.tsx b/apps/mobile/src/screens/+not-found.tsx deleted file mode 100644 index 7dc664b8c14..00000000000 --- a/apps/mobile/src/screens/+not-found.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { NavigationLink } from "../navigation/native-stack-header"; -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")); - const primaryBgStyle = StyleSheet.flatten(useResolveClassNames("bg-primary")); - const returnHomeButtonStyle = StyleSheet.flatten([ - { - borderRadius: 999, - paddingHorizontal: 20, - paddingVertical: 14, - }, - primaryBgStyle, - ]); - - return ( - - - Route not found - - - - Return home - - - - ); -} diff --git a/apps/mobile/src/screens/debug/rns-glass.tsx b/apps/mobile/src/screens/debug/rns-glass.tsx deleted file mode 100644 index b459dbd5807..00000000000 --- a/apps/mobile/src/screens/debug/rns-glass.tsx +++ /dev/null @@ -1,1099 +0,0 @@ -import { NativeStackScreenOptions } from "../../navigation/native-stack-header"; -import { useEffect, useRef, useState, type ComponentProps } from "react"; -import { - Platform, - Pressable, - ScrollView, - StyleSheet, - Text, - useColorScheme, - useWindowDimensions, - View, - type NativeSyntheticEvent, - type ScrollView as ScrollViewType, -} from "react-native"; -import { useSafeAreaInsets } from "react-native-safe-area-context"; -import { - Screen, - ScreenStack, - ScreenStackHeaderConfig, - ScreenStackHeaderSearchBarView, - SearchBar, -} from "react-native-screens"; - -const CODE_LINES = [ - "# Native RNS glass debug route", - "", - "This screen intentionally avoids app-level header wrappers.", - "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", -]; - -const SWATCHES = ["#0A84FF", "#30D158", "#FF9F0A", "#BF5AF2", "#64D2FF", "#FF375F"]; - -const CODE_ROWS = CODE_LINES.concat(CODE_LINES).map((line, index) => ({ - 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; - - if (props.compact) { - return ( - - - - - - {}, - type: "button", - }, - ] 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" - 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.foreground; - - return ( - - - - - - {}, - sharesBackground: true, - tintColor: headerButtonTint, - type: "button", - variant: "prominent", - }, - { - accessibilityLabel: "Open settings", - icon: { name: "gearshape", type: "sfSymbol" }, - identifier: "rns-glass-ipad-settings", - 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.foreground; - - 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"] - } - 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="integratedButton" - 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/screens/new/add-project/destination.tsx b/apps/mobile/src/screens/new/add-project/destination.tsx deleted file mode 100644 index 7c00aa28c65..00000000000 --- a/apps/mobile/src/screens/new/add-project/destination.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AddProjectDestinationScreen } from "../../../features/projects/AddProjectScreen"; - -export default function AddProjectDestinationRoute() { - return ; -} diff --git a/apps/mobile/src/screens/new/add-project/index.tsx b/apps/mobile/src/screens/new/add-project/index.tsx deleted file mode 100644 index e7d88b9c4f5..00000000000 --- a/apps/mobile/src/screens/new/add-project/index.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AddProjectSourceScreen } from "../../../features/projects/AddProjectScreen"; - -export default function AddProjectRoute() { - return ; -} diff --git a/apps/mobile/src/screens/new/add-project/local.tsx b/apps/mobile/src/screens/new/add-project/local.tsx deleted file mode 100644 index db8e2f4d617..00000000000 --- a/apps/mobile/src/screens/new/add-project/local.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { AddProjectLocalFolderScreen } from "../../../features/projects/AddProjectScreen"; - -export default function AddProjectLocalRoute() { - return ; -} diff --git a/apps/mobile/src/screens/new/add-project/repository.tsx b/apps/mobile/src/screens/new/add-project/repository.tsx deleted file mode 100644 index c085bcb94b3..00000000000 --- a/apps/mobile/src/screens/new/add-project/repository.tsx +++ /dev/null @@ -1,23 +0,0 @@ -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 = useRouteParams<{ source?: string | string[] }>(); - const source = Array.isArray(params.source) ? params.source[0] : params.source; - const title = - source === "github" || - source === "gitlab" || - source === "bitbucket" || - source === "azure-devops" - ? addProjectRemoteSourceLabel(source) - : "Git URL"; - - return ( - <> - - - - ); -} diff --git a/apps/mobile/src/state/use-thread-selection.ts b/apps/mobile/src/state/use-thread-selection.ts index bc077f4c93a..d369fbc4377 100644 --- a/apps/mobile/src/state/use-thread-selection.ts +++ b/apps/mobile/src/state/use-thread-selection.ts @@ -1,5 +1,5 @@ -import { useCurrentRouteParams } from "../navigation/native-stack-header"; -import { createContext, createElement, use, useMemo, useRef, type ReactNode } from "react"; +import { useRoute, type RouteProp } from "@react-navigation/native"; +import { useMemo, useRef } from "react"; import { EnvironmentId, type OrchestrationThread, @@ -16,6 +16,10 @@ import { useRemoteEnvironmentRuntime, useSavedRemoteConnection, } from "./use-remote-environment-registry"; +type ThreadSelectionRouteParams = { + readonly environmentId?: string | string[]; + readonly threadId?: string | string[]; +}; function firstRouteParam(value: string | string[] | undefined): string | null { if (Array.isArray(value)) { @@ -62,14 +66,11 @@ function threadDetailToShell( }; } -function useResolvedThreadSelection() { - const params = useCurrentRouteParams<{ - environmentId?: string | string[]; - threadId?: string | string[]; - }>(); +function useResolvedThreadSelection(params: ThreadSelectionRouteParams | undefined) { + const routeParams = params ?? {}; const routeThreadRef = useMemo(() => { - const environmentId = firstRouteParam(params.environmentId); - const threadId = firstRouteParam(params.threadId); + const environmentId = firstRouteParam(routeParams.environmentId); + const threadId = firstRouteParam(routeParams.threadId); if (!environmentId || !threadId) { return null; } @@ -78,7 +79,7 @@ function useResolvedThreadSelection() { environmentId: EnvironmentId.make(environmentId), threadId: ThreadId.make(threadId), }; - }, [params.environmentId, params.threadId]); + }, [routeParams.environmentId, routeParams.threadId]); const lastRouteThreadRef = useRef(null); if (routeThreadRef !== null) { lastRouteThreadRef.current = routeThreadRef; @@ -133,17 +134,7 @@ function useResolvedThreadSelection() { 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; + const route = useRoute>>(); + return useResolvedThreadSelection(route.params); } diff --git a/packages/client-runtime/src/state/threadDetail.ts b/packages/client-runtime/src/state/threadDetail.ts index f048573c2ef..430900bdbb0 100644 --- a/packages/client-runtime/src/state/threadDetail.ts +++ b/packages/client-runtime/src/state/threadDetail.ts @@ -13,7 +13,7 @@ import { AsyncResult, Atom } from "effect/unstable/reactivity"; import type { EnvironmentThread, EnvironmentThreadShell } from "./models.ts"; import { scopeThread } from "./models.ts"; -import { EMPTY_ENVIRONMENT_THREAD_STATE, type EnvironmentThreadState } from "./threads.ts"; +import { EMPTY_ENVIRONMENT_THREAD_STATE, type EnvironmentThreadState } from "./threadState.ts"; import { parseThreadKey, threadKey } from "./entities.ts"; import { THREAD_STATE_IDLE_TTL_MS } from "./threadRetention.ts"; diff --git a/packages/client-runtime/src/state/threadState.ts b/packages/client-runtime/src/state/threadState.ts new file mode 100644 index 00000000000..89be139e925 --- /dev/null +++ b/packages/client-runtime/src/state/threadState.ts @@ -0,0 +1,16 @@ +import type { OrchestrationThread } from "@t3tools/contracts"; +import * as Option from "effect/Option"; + +export type EnvironmentThreadStatus = "empty" | "cached" | "synchronizing" | "live" | "deleted"; + +export interface EnvironmentThreadState { + readonly data: Option.Option; + readonly status: EnvironmentThreadStatus; + readonly error: Option.Option; +} + +export const EMPTY_ENVIRONMENT_THREAD_STATE: EnvironmentThreadState = { + data: Option.none(), + status: "empty", + error: Option.none(), +}; diff --git a/packages/client-runtime/src/state/threads.ts b/packages/client-runtime/src/state/threads.ts index bbb38f8d4be..a01baf1594d 100644 --- a/packages/client-runtime/src/state/threads.ts +++ b/packages/client-runtime/src/state/threads.ts @@ -22,20 +22,11 @@ import { parseThreadKey, threadKey } from "./entities.ts"; import { applyThreadDetailEvent } from "./threadReducer.ts"; import { THREAD_STATE_IDLE_TTL_MS } from "./threadRetention.ts"; import { followStreamInEnvironment } from "./runtime.ts"; - -export type EnvironmentThreadStatus = "empty" | "cached" | "synchronizing" | "live" | "deleted"; - -export interface EnvironmentThreadState { - readonly data: Option.Option; - readonly status: EnvironmentThreadStatus; - readonly error: Option.Option; -} - -export const EMPTY_ENVIRONMENT_THREAD_STATE: EnvironmentThreadState = { - data: Option.none(), - status: "empty", - error: Option.none(), -}; +import { + EMPTY_ENVIRONMENT_THREAD_STATE, + type EnvironmentThreadState, + type EnvironmentThreadStatus, +} from "./threadState.ts"; function statusWithoutLiveData(data: Option.Option): EnvironmentThreadStatus { return Option.isSome(data) ? "cached" : "empty"; @@ -254,3 +245,4 @@ export * from "./threadCommands.ts"; export * from "./threadDetail.ts"; export * from "./threadReducer.ts"; export * from "./threadShell.ts"; +export * from "./threadState.ts"; diff --git a/packages/shared/src/dpop.ts b/packages/shared/src/dpop.ts index 88dcf8e3090..dabfaffa4cd 100644 --- a/packages/shared/src/dpop.ts +++ b/packages/shared/src/dpop.ts @@ -5,22 +5,20 @@ import * as Option from "effect/Option"; import * as Result from "effect/Result"; import * as Schema from "effect/Schema"; +import { DpopPublicJwk as DpopPublicJwkSchema, normalizeDpopHtu } from "./dpopCommon.ts"; +import type { DpopPublicJwk as DpopPublicJwkType } from "./dpopCommon.ts"; import { stableStringify } from "./relaySigning.ts"; const DPOP_TYP = "dpop+jwt"; const DPOP_ALG = "ES256"; const DEFAULT_MAX_AGE_SECONDS = 300; -export const DpopPublicJwk = Schema.Struct({ - kty: Schema.Literal("EC"), - crv: Schema.Literal("P-256"), - x: Schema.String.check(Schema.isNonEmpty()), - y: Schema.String.check(Schema.isNonEmpty()), -}); -export type DpopPublicJwk = typeof DpopPublicJwk.Type; +export const DpopPublicJwk = DpopPublicJwkSchema; +export type DpopPublicJwk = DpopPublicJwkType; +export { normalizeDpopHtu }; const DpopJwtHeaderPublicJwk = Schema.Struct({ - ...DpopPublicJwk.fields, + ...DpopPublicJwkSchema.fields, d: Schema.optionalKey(Schema.Never), }); @@ -68,7 +66,7 @@ function decodeBase64UrlDpopJwtPayload(value: string) { return decodeDpopJwtPayloadJson(Result.getOrThrow(Encoding.decodeBase64UrlString(value))); } -function dpopThumbprintInput(jwk: DpopPublicJwk): string { +function dpopThumbprintInput(jwk: DpopPublicJwkType): string { return stableStringify({ crv: jwk.crv, kty: jwk.kty, @@ -77,18 +75,7 @@ function dpopThumbprintInput(jwk: DpopPublicJwk): string { }); } -export function normalizeDpopHtu(url: string): string | null { - try { - const parsed = new URL(url); - parsed.hash = ""; - parsed.search = ""; - return parsed.toString(); - } catch { - return null; - } -} - -export function computeDpopJwkThumbprint(jwk: DpopPublicJwk): string { +export function computeDpopJwkThumbprint(jwk: DpopPublicJwkType): string { return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(dpopThumbprintInput(jwk)))); } @@ -96,7 +83,7 @@ export function computeDpopAccessTokenHash(accessToken: string): string { return Encoding.encodeBase64Url(sha256(new TextEncoder().encode(accessToken))); } -function publicKeyBytesFromJwk(jwk: DpopPublicJwk): Uint8Array { +function publicKeyBytesFromJwk(jwk: DpopPublicJwkType): Uint8Array { const x = base64UrlToBytes(jwk.x); const y = base64UrlToBytes(jwk.y); if (x.length !== 32 || y.length !== 32) { diff --git a/patches/@react-navigation%2Fnative-stack@7.17.6.patch b/patches/@react-navigation%2Fnative-stack@7.17.6.patch new file mode 100644 index 00000000000..cd2b86fb76c --- /dev/null +++ b/patches/@react-navigation%2Fnative-stack@7.17.6.patch @@ -0,0 +1,100 @@ +--- a/lib/module/views/useHeaderConfigProps.js ++++ b/lib/module/views/useHeaderConfigProps.js +@@ -19,6 +19,12 @@ + } + return item; + } ++ if (item.type === 'mailSearchToolbar') { ++ return { ++ ...item, ++ index ++ }; ++ } + 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)}`); +@@ -79,7 +85,7 @@ + } + return processedItem; + } +- throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom' and 'spacing'.`); ++ throw new Error(`Invalid item type: ${JSON.stringify(item)}. Valid types are 'button', 'menu', 'custom', 'spacing' and 'mailSearchToolbar'.`); + }).filter(item => item != null); + }; + const transformIcon = icon => { +@@ -158,8 +164,12 @@ + headerBack, + route, + title, ++ unstable_headerCenterItems: headerCenterItems, + unstable_headerLeftItems: headerLeftItems, +- unstable_headerRightItems: headerRightItems ++ unstable_headerRightItems: headerRightItems, ++ unstable_headerSubtitle: headerSubtitle, ++ unstable_headerToolbarItems: headerToolbarItems, ++ unstable_navigationItemStyle: navigationItemStyle + }) { + const { + direction +@@ -255,6 +265,10 @@ + tintColor, + canGoBack + }); ++ const centerItems = headerCenterItems?.({ ++ tintColor, ++ canGoBack ++ }); + let rightItems = headerRightItems?.({ + tintColor, + canGoBack +@@ -264,6 +278,10 @@ + // So we need to reverse them here to match the order + rightItems = [...rightItems].reverse(); + } ++ const toolbarItems = headerToolbarItems?.({ ++ tintColor, ++ canGoBack ++ }); + const children = /*#__PURE__*/_jsxs(_Fragment, { + children: [Platform.OS === 'ios' ? /*#__PURE__*/_jsxs(_Fragment, { + children: [leftItems ? leftItems.map((item, index) => { +@@ -278,7 +296,15 @@ + return null; + }) : headerLeftElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderLeftView, { + children: headerLeftElement +- }) : null, headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, { ++ }) : null, centerItems ? centerItems.map((item, index) => { ++ if (item.type === 'custom') { ++ return /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, { ++ hidesSharedBackground: item.hidesSharedBackground, ++ children: item.element ++ }, index); ++ } ++ return null; ++ }) : headerTitleElement != null ? /*#__PURE__*/_jsx(ScreenStackHeaderCenterView, { + children: headerTitleElement + }) : null] + }) : /*#__PURE__*/_jsxs(_Fragment, { +@@ -356,6 +382,7 @@ + largeTitleFontWeight, + largeTitleHideShadow: headerLargeTitleShadowVisible === false, + title: titleText, ++ subtitle: headerSubtitle, + titleColor, + titleFontFamily, + titleFontSize, +@@ -364,9 +391,12 @@ + disableTopInsetApplication: !headerTopInsetEnabled, + translucent: translucent === true, + children, ++ navigationItemStyle, ++ headerToolbarItems: processBarButtonItems(toolbarItems, colors, fonts), ++ headerCenterBarButtonItems: processBarButtonItems(centerItems, colors, fonts), + headerLeftBarButtonItems: processBarButtonItems(leftItems, colors, fonts), + headerRightBarButtonItems: processBarButtonItems(rightItems, colors, fonts), + experimental_userInterfaceStyle: dark ? 'dark' : 'light' + }; + } +-//# sourceMappingURL=useHeaderConfigProps.js.map +\ No newline at end of file ++//# sourceMappingURL=useHeaderConfigProps.js.map diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index 21b8effc574..746954492ca 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -15,7 +15,7 @@ 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); +@@ -110,6 +110,58 @@ - (instancetype)initWithConfig:(NSDictionary *)dict if (dict[@"accessibilityHint"]) { self.accessibilityHint = dict[@"accessibilityHint"]; } @@ -74,55 +74,6 @@ index 0eb1f09..73298f1 100644 #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 -+++ 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/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h index 919b984..5bb0cd6 100644 --- a/ios/RNSScreenStackHeaderConfig.h @@ -150,10 +101,10 @@ index 919b984..5bb0cd6 100644 NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm -index 5970e3e..0849992 100644 +index 5970e3e..cb050f9 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm -@@ -30,6 +30,20 @@ namespace react = facebook::react; +@@ -30,6 +30,20 @@ static const NSNumber *const DEFAULT_TITLE_FONT_SIZE = @17; static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; @@ -174,7 +125,7 @@ index 5970e3e..0849992 100644 @interface RCTImageLoader (Private) - (id)imageCache; @end -@@ -47,6 +61,9 @@ static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +@@ -47,6 +61,9 @@ + (BOOL)rnscreens_isBlankOrNull:(NSString *)string @end @interface RNSScreenStackHeaderConfig () @@ -184,7 +135,7 @@ index 5970e3e..0849992 100644 @end @implementation RNSScreenStackHeaderConfig { -@@ -81,6 +98,7 @@ static const NSNumber *const DEFAULT_TITLE_LARGE_FONT_SIZE = @34; +@@ -81,6 +98,7 @@ - (void)initProps self.hidden = YES; _reactSubviews = [NSMutableArray new]; _backTitleVisible = YES; @@ -192,7 +143,7 @@ index 5970e3e..0849992 100644 _blurEffect = RNSBlurEffectStyleNone; } -@@ -496,6 +514,10 @@ RNS_IGNORE_SUPER_CALL_END +@@ -496,6 +514,10 @@ + (void)updateViewController:(UIViewController *)vc if (shouldHide) { navitem.title = config.title; @@ -203,7 +154,7 @@ index 5970e3e..0849992 100644 // Setting navigation bar visibility is split to mitigate iOS 26 bug with bar button items. [navctr setNavigationBarHidden:YES animated:animated]; -@@ -512,11 +534,19 @@ RNS_IGNORE_SUPER_CALL_END +@@ -512,11 +534,19 @@ + (void)updateViewController:(UIViewController *)vc } navitem.largeTitleDisplayMode = config.largeTitle ? UINavigationItemLargeTitleDisplayModeAlways : UINavigationItemLargeTitleDisplayModeNever; @@ -223,7 +174,7 @@ index 5970e3e..0849992 100644 // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV -@@ -637,10 +667,245 @@ RNS_IGNORE_SUPER_CALL_END +@@ -637,10 +667,270 @@ + (void)updateViewController:(UIViewController *)vc // 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; @@ -299,15 +250,28 @@ index 5970e3e..0849992 100644 + resolvedWidth = MIN(resolvedWidth, MAX(260.0, hostWidth - 12.0)); + } + ++ CGFloat toolbarHeight = 48.0; ++ CGFloat toolbarRadius = toolbarHeight / 2.0; ++ CGFloat buttonSize = toolbarHeight; ++ CGFloat sideButtonReserve = buttonSize + 8.0; ++ CGFloat searchFieldHeight = 42.0; ++ CGFloat toolbarBottomSpacing = 4.0; ++ UIFont *searchFont = [UIFont preferredFontForTextStyle:UIFontTextStyleTitle3]; ++ NSDictionary *placeholderAttributes = @{ ++ NSForegroundColorAttributeName : UIColor.secondaryLabelColor, ++ NSFontAttributeName : searchFont, ++ }; ++ UIKeyboardLayoutGuide *keyboardLayoutGuide = chromeHostView.keyboardLayoutGuide; ++ + 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.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor constant:-toolbarBottomSpacing], + [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth], -+ [toolbarHost.heightAnchor constraintEqualToConstant:56.0], ++ [toolbarHost.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; + [chromeHostView bringSubviewToFront:toolbarHost]; + @@ -315,20 +279,20 @@ index 5970e3e..0849992 100644 + glassEffect.interactive = YES; + UIVisualEffectView *glassView = [[UIVisualEffectView alloc] initWithEffect:glassEffect]; + glassView.clipsToBounds = YES; -+ glassView.layer.cornerRadius = 28.0; ++ glassView.layer.cornerRadius = toolbarRadius; + glassView.translatesAutoresizingMaskIntoConstraints = NO; + [toolbarHost addSubview:glassView]; + BOOL hasFilterButton = + mailSearchToolbarConfig[@"filterButtonId"] != nil || mailSearchToolbarConfig[@"filterMenu"] != nil; + BOOL hasComposeButton = + mailSearchToolbarConfig[@"composeButtonId"] != nil || mailSearchToolbarConfig[@"composeMenu"] != nil; -+ CGFloat glassLeadingInset = hasFilterButton ? 64.0 : 0.0; -+ CGFloat glassTrailingInset = hasComposeButton ? -64.0 : 0.0; ++ CGFloat glassLeadingInset = hasFilterButton ? sideButtonReserve : 0.0; ++ CGFloat glassTrailingInset = hasComposeButton ? -sideButtonReserve : 0.0; + [NSLayoutConstraint activateConstraints:@[ + [glassView.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor constant:glassLeadingInset], + [glassView.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor constant:glassTrailingInset], + [glassView.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], -+ [glassView.heightAnchor constraintEqualToConstant:56.0], ++ [glassView.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; + + void (^emitButtonPress)(NSString *) = ^(NSString *buttonId) { @@ -388,20 +352,28 @@ index 5970e3e..0849992 100644 + searchBar.searchTextField.hidden = NO; + searchBar.searchTextField.alpha = 1.0; + searchBar.searchTextField.backgroundColor = UIColor.clearColor; ++ searchBar.searchTextField.font = searchFont; ++ searchBar.searchTextField.adjustsFontForContentSizeCategory = YES; + searchBar.searchTextField.textColor = UIColor.labelColor; + searchBar.searchTextField.tintColor = UIColor.labelColor; ++ if (placeholder != nil) { ++ searchBar.searchTextField.attributedPlaceholder = ++ [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes]; ++ } + 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], ++ [searchBar.heightAnchor constraintEqualToConstant:searchFieldHeight], + ]]; + } else { + UISearchTextField *searchField = [UISearchTextField new]; + if (placeholder != nil) { + searchField.placeholder = placeholder; ++ searchField.attributedPlaceholder = ++ [[NSAttributedString alloc] initWithString:placeholder attributes:placeholderAttributes]; + } + NSString *searchTextChangeId = mailSearchToolbarConfig[@"searchTextChangeId"]; + if (searchTextChangeId != nil) { @@ -412,13 +384,17 @@ index 5970e3e..0849992 100644 + forControlEvents:UIControlEventEditingChanged]; + } + searchField.borderStyle = UITextBorderStyleNone; ++ searchField.font = searchFont; ++ searchField.adjustsFontForContentSizeCategory = YES; ++ searchField.textColor = UIColor.labelColor; ++ searchField.tintColor = UIColor.labelColor; + 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.leadingAnchor constraintEqualToAnchor:glassView.contentView.leadingAnchor constant:14.0], ++ [searchField.trailingAnchor constraintEqualToAnchor:glassView.contentView.trailingAnchor constant:-14.0], + [searchField.centerYAnchor constraintEqualToAnchor:glassView.contentView.centerYAnchor], -+ [searchField.heightAnchor constraintEqualToConstant:48.0], ++ [searchField.heightAnchor constraintEqualToConstant:searchFieldHeight], + ]]; + } + @@ -432,8 +408,8 @@ index 5970e3e..0849992 100644 + [NSLayoutConstraint activateConstraints:@[ + [filterButton.leadingAnchor constraintEqualToAnchor:toolbarHost.leadingAnchor], + [filterButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], -+ [filterButton.widthAnchor constraintEqualToConstant:56.0], -+ [filterButton.heightAnchor constraintEqualToConstant:56.0], ++ [filterButton.widthAnchor constraintEqualToConstant:buttonSize], ++ [filterButton.heightAnchor constraintEqualToConstant:buttonSize], + ]]; + } + @@ -447,8 +423,8 @@ index 5970e3e..0849992 100644 + [NSLayoutConstraint activateConstraints:@[ + [composeButton.trailingAnchor constraintEqualToAnchor:toolbarHost.trailingAnchor], + [composeButton.centerYAnchor constraintEqualToAnchor:toolbarHost.centerYAnchor], -+ [composeButton.widthAnchor constraintEqualToConstant:56.0], -+ [composeButton.heightAnchor constraintEqualToConstant:56.0], ++ [composeButton.widthAnchor constraintEqualToConstant:buttonSize], ++ [composeButton.heightAnchor constraintEqualToConstant:buttonSize], + ]]; + } + } @@ -473,7 +449,7 @@ index 5970e3e..0849992 100644 // 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 +1038,7 @@ RNS_IGNORE_SUPER_CALL_END +@@ -773,6 +1063,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * - (NSArray *)barButtonItemsFromConfigs:(NSArray *> *)dicts withCurrentItems:(NSArray *)currentItems @@ -481,7 +457,7 @@ index 5970e3e..0849992 100644 { if (dicts.count == 0) { return currentItems; -@@ -781,7 +1047,197 @@ RNS_IGNORE_SUPER_CALL_END +@@ -781,7 +1072,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; @@ -680,7 +656,7 @@ index 5970e3e..0849992 100644 RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict action:^(NSString *buttonId) { auto eventEmitter = std::static_pointer_cast( -@@ -809,11 +1265,15 @@ RNS_IGNORE_SUPER_CALL_END +@@ -809,11 +1290,15 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * [items addObject:item]; } } else if (dict[@"spacing"]) { @@ -700,7 +676,7 @@ index 5970e3e..0849992 100644 NSNumber *index = dict[@"index"]; if (index.integerValue < items.count) { [items insertObject:item atIndex:index.integerValue]; -@@ -825,6 +1285,47 @@ RNS_IGNORE_SUPER_CALL_END +@@ -825,6 +1310,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * return items; } @@ -748,7 +724,7 @@ index 5970e3e..0849992 100644 RNS_IGNORE_SUPER_CALL_BEGIN - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex { -@@ -1013,6 +1514,8 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1013,6 +1539,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); @@ -757,7 +733,7 @@ index 5970e3e..0849992 100644 if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); } -@@ -1038,6 +1541,7 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1038,6 +1566,7 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; _backButtonDisplayMode = [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; @@ -765,7 +741,7 @@ index 5970e3e..0849992 100644 if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; -@@ -1084,6 +1588,30 @@ static RCTResizeMode resizeModeFromCppEquiv(react::ImageResizeMode resizeMode) +@@ -1084,6 +1613,30 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: _headerRightBarButtonItems = array; } @@ -796,78 +772,6 @@ index 5970e3e..0849992 100644 [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 ---- 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..cb4d6be 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 -@@ -60,16 +61,14 @@ 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 +121,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 +138,7 @@ namespace react = facebook::react; - return static_cast(childView).scrollView; - } - -- return nil; -+ return [RNSScrollViewFinder findScrollViewInFirstDescendantChainFrom:childView]; - } - - #pragma mark - Override diff --git a/lib/commonjs/components/ScreenStackHeaderConfig.js b/lib/commonjs/components/ScreenStackHeaderConfig.js index 16b979b..01415d4 100644 --- a/lib/commonjs/components/ScreenStackHeaderConfig.js @@ -989,36 +893,6 @@ index ab93f62..b5ea122 100644 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..bf73f41 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,6 +214,17 @@ 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; } diff --git a/lib/module/components/ScreenStackHeaderConfig.js b/lib/module/components/ScreenStackHeaderConfig.js index cf15f36..dd66167 100644 --- a/lib/module/components/ScreenStackHeaderConfig.js @@ -1140,17 +1014,6 @@ index 8a70ffd..8dbd0ba 100644 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..07fd7a5 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'; -+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 b7568ec..41ca1f2 100644 --- a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts @@ -1187,18 +1050,6 @@ index b7568ec..41ca1f2 100644 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..883c5f2 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 diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts index 3b384e0..1e3a966 100644 --- a/lib/typescript/types.d.ts @@ -1546,16 +1397,6 @@ index ba2479d..8677583 100644 onPressHeaderBarButtonItem?: | CT.DirectEventHandler | undefined; -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/src/types.tsx b/src/types.tsx index 76a83f3..9e4499f 100644 --- a/src/types.tsx diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 151d801a139..ad4f4e59660 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -66,14 +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 - react-native-nitro-modules@0.35.9: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 - react-native-screens@4.25.2: 914870f7aa1cbbbb0bff272c223fabfb154d84dc3a53054b76c3c2c8727c71b7 + '@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 + '@react-navigation/native-stack@7.17.6': + hash: 2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca + path: patches/@react-navigation%2Fnative-stack@7.17.6.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: 7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3 + path: patches/react-native-screens@4.25.2.patch importers: @@ -182,10 +201,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.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)(bufferutil@4.1.0)(utf-8-validate@6.0.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) + 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) @@ -194,13 +213,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 @@ -212,16 +231,16 @@ 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) '@react-navigation/elements': specifier: 2.9.26 - version: 2.9.26(c6a2ad0e2c930f8e3896e77c3ba11bc4) + version: 2.9.26(@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-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))(@types/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@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@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': 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) + 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))(@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) + version: 7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(d5eb07f8f6ba8c11ae9315cbbd4a373a) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -242,7 +261,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 @@ -263,40 +282,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) @@ -305,16 +324,16 @@ 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-secure-store: specifier: ~56.0.4 version: 56.0.4(expo@56.0.12) @@ -323,16 +342,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 @@ -344,43 +363,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(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) + version: 4.25.2(patch_hash=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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 @@ -389,7 +408,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 @@ -910,25 +929,21 @@ 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==} @@ -980,28 +995,24 @@ 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==} @@ -1590,25 +1601,21 @@ 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==} @@ -2594,25 +2601,21 @@ 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==} @@ -2699,105 +2702,89 @@ 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==} @@ -3256,56 +3243,48 @@ 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==} @@ -3408,56 +3387,48 @@ 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==} @@ -3979,126 +3950,108 @@ 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==} @@ -4235,79 +4188,66 @@ 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==} @@ -4542,56 +4482,48 @@ 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==} @@ -5064,28 +4996,24 @@ 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==} @@ -5168,28 +5096,24 @@ 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==} @@ -5280,7 +5204,7 @@ packages: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} alchemy@https://pkg.ing/alchemy/078ff00: - resolution: {integrity: sha512-+yX8sLZZSS5EfHHiO5vKHK782VMo1MkcvMGaHeOLoLSGTFWMT+wRqU5CX+LOOPXyymuMWW9cydg8Hg3c9/u1bw==, tarball: https://pkg.ing/alchemy/078ff00} + resolution: {tarball: https://pkg.ing/alchemy/078ff00} version: 2.0.0-beta.51 hasBin: true peerDependencies: @@ -7568,84 +7492,72 @@ 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==} @@ -11280,10 +11192,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: @@ -11387,23 +11299,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)(bufferutil@4.1.0)(utf-8-validate@6.0.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.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.1(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': @@ -11986,7 +11898,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) @@ -11996,7 +11908,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 @@ -12021,7 +11933,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 @@ -12047,8 +11959,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(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) + expo-router: 56.2.11(f23e7c64314c5d311da1a70c8215e664) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@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' @@ -12131,18 +12043,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: @@ -12203,13 +12115,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)': @@ -12238,7 +12150,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 @@ -12256,14 +12168,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: @@ -12338,14 +12250,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(58010f146fb71a08f4c76f0e25e5e6b3) + '@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(f23e7c64314c5d311da1a70c8215e664) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -12360,18 +12272,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' @@ -12634,13 +12546,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: @@ -13468,16 +13380,16 @@ 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) 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)': + '@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': {} @@ -13537,7 +13449,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 @@ -13547,7 +13459,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 @@ -13595,7 +13507,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) @@ -13603,18 +13515,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 @@ -13630,40 +13540,40 @@ snapshots: 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)': + '@react-navigation/elements@2.9.26(@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-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))(@types/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@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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-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) + '@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))(@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: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/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-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) 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-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-navigation/native-stack@7.17.6(dc1fa3b83a3d36b06fc35a0865f3d516)': + '@react-navigation/native-stack@7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(d5eb07f8f6ba8c11ae9315cbbd4a373a)': 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) + '@react-navigation/elements': 2.9.26(@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-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))(@types/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@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@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': 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))(@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) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/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-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=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@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)': + '@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))(@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) + react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@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) @@ -14052,15 +13962,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': {} @@ -14522,19 +14432,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)': + '@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)))': 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) - 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(@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)) 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)': + '@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)))': 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)) @@ -14543,7 +14453,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))(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)) ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil @@ -15176,8 +15086,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 @@ -15229,8 +15139,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 @@ -16130,29 +16040,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 @@ -16160,119 +16070,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): @@ -16285,66 +16195,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(58010f146fb71a08f4c76f0e25e5e6b3): + expo-router@56.2.11(f23e7c64314c5d311da1a70c8215e664): 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 @@ -16352,10 +16262,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(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: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/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=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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 @@ -16363,8 +16273,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' @@ -16377,7 +16287,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: {} @@ -16385,7 +16295,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 @@ -16393,20 +16303,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 @@ -16414,7 +16324,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 @@ -16424,25 +16334,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' @@ -16451,37 +16361,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 @@ -18905,103 +18815,103 @@ 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) 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): + 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(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-screens@4.25.2(patch_hash=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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) @@ -19013,23 +18923,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 @@ -20087,14 +19997,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: {} @@ -20218,8 +20128,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) - '@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/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/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 @@ -20232,7 +20142,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))(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)) optionalDependencies: '@voidzero-dev/vite-plus-darwin-arm64': 0.2.1 '@voidzero-dev/vite-plus-darwin-x64': 0.2.1 @@ -20278,7 +20188,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))(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)): 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)) @@ -20302,7 +20212,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) + '@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))) transitivePeerDependencies: - msw diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 43c03137157..b3b0785538b 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -104,6 +104,7 @@ packageExtensions: vite: "catalog:" patchedDependencies: + "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch "@effect/vitest@4.0.0-beta.78": patches/@effect__vitest@4.0.0-beta.78.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 From 7417005c15b0fe4201e700bb47a4f3b290502a93 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 11:49:51 -0700 Subject: [PATCH 72/81] Make mobile layout and typography responsive - Scale text and markdown/code surfaces from appearance settings - Keep workspace layout stable when overlay sheets are open - Refresh mobile settings and spacing to fit iPad-style screens --- apps/mobile/global.css | 36 +- .../src/NativeMarkdownBlock.ios.tsx | 19 +- .../src/NativeMarkdownSelectableText.ios.tsx | 25 +- .../src/SelectableMarkdownText.types.ts | 1 + apps/mobile/src/App.tsx | 43 +-- apps/mobile/src/Stack.tsx | 44 ++- apps/mobile/src/components/EmptyState.tsx | 4 +- apps/mobile/src/components/ErrorBanner.tsx | 2 +- .../archive/ArchivedThreadsScreen.tsx | 4 +- .../cloud/CloudWaitlistEnrollment.tsx | 54 +-- .../connection/ConnectionEnvironmentRow.tsx | 8 +- .../connection/ConnectionsNewRouteScreen.tsx | 2 +- .../connection/ConnectionsRouteScreen.tsx | 2 +- .../EnvironmentConnectionNotice.tsx | 4 +- .../features/files/FileMarkdownPreview.tsx | 24 +- .../src/features/files/FileTreeBrowser.tsx | 6 +- .../src/features/files/SourceFileSurface.tsx | 115 +++--- .../features/files/ThreadFilesRouteScreen.tsx | 2 +- .../files/WorkspaceFileWebPreview.tsx | 2 +- .../features/files/nativeSourceFileAdapter.ts | 40 ++- .../src/features/home/HomeRouteScreen.tsx | 29 ++ apps/mobile/src/features/home/HomeScreen.tsx | 2 +- .../layout/AdaptiveWorkspaceLayout.tsx | 9 +- .../features/layout/WorkspaceEmptyDetail.tsx | 13 +- .../features/projects/AddProjectScreen.tsx | 10 +- .../review/ReviewCommentComposerSheet.tsx | 17 +- .../src/features/review/ReviewSheet.tsx | 21 +- .../review/nativeReviewDiffAdapter.ts | 72 ++-- ...e.test.ts => reviewDiffBridgeKeys.test.ts} | 2 +- .../features/review/reviewDiffBridgeKeys.ts | 35 ++ .../features/review/reviewDiffRendering.tsx | 25 +- .../review/useNativeReviewDiffBridge.ts | 42 +-- .../SettingsAppearanceRouteScreen.tsx | 30 ++ .../SettingsEnvironmentsRouteScreen.tsx | 16 +- .../features/settings/SettingsRouteScreen.tsx | 127 +------ .../AppearancePreferencesProvider.tsx | 158 +++++++++ .../components/FontSizeControlRow.tsx | 81 +++++ .../sections/CodeAppearanceSection.tsx | 60 ++++ .../sections/TerminalAppearanceSection.tsx | 53 +++ .../sections/TextAppearanceSection.tsx | 37 ++ .../appearance/useAppearanceCodeSurface.ts | 34 ++ .../settings/appearance/useScaledTextRole.ts | 36 ++ .../settings/components/SettingsRow.tsx | 76 ++++ .../settings/components/SettingsSection.tsx | 18 + .../settings/components/SettingsSwitchRow.tsx | 37 ++ .../components/settings-sheet-targets.ts | 1 + .../terminal/NativeTerminalSurface.tsx | 6 +- .../terminal/ThreadTerminalRouteScreen.tsx | 83 +---- .../features/terminal/terminalPreferences.ts | 2 +- .../threads/ComposerCommandPopover.tsx | 4 +- .../features/threads/NewTaskDraftScreen.tsx | 5 +- .../features/threads/NewTaskRouteScreen.tsx | 4 +- .../features/threads/PendingApprovalCard.tsx | 2 +- .../features/threads/PendingUserInputCard.tsx | 2 +- .../src/features/threads/ThreadComposer.tsx | 15 +- .../src/features/threads/ThreadFeed.tsx | 83 +++-- .../threads/ThreadNavigationSidebar.tsx | 329 +++++------------- .../features/threads/ThreadRouteScreen.tsx | 45 +-- .../features/threads/git/GitCommitSheet.tsx | 10 +- .../features/threads/git/GitConfirmSheet.tsx | 2 +- .../features/threads/git/GitOverviewSheet.tsx | 2 +- .../threads/git/gitSheetComponents.tsx | 2 +- .../threads/sidebar-filter-button.ios.tsx | 24 -- .../threads/sidebar-filter-button.tsx | 20 +- .../threads/sidebar-header-actions.ios.tsx | 23 -- .../threads/sidebar-header-actions.tsx | 29 +- .../src/features/threads/thread-work-log.tsx | 4 +- .../src/lib/adaptive-navigation.test.ts | 9 + apps/mobile/src/lib/adaptive-navigation.ts | 4 +- .../src/lib/appearancePreferences.test.ts | 142 ++++++++ apps/mobile/src/lib/appearancePreferences.ts | 245 +++++++++++++ apps/mobile/src/lib/storage.ts | 29 +- apps/mobile/src/lib/typography.test.ts | 11 +- apps/mobile/src/lib/typography.ts | 21 +- .../src/native/T3ComposerEditor.ios.tsx | 7 +- apps/mobile/src/native/T3ComposerEditor.tsx | 5 +- apps/mobile/src/native/T3HeaderButton.ios.tsx | 30 -- 77 files changed, 1734 insertions(+), 943 deletions(-) rename apps/mobile/src/features/review/{useNativeReviewDiffBridge.test.ts => reviewDiffBridgeKeys.test.ts} (96%) create mode 100644 apps/mobile/src/features/review/reviewDiffBridgeKeys.ts create mode 100644 apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx create mode 100644 apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx create mode 100644 apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx create mode 100644 apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx create mode 100644 apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx create mode 100644 apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx create mode 100644 apps/mobile/src/features/settings/appearance/useAppearanceCodeSurface.ts create mode 100644 apps/mobile/src/features/settings/appearance/useScaledTextRole.ts create mode 100644 apps/mobile/src/features/settings/components/SettingsRow.tsx create mode 100644 apps/mobile/src/features/settings/components/SettingsSection.tsx create mode 100644 apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx create mode 100644 apps/mobile/src/features/settings/components/settings-sheet-targets.ts delete mode 100644 apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx delete mode 100644 apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx create mode 100644 apps/mobile/src/lib/appearancePreferences.test.ts create mode 100644 apps/mobile/src/lib/appearancePreferences.ts delete mode 100644 apps/mobile/src/native/T3HeaderButton.ios.tsx diff --git a/apps/mobile/global.css b/apps/mobile/global.css index b2014bf9353..f76300ee524 100644 --- a/apps/mobile/global.css +++ b/apps/mobile/global.css @@ -199,24 +199,24 @@ --font-bold: "DMSans_700Bold"; /* Keep this scale aligned with src/lib/typography.ts for native style props. */ - --text-3xs: 10px; - --text-3xs--line-height: 13px; - --text-2xs: 11px; - --text-2xs--line-height: 15px; - --text-xs: 12px; - --text-xs--line-height: 16px; - --text-sm: 13px; - --text-sm--line-height: 18px; - --text-base: 15px; - --text-base--line-height: 22px; - --text-lg: 17px; - --text-lg--line-height: 22px; - --text-xl: 20px; - --text-xl--line-height: 26px; - --text-2xl: 24px; - --text-2xl--line-height: 30px; - --text-3xl: 28px; - --text-3xl--line-height: 34px; + --text-3xs: 11px; + --text-3xs--line-height: 14px; + --text-2xs: 12px; + --text-2xs--line-height: 16px; + --text-xs: 13px; + --text-xs--line-height: 17px; + --text-sm: 14px; + --text-sm--line-height: 19px; + --text-base: 16px; + --text-base--line-height: 23px; + --text-lg: 18px; + --text-lg--line-height: 23px; + --text-xl: 21px; + --text-xl--line-height: 28px; + --text-2xl: 26px; + --text-2xl--line-height: 32px; + --text-3xl: 30px; + --text-3xl--line-height: 36px; } /* ─── Custom utilities ──────────────────────────────────────────────── */ diff --git a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx index 212c385124e..e6a045b3cd9 100644 --- a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx +++ b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownBlock.ios.tsx @@ -26,6 +26,15 @@ function nodeKey(node: MarkdownNode, index: number): string { return `${node.type}:${node.beg ?? index}:${node.end ?? index}`; } +/** Code inside markdown scales with the base text size (12pt at the default 15pt body). */ +function codeBlockFontSize(textStyle: NativeMarkdownTextStyle): number { + return Math.max(10, Math.round(textStyle.fontSize * 0.8)); +} + +function codeBlockLineHeight(textStyle: NativeMarkdownTextStyle): number { + return codeBlockFontSize(textStyle) + 6; +} + function nodeText(node: MarkdownNode): string { if (node.content !== undefined) { return node.content; @@ -161,8 +170,8 @@ function HighlightedCodeText(props: { style={{ color: props.textStyle.codeColor, fontFamily: "ui-monospace", - fontSize: 12, - lineHeight: 18, + fontSize: codeBlockFontSize(props.textStyle), + lineHeight: codeBlockLineHeight(props.textStyle), }} > {props.content} @@ -196,8 +205,8 @@ function HighlightedCodeText(props: { style={{ color: props.textStyle.codeColor, fontFamily: "ui-monospace", - fontSize: 12, - lineHeight: 18, + fontSize: codeBlockFontSize(props.textStyle), + lineHeight: codeBlockLineHeight(props.textStyle), }} > {keyedLines.map((line, lineIndex) => ( @@ -264,7 +273,7 @@ function NativeCodeBlock(props: { flex: 1, color: props.textStyle.mutedColor, fontFamily: "ui-monospace", - fontSize: 12, + fontSize: codeBlockFontSize(props.textStyle), }} > {languageLabel} diff --git a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx index c7a5a16d6fd..994c8ce2ed5 100644 --- a/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx +++ b/apps/mobile/modules/t3-markdown-text/src/NativeMarkdownSelectableText.ios.tsx @@ -32,11 +32,25 @@ function runKeySignature(run: NativeMarkdownTextRun): string { ].join(":"); } +const DEFAULT_BODY_FONT_SIZE = 15; +const DEFAULT_HEADING_FONT_SIZES = [22, 19, 17, 16, 15, 15] as const; + +function resolveHeadingFontSize(textStyle: NativeMarkdownTextStyle, headingLevel: number): number { + const index = Math.max(0, Math.min(5, headingLevel - 1)); + const configured = textStyle.headingFontSizes?.[index]; + if (typeof configured === "number" && Number.isFinite(configured)) { + return configured; + } + + const scale = textStyle.fontSize / DEFAULT_BODY_FONT_SIZE; + return Math.max(12, Math.round(DEFAULT_HEADING_FONT_SIZES[index] * scale)); +} + function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle): TextStyle { const isFile = run.fileIcon != null; const isSkill = run.skillName != null; const headingLevel = Math.max(1, Math.min(6, run.headingLevel ?? 1)); - const headingFontSize = [22, 19, 17, 16, 15, 15][headingLevel - 1] ?? 15; + const headingFontSize = resolveHeadingFontSize(textStyle, headingLevel); const isHeading = run.role === "heading"; const isCodeBlock = run.role === "code-block" || run.role === "code-language"; const hasParagraphStyle = run.headIndent !== undefined; @@ -88,7 +102,7 @@ function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle : isHeading ? headingFontSize : run.role === "code-language" - ? 11 + ? Math.max(10, Math.round(textStyle.fontSize * 0.73)) : run.code || isCodeBlock ? Math.max(12, textStyle.fontSize - 2) : textStyle.fontSize, @@ -98,9 +112,9 @@ function runStyle(run: NativeMarkdownTextRun, textStyle: NativeMarkdownTextStyle : run.role === "list-break" ? textStyle.lineHeight + (run.spacing ?? 0) : isHeading - ? Math.max(headingFontSize + 6, 20) + ? Math.max(headingFontSize + 6, textStyle.lineHeight + 2) : isCodeBlock - ? 18 + ? Math.max(16, textStyle.lineHeight - 2) : textStyle.lineHeight, fontStyle: run.italic ? "italic" : "normal", fontWeight: isHeading || run.bold || isFile || isSkill ? "700" : "400", @@ -148,6 +162,9 @@ export function NativeMarkdownSelectableText(props: { // color-only child update can otherwise leave the previous appearance cached. const appearanceKey = [ colorScheme ?? "unspecified", + props.textStyle.fontSize, + props.textStyle.lineHeight, + props.textStyle.headingFontSizes?.join(","), props.textStyle.color, props.textStyle.strongColor, props.textStyle.mutedColor, diff --git a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.types.ts b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.types.ts index 76c1402d3c8..42cc3cd6fb6 100644 --- a/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.types.ts +++ b/apps/mobile/modules/t3-markdown-text/src/SelectableMarkdownText.types.ts @@ -16,6 +16,7 @@ export interface NativeMarkdownTextStyle { readonly fontFamily: string; readonly headingFontFamily: string; readonly boldFontFamily: string; + readonly headingFontSizes?: ReadonlyArray; } export interface MarkdownHighlightedToken { diff --git a/apps/mobile/src/App.tsx b/apps/mobile/src/App.tsx index 3c597f8040f..d0880a39798 100644 --- a/apps/mobile/src/App.tsx +++ b/apps/mobile/src/App.tsx @@ -15,6 +15,7 @@ import { createStaticNavigation, DarkTheme, DefaultTheme } from "@react-navigati import { RegistryContext } from "@effect/atom-react"; import { useEffect } from "react"; import { CloudAuthProvider } from "./features/cloud/CloudAuthProvider"; +import { AppearancePreferencesProvider } from "./features/settings/appearance/AppearancePreferencesProvider"; import { RootStack } from "./Stack"; import { appAtomRegistry } from "./state/atom-registry"; import { useThemeColor } from "./lib/useThemeColor"; @@ -43,26 +44,28 @@ export default function App() { return ( - - - - - {/* The navigation theme drives the NATIVE header appearance: native-stack - forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without - this, React Navigation defaults to its light theme and every native - header (glass buttons, title, materials) is forced light even when - the system is in dark mode. */} - - - - + + + + + + {/* The navigation theme drives the NATIVE header appearance: native-stack + forwards `dark` as the nav bar's overrideUserInterfaceStyle. Without + this, React Navigation defaults to its light theme and every native + header (glass buttons, title, materials) is forced light even when + the system is in dark mode. */} + + + + + ); diff --git a/apps/mobile/src/Stack.tsx b/apps/mobile/src/Stack.tsx index 8086f5eef04..29cf29da90a 100644 --- a/apps/mobile/src/Stack.tsx +++ b/apps/mobile/src/Stack.tsx @@ -38,6 +38,7 @@ import { AddProjectSourceRoute } from "./features/projects/AddProjectSourceRoute import { NewTaskDraftRouteScreen } from "./features/threads/NewTaskDraftRouteScreen"; import { NewTaskFlowProvider } from "./features/threads/new-task-flow-provider"; import { NewTaskRouteScreen } from "./features/threads/NewTaskRouteScreen"; +import { SettingsAppearanceRouteScreen } from "./features/settings/SettingsAppearanceRouteScreen"; import { SettingsAuthRouteScreen } from "./features/settings/SettingsAuthRouteScreen"; import { SettingsEnvironmentsRouteScreen } from "./features/settings/SettingsEnvironmentsRouteScreen"; import { SettingsRouteScreen } from "./features/settings/SettingsRouteScreen"; @@ -137,6 +138,13 @@ const SettingsSheetStack = createNativeStackNavigator({ title: "Archived Threads", }, }), + SettingsAppearance: createNativeStackScreen({ + screen: SettingsAppearanceRouteScreen, + linking: "appearance", + options: { + title: "Appearance", + }, + }), SettingsAuth: createNativeStackScreen({ screen: SettingsAuthRouteScreen, linking: "auth", @@ -208,19 +216,53 @@ const NewTaskSheetStack = createNativeStackNavigator({ }, }); +// Routes presented as sheets/overlays ON TOP of the workspace. They must not +// influence the adaptive workspace layout: opening Settings over Home should +// not flip the sidebar in or change the active thread. +const WORKSPACE_OVERLAY_ROUTES = new Set([ + "Connections", + "ConnectionsNew", + "GitBranches", + "GitCommit", + "GitConfirm", + "GitOverview", + "NewTaskSheet", + "SettingsSheet", + "ThreadReviewComment", +]); + +/** + * Pathname of the topmost NON-overlay route — the screen the workspace is + * actually "on", regardless of any sheets floating above it. + */ +function workspacePathFromState(state: NavigationState): string { + const routes = state.routes.filter((route) => !WORKSPACE_OVERLAY_ROUTES.has(route.name)); + const effectiveState = + routes.length > 0 && routes.length !== state.routes.length + ? ({ ...state, routes, index: routes.length - 1 } as NavigationState) + : state; + const path = getPathFromState(effectiveState, navigationPathConfig); + return path.startsWith("/") ? path : `/${path}`; +} + function RootStackLayout(props: { readonly children: React.ReactNode; readonly state: NavigationState; }) { useAgentNotificationNavigation(); useThreadOutboxDrain(); + // Full pathname (sheets included) for keyboard-command scoping; the + // workspace layout only reacts to the underlying non-overlay route. const path = getPathFromState(props.state, navigationPathConfig); const pathname = path.startsWith("/") ? path : `/${path}`; + const workspacePathname = workspacePathFromState(props.state); return ( - {props.children} + + {props.children} + ); diff --git a/apps/mobile/src/components/EmptyState.tsx b/apps/mobile/src/components/EmptyState.tsx index 4ba21e1632d..ce068bc4613 100644 --- a/apps/mobile/src/components/EmptyState.tsx +++ b/apps/mobile/src/components/EmptyState.tsx @@ -13,7 +13,7 @@ export function EmptyState(props: { return ( {props.title} - + {props.detail} {props.actionLabel && props.onAction ? ( @@ -33,7 +33,7 @@ export function EmptyState(props: { return ( {props.title} - + {props.detail} {props.actionLabel && props.onAction ? ( diff --git a/apps/mobile/src/components/ErrorBanner.tsx b/apps/mobile/src/components/ErrorBanner.tsx index d47f924b398..76e06edcd16 100644 --- a/apps/mobile/src/components/ErrorBanner.tsx +++ b/apps/mobile/src/components/ErrorBanner.tsx @@ -4,7 +4,7 @@ import { AppText as Text } from "./AppText"; export function ErrorBanner(props: { readonly message: string }) { return ( - + {props.message} diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index d6b93cf0d89..eeec46333a8 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -305,7 +305,7 @@ function ArchivedThreadRow(props: { {props.thread.title} @@ -358,7 +358,7 @@ function ArchiveError(props: { readonly message: string; readonly onRetry: () => Could not load every archive - {props.message} + {props.message} Try again diff --git a/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx b/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx index 4d5b5703329..dbaf564e91f 100644 --- a/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx +++ b/apps/mobile/src/features/cloud/CloudWaitlistEnrollment.tsx @@ -2,7 +2,6 @@ import { useWaitlist } from "@clerk/expo"; import { ActivityIndicator, Pressable, StyleSheet, Text, TextInput, View } from "react-native"; import { useState } from "react"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { useThemeColor } from "../../lib/useThemeColor"; import { CloudWaitlistJoinRejectedError, joinCloudWaitlist } from "./cloudWaitlistJoin"; @@ -36,8 +35,10 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } if (waitlist.id) { return ( - You are on the waitlist - + + You are on the waitlist + + We will email you when your T3 Cloud access is ready. @@ -47,17 +48,18 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } return ( - + Enter your email and we will let you know when access is ready. - Email address + Email address { setEmailAddress(value); @@ -73,7 +75,6 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } backgroundColor: colors.input, borderColor: fieldError || requestError ? colors.dangerForeground : colors.inputBorder, - color: colors.foreground, }, ]} textContentType="emailAddress" @@ -82,7 +83,7 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } {fieldError || requestError ? ( {fieldError ?? requestError} @@ -107,7 +108,7 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } ]} > {isSubmitting ? : null} - + {isSubmitting ? "Joining" : "Join the waitlist"} @@ -118,12 +119,11 @@ export function CloudWaitlistEnrollment(props: { readonly onSignIn: () => void } } function SignInAction(props: { readonly onPress: () => void }) { - const colors = useCloudWaitlistColors(); return ( - Already have access? + Already have access? - Sign in + Sign in ); @@ -132,35 +132,18 @@ function SignInAction(props: { readonly onPress: () => void }) { function useCloudWaitlistColors() { return { dangerForeground: String(useThemeColor("--color-danger-foreground")), - foreground: String(useThemeColor("--color-foreground")), input: String(useThemeColor("--color-input")), inputBorder: String(useThemeColor("--color-input-border")), placeholder: String(useThemeColor("--color-placeholder")), primary: String(useThemeColor("--color-primary")), primaryForeground: String(useThemeColor("--color-primary-foreground")), - secondaryForeground: String(useThemeColor("--color-foreground-secondary")), }; } const styles = StyleSheet.create({ - body: { - fontFamily: "DMSans_400Regular", - ...MOBILE_TYPOGRAPHY.body, - }, - buttonText: { - fontFamily: "DMSans_700Bold", - fontSize: MOBILE_TYPOGRAPHY.body.fontSize, - }, content: { gap: 18, }, - confirmationBody: { - textAlign: "center", - }, - error: { - fontFamily: "DMSans_400Regular", - ...MOBILE_TYPOGRAPHY.footnote, - }, field: { gap: 8, }, @@ -168,16 +151,10 @@ const styles = StyleSheet.create({ borderCurve: "continuous", borderRadius: 16, borderWidth: 1, - fontFamily: "DMSans_400Regular", - fontSize: MOBILE_TYPOGRAPHY.headline.fontSize, minHeight: 54, paddingHorizontal: 16, paddingVertical: 14, }, - label: { - fontFamily: "DMSans_700Bold", - ...MOBILE_TYPOGRAPHY.footnote, - }, primaryButton: { alignItems: "center", borderRadius: 999, @@ -195,13 +172,4 @@ const styles = StyleSheet.create({ justifyContent: "center", paddingTop: 4, }, - signInText: { - fontFamily: "DMSans_700Bold", - ...MOBILE_TYPOGRAPHY.body, - }, - title: { - fontFamily: "DMSans_700Bold", - ...MOBILE_TYPOGRAPHY.title, - textAlign: "center", - }, }); diff --git a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx index 7b901ec4c66..394bc180183 100644 --- a/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx +++ b/apps/mobile/src/features/connection/ConnectionEnvironmentRow.tsx @@ -76,16 +76,16 @@ export function ConnectionEnvironmentRow(props: { /> - + {props.environment.environmentLabel} - + {props.environment.displayUrl} {statusLabel ? ( {props.environment.isRelayManaged ? ( - + Managed by T3 Cloud. Tunnel details update automatically. ) : ( diff --git a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx index 7fe43755697..af4431c6a66 100644 --- a/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx +++ b/apps/mobile/src/features/connection/ConnectionsNewRouteScreen.tsx @@ -183,7 +183,7 @@ export function ConnectionsNewRouteScreen({ className="items-center gap-3 rounded-[24px] bg-card px-5 py-8" style={{ borderCurve: "continuous" }} > - + Camera permission is required to scan a QR code. - + No environments connected yet.{"\n"}Tap{" "} + to add one. diff --git a/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx b/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx index 373b0d3ef03..519dafb004c 100644 --- a/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx +++ b/apps/mobile/src/features/connection/EnvironmentConnectionNotice.tsx @@ -73,10 +73,10 @@ export function EnvironmentConnectionNotice(props: { /> )} - + {noticeTitle(props.connection.phase, props.environmentLabel)} - + {noticeDetail(props.connection.phase, props.resourceName, props.connection.error)} {props.connection.traceId ? ( <> diff --git a/apps/mobile/src/features/files/FileMarkdownPreview.tsx b/apps/mobile/src/features/files/FileMarkdownPreview.tsx index e7579dfb155..4109ce2a999 100644 --- a/apps/mobile/src/features/files/FileMarkdownPreview.tsx +++ b/apps/mobile/src/features/files/FileMarkdownPreview.tsx @@ -8,8 +8,12 @@ import { import { RefreshControl, ScrollView, Text as NativeText, View } from "react-native"; import { tryOpenExternalUrl } from "../../lib/openExternalUrl"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; +import { + resolveMarkdownFontSizes, + resolveNativeMarkdownTypography, +} from "../../lib/appearancePreferences"; import { useThemeColor } from "../../lib/useThemeColor"; +import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { hasNativeSelectableMarkdownText, SelectableMarkdownText, @@ -24,6 +28,15 @@ interface MarkdownPreviewStyles { } function useMarkdownPreviewStyles(): MarkdownPreviewStyles { + const { appearance } = useAppearancePreferences(); + const markdownFontSizes = useMemo( + () => resolveMarkdownFontSizes(appearance.baseFontSize), + [appearance.baseFontSize], + ); + const nativeMarkdownTypography = useMemo( + () => resolveNativeMarkdownTypography(appearance.baseFontSize), + [appearance.baseFontSize], + ); const body = String(useThemeColor("--color-md-body")); const strong = String(useThemeColor("--color-md-strong")); const link = String(useThemeColor("--color-md-link")); @@ -74,7 +87,8 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { text: { color: body, fontFamily: "DMSans_400Regular", - ...MOBILE_TYPOGRAPHY.body, + fontSize: markdownFontSizes.m, + lineHeight: markdownFontSizes.bodyLineHeight, }, heading: { color: strong, @@ -124,7 +138,9 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { skillTextColor: codeText, quoteMarkerColor: blockquoteBorder, dividerColor: horizontalRule, - ...MOBILE_TYPOGRAPHY.body, + fontSize: nativeMarkdownTypography.fontSize, + lineHeight: nativeMarkdownTypography.lineHeight, + headingFontSizes: nativeMarkdownTypography.headingFontSizes, fontFamily: "DMSans_400Regular", headingFontFamily: "DMSans_700Bold", boldFontFamily: "DMSans_700Bold", @@ -138,6 +154,8 @@ function useMarkdownPreviewStyles(): MarkdownPreviewStyles { codeText, horizontalRule, link, + markdownFontSizes, + nativeMarkdownTypography, strong, ]); } diff --git a/apps/mobile/src/features/files/FileTreeBrowser.tsx b/apps/mobile/src/features/files/FileTreeBrowser.tsx index 1d6fe43dcba..e0a9808bc3c 100644 --- a/apps/mobile/src/features/files/FileTreeBrowser.tsx +++ b/apps/mobile/src/features/files/FileTreeBrowser.tsx @@ -93,7 +93,7 @@ const FileTreeRow = memo(function FileTreeRow(props: { Files unavailable - {props.error} + {props.error} ); } @@ -269,7 +269,7 @@ export function FileTreeBrowser(props: { ) : ( <> No files found - + {props.searchQuery.trim().length > 0 ? "Try a different search." : "The workspace file index is empty."} diff --git a/apps/mobile/src/features/files/SourceFileSurface.tsx b/apps/mobile/src/features/files/SourceFileSurface.tsx index 7e4e8160c96..9774130eb2e 100644 --- a/apps/mobile/src/features/files/SourceFileSurface.tsx +++ b/apps/mobile/src/features/files/SourceFileSurface.tsx @@ -2,7 +2,14 @@ import { useAtomValue } from "@effect/atom-react"; import { AsyncResult } from "effect/unstable/reactivity"; import type { ComponentType } from "react"; import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { FlatList, ScrollView, Text as NativeText, useColorScheme, View } from "react-native"; +import { + FlatList, + ScrollView, + Text as NativeText, + useColorScheme, + useWindowDimensions, + View, +} from "react-native"; import { AppText as Text } from "../../components/AppText"; import { LoadingStrip } from "../../components/LoadingStrip"; @@ -11,28 +18,19 @@ import { resolveNativeReviewDiffView, } from "../diffs/nativeReviewDiffSurface"; import { createNativeReviewDiffTheme } from "../review/nativeReviewDiffAdapter"; -import { - REVIEW_DIFF_LINE_HEIGHT, - REVIEW_MONO_FONT_FAMILY, - renderVisibleWhitespace, -} from "../review/reviewDiffRendering"; +import { REVIEW_MONO_FONT_FAMILY, renderVisibleWhitespace } from "../review/reviewDiffRendering"; import type { ReviewHighlightedToken } from "../review/shikiReviewHighlighter"; import { cn } from "../../lib/cn"; -import { MOBILE_CODE_SURFACE } from "../../lib/typography"; +import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences"; +import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { 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; -const SOURCE_LINE_NUMBER_WIDTH = MOBILE_CODE_SURFACE.gutterWidth; -const NATIVE_SOURCE_STYLE_JSON = JSON.stringify(NATIVE_SOURCE_STYLE); - interface SourceFileSurfaceProps { readonly contents: string; readonly path: string; @@ -44,36 +42,38 @@ interface SourceFileSurfaceProps { type SourceHighlightStatus = "highlighting" | "ready" | "error"; const HighlightedSourceLine = memo(function HighlightedSourceLine(props: { + readonly codeSurface: ResolvedMobileCodeSurface; readonly index: number; readonly line: string; readonly tokens: ReadonlyArray | null; readonly highlighted: boolean; + readonly wordBreak: boolean; }) { return ( {props.index + 1} {props.tokens && props.tokens.length > 0 @@ -158,6 +158,8 @@ function NativeSourceFileSurface( }, ) { const { NativeView, onRefresh } = props; + const { codeSurface, codeWordBreak, nativeSourceStyle } = useAppearanceCodeSurface(); + const { width: viewportWidth } = useWindowDimensions(); const { rowsJson, status, targetIndex, theme, tokens } = useSourceFileModel(props); const [isPullRefreshing, setIsPullRefreshing] = useState(false); const handlePullToRefresh = useCallback(async () => { @@ -177,6 +179,10 @@ function NativeSourceFileSurface( [targetIndex], ); const themeJson = useMemo(() => JSON.stringify(createNativeReviewDiffTheme(theme)), [theme]); + const styleJson = useMemo(() => JSON.stringify(nativeSourceStyle), [nativeSourceStyle]); + const contentWidth = codeWordBreak + ? Math.max(240, viewportWidth - codeSurface.gutterWidth - 24) + : NATIVE_SOURCE_CONTENT_WIDTH; return ( @@ -187,12 +193,12 @@ function NativeSourceFileSurface( style={{ flex: 1 }} appearanceScheme={theme} contentResetKey={props.path} - contentWidth={NATIVE_SOURCE_CONTENT_WIDTH} + contentWidth={contentWidth} initialRowIndex={targetIndex ?? -1} - rowHeight={NATIVE_SOURCE_ROW_HEIGHT} + rowHeight={nativeSourceStyle.rowHeight ?? codeSurface.rowHeight} rowsJson={rowsJson} selectedRowIdsJson={selectedRowIdsJson} - styleJson={NATIVE_SOURCE_STYLE_JSON} + styleJson={styleJson} themeJson={themeJson} tokensJson={tokensJson} {...(onRefresh @@ -207,6 +213,7 @@ function NativeSourceFileSurface( } function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { + const { codeSurface, codeWordBreak } = useAppearanceCodeSurface(); const { lines, status, targetIndex, tokens } = useSourceFileModel(props); const listRef = useRef>(null); @@ -223,39 +230,53 @@ function JavaScriptSourceFileSurface(props: SourceFileSurfaceProps) { const renderLine = useCallback( ({ item, index }: { item: string; index: number }) => ( ), - [targetIndex, tokens], + [codeSurface, codeWordBreak, targetIndex, tokens], + ); + + const list = ( + String(index)} + initialNumToRender={80} + maxToRenderPerBatch={80} + windowSize={12} + {...(codeWordBreak + ? {} + : { + getItemLayout: (_data, index) => ({ + length: codeSurface.rowHeight, + offset: codeSurface.rowHeight * index, + index, + }), + })} + contentContainerStyle={{ + minWidth: codeWordBreak ? undefined : "100%", + paddingBottom: codeSurface.rowHeight, + paddingTop: 8, + }} + renderItem={renderLine} + /> ); return ( - - String(index)} - initialNumToRender={80} - maxToRenderPerBatch={80} - windowSize={12} - getItemLayout={(_data, index) => ({ - length: SOURCE_LINE_HEIGHT, - offset: SOURCE_LINE_HEIGHT * index, - index, - })} - contentContainerStyle={{ - minWidth: "100%", - paddingBottom: REVIEW_DIFF_LINE_HEIGHT, - paddingTop: 8, - }} - renderItem={renderLine} - /> - + {codeWordBreak ? ( + list + ) : ( + + {list} + + )} ); } diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 56213e9224b..761bfd71e9f 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -141,7 +141,7 @@ function FileContent(props: { Partial file - + Preview limited to the first 1 MB of a truncated file. diff --git a/apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx b/apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx index 6d03a23d52a..efa7ee88cba 100644 --- a/apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx +++ b/apps/mobile/src/features/files/WorkspaceFileWebPreview.tsx @@ -24,7 +24,7 @@ export function WorkspaceFileWebPreview(props: { readonly uri: string | null }) {loadError ? ( Preview failed - {loadError} + {loadError} ) : null} + + navigation.navigate("NewTaskSheet", { screen: "NewTask" })} + /> + } + /> + navigation.navigate("NewTaskSheet", { screen: "NewTask" })} + /> + + ); + } + return ( <> + {/* Restore the compact title in case the split branch blanked it. */} + {props.thread.title} diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 7977e13d067..306a137af04 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -110,7 +110,10 @@ export function AdaptiveWorkspaceLayout(props: { useState(null); const baseLayout = useMemo(() => deriveLayout({ width, height }), [height, width]); const layout = baseLayout; - const shouldRenderPrimarySidebar = layout.usesSplitView && pathname !== "/"; + // In split layouts the sidebar IS the thread list — it renders on every + // route, including Home (which shows an empty-detail pane instead of the + // compact list). + const shouldRenderPrimarySidebar = layout.usesSplitView; const fileInspector = useMemo( () => deriveFileInspectorPaneLayout({ @@ -273,9 +276,6 @@ export function AdaptiveWorkspaceLayout(props: { const handleOpenSettings = useCallback(() => { navigation.navigate("SettingsSheet", { screen: "Settings" }); }, [navigation]); - const handleStartNewTask = useCallback(() => { - navigation.navigate("NewTaskSheet", { screen: "NewTask" }); - }, [navigation]); const renderedSidebarWidth = useSharedValue( panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0, @@ -344,7 +344,6 @@ export function AdaptiveWorkspaceLayout(props: { onOpenSettings={handleOpenSettings} onSelectThread={handleSelectThread} onSearchQueryChange={setPrimarySidebarSearchQuery} - onStartNewTask={handleStartNewTask} searchQuery={primarySidebarSearchQuery} /> diff --git a/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx b/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx index 9d052a89a20..e21920977bc 100644 --- a/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx +++ b/apps/mobile/src/features/layout/WorkspaceEmptyDetail.tsx @@ -1,10 +1,10 @@ import { SymbolView } from "expo-symbols"; -import { View } from "react-native"; +import { Pressable, View } from "react-native"; import { AppText as Text } from "../../components/AppText"; import { useThemeColor } from "../../lib/useThemeColor"; -export function WorkspaceEmptyDetail() { +export function WorkspaceEmptyDetail(props: { readonly onStartNewTask?: () => void }) { const iconColor = useThemeColor("--color-icon-subtle"); return ( @@ -15,6 +15,15 @@ export function WorkspaceEmptyDetail() { Choose a thread from the sidebar or start a new task. + {props.onStartNewTask ? ( + + New Task + + ) : null} ); diff --git a/apps/mobile/src/features/projects/AddProjectScreen.tsx b/apps/mobile/src/features/projects/AddProjectScreen.tsx index af730307ab4..841b96f70e2 100644 --- a/apps/mobile/src/features/projects/AddProjectScreen.tsx +++ b/apps/mobile/src/features/projects/AddProjectScreen.tsx @@ -168,9 +168,9 @@ function ListRow(props: { {props.icon} - {props.title} + {props.title} {props.subtitle ? ( - + {props.subtitle} ) : null} @@ -215,7 +215,7 @@ function ProjectPathInput(props: { }) { return ( No environments connected - + Add an environment before adding a project. {error ? : null} { @@ -169,7 +170,7 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp {!target ? ( No selection - + Select a diff line or range first. @@ -180,7 +181,7 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp {selectionLabel} @@ -213,9 +214,9 @@ export function ReviewCommentComposerSheet(props: ReviewCommentComposerSheetProp - + diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 28f4dc06f67..1efbac6dff3 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -43,10 +43,8 @@ import { type NativeReviewDiffViewHandle, resolveNativeReviewDiffView, } from "../diffs/nativeReviewDiffSurface"; -import { - NATIVE_REVIEW_DIFF_CONTENT_WIDTH, - NATIVE_REVIEW_DIFF_ROW_HEIGHT, -} from "./nativeReviewDiffAdapter"; +import { NATIVE_REVIEW_DIFF_CONTENT_WIDTH } from "./nativeReviewDiffAdapter"; +import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { useReviewDiffData } from "./useReviewDiffData"; import { useReviewDiffPrewarming } from "./useReviewDiffPrewarming"; import { useReviewFileVisibility } from "./reviewFileVisibility"; @@ -65,7 +63,7 @@ const ReviewNotice = memo(function ReviewNotice(props: { readonly notice: string Partial diff - + {props.notice} @@ -287,6 +285,7 @@ type ReviewSheetProps = StaticScreenProps<{ }>; export function ReviewSheet(props: ReviewSheetProps) { + const { nativeReviewDiffStyle } = useAppearanceCodeSurface(); useAdaptiveWorkspacePaneRole("inspector"); const { layout, panes, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar } = useAdaptiveWorkspaceLayout(); @@ -447,7 +446,7 @@ export function ReviewSheet(props: ReviewSheetProps) { children.push( Review unavailable - {error} + {error} , ); } @@ -611,7 +610,7 @@ export function ReviewSheet(props: ReviewSheetProps) { contentResetKey={`${reviewCache.threadKey}:${selectedSection.id}`} contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH} nativeViewRef={nativeReviewDiffViewRef} - rowHeight={NATIVE_REVIEW_DIFF_ROW_HEIGHT} + rowHeight={nativeReviewDiffStyle.rowHeight} rowsJson={nativeBridge.rowsJson} selectedRowIdsJson={nativeBridge.selectedRowIdsJson} styleJson={nativeBridge.styleJson} @@ -646,7 +645,7 @@ export function ReviewSheet(props: ReviewSheetProps) { {!selectedSection ? ( No review diffs - + This thread has no ready turn diffs and the worktree diff is empty. @@ -658,17 +657,17 @@ export function ReviewSheet(props: ReviewSheetProps) { ) : parsedDiff.kind === "empty" ? ( No changes - + {selectedSection.subtitle ?? "This diff is empty."} ) : parsedDiff.kind === "raw" ? ( - + {parsedDiff.reason} - + {parsedDiff.text} diff --git a/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts b/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts index a5b5f2de2f0..6d82940bb02 100644 --- a/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts +++ b/apps/mobile/src/features/review/nativeReviewDiffAdapter.ts @@ -5,6 +5,8 @@ import type { } from "../diffs/nativeReviewDiffTypes"; import * as Arr from "effect/Array"; import { pipe } from "effect/Function"; +import type { ResolvedMobileCodeSurface } from "../../lib/appearancePreferences"; +import { resolveMobileCodeSurface } from "../../lib/appearancePreferences"; import { MOBILE_CODE_SURFACE } from "../../lib/typography"; import { getPierreTerminalTheme, type TerminalAppearanceScheme } from "../terminal/terminalTheme"; import { computeWordAltDiffRanges } from "./reviewWordDiffs"; @@ -22,38 +24,44 @@ const NATIVE_REVIEW_MAX_WORD_DIFF_COVERAGE = 0.45; export const NATIVE_REVIEW_DIFF_ROW_HEIGHT = MOBILE_CODE_SURFACE.rowHeight; export const NATIVE_REVIEW_DIFF_CONTENT_WIDTH = 2_800; -export const NATIVE_REVIEW_DIFF_STYLE = { - rowHeight: NATIVE_REVIEW_DIFF_ROW_HEIGHT, - contentWidth: NATIVE_REVIEW_DIFF_CONTENT_WIDTH, - changeBarWidth: 4, - gutterWidth: MOBILE_CODE_SURFACE.gutterWidth, - codePadding: MOBILE_CODE_SURFACE.codePadding, - textVerticalInset: MOBILE_CODE_SURFACE.textVerticalInset, - fileHeaderHeight: 56, - fileHeaderHorizontalMargin: 8, - fileHeaderVerticalMargin: 6, - fileHeaderCornerRadius: 10, - fileHeaderHorizontalPadding: 10, - fileHeaderPathRightPadding: 118, - fileHeaderCountColumnWidth: 38, - fileHeaderCountGap: 5, - codeFontSize: MOBILE_CODE_SURFACE.fontSize, - codeFontWeight: "regular", - lineNumberFontSize: MOBILE_CODE_SURFACE.lineNumberFontSize, - lineNumberFontWeight: "regular", - hunkFontSize: 11, - hunkFontWeight: "medium", - fileHeaderFontSize: 11, - fileHeaderFontWeight: "semibold", - fileHeaderMetaFontSize: 10, - fileHeaderMetaFontWeight: "semibold", - fileHeaderSubtextFontSize: 11, - fileHeaderSubtextFontWeight: "medium", - fileHeaderStatusFontSize: 9, - fileHeaderStatusFontWeight: "bold", - emptyStateFontSize: 12, - emptyStateFontWeight: "medium", -} as const; +export const NATIVE_REVIEW_DIFF_STYLE = createNativeReviewDiffStyle( + resolveMobileCodeSurface(MOBILE_CODE_SURFACE.fontSize), +); + +export function createNativeReviewDiffStyle(codeSurface: ResolvedMobileCodeSurface) { + return { + rowHeight: codeSurface.rowHeight, + contentWidth: NATIVE_REVIEW_DIFF_CONTENT_WIDTH, + changeBarWidth: 4, + gutterWidth: codeSurface.gutterWidth, + codePadding: codeSurface.codePadding, + textVerticalInset: codeSurface.textVerticalInset, + fileHeaderHeight: 56, + fileHeaderHorizontalMargin: 8, + fileHeaderVerticalMargin: 6, + fileHeaderCornerRadius: 10, + fileHeaderHorizontalPadding: 10, + fileHeaderPathRightPadding: 118, + fileHeaderCountColumnWidth: 38, + fileHeaderCountGap: 5, + codeFontSize: codeSurface.fontSize, + codeFontWeight: "regular", + lineNumberFontSize: codeSurface.lineNumberFontSize, + lineNumberFontWeight: "regular", + hunkFontSize: 11, + hunkFontWeight: "medium", + fileHeaderFontSize: 11, + fileHeaderFontWeight: "semibold", + fileHeaderMetaFontSize: 10, + fileHeaderMetaFontWeight: "semibold", + fileHeaderSubtextFontSize: 11, + fileHeaderSubtextFontWeight: "medium", + fileHeaderStatusFontSize: 9, + fileHeaderStatusFontWeight: "bold", + emptyStateFontSize: 12, + emptyStateFontWeight: "medium", + } as const; +} export interface NativeReviewDiffData { readonly rows: ReadonlyArray; diff --git a/apps/mobile/src/features/review/useNativeReviewDiffBridge.test.ts b/apps/mobile/src/features/review/reviewDiffBridgeKeys.test.ts similarity index 96% rename from apps/mobile/src/features/review/useNativeReviewDiffBridge.test.ts rename to apps/mobile/src/features/review/reviewDiffBridgeKeys.test.ts index cb28a30d92e..9b39a51dc90 100644 --- a/apps/mobile/src/features/review/useNativeReviewDiffBridge.test.ts +++ b/apps/mobile/src/features/review/reviewDiffBridgeKeys.test.ts @@ -1,6 +1,6 @@ import { describe, expect, it } from "vite-plus/test"; -import { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./useNativeReviewDiffBridge"; +import { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./reviewDiffBridgeKeys"; describe("native review diff bridge", () => { it("builds stable reset keys from the rendered diff identity", () => { diff --git a/apps/mobile/src/features/review/reviewDiffBridgeKeys.ts b/apps/mobile/src/features/review/reviewDiffBridgeKeys.ts new file mode 100644 index 00000000000..d04534003b3 --- /dev/null +++ b/apps/mobile/src/features/review/reviewDiffBridgeKeys.ts @@ -0,0 +1,35 @@ +import type { NativeReviewDiffHighlightScheme } from "../diffs/nativeReviewDiffHighlighter"; + +// Pure key-derivation helpers for the native review diff bridge. Kept free of +// react-native / hook imports so they stay unit-testable in node. + +export function hashReviewDiffKey(diff: string | null | undefined): string { + if (!diff) { + return "empty"; + } + + let hash = 5381; + for (let index = 0; index < diff.length; index += 1) { + hash = (hash * 33) ^ diff.charCodeAt(index); + } + + return `${diff.length}:${(hash >>> 0).toString(36)}`; +} + +export function buildNativeReviewTokensResetKey(input: { + readonly threadKey: string | null; + readonly sectionId: string | null; + readonly scheme: NativeReviewDiffHighlightScheme; + readonly diff: string | null | undefined; + readonly fileCount: number; + readonly rowCount: number; +}): string { + return [ + input.threadKey ?? "none", + input.sectionId ?? "none", + input.scheme, + hashReviewDiffKey(input.diff), + input.fileCount, + input.rowCount, + ].join(":"); +} diff --git a/apps/mobile/src/features/review/reviewDiffRendering.tsx b/apps/mobile/src/features/review/reviewDiffRendering.tsx index 14ff0276657..d7f5c5ff8a8 100644 --- a/apps/mobile/src/features/review/reviewDiffRendering.tsx +++ b/apps/mobile/src/features/review/reviewDiffRendering.tsx @@ -13,7 +13,6 @@ export const REVIEW_MONO_FONT_FAMILY = Platform.select({ }); export const REVIEW_DIFF_LINE_HEIGHT = MOBILE_CODE_SURFACE.rowHeight; -const REVIEW_DELETE_STRIPE_COUNT = REVIEW_DIFF_LINE_HEIGHT / 2; export function renderVisibleWhitespace(value: string): string { const expandedTabs = value.replace(/\t/g, " "); @@ -38,12 +37,16 @@ function diffHighlightColor(change: ReviewRenderableLineRow["change"]): string | return undefined; } -export function ReviewChangeBar(props: { readonly change: ReviewRenderableLineRow["change"] }) { +export function ReviewChangeBar(props: { + readonly change: ReviewRenderableLineRow["change"]; + readonly height?: number; +}) { + const height = props.height ?? REVIEW_DIFF_LINE_HEIGHT; if (props.change === "delete") { return ( - + - {Array.from({ length: REVIEW_DELETE_STRIPE_COUNT }, (_, index) => ( + {Array.from({ length: Math.ceil(height / 2) }, (_, index) => ( @@ -55,7 +58,7 @@ export function ReviewChangeBar(props: { readonly change: ReviewRenderableLineRo } return ( - + ); @@ -66,7 +69,11 @@ export function DiffTokenText(props: { readonly fallback: string; readonly change?: ReviewRenderableLineRow["change"]; readonly className?: string; + readonly fontSize?: number; + readonly lineHeight?: number; }) { + const fontSize = props.fontSize ?? MOBILE_CODE_SURFACE.fontSize; + const lineHeight = props.lineHeight ?? MOBILE_CODE_SURFACE.rowHeight; if (!props.tokens || props.tokens.length === 0) { return ( {renderVisibleWhitespace(props.fallback || " ")} @@ -91,8 +98,8 @@ export function DiffTokenText(props: { className={cn("font-normal text-foreground", props.className)} style={{ fontFamily: REVIEW_MONO_FONT_FAMILY, - fontSize: MOBILE_CODE_SURFACE.fontSize, - lineHeight: MOBILE_CODE_SURFACE.rowHeight, + fontSize, + lineHeight, }} > {(() => { diff --git a/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts b/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts index d0030e51a4a..d28e45844f6 100644 --- a/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts +++ b/apps/mobile/src/features/review/useNativeReviewDiffBridge.ts @@ -2,43 +2,12 @@ import { useCallback, useMemo, useState } from "react"; import type { NativeSyntheticEvent } from "react-native"; import { type NativeReviewDiffHighlightScheme } from "../diffs/nativeReviewDiffHighlighter"; -import { - createNativeReviewDiffTheme, - NATIVE_REVIEW_DIFF_STYLE, - type NativeReviewDiffData, -} from "./nativeReviewDiffAdapter"; +import { createNativeReviewDiffTheme, type NativeReviewDiffData } from "./nativeReviewDiffAdapter"; +import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { useNativeReviewDiffHighlighting } from "./useNativeReviewDiffHighlighting"; +import { buildNativeReviewTokensResetKey } from "./reviewDiffBridgeKeys"; -export function hashReviewDiffKey(diff: string | null | undefined): string { - if (!diff) { - return "empty"; - } - - let hash = 5381; - for (let index = 0; index < diff.length; index += 1) { - hash = (hash * 33) ^ diff.charCodeAt(index); - } - - return `${diff.length}:${(hash >>> 0).toString(36)}`; -} - -export function buildNativeReviewTokensResetKey(input: { - readonly threadKey: string | null; - readonly sectionId: string | null; - readonly scheme: NativeReviewDiffHighlightScheme; - readonly diff: string | null | undefined; - readonly fileCount: number; - readonly rowCount: number; -}): string { - return [ - input.threadKey ?? "none", - input.sectionId ?? "none", - input.scheme, - hashReviewDiffKey(input.diff), - input.fileCount, - input.rowCount, - ].join(":"); -} +export { buildNativeReviewTokensResetKey, hashReviewDiffKey } from "./reviewDiffBridgeKeys"; export function useNativeReviewDiffBridge(input: { readonly threadKey: string | null; @@ -62,6 +31,7 @@ export function useNativeReviewDiffBridge(input: { threadKey, viewedFileIds, } = input; + const { nativeReviewDiffStyle } = useAppearanceCodeSurface(); const [collapsedCommentIds, setCollapsedCommentIds] = useState>( () => new Set(), ); @@ -76,7 +46,7 @@ export function useNativeReviewDiffBridge(input: { [collapsedCommentIds], ); const themeJson = useMemo(() => JSON.stringify(theme), [theme]); - const styleJson = useMemo(() => JSON.stringify(NATIVE_REVIEW_DIFF_STYLE), []); + const styleJson = useMemo(() => JSON.stringify(nativeReviewDiffStyle), [nativeReviewDiffStyle]); const tokensResetKey = useMemo( () => buildNativeReviewTokensResetKey({ diff --git a/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx new file mode 100644 index 00000000000..46b7d210236 --- /dev/null +++ b/apps/mobile/src/features/settings/SettingsAppearanceRouteScreen.tsx @@ -0,0 +1,30 @@ +import { ScrollView, View } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; + +import { CodeAppearanceSection } from "./appearance/sections/CodeAppearanceSection"; +import { TerminalAppearanceSection } from "./appearance/sections/TerminalAppearanceSection"; +import { TextAppearanceSection } from "./appearance/sections/TextAppearanceSection"; + +export function SettingsAppearanceRouteScreen() { + const insets = useSafeAreaInsets(); + + return ( + + + + + + + + ); +} diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx index e252098de37..621da8f34a7 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx @@ -109,7 +109,7 @@ export function SettingsEnvironmentsRouteScreen() { type="monochrome" /> - + No environments connected yet.{"\n"}Tap{" "} + to add one. @@ -202,7 +202,7 @@ function ConfiguredCloudEnvironmentRows(props: { ) : controller.relayDiscovery.isRefreshing ? ( - + Loading linked cloud environments. @@ -211,7 +211,7 @@ function ConfiguredCloudEnvironmentRows(props: { Could not load T3 Cloud environments - + {controller.relayDiscovery.error} {controller.relayDiscovery.errorTraceId ? ( @@ -220,7 +220,7 @@ function ConfiguredCloudEnvironmentRows(props: { ) : ( - + No additional linked cloud environments. @@ -359,7 +359,7 @@ function CloudEnvironmentRowShell(props: { {props.label} @@ -368,7 +368,7 @@ function CloudEnvironmentRowShell(props: { {props.connectionError ? ( @@ -382,7 +382,7 @@ function CloudEnvironmentRowShell(props: { className="min-w-0 flex-row items-start gap-1" > {statusText} @@ -392,7 +392,7 @@ function CloudEnvironmentRowShell(props: { { event.stopPropagation(); copyTextWithHaptic(errorTraceId, { target: "connection-trace-id" }); diff --git a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx index f78b2c795ba..11d363dbf69 100644 --- a/apps/mobile/src/features/settings/SettingsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsRouteScreen.tsx @@ -5,8 +5,7 @@ import { NativeStackScreenOptions } from "../../native/StackHeader"; import { SymbolView } from "expo-symbols"; import * as Effect from "effect/Effect"; import { useCallback, useEffect, useMemo, useState } from "react"; -import type { ComponentProps, ReactNode } from "react"; -import { Alert, Linking, Platform, Pressable, ScrollView, Switch, View } from "react-native"; +import { Alert, Linking, Platform, ScrollView, View } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { @@ -29,6 +28,9 @@ import { runtime } from "../../lib/runtime"; import { loadPreferences } from "../../lib/storage"; import { useThemeColor } from "../../lib/useThemeColor"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; +import { SettingsRow } from "./components/SettingsRow"; +import { SettingsSection } from "./components/SettingsSection"; +import { SettingsSwitchRow } from "./components/SettingsSwitchRow"; type NotificationStatus = "checking" | "enabled" | "disabled" | "unsupported"; type LiveActivityStatus = "checking" | "enabled" | "disabled" | "signed-out" | "linking"; @@ -88,6 +90,10 @@ function LocalSettingsRouteScreen() { /> + + + + @@ -371,7 +377,7 @@ function ConfiguredSettingsRouteScreen() { onPress={openAccount} /> - + T3 Code works locally without signing in. Cloud features are optional. @@ -401,6 +407,10 @@ function ConfiguredSettingsRouteScreen() { /> + + + + @@ -409,22 +419,6 @@ function ConfiguredSettingsRouteScreen() { ); } -type SymbolName = ComponentProps["name"]; - -function SettingsSection(props: { readonly title: string; readonly children: ReactNode }) { - return ( - - {props.title} - - {props.children} - - - ); -} - function AppSettingsSection() { const icon = useThemeColor("--color-icon"); @@ -452,98 +446,3 @@ function ArchivedThreadsSettingsSection() { ); } - -function SettingsRow(props: { - readonly disabled?: boolean; - readonly icon: SymbolName; - readonly label: string; - readonly value?: string; - readonly target?: "SettingsEnvironments" | "SettingsArchive"; - readonly onPress?: () => void; -}) { - const navigation = useNavigation(); - const icon = useThemeColor("--color-icon"); - const chevron = useThemeColor("--color-chevron"); - const content = ( - - - - {props.label} - - - {props.value ? ( - - {props.value} - - ) : null} - - - - ); - - const target = props.target; - if (target) { - return ( - - navigation.navigate("SettingsSheet", { - screen: target, - }) - } - > - {content} - - ); - } - - return ( - - {content} - - ); -} - -function SettingsSwitchRow(props: { - readonly disabled?: boolean; - readonly icon: SymbolName; - readonly label: string; - readonly value: boolean; - readonly onValueChange: (value: boolean) => void; -}) { - const icon = useThemeColor("--color-icon"); - const activeTrack = String(useThemeColor("--color-switch-active")); - const track = String(useThemeColor("--color-secondary-border")); - - return ( - - - {props.label} - - - ); -} diff --git a/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx b/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx new file mode 100644 index 00000000000..8d8ae322e13 --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/AppearancePreferencesProvider.tsx @@ -0,0 +1,158 @@ +import { + createContext, + useCallback, + useContext, + useEffect, + useMemo, + useState, + type ReactNode, +} from "react"; + +import { Uniwind } from "uniwind"; + +import { + resolveAppearance, + resolveAppearancePreferences, + resolveTextScaleVariables, + type AppearancePreferences, + type ResolvedAppearance, +} from "../../../lib/appearancePreferences"; +import { loadPreferences, savePreferencesPatch } from "../../../lib/storage"; +import { cacheTerminalFontSize } from "../../terminal/terminalUiState"; + +interface AppearancePreferencesContextValue { + /** Effective values with base-size derivation applied. Use this for rendering. */ + readonly appearance: ResolvedAppearance; + readonly isReady: boolean; + readonly setBaseFontSize: (value: number) => void; + /** Pass null to clear the override and follow the base font size. */ + readonly setTerminalFontSize: (value: number | null) => void; + /** Pass null to clear the override and follow the base font size. */ + readonly setCodeFontSize: (value: number | null) => void; + readonly setCodeWordBreak: (value: boolean) => void; +} + +const AppearancePreferencesContext = createContext(null); + +/** + * Injects the scaled `--text-*` variables into Uniwind so every + * className-based text size (`text-sm`, `text-base`, ...) re-resolves live. + * Updates the current theme last so the active stylesheet settles correctly. + */ +function applyTextScaleVariables(baseFontSize: number) { + const variables = resolveTextScaleVariables(baseFontSize); + const currentTheme = Uniwind.currentTheme; + + for (const theme of ["light", "dark"] as const) { + if (theme !== currentTheme) { + Uniwind.updateCSSVariables(theme, variables); + } + } + Uniwind.updateCSSVariables(currentTheme, variables); +} + +export function AppearancePreferencesProvider(props: { readonly children: ReactNode }) { + const [preferences, setPreferences] = useState(() => + resolveAppearancePreferences(null), + ); + const [isReady, setIsReady] = useState(false); + + useEffect(() => { + let cancelled = false; + + void loadPreferences() + .then((stored) => { + if (cancelled) { + return; + } + + const resolved = resolveAppearancePreferences(stored); + setPreferences(resolved); + cacheTerminalFontSize(resolveAppearance(resolved).terminalFontSize); + setIsReady(true); + }) + .catch(() => { + if (cancelled) { + return; + } + + setIsReady(true); + }); + + return () => { + cancelled = true; + }; + }, []); + + useEffect(() => { + applyTextScaleVariables(preferences.baseFontSize); + }, [preferences.baseFontSize]); + + const updatePreferences = useCallback((patch: Partial) => { + setPreferences((current) => { + const next = resolveAppearancePreferences({ ...current, ...patch }); + cacheTerminalFontSize(resolveAppearance(next).terminalFontSize); + void savePreferencesPatch({ + baseFontSize: next.baseFontSize, + terminalFontSize: next.terminalFontSize, + codeFontSize: next.codeFontSize, + codeWordBreak: next.codeWordBreak, + }).catch(() => undefined); + return next; + }); + }, []); + + const setBaseFontSize = useCallback( + (value: number) => { + updatePreferences({ baseFontSize: value }); + }, + [updatePreferences], + ); + + const setTerminalFontSize = useCallback( + (value: number | null) => { + updatePreferences({ terminalFontSize: value }); + }, + [updatePreferences], + ); + + const setCodeFontSize = useCallback( + (value: number | null) => { + updatePreferences({ codeFontSize: value }); + }, + [updatePreferences], + ); + + const setCodeWordBreak = useCallback( + (value: boolean) => { + updatePreferences({ codeWordBreak: value }); + }, + [updatePreferences], + ); + + const value = useMemo( + (): AppearancePreferencesContextValue => ({ + appearance: resolveAppearance(preferences), + isReady, + setBaseFontSize, + setTerminalFontSize, + setCodeFontSize, + setCodeWordBreak, + }), + [preferences, isReady, setBaseFontSize, setTerminalFontSize, setCodeFontSize, setCodeWordBreak], + ); + + return ( + + {props.children} + + ); +} + +export function useAppearancePreferences(): AppearancePreferencesContextValue { + const context = useContext(AppearancePreferencesContext); + if (!context) { + throw new Error("useAppearancePreferences must be used within AppearancePreferencesProvider"); + } + return context; +} diff --git a/apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx b/apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx new file mode 100644 index 00000000000..bb0f749a50c --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx @@ -0,0 +1,81 @@ +import { SymbolView } from "expo-symbols"; +import type { ComponentProps } from "react"; +import { Pressable, View } from "react-native"; + +import { AppText as Text } from "../../../../components/AppText"; +import { useThemeColor } from "../../../../lib/useThemeColor"; + +type SymbolName = ComponentProps["name"]; + +export function FontSizeControlRow(props: { + readonly disabled?: boolean; + readonly icon: SymbolName; + readonly label: string; + readonly valueLabel: string; + readonly canDecrease: boolean; + readonly canIncrease: boolean; + readonly onDecrease: () => void; + readonly onIncrease: () => void; +}) { + const icon = useThemeColor("--color-icon"); + const buttonBackground = String(useThemeColor("--color-secondary")); + const buttonForeground = String(useThemeColor("--color-foreground")); + + return ( + + + {props.label} + + + + {props.valueLabel} + + + + + ); +} + +function FontSizeStepButton(props: { + readonly accessibilityLabel: string; + readonly backgroundColor: string; + readonly disabled: boolean; + readonly foregroundColor: string; + readonly label: string; + readonly onPress: () => void; +}) { + return ( + + + {props.label} + + + ); +} diff --git a/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx new file mode 100644 index 00000000000..378e6d88fbb --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx @@ -0,0 +1,60 @@ +import { useCallback } from "react"; + +import { + MAX_CODE_FONT_SIZE, + MIN_CODE_FONT_SIZE, + stepCodeFontSize, +} from "../../../../lib/appearancePreferences"; +import { SettingsSection } from "../../components/SettingsSection"; +import { SettingsSwitchRow } from "../../components/SettingsSwitchRow"; +import { useAppearancePreferences } from "../AppearancePreferencesProvider"; +import { FontSizeControlRow } from "../components/FontSizeControlRow"; + +export function CodeAppearanceSection() { + const { isReady, appearance, setCodeFontSize, setCodeWordBreak } = useAppearancePreferences(); + const custom = appearance.isCodeFontSizeCustom; + + const handleToggleCustom = useCallback( + (enabled: boolean) => { + setCodeFontSize(enabled ? appearance.codeFontSize : null); + }, + [appearance.codeFontSize, setCodeFontSize], + ); + + const handleDecrease = useCallback(() => { + setCodeFontSize(stepCodeFontSize(appearance.codeFontSize, -1)); + }, [appearance.codeFontSize, setCodeFontSize]); + + const handleIncrease = useCallback(() => { + setCodeFontSize(stepCodeFontSize(appearance.codeFontSize, 1)); + }, [appearance.codeFontSize, setCodeFontSize]); + + return ( + + + MIN_CODE_FONT_SIZE} + canIncrease={appearance.codeFontSize < MAX_CODE_FONT_SIZE} + disabled={!isReady || !custom} + icon="textformat.size" + label="Font size" + onDecrease={handleDecrease} + onIncrease={handleIncrease} + valueLabel={`${appearance.codeFontSize} pt`} + /> + + + ); +} diff --git a/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx new file mode 100644 index 00000000000..fee7228ef08 --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/sections/TerminalAppearanceSection.tsx @@ -0,0 +1,53 @@ +import { useCallback } from "react"; + +import { + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + stepTerminalFontSize, +} from "../../../../lib/appearancePreferences"; +import { SettingsSection } from "../../components/SettingsSection"; +import { SettingsSwitchRow } from "../../components/SettingsSwitchRow"; +import { useAppearancePreferences } from "../AppearancePreferencesProvider"; +import { FontSizeControlRow } from "../components/FontSizeControlRow"; + +export function TerminalAppearanceSection() { + const { isReady, appearance, setTerminalFontSize } = useAppearancePreferences(); + const custom = appearance.isTerminalFontSizeCustom; + + const handleToggleCustom = useCallback( + (enabled: boolean) => { + setTerminalFontSize(enabled ? appearance.terminalFontSize : null); + }, + [appearance.terminalFontSize, setTerminalFontSize], + ); + + const handleDecrease = useCallback(() => { + setTerminalFontSize(stepTerminalFontSize(appearance.terminalFontSize, -1)); + }, [appearance.terminalFontSize, setTerminalFontSize]); + + const handleIncrease = useCallback(() => { + setTerminalFontSize(stepTerminalFontSize(appearance.terminalFontSize, 1)); + }, [appearance.terminalFontSize, setTerminalFontSize]); + + return ( + + + MIN_TERMINAL_FONT_SIZE} + canIncrease={appearance.terminalFontSize < MAX_TERMINAL_FONT_SIZE} + disabled={!isReady || !custom} + icon="textformat.size" + label="Font size" + onDecrease={handleDecrease} + onIncrease={handleIncrease} + valueLabel={`${appearance.terminalFontSize.toFixed(1)} pt`} + /> + + ); +} diff --git a/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx new file mode 100644 index 00000000000..e7c85cce51a --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx @@ -0,0 +1,37 @@ +import { useCallback } from "react"; + +import { + MAX_BASE_FONT_SIZE, + MIN_BASE_FONT_SIZE, + stepBaseFontSize, +} from "../../../../lib/appearancePreferences"; +import { SettingsSection } from "../../components/SettingsSection"; +import { useAppearancePreferences } from "../AppearancePreferencesProvider"; +import { FontSizeControlRow } from "../components/FontSizeControlRow"; + +export function TextAppearanceSection() { + const { isReady, appearance, setBaseFontSize } = useAppearancePreferences(); + + const handleDecrease = useCallback(() => { + setBaseFontSize(stepBaseFontSize(appearance.baseFontSize, -1)); + }, [appearance.baseFontSize, setBaseFontSize]); + + const handleIncrease = useCallback(() => { + setBaseFontSize(stepBaseFontSize(appearance.baseFontSize, 1)); + }, [appearance.baseFontSize, setBaseFontSize]); + + return ( + + MIN_BASE_FONT_SIZE} + canIncrease={appearance.baseFontSize < MAX_BASE_FONT_SIZE} + disabled={!isReady} + icon="textformat.size" + label="Base font size" + onDecrease={handleDecrease} + onIncrease={handleIncrease} + valueLabel={`${appearance.baseFontSize} pt`} + /> + + ); +} diff --git a/apps/mobile/src/features/settings/appearance/useAppearanceCodeSurface.ts b/apps/mobile/src/features/settings/appearance/useAppearanceCodeSurface.ts new file mode 100644 index 00000000000..62760f1e43f --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/useAppearanceCodeSurface.ts @@ -0,0 +1,34 @@ +import { useMemo } from "react"; + +import { + resolveMobileCodeSurface, + type ResolvedMobileCodeSurface, +} from "../../../lib/appearancePreferences"; +import { createNativeReviewDiffStyle } from "../../review/nativeReviewDiffAdapter"; +import { createNativeSourceStyle } from "../../files/nativeSourceFileAdapter"; +import { useAppearancePreferences } from "./AppearancePreferencesProvider"; + +export function useAppearanceCodeSurface(): { + readonly codeSurface: ResolvedMobileCodeSurface; + readonly codeWordBreak: boolean; + readonly nativeSourceStyle: ReturnType; + readonly nativeReviewDiffStyle: ReturnType; +} { + const { appearance } = useAppearancePreferences(); + const codeSurface = useMemo( + () => resolveMobileCodeSurface(appearance.codeFontSize), + [appearance.codeFontSize], + ); + const nativeSourceStyle = useMemo(() => createNativeSourceStyle(codeSurface), [codeSurface]); + const nativeReviewDiffStyle = useMemo( + () => createNativeReviewDiffStyle(codeSurface), + [codeSurface], + ); + + return { + codeSurface, + codeWordBreak: appearance.codeWordBreak, + nativeSourceStyle, + nativeReviewDiffStyle, + }; +} diff --git a/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts b/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts new file mode 100644 index 00000000000..4224740c26f --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/useScaledTextRole.ts @@ -0,0 +1,36 @@ +import { useCSSVariable } from "uniwind"; + +import { MOBILE_TYPOGRAPHY } from "../../../lib/typography"; + +const TEXT_ROLE_VARIABLES = { + micro: "--text-3xs", + caption: "--text-2xs", + label: "--text-xs", + footnote: "--text-sm", + body: "--text-base", + headline: "--text-lg", + title: "--text-xl", + largeTitle: "--text-2xl", + display: "--text-3xl", +} as const satisfies Record; + +export interface ScaledTextRole { + readonly fontSize: number; + readonly lineHeight: number; +} + +/** + * Reads a typography role's current size from the Uniwind `--text-*` CSS + * variables (scaled at runtime with the base font size). Use for style-prop + * consumers that can't express their size as a `text-*` className. Reactive: + * re-renders when the appearance provider re-injects the variables. + */ +export function useScaledTextRole(role: keyof typeof MOBILE_TYPOGRAPHY): ScaledTextRole { + const variable = TEXT_ROLE_VARIABLES[role]; + const [fontSize, lineHeight] = useCSSVariable([variable, `${variable}--line-height`]); + + return { + fontSize: typeof fontSize === "number" ? fontSize : MOBILE_TYPOGRAPHY[role].fontSize, + lineHeight: typeof lineHeight === "number" ? lineHeight : MOBILE_TYPOGRAPHY[role].lineHeight, + }; +} diff --git a/apps/mobile/src/features/settings/components/SettingsRow.tsx b/apps/mobile/src/features/settings/components/SettingsRow.tsx new file mode 100644 index 00000000000..23089c33950 --- /dev/null +++ b/apps/mobile/src/features/settings/components/SettingsRow.tsx @@ -0,0 +1,76 @@ +import { useNavigation } from "@react-navigation/native"; +import { SymbolView } from "expo-symbols"; +import type { ComponentProps } from "react"; +import { Pressable, View } from "react-native"; + +import { AppText as Text } from "../../../components/AppText"; +import { useThemeColor } from "../../../lib/useThemeColor"; +import type { SettingsSheetTarget } from "./settings-sheet-targets"; + +type SymbolName = ComponentProps["name"]; + +export function SettingsRow(props: { + readonly disabled?: boolean; + readonly icon: SymbolName; + readonly label: string; + readonly value?: string; + readonly target?: SettingsSheetTarget; + readonly onPress?: () => void; +}) { + const navigation = useNavigation(); + const icon = useThemeColor("--color-icon"); + const chevron = useThemeColor("--color-chevron"); + const content = ( + + + + {props.label} + + + {props.value ? ( + + {props.value} + + ) : null} + + + + ); + + const target = props.target; + if (target) { + return ( + + navigation.navigate("SettingsSheet", { + screen: target, + }) + } + > + {content} + + ); + } + + return ( + + {content} + + ); +} diff --git a/apps/mobile/src/features/settings/components/SettingsSection.tsx b/apps/mobile/src/features/settings/components/SettingsSection.tsx new file mode 100644 index 00000000000..7b4f0379161 --- /dev/null +++ b/apps/mobile/src/features/settings/components/SettingsSection.tsx @@ -0,0 +1,18 @@ +import type { ReactNode } from "react"; +import { View } from "react-native"; + +import { AppText as Text } from "../../../components/AppText"; + +export function SettingsSection(props: { readonly title: string; readonly children: ReactNode }) { + return ( + + {props.title} + + {props.children} + + + ); +} diff --git a/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx b/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx new file mode 100644 index 00000000000..8620166eff2 --- /dev/null +++ b/apps/mobile/src/features/settings/components/SettingsSwitchRow.tsx @@ -0,0 +1,37 @@ +import { SymbolView } from "expo-symbols"; +import type { ComponentProps } from "react"; +import { Switch, View } from "react-native"; + +import { AppText as Text } from "../../../components/AppText"; +import { useThemeColor } from "../../../lib/useThemeColor"; + +type SymbolName = ComponentProps["name"]; + +export function SettingsSwitchRow(props: { + readonly disabled?: boolean; + readonly icon: SymbolName; + readonly label: string; + readonly value: boolean; + readonly onValueChange: (value: boolean) => void; +}) { + const icon = useThemeColor("--color-icon"); + const activeTrack = String(useThemeColor("--color-switch-active")); + const track = String(useThemeColor("--color-secondary-border")); + + return ( + + + {props.label} + + + ); +} diff --git a/apps/mobile/src/features/settings/components/settings-sheet-targets.ts b/apps/mobile/src/features/settings/components/settings-sheet-targets.ts new file mode 100644 index 00000000000..7de54bceee5 --- /dev/null +++ b/apps/mobile/src/features/settings/components/settings-sheet-targets.ts @@ -0,0 +1 @@ +export type SettingsSheetTarget = "SettingsEnvironments" | "SettingsArchive" | "SettingsAppearance"; diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index 1d42af6ba85..aa89bcaf70c 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -92,9 +92,9 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter > @@ -137,11 +137,11 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter placeholder="type and press return" placeholderTextColor={theme.mutedForeground} returnKeyType="send" + className="text-sm" style={{ color: theme.foreground, flex: 1, fontFamily: "Menlo", - fontSize: MOBILE_TYPOGRAPHY.footnote.fontSize, padding: 0, }} onSubmitEditing={(event) => { @@ -163,10 +163,10 @@ const FallbackTerminalSurface = memo(function FallbackTerminalSurface(props: Ter onPress={() => props.onInput("\u0003")} > Ctrl-C diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 11d64968f13..5614b2c8895 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -26,6 +26,13 @@ import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useWorkspaceState } from "../../state/workspace"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; +import { + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, + stepTerminalFontSize, +} from "../../lib/appearancePreferences"; +import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; import { useAttachedTerminalSession, useKnownTerminalSessions, @@ -36,7 +43,6 @@ import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnection import { useAdaptiveWorkspaceLayout } from "../layout/AdaptiveWorkspaceLayout"; import { TerminalSurface } from "./NativeTerminalSurface"; import { getPierreTerminalTheme } from "./terminalTheme"; -import { loadPreferences, savePreferencesPatch } from "../../lib/storage"; import { terminalDebugLog } from "./terminalDebugLog"; import { getTerminalBufferReplayKey, @@ -56,19 +62,7 @@ import { resolveTerminalSessionLabel, type TerminalMenuSession, } from "./terminalMenu"; -import { - DEFAULT_TERMINAL_FONT_SIZE, - MAX_TERMINAL_FONT_SIZE, - MIN_TERMINAL_FONT_SIZE, - TERMINAL_FONT_SIZE_STEP, - normalizeTerminalFontSize, -} from "./terminalPreferences"; -import { - cacheTerminalFontSize, - cacheTerminalGridSize, - getCachedTerminalFontSize, - getCachedTerminalGridSize, -} from "./terminalUiState"; +import { cacheTerminalGridSize, getCachedTerminalGridSize } from "./terminalUiState"; const DEFAULT_TERMINAL_COLS = 80; const DEFAULT_TERMINAL_ROWS = 24; @@ -224,7 +218,12 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) const isEnvironmentReady = environment.presentation?.connection.phase === "connected"; const requestedTerminalId = firstRouteParam(params.terminalId); const terminalId = requestedTerminalId ?? DEFAULT_TERMINAL_ID; - const cachedFontSize = getCachedTerminalFontSize(); + const { + isReady: hasResolvedFontPreference, + appearance, + setTerminalFontSize, + } = useAppearancePreferences(); + const fontSize = appearance.terminalFontSize; const cachedRouteGridSize = routeEnvironmentId && routeThreadId ? getCachedTerminalGridSize({ @@ -284,7 +283,6 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) rows: DEFAULT_TERMINAL_ROWS, }, ); - const [fontSize, setFontSize] = useState(cachedFontSize ?? DEFAULT_TERMINAL_FONT_SIZE); const [keyboardFocusRequest, setKeyboardFocusRequest] = useState(0); const [isAccessoryDismissed, setIsAccessoryDismissed] = useState(false); const bufferReplayTimerRef = useRef | null>(null); @@ -292,9 +290,6 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) const lastBufferReplayKeyRef = useRef(null); const sentInitialInputKeyRef = useRef(null); const [readyBufferReplayKey, setReadyBufferReplayKey] = useState(null); - const [hasResolvedFontPreference, setHasResolvedFontPreference] = useState( - cachedFontSize !== null, - ); /** Default grid is always valid for attach; onResize refines cols/rows. Requiring a cached size blocked bootstrap for new terminal routes. */ const [hasMeasuredSurface, setHasMeasuredSurface] = useState(true); const [pendingModifierState, setPendingModifierState] = useState<{ @@ -705,42 +700,6 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) setHasMeasuredSurface(true); }, [routeEnvironmentId, routeThreadId, terminalId]); - useEffect(() => { - let cancelled = false; - - void loadPreferences() - .then((preferences) => { - if (cancelled) { - return; - } - - setFontSize(cacheTerminalFontSize(preferences.terminalFontSize)); - setHasResolvedFontPreference(true); - }) - .catch(() => { - if (cancelled) { - return; - } - - setHasResolvedFontPreference(true); - }); - - return () => { - cancelled = true; - }; - }, []); - - useEffect(() => { - if (!hasResolvedFontPreference) { - return; - } - - cacheTerminalFontSize(fontSize); - void savePreferencesPatch({ - terminalFontSize: normalizeTerminalFontSize(fontSize), - }); - }, [fontSize, hasResolvedFontPreference]); - const writeInput = useCallback( (data: string) => { if (!selectedThread || !isRunning) { @@ -868,19 +827,13 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) ); }, [navigation, selectedThread, terminalId, terminalMenuSessions]); - const adjustFontSize = useCallback((delta: number) => { - setTimeout(() => { - setFontSize((current) => cacheTerminalFontSize(current + delta)); - }, 0); - }, []); - const handleDecreaseFontSize = useCallback(() => { - adjustFontSize(-TERMINAL_FONT_SIZE_STEP); - }, [adjustFontSize]); + setTerminalFontSize(stepTerminalFontSize(fontSize, -1)); + }, [fontSize, setTerminalFontSize]); const handleIncreaseFontSize = useCallback(() => { - adjustFontSize(TERMINAL_FONT_SIZE_STEP); - }, [adjustFontSize]); + setTerminalFontSize(stepTerminalFontSize(fontSize, 1)); + }, [fontSize, setTerminalFontSize]); const handleClearTerminal = useCallback(() => { if (!selectedThread) { diff --git a/apps/mobile/src/features/terminal/terminalPreferences.ts b/apps/mobile/src/features/terminal/terminalPreferences.ts index 095876fa2a8..330a31d5f23 100644 --- a/apps/mobile/src/features/terminal/terminalPreferences.ts +++ b/apps/mobile/src/features/terminal/terminalPreferences.ts @@ -1,4 +1,4 @@ -export const DEFAULT_TERMINAL_FONT_SIZE = 10; +export const DEFAULT_TERMINAL_FONT_SIZE = 10.5; export const TERMINAL_FONT_SIZE_STEP = 0.5; export const MIN_TERMINAL_FONT_SIZE = 6; export const MAX_TERMINAL_FONT_SIZE = 14; diff --git a/apps/mobile/src/features/threads/ComposerCommandPopover.tsx b/apps/mobile/src/features/threads/ComposerCommandPopover.tsx index 8b6fe078088..87ac2a6f951 100644 --- a/apps/mobile/src/features/threads/ComposerCommandPopover.tsx +++ b/apps/mobile/src/features/threads/ComposerCommandPopover.tsx @@ -7,8 +7,6 @@ import { Pressable, ScrollView, useColorScheme, View, type ViewStyle } from "rea import { AppText as Text } from "../../components/AppText"; import { PierreEntryIcon } from "../../components/PierreEntryIcon"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; - export type ComposerCommandItem = | { readonly id: string; @@ -165,11 +163,11 @@ const CommandRow = memo(function CommandRow(props: { {props.item.description ? ( diff --git a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx index 482f223a80e..de41e18571f 100644 --- a/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskDraftScreen.tsx @@ -31,7 +31,7 @@ import { resolveProviderOptionDescriptors, } from "../../lib/providerOptions"; import { scopedProjectKey } from "../../lib/scopedEntities"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; +import { useScaledTextRole } from "../settings/appearance/useScaledTextRole"; import { getComposerDraftSnapshot } from "../../state/use-composer-drafts"; import { useProjects } from "../../state/entities"; import { branchBadgeLabel, useNewTaskFlow } from "./new-task-flow-provider"; @@ -68,6 +68,7 @@ export function NewTaskDraftScreen(props: { const loadedBranchesProjectKeyRef = useRef(null); const borderColor = useThemeColor("--color-border"); + const bodyText = useScaledTextRole("body"); const sheetFadeOpaque = colorScheme === "dark" ? "rgba(14,14,14,0.98)" : "rgba(242,242,247,0.98)"; const sheetFadeTransparent = colorScheme === "dark" ? "rgba(14,14,14,0)" : "rgba(242,242,247,0)"; @@ -462,7 +463,7 @@ export function NewTaskDraftScreen(props: { onPasteImages={(uris) => void handleNativePasteImages(uris)} placeholder={`Describe a coding task in ${selectedProject.title}`} style={{ flex: 1, minHeight: 0 }} - textStyle={MOBILE_TYPOGRAPHY.composer} + textStyle={bodyText} /> diff --git a/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx index c4f7a54dec7..896a43d636b 100644 --- a/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx +++ b/apps/mobile/src/features/threads/NewTaskRouteScreen.tsx @@ -143,7 +143,7 @@ export function NewTaskRouteScreen() { {projectEmptyState.title} - + {projectEmptyState.detail} {!catalogState.hasReadyEnvironment ? ( @@ -207,7 +207,7 @@ export function NewTaskRouteScreen() { /> - {item.title} + {item.title} {props.approval.detail ? ( - + {props.approval.detail} ) : null} diff --git a/apps/mobile/src/features/threads/PendingUserInputCard.tsx b/apps/mobile/src/features/threads/PendingUserInputCard.tsx index c9e01777214..4d2c2c84159 100644 --- a/apps/mobile/src/features/threads/PendingUserInputCard.tsx +++ b/apps/mobile/src/features/threads/PendingUserInputCard.tsx @@ -39,7 +39,7 @@ export function PendingUserInputCard(props: PendingUserInputCardProps) { {question.header} - + {question.question} diff --git a/apps/mobile/src/features/threads/ThreadComposer.tsx b/apps/mobile/src/features/threads/ThreadComposer.tsx index a0b493153e5..eded9870dc6 100644 --- a/apps/mobile/src/features/threads/ThreadComposer.tsx +++ b/apps/mobile/src/features/threads/ThreadComposer.tsx @@ -45,7 +45,7 @@ import { ControlPill, ControlPillMenu } from "../../components/ControlPill"; import { ProviderIcon } from "../../components/ProviderIcon"; import type { DraftComposerImageAttachment } from "../../lib/composerImages"; import { buildModelOptions, groupByProvider } from "../../lib/modelOptions"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; +import { useScaledTextRole } from "../settings/appearance/useScaledTextRole"; import type { RemoteClientConnectionState } from "../../lib/connection"; import { insertRankedSearchResult, @@ -217,7 +217,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill( )} {props.status.label} @@ -230,6 +230,7 @@ const ComposerConnectionStatusPill = memo(function ComposerConnectionStatusPill( export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposerProps) { const isDarkMode = useColorScheme() === "dark"; const foregroundColor = useThemeColor("--color-foreground"); + const bodyText = useScaledTextRole("body"); const fallbackInputRef = useRef(null); const inputRef = props.editorRef ?? fallbackInputRef; const [isFocused, setIsFocused] = useState(false); @@ -756,7 +757,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer } } textStyle={{ - ...MOBILE_TYPOGRAPHY.composer, + ...bodyText, color: foregroundColor, fontFamily: "DMSans_400Regular", }} @@ -866,13 +867,7 @@ export const ThreadComposer = memo(function ThreadComposer(props: ThreadComposer {/* Queue count */} {props.queueCount > 0 ? ( - + {props.queueCount} queued message{props.queueCount === 1 ? "" : "s"} will send automatically. diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 4821a49234c..0abfb94feb1 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -62,13 +62,16 @@ import { buildNativeReviewDiffData, createNativeReviewDiffTheme, NATIVE_REVIEW_DIFF_CONTENT_WIDTH, - NATIVE_REVIEW_DIFF_ROW_HEIGHT, - NATIVE_REVIEW_DIFF_STYLE, } from "../review/nativeReviewDiffAdapter"; import { buildReviewParsedDiff } from "../review/reviewModel"; import { cn } from "../../lib/cn"; import { deriveCenteredContentHorizontalPadding, type LayoutVariant } from "../../lib/layout"; -import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "../../lib/typography"; +import { + resolveMarkdownFontSizes, + resolveNativeMarkdownTypography, +} from "../../lib/appearancePreferences"; +import { useAppearancePreferences } from "../settings/appearance/AppearancePreferencesProvider"; +import { useAppearanceCodeSurface } from "../settings/appearance/useAppearanceCodeSurface"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; import { resolveMarkdownLinkPresentation } from "@t3tools/mobile-markdown-text/links"; import { @@ -278,6 +281,15 @@ function useReviewCommentColors(): ReviewCommentColors { function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSets { const colorScheme = useColorScheme(); + const { appearance } = useAppearancePreferences(); + const markdownFontSizes = useMemo( + () => resolveMarkdownFontSizes(appearance.baseFontSize), + [appearance.baseFontSize], + ); + const nativeMarkdownTypography = useMemo( + () => resolveNativeMarkdownTypography(appearance.baseFontSize), + [appearance.baseFontSize], + ); const colors = MARKDOWN_COLORS[colorScheme === "dark" ? "dark" : "light"]; const inlineSkillForeground = String(useThemeColor("--color-inline-skill-foreground")); @@ -321,14 +333,14 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe xl: 16, }, fontSizes: { - s: 13, - m: 15, - h1: 20, - h2: 18, - h3: 16, - h4: 14, - h5: 14, - h6: 14, + s: markdownFontSizes.s, + m: markdownFontSizes.m, + h1: markdownFontSizes.h1, + h2: markdownFontSizes.h2, + h3: markdownFontSizes.h3, + h4: markdownFontSizes.h4, + h5: markdownFontSizes.h5, + h6: markdownFontSizes.h6, }, fontFamilies: { regular: "DMSans_400Regular", @@ -350,7 +362,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe list: { marginTop: 4, marginBottom: 8 }, list_item: { marginTop: 0, marginBottom: 4 }, task_list_item: { marginTop: 0, marginBottom: 4 }, - text: { lineHeight: 22 }, + text: { lineHeight: markdownFontSizes.bodyLineHeight }, bold: { fontWeight: "700", color: markdownStrongColor, @@ -459,7 +471,8 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe marginRight: 5, color: inlineTextColor, fontFamily: "DMSans_400Regular", - ...MOBILE_TYPOGRAPHY.body, + fontSize: markdownFontSizes.m, + lineHeight: markdownFontSizes.bodyLineHeight, textAlign: ordered ? "right" : "center", }} > @@ -480,8 +493,8 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe style={{ color: inlineCodeTextColor, fontFamily: "ui-monospace", - fontSize: MOBILE_TYPOGRAPHY.label.fontSize, - lineHeight: 22, + fontSize: markdownFontSizes.codeBlockFontSize, + lineHeight: markdownFontSizes.bodyLineHeight, }} > {value} @@ -517,7 +530,7 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe style={{ color: markdownBodyColor, fontFamily: "ui-monospace", - fontSize: MOBILE_TYPOGRAPHY.label.fontSize, + fontSize: markdownFontSizes.codeBlockFontSize, opacity: 0.7, textTransform: "uppercase", }} @@ -537,8 +550,8 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe style={{ color: blockTextColor, fontFamily: "ui-monospace", - fontSize: MOBILE_TYPOGRAPHY.label.fontSize, - lineHeight: 18, + fontSize: markdownFontSizes.codeBlockFontSize, + lineHeight: markdownFontSizes.codeBlockLineHeight, }} > {content} @@ -617,7 +630,9 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe skillTextColor: "#f0abfc", quoteMarkerColor: markdownUserBodyColor, dividerColor: markdownUserBodyColor, - ...MOBILE_TYPOGRAPHY.body, + fontSize: nativeMarkdownTypography.fontSize, + lineHeight: nativeMarkdownTypography.lineHeight, + headingFontSizes: nativeMarkdownTypography.headingFontSizes, fontFamily: "DMSans_400Regular", headingFontFamily: "DMSans_700Bold", boldFontFamily: "DMSans_700Bold", @@ -646,14 +661,16 @@ function useMarkdownStyles(onLinkPress: (href: string) => void): MarkdownStyleSe skillTextColor: inlineSkillForeground, quoteMarkerColor: markdownBlockquoteBorder, dividerColor: markdownHrColor, - ...MOBILE_TYPOGRAPHY.body, + fontSize: nativeMarkdownTypography.fontSize, + lineHeight: nativeMarkdownTypography.lineHeight, + headingFontSizes: nativeMarkdownTypography.headingFontSizes, fontFamily: "DMSans_400Regular", headingFontFamily: "DMSans_700Bold", boldFontFamily: "DMSans_700Bold", }, }, }; - }, [colors, inlineSkillForeground, onLinkPress]); + }, [colors, inlineSkillForeground, markdownFontSizes, nativeMarkdownTypography, onLinkPress]); } function renderFeedEntry( @@ -929,6 +946,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { readonly comment: ReviewInlineComment; readonly colors: ReviewCommentColors; }) { + const { codeSurface, nativeReviewDiffStyle } = useAppearanceCodeSurface(); const colorScheme = useColorScheme(); const appearanceScheme = colorScheme === "light" ? "light" : "dark"; const NativeReviewDiffView = resolveNativeReviewDiffView(); @@ -951,18 +969,21 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { () => JSON.stringify(nativeReviewDiffTheme), [nativeReviewDiffTheme], ); - const nativeStyleJson = useMemo(() => JSON.stringify(NATIVE_REVIEW_DIFF_STYLE), []); + const nativeStyleJson = useMemo( + () => JSON.stringify(nativeReviewDiffStyle), + [nativeReviewDiffStyle], + ); const nativeDiffHeight = useMemo( () => Math.min( 360, Math.max( 112, - compactNativeRows.length * NATIVE_REVIEW_DIFF_ROW_HEIGHT + - NATIVE_REVIEW_DIFF_STYLE.fileHeaderVerticalMargin, + compactNativeRows.length * nativeReviewDiffStyle.rowHeight + + nativeReviewDiffStyle.fileHeaderVerticalMargin, ), ), - [compactNativeRows.length], + [compactNativeRows.length, nativeReviewDiffStyle], ); const shouldRenderNativeDiff = NativeReviewDiffView != null && compactNativeRows.length > 0; @@ -992,7 +1013,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { @@ -1015,7 +1036,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { style={StyleSheet.absoluteFill} appearanceScheme={appearanceScheme} contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH} - rowHeight={NATIVE_REVIEW_DIFF_ROW_HEIGHT} + rowHeight={nativeReviewDiffStyle.rowHeight} rowsJson={nativeRowsJson} styleJson={nativeStyleJson} themeJson={nativeThemeJson} @@ -1037,8 +1058,8 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { style={{ color: props.colors.text, fontFamily: "ui-monospace", - fontSize: MOBILE_CODE_SURFACE.fontSize, - lineHeight: MOBILE_CODE_SURFACE.rowHeight, + fontSize: codeSurface.fontSize, + lineHeight: codeSurface.rowHeight, }} > {props.comment.diff.trim()} @@ -1049,7 +1070,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { {props.comment.text} @@ -1109,7 +1130,7 @@ function ThreadFeedPlaceholder(props: { > {props.title} - + {props.detail} diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 812a5829904..6803d108101 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -1,10 +1,24 @@ +import { isLiquidGlassSupported, LiquidGlassView } from "@callstack/liquid-glass"; 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 { useNavigation } from "@react-navigation/native"; -import { memo, useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; -import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; +import { + memo, + useCallback, + useMemo, + useRef, + useState, + type ComponentProps, + type ReactNode, +} from "react"; +import type { + ColorValue, + LayoutChangeEvent, + NativeScrollEvent, + NativeSyntheticEvent, +} 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"; @@ -14,7 +28,6 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; -import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -35,14 +48,48 @@ 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"; +/** + * Shared capsule behind the sidebar header buttons — a native liquid-glass + * surface on iOS 26+, a tinted pill everywhere else. + */ +function SidebarHeaderButtonGroup(props: { + readonly children: ReactNode; + readonly colorScheme: "light" | "dark"; +}) { + if (isLiquidGlassSupported) { + return ( + + {props.children} + + ); + } + + return ( + + {props.children} + + ); +} + const SIDEBAR_STICKY_HEADER_HEIGHT = 106; const SIDEBAR_STICKY_HEADER_FADE_HEIGHT = 44; -const HEADER_SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); const IOS_SEARCH_FILL_DARK = "rgba(118, 118, 128, 0.24)"; const IOS_SEARCH_FILL_LIGHT = "rgba(118, 118, 128, 0.12)"; const SIDEBAR_HEADER_WASH_OPACITY = { @@ -50,16 +97,6 @@ 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; @@ -251,7 +288,6 @@ export function ThreadNavigationSidebar(props: { readonly onOpenSettings: () => void; readonly onSearchQueryChange: (query: string) => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; - readonly onStartNewTask: () => void; readonly onRequestVisibility: () => void; readonly searchQuery: string; }) { @@ -315,35 +351,6 @@ 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( () => [ { @@ -455,9 +462,16 @@ export function ThreadNavigationSidebar(props: { const listExtraData = `${listThemeKey}:${props.selectedThreadKey ?? ""}:${props.searchQuery}`; const headerFadeColor = String(backgroundColor); const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; - const usesNativeSidebarChrome = Platform.OS === "ios"; - const topListInset = insets.top + SIDEBAR_STICKY_HEADER_HEIGHT - 6; - const nativeTopListInset = insets.top + 76; + const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(null); + // The sticky header (title row, search field, optional connection status) + // is measured so the list inset always matches its real height — no + // hardcoded per-variant constants. + const stickyHeaderHeight = measuredHeaderHeight ?? insets.top + SIDEBAR_STICKY_HEADER_HEIGHT; + const topListInset = stickyHeaderHeight + 6; + const handleStickyHeaderLayout = useCallback((event: LayoutChangeEvent) => { + const nextHeight = Math.round(event.nativeEvent.layout.height); + setMeasuredHeaderHeight((current) => (current === nextHeight ? current : nextHeight)); + }, []); const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current !== methods) { openSwipeableRef.current?.close(); @@ -485,9 +499,6 @@ export function ThreadNavigationSidebar(props: { setHeaderIsOverContent(next); }, []); const focusSearch = useCallback(() => { - if (usesNativeSidebarChrome) { - return false; - } if (!props.visible) { props.onRequestVisibility(); setTimeout(() => { @@ -497,7 +508,7 @@ export function ThreadNavigationSidebar(props: { searchInputRef.current?.focus(); } return true; - }, [props.onRequestVisibility, props.visible, usesNativeSidebarChrome]); + }, [props.onRequestVisibility, props.visible]); useHardwareKeyboardCommand("focusSearch", focusSearch); const renderListItem = useCallback( ({ item }: { readonly item: SidebarListItem }) => { @@ -562,181 +573,6 @@ export function ThreadNavigationSidebar(props: { const filterIcon = hasCustomHomeListOptions(options) ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle"; - 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), - 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", - }), - ] 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 ? ( - - - navigation.navigate("SettingsSheet", { - screen: "SettingsEnvironments", - }) - } - state={catalogState} - variant="sidebar" - /> - - ) : null - } - ListEmptyComponent={ - - {catalogState.isLoadingConnections - ? "Loading threads…" - : props.searchQuery.trim().length > 0 - ? "No matching threads" - : "No threads yet"} - - } - /> - - - - - - - - ); - } - return ( @@ -838,13 +675,16 @@ export function ThreadNavigationSidebar(props: { > Threads - - - - + + + + + + @@ -914,19 +755,16 @@ const styles = StyleSheet.create({ alignItems: "flex-end", gap: 2, }, + headerButtonGroup: { + alignItems: "center", + borderRadius: 22, + flexDirection: "row", + overflow: "hidden", + }, connectionStatus: { paddingTop: 10, paddingHorizontal: 14, }, - nativeConnectionStatus: { - paddingBottom: 10, - paddingHorizontal: 14, - }, - nativeHeaderActions: { - alignItems: "center", - flexDirection: "row", - gap: 2, - }, searchField: { height: 38, marginTop: 9, @@ -944,7 +782,6 @@ const styles = StyleSheet.create({ paddingVertical: 0, paddingHorizontal: 0, fontFamily: "DMSans_400Regular", - fontSize: 15, }, threadList: { flex: 1, diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 119e9b378a6..368848ffba8 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -10,7 +10,6 @@ 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 { useWorkspaceState } from "../../state/workspace"; import { useThemeColor } from "../../lib/useThemeColor"; import { useEnvironmentQuery } from "../../state/query"; @@ -179,7 +178,6 @@ function ThreadRouteContent( fileInspector, layout, panes, - setPrimarySidebarSearchQuery, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar, @@ -189,7 +187,6 @@ 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(); @@ -283,16 +280,6 @@ function ThreadRouteContent( /* ─── Native header theming ──────────────────────────────────────── */ const foregroundColor = String(useThemeColor("--color-foreground")); const usesNativeHeaderGlass = Platform.OS === "ios"; - const usesThreadSearchToolbar = - Platform.OS === "ios" && layout.usesSplitView && inspectorMode === null; - const focusThreadSearch = useCallback(() => { - if (!usesThreadSearchToolbar) { - return false; - } - threadSearchBarRef.current?.focus(); - return true; - }, [usesThreadSearchToolbar]); - useHardwareKeyboardCommand("focusSearch", focusThreadSearch); const headerSubtitle = [ selectedThreadProject?.title ?? null, selectedEnvironmentConnection?.environmentLabel ?? null, @@ -744,44 +731,26 @@ function ThreadRouteContent( {activeInspectorRenderer ? : null} null : selectedThread.title, + headerTitle: selectedThread.title, headerTitleStyle: usesNativeHeaderGlass ? { fontSize: 17, fontWeight: "800", } : undefined, - title: layout.usesSplitView ? "" : selectedThread.title, + title: selectedThread.title, headerBackVisible: !layout.usesSplitView, - headerSearchBarOptions: usesThreadSearchToolbar - ? { - ref: threadSearchBarRef, - allowToolbarIntegration: true, - autoCapitalize: "none", - hideNavigationBar: false, - obscureBackground: false, - onCancelButtonPress: () => { - setPrimarySidebarSearchQuery(""); - }, - onChangeText: (event) => { - setPrimarySidebarSearchQuery(event.nativeEvent.text); - }, - placeholder: "Search", - placement: "integratedButton", - } - : undefined, // Compact uses the NATIVE back button (Thread lives flat in the root // stack now, so a real previous route exists); only split view needs // custom left items. unstable_headerLeftItems: Platform.OS === "ios" && layout.usesSplitView ? () => splitLeftHeaderItems : undefined, - unstable_headerCenterItems: - layout.usesSplitView && Platform.OS === "ios" - ? () => threadCenterHeaderItems - : undefined, + // Search lives in the persistent sidebar, so the split header keeps + // the git controls on the RIGHT (no center items — center space is + // reserved for future breadcrumbs/status). unstable_headerRightItems: - !layout.usesSplitView && Platform.OS === "ios" - ? () => compactRightHeaderItems + Platform.OS === "ios" + ? () => (layout.usesSplitView ? threadCenterHeaderItems : compactRightHeaderItems) : undefined, unstable_headerSubtitle: usesNativeHeaderGlass ? headerSubtitle : undefined, }} diff --git a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx index a2620300be7..bf2d187af0d 100644 --- a/apps/mobile/src/features/threads/git/GitCommitSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitCommitSheet.tsx @@ -91,7 +91,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { {isDefaultRef ? ( Warning: this is the default branch. @@ -103,7 +103,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { Files - + {selectedFiles.length} selected · +{selectedInsertions} / -{selectedDeletions} @@ -128,7 +128,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { {allFiles.length === 0 ? ( - + No changed files are available to commit. ) : !isEditingFiles ? ( @@ -147,7 +147,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { ))} {selectedFiles.length > selectedFilePreview.length ? ( - + +{selectedFiles.length - selectedFilePreview.length} more files ) : null} @@ -187,7 +187,7 @@ export function GitCommitSheet(_props: GitCommitSheetProps) { {file.path} {!included ? ( - + Excluded from this commit ) : null} diff --git a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx index 7a9199ff9f2..b4f78742141 100644 --- a/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitConfirmSheet.tsx @@ -113,7 +113,7 @@ export function GitConfirmSheet(props: GitConfirmSheetProps) { {copy?.title ?? "Run action on default branch?"} - + {copy?.description ?? "Choose how to continue."} diff --git a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx index 7a652cb1b22..ab2e0168ff5 100644 --- a/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx +++ b/apps/mobile/src/features/threads/git/GitOverviewSheet.tsx @@ -387,7 +387,7 @@ export function GitOverviewSheet(props: GitOverviewSheetProps) { {currentBranchLabel} - + {currentStatusSummary} diff --git a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx index 16c311bff57..8045de78228 100644 --- a/apps/mobile/src/features/threads/git/gitSheetComponents.tsx +++ b/apps/mobile/src/features/threads/git/gitSheetComponents.tsx @@ -104,7 +104,7 @@ export function SheetListRow(props: { {props.title} {props.subtitle ? ( - {props.subtitle} + {props.subtitle} ) : null} diff --git a/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx b/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx deleted file mode 100644 index 8fa5ca6fdb2..00000000000 --- a/apps/mobile/src/features/threads/sidebar-filter-button.ios.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { Pressable, 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 index 4f56588efcc..642a4a957bb 100644 --- a/apps/mobile/src/features/threads/sidebar-filter-button.tsx +++ b/apps/mobile/src/features/threads/sidebar-filter-button.tsx @@ -10,8 +10,10 @@ export type SidebarFilterButtonIcon = export function SidebarFilterButton(props: { readonly accessibilityLabel: string; readonly icon: SidebarFilterButtonIcon; + /** Rendered inside a shared capsule group — no own background/border. */ + readonly grouped?: boolean; }) { - const iconColor = useThemeColor("--color-icon-muted"); + const iconColor = useThemeColor("--color-foreground"); const pressedBackgroundColor = useThemeColor("--color-subtle"); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; const idleBackgroundColor = @@ -25,20 +27,24 @@ export function SidebarFilterButton(props: { hitSlop={4} style={({ pressed }) => [ styles.button, - { - backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, - borderColor, - }, + props.grouped + ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } + : { + backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, + borderColor, + }, ]} > - + ); } const styles = StyleSheet.create({ button: { - width: 44, + // Match the native glass UIBarButtonItem group metrics (~50pt slots, + // 44pt bar height, label-colored ~20pt glyphs). + width: 50, height: 44, borderRadius: 22, borderWidth: StyleSheet.hairlineWidth, diff --git a/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx b/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx deleted file mode 100644 index 56f908d1b02..00000000000 --- a/apps/mobile/src/features/threads/sidebar-header-actions.ios.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { View } from "react-native"; - -import { T3HeaderButton } from "../../native/T3HeaderButton.ios"; -import type { SidebarHeaderActionsProps } from "./sidebar-header-actions"; - -export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { - return ( - - - {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 5d0cb5d2e82..e193f1f2d9b 100644 --- a/apps/mobile/src/features/threads/sidebar-header-actions.tsx +++ b/apps/mobile/src/features/threads/sidebar-header-actions.tsx @@ -5,15 +5,17 @@ import { useThemeColor } from "../../lib/useThemeColor"; export interface SidebarHeaderActionsProps { readonly onOpenSettings: () => void; - readonly onStartNewTask?: () => void; + /** Rendered inside a shared capsule group — buttons drop their own chrome. */ + readonly grouped?: boolean; } function FallbackHeaderButton(props: { readonly accessibilityLabel: string; readonly icon: "gearshape" | "square.and.pencil"; + readonly grouped?: boolean; readonly onPress: () => void; }) { - const iconColor = useThemeColor("--color-icon-muted"); + const iconColor = useThemeColor("--color-foreground"); const pressedBackgroundColor = useThemeColor("--color-subtle"); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; const idleBackgroundColor = @@ -28,13 +30,15 @@ function FallbackHeaderButton(props: { onPress={props.onPress} style={({ pressed }) => [ styles.button, - { - backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, - borderColor, - }, + props.grouped + ? { backgroundColor: pressed ? pressedBackgroundColor : "transparent", borderWidth: 0 } + : { + backgroundColor: pressed ? pressedBackgroundColor : idleBackgroundColor, + borderColor, + }, ]} > - + ); } @@ -44,16 +48,10 @@ export function SidebarHeaderActions(props: SidebarHeaderActionsProps) { - {props.onStartNewTask ? ( - - ) : null} ); } @@ -65,7 +63,8 @@ const styles = StyleSheet.create({ gap: 2, }, button: { - width: 44, + // Match the native glass UIBarButtonItem group metrics. + width: 50, height: 44, borderRadius: 22, borderWidth: StyleSheet.hairlineWidth, diff --git a/apps/mobile/src/features/threads/thread-work-log.tsx b/apps/mobile/src/features/threads/thread-work-log.tsx index a788f8a409e..11729734a89 100644 --- a/apps/mobile/src/features/threads/thread-work-log.tsx +++ b/apps/mobile/src/features/threads/thread-work-log.tsx @@ -139,7 +139,7 @@ export function ThreadWorkLog(props: { {row.fullDetail} diff --git a/apps/mobile/src/lib/adaptive-navigation.test.ts b/apps/mobile/src/lib/adaptive-navigation.test.ts index 7f8ce80a1ae..324881eb4bb 100644 --- a/apps/mobile/src/lib/adaptive-navigation.test.ts +++ b/apps/mobile/src/lib/adaptive-navigation.test.ts @@ -34,6 +34,15 @@ describe("resolveThreadSelectionNavigationAction", () => { ).toBe("replace"); }); + it("pushes from Home so the back stack survives collapsing to compact", () => { + expect( + resolveThreadSelectionNavigationAction({ + usesSplitView: true, + pathname: "/", + }), + ).toBe("push"); + }); + it("pushes compact list selections onto the native stack", () => { expect( resolveThreadSelectionNavigationAction({ diff --git a/apps/mobile/src/lib/adaptive-navigation.ts b/apps/mobile/src/lib/adaptive-navigation.ts index 0d0fa59f1fb..7eb6f658dc6 100644 --- a/apps/mobile/src/lib/adaptive-navigation.ts +++ b/apps/mobile/src/lib/adaptive-navigation.ts @@ -9,12 +9,14 @@ export function isBaseThreadRoute(pathname: string): boolean { /** * A persistent sidebar selects a peer destination in place. A compact list * drills into a new destination so the native back stack remains available. + * From Home the selection pushes (never replaces) so Home stays beneath the + * thread — collapsing back to a compact width keeps a sane back stack. */ export function resolveThreadSelectionNavigationAction(input: { readonly usesSplitView: boolean; readonly pathname: string; }): AdaptiveNavigationAction { - if (!input.usesSplitView) { + if (!input.usesSplitView || input.pathname === "/") { return "push"; } diff --git a/apps/mobile/src/lib/appearancePreferences.test.ts b/apps/mobile/src/lib/appearancePreferences.test.ts new file mode 100644 index 00000000000..3458f0120f9 --- /dev/null +++ b/apps/mobile/src/lib/appearancePreferences.test.ts @@ -0,0 +1,142 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { + DEFAULT_BASE_FONT_SIZE, + deriveCodeFontSize, + deriveTerminalFontSize, + normalizeBaseFontSize, + normalizeCodeFontSize, + normalizeCodeWordBreak, + resolveAppearance, + resolveAppearancePreferences, + resolveMarkdownFontSizes, + resolveMobileCodeSurface, + resolveNativeMarkdownTypography, + resolveTextScaleVariables, + stepBaseFontSize, + stepCodeFontSize, + stepTerminalFontSize, +} from "./appearancePreferences"; + +describe("appearancePreferences", () => { + it("resolves defaults for empty stored preferences", () => { + expect(resolveAppearancePreferences({})).toEqual({ + baseFontSize: DEFAULT_BASE_FONT_SIZE, + terminalFontSize: null, + codeFontSize: null, + codeWordBreak: false, + }); + }); + + it("migrates the legacy markdownFontSize key to baseFontSize", () => { + expect(resolveAppearancePreferences({ markdownFontSize: 18 }).baseFontSize).toBe(18); + expect( + resolveAppearancePreferences({ baseFontSize: 16, markdownFontSize: 18 }).baseFontSize, + ).toBe(16); + }); + + it("keeps explicit overrides and treats missing values as automatic", () => { + const preferences = resolveAppearancePreferences({ terminalFontSize: 12, codeFontSize: 14 }); + expect(preferences.terminalFontSize).toBe(12); + expect(preferences.codeFontSize).toBe(14); + expect(resolveAppearancePreferences({ terminalFontSize: null }).terminalFontSize).toBe(null); + }); + + it("derives terminal and code sizes from the base size when not overridden", () => { + const appearance = resolveAppearance(resolveAppearancePreferences({ baseFontSize: 15 })); + expect(appearance.terminalFontSize).toBe(10); + expect(appearance.codeFontSize).toBe(11); + expect(appearance.isTerminalFontSizeCustom).toBe(false); + expect(appearance.isCodeFontSizeCustom).toBe(false); + + const scaled = resolveAppearance(resolveAppearancePreferences({ baseFontSize: 22 })); + expect(scaled.terminalFontSize).toBe(deriveTerminalFontSize(22)); + expect(scaled.codeFontSize).toBe(deriveCodeFontSize(22)); + expect(scaled.terminalFontSize).toBeGreaterThan(10); + expect(scaled.codeFontSize).toBeGreaterThan(11); + }); + + it("applies explicit overrides over derived values", () => { + const appearance = resolveAppearance( + resolveAppearancePreferences({ baseFontSize: 22, terminalFontSize: 8, codeFontSize: 9 }), + ); + expect(appearance.terminalFontSize).toBe(8); + expect(appearance.codeFontSize).toBe(9); + expect(appearance.isTerminalFontSizeCustom).toBe(true); + expect(appearance.isCodeFontSizeCustom).toBe(true); + }); + + it("clamps base and code font sizes", () => { + expect(normalizeBaseFontSize(4)).toBe(11); + expect(normalizeBaseFontSize(30)).toBe(22); + expect(normalizeCodeFontSize(4)).toBe(8); + expect(normalizeCodeFontSize(30)).toBe(18); + }); + + it("steps font sizes within bounds", () => { + expect(stepTerminalFontSize(6, -1)).toBe(6); + expect(stepBaseFontSize(11, -1)).toBe(11); + expect(stepCodeFontSize(8, -1)).toBe(8); + expect(stepBaseFontSize(15, 1)).toBe(16); + }); + + it("scales markdown typography from the base size", () => { + expect(resolveMarkdownFontSizes(15)).toMatchObject({ + m: 15, + h1: 20, + bodyLineHeight: 22, + codeBlockFontSize: 12, + codeBlockLineHeight: 18, + }); + }); + + it("scales code surface geometry from the code font size", () => { + expect(resolveMobileCodeSurface(11)).toMatchObject({ + fontSize: 11, + rowHeight: 20, + }); + }); + + it("defaults code word break to false", () => { + expect(normalizeCodeWordBreak(undefined)).toBe(false); + expect(normalizeCodeWordBreak(true)).toBe(true); + }); + + it("returns the authored text scale at the 16pt default", () => { + expect(DEFAULT_BASE_FONT_SIZE).toBe(16); + + const variables = resolveTextScaleVariables(DEFAULT_BASE_FONT_SIZE); + expect(variables["--text-base"]).toBe(16); + expect(variables["--text-base--line-height"]).toBe(23); + expect(variables["--text-sm"]).toBe(14); + expect(variables["--text-sm--line-height"]).toBe(19); + expect(variables["--text-lg"]).toBe(18); + expect(variables["--text-3xl"]).toBe(30); + }); + + it("scales every text variable proportionally with the base size", () => { + const smallerVariables = resolveTextScaleVariables(15); + expect(smallerVariables["--text-base"]).toBe(15); + expect(smallerVariables["--text-sm"]).toBe(13); + + const variables = resolveTextScaleVariables(20); + expect(variables["--text-base"]).toBe(20); + expect(variables["--text-base--line-height"]).toBe(29); + expect(variables["--text-sm"]).toBe(18); + expect(variables["--text-xs"]).toBe(16); + expect(variables["--text-lg"]).toBe(23); + + const smaller = resolveTextScaleVariables(11); + expect(smaller["--text-base"]).toBe(11); + expect(smaller["--text-3xs"]).toBeGreaterThanOrEqual(8); + expect(smaller["--text-3xs--line-height"]).toBeGreaterThanOrEqual(10); + }); + + it("derives native markdown typography from the base size", () => { + expect(resolveNativeMarkdownTypography(22)).toEqual({ + fontSize: 22, + lineHeight: 32, + headingFontSizes: [29, 26, 23, 21, 21, 21], + }); + }); +}); diff --git a/apps/mobile/src/lib/appearancePreferences.ts b/apps/mobile/src/lib/appearancePreferences.ts new file mode 100644 index 00000000000..7287eabba7c --- /dev/null +++ b/apps/mobile/src/lib/appearancePreferences.ts @@ -0,0 +1,245 @@ +import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "./typography"; +import { + DEFAULT_TERMINAL_FONT_SIZE, + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, + normalizeTerminalFontSize, +} from "../features/terminal/terminalPreferences"; + +export const DEFAULT_BASE_FONT_SIZE = MOBILE_TYPOGRAPHY.body.fontSize; +export const MIN_BASE_FONT_SIZE = 11; +export const MAX_BASE_FONT_SIZE = 22; +export const BASE_FONT_SIZE_STEP = 1; + +export const DEFAULT_CODE_FONT_SIZE = MOBILE_CODE_SURFACE.fontSize; +export const MIN_CODE_FONT_SIZE = 8; +export const MAX_CODE_FONT_SIZE = 18; +export const CODE_FONT_SIZE_STEP = 1; + +/** + * User-configurable appearance preferences as stored. `null` overrides mean + * "automatic": the value is derived from the base font size. + */ +export interface AppearancePreferences { + readonly baseFontSize: number; + readonly terminalFontSize: number | null; + readonly codeFontSize: number | null; + readonly codeWordBreak: boolean; +} + +/** Effective appearance values after applying base-size derivation. */ +export interface ResolvedAppearance { + readonly baseFontSize: number; + readonly terminalFontSize: number; + readonly codeFontSize: number; + readonly codeWordBreak: boolean; + readonly isTerminalFontSizeCustom: boolean; + readonly isCodeFontSizeCustom: boolean; +} + +export interface ResolvedMobileCodeSurface { + readonly fontSize: number; + readonly lineNumberFontSize: number; + readonly rowHeight: number; + readonly gutterWidth: number; + readonly codePadding: number; + readonly textVerticalInset: number; +} + +export interface ResolvedMarkdownFontSizes { + readonly s: number; + readonly m: number; + readonly h1: number; + readonly h2: number; + readonly h3: number; + readonly h4: number; + readonly h5: number; + readonly h6: number; + readonly bodyLineHeight: number; + readonly codeBlockFontSize: number; + readonly codeBlockLineHeight: number; +} + +export interface NativeMarkdownTypography { + readonly fontSize: number; + readonly lineHeight: number; + readonly headingFontSizes: readonly [number, number, number, number, number, number]; +} + +export function normalizeBaseFontSize(value: number | null | undefined): number { + if (typeof value !== "number" || !Number.isFinite(value)) { + return DEFAULT_BASE_FONT_SIZE; + } + + return Math.min(MAX_BASE_FONT_SIZE, Math.max(MIN_BASE_FONT_SIZE, Math.round(value))); +} + +export function normalizeCodeFontSize(value: number | null | undefined): number { + if (typeof value !== "number" || !Number.isFinite(value)) { + return DEFAULT_CODE_FONT_SIZE; + } + + return Math.min(MAX_CODE_FONT_SIZE, Math.max(MIN_CODE_FONT_SIZE, Math.round(value))); +} + +export function normalizeCodeWordBreak(value: boolean | null | undefined): boolean { + return value === true; +} + +/** Terminal size derived from base: 10.5pt at base 16, snapped to 0.5pt steps. */ +export function deriveTerminalFontSize(baseFontSize: number): number { + const scale = normalizeBaseFontSize(baseFontSize) / DEFAULT_BASE_FONT_SIZE; + return normalizeTerminalFontSize(Math.round(DEFAULT_TERMINAL_FONT_SIZE * scale * 2) / 2); +} + +/** Code/diff size derived from base: 12pt at base 16. */ +export function deriveCodeFontSize(baseFontSize: number): number { + const scale = normalizeBaseFontSize(baseFontSize) / DEFAULT_BASE_FONT_SIZE; + return normalizeCodeFontSize(Math.round(DEFAULT_CODE_FONT_SIZE * scale)); +} + +interface StoredAppearancePreferences { + readonly baseFontSize?: number | null | undefined; + /** Legacy key from before base font size existed; migrated to baseFontSize. */ + readonly markdownFontSize?: number | null | undefined; + readonly terminalFontSize?: number | null | undefined; + readonly codeFontSize?: number | null | undefined; + readonly codeWordBreak?: boolean | null | undefined; +} + +export function resolveAppearancePreferences( + stored: StoredAppearancePreferences | null | undefined, +): AppearancePreferences { + return { + baseFontSize: normalizeBaseFontSize(stored?.baseFontSize ?? stored?.markdownFontSize), + terminalFontSize: + typeof stored?.terminalFontSize === "number" && Number.isFinite(stored.terminalFontSize) + ? normalizeTerminalFontSize(stored.terminalFontSize) + : null, + codeFontSize: + typeof stored?.codeFontSize === "number" && Number.isFinite(stored.codeFontSize) + ? normalizeCodeFontSize(stored.codeFontSize) + : null, + codeWordBreak: normalizeCodeWordBreak(stored?.codeWordBreak), + }; +} + +export function resolveAppearance(preferences: AppearancePreferences): ResolvedAppearance { + return { + baseFontSize: preferences.baseFontSize, + terminalFontSize: + preferences.terminalFontSize ?? deriveTerminalFontSize(preferences.baseFontSize), + codeFontSize: preferences.codeFontSize ?? deriveCodeFontSize(preferences.baseFontSize), + codeWordBreak: preferences.codeWordBreak, + isTerminalFontSizeCustom: preferences.terminalFontSize !== null, + isCodeFontSizeCustom: preferences.codeFontSize !== null, + }; +} + +export function resolveMobileCodeSurface(codeFontSize: number): ResolvedMobileCodeSurface { + const fontSize = normalizeCodeFontSize(codeFontSize); + const scale = fontSize / DEFAULT_CODE_FONT_SIZE; + + return { + fontSize, + lineNumberFontSize: Math.max(8, Math.round(MOBILE_CODE_SURFACE.lineNumberFontSize * scale)), + rowHeight: Math.max(14, Math.round(MOBILE_CODE_SURFACE.rowHeight * scale)), + gutterWidth: MOBILE_CODE_SURFACE.gutterWidth, + codePadding: MOBILE_CODE_SURFACE.codePadding, + textVerticalInset: MOBILE_CODE_SURFACE.textVerticalInset, + }; +} + +export function resolveMarkdownFontSizes(baseFontSize: number): ResolvedMarkdownFontSizes { + const m = normalizeBaseFontSize(baseFontSize); + const scale = m / DEFAULT_BASE_FONT_SIZE; + const codeBlockFontSize = Math.max(10, Math.round(13 * scale)); + + return { + s: Math.max(10, Math.round(14 * scale)), + m, + h1: Math.max(16, Math.round(21 * scale)), + h2: Math.max(14, Math.round(19 * scale)), + h3: Math.max(13, Math.round(17 * scale)), + h4: Math.max(12, Math.round(15 * scale)), + h5: Math.max(12, Math.round(15 * scale)), + h6: Math.max(12, Math.round(15 * scale)), + bodyLineHeight: Math.max(18, Math.round(MOBILE_TYPOGRAPHY.body.lineHeight * scale)), + codeBlockFontSize, + codeBlockLineHeight: codeBlockFontSize + 6, + }; +} + +/** + * Maps the Uniwind `--text-*` theme variables (see global.css) to the + * MOBILE_TYPOGRAPHY roles they were authored from. Keep in sync with both. + */ +const TEXT_SCALE_VARIABLE_ROLES = { + "--text-3xs": MOBILE_TYPOGRAPHY.micro, + "--text-2xs": MOBILE_TYPOGRAPHY.caption, + "--text-xs": MOBILE_TYPOGRAPHY.label, + "--text-sm": MOBILE_TYPOGRAPHY.footnote, + "--text-base": MOBILE_TYPOGRAPHY.body, + "--text-lg": MOBILE_TYPOGRAPHY.headline, + "--text-xl": MOBILE_TYPOGRAPHY.title, + "--text-2xl": MOBILE_TYPOGRAPHY.largeTitle, + "--text-3xl": MOBILE_TYPOGRAPHY.display, +} as const; + +/** + * Scaled values for every `--text-*` size and line-height variable, ready to + * pass to `Uniwind.updateCSSVariables`. All className-based text (`text-sm`, + * `text-base`, ...) re-resolves live when these are injected. + */ +export function resolveTextScaleVariables(baseFontSize: number): Record { + const scale = normalizeBaseFontSize(baseFontSize) / DEFAULT_BASE_FONT_SIZE; + const variables: Record = {}; + + for (const [name, role] of Object.entries(TEXT_SCALE_VARIABLE_ROLES)) { + variables[name] = Math.max(8, Math.round(role.fontSize * scale)); + variables[`${name}--line-height`] = Math.max(10, Math.round(role.lineHeight * scale)); + } + + return variables; +} + +export function resolveNativeMarkdownTypography(baseFontSize: number): NativeMarkdownTypography { + const fontSizes = resolveMarkdownFontSizes(baseFontSize); + return { + fontSize: fontSizes.m, + lineHeight: fontSizes.bodyLineHeight, + headingFontSizes: [ + fontSizes.h1, + fontSizes.h2, + fontSizes.h3, + fontSizes.h4, + fontSizes.h5, + fontSizes.h6, + ], + }; +} + +export function stepBaseFontSize(current: number, direction: -1 | 1): number { + const next = direction === -1 ? current - BASE_FONT_SIZE_STEP : current + BASE_FONT_SIZE_STEP; + return normalizeBaseFontSize(next); +} + +export function stepTerminalFontSize(current: number, direction: -1 | 1): number { + const next = + direction === -1 ? current - TERMINAL_FONT_SIZE_STEP : current + TERMINAL_FONT_SIZE_STEP; + return normalizeTerminalFontSize(next); +} + +export function stepCodeFontSize(current: number, direction: -1 | 1): number { + const next = direction === -1 ? current - CODE_FONT_SIZE_STEP : current + CODE_FONT_SIZE_STEP; + return normalizeCodeFontSize(next); +} + +export { + DEFAULT_TERMINAL_FONT_SIZE, + MAX_TERMINAL_FONT_SIZE, + MIN_TERMINAL_FONT_SIZE, + TERMINAL_FONT_SIZE_STEP, + normalizeTerminalFontSize, +}; diff --git a/apps/mobile/src/lib/storage.ts b/apps/mobile/src/lib/storage.ts index 114648277b9..8ed999225e1 100644 --- a/apps/mobile/src/lib/storage.ts +++ b/apps/mobile/src/lib/storage.ts @@ -59,7 +59,14 @@ export class MobileStorageEncodeError extends Schema.TaggedErrorClass { @@ -153,15 +160,31 @@ export async function loadPreferences(): Promise { const preferences: { liveActivitiesEnabled?: boolean; - terminalFontSize?: number; + baseFontSize?: number; + terminalFontSize?: number | null; + markdownFontSize?: number; + codeFontSize?: number | null; + codeWordBreak?: boolean; } = {}; if (typeof parsed.liveActivitiesEnabled === "boolean") { preferences.liveActivitiesEnabled = parsed.liveActivitiesEnabled; } - if (typeof parsed.terminalFontSize === "number") { + if (typeof parsed.baseFontSize === "number") { + preferences.baseFontSize = parsed.baseFontSize; + } + if (typeof parsed.terminalFontSize === "number" || parsed.terminalFontSize === null) { preferences.terminalFontSize = parsed.terminalFontSize; } + if (typeof parsed.markdownFontSize === "number") { + preferences.markdownFontSize = parsed.markdownFontSize; + } + if (typeof parsed.codeFontSize === "number" || parsed.codeFontSize === null) { + preferences.codeFontSize = parsed.codeFontSize; + } + if (typeof parsed.codeWordBreak === "boolean") { + preferences.codeWordBreak = parsed.codeWordBreak; + } return preferences; } diff --git a/apps/mobile/src/lib/typography.test.ts b/apps/mobile/src/lib/typography.test.ts index 6a021dabcce..5b62e9bd312 100644 --- a/apps/mobile/src/lib/typography.test.ts +++ b/apps/mobile/src/lib/typography.test.ts @@ -3,21 +3,18 @@ import { describe, expect, it } from "vite-plus/test"; import { MOBILE_CODE_SURFACE, MOBILE_TYPOGRAPHY } from "./typography"; describe("mobile typography", () => { - it("uses the intentional compact mobile font scale", () => { + it("uses the intentional mobile font scale anchored at a 16pt body", () => { expect(Object.values(MOBILE_TYPOGRAPHY).map(({ fontSize }) => fontSize)).toEqual([ - 10, 11, 12, 13, 14, 15, 17, 20, 24, 28, + 11, 12, 13, 14, 16, 18, 21, 26, 30, ]); - }); - - it("uses a compact shared style for editable composer text", () => { - expect(MOBILE_TYPOGRAPHY.composer).toEqual({ fontSize: 14, lineHeight: 20 }); + expect(MOBILE_TYPOGRAPHY.body).toEqual({ fontSize: 16, lineHeight: 23 }); }); it("uses caption-sized code with a compact readable row height", () => { expect(MOBILE_CODE_SURFACE).toMatchObject({ fontSize: MOBILE_TYPOGRAPHY.caption.fontSize, lineNumberFontSize: MOBILE_TYPOGRAPHY.micro.fontSize, - rowHeight: 20, + rowHeight: 22, }); }); }); diff --git a/apps/mobile/src/lib/typography.ts b/apps/mobile/src/lib/typography.ts index 644fee36589..6572f38dde2 100644 --- a/apps/mobile/src/lib/typography.ts +++ b/apps/mobile/src/lib/typography.ts @@ -1,19 +1,18 @@ export const MOBILE_TYPOGRAPHY = { - micro: { fontSize: 10, lineHeight: 13 }, - caption: { fontSize: 11, lineHeight: 15 }, - label: { fontSize: 12, lineHeight: 16 }, - footnote: { fontSize: 13, lineHeight: 18 }, - composer: { fontSize: 14, lineHeight: 20 }, - body: { fontSize: 15, lineHeight: 22 }, - headline: { fontSize: 17, lineHeight: 22 }, - title: { fontSize: 20, lineHeight: 26 }, - largeTitle: { fontSize: 24, lineHeight: 30 }, - display: { fontSize: 28, lineHeight: 34 }, + micro: { fontSize: 11, lineHeight: 14 }, + caption: { fontSize: 12, lineHeight: 16 }, + label: { fontSize: 13, lineHeight: 17 }, + footnote: { fontSize: 14, lineHeight: 19 }, + body: { fontSize: 16, lineHeight: 23 }, + headline: { fontSize: 18, lineHeight: 23 }, + title: { fontSize: 21, lineHeight: 28 }, + largeTitle: { fontSize: 26, lineHeight: 32 }, + display: { fontSize: 30, lineHeight: 36 }, } as const; /** Shared geometry for dense, horizontally scrolling code surfaces. */ export const MOBILE_CODE_SURFACE = { - rowHeight: 20, + rowHeight: 22, gutterWidth: 46, codePadding: 7, textVerticalInset: 2, diff --git a/apps/mobile/src/native/T3ComposerEditor.ios.tsx b/apps/mobile/src/native/T3ComposerEditor.ios.tsx index 7425e7da228..1089ab3ac1f 100644 --- a/apps/mobile/src/native/T3ComposerEditor.ios.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.ios.tsx @@ -14,8 +14,8 @@ import { Image, StyleSheet } from "react-native"; import { markdownFileIconSource } from "@t3tools/mobile-markdown-text/file-icons"; import { resolveMarkdownFileIcon } from "@t3tools/mobile-markdown-text/links"; -import { MOBILE_TYPOGRAPHY } from "../lib/typography"; import { useThemeColor } from "../lib/useThemeColor"; +import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole"; import { acknowledgeComposerNativeEvent, isComposerNativeEcho, @@ -107,6 +107,7 @@ export function ComposerEditor({ { eventCount: 0, value: props.value, selection: selection ?? null }, ]); const confirmedTokensRef = useRef(collectComposerInlineTokens(props.value)); + const bodyText = useScaledTextRole("body"); const textColor = useThemeColor("--color-foreground"); const placeholderColor = useThemeColor("--color-placeholder"); const chipBackground = useThemeColor("--color-subtle"); @@ -232,12 +233,12 @@ export function ComposerEditor({ fontSize={ typeof resolvedTextStyle.fontSize === "number" ? resolvedTextStyle.fontSize - : MOBILE_TYPOGRAPHY.composer.fontSize + : bodyText.fontSize } lineHeight={ typeof resolvedTextStyle.lineHeight === "number" ? resolvedTextStyle.lineHeight - : MOBILE_TYPOGRAPHY.composer.lineHeight + : bodyText.lineHeight } contentInsetVertical={contentInsetVertical} editable={props.editable ?? true} diff --git a/apps/mobile/src/native/T3ComposerEditor.tsx b/apps/mobile/src/native/T3ComposerEditor.tsx index dc2dfdfee03..27e61e1b99c 100644 --- a/apps/mobile/src/native/T3ComposerEditor.tsx +++ b/apps/mobile/src/native/T3ComposerEditor.tsx @@ -2,8 +2,8 @@ import { TextInputWrapper } from "expo-paste-input"; import { useImperativeHandle, useRef } from "react"; import { TextInput, type TextInput as RNTextInput } from "react-native"; -import { MOBILE_TYPOGRAPHY } from "../lib/typography"; import { useThemeColor } from "../lib/useThemeColor"; +import { useScaledTextRole } from "../features/settings/appearance/useScaledTextRole"; import { useNativePaste } from "../lib/useNativePaste"; import type { ComposerEditorProps } from "./T3ComposerEditor.types"; @@ -18,6 +18,7 @@ export function ComposerEditor({ ...props }: ComposerEditorProps) { const inputRef = useRef(null); + const bodyText = useScaledTextRole("body"); const foregroundColor = useThemeColor("--color-foreground"); const placeholderColor = useThemeColor("--color-placeholder"); const handlePaste = useNativePaste((uris) => onPasteImages?.(uris)); @@ -48,7 +49,7 @@ export function ComposerEditor({ minHeight: 0, color: foregroundColor, fontFamily: "DMSans_400Regular", - ...MOBILE_TYPOGRAPHY.composer, + ...bodyText, paddingVertical: contentInsetVertical, }, textStyle, diff --git a/apps/mobile/src/native/T3HeaderButton.ios.tsx b/apps/mobile/src/native/T3HeaderButton.ios.tsx deleted file mode 100644 index 49567d3c3ed..00000000000 --- a/apps/mobile/src/native/T3HeaderButton.ios.tsx +++ /dev/null @@ -1,30 +0,0 @@ -import { requireNativeView } from "expo"; -import type { NativeSyntheticEvent, StyleProp, ViewProps, ViewStyle } from "react-native"; - -interface NativeHeaderButtonProps extends ViewProps { - readonly label: string; - readonly systemImage: - | "gearshape" - | "line.3.horizontal.decrease.circle" - | "line.3.horizontal.decrease.circle.fill" - | "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 83d6d786695a3062162180818320cf6f80bdaf62 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 14:14:19 -0700 Subject: [PATCH 73/81] Add native iPad sidebar chrome and richer thread list presentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sidebar (iPad): host the pane in a navigation-inert single-screen native stack so the column gets a real UINavigationBar — inline leading title, glass bar-button items (filter menu + settings), pinned UISearchController search field, native scroll-edge blur. Row actions move from a per-row @react-native-menu ⋯ button (the component behind Fabric unmount-index crashes) to a Messages-style context menu on long-press/right-click, with swipe actions unchanged. Row swipes now fail on vertically-dominant pans (patched failOffsetY into RNGH ReanimatedSwipeable) so trackpad scrolling can't open rows. Thread lists (compact): group collapse + show-more display model (homeListItems), PR status via use-thread-pr, unified status resolution in threadPresentation. Co-Authored-By: Claude Fable 5 --- apps/mobile/package.json | 2 +- apps/mobile/src/components/ControlPill.tsx | 2 +- .../archive/ArchivedThreadsScreen.tsx | 173 +++-- apps/mobile/src/features/home/HomeScreen.tsx | 663 +++++++++++------- .../src/features/home/homeListItems.test.ts | 192 +++++ .../mobile/src/features/home/homeListItems.ts | 126 ++++ .../features/home/thread-swipe-actions.tsx | 5 + .../layout/AdaptiveWorkspaceLayout.tsx | 7 + .../threads/ThreadNavigationDrawer.tsx | 5 +- .../threads/ThreadNavigationSidebar.tsx | 273 ++++++-- .../threads/sidebar-native-header-items.ts | 63 ++ .../threads/sidebar-navigation-shell.tsx | 72 ++ .../features/threads/threadPresentation.ts | 121 +++- apps/mobile/src/state/use-thread-pr.ts | 67 ++ .../react-native-gesture-handler@2.31.2.patch | 124 ++++ pnpm-lock.yaml | 33 +- pnpm-workspace.yaml | 120 ++-- 17 files changed, 1569 insertions(+), 479 deletions(-) create mode 100644 apps/mobile/src/features/home/homeListItems.test.ts create mode 100644 apps/mobile/src/features/home/homeListItems.ts create mode 100644 apps/mobile/src/features/threads/sidebar-native-header-items.ts create mode 100644 apps/mobile/src/features/threads/sidebar-navigation-shell.tsx create mode 100644 apps/mobile/src/state/use-thread-pr.ts create mode 100644 patches/react-native-gesture-handler@2.31.2.patch diff --git a/apps/mobile/package.json b/apps/mobile/package.json index 4212c221696..f35b92574f5 100644 --- a/apps/mobile/package.json +++ b/apps/mobile/package.json @@ -5,7 +5,7 @@ "main": "index.ts", "scripts": { "dev": "expo start --clear", - "dev:client": "APP_VARIANT=development expo start --dev-client --clear", + "dev:client": "APP_VARIANT=development expo start --dev-client --scheme t3code-dev --clear", "start": "expo start", "start:dev": "APP_VARIANT=development expo start", "start:preview": "APP_VARIANT=preview expo start", diff --git a/apps/mobile/src/components/ControlPill.tsx b/apps/mobile/src/components/ControlPill.tsx index 62c1b061785..670c17fbfbf 100644 --- a/apps/mobile/src/components/ControlPill.tsx +++ b/apps/mobile/src/components/ControlPill.tsx @@ -75,7 +75,7 @@ export function ControlPill(props: { } export function ControlPillMenu( - props: Omit, "children" | "style" | "themeVariant"> & { + props: Omit, "children" | "themeVariant"> & { readonly children: ReactNode; }, ) { diff --git a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx index eeec46333a8..7f6f22e7499 100644 --- a/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx +++ b/apps/mobile/src/features/archive/ArchivedThreadsScreen.tsx @@ -2,6 +2,7 @@ import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; +import { LegendList } from "@legendapp/list/react-native"; import type { EnvironmentId } from "@t3tools/contracts"; import type { MenuAction } from "@react-native-menu/menu"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; @@ -12,7 +13,6 @@ import { Platform, Pressable, RefreshControl, - ScrollView, useWindowDimensions, View, } from "react-native"; @@ -48,6 +48,22 @@ const THREAD_ACTIONS: MenuAction[] = [ }, ]; +type ArchivedThreadListItem = + | { + readonly kind: "project"; + readonly key: string; + readonly environmentLabel: string | null; + readonly project: EnvironmentProject; + } + | { + readonly kind: "thread"; + readonly key: string; + readonly environmentLabel: string | null; + readonly isFirst: boolean; + readonly isLast: boolean; + readonly thread: EnvironmentThreadShell; + }; + function ArchivedThreadsHeader(props: { readonly environments: ReadonlyArray; readonly selectedEnvironmentId: EnvironmentId | null; @@ -245,6 +261,7 @@ function ProjectGroupLabel(props: { function ArchivedThreadRow(props: { readonly environmentLabel: string | null; + readonly isFirst: boolean; readonly isLast: boolean; readonly onDelete: () => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; @@ -296,6 +313,10 @@ function ArchivedThreadRow(props: { style={{ borderBottomColor: separatorColor, borderBottomWidth: props.isLast ? 0 : 1, + borderTopLeftRadius: props.isFirst ? 20 : 0, + borderTopRightRadius: props.isFirst ? 20 : 0, + borderBottomLeftRadius: props.isLast ? 20 : 0, + borderBottomRightRadius: props.isLast ? 20 : 0, }} > @@ -381,9 +402,41 @@ export function ArchivedThreadsScreen(props: { readonly onSortOrderChange: (sortOrder: ArchivedThreadSortOrder) => void; readonly onUnarchiveThread: (thread: EnvironmentThreadShell) => void; }) { + const { onDeleteThread, onUnarchiveThread } = props; const openSwipeableRef = useRef(null); const archiveScrollGesture = useMemo(() => Gesture.Native(), []); const refreshTint = useThemeColor("--color-icon"); + const environmentLabelsById = useMemo( + () => + new Map( + props.environments.map((environment) => [environment.environmentId, environment.label]), + ), + [props.environments], + ); + const listItems = useMemo>(() => { + const items: ArchivedThreadListItem[] = []; + for (const group of props.groups) { + const environmentLabel = environmentLabelsById.get(group.project.environmentId) ?? null; + items.push({ + kind: "project", + key: `${group.key}:project`, + environmentLabel, + project: group.project, + }); + + group.threads.forEach((thread, index) => { + items.push({ + kind: "thread", + key: `${thread.environmentId}:${thread.id}`, + environmentLabel, + isFirst: index === 0, + isLast: index === group.threads.length - 1, + thread, + }); + }); + } + return items; + }, [environmentLabelsById, props.groups]); const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current && openSwipeableRef.current !== methods) { openSwipeableRef.current.close(); @@ -397,6 +450,59 @@ export function ArchivedThreadsScreen(props: { }, []); const isInitialLoad = props.isLoading && props.groups.length === 0 && props.error === null; const isFiltered = props.searchQuery.trim().length > 0 || props.selectedEnvironmentId !== null; + const renderListItem = useCallback( + ({ item }: { item: ArchivedThreadListItem }) => { + if (item.kind === "project") { + return ( + + + + ); + } + + return ( + onDeleteThread(item.thread)} + onSwipeableClose={handleSwipeableClose} + onSwipeableWillOpen={handleSwipeableWillOpen} + onUnarchive={() => onUnarchiveThread(item.thread)} + simultaneousSwipeGesture={archiveScrollGesture} + thread={item.thread} + /> + ); + }, + [ + archiveScrollGesture, + handleSwipeableClose, + handleSwipeableWillOpen, + onDeleteThread, + onUnarchiveThread, + ], + ); + const listEmptyComponent = useMemo(() => { + if (isInitialLoad) { + return ( + + + Loading archive... + + ); + } + + return ( + + ); + }, [isFiltered, isInitialLoad, refreshTint]); return ( @@ -411,17 +517,24 @@ export function ArchivedThreadsScreen(props: { /> - item.kind} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" + keyExtractor={(item) => item.key} + ListEmptyComponent={listEmptyComponent} + ListHeaderComponent={ + props.error ? : null + } onScrollBeginDrag={() => openSwipeableRef.current?.close()} refreshControl={ } + renderItem={renderListItem} 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; - - return ( - - - - {group.threads.map((thread, index) => ( - props.onDeleteThread(thread)} - onSwipeableClose={handleSwipeableClose} - onSwipeableWillOpen={handleSwipeableWillOpen} - onUnarchive={() => props.onUnarchiveThread(thread)} - simultaneousSwipeGesture={archiveScrollGesture} - thread={thread} - /> - ))} - - - ); - }) - )} - + /> ); diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index feee8b51565..a08d8b837a1 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -1,3 +1,8 @@ +import { + LegendList, + type LegendListRef, + type LegendListRenderItemProps, +} from "@legendapp/list/react-native"; import { type EnvironmentProject, type EnvironmentThreadShell, @@ -8,23 +13,9 @@ import type { SidebarThreadSortOrder, } from "@t3tools/contracts"; import { SymbolView } from "expo-symbols"; -import { useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; -import { - ActivityIndicator, - Platform, - Pressable, - ScrollView, - useWindowDimensions, - View, -} from "react-native"; +import { memo, useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; +import { ActivityIndicator, Platform, Pressable, useWindowDimensions, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; -import Animated, { - Easing, - LinearTransition, - type ExitAnimationsValues, - withDelay, - withTiming, -} from "react-native-reanimated"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; @@ -33,9 +24,19 @@ import { EmptyState } from "../../components/EmptyState"; import { ProjectFavicon } from "../../components/ProjectFavicon"; import type { WorkspaceState } from "../../state/workspaceModel"; import type { SavedRemoteConnection } from "../../lib/connection"; +import { scopedProjectKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; -import { threadStatusTone } from "../threads/threadPresentation"; +import { useThreadPr } from "../../state/use-thread-pr"; +import { resolveThreadStatus } from "../threads/threadPresentation"; import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; +import { + buildHomeListLayout, + DEFAULT_GROUP_DISPLAY_STATE, + nextGroupDisplayState, + type HomeGroupDisplayAction, + type HomeGroupDisplayState, + type HomeListItem, +} from "./homeListItems"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; import { ThreadSwipeable } from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; @@ -68,51 +69,13 @@ interface HomeScreenProps { readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; } -/* ─── Status indicator colors ────────────────────────────────────────── */ - -function statusColors(thread: EnvironmentThreadShell): { bg: string; fg: string } { - switch (thread.session?.status) { - case "running": - return { bg: "rgba(249,115,22,0.22)", fg: "#ff9f0a" }; - case "ready": - return { bg: "rgba(48,209,88,0.22)", fg: "#30d158" }; - case "starting": - return { bg: "rgba(10,132,255,0.22)", fg: "#0a84ff" }; - case "error": - return { bg: "rgba(255,69,58,0.22)", fg: "#ff453a" }; - default: - return { bg: "rgba(142,142,147,0.22)", fg: "#8e8e93" }; - } -} - -const COLLAPSED_THREAD_LIMIT = 6; -const THREAD_LAYOUT_TRANSITION = LinearTransition.duration(220).easing(Easing.out(Easing.cubic)); +/* ─── Layout constants ───────────────────────────────────────────────── */ -function threadRowExit(values: ExitAnimationsValues) { - "worklet"; - - return { - initialValues: { - height: values.currentHeight, - opacity: 1, - originX: values.currentOriginX, - }, - animations: { - height: withDelay( - 90, - withTiming(0, { - duration: 170, - easing: Easing.inOut(Easing.cubic), - }), - ), - opacity: withDelay(80, withTiming(0, { duration: 100 })), - originX: withTiming(values.currentOriginX - values.windowWidth, { - duration: 190, - easing: Easing.out(Easing.cubic), - }), - }, - }; -} +const ESTIMATED_THREAD_ROW_HEIGHT = 64; +/** Left inset that aligns secondary rows with the thread title column. */ +const THREAD_TEXT_COLUMN_INSET = 20; +/** Height of the floating custom header on non-iOS platforms. */ +const CUSTOM_HEADER_HEIGHT = 78; function deriveEmptyState(props: { readonly catalogState: WorkspaceState; @@ -179,58 +142,161 @@ function deriveEmptyState(props: { /* ─── Project group header ───────────────────────────────────────────── */ -function ProjectGroupLabel(props: { +const ProjectGroupHeader = memo(function ProjectGroupHeader(props: { readonly project: EnvironmentProject; readonly title: string; - readonly totalThreadCount: number; - readonly isExpanded: boolean; + readonly threadCount: number; + readonly collapsed: boolean; readonly isFirst: boolean; - readonly onToggleExpand: () => void; + readonly groupKey: string; + readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; }) { - const hiddenCount = props.totalThreadCount - COLLAPSED_THREAD_LIMIT; + const iconSubtleColor = useThemeColor("--color-icon-subtle"); + const { groupKey, onGroupAction } = props; + const handleToggle = useCallback( + () => onGroupAction(groupKey, "toggle-collapsed"), + [groupKey, onGroupAction], + ); return ( - - - + - {props.title} - - - {hiddenCount > 0 ? ( - - - {props.isExpanded ? "Show less" : `${hiddenCount} more`} - + + + {props.title} + + + {props.threadCount} + + + + + ); +}); + +/* ─── Show more / show less row ──────────────────────────────────────── */ + +const ShowMoreRow = memo(function ShowMoreRow(props: { + readonly hiddenCount: number; + readonly canShowLess: boolean; + readonly groupKey: string; + readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; +}) { + const iconSubtleColor = useThemeColor("--color-icon-subtle"); + const showsMore = props.hiddenCount > 0; + const { groupKey, onGroupAction } = props; + const handleShowMore = useCallback( + () => onGroupAction(groupKey, "show-more"), + [groupKey, onGroupAction], + ); + const handleShowLess = useCallback( + () => onGroupAction(groupKey, "show-less"), + [groupKey, onGroupAction], + ); + + return ( + + {showsMore ? ( + ({ + opacity: pressed ? 0.6 : 1, + paddingHorizontal: 14, + paddingVertical: 7, + borderCurve: "continuous", + })} + > + + + Show more + + + ) : null} + {props.canShowLess ? ( + ({ + opacity: pressed ? 0.6 : 1, + paddingHorizontal: 14, + paddingVertical: 7, + borderCurve: "continuous", + })} + > + + + Show less + ) : null} ); -} +}); function HomeTopContentSpacer(props: { readonly topInset: number }) { - return ; + return ; } /* ─── Thread row ─────────────────────────────────────────────────────── */ -function ThreadRow(props: { +const ThreadRow = memo(function ThreadRow(props: { readonly thread: EnvironmentThreadShell; readonly environmentLabel: string | null; - readonly onPress: () => void; - readonly onArchive: () => void; - readonly onDelete: () => void; + readonly projectCwd: string | null; + readonly onSelectThread: (thread: EnvironmentThreadShell) => void; + readonly onArchiveThread: (thread: EnvironmentThreadShell) => void; + readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; readonly onSwipeableClose: (methods: SwipeableMethods) => void; readonly simultaneousSwipeGesture?: ComponentProps< @@ -242,93 +308,80 @@ function ThreadRow(props: { const separatorColor = useThemeColor("--color-separator"); const iconSubtleColor = useThemeColor("--color-icon-subtle"); const screenColor = useThemeColor("--color-screen"); - const { bg, fg } = statusColors(props.thread); - const tone = threadStatusTone(props.thread); + const { thread, onSelectThread, onArchiveThread, onDeleteThread } = props; + const status = resolveThreadStatus(thread); + const pr = useThreadPr(thread, props.projectCwd); const timestamp = relativeTime( - props.thread.latestUserMessageAt ?? props.thread.updatedAt ?? props.thread.createdAt, + thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt, ); - const branch = props.thread.branch; + const branch = thread.branch; const subtitleParts = [props.environmentLabel, branch].filter((part): part is string => Boolean(part), ); + const handleDelete = useCallback(() => onDeleteThread(thread), [onDeleteThread, thread]); + const primaryAction = useMemo( + () => ({ + accessibilityLabel: `Archive ${thread.title}`, + icon: "archivebox" as const, + label: "Archive", + onPress: () => onArchiveThread(thread), + }), + [onArchiveThread, thread], + ); return ( {(close) => ( { close(); - props.onPress(); + onSelectThread(thread); }} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} > - - - - - - {props.thread.title} + + {thread.title} - - - {tone.label} - - + {status ? ( + + + {status.label} + + + ) : null} - {subtitleParts.length > 0 ? ( + {subtitleParts.length > 0 || pr !== null ? ( - - - {subtitleParts.join(" · ")} - + {subtitleParts.length > 0 ? ( + <> + + + {subtitleParts.join(" · ")} + + + ) : null} + {pr !== null ? ( + + {pr.label} + + ) : null} ) : null} @@ -363,23 +429,32 @@ function ThreadRow(props: { )} ); -} +}); /* ─── Main screen ────────────────────────────────────────────────────── */ export function HomeScreen(props: HomeScreenProps) { - const [expandedProjects, setExpandedProjects] = useState>(() => new Set()); + const [groupDisplayStates, setGroupDisplayStates] = useState< + ReadonlyMap + >(() => new Map()); const openSwipeableRef = useRef(null); + const listRef = useRef(null); const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); - const toggleExpanded = useCallback((key: string) => { - setExpandedProjects((prev) => { - const next = new Set(prev); - if (next.has(key)) next.delete(key); - else next.add(key); - return next; - }); - }, []); + + const updateGroupDisplay = useCallback( + (key: string, action: HomeGroupDisplayAction) => { + setGroupDisplayStates((previous) => { + const next = new Map(previous); + next.set( + key, + nextGroupDisplayState(previous.get(key) ?? DEFAULT_GROUP_DISPLAY_STATE, action), + ); + return next; + }); + }, + [], + ); const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current !== methods) { @@ -393,6 +468,7 @@ export function HomeScreen(props: HomeScreenProps) { openSwipeableRef.current = null; } }, []); + const projectGroups = useMemo( () => buildHomeThreadGroups({ @@ -415,6 +491,121 @@ export function HomeScreen(props: HomeScreenProps) { ], ); + const hasSearchQuery = props.searchQuery.trim().length > 0; + const listLayout = useMemo( + () => + buildHomeListLayout({ + groups: projectGroups, + displayStates: groupDisplayStates, + showAllThreads: hasSearchQuery, + }), + [projectGroups, groupDisplayStates, hasSearchQuery], + ); + + const projectCwdByKey = useMemo(() => { + const map = new Map(); + for (const project of props.projects) { + map.set(scopedProjectKey(project.environmentId, project.id), project.workspaceRoot); + } + return map; + }, [props.projects]); + + const extraData = useMemo( + () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }), + [props.savedConnectionsById, projectCwdByKey], + ); + + const renderItem = useCallback( + ({ item }: LegendListRenderItemProps) => { + switch (item.type) { + case "header": + return ( + + ); + case "thread": { + const thread = item.thread; + return ( + + ); + } + case "show-more": + return ( + + ); + } + }, + [ + handleSwipeableClose, + handleSwipeableWillOpen, + projectCwdByKey, + props.onArchiveThread, + props.onDeleteThread, + props.onSelectThread, + props.savedConnectionsById, + updateGroupDisplay, + ], + ); + + const keyExtractor = useCallback((item: HomeListItem) => item.key, []); + + // Item objects are rebuilt on every collapse/show-more toggle; without this + // LegendList would consider every mounted row changed and re-render all of + // them (each carrying a swipeable + a vcs-status subscription), which made + // taps visibly laggy. Group/thread references are stable across toggles. + const itemsAreEqual = useCallback((previous: HomeListItem, item: HomeListItem) => { + if (previous.type !== item.type) return false; + switch (item.type) { + case "header": + return ( + previous.type === "header" && + previous.group === item.group && + previous.collapsed === item.collapsed && + previous.isFirst === item.isFirst + ); + case "thread": + return ( + previous.type === "thread" && + previous.thread === item.thread && + previous.isLast === item.isLast + ); + case "show-more": + return ( + previous.type === "show-more" && + previous.groupKey === item.groupKey && + previous.hiddenCount === item.hiddenCount && + previous.canShowLess === item.canShowLess + ); + } + }, []); + /* Empty states */ const hasAnyThreads = props.threads.some((thread) => thread.archivedAt === null); const hasResults = projectGroups.length > 0; @@ -423,7 +614,6 @@ export function HomeScreen(props: HomeScreenProps) { ? null : (props.savedConnectionsById[props.selectedEnvironmentId]?.environmentLabel ?? "this environment"); - const hasSearchQuery = props.searchQuery.trim().length > 0; const shouldShowConnectionStatus = shouldShowWorkspaceConnectionStatus(props.catalogState); const emptyState = deriveEmptyState({ catalogState: props.catalogState, @@ -467,7 +657,7 @@ export function HomeScreen(props: HomeScreenProps) { ); } - const listContent = ( + const listHeader = ( <> {Platform.OS === "ios" ? null : } @@ -480,106 +670,61 @@ export function HomeScreen(props: HomeScreenProps) { /> ) : null} - - {!hasResults && hasSearchQuery ? ( - - ) : !hasResults && selectedEnvironmentLabel ? ( - - ) : !hasResults ? ( - - ) : ( - projectGroups.map((group, groupIndex) => { - 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={{ - flexGrow: 1, - paddingHorizontal: 0, - paddingBottom: Platform.OS === "ios" ? Math.max(insets.bottom, 24) + 24 : 24, - gap: 0, - }} - scrollIndicatorInsets={ - Platform.OS === "ios" - ? { - bottom: Math.max(insets.bottom, 16) + 24, - top: 0, - } - : undefined - } - > - {listContent} - - ); + const listEmpty = !hasResults ? ( + hasSearchQuery ? ( + + ) : selectedEnvironmentLabel ? ( + + ) : ( + + ) + ) : null; return ( - <> - {scrollView} + + {/* Sticky headers are deliberately not wired up: LegendList's JS sticky + implementation mispositions pinned headers at mount under iOS + automatic content insets (headers render one nav-inset too low until + the first scroll event) and blanks non-pinned headers after + collapse/expand data changes. The flattened layout still exposes + `stickyHeaderIndices` if this gets revisited. */} + openSwipeableRef.current?.close()} + scrollEventThrottle={16} + contentContainerStyle={{ + paddingBottom: Platform.OS === "ios" ? Math.max(insets.bottom, 24) + 24 : 24, + }} + scrollIndicatorInsets={ + Platform.OS === "ios" + ? { + bottom: Math.max(insets.bottom, 16) + 24, + top: 0, + } + : undefined + } + /> {connectionStatus} - + ); } diff --git a/apps/mobile/src/features/home/homeListItems.test.ts b/apps/mobile/src/features/home/homeListItems.test.ts new file mode 100644 index 00000000000..891a7271104 --- /dev/null +++ b/apps/mobile/src/features/home/homeListItems.test.ts @@ -0,0 +1,192 @@ +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 { + buildHomeListLayout, + DEFAULT_GROUP_DISPLAY_STATE, + HOME_INITIAL_VISIBLE_THREADS, + HOME_SHOW_MORE_STEP, + nextGroupDisplayState, + type HomeGroupDisplayState, + type HomeListItem, +} from "./homeListItems"; +import type { HomeThreadGroup } from "./homeThreadList"; + +const environmentId = EnvironmentId.make("environment-1"); + +function makeProject(id: string, title: string): EnvironmentProject { + return { + environmentId, + id: ProjectId.make(id), + title, + workspaceRoot: `/workspaces/${id}`, + repositoryIdentity: null, + defaultModelSelection: null, + scripts: [], + createdAt: "2026-06-01T00:00:00.000Z", + updatedAt: "2026-06-01T00:00:00.000Z", + }; +} + +function makeThread(id: string, projectId: ProjectId): EnvironmentThreadShell { + return { + environmentId, + id: ThreadId.make(id), + projectId, + title: `Thread ${id}`, + 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, + }; +} + +function makeGroup(key: string, threadCount: number): HomeThreadGroup { + const project = makeProject(key, key); + return { + key, + title: key, + representative: project, + projects: [project], + threads: Array.from({ length: threadCount }, (_, index) => + makeThread(`${key}-thread-${index}`, project.id), + ), + }; +} + +function itemTypes(items: ReadonlyArray): string[] { + return items.map((item) => item.type); +} + +function displayStates( + entries: Record, +): ReadonlyMap { + return new Map(Object.entries(entries)); +} + +describe("buildHomeListLayout", () => { + it("renders a header plus all threads for a small group without a show-more row", () => { + const layout = buildHomeListLayout({ + groups: [makeGroup("alpha", 3)], + displayStates: displayStates({}), + }); + + expect(itemTypes(layout.items)).toEqual(["header", "thread", "thread", "thread"]); + expect(layout.stickyHeaderIndices).toEqual([0]); + expect(layout.items.at(-1)).toMatchObject({ type: "thread", isLast: true }); + }); + + it("limits large groups to the initial visible count with a show-more row", () => { + const layout = buildHomeListLayout({ + groups: [makeGroup("alpha", 133)], + displayStates: displayStates({}), + }); + + const threadItems = layout.items.filter((item) => item.type === "thread"); + expect(threadItems).toHaveLength(HOME_INITIAL_VISIBLE_THREADS); + expect(layout.items.at(-1)).toMatchObject({ + type: "show-more", + groupKey: "alpha", + hiddenCount: 133 - HOME_INITIAL_VISIBLE_THREADS, + canShowLess: false, + }); + // The show-more row takes over the last slot, so no thread is marked last. + expect(threadItems.every((item) => item.type === "thread" && !item.isLast)).toBe(true); + }); + + it("reveals more threads per show-more step and offers show-less when exhausted", () => { + const group = makeGroup("alpha", 20); + + const expandedOnce = buildHomeListLayout({ + groups: [group], + displayStates: displayStates({ + alpha: nextGroupDisplayState(DEFAULT_GROUP_DISPLAY_STATE, "show-more"), + }), + }); + expect(expandedOnce.items.filter((item) => item.type === "thread")).toHaveLength( + HOME_INITIAL_VISIBLE_THREADS + HOME_SHOW_MORE_STEP, + ); + expect(expandedOnce.items.at(-1)).toMatchObject({ + type: "show-more", + hiddenCount: 4, + canShowLess: true, + }); + + const fullyExpanded = buildHomeListLayout({ + groups: [group], + displayStates: displayStates({ + alpha: nextGroupDisplayState( + nextGroupDisplayState(DEFAULT_GROUP_DISPLAY_STATE, "show-more"), + "show-more", + ), + }), + }); + expect(fullyExpanded.items.filter((item) => item.type === "thread")).toHaveLength(20); + expect(fullyExpanded.items.at(-1)).toMatchObject({ + type: "show-more", + hiddenCount: 0, + canShowLess: true, + }); + + const reset = nextGroupDisplayState( + nextGroupDisplayState( + nextGroupDisplayState(DEFAULT_GROUP_DISPLAY_STATE, "show-more"), + "show-more", + ), + "show-less", + ); + expect(reset.visibleCount).toBe(HOME_INITIAL_VISIBLE_THREADS); + }); + + it("hides threads and the show-more row for collapsed groups", () => { + const layout = buildHomeListLayout({ + groups: [makeGroup("alpha", 12), makeGroup("beta", 2)], + displayStates: displayStates({ + alpha: nextGroupDisplayState(DEFAULT_GROUP_DISPLAY_STATE, "toggle-collapsed"), + }), + }); + + expect(itemTypes(layout.items)).toEqual(["header", "header", "thread", "thread"]); + expect(layout.items[0]).toMatchObject({ type: "header", collapsed: true, isFirst: true }); + expect(layout.items[1]).toMatchObject({ type: "header", collapsed: false, isFirst: false }); + expect(layout.stickyHeaderIndices).toEqual([0, 1]); + }); + + it("suspends collapse and pagination while searching", () => { + const layout = buildHomeListLayout({ + groups: [makeGroup("alpha", 12)], + displayStates: displayStates({ + alpha: nextGroupDisplayState(DEFAULT_GROUP_DISPLAY_STATE, "toggle-collapsed"), + }), + showAllThreads: true, + }); + + expect(layout.items.filter((item) => item.type === "thread")).toHaveLength(12); + expect(layout.items.some((item) => item.type === "show-more")).toBe(false); + }); + + it("keeps sticky indices aligned across multiple expanded groups", () => { + const layout = buildHomeListLayout({ + groups: [makeGroup("alpha", 8), makeGroup("beta", 1)], + displayStates: displayStates({}), + }); + + // header + 6 threads + show-more = 8 items, so beta's header is index 8. + expect(layout.stickyHeaderIndices).toEqual([0, 8]); + expect(layout.items[8]).toMatchObject({ type: "header", isFirst: false }); + }); +}); diff --git a/apps/mobile/src/features/home/homeListItems.ts b/apps/mobile/src/features/home/homeListItems.ts new file mode 100644 index 00000000000..3a66c1b2787 --- /dev/null +++ b/apps/mobile/src/features/home/homeListItems.ts @@ -0,0 +1,126 @@ +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; + +import type { HomeThreadGroup } from "./homeThreadList"; + +/** Threads shown per project before the "Show more" affordance appears. */ +export const HOME_INITIAL_VISIBLE_THREADS = 6; +/** Additional threads revealed per "Show more" tap. */ +export const HOME_SHOW_MORE_STEP = 10; + +export interface HomeGroupDisplayState { + readonly collapsed: boolean; + /** How many threads are currently revealed (clamped to the group size). */ + readonly visibleCount: number; +} + +export const DEFAULT_GROUP_DISPLAY_STATE: HomeGroupDisplayState = { + collapsed: false, + visibleCount: HOME_INITIAL_VISIBLE_THREADS, +}; + +export interface HomeHeaderListItem { + readonly type: "header"; + readonly key: string; + readonly group: HomeThreadGroup; + readonly collapsed: boolean; + readonly isFirst: boolean; +} + +export interface HomeThreadListItem { + readonly type: "thread"; + readonly key: string; + readonly thread: EnvironmentThreadShell; + readonly isLast: boolean; +} + +export interface HomeShowMoreListItem { + readonly type: "show-more"; + readonly key: string; + readonly groupKey: string; + /** Threads still hidden. 0 means the group is fully expanded. */ + readonly hiddenCount: number; + /** Whether more than the initial count is revealed, so "Show less" applies. */ + readonly canShowLess: boolean; +} + +export type HomeListItem = HomeHeaderListItem | HomeThreadListItem | HomeShowMoreListItem; + +export interface HomeListLayout { + readonly items: ReadonlyArray; + readonly stickyHeaderIndices: ReadonlyArray; +} + +export type HomeGroupDisplayAction = "toggle-collapsed" | "show-more" | "show-less"; + +export function nextGroupDisplayState( + current: HomeGroupDisplayState, + action: HomeGroupDisplayAction, +): HomeGroupDisplayState { + switch (action) { + case "toggle-collapsed": + return { ...current, collapsed: !current.collapsed }; + case "show-more": + return { ...current, visibleCount: current.visibleCount + HOME_SHOW_MORE_STEP }; + case "show-less": + return { ...current, visibleCount: HOME_INITIAL_VISIBLE_THREADS }; + } +} + +export function buildHomeListLayout(input: { + readonly groups: ReadonlyArray; + readonly displayStates: ReadonlyMap; + /** + * When searching, pagination is suspended so every match stays visible. + */ + readonly showAllThreads?: boolean; +}): HomeListLayout { + const items: HomeListItem[] = []; + const stickyHeaderIndices: number[] = []; + + for (const [groupIndex, group] of input.groups.entries()) { + const display = input.displayStates.get(group.key) ?? DEFAULT_GROUP_DISPLAY_STATE; + const collapsed = display.collapsed && input.showAllThreads !== true; + + stickyHeaderIndices.push(items.length); + items.push({ + type: "header", + key: `header:${group.key}`, + group, + collapsed, + isFirst: groupIndex === 0, + }); + + if (collapsed) { + continue; + } + + const totalCount = group.threads.length; + const visibleCount = input.showAllThreads + ? totalCount + : Math.min(Math.max(display.visibleCount, HOME_INITIAL_VISIBLE_THREADS), totalCount); + const visibleThreads = group.threads.slice(0, visibleCount); + const hiddenCount = totalCount - visibleCount; + const hasShowMoreRow = !input.showAllThreads && totalCount > HOME_INITIAL_VISIBLE_THREADS; + + for (const [threadIndex, thread] of visibleThreads.entries()) { + items.push({ + type: "thread", + key: `thread:${thread.environmentId}:${thread.id}`, + thread, + isLast: threadIndex === visibleThreads.length - 1 && !hasShowMoreRow, + }); + } + + if (hasShowMoreRow) { + items.push({ + type: "show-more", + key: `show-more:${group.key}`, + groupKey: group.key, + hiddenCount, + canShowLess: visibleCount > HOME_INITIAL_VISIBLE_THREADS, + }); + } + } + + return { items, stickyHeaderIndices }; +} diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index 2bc0f3378e5..152fa6e8ac1 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -70,6 +70,11 @@ export function ThreadSwipeable(props: { containerStyle={[{ backgroundColor: props.backgroundColor }, props.containerStyle]} dragOffsetFromRightEdge={8} enableTrackpadTwoFingerGesture={props.enableTrackpadSwipe ?? true} + // Fail the swipe once the pan is vertically dominant (patched-in RNGH + // prop) — otherwise trackpad scrolls with ~8px of horizontal drift + // start opening rows because the swipe pan runs simultaneously with + // the list scroll gesture and never gets disqualified by Y movement. + failOffsetY={[-10, 10]} friction={1} onSwipeableClose={() => { fullSwipeArmedRef.current = false; diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 306a137af04..17c45a1f606 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -277,6 +277,12 @@ export function AdaptiveWorkspaceLayout(props: { navigation.navigate("SettingsSheet", { screen: "Settings" }); }, [navigation]); + // Minted here (root stack navigation) so the sidebar pane stays free of + // navigation hooks — on iOS it renders inside an independent nav tree. + const handleOpenEnvironmentSettings = useCallback(() => { + navigation.navigate("SettingsSheet", { screen: "SettingsEnvironments" }); + }, [navigation]); + const renderedSidebarWidth = useSharedValue( panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0, ); @@ -342,6 +348,7 @@ export function AdaptiveWorkspaceLayout(props: { onRequestVisibility={revealPrimarySidebar} selectedThreadKey={selectedThreadKey} onOpenSettings={handleOpenSettings} + onOpenEnvironmentSettings={handleOpenEnvironmentSettings} onSelectThread={handleSelectThread} onSearchQueryChange={setPrimarySidebarSearchQuery} searchQuery={primarySidebarSearchQuery} diff --git a/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx b/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx index 3710bf255ac..a143c2f1834 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationDrawer.tsx @@ -23,7 +23,7 @@ import { StatusPill } from "../../components/StatusPill"; import { useProjects, useThreadShells } from "../../state/entities"; import { scopedThreadKey } from "../../lib/scopedEntities"; import { relativeTime } from "../../lib/time"; -import { threadStatusTone } from "./threadPresentation"; +import { resolveThreadStatus } from "./threadPresentation"; import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { buildThreadNavigationGroups } from "./thread-navigation-groups"; @@ -211,6 +211,7 @@ function ThreadNavigationDrawerContent(props: { group.threads.map((thread, index) => { const threadKey = scopedThreadKey(thread.environmentId, thread.id); const selected = props.selectedThreadKey === threadKey; + const status = resolveThreadStatus(thread); return ( - + {status ? : null} ); diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 6803d108101..d02d853f343 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -3,7 +3,6 @@ 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 { useNavigation } from "@react-navigation/native"; import { memo, useCallback, @@ -23,17 +22,18 @@ import { Platform, Pressable, StyleSheet, TextInput, View, useColorScheme } from 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"; +import type { SearchBarCommands } from "react-native-screens"; import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; import { StatusPill } from "../../components/StatusPill"; +import { NativeStackScreenOptions } from "../../native/StackHeader"; 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 type { WorkspaceState } from "../../state/workspaceModel"; import { useSavedRemoteConnections } from "../../state/use-remote-environment-registry"; import { useHardwareKeyboardCommand } from "../keyboard/hardwareKeyboardCommands"; import { @@ -43,6 +43,7 @@ import { THREAD_SORT_OPTIONS, useHomeListOptions, } from "../home/home-list-options"; +import { buildHomeListFilterMenu } from "../home/home-list-filter-menu"; import { buildHomeThreadGroups } from "../home/homeThreadList"; import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { useThreadListActions } from "../home/useThreadListActions"; @@ -50,7 +51,9 @@ import { WorkspaceConnectionStatus } from "../home/WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "../home/workspace-connection-status"; import { SidebarHeaderActions } from "./sidebar-header-actions"; import { SidebarFilterButton } from "./sidebar-filter-button"; -import { threadStatusTone } from "./threadPresentation"; +import { createSidebarHeaderItems } from "./sidebar-native-header-items"; +import { SidebarNavigationShell } from "./sidebar-navigation-shell"; +import { resolveThreadStatus } from "./threadPresentation"; /** * Shared capsule behind the sidebar header buttons — a native liquid-glass @@ -119,7 +122,6 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { readonly thread: EnvironmentThreadShell; readonly environmentLabel: string | null; }) { - const iconColor = useThemeColor("--color-icon-muted"); const [hovered, setHovered] = useState(false); const { backgroundColor, @@ -178,14 +180,15 @@ const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { const subtitle = [environmentLabel, thread.branch].filter((part): part is string => Boolean(part), ); - const statusTone = threadStatusTone(thread); - const effectiveStatusTone = selected - ? { - ...statusTone, - pillClassName: "bg-white/20", - textClassName: "text-white", - } - : statusTone; + const statusTone = resolveThreadStatus(thread); + const effectiveStatusTone = + selected && statusTone + ? { + ...statusTone, + pillClassName: "bg-white/20", + textClassName: "text-white", + } + : statusTone; return ( {() => ( - + - + {effectiveStatusTone ? : null} - - [ - styles.moreButton, - { backgroundColor: pressed ? effectivePressedBackgroundColor : "transparent" }, - ]} - > - - - - + + )} ); @@ -281,25 +276,70 @@ type SidebarListItem = readonly thread: EnvironmentThreadShell; }; -export function ThreadNavigationSidebar(props: { +interface ThreadNavigationSidebarProps { readonly width: number; readonly visible: boolean; readonly selectedThreadKey: string | null; readonly onOpenSettings: () => void; + readonly onOpenEnvironmentSettings: () => void; readonly onSearchQueryChange: (query: string) => void; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; readonly onRequestVisibility: () => void; readonly searchQuery: string; -}) { +} + +/** + * iPad/large-width sidebar column. + * + * On iOS the pane is hosted inside its own navigation-inert single-screen + * native stack (SidebarNavigationShell) so the header is a real + * UINavigationBar: large title, native bar-button items, and a + * UISearchController search field — the same chrome a UISplitViewController + * column gets. Other platforms keep the custom header chrome. + */ +export function ThreadNavigationSidebar(props: ThreadNavigationSidebarProps) { + if (Platform.OS !== "ios") { + return ; + } + return ; +} + +function NativeSidebarContainer(props: ThreadNavigationSidebarProps) { + const backgroundColor = useThemeColor("--color-drawer"); + const borderColor = useThemeColor("--color-border"); + + return ( + + + + + + ); +} + +function ThreadNavigationSidebarPane( + props: ThreadNavigationSidebarProps & { readonly nativeChrome: boolean }, +) { const insets = useSafeAreaInsets(); const colorScheme = useColorScheme() === "dark" ? "dark" : "light"; - const navigation = useNavigation(); const projects = useProjects(); const threads = useThreadShells(); const { state: catalogState } = useWorkspaceState(); const { savedConnectionsById } = useSavedRemoteConnections(); const [headerIsOverContent, setHeaderIsOverContent] = useState(false); const searchInputRef = useRef(null); + const searchBarRef = useRef(null); const openSwipeableRef = useRef(null); const headerIsOverContentRef = useRef(false); const sidebarScrollGesture = useMemo(() => Gesture.Native(), []); @@ -499,16 +539,21 @@ export function ThreadNavigationSidebar(props: { setHeaderIsOverContent(next); }, []); const focusSearch = useCallback(() => { + const focus = () => { + if (props.nativeChrome) { + searchBarRef.current?.focus(); + return; + } + searchInputRef.current?.focus(); + }; if (!props.visible) { props.onRequestVisibility(); - setTimeout(() => { - searchInputRef.current?.focus(); - }, 240); + setTimeout(focus, 240); } else { - searchInputRef.current?.focus(); + focus(); } return true; - }, [props.onRequestVisibility, props.visible]); + }, [props.nativeChrome, props.onRequestVisibility, props.visible]); useHardwareKeyboardCommand("focusSearch", focusSearch); const renderListItem = useCallback( ({ item }: { readonly item: SidebarListItem }) => { @@ -573,6 +618,115 @@ export function ThreadNavigationSidebar(props: { const filterIcon = hasCustomHomeListOptions(options) ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle"; + const filterMenu = useMemo( + () => + buildHomeListFilterMenu({ + environments, + selectedEnvironmentId: options.selectedEnvironmentId, + projectSortOrder: options.projectSortOrder, + threadSortOrder: options.threadSortOrder, + projectGroupingMode: options.projectGroupingMode, + onEnvironmentChange: setSelectedEnvironmentId, + onProjectSortOrderChange: setProjectSortOrder, + onThreadSortOrderChange: setThreadSortOrder, + onProjectGroupingModeChange: setProjectGroupingMode, + }), + [ + environments, + options, + setProjectGroupingMode, + setProjectSortOrder, + setSelectedEnvironmentId, + setThreadSortOrder, + ], + ); + const nativeHeaderItems = useMemo( + () => + createSidebarHeaderItems({ + filterIcon, + filterMenu, + onOpenSettings: props.onOpenSettings, + }), + [filterIcon, filterMenu, props.onOpenSettings], + ); + const listEmpty = ( + + {catalogState.isLoadingConnections + ? "Loading threads…" + : props.searchQuery.trim().length > 0 + ? "No matching threads" + : "No threads yet"} + + ); + + if (props.nativeChrome) { + return ( + <> + { + props.onSearchQueryChange(""); + }, + onChangeText: (event) => { + props.onSearchQueryChange(event.nativeEvent.text); + }, + }, + unstable_headerRightItems: () => nativeHeaderItems, + }} + /> + + + item.kind} + keyExtractor={(item) => item.key} + renderItem={renderListItem} + automaticallyAdjustsScrollIndicatorInsets + contentInsetAdjustmentBehavior="automatic" + contentContainerStyle={[ + styles.threadListContent, + { + paddingBottom: Math.max(insets.bottom, 16) + 16, + paddingTop: 6, + }, + ]} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="handled" + onScrollBeginDrag={() => openSwipeableRef.current?.close()} + scrollEventThrottle={16} + showsVerticalScrollIndicator={false} + style={styles.threadList} + ListHeaderComponent={ + showsConnectionStatus ? ( + + + + ) : null + } + ListEmptyComponent={listEmpty} + /> + + + + ); + } + return ( - {catalogState.isLoadingConnections - ? "Loading threads…" - : props.searchQuery.trim().length > 0 - ? "No matching threads" - : "No threads yet"} - - } + ListEmptyComponent={listEmpty} /> @@ -715,11 +861,7 @@ export function ThreadNavigationSidebar(props: { {showsConnectionStatus ? ( - navigation.navigate("SettingsSheet", { - screen: "SettingsEnvironments", - }) - } + onPress={props.onOpenEnvironmentSettings} state={catalogState} variant="sidebar" /> @@ -765,6 +907,11 @@ const styles = StyleSheet.create({ paddingTop: 10, paddingHorizontal: 14, }, + connectionStatusNative: { + paddingBottom: 8, + paddingHorizontal: 6, + paddingTop: 2, + }, searchField: { height: 38, marginTop: 9, @@ -831,12 +978,4 @@ const styles = StyleSheet.create({ alignItems: "center", gap: 6, }, - moreButton: { - width: 30, - height: 30, - borderRadius: 9, - alignItems: "center", - justifyContent: "center", - cursor: "pointer", - }, }); diff --git a/apps/mobile/src/features/threads/sidebar-native-header-items.ts b/apps/mobile/src/features/threads/sidebar-native-header-items.ts new file mode 100644 index 00000000000..b80fffda057 --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-native-header-items.ts @@ -0,0 +1,63 @@ +import type { + NativeStackHeaderItem, + NativeStackHeaderItemMenu, +} from "@react-navigation/native-stack"; + +import type { HomeListFilterMenu } from "../home/home-list-filter-menu"; +import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; + +type NativeHeaderMenuItems = NativeStackHeaderItemMenu["menu"]["items"]; +type NativeHeaderIcon = NonNullable["icon"]>; + +function sfSymbolIcon(name: string): NativeHeaderIcon { + return { type: "sfSymbol", name: name as never }; +} + +function toNativeHeaderMenuItems(items: HomeListFilterMenu["items"]): NativeHeaderMenuItems { + return items.map((item) => + item.type === "action" + ? { + type: "action" as const, + label: item.title, + description: item.subtitle, + onPress: item.onPress, + state: item.state === "on" ? ("on" as const) : undefined, + } + : { + type: "submenu" as const, + label: item.title, + items: toNativeHeaderMenuItems(item.items), + }, + ); +} + +/** + * Right-side UINavigationBar items for the sidebar column: the thread list + * filter/sort menu plus the settings button, sharing one glass capsule — + * the Messages-style grouped header buttons. + */ +export function createSidebarHeaderItems(input: { + readonly filterIcon: string; + readonly filterMenu: HomeListFilterMenu; + readonly onOpenSettings: () => void; +}): NativeStackHeaderItem[] { + return [ + withNativeGlassHeaderItem({ + type: "menu", + label: "", + accessibilityLabel: "Filter and sort threads", + icon: sfSymbolIcon(input.filterIcon), + menu: { + title: input.filterMenu.title, + items: toNativeHeaderMenuItems(input.filterMenu.items), + }, + }), + withNativeGlassHeaderItem({ + type: "button", + label: "", + accessibilityLabel: "Open settings", + icon: sfSymbolIcon("gearshape"), + onPress: input.onOpenSettings, + }), + ]; +} diff --git a/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx new file mode 100644 index 00000000000..f1e06926d0b --- /dev/null +++ b/apps/mobile/src/features/threads/sidebar-navigation-shell.tsx @@ -0,0 +1,72 @@ +import { + DarkTheme, + DefaultTheme, + NavigationContainer, + NavigationIndependentTree, +} from "@react-navigation/native"; +import { + createNativeStackNavigator, + type NativeStackNavigationOptions, +} from "@react-navigation/native-stack"; +import type { ReactNode } from "react"; +import { Platform, useColorScheme } from "react-native"; + +import { nativeHeaderScrollEdgeEffects } from "../../native/StackHeader"; + +const SCROLL_EDGE_EFFECTS = nativeHeaderScrollEdgeEffects(Platform.OS, Platform.Version); + +type SidebarScreenOptions = NativeStackNavigationOptions & { + // Same patched RNS option the GLASS/SOLID presets in Stack.tsx use — the + // iOS 26 "editor" navigation-item style leading-aligns the inline title. + readonly unstable_navigationItemStyle?: "editor"; +}; + +/** + * Static chrome for the sidebar column: a real UINavigationBar with a fixed + * inline title (no large title — saves vertical space, left-aligned via the + * editor item style) and the search bar pinned below it, scroll-edge blur + * sampling the list. Only genuinely dynamic values (search callbacks, header + * items) are set by the screen content via NativeStackScreenOptions. + */ +const SIDEBAR_SCREEN_OPTIONS: SidebarScreenOptions = { + contentStyle: { backgroundColor: "transparent" }, + headerLargeTitle: false, + headerShadowVisible: false, + headerShown: true, + headerStyle: { backgroundColor: "transparent" }, + headerTitleStyle: { fontSize: 18, fontWeight: "800" }, + headerTransparent: true, + scrollEdgeEffects: SCROLL_EDGE_EFFECTS, + title: "Threads", + unstable_navigationItemStyle: "editor", +}; + +const SidebarStack = createNativeStackNavigator(); + +/** + * Hosts the iPad sidebar pane inside its own single-screen native stack. + * + * The stack is navigation-inert — nothing is ever pushed onto it. It exists so + * the sidebar column owns a real UINavigationBar (large title, native bar + * button items, UISearchController), mirroring how each column of a + * UISplitViewController has its own UINavigationController. All real + * navigation still flows through the root stack via callbacks minted in + * AdaptiveWorkspaceLayout; NavigationIndependentTree only isolates the + * navigation hooks used for header configuration inside the pane. + */ +export function SidebarNavigationShell(props: { readonly children: ReactNode }) { + const colorScheme = useColorScheme(); + + return ( + + + + {() => props.children} + + + + ); +} diff --git a/apps/mobile/src/features/threads/threadPresentation.ts b/apps/mobile/src/features/threads/threadPresentation.ts index cf5eb1817a4..9de3d4d3089 100644 --- a/apps/mobile/src/features/threads/threadPresentation.ts +++ b/apps/mobile/src/features/threads/threadPresentation.ts @@ -1,4 +1,5 @@ import type { StatusTone } from "../../components/StatusPill"; +import type { OrchestrationLatestTurn, OrchestrationSession } from "@t3tools/contracts"; import { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; export function threadSortValue(thread: EnvironmentThreadShell): number { @@ -6,39 +7,123 @@ export function threadSortValue(thread: EnvironmentThreadShell): number { return Number.isNaN(candidate) ? 0 : candidate; } -export function threadStatusTone(thread: EnvironmentThreadShell): StatusTone { - const status = thread.session?.status; - if (status === "running") { +export type ThreadStatusKind = + | "pending-approval" + | "awaiting-input" + | "working" + | "connecting" + | "error" + | "plan-ready"; + +export interface ThreadStatusPresentation extends StatusTone { + readonly kind: ThreadStatusKind; + /** Foreground color for the leading status icon. */ + readonly iconColor: string; + /** Background color for the leading status icon circle. */ + readonly iconBackground: string; + /** Whether the indicator represents in-flight activity. */ + readonly pulse: boolean; +} + +/** Neutral icon colors for threads with no actionable status. */ +export const THREAD_STATUS_NEUTRAL_ICON = { + iconColor: "#8e8e93", + iconBackground: "rgba(142,142,147,0.22)", +} as const; + +function isLatestTurnSettled( + latestTurn: OrchestrationLatestTurn | null, + session: OrchestrationSession | null, +): boolean { + if (!latestTurn?.startedAt) return false; + if (!latestTurn.completedAt) return false; + if (!session) return true; + return session.status !== "running"; +} + +/** + * Resolves the user-facing status of a thread, in priority order. Returns + * `null` for quiescent threads so rows stay free of "Idle"-style noise. + * Mirrors `resolveThreadStatusPill` in apps/web/src/components/Sidebar.logic.ts. + */ +export function resolveThreadStatus( + thread: EnvironmentThreadShell, +): ThreadStatusPresentation | null { + if (thread.hasPendingApprovals) { + return { + kind: "pending-approval", + label: "Needs Approval", + pillClassName: "bg-amber-500/12 dark:bg-amber-500/16", + textClassName: "text-amber-700 dark:text-amber-300", + iconColor: "#ff9f0a", + iconBackground: "rgba(255,159,10,0.22)", + pulse: false, + }; + } + + if (thread.hasPendingUserInput) { return { - label: "Running", - pillClassName: "bg-orange-500/12 dark:bg-orange-500/16", - textClassName: "text-orange-700 dark:text-orange-300", + kind: "awaiting-input", + label: "Awaiting Input", + pillClassName: "bg-indigo-500/12 dark:bg-indigo-500/16", + textClassName: "text-indigo-700 dark:text-indigo-300", + iconColor: "#5e5ce6", + iconBackground: "rgba(94,92,230,0.22)", + pulse: false, }; } - if (status === "ready") { + + if (thread.session?.status === "running") { return { - label: "Ready", - pillClassName: "bg-emerald-500/12 dark:bg-emerald-500/16", - textClassName: "text-emerald-700 dark:text-emerald-300", + kind: "working", + label: "Working", + pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", + textClassName: "text-sky-700 dark:text-sky-300", + iconColor: "#0a84ff", + iconBackground: "rgba(10,132,255,0.22)", + pulse: true, }; } - if (status === "starting") { + + if (thread.session?.status === "starting") { return { - label: "Starting", + kind: "connecting", + label: "Connecting", pillClassName: "bg-sky-500/12 dark:bg-sky-500/16", textClassName: "text-sky-700 dark:text-sky-300", + iconColor: "#0a84ff", + iconBackground: "rgba(10,132,255,0.22)", + pulse: true, }; } - if (status === "error") { + + if (thread.session?.status === "error" || thread.latestTurn?.state === "error") { return { + kind: "error", label: "Error", pillClassName: "bg-rose-500/12 dark:bg-rose-500/16", textClassName: "text-rose-700 dark:text-rose-300", + iconColor: "#ff453a", + iconBackground: "rgba(255,69,58,0.22)", + pulse: false, }; } - return { - label: "Idle", - pillClassName: "bg-neutral-500/10 dark:bg-neutral-500/16", - textClassName: "text-neutral-600 dark:text-neutral-300", - }; + + const hasPlanReadyPrompt = + thread.interactionMode === "plan" && + isLatestTurnSettled(thread.latestTurn, thread.session) && + thread.hasActionableProposedPlan; + if (hasPlanReadyPrompt) { + return { + kind: "plan-ready", + label: "Plan Ready", + pillClassName: "bg-violet-500/12 dark:bg-violet-500/16", + textClassName: "text-violet-700 dark:text-violet-300", + iconColor: "#bf5af2", + iconBackground: "rgba(191,90,242,0.22)", + pulse: false, + }; + } + + return null; } diff --git a/apps/mobile/src/state/use-thread-pr.ts b/apps/mobile/src/state/use-thread-pr.ts new file mode 100644 index 00000000000..b7d890bbe7f --- /dev/null +++ b/apps/mobile/src/state/use-thread-pr.ts @@ -0,0 +1,67 @@ +import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; +import type { VcsStatusResult } from "@t3tools/contracts"; +import { resolveChangeRequestPresentation } from "@t3tools/shared/sourceControl"; + +import { useEnvironmentQuery } from "./query"; +import { vcsEnvironment } from "./vcs"; + +export type ThreadPr = NonNullable; + +export interface ThreadPrPresentation { + readonly number: number; + readonly state: ThreadPr["state"]; + readonly url: string; + /** Compact chip label, e.g. "PR open" / "MR merged". */ + readonly label: string; + readonly textClassName: string; +} + +const PR_STATE_TEXT_CLASS: Record = { + open: "text-emerald-600 dark:text-emerald-400", + merged: "text-violet-600 dark:text-violet-400", + closed: "text-zinc-500 dark:text-zinc-400", +}; + +export function presentThreadPr( + pr: ThreadPr, + provider: VcsStatusResult["sourceControlProvider"] | null | undefined, +): ThreadPrPresentation { + const shortName = resolveChangeRequestPresentation(provider).shortName; + return { + number: pr.number, + state: pr.state, + url: pr.url, + label: `${shortName} ${pr.state}`, + textClassName: PR_STATE_TEXT_CLASS[pr.state], + }; +} + +/** + * Live PR status for a thread's branch. Subscriptions are deduplicated per + * (environmentId, cwd) by the atom family, so many rows on the same worktree + * or project root share one stream — and virtualization means only visible + * rows subscribe at all. + */ +export function useThreadPr( + thread: EnvironmentThreadShell, + projectCwd: string | null, +): ThreadPrPresentation | null { + const cwd = thread.worktreePath ?? projectCwd; + const gitStatus = useEnvironmentQuery( + thread.branch !== null && cwd !== null + ? vcsEnvironment.status({ + environmentId: thread.environmentId, + input: { cwd }, + }) + : null, + ); + + const status = gitStatus.data; + if (status === null || thread.branch === null || status.refName !== thread.branch) { + return null; + } + if (!status.pr) { + return null; + } + return presentThreadPr(status.pr, status.sourceControlProvider); +} diff --git a/patches/react-native-gesture-handler@2.31.2.patch b/patches/react-native-gesture-handler@2.31.2.patch new file mode 100644 index 00000000000..a4f345e9e33 --- /dev/null +++ b/patches/react-native-gesture-handler@2.31.2.patch @@ -0,0 +1,124 @@ +diff --git a/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js +index 551ab92bf58db0b79d428dcd2f2df898ef686493..f1c355b8e40cd4f583ef3a501b044fb6770f4a24 100644 +--- a/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js ++++ b/lib/commonjs/components/ReanimatedSwipeable/ReanimatedSwipeable.js +@@ -34,6 +34,7 @@ const Swipeable = props => { + enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, + dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, + dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, ++ failOffsetY, + friction = DEFAULT_FRICTION, + overshootFriction = DEFAULT_OVERSHOOT_FRICTION, + onSwipeableOpenStartDrag, +@@ -285,11 +286,14 @@ const Swipeable = props => { + }).onFinalize(() => { + dragStarted.value = false; + }); ++ if (failOffsetY !== undefined) { ++ pan.failOffsetY(failOffsetY); ++ } + Object.entries(relationProps).forEach(([relationName, relation]) => { + (0, _utils.applyRelationProp)(pan, relationName, relation); + }); + return pan; +- }, [enabled, hitSlop, enableTrackpadTwoFingerGesture, dragOffsetFromRightEdge, dragOffsetFromLeftEdge, updateElementWidths, relationProps, userDrag, rowState, dragStarted, updateAnimatedEvent, onSwipeableOpenStartDrag, onSwipeableCloseStartDrag, handleRelease]); ++ }, [enabled, hitSlop, enableTrackpadTwoFingerGesture, dragOffsetFromRightEdge, dragOffsetFromLeftEdge, failOffsetY, updateElementWidths, relationProps, userDrag, rowState, dragStarted, updateAnimatedEvent, onSwipeableOpenStartDrag, onSwipeableCloseStartDrag, handleRelease]); + (0, _react.useImperativeHandle)(ref, () => swipeableMethods, [swipeableMethods]); + const animatedStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({ + transform: [{ +diff --git a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js +index a2835d5416ffd5cf9a04e98774516b9e6569691e..b73c177227bf3a232737b0eb1c0e2bb830493c22 100644 +--- a/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js ++++ b/lib/module/components/ReanimatedSwipeable/ReanimatedSwipeable.js +@@ -29,6 +29,7 @@ const Swipeable = props => { + enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, + dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, + dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, ++ failOffsetY, + friction = DEFAULT_FRICTION, + overshootFriction = DEFAULT_OVERSHOOT_FRICTION, + onSwipeableOpenStartDrag, +@@ -280,11 +281,14 @@ const Swipeable = props => { + }).onFinalize(() => { + dragStarted.value = false; + }); ++ if (failOffsetY !== undefined) { ++ pan.failOffsetY(failOffsetY); ++ } + Object.entries(relationProps).forEach(([relationName, relation]) => { + applyRelationProp(pan, relationName, relation); + }); + return pan; +- }, [enabled, hitSlop, enableTrackpadTwoFingerGesture, dragOffsetFromRightEdge, dragOffsetFromLeftEdge, updateElementWidths, relationProps, userDrag, rowState, dragStarted, updateAnimatedEvent, onSwipeableOpenStartDrag, onSwipeableCloseStartDrag, handleRelease]); ++ }, [enabled, hitSlop, enableTrackpadTwoFingerGesture, dragOffsetFromRightEdge, dragOffsetFromLeftEdge, failOffsetY, updateElementWidths, relationProps, userDrag, rowState, dragStarted, updateAnimatedEvent, onSwipeableOpenStartDrag, onSwipeableCloseStartDrag, handleRelease]); + useImperativeHandle(ref, () => swipeableMethods, [swipeableMethods]); + const animatedStyle = useAnimatedStyle(() => ({ + transform: [{ +diff --git a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts +index ac8b76830d468edfbc29052b452a36221323c3de..589e329d2aa706ddfff0d1037df0d85a050edbb0 100644 +--- a/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts ++++ b/lib/typescript/components/ReanimatedSwipeable/ReanimatedSwipeableProps.d.ts +@@ -64,6 +64,13 @@ export interface SwipeableProps { + * a swipe. The default value is 10. + */ + dragOffsetFromRightEdge?: number; ++ /** ++ * Range along the Y axis (in points) that, once exceeded before the swipe ++ * activates, makes the pan fail. Lets vertically-dominant pans (list ++ * scrolling, especially trackpad two-finger pans) skip the swipe entirely. ++ * Passed straight to the underlying Pan gesture's `failOffsetY`. ++ */ ++ failOffsetY?: number | [start: number, end: number]; + /** + * Value indicating if the swipeable panel can be pulled further than the left + * actions panel's width. It is set to true by default as long as the left +diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx +index b6134c908624adc590ebd264e90e558b88e235d5..41535348e937ad7762bd531d5b63ba6e092ea997 100644 +--- a/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx ++++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeable.tsx +@@ -58,6 +58,7 @@ const Swipeable = (props: SwipeableProps) => { + enableTrackpadTwoFingerGesture = DEFAULT_ENABLE_TRACKING_TWO_FINGER_GESTURE, + dragOffsetFromLeftEdge = DEFAULT_DRAG_OFFSET, + dragOffsetFromRightEdge = DEFAULT_DRAG_OFFSET, ++ failOffsetY, + friction = DEFAULT_FRICTION, + overshootFriction = DEFAULT_OVERSHOOT_FRICTION, + onSwipeableOpenStartDrag, +@@ -537,6 +538,10 @@ const Swipeable = (props: SwipeableProps) => { + dragStarted.value = false; + }); + ++ if (failOffsetY !== undefined) { ++ pan.failOffsetY(failOffsetY); ++ } ++ + Object.entries(relationProps).forEach(([relationName, relation]) => { + applyRelationProp( + pan, +@@ -552,6 +557,7 @@ const Swipeable = (props: SwipeableProps) => { + enableTrackpadTwoFingerGesture, + dragOffsetFromRightEdge, + dragOffsetFromLeftEdge, ++ failOffsetY, + updateElementWidths, + relationProps, + userDrag, +diff --git a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts +index 0c0e517e0d340faf50ff78c3d48e7a2bbcf808ec..d4b6ff508077728c8be83762923d866dc95b144c 100644 +--- a/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts ++++ b/src/components/ReanimatedSwipeable/ReanimatedSwipeableProps.ts +@@ -77,6 +77,14 @@ export interface SwipeableProps { + */ + dragOffsetFromRightEdge?: number; + ++ /** ++ * Range along the Y axis (in points) that, once exceeded before the swipe ++ * activates, makes the pan fail. Lets vertically-dominant pans (list ++ * scrolling, especially trackpad two-finger pans) skip the swipe entirely. ++ * Passed straight to the underlying Pan gesture's `failOffsetY`. ++ */ ++ failOffsetY?: number | [start: number, end: number]; ++ + /** + * Value indicating if the swipeable panel can be pulled further than the left + * actions panel's width. It is set to true by default as long as the left diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ad4f4e59660..2ae8706df8e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -37,17 +37,17 @@ catalogs: overrides: '@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': '-' '@clerk/clerk-js>@solana/wallet-adapter-react': '-' '@clerk/clerk-js>@solana/wallet-standard': '-' '@clerk/clerk-js>@wallet-standard/core': '-' + '@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/platform-bun': 4.0.0-beta.78 '@effect/platform-node': 4.0.0-beta.78 @@ -87,6 +87,9 @@ patchedDependencies: expo-modules-jsi@56.0.10: hash: 9170f8074ae4e35a0a086e756c8f815794fd3abe51eac67ca3ba02804225ec1f path: patches/expo-modules-jsi@56.0.10.patch + react-native-gesture-handler@2.31.2: + hash: 808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3 + path: patches/react-native-gesture-handler@2.31.2.patch react-native-nitro-modules@0.35.9: hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 path: patches/react-native-nitro-modules@0.35.9.patch @@ -366,7 +369,7 @@ importers: 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))(@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(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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) @@ -11959,7 +11962,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(f23e7c64314c5d311da1a70c8215e664) + expo-router: 56.2.11(edfa63c4d244c7ff5b11fa13f7e1f1c9) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@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' @@ -12257,7 +12260,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(f23e7c64314c5d311da1a70c8215e664) + expo-router: 56.2.11(edfa63c4d244c7ff5b11fa13f7e1f1c9) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -16234,7 +16237,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(f23e7c64314c5d311da1a70c8215e664): + expo-router@56.2.11(edfa63c4d244c7ff5b11fa13f7e1f1c9): 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) @@ -16263,7 +16266,7 @@ snapshots: 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-drawer-layout: 4.2.4(react-native-gesture-handler@2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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 @@ -16273,7 +16276,7 @@ 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-gesture-handler: 2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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' @@ -18815,17 +18818,17 @@ 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): - dependencies: + ? react-native-drawer-layout@4.2.4(react-native-gesture-handler@2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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))(@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-gesture-handler: 2.31.2(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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) 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))(@types/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(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index b3b0785538b..e27b63e475e 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -20,27 +20,27 @@ supportedArchitectures: - glibc catalog: - "@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 - "@effect/platform-node": 4.0.0-beta.78 - "@effect/platform-node-shared": 4.0.0-beta.78 - "@effect/sql-pg": 4.0.0-beta.78 - "@effect/sql-sqlite-bun": 4.0.0-beta.78 - "@effect/tsgo": 0.13.2 - "@effect/vitest": 4.0.0-beta.78 - "@noble/curves": 1.9.1 - "@noble/hashes": 1.8.0 - "@pierre/diffs": 1.3.0-beta.5 - "@types/node": 24.12.4 - "@typescript/native-preview": 7.0.0-dev.20260604.1 + '@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 + '@effect/platform-node': 4.0.0-beta.78 + '@effect/platform-node-shared': 4.0.0-beta.78 + '@effect/sql-pg': 4.0.0-beta.78 + '@effect/sql-sqlite-bun': 4.0.0-beta.78 + '@effect/tsgo': 0.13.2 + '@effect/vitest': 4.0.0-beta.78 + '@noble/curves': 1.9.1 + '@noble/hashes': 1.8.0 + '@pierre/diffs': 1.3.0-beta.5 + '@types/node': 24.12.4 + '@typescript/native-preview': 7.0.0-dev.20260604.1 effect: 4.0.0-beta.78 jose: 6.2.2 typescript: ~6.0.3 @@ -59,58 +59,54 @@ onlyBuiltDependencies: - sharp overrides: - # Keep every Clerk consumer on the same snapshot train. Clerk publishes wallet - # auth integrations as required dependencies, but T3 Code does not support - # wallet auth, so keep that unused dependency tree out of installs. - "@clerk/backend": "catalog:" - "@clerk/clerk-js": "catalog:" - "@clerk/electron": "catalog:" - "@clerk/electron-passkeys": "catalog:" - "@clerk/expo": "catalog:" - "@clerk/react": "catalog:" - "@clerk/shared": "catalog:" - "@clerk/clerk-js>@base-org/account": "-" - "@clerk/clerk-js>@coinbase/wallet-sdk": "-" - "@clerk/clerk-js>@solana/wallet-adapter-base": "-" - "@clerk/clerk-js>@solana/wallet-adapter-react": "-" - "@clerk/clerk-js>@solana/wallet-standard": "-" - "@clerk/clerk-js>@wallet-standard/core": "-" - "@effect/atom-react": "catalog:" - "@effect/platform-bun": "catalog:" - "@effect/platform-node": "catalog:" - "@effect/platform-node-shared": "catalog:" - "@effect/sql-pg": "catalog:" - "@effect/sql-sqlite-bun": "catalog:" - "@effect/vitest": "catalog:" - # @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.14 - "@pierre/diffs>@shikijs/transformers": ^4.2.0 - "@types/node": "catalog:" - effect: "catalog:" - vite: "catalog:" - yaml: "catalog:" + '@clerk/backend': 'catalog:' + '@clerk/clerk-js': 'catalog:' + '@clerk/clerk-js>@base-org/account': '-' + '@clerk/clerk-js>@coinbase/wallet-sdk': '-' + '@clerk/clerk-js>@solana/wallet-adapter-base': '-' + '@clerk/clerk-js>@solana/wallet-adapter-react': '-' + '@clerk/clerk-js>@solana/wallet-standard': '-' + '@clerk/clerk-js>@wallet-standard/core': '-' + '@clerk/electron': 'catalog:' + '@clerk/electron-passkeys': 'catalog:' + '@clerk/expo': 'catalog:' + '@clerk/react': 'catalog:' + '@clerk/shared': 'catalog:' + '@effect/atom-react': 'catalog:' + '@effect/platform-bun': 'catalog:' + '@effect/platform-node': 'catalog:' + '@effect/platform-node-shared': 'catalog:' + '@effect/sql-pg': 'catalog:' + '@effect/sql-sqlite-bun': 'catalog:' + '@effect/vitest': 'catalog:' + '@effect/vitest>vitest': '-' + '@expo/metro-config': 56.0.14 + '@pierre/diffs>@shikijs/transformers': ^4.2.0 + '@types/node': 'catalog:' + effect: 'catalog:' + vite: 'catalog:' + yaml: 'catalog:' packageExtensions: - "@effect/vitest@*": + '@effect/vitest@*': dependencies: - vite-plus: "catalog:" + vite-plus: 'catalog:' peerDependenciesMeta: vitest: optional: true vite-plus@*: dependencies: - vite: "catalog:" + vite: 'catalog:' patchedDependencies: - "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.patch - "@effect/vitest@4.0.0-beta.78": patches/@effect__vitest@4.0.0-beta.78.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/vitest@4.0.0-beta.78': patches/@effect__vitest@4.0.0-beta.78.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 + '@react-navigation/native-stack@7.17.6': patches/@react-navigation%2Fnative-stack@7.17.6.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-gesture-handler@2.31.2: patches/react-native-gesture-handler@2.31.2.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 @@ -118,4 +114,4 @@ peerDependencyRules: allowAny: - vite allowedVersions: - vite: "*" + vite: '*' From 7e23abec59ddbf5e55741503b27770023d4b8236 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 14:23:09 -0700 Subject: [PATCH 74/81] Share thread list items between compact Home and iPad sidebar Extract the group header (collapse), thread row (status pill, PR badge, branch subtitle, timestamp), and show-more row into thread-list-items with compact/sidebar variants, and drive the sidebar from the same homeListItems display model as Home. The iPad sidebar gains group collapse, show-more pagination, PR badges, and project favicons; both lists now share the swipe actions and the long-press/right-click context menu, plus one homeListItemsAreEqual implementation. Sidebar rows keep their selected-state bubble and tighter metrics via the "sidebar" variant; Home keeps its full-bleed separator look via "compact". Co-Authored-By: Claude Fable 5 --- apps/mobile/src/features/home/HomeScreen.tsx | 347 +------------- .../mobile/src/features/home/homeListItems.ts | 32 ++ .../threads/ThreadNavigationSidebar.tsx | 412 +++++------------ .../features/threads/thread-list-items.tsx | 434 ++++++++++++++++++ 4 files changed, 590 insertions(+), 635 deletions(-) create mode 100644 apps/mobile/src/features/threads/thread-list-items.tsx diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index a08d8b837a1..61312177f0a 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -12,33 +12,32 @@ import type { SidebarProjectGroupingMode, SidebarThreadSortOrder, } from "@t3tools/contracts"; -import { SymbolView } from "expo-symbols"; -import { memo, useCallback, useMemo, useRef, useState, type ComponentProps } from "react"; -import { ActivityIndicator, Platform, Pressable, useWindowDimensions, View } from "react-native"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { ActivityIndicator, Platform, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useThemeColor } from "../../lib/useThemeColor"; -import { AppText as Text } from "../../components/AppText"; import { EmptyState } from "../../components/EmptyState"; -import { ProjectFavicon } from "../../components/ProjectFavicon"; import type { WorkspaceState } from "../../state/workspaceModel"; import type { SavedRemoteConnection } from "../../lib/connection"; import { scopedProjectKey } from "../../lib/scopedEntities"; -import { relativeTime } from "../../lib/time"; -import { useThreadPr } from "../../state/use-thread-pr"; -import { resolveThreadStatus } from "../threads/threadPresentation"; +import { + ThreadListGroupHeader, + ThreadListRow, + ThreadListShowMoreRow, +} from "../threads/thread-list-items"; import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu"; import { buildHomeListLayout, DEFAULT_GROUP_DISPLAY_STATE, + homeListItemsAreEqual, nextGroupDisplayState, type HomeGroupDisplayAction, type HomeGroupDisplayState, type HomeListItem, } from "./homeListItems"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; -import { ThreadSwipeable } from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "./workspace-connection-status"; @@ -72,8 +71,6 @@ interface HomeScreenProps { /* ─── Layout constants ───────────────────────────────────────────────── */ const ESTIMATED_THREAD_ROW_HEIGHT = 64; -/** Left inset that aligns secondary rows with the thread title column. */ -const THREAD_TEXT_COLUMN_INSET = 20; /** Height of the floating custom header on non-iOS platforms. */ const CUSTOM_HEADER_HEIGHT = 78; @@ -140,297 +137,10 @@ function deriveEmptyState(props: { }; } -/* ─── Project group header ───────────────────────────────────────────── */ - -const ProjectGroupHeader = memo(function ProjectGroupHeader(props: { - readonly project: EnvironmentProject; - readonly title: string; - readonly threadCount: number; - readonly collapsed: boolean; - readonly isFirst: boolean; - readonly groupKey: string; - readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; -}) { - const iconSubtleColor = useThemeColor("--color-icon-subtle"); - const { groupKey, onGroupAction } = props; - const handleToggle = useCallback( - () => onGroupAction(groupKey, "toggle-collapsed"), - [groupKey, onGroupAction], - ); - - return ( - - - - - {props.title} - - - {props.threadCount} - - - - - ); -}); - -/* ─── Show more / show less row ──────────────────────────────────────── */ - -const ShowMoreRow = memo(function ShowMoreRow(props: { - readonly hiddenCount: number; - readonly canShowLess: boolean; - readonly groupKey: string; - readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; -}) { - const iconSubtleColor = useThemeColor("--color-icon-subtle"); - const showsMore = props.hiddenCount > 0; - const { groupKey, onGroupAction } = props; - const handleShowMore = useCallback( - () => onGroupAction(groupKey, "show-more"), - [groupKey, onGroupAction], - ); - const handleShowLess = useCallback( - () => onGroupAction(groupKey, "show-less"), - [groupKey, onGroupAction], - ); - - return ( - - {showsMore ? ( - ({ - opacity: pressed ? 0.6 : 1, - paddingHorizontal: 14, - paddingVertical: 7, - borderCurve: "continuous", - })} - > - - - Show more - - - ) : null} - {props.canShowLess ? ( - ({ - opacity: pressed ? 0.6 : 1, - paddingHorizontal: 14, - paddingVertical: 7, - borderCurve: "continuous", - })} - > - - - Show less - - - ) : null} - - ); -}); - function HomeTopContentSpacer(props: { readonly topInset: number }) { return ; } -/* ─── Thread row ─────────────────────────────────────────────────────── */ - -const ThreadRow = memo(function ThreadRow(props: { - readonly thread: EnvironmentThreadShell; - readonly environmentLabel: string | null; - readonly projectCwd: string | null; - readonly onSelectThread: (thread: EnvironmentThreadShell) => void; - readonly onArchiveThread: (thread: EnvironmentThreadShell) => void; - readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; - readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; - readonly onSwipeableClose: (methods: SwipeableMethods) => void; - readonly simultaneousSwipeGesture?: ComponentProps< - typeof ThreadSwipeable - >["simultaneousWithExternalGesture"]; - readonly isLast: boolean; -}) { - const { width: windowWidth } = useWindowDimensions(); - const separatorColor = useThemeColor("--color-separator"); - const iconSubtleColor = useThemeColor("--color-icon-subtle"); - const screenColor = useThemeColor("--color-screen"); - const { thread, onSelectThread, onArchiveThread, onDeleteThread } = props; - const status = resolveThreadStatus(thread); - const pr = useThreadPr(thread, props.projectCwd); - const timestamp = relativeTime( - thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt, - ); - const branch = thread.branch; - const subtitleParts = [props.environmentLabel, branch].filter((part): part is string => - Boolean(part), - ); - const handleDelete = useCallback(() => onDeleteThread(thread), [onDeleteThread, thread]); - const primaryAction = useMemo( - () => ({ - accessibilityLabel: `Archive ${thread.title}`, - icon: "archivebox" as const, - label: "Archive", - onPress: () => onArchiveThread(thread), - }), - [onArchiveThread, thread], - ); - - return ( - - {(close) => ( - { - close(); - onSelectThread(thread); - }} - style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} - > - - - - - {thread.title} - - - {status ? ( - - - {status.label} - - - ) : null} - - {timestamp} - - - - - - {subtitleParts.length > 0 || pr !== null ? ( - - {subtitleParts.length > 0 ? ( - <> - - - {subtitleParts.join(" · ")} - - - ) : null} - {pr !== null ? ( - - {pr.label} - - ) : null} - - ) : null} - - - - )} - - ); -}); - /* ─── Main screen ────────────────────────────────────────────────────── */ export function HomeScreen(props: HomeScreenProps) { @@ -520,7 +230,8 @@ export function HomeScreen(props: HomeScreenProps) { switch (item.type) { case "header": return ( - item.key, []); - // Item objects are rebuilt on every collapse/show-more toggle; without this - // LegendList would consider every mounted row changed and re-render all of - // them (each carrying a swipeable + a vcs-status subscription), which made - // taps visibly laggy. Group/thread references are stable across toggles. - const itemsAreEqual = useCallback((previous: HomeListItem, item: HomeListItem) => { - if (previous.type !== item.type) return false; - switch (item.type) { - case "header": - return ( - previous.type === "header" && - previous.group === item.group && - previous.collapsed === item.collapsed && - previous.isFirst === item.isFirst - ); - case "thread": - return ( - previous.type === "thread" && - previous.thread === item.thread && - previous.isLast === item.isLast - ); - case "show-more": - return ( - previous.type === "show-more" && - previous.groupKey === item.groupKey && - previous.hiddenCount === item.hiddenCount && - previous.canShowLess === item.canShowLess - ); - } - }, []); - /* Empty states */ const hasAnyThreads = props.threads.some((thread) => thread.archivedAt === null); const hasResults = projectGroups.length > 0; @@ -699,7 +382,7 @@ export function HomeScreen(props: HomeScreenProps) { data={listLayout.items} renderItem={renderItem} keyExtractor={keyExtractor} - itemsAreEqual={itemsAreEqual} + itemsAreEqual={homeListItemsAreEqual} estimatedItemSize={ESTIMATED_THREAD_ROW_HEIGHT} extraData={extraData} ListHeaderComponent={listHeader} diff --git a/apps/mobile/src/features/home/homeListItems.ts b/apps/mobile/src/features/home/homeListItems.ts index 3a66c1b2787..4a938514227 100644 --- a/apps/mobile/src/features/home/homeListItems.ts +++ b/apps/mobile/src/features/home/homeListItems.ts @@ -66,6 +66,38 @@ export function nextGroupDisplayState( } } +/** + * Structural equality for list items. Item objects are rebuilt on every + * collapse/show-more toggle; without this the lists would consider every + * mounted row changed and re-render all of them (each carrying a swipeable + + * a vcs-status subscription). Group/thread references are stable across + * toggles. + */ +export function homeListItemsAreEqual(previous: HomeListItem, item: HomeListItem): boolean { + switch (item.type) { + case "header": + return ( + previous.type === "header" && + previous.group === item.group && + previous.collapsed === item.collapsed && + previous.isFirst === item.isFirst + ); + case "thread": + return ( + previous.type === "thread" && + previous.thread === item.thread && + previous.isLast === item.isLast + ); + case "show-more": + return ( + previous.type === "show-more" && + previous.groupKey === item.groupKey && + previous.hiddenCount === item.hiddenCount && + previous.canShowLess === item.canShowLess + ); + } +} + export function buildHomeListLayout(input: { readonly groups: ReadonlyArray; readonly displayStates: ReadonlyMap; diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index d02d853f343..08fead61386 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -3,22 +3,13 @@ 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 { - memo, - useCallback, - useMemo, - useRef, - useState, - type ComponentProps, - type ReactNode, -} from "react"; +import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; import type { - ColorValue, LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent, } from "react-native"; -import { Platform, Pressable, StyleSheet, TextInput, View, useColorScheme } from "react-native"; +import { Platform, 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"; @@ -27,10 +18,8 @@ import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { AppText as Text } from "../../components/AppText"; import { ControlPillMenu } from "../../components/ControlPill"; -import { StatusPill } from "../../components/StatusPill"; import { NativeStackScreenOptions } from "../../native/StackHeader"; -import { scopedThreadKey } from "../../lib/scopedEntities"; -import { relativeTime } from "../../lib/time"; +import { scopedProjectKey, scopedThreadKey } from "../../lib/scopedEntities"; import { useThemeColor } from "../../lib/useThemeColor"; import { useProjects, useThreadShells } from "../../state/entities"; import { useWorkspaceState } from "../../state/workspace"; @@ -44,8 +33,16 @@ import { useHomeListOptions, } from "../home/home-list-options"; import { buildHomeListFilterMenu } from "../home/home-list-filter-menu"; +import { + buildHomeListLayout, + DEFAULT_GROUP_DISPLAY_STATE, + homeListItemsAreEqual, + nextGroupDisplayState, + type HomeGroupDisplayAction, + type HomeGroupDisplayState, + type HomeListItem, +} from "../home/homeListItems"; import { buildHomeThreadGroups } from "../home/homeThreadList"; -import { ThreadSwipeable } from "../home/thread-swipe-actions"; import { useThreadListActions } from "../home/useThreadListActions"; import { WorkspaceConnectionStatus } from "../home/WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "../home/workspace-connection-status"; @@ -53,7 +50,11 @@ import { SidebarHeaderActions } from "./sidebar-header-actions"; import { SidebarFilterButton } from "./sidebar-filter-button"; import { createSidebarHeaderItems } from "./sidebar-native-header-items"; import { SidebarNavigationShell } from "./sidebar-navigation-shell"; -import { resolveThreadStatus } from "./threadPresentation"; +import { + ThreadListGroupHeader, + ThreadListRow, + ThreadListShowMoreRow, +} from "./thread-list-items"; /** * Shared capsule behind the sidebar header buttons — a native liquid-glass @@ -100,182 +101,6 @@ const SIDEBAR_HEADER_WASH_OPACITY = { light: [0.46, 0.3, 0.08], } as const; -const ThreadNavigationRow = memo(function ThreadNavigationRow(props: { - readonly backgroundColor: ColorValue; - readonly foregroundColor: ColorValue; - readonly fullSwipeWidth: number; - readonly mutedColor: ColorValue; - 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 selectedForegroundColor: ColorValue; - readonly selectedMutedColor: ColorValue; - readonly selectedPressedBackgroundColor: ColorValue; - readonly simultaneousSwipeGesture?: ComponentProps< - typeof ThreadSwipeable - >["simultaneousWithExternalGesture"]; - readonly thread: EnvironmentThreadShell; - readonly environmentLabel: string | null; -}) { - const [hovered, setHovered] = useState(false); - const { - backgroundColor, - foregroundColor, - fullSwipeWidth, - mutedColor, - onArchiveThread, - onDeleteThread, - onSelectThread, - onSwipeableClose, - onSwipeableWillOpen, - 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]); - 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], - ); - 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), - ); - const statusTone = resolveThreadStatus(thread); - const effectiveStatusTone = - selected && statusTone - ? { - ...statusTone, - pillClassName: "bg-white/20", - textClassName: "text-white", - } - : statusTone; - - return ( - - {() => ( - // Messages-style row actions: a native context menu on long-press / - // pointer right-click (MenuViewImplementation attaches a real - // UIContextMenuInteraction). No visible ⋯ button; touch users also - // have the swipe actions. - - - setHovered(true)} - onHoverOut={() => setHovered(false)} - onPress={() => onSelectThread(thread)} - style={({ pressed }) => [ - styles.threadSelectionTarget, - { - backgroundColor: - pressed || hovered ? effectivePressedBackgroundColor : "transparent", - cursor: "pointer", - }, - ]} - > - - - {thread.title} - - - {subtitle.length > 0 ? ( - - {subtitle.join(" · ")} - - ) : null} - - {relativeTime(thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt)} - - - - {effectiveStatusTone ? : null} - - - - )} - - ); -}); - -type SidebarListItem = - | { readonly kind: "section"; readonly key: string; readonly title: string } - | { - readonly kind: "thread"; - readonly key: string; - readonly thread: EnvironmentThreadShell; - }; - interface ThreadNavigationSidebarProps { readonly width: number; readonly visible: boolean; @@ -378,18 +203,36 @@ function ThreadNavigationSidebarPane( }), [options, projects, props.searchQuery, threads], ); - const listItems = useMemo>( + const [groupDisplayStates, setGroupDisplayStates] = useState< + ReadonlyMap + >(() => new Map()); + const updateGroupDisplay = useCallback((key: string, action: HomeGroupDisplayAction) => { + setGroupDisplayStates((previous) => { + const next = new Map(previous); + next.set( + key, + nextGroupDisplayState(previous.get(key) ?? DEFAULT_GROUP_DISPLAY_STATE, action), + ); + return next; + }); + }, []); + const hasSearchQuery = props.searchQuery.trim().length > 0; + const listLayout = 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], + buildHomeListLayout({ + groups, + displayStates: groupDisplayStates, + showAllThreads: hasSearchQuery, + }), + [groups, groupDisplayStates, hasSearchQuery], ); + const projectCwdByKey = useMemo(() => { + const map = new Map(); + for (const project of projects) { + map.set(scopedProjectKey(project.environmentId, project.id), project.workspaceRoot); + } + return map; + }, [projects]); const showsConnectionStatus = shouldShowWorkspaceConnectionStatus(catalogState); const listMenuActions = useMemo( () => [ @@ -493,13 +336,7 @@ function ThreadNavigationSidebarPane( const placeholderColor = useThemeColor("--color-placeholder"); const searchBackgroundColor = colorScheme === "dark" ? IOS_SEARCH_FILL_DARK : IOS_SEARCH_FILL_LIGHT; - 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 ?? ""}:${props.searchQuery}`; + const listExtraData = `${props.selectedThreadKey ?? ""}:${props.searchQuery}`; const headerFadeColor = String(backgroundColor); const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(null); @@ -556,63 +393,72 @@ function ThreadNavigationSidebarPane( }, [props.nativeChrome, props.onRequestVisibility, props.visible]); useHardwareKeyboardCommand("focusSearch", focusSearch); const renderListItem = useCallback( - ({ item }: { readonly item: SidebarListItem }) => { - if (item.kind === "section") { - return ( - - {item.title} - - ); + ({ item }: { readonly item: HomeListItem }) => { + switch (item.type) { + case "header": + return ( + + ); + case "thread": { + const thread = item.thread; + return ( + + ); + } + case "show-more": + return ( + + ); } - const thread = item.thread; - return ( - - - - ); }, [ archiveThread, - backgroundColor, - foregroundColor, confirmDeleteThread, handleSelectThread, handleSwipeableClose, handleSwipeableWillOpen, - pressedBackgroundColor, + projectCwdByKey, props.selectedThreadKey, props.width, savedConnectionsById, - selectedBackgroundColor, - selectedForegroundColor, - selectedMutedColor, - selectedPressedBackgroundColor, - listThemeKey, - mutedColor, + sidebarScrollGesture, + updateGroupDisplay, ], ); const filterIcon = hasCustomHomeListOptions(options) @@ -687,10 +533,11 @@ function ThreadNavigationSidebarPane( item.kind} + getItemType={(item) => item.type} + itemsAreEqual={homeListItemsAreEqual} keyExtractor={(item) => item.key} renderItem={renderListItem} automaticallyAdjustsScrollIndicatorInsets @@ -743,10 +590,11 @@ function ThreadNavigationSidebarPane( item.kind} + getItemType={(item) => item.type} + itemsAreEqual={homeListItemsAreEqual} keyExtractor={(item) => item.key} renderItem={renderListItem} contentContainerStyle={[ @@ -936,46 +784,4 @@ const styles = StyleSheet.create({ threadListContent: { paddingHorizontal: 8, }, - sectionTitle: { - paddingHorizontal: 20, - paddingBottom: 4, - paddingTop: 16, - }, - threadItem: { - paddingBottom: 0, - }, - threadRow: { - minHeight: 64, - borderRadius: 12, - flexDirection: "row", - alignItems: "center", - paddingRight: 6, - }, - threadSelectionTarget: { - minWidth: 0, - flex: 1, - alignSelf: "stretch", - borderRadius: 12, - paddingLeft: 14, - paddingRight: 6, - paddingVertical: 10, - flexDirection: "row", - alignItems: "center", - gap: 8, - }, - threadRowContainer: { - borderRadius: 12, - overflow: "hidden", - }, - threadText: { - minWidth: 0, - flex: 1, - gap: 2, - }, - threadMetadata: { - minWidth: 0, - flexDirection: "row", - alignItems: "center", - gap: 6, - }, }); diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx new file mode 100644 index 00000000000..b62a9a7569e --- /dev/null +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -0,0 +1,434 @@ +import type { + EnvironmentProject, + EnvironmentThreadShell, +} from "@t3tools/client-runtime/state/shell"; +import type { MenuAction } from "@react-native-menu/menu"; +import { SymbolView } from "expo-symbols"; +import { memo, useCallback, useMemo, useState, type ComponentProps } from "react"; +import { Pressable, useWindowDimensions, View } from "react-native"; +import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; + +import { AppText as Text } from "../../components/AppText"; +import { ControlPillMenu } from "../../components/ControlPill"; +import { ProjectFavicon } from "../../components/ProjectFavicon"; +import { relativeTime } from "../../lib/time"; +import { useThemeColor } from "../../lib/useThemeColor"; +import { useThreadPr } from "../../state/use-thread-pr"; +import type { HomeGroupDisplayAction } from "../home/homeListItems"; +import { ThreadSwipeable } from "../home/thread-swipe-actions"; +import { resolveThreadStatus } from "./threadPresentation"; + +/** + * Shared presentation for the thread lists: the compact (phone) Home list and + * the iPad sidebar render the SAME items — group headers with collapse, + * thread rows with status/PR/subtitle, and show-more rows — differing only in + * metrics and chrome via `variant`. + */ +export type ThreadListVariant = "compact" | "sidebar"; + +/** Left inset that aligns compact secondary rows with the title column. */ +export const THREAD_LIST_COMPACT_INSET = 20; +const SIDEBAR_ROW_RADIUS = 12; + +/* ─── Project group header ───────────────────────────────────────────── */ + +export const ThreadListGroupHeader = memo(function ThreadListGroupHeader(props: { + readonly variant: ThreadListVariant; + readonly project: EnvironmentProject; + readonly title: string; + readonly threadCount: number; + readonly collapsed: boolean; + readonly isFirst: boolean; + readonly groupKey: string; + readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; +}) { + const iconSubtleColor = useThemeColor("--color-icon-subtle"); + const { groupKey, onGroupAction } = props; + const compact = props.variant === "compact"; + const handleToggle = useCallback( + () => onGroupAction(groupKey, "toggle-collapsed"), + [groupKey, onGroupAction], + ); + + return ( + + + + + {props.title} + + + {props.threadCount} + + + + + ); +}); + +/* ─── Show more / show less row ──────────────────────────────────────── */ + +export const ThreadListShowMoreRow = memo(function ThreadListShowMoreRow(props: { + readonly variant: ThreadListVariant; + readonly hiddenCount: number; + readonly canShowLess: boolean; + readonly groupKey: string; + readonly onGroupAction: (key: string, action: HomeGroupDisplayAction) => void; +}) { + const iconSubtleColor = useThemeColor("--color-icon-subtle"); + const showsMore = props.hiddenCount > 0; + const compact = props.variant === "compact"; + const { groupKey, onGroupAction } = props; + const handleShowMore = useCallback( + () => onGroupAction(groupKey, "show-more"), + [groupKey, onGroupAction], + ); + const handleShowLess = useCallback( + () => onGroupAction(groupKey, "show-less"), + [groupKey, onGroupAction], + ); + + const button = (label: string, icon: "chevron.down" | "chevron.up", onPress: () => void) => ( + ({ + opacity: pressed ? 0.6 : 1, + paddingHorizontal: compact ? 14 : 12, + paddingVertical: compact ? 7 : 6, + borderCurve: "continuous", + })} + > + + + + {label} + + + + ); + + return ( + + {showsMore ? button("Show more", "chevron.down", handleShowMore) : null} + {props.canShowLess ? button("Show less", "chevron.up", handleShowLess) : null} + + ); +}); + +/* ─── Thread row ─────────────────────────────────────────────────────── */ + +const THREAD_ROW_MENU_ACTIONS: MenuAction[] = [ + { id: "archive", title: "Archive", image: "archivebox" }, + { id: "delete", title: "Delete", image: "trash", attributes: { destructive: true } }, +]; + +export const ThreadListRow = memo(function ThreadListRow(props: { + readonly variant: ThreadListVariant; + readonly thread: EnvironmentThreadShell; + readonly environmentLabel: string | null; + readonly projectCwd: string | null; + readonly isLast: boolean; + /** Sidebar only: the thread currently open in the detail pane. */ + readonly selected?: boolean; + /** Defaults to window width minus compact margins. */ + readonly fullSwipeWidth?: number; + readonly onSelectThread: (thread: EnvironmentThreadShell) => void; + readonly onArchiveThread: (thread: EnvironmentThreadShell) => void; + readonly onDeleteThread: (thread: EnvironmentThreadShell) => void; + readonly onSwipeableWillOpen: (methods: SwipeableMethods) => void; + readonly onSwipeableClose: (methods: SwipeableMethods) => void; + readonly simultaneousSwipeGesture?: ComponentProps< + typeof ThreadSwipeable + >["simultaneousWithExternalGesture"]; +}) { + const { width: windowWidth } = useWindowDimensions(); + const compact = props.variant === "compact"; + const selected = props.selected === true; + const [hovered, setHovered] = useState(false); + + const separatorColor = useThemeColor("--color-separator"); + const iconSubtleColor = useThemeColor("--color-icon-subtle"); + const screenColor = useThemeColor("--color-screen"); + const drawerColor = useThemeColor("--color-drawer"); + const foregroundColor = useThemeColor("--color-foreground"); + const mutedColor = useThemeColor("--color-foreground-muted"); + const pressedBackgroundColor = useThemeColor("--color-subtle"); + const selectedBackgroundColor = useThemeColor("--color-user-bubble"); + const selectedForegroundColor = useThemeColor("--color-user-bubble-foreground"); + const selectedMutedColor = useThemeColor("--color-user-bubble-foreground-muted"); + + const { thread, onSelectThread, onArchiveThread, onDeleteThread } = props; + const status = resolveThreadStatus(thread); + const pr = useThreadPr(thread, props.projectCwd); + const timestamp = relativeTime( + thread.latestUserMessageAt ?? thread.updatedAt ?? thread.createdAt, + ); + const subtitleParts = [props.environmentLabel, thread.branch].filter((part): part is string => + Boolean(part), + ); + + const backgroundColor = compact ? screenColor : drawerColor; + const effectiveForeground = selected ? selectedForegroundColor : foregroundColor; + const effectiveMuted = selected ? selectedMutedColor : mutedColor; + const effectivePressedBackground = selected ? "rgba(255,255,255,0.16)" : pressedBackgroundColor; + const effectiveStatus = + selected && status + ? { ...status, pillClassName: "bg-white/20", textClassName: "text-white" } + : status; + + const handleDelete = useCallback(() => onDeleteThread(thread), [onDeleteThread, thread]); + const handleArchive = useCallback(() => onArchiveThread(thread), [onArchiveThread, thread]); + const primaryAction = useMemo( + () => ({ + accessibilityLabel: `Archive ${thread.title}`, + icon: "archivebox" as const, + label: "Archive", + onPress: handleArchive, + }), + [handleArchive, thread.title], + ); + const handleMenuAction = useCallback( + ({ nativeEvent }: { readonly nativeEvent: { readonly event: string } }) => { + if (nativeEvent.event === "archive") handleArchive(); + if (nativeEvent.event === "delete") handleDelete(); + }, + [handleArchive, handleDelete], + ); + + const statusPill = effectiveStatus ? ( + + + {effectiveStatus.label} + + + ) : null; + + const subtitleRow = + subtitleParts.length > 0 || pr !== null ? ( + + {subtitleParts.length > 0 ? ( + <> + + + {subtitleParts.join(" · ")} + + + ) : null} + {pr !== null ? ( + + {pr.label} + + ) : null} + + ) : null; + + const rowContent = (close: () => void) => + compact ? ( + { + close(); + onSelectThread(thread); + }} + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} + > + + + + + {thread.title} + + + {statusPill} + + {timestamp} + + + + + {subtitleRow} + + + + ) : ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => { + close(); + onSelectThread(thread); + }} + style={({ pressed }) => ({ + backgroundColor: selected + ? selectedBackgroundColor + : pressed || hovered + ? effectivePressedBackground + : backgroundColor, + borderRadius: SIDEBAR_ROW_RADIUS, + cursor: "pointer", + minHeight: 64, + justifyContent: "center", + paddingHorizontal: 12, + paddingVertical: 10, + })} + > + + + + {thread.title} + + + {statusPill} + + {timestamp} + + + + {subtitleRow} + + + ); + + return ( + + {(close) => ( + // Messages-style row actions: native context menu on long-press / + // pointer right-click, shared across compact and sidebar variants. + + {rowContent(close)} + + )} + + ); +}); From 480b90632e6ef4f4f2d1330d762239883b6a70b7 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 14:36:56 -0700 Subject: [PATCH 75/81] Fix row taps with true context menus and gate swipes while scrolling Patch @react-native-menu so shouldOpenOnLongPress mode stops the UIButton contentView from swallowing touches: the button sits full-bounds IN FRONT of the React children, which made row taps dead when MenuView wraps a row. In long-press mode the button now passes touches through and the UIContextMenuInteraction is hosted by the component view instead, so taps reach the row Pressable while long-press / pointer right-click present the menu with the row as the zoom preview (requires a native rebuild). Rows swipes are additionally gated on list scroll activity (useSwipeableScrollGate, mirroring UIKit's !isDragging && !isDecelerating): failOffsetY covers the first pan, but trackpad scroll sessions spawn fresh gesture sessions whose reset translation could re-activate a swipe mid-scroll. Lists also opt out of item recycling explicitly (rows carry hover state and vcs subscriptions). Co-Authored-By: Claude Fable 5 --- apps/mobile/src/features/home/HomeScreen.tsx | 17 +++- .../features/home/thread-swipe-actions.tsx | 95 ++++++++++++++++++- .../threads/ThreadNavigationSidebar.tsx | 19 +++- .../features/threads/thread-list-items.tsx | 11 ++- patches/@react-native-menu__menu@2.0.0.patch | 46 +++++++++ pnpm-lock.yaml | 7 +- pnpm-workspace.yaml | 1 + 7 files changed, 183 insertions(+), 13 deletions(-) create mode 100644 patches/@react-native-menu__menu@2.0.0.patch diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 61312177f0a..ed6da9fa96a 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -38,6 +38,7 @@ import { type HomeListItem, } from "./homeListItems"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; +import { useSwipeableScrollGate } from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "./workspace-connection-status"; @@ -179,6 +180,13 @@ export function HomeScreen(props: HomeScreenProps) { } }, []); + const handleScrollBeginDrag = useCallback(() => { + openSwipeableRef.current?.close(); + }, []); + const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ + onScrollBeginDrag: handleScrollBeginDrag, + }); + const projectGroups = useMemo( () => buildHomeThreadGroups({ @@ -221,8 +229,8 @@ export function HomeScreen(props: HomeScreenProps) { }, [props.projects]); const extraData = useMemo( - () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }), - [props.savedConnectionsById, projectCwdByKey], + () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey, swipeEnabled }), + [props.savedConnectionsById, projectCwdByKey, swipeEnabled], ); const renderItem = useCallback( @@ -260,6 +268,7 @@ export function HomeScreen(props: HomeScreenProps) { onSelectThread={props.onSelectThread} onSwipeableClose={handleSwipeableClose} onSwipeableWillOpen={handleSwipeableWillOpen} + swipeEnabled={swipeEnabled} /> ); } @@ -283,6 +292,7 @@ export function HomeScreen(props: HomeScreenProps) { props.onDeleteThread, props.onSelectThread, props.savedConnectionsById, + swipeEnabled, updateGroupDisplay, ], ); @@ -393,7 +403,8 @@ export function HomeScreen(props: HomeScreenProps) { showsVerticalScrollIndicator={false} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" - onScrollBeginDrag={() => openSwipeableRef.current?.close()} + {...scrollGateHandlers} + recycleItems={false} scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: Platform.OS === "ios" ? Math.max(insets.bottom, 24) + 24 : 24, diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index 152fa6e8ac1..a6d389c9811 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -1,7 +1,7 @@ import { SymbolView } from "expo-symbols"; import * as Haptics from "expo-haptics"; -import { useCallback, useRef, type ComponentProps, type ReactNode } from "react"; -import type { ColorValue, StyleProp, ViewStyle } from "react-native"; +import { useCallback, useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react"; +import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"; import { Pressable, View } from "react-native"; import ReanimatedSwipeable, { type SwipeableMethods, @@ -36,10 +36,100 @@ interface ThreadSwipePrimaryAction { readonly onPress: () => void; } +/** + * Gates row swipes on list scroll activity, mirroring UIKit's own swipe + * actions (`!isDragging && !isDecelerating`). failOffsetY on the swipe pan + * covers the first pan of a scroll, but trackpad scroll sessions spawn fresh + * gesture sessions (momentum catch, direction changes) whose reset + * translation can re-activate a swipe mid-scroll — so while the list has + * moved vertically during an active drag/momentum phase, row swipes are + * disabled entirely. + * + * Spread the returned handlers onto the list and pass `swipeEnabled` to rows. + */ +export function useSwipeableScrollGate(options?: { + readonly onScroll?: (event: NativeSyntheticEvent) => void; + readonly onScrollBeginDrag?: (event: NativeSyntheticEvent) => void; +}) { + const [gateActive, setGateActive] = useState(false); + const gateActiveRef = useRef(false); + const draggingRef = useRef(false); + const dragStartYRef = useRef(0); + const settleTimerRef = useRef | null>(null); + const externalOnScroll = options?.onScroll; + const externalOnScrollBeginDrag = options?.onScrollBeginDrag; + + const update = useCallback((next: boolean) => { + if (gateActiveRef.current !== next) { + gateActiveRef.current = next; + setGateActive(next); + } + }, []); + const clearSettle = useCallback(() => { + if (settleTimerRef.current !== null) { + clearTimeout(settleTimerRef.current); + settleTimerRef.current = null; + } + }, []); + useEffect(() => clearSettle, [clearSettle]); + + const onScrollBeginDrag = useCallback( + (event: NativeSyntheticEvent) => { + draggingRef.current = true; + dragStartYRef.current = event.nativeEvent.contentOffset.y; + clearSettle(); + externalOnScrollBeginDrag?.(event); + }, + [clearSettle, externalOnScrollBeginDrag], + ); + const onScroll = useCallback( + (event: NativeSyntheticEvent) => { + // Only vertical movement during a user drag arms the gate — a purely + // horizontal row swipe never moves contentOffset.y, and inset-driven + // offset changes at mount happen outside a drag. + if ( + draggingRef.current && + !gateActiveRef.current && + Math.abs(event.nativeEvent.contentOffset.y - dragStartYRef.current) > 4 + ) { + update(true); + } + externalOnScroll?.(event); + }, + [externalOnScroll, update], + ); + const onScrollEndDrag = useCallback(() => { + draggingRef.current = false; + clearSettle(); + // If momentum follows, onMomentumScrollBegin cancels this and the gate + // stays armed until the deceleration finishes. + settleTimerRef.current = setTimeout(() => update(false), 160); + }, [clearSettle, update]); + const onMomentumScrollBegin = useCallback(() => { + clearSettle(); + }, [clearSettle]); + const onMomentumScrollEnd = useCallback(() => { + update(false); + }, [update]); + + return { + swipeEnabled: !gateActive, + scrollGateHandlers: { + onScroll, + onScrollBeginDrag, + onScrollEndDrag, + onMomentumScrollBegin, + onMomentumScrollEnd, + }, + }; +} + export function ThreadSwipeable(props: { readonly backgroundColor: ColorValue; readonly children: (close: () => void) => ReactNode; readonly containerStyle?: StyleProp; + /** Disables NEW swipe activations (e.g. while the list scrolls). */ + readonly enabled?: boolean; readonly enableTrackpadSwipe?: boolean; readonly fullSwipeWidth: number; readonly onDelete: () => void; @@ -69,6 +159,7 @@ export function ThreadSwipeable(props: { childrenContainerStyle={{ backgroundColor: props.backgroundColor }} containerStyle={[{ backgroundColor: props.backgroundColor }, props.containerStyle]} dragOffsetFromRightEdge={8} + enabled={props.enabled} enableTrackpadTwoFingerGesture={props.enableTrackpadSwipe ?? true} // Fail the swipe once the pan is vertically dominant (patched-in RNGH // prop) — otherwise trackpad scrolls with ~8px of horizontal drift diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 08fead61386..98cf791048d 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -43,6 +43,7 @@ import { type HomeListItem, } from "../home/homeListItems"; import { buildHomeThreadGroups } from "../home/homeThreadList"; +import { useSwipeableScrollGate } from "../home/thread-swipe-actions"; import { useThreadListActions } from "../home/useThreadListActions"; import { WorkspaceConnectionStatus } from "../home/WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "../home/workspace-connection-status"; @@ -336,7 +337,6 @@ function ThreadNavigationSidebarPane( const placeholderColor = useThemeColor("--color-placeholder"); const searchBackgroundColor = colorScheme === "dark" ? IOS_SEARCH_FILL_DARK : IOS_SEARCH_FILL_LIGHT; - const listExtraData = `${props.selectedThreadKey ?? ""}:${props.searchQuery}`; const headerFadeColor = String(backgroundColor); const headerWashOpacity = SIDEBAR_HEADER_WASH_OPACITY[colorScheme]; const [measuredHeaderHeight, setMeasuredHeaderHeight] = useState(null); @@ -375,6 +375,14 @@ function ThreadNavigationSidebarPane( headerIsOverContentRef.current = next; setHeaderIsOverContent(next); }, []); + const handleScrollBeginDrag = useCallback(() => { + openSwipeableRef.current?.close(); + }, []); + const { swipeEnabled, scrollGateHandlers } = useSwipeableScrollGate({ + onScroll: handleScroll, + onScrollBeginDrag: handleScrollBeginDrag, + }); + const listExtraData = `${props.selectedThreadKey ?? ""}:${props.searchQuery}:${swipeEnabled}`; const focusSearch = useCallback(() => { const focus = () => { if (props.nativeChrome) { @@ -432,6 +440,7 @@ function ThreadNavigationSidebarPane( onSwipeableClose={handleSwipeableClose} onSwipeableWillOpen={handleSwipeableWillOpen} simultaneousSwipeGesture={sidebarScrollGesture} + swipeEnabled={swipeEnabled} /> ); } @@ -458,6 +467,7 @@ function ThreadNavigationSidebarPane( props.width, savedConnectionsById, sidebarScrollGesture, + swipeEnabled, updateGroupDisplay, ], ); @@ -551,7 +561,8 @@ function ThreadNavigationSidebarPane( ]} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" - onScrollBeginDrag={() => openSwipeableRef.current?.close()} + {...scrollGateHandlers} + recycleItems={false} scrollEventThrottle={16} showsVerticalScrollIndicator={false} style={styles.threadList} @@ -606,8 +617,8 @@ function ThreadNavigationSidebarPane( ]} keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" - onScroll={handleScroll} - onScrollBeginDrag={() => openSwipeableRef.current?.close()} + {...scrollGateHandlers} + recycleItems={false} scrollEventThrottle={16} showsVerticalScrollIndicator={false} style={styles.threadList} diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index b62a9a7569e..24bf31a33b4 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -192,6 +192,8 @@ export const ThreadListRow = memo(function ThreadListRow(props: { readonly isLast: boolean; /** Sidebar only: the thread currently open in the detail pane. */ readonly selected?: boolean; + /** Gate from useSwipeableScrollGate — false while the list is scrolling. */ + readonly swipeEnabled?: boolean; /** Defaults to window width minus compact margins. */ readonly fullSwipeWidth?: number; readonly onSelectThread: (thread: EnvironmentThreadShell) => void; @@ -409,6 +411,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { containerStyle={ compact ? undefined : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" } } + enabled={props.swipeEnabled} enableTrackpadSwipe fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32} onDelete={handleDelete} @@ -419,8 +422,12 @@ export const ThreadListRow = memo(function ThreadListRow(props: { threadTitle={thread.title} > {(close) => ( - // Messages-style row actions: native context menu on long-press / - // pointer right-click, shared across compact and sidebar variants. + // Messages-style row actions: a real UIContextMenuInteraction on + // long-press / pointer right-click, with the row as the zoom preview. + // Requires the patched @react-native-menu (see + // patches/@react-native-menu__menu@2.0.0.patch): in long-press mode + // the interaction is hosted by the component view and the underlying + // UIButton passes touches through, so row taps keep working. Date: Thu, 2 Jul 2026 17:51:11 -0700 Subject: [PATCH 76/81] Enable list recycling with recycling-safe thread rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit recycleItems={false} was only silencing the LegendList hint — this turns recycling ON so scrolling reuses mounted rows (each carries a reanimated swipeable, gesture handlers, a MenuView, and a vcs-status subscription) instead of unmounting/remounting them, which was the actual jank source. Recycling safety: row hover state moves to useRecyclingState (auto-resets on container reuse) and ThreadSwipeable gains a resetKey that snaps a reused swipeable back to closed so open/mid-drag state can't leak onto another thread's row. Compact row estimate corrected to 72pt and drawDistance raised to 500 so fast scrolls hit pre-rendered rows. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/features/home/HomeScreen.tsx | 5 +++-- .../src/features/home/thread-swipe-actions.tsx | 14 ++++++++++++++ .../features/threads/ThreadNavigationSidebar.tsx | 6 ++++-- .../src/features/threads/thread-list-items.tsx | 8 ++++++-- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index ed6da9fa96a..fc4e3090dba 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -71,7 +71,7 @@ interface HomeScreenProps { /* ─── Layout constants ───────────────────────────────────────────────── */ -const ESTIMATED_THREAD_ROW_HEIGHT = 64; +const ESTIMATED_THREAD_ROW_HEIGHT = 72; /** Height of the floating custom header on non-iOS platforms. */ const CUSTOM_HEADER_HEIGHT = 78; @@ -393,6 +393,7 @@ export function HomeScreen(props: HomeScreenProps) { renderItem={renderItem} keyExtractor={keyExtractor} itemsAreEqual={homeListItemsAreEqual} + drawDistance={500} estimatedItemSize={ESTIMATED_THREAD_ROW_HEIGHT} extraData={extraData} ListHeaderComponent={listHeader} @@ -404,7 +405,7 @@ export function HomeScreen(props: HomeScreenProps) { keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" {...scrollGateHandlers} - recycleItems={false} + recycleItems scrollEventThrottle={16} contentContainerStyle={{ paddingBottom: Platform.OS === "ios" ? Math.max(insets.bottom, 24) + 24 : 24, diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index a6d389c9811..eedc769723a 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -136,6 +136,12 @@ export function ThreadSwipeable(props: { readonly onSwipeableClose?: (methods: SwipeableMethods) => void; readonly onSwipeableWillOpen?: (methods: SwipeableMethods) => void; readonly primaryAction: ThreadSwipePrimaryAction; + /** + * Identity of the content being wrapped. When a recycled list reuses this + * component for a different item, the swipeable snaps back to closed so an + * open/mid-drag state can't leak onto another row. + */ + readonly resetKey?: string; readonly simultaneousWithExternalGesture?: ComponentProps< typeof ReanimatedSwipeable >["simultaneousWithExternalGesture"]; @@ -145,6 +151,14 @@ export function ThreadSwipeable(props: { const fullSwipeArmedRef = useRef(false); const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, props.fullSwipeWidth * 0.58); const close = useCallback(() => swipeableRef.current?.close(), []); + const resetKey = props.resetKey; + useEffect(() => { + if (resetKey === undefined) { + return; + } + fullSwipeArmedRef.current = false; + swipeableRef.current?.reset(); + }, [resetKey]); const handleFullSwipeArmedChange = useCallback((armed: boolean) => { if (armed && !fullSwipeArmedRef.current && process.env.EXPO_OS === "ios") { void Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 98cf791048d..bdcd7097752 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -544,6 +544,7 @@ function ThreadNavigationSidebarPane( item.type} @@ -562,7 +563,7 @@ function ThreadNavigationSidebarPane( keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" {...scrollGateHandlers} - recycleItems={false} + recycleItems scrollEventThrottle={16} showsVerticalScrollIndicator={false} style={styles.threadList} @@ -602,6 +603,7 @@ function ThreadNavigationSidebarPane( item.type} @@ -618,7 +620,7 @@ function ThreadNavigationSidebarPane( keyboardDismissMode="on-drag" keyboardShouldPersistTaps="handled" {...scrollGateHandlers} - recycleItems={false} + recycleItems scrollEventThrottle={16} showsVerticalScrollIndicator={false} style={styles.threadList} diff --git a/apps/mobile/src/features/threads/thread-list-items.tsx b/apps/mobile/src/features/threads/thread-list-items.tsx index 24bf31a33b4..58ac7b897c0 100644 --- a/apps/mobile/src/features/threads/thread-list-items.tsx +++ b/apps/mobile/src/features/threads/thread-list-items.tsx @@ -1,10 +1,11 @@ +import { useRecyclingState } from "@legendapp/list/react-native"; import type { EnvironmentProject, EnvironmentThreadShell, } from "@t3tools/client-runtime/state/shell"; import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "expo-symbols"; -import { memo, useCallback, useMemo, useState, type ComponentProps } from "react"; +import { memo, useCallback, useMemo, type ComponentProps } from "react"; import { Pressable, useWindowDimensions, View } from "react-native"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; @@ -208,7 +209,9 @@ export const ThreadListRow = memo(function ThreadListRow(props: { const { width: windowWidth } = useWindowDimensions(); const compact = props.variant === "compact"; const selected = props.selected === true; - const [hovered, setHovered] = useState(false); + // Recycling-safe: resets when the list container is reused for another + // thread, so a hover highlight can't leak across rows. + const [hovered, setHovered] = useRecyclingState(false); const separatorColor = useThemeColor("--color-separator"); const iconSubtleColor = useThemeColor("--color-icon-subtle"); @@ -418,6 +421,7 @@ export const ThreadListRow = memo(function ThreadListRow(props: { onSwipeableClose={props.onSwipeableClose} onSwipeableWillOpen={props.onSwipeableWillOpen} primaryAction={primaryAction} + resetKey={`${thread.environmentId}:${thread.id}`} simultaneousWithExternalGesture={props.simultaneousSwipeGesture} threadTitle={thread.title} > From ec9318bc8c6e1164895ffad197b484d40d1daae1 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 17:58:34 -0700 Subject: [PATCH 77/81] Deliver the swipe scroll gate via context instead of extraData MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Flipping the gate through list extraData + renderItem deps re-rendered every visible row (hooks, vcs subscriptions and all) exactly at scroll start — the moment frame budget matters most. As a context value consumed inside ThreadSwipeable, only the swipeable re-renders and the row's expensive work is skipped. Also drops searchQuery from the sidebar extraData: rows never read it, so typing was re-rendering the whole visible list. Co-Authored-By: Claude Fable 5 --- apps/mobile/src/features/home/HomeScreen.tsx | 10 +++--- .../features/home/thread-swipe-actions.tsx | 34 +++++++++++++++++-- .../threads/ThreadNavigationSidebar.tsx | 12 ++++--- .../features/threads/thread-list-items.tsx | 3 -- 4 files changed, 44 insertions(+), 15 deletions(-) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index fc4e3090dba..1558980c88c 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -38,7 +38,7 @@ import { type HomeListItem, } from "./homeListItems"; import { buildHomeThreadGroups, type HomeProjectSortOrder } from "./homeThreadList"; -import { useSwipeableScrollGate } from "./thread-swipe-actions"; +import { SwipeableScrollGateProvider, useSwipeableScrollGate } from "./thread-swipe-actions"; import { WorkspaceConnectionStatus } from "./WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "./workspace-connection-status"; @@ -229,8 +229,8 @@ export function HomeScreen(props: HomeScreenProps) { }, [props.projects]); const extraData = useMemo( - () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey, swipeEnabled }), - [props.savedConnectionsById, projectCwdByKey, swipeEnabled], + () => ({ savedConnectionsById: props.savedConnectionsById, projectCwdByKey }), + [props.savedConnectionsById, projectCwdByKey], ); const renderItem = useCallback( @@ -268,7 +268,6 @@ export function HomeScreen(props: HomeScreenProps) { onSelectThread={props.onSelectThread} onSwipeableClose={handleSwipeableClose} onSwipeableWillOpen={handleSwipeableWillOpen} - swipeEnabled={swipeEnabled} /> ); } @@ -292,7 +291,6 @@ export function HomeScreen(props: HomeScreenProps) { props.onDeleteThread, props.onSelectThread, props.savedConnectionsById, - swipeEnabled, updateGroupDisplay, ], ); @@ -387,6 +385,7 @@ export function HomeScreen(props: HomeScreenProps) { the first scroll event) and blanks non-pinned headers after collapse/expand data changes. The flattened layout still exposes `stickyHeaderIndices` if this gets revisited. */} + + {connectionStatus} ); diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index eedc769723a..791bf906cb6 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -1,6 +1,15 @@ import { SymbolView } from "expo-symbols"; import * as Haptics from "expo-haptics"; -import { useCallback, useEffect, useRef, useState, type ComponentProps, type ReactNode } from "react"; +import { + createContext, + use, + useCallback, + useEffect, + useRef, + useState, + type ComponentProps, + type ReactNode, +} from "react"; import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"; import { Pressable, View } from "react-native"; import ReanimatedSwipeable, { @@ -36,6 +45,26 @@ interface ThreadSwipePrimaryAction { readonly onPress: () => void; } +/** + * Delivers the scroll gate to swipeables via context so that flipping it does + * NOT re-render whole rows: putting the flag in list extraData/renderItem deps + * re-rendered every visible row (hooks, subscriptions and all) exactly at + * scroll start — peak frame pressure. As a context value only the + * ThreadSwipeable consumers re-render. + */ +const SwipeableScrollGateContext = createContext(true); + +export function SwipeableScrollGateProvider(props: { + readonly enabled: boolean; + readonly children: ReactNode; +}) { + return ( + + {props.children} + + ); +} + /** * Gates row swipes on list scroll activity, mirroring UIKit's own swipe * actions (`!isDragging && !isDecelerating`). failOffsetY on the swipe pan @@ -151,6 +180,7 @@ export function ThreadSwipeable(props: { const fullSwipeArmedRef = useRef(false); const fullSwipeThreshold = Math.max(THREAD_SWIPE_ACTIONS_WIDTH + 44, props.fullSwipeWidth * 0.58); const close = useCallback(() => swipeableRef.current?.close(), []); + const gateEnabled = use(SwipeableScrollGateContext); const resetKey = props.resetKey; useEffect(() => { if (resetKey === undefined) { @@ -173,7 +203,7 @@ export function ThreadSwipeable(props: { childrenContainerStyle={{ backgroundColor: props.backgroundColor }} containerStyle={[{ backgroundColor: props.backgroundColor }, props.containerStyle]} dragOffsetFromRightEdge={8} - enabled={props.enabled} + enabled={props.enabled !== false && gateEnabled} enableTrackpadTwoFingerGesture={props.enableTrackpadSwipe ?? true} // Fail the swipe once the pan is vertically dominant (patched-in RNGH // prop) — otherwise trackpad scrolls with ~8px of horizontal drift diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index bdcd7097752..8f3ef98bdaf 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -43,7 +43,7 @@ import { type HomeListItem, } from "../home/homeListItems"; import { buildHomeThreadGroups } from "../home/homeThreadList"; -import { useSwipeableScrollGate } from "../home/thread-swipe-actions"; +import { SwipeableScrollGateProvider, useSwipeableScrollGate } from "../home/thread-swipe-actions"; import { useThreadListActions } from "../home/useThreadListActions"; import { WorkspaceConnectionStatus } from "../home/WorkspaceConnectionStatus"; import { shouldShowWorkspaceConnectionStatus } from "../home/workspace-connection-status"; @@ -382,7 +382,7 @@ function ThreadNavigationSidebarPane( onScroll: handleScroll, onScrollBeginDrag: handleScrollBeginDrag, }); - const listExtraData = `${props.selectedThreadKey ?? ""}:${props.searchQuery}:${swipeEnabled}`; + const listExtraData = props.selectedThreadKey ?? ""; const focusSearch = useCallback(() => { const focus = () => { if (props.nativeChrome) { @@ -440,7 +440,6 @@ function ThreadNavigationSidebarPane( onSwipeableClose={handleSwipeableClose} onSwipeableWillOpen={handleSwipeableWillOpen} simultaneousSwipeGesture={sidebarScrollGesture} - swipeEnabled={swipeEnabled} /> ); } @@ -467,7 +466,6 @@ function ThreadNavigationSidebarPane( props.width, savedConnectionsById, sidebarScrollGesture, - swipeEnabled, updateGroupDisplay, ], ); @@ -541,6 +539,7 @@ function ThreadNavigationSidebarPane( }} /> + + ); @@ -600,7 +600,8 @@ function ThreadNavigationSidebarPane( ]} > - + + + void; @@ -414,7 +412,6 @@ export const ThreadListRow = memo(function ThreadListRow(props: { containerStyle={ compact ? undefined : { borderRadius: SIDEBAR_ROW_RADIUS, overflow: "hidden" } } - enabled={props.swipeEnabled} enableTrackpadSwipe fullSwipeWidth={props.fullSwipeWidth ?? windowWidth - 32} onDelete={handleDelete} From 573c97cde48157ae86347f5cf81a703adedf771f Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 21:16:17 -0700 Subject: [PATCH 78/81] Improve iPad workspace panes and terminal key handling - Move file and review inspectors into dedicated workspace columns - Smooth pane animations and preserve route context across transitions - Add native hardware keyboard handling for terminal control keys --- .../modules/t3terminal/T3TerminalModule.kt | 10 + .../expo/modules/t3terminal/T3TerminalView.kt | 21 +- .../t3-terminal/ios/T3TerminalModule.swift | 6 + .../t3-terminal/ios/T3TerminalView.swift | 128 +++++++-- .../features/files/ThreadFilesRouteScreen.tsx | 39 +-- .../layout/AdaptiveWorkspaceLayout.tsx | 172 ++++++++++-- .../layout/adaptive-inspector-layout.tsx | 103 -------- .../layout/workspace-inspector-pane.tsx | 145 ++++++++++ .../layout/workspace-pane-animation.ts | 16 ++ .../src/features/review/ReviewSheet.tsx | 249 ++++++++++++------ .../components/AppearancePreviews.tsx | 177 +++++++++++++ .../components/FontSizeControlRow.tsx | 81 ------ .../components/FontSizeSliderRow.tsx | 214 +++++++++++++++ .../sections/CodeAppearanceSection.tsx | 44 ++-- .../sections/TerminalAppearanceSection.tsx | 41 +-- .../sections/TextAppearanceSection.tsx | 33 ++- .../terminal/NativeTerminalSurface.tsx | 10 +- .../terminal/ThreadTerminalRouteScreen.tsx | 90 +------ .../features/terminal/nativeTerminalModule.ts | 21 +- .../features/threads/ThreadGitControls.tsx | 107 ++++---- .../features/threads/ThreadRouteScreen.tsx | 155 ++++++----- 21 files changed, 1285 insertions(+), 577 deletions(-) delete mode 100644 apps/mobile/src/features/layout/adaptive-inspector-layout.tsx create mode 100644 apps/mobile/src/features/layout/workspace-inspector-pane.tsx create mode 100644 apps/mobile/src/features/layout/workspace-pane-animation.ts create mode 100644 apps/mobile/src/features/settings/appearance/components/AppearancePreviews.tsx delete mode 100644 apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx create mode 100644 apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt index abb3982be1e..20e1bab41e5 100644 --- a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalModule.kt @@ -7,6 +7,12 @@ class T3TerminalModule : Module() { override fun definition() = ModuleDefinition { Name("T3TerminalSurface") + // Bumped when native hardware-keyboard handling changes; surfaced in the JS debug + // logs so a stale native binary is distinguishable from a broken key pipeline. + Constants( + "hardwareKeyRevision" to 2, + ) + View(T3TerminalView::class) { Prop("terminalKey") { view: T3TerminalView, terminalKey: String -> view.terminalKey = terminalKey @@ -20,6 +26,10 @@ class T3TerminalModule : Module() { view.fontSize = fontSize.toFloat() } + Prop("focusRequest") { view: T3TerminalView, focusRequest: Double -> + view.focusRequest = focusRequest + } + Prop("appearanceScheme") { view: T3TerminalView, appearanceScheme: String -> view.appearanceScheme = appearanceScheme } diff --git a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt index ec85d0ba070..cebe86272fd 100644 --- a/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt +++ b/apps/mobile/modules/t3-terminal/android/src/main/java/expo/modules/t3terminal/T3TerminalView.kt @@ -62,6 +62,15 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex var themeConfig: String = "" + var focusRequest: Double = 0.0 + set(value) { + val previous = field + field = value + if (value != previous && value > 0) { + requestKeyboardFocus() + } + } + var backgroundColorHex: String = "#24292E" set(value) { field = value @@ -113,11 +122,19 @@ class T3TerminalView(context: Context, appContext: AppContext) : ExpoView(contex } inputView.setOnKeyListener { _, keyCode, event -> if (event.action != android.view.KeyEvent.ACTION_DOWN) return@setOnKeyListener false - when (keyCode) { - android.view.KeyEvent.KEYCODE_DEL -> { + when { + keyCode == android.view.KeyEvent.KEYCODE_DEL -> { onInput(mapOf("data" to "\u007F")) true } + // Hardware keyboard Ctrl+A..Z -> control bytes 0x01..0x1A (Ctrl+C, Ctrl+Z, ...). + event.isCtrlPressed && + keyCode in android.view.KeyEvent.KEYCODE_A..android.view.KeyEvent.KEYCODE_Z -> { + onInput( + mapOf("data" to (keyCode - android.view.KeyEvent.KEYCODE_A + 1).toChar().toString()), + ) + true + } else -> false } } diff --git a/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift b/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift index c4eacb311d4..39e1874d5d7 100644 --- a/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift +++ b/apps/mobile/modules/t3-terminal/ios/T3TerminalModule.swift @@ -4,6 +4,12 @@ public class T3TerminalModule: Module { public func definition() -> ModuleDefinition { Name("T3TerminalSurface") + // Bumped when native hardware-keyboard handling changes; surfaced in the JS debug + // logs so a stale native binary is distinguishable from a broken key pipeline. + Constants([ + "hardwareKeyRevision": 2, + ]) + View(T3TerminalView.self) { Prop("terminalKey") { (view: T3TerminalView, terminalKey: String) in view.terminalKey = terminalKey diff --git a/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift b/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift index 0d63ddf6b0e..14cdb4b7802 100644 --- a/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift +++ b/apps/mobile/modules/t3-terminal/ios/T3TerminalView.swift @@ -22,38 +22,128 @@ 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)), +/// Encodes hardware-keyboard combos that UITextField never surfaces through its +/// text-editing delegate (control combos, Escape, Tab, arrow keys) into the byte +/// sequences a terminal expects. +/// +/// Capture uses UIKeyCommand with `wantsPriorityOverSystemBehavior` rather than +/// `pressesBegan`: while a text field is first responder, iPadOS routes hardware key +/// events through the text-input system, which can consume presses before they reach +/// responder press callbacks. Registered key commands are matched deterministically +/// before that happens. +private enum TerminalHardwareKeyEncoder { + /// Characters that produce a control byte when combined with Ctrl. + private static let controlInputs = "abcdefghijklmnopqrstuvwxyz@[\\]^_-? " + + static func makeKeyCommands(action: Selector) -> [UIKeyCommand] { + var commands: [UIKeyCommand] = [] + + let specialInputs = [ + UIKeyCommand.inputEscape, + UIKeyCommand.inputUpArrow, + UIKeyCommand.inputDownArrow, + UIKeyCommand.inputLeftArrow, + UIKeyCommand.inputRightArrow, + "\t", ] - } + for input in specialInputs { + commands.append(makeCommand(input: input, modifierFlags: [], action: action)) + } + commands.append(makeCommand(input: "\t", modifierFlags: .shift, action: action)) - override func deleteBackward() { - onDeleteBackward?() - super.deleteBackward() + for character in controlInputs { + commands.append(makeCommand(input: String(character), modifierFlags: .control, action: action)) + commands.append( + makeCommand(input: String(character), modifierFlags: [.control, .shift], action: action) + ) + } + + return commands } - private func command( + private static func makeCommand( input: String, - modifiers: UIKeyModifierFlags, + modifierFlags: UIKeyModifierFlags, action: Selector ) -> UIKeyCommand { - let command = UIKeyCommand(input: input, modifierFlags: modifiers, action: action) + let command = UIKeyCommand(input: input, modifierFlags: modifierFlags, action: action) command.wantsPriorityOverSystemBehavior = true return command } - @objc private func handleTab() { - onInsert?("\t") + static func sequence(input: String, modifiers: UIKeyModifierFlags) -> String? { + switch input { + case UIKeyCommand.inputEscape: + return "\u{1B}" + case UIKeyCommand.inputUpArrow: + return "\u{1B}[A" + case UIKeyCommand.inputDownArrow: + return "\u{1B}[B" + case UIKeyCommand.inputRightArrow: + return "\u{1B}[C" + case UIKeyCommand.inputLeftArrow: + return "\u{1B}[D" + case "\t": + return modifiers.contains(.shift) ? "\u{1B}[Z" : "\t" + default: + break + } + + guard modifiers.contains(.control) else { return nil } + guard let scalar = input.lowercased().unicodeScalars.first else { return nil } + return controlSequence(for: scalar) + } + + private static func controlSequence(for scalar: Unicode.Scalar) -> String? { + switch scalar { + case "a"..."z": + // Ctrl+A..Z -> 0x01..0x1A (Ctrl+C = ETX, Ctrl+Z = SUB, ...). + return UnicodeScalar(scalar.value - 96).map(String.init) + case " ", "@": + return "\u{00}" + case "[": + return "\u{1B}" + case "\\": + return "\u{1C}" + case "]": + return "\u{1D}" + case "^": + return "\u{1E}" + case "_", "-": + return "\u{1F}" + case "?": + return "\u{7F}" + default: + return nil + } + } +} + +private final class TerminalInputField: UITextField { + var onDeleteBackward: (() -> Void)? + var onInsert: ((String) -> Void)? + + private static let hardwareKeyCommands = TerminalHardwareKeyEncoder.makeKeyCommands( + action: #selector(handleHardwareKeyCommand(_:)) + ) + + override var keyCommands: [UIKeyCommand]? { + Self.hardwareKeyCommands + } + + override func deleteBackward() { + onDeleteBackward?() + super.deleteBackward() } - @objc private func handleBackTab() { - onInsert?("\u{1B}[Z") + @objc + private func handleHardwareKeyCommand(_ command: UIKeyCommand) { + guard let input = command.input else { return } + guard let sequence = TerminalHardwareKeyEncoder.sequence( + input: input, + modifiers: command.modifierFlags + ) else { return } + onInsert?(sequence) } } diff --git a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx index 761bfd71e9f..487d8271e1b 100644 --- a/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx +++ b/apps/mobile/src/features/files/ThreadFilesRouteScreen.tsx @@ -9,6 +9,7 @@ import { useColorScheme, View, } from "react-native"; +import { useSafeAreaInsets } from "react-native-safe-area-context"; import Svg, { Defs, LinearGradient, Rect, Stop } from "react-native-svg"; import { EnvironmentId, @@ -29,10 +30,10 @@ import { useThreadSelection } from "../../state/use-thread-selection"; import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree"; import { useEnvironmentQuery } from "../../state/query"; import { projectEnvironment } from "../../state/projects"; -import { AdaptiveInspectorLayout } from "../layout/adaptive-inspector-layout"; import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, + useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; import { createNativeMailSearchToolbarItem } from "../layout/native-mail-search-toolbar"; import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; @@ -506,6 +507,18 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { ) : undefined, [cwd, environmentId, fileInspector.supported, handleSelectFile, projectName, relativePath], ); + // The workspace inspector column spans the full window height. On iOS the + // pane brings its own nested native header; elsewhere it pads itself below + // the top inset. + const safeAreaInsets = useSafeAreaInsets(); + const inspectorHeaderInset = Platform.OS === "ios" ? 0 : safeAreaInsets.top; + // Hand the file navigator to the workspace so it renders beside the + // navigator, outside this screen's native header. + const renderWorkspaceInspector = useCallback( + () => renderInspector(inspectorHeaderInset), + [inspectorHeaderInset, renderInspector], + ); + useRegisterWorkspaceInspector(fileInspector.supported ? renderWorkspaceInspector : undefined); if (selectedThread === null || environmentId === null || threadId === null) { return ; @@ -616,20 +629,16 @@ export function ThreadFileScreen(props: ThreadFileRouteScreenProps) { ) : null} - renderInspector(0) : undefined} - > - fileQuery.refresh()} - /> - + fileQuery.refresh()} + /> ); diff --git a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx index 17c45a1f606..fe3db9a4f03 100644 --- a/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx +++ b/apps/mobile/src/features/layout/AdaptiveWorkspaceLayout.tsx @@ -1,7 +1,12 @@ import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; import { EnvironmentId, ThreadId } from "@t3tools/contracts"; import { useFocusEffect } from "@react-navigation/native"; -import { StackActions, useNavigation } from "@react-navigation/native"; +import { + NavigationContext, + NavigationRouteContext, + StackActions, + useNavigation, +} from "@react-navigation/native"; import { createContext, use, @@ -13,13 +18,7 @@ import { type ReactNode, } from "react"; import { useWindowDimensions, View } from "react-native"; -import Animated, { - Easing, - ReduceMotion, - useAnimatedStyle, - useSharedValue, - withTiming, -} from "react-native-reanimated"; +import Animated, { useAnimatedStyle, useSharedValue, withTiming } from "react-native-reanimated"; import { deriveFileInspectorPaneLayout, @@ -38,6 +37,8 @@ import { } from "../keyboard/hardwareKeyboardCommands"; import { HomeListOptionsProvider } from "../home/home-list-options"; import { ThreadNavigationSidebar } from "../threads/ThreadNavigationSidebar"; +import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; +import { WorkspaceInspectorPane } from "./workspace-inspector-pane"; interface AdaptiveWorkspaceContextValue { readonly layout: Layout; @@ -45,6 +46,15 @@ interface AdaptiveWorkspaceContextValue { readonly fileInspector: FileInspectorPaneLayout; readonly primarySidebarSearchQuery: string; readonly activateAuxiliaryPaneRole: (role: WorkspaceAuxiliaryPaneRole) => () => void; + /** + * Route screens hand their inspector pane content to the workspace so it + * renders BESIDE the navigator (outside the native stack header) instead of + * inside the route. Returns a deactivate callback: the pane animates closed + * (content kept mounted for the exit transition) unless a newer + * registration already took over — stale deactivates never clobber it. + * Prefer useRegisterWorkspaceInspector over calling this directly. + */ + readonly registerWorkspaceInspector: (render: () => ReactNode) => () => void; readonly setPrimarySidebarSearchQuery: (query: string) => void; readonly showAuxiliaryPane: (role: WorkspaceAuxiliaryPaneRole) => void; readonly toggleAuxiliaryPane: () => void; @@ -69,6 +79,7 @@ const AdaptiveWorkspaceContext = createContext({ fileInspector: compactFileInspector, primarySidebarSearchQuery: "", activateAuxiliaryPaneRole: () => () => undefined, + registerWorkspaceInspector: () => () => undefined, setPrimarySidebarSearchQuery: () => undefined, showAuxiliaryPane: () => undefined, toggleAuxiliaryPane: () => undefined, @@ -88,6 +99,84 @@ export function useAdaptiveWorkspacePaneRole(role: WorkspaceAuxiliaryPaneRole) { ); } +/** + * Register this screen's inspector pane content with the workspace column. + * + * The column renders BESIDE the navigator — outside any screen — so the + * registering screen's navigation and route contexts are captured here and + * re-provided around the portal content. Without them, useNavigation/useRoute + * inside the pane (e.g. GitOverviewSheet via useThreadSelection) throw + * "Couldn't find a route object". + * + * Registration is FOCUS-scoped, driven by navigation events rather than the + * screen's own render cycle: react-native-screens freezes blurred screens, so + * a cleanup that depends on the blurred subtree re-rendering never runs and + * would leak the pane into the next route. Blur deactivates the pane (it + * animates closed, or is replaced seamlessly when the next route registers in + * the same commit); focus re-registers it. + */ +export function useRegisterWorkspaceInspector(render: (() => ReactNode) | undefined) { + const { registerWorkspaceInspector } = useAdaptiveWorkspaceLayout(); + // Raw context values (not the useNavigation/useRoute wrappers) so the + // portal re-provides exactly what this screen sees. + const navigation = use(NavigationContext); + const route = use(NavigationRouteContext); + + const wrappedRender = useMemo(() => { + if (render === undefined) { + return undefined; + } + return () => ( + + {render()} + + ); + }, [navigation, render, route]); + + const wrappedRenderRef = useRef(wrappedRender); + wrappedRenderRef.current = wrappedRender; + const focusedRef = useRef(false); + const deactivateRef = useRef<(() => void) | null>(null); + + const syncRegistration = useCallback(() => { + if (!focusedRef.current || wrappedRenderRef.current === undefined) { + deactivateRef.current?.(); + return; + } + deactivateRef.current = registerWorkspaceInspector(wrappedRenderRef.current); + }, [registerWorkspaceInspector]); + + // Focus lifecycle. Blur/focus events fire even when the blurred subtree is + // frozen (events are navigation-driven, renders are not). + useFocusEffect( + useCallback(() => { + focusedRef.current = true; + syncRegistration(); + return () => { + focusedRef.current = false; + syncRegistration(); + }; + }, [syncRegistration]), + ); + + // Content changes while focused re-register in place. + useEffect(() => { + if (focusedRef.current) { + syncRegistration(); + } + }, [syncRegistration, wrappedRender]); + + // Unmount: hand the pane back (owner-guarded, so a route that already + // took over is unaffected). + useEffect( + () => () => { + deactivateRef.current?.(); + deactivateRef.current = null; + }, + [], + ); +} + export function AdaptiveWorkspaceLayout(props: { readonly children: ReactNode; readonly pathname: string; @@ -175,6 +264,33 @@ export function AdaptiveWorkspaceLayout(props: { return null; } }, [environmentId, threadId]); + // Wrapped in an object: bare functions in useState would be treated as + // lazy initializers/updaters. `active: false` keeps the outgoing route's + // content mounted so the pane can animate closed (or be replaced + // seamlessly by the next route's registration in the same commit). + const [workspaceInspector, setWorkspaceInspector] = useState<{ + readonly render: () => ReactNode; + readonly active: boolean; + } | null>(null); + const workspaceInspectorOwner = useRef(null); + const registerWorkspaceInspector = useCallback((render: () => ReactNode) => { + const owner = Symbol("workspace-inspector"); + workspaceInspectorOwner.current = owner; + setWorkspaceInspector({ render, active: true }); + + return () => { + // During a push/replace the outgoing screen deactivates AFTER the + // incoming screen registered — only the current owner may deactivate. + if (workspaceInspectorOwner.current !== owner) { + return; + } + setWorkspaceInspector((current) => (current === null ? null : { ...current, active: false })); + }; + }, []); + // Once the close animation settles, drop the stale content entirely. + const handleWorkspaceInspectorClosed = useCallback(() => { + setWorkspaceInspector((current) => (current !== null && !current.active ? null : current)); + }, []); const activateAuxiliaryPaneRole = useCallback((role: WorkspaceAuxiliaryPaneRole) => { const owner = Symbol(role); activeRoleOwner.current = owner; @@ -253,6 +369,7 @@ export function AdaptiveWorkspaceLayout(props: { fileInspector, primarySidebarSearchQuery, activateAuxiliaryPaneRole, + registerWorkspaceInspector, setPrimarySidebarSearchQuery, showAuxiliaryPane, toggleAuxiliaryPane, @@ -265,6 +382,7 @@ export function AdaptiveWorkspaceLayout(props: { layout, panes, primarySidebarSearchQuery, + registerWorkspaceInspector, showAuxiliaryPane, setPrimarySidebarSearchQuery, setAuxiliaryPaneWidth, @@ -288,17 +406,27 @@ export function AdaptiveWorkspaceLayout(props: { ); useEffect(() => { const targetWidth = panes.primarySidebarVisible ? (layout.listPaneWidth ?? 0) : 0; - renderedSidebarWidth.value = withTiming(targetWidth, { - duration: panes.primarySidebarVisible ? 220 : 160, - easing: panes.primarySidebarVisible ? Easing.out(Easing.cubic) : Easing.in(Easing.cubic), - reduceMotion: ReduceMotion.System, - }); + renderedSidebarWidth.value = withTiming(targetWidth, WORKSPACE_PANE_TIMING); }, [layout.listPaneWidth, panes.primarySidebarVisible, renderedSidebarWidth]); const sidebarAnimatedStyle = useAnimatedStyle(() => ({ opacity: Math.min(1, renderedSidebarWidth.value / 80), width: renderedSidebarWidth.value, })); + // Freeze the content pane at its SETTLED width while the side panes + // animate. The navigator (native header + markdown feed) lays out ONCE per + // pane toggle instead of re-measuring on every animation frame — the + // animating columns merely clip/reveal it over a matching background. + // Continuously re-wrapping the chat feed was the main source of dropped + // frames during sidebar/inspector transitions. + const inspectorColumnTargetWidth = + workspaceInspector !== null && workspaceInspector.active && panes.auxiliaryPaneVisible + ? (panes.auxiliaryPaneWidth ?? 0) + : 0; + const contentSettledWidth = layout.usesSplitView + ? Math.max(0, panes.contentPaneWidth - inspectorColumnTargetWidth) + : null; + const handleSelectThread = useCallback( (thread: EnvironmentThreadShell) => { const params = { @@ -355,9 +483,23 @@ export function AdaptiveWorkspaceLayout(props: { /> ) : 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 deleted file mode 100644 index 9e9f4fd68da..00000000000 --- a/apps/mobile/src/features/layout/adaptive-inspector-layout.tsx +++ /dev/null @@ -1,103 +0,0 @@ -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, 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 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, - }); - 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?.()} - - ) : null} - - ); -} diff --git a/apps/mobile/src/features/layout/workspace-inspector-pane.tsx b/apps/mobile/src/features/layout/workspace-inspector-pane.tsx new file mode 100644 index 00000000000..efc97380c84 --- /dev/null +++ b/apps/mobile/src/features/layout/workspace-inspector-pane.tsx @@ -0,0 +1,145 @@ +import { useCallback, useEffect, useRef, useState, type ReactNode } from "react"; +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; + +import { constrainAuxiliaryPaneWidth, type WorkspacePaneLayout } from "../../lib/layout"; +import { WORKSPACE_PANE_TIMING } from "./workspace-pane-animation"; +import { WorkspacePaneDivider } from "./workspace-pane-divider"; + +/** + * The trailing inspector column: resize divider + animated reveal. + * + * Rendered by AdaptiveWorkspaceLayout as a SIBLING of the navigator so the + * native stack header (and its trailing toolbar items) spans only the content + * pane — the inspector owns its own full-height column, mirroring how each + * column of a UISplitViewController has its own chrome. + * + * Receives the pane layout via props (not the workspace context hook) so this + * module stays import-cycle-free with AdaptiveWorkspaceLayout. + */ +export function WorkspaceInspectorPane(props: { + /** + * When false the pane animates closed but keeps its content mounted for the + * exit transition (a route that lost focus). `onClosed` fires once the + * close animation settles so the owner can drop the stale content. + */ + readonly active?: boolean; + readonly onClosed?: () => void; + readonly panes: WorkspacePaneLayout; + readonly renderInspector?: () => ReactNode; + readonly setAuxiliaryPaneWidth: (width: number) => void; +}) { + const { panes, setAuxiliaryPaneWidth } = props; + const inspectorWidth = panes.auxiliaryPaneWidth; + const inspectorSupported = props.renderInspector !== undefined && inspectorWidth !== null; + const inspectorVisible = + inspectorSupported && panes.auxiliaryPaneVisible && (props.active ?? true); + 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 visibility and explicit resizing change it. + const inspectorProgress = useSharedValue(inspectorVisible ? 1 : 0); + const renderedInspectorWidth = useSharedValue(inspectorVisible ? (inspectorWidth ?? 0) : 0); + // The content keeps its own width so the reveal (outer width) clips a + // fully-laid-out pane instead of reflowing text every frame. When the OPEN + // pane's target width changes (e.g. the sidebar toggles and reserves + // space), animate the content width in lockstep rather than snapping. + const renderedContentWidth = useSharedValue(inspectorWidth ?? 0); + + const onClosed = props.onClosed; + useEffect(() => { + inspectorProgress.value = withTiming( + inspectorVisible ? 1 : 0, + WORKSPACE_PANE_TIMING, + (finished) => { + if (finished === true && !inspectorVisible && onClosed !== undefined) { + runOnJS(onClosed)(); + } + }, + ); + const targetWidth = inspectorVisible ? (inspectorWidth ?? 0) : 0; + renderedInspectorWidth.value = resizing + ? targetWidth + : withTiming(targetWidth, WORKSPACE_PANE_TIMING); + }, [ + inspectorProgress, + inspectorVisible, + inspectorWidth, + onClosed, + renderedInspectorWidth, + resizing, + ]); + + useEffect(() => { + const targetWidth = inspectorWidth ?? 0; + if (!inspectorVisible || resizing) { + // Hidden panes re-measure silently; during a divider drag the content + // tracks the finger directly. + renderedContentWidth.value = targetWidth; + return; + } + renderedContentWidth.value = withTiming(targetWidth, WORKSPACE_PANE_TIMING); + }, [inspectorVisible, inspectorWidth, renderedContentWidth, resizing]); + + const inspectorStyle = useAnimatedStyle( + () => ({ + opacity: inspectorProgress.value, + transform: [{ translateX: (1 - inspectorProgress.value) * 24 }], + width: renderedInspectorWidth.value, + }), + [], + ); + const inspectorContentStyle = useAnimatedStyle(() => ({ width: renderedContentWidth.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 ( + <> + {inspectorVisible ? ( + + ) : null} + {inspectorSupported ? ( + + + {props.renderInspector?.()} + + + ) : null} + + ); +} diff --git a/apps/mobile/src/features/layout/workspace-pane-animation.ts b/apps/mobile/src/features/layout/workspace-pane-animation.ts new file mode 100644 index 00000000000..5f037cbcc6a --- /dev/null +++ b/apps/mobile/src/features/layout/workspace-pane-animation.ts @@ -0,0 +1,16 @@ +import { Easing, ReduceMotion } from "react-native-reanimated"; + +/** + * One timing curve for every workspace pane (primary sidebar + inspector). + * + * Panes frequently animate together — opening the sidebar can auto-close the + * inspector when both no longer fit — so identical duration and easing on + * every pane keeps the center content pane from wobbling while both edges + * move. Asymmetric open/close timings (the previous 220ms out-cubic open vs + * 160ms in-cubic close) read as jank during those simultaneous swaps. + */ +export const WORKSPACE_PANE_TIMING = { + duration: 260, + easing: Easing.inOut(Easing.cubic), + reduceMotion: ReduceMotion.System, +} as const; diff --git a/apps/mobile/src/features/review/ReviewSheet.tsx b/apps/mobile/src/features/review/ReviewSheet.tsx index 1efbac6dff3..a79dfa108bb 100644 --- a/apps/mobile/src/features/review/ReviewSheet.tsx +++ b/apps/mobile/src/features/review/ReviewSheet.tsx @@ -1,6 +1,11 @@ import type { EnvironmentId, ThreadId } from "@t3tools/contracts"; -import type { StaticScreenProps } from "@react-navigation/native"; -import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; +import { useNavigation, type StaticScreenProps } from "@react-navigation/native"; +import { + NativeHeaderToolbar, + NativeStackScreenOptions, + nativeHeaderScrollEdgeEffects, +} from "../../native/StackHeader"; +import { Screen, ScreenStack, ScreenStackHeaderConfig } from "react-native-screens"; import { SymbolView } from "expo-symbols"; import { memo, @@ -33,11 +38,19 @@ import { useAtomCommand } from "../../state/use-atom-command"; import { useThemeColor } from "../../lib/useThemeColor"; import { useThreadDraftForThread } from "../../state/use-thread-composer-state"; import { EnvironmentConnectionNotice } from "../connection/EnvironmentConnectionNotice"; -import { AdaptiveInspectorLayout } from "../layout/adaptive-inspector-layout"; import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, + useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; +import { useEnvironmentQuery } from "../../state/query"; +import { useSelectedThreadGitActions } from "../../state/use-selected-thread-git-actions"; +import { useSelectedThreadGitState } from "../../state/use-selected-thread-git-state"; +import { useSelectedThreadWorktree } from "../../state/use-selected-thread-worktree"; +import { useThreadSelection } from "../../state/use-thread-selection"; +import { vcsEnvironment } from "../../state/vcs"; +import { WorkspaceSidebarToolbar } from "../layout/workspace-sidebar-toolbar"; +import { ThreadGitMenu } from "../threads/ThreadGitControls"; import { useReviewCacheForThread } from "./reviewState"; import { type NativeReviewDiffViewHandle, @@ -141,9 +154,10 @@ const ReviewFileNavigatorRow = memo(function ReviewFileNavigatorRow(props: { readonly onSelectFile: (fileId: string | null) => void; }) { const { file, selected, onSelectFile } = props; + // Tapping the selected file again returns to the all-files diff. const handlePress = useCallback(() => { - onSelectFile(file.id); - }, [file.id, onSelectFile]); + onSelectFile(selected ? null : file.id); + }, [file.id, onSelectFile, selected]); return ( file.id} + contentContainerStyle={{ + paddingHorizontal: 8, + paddingBottom: 8, + // The nested native header is translucent; start the list below it so + // the scroll-edge effect can sample the content (same treatment as + // FileTreeBrowser in the Files pane). + paddingTop: Platform.OS === "ios" ? insets.top + 44 + 8 : 8, + }} + scrollIndicatorInsets={Platform.OS === "ios" ? { top: insets.top + 44 } : undefined} + renderItem={renderFile} + /> + ); + + if (Platform.OS === "ios") { + return ( + + + + {fileList} + + + + + ); + } + return ( @@ -251,30 +319,7 @@ function ReviewFileNavigator({ - file.id} - contentContainerStyle={{ paddingHorizontal: 8, paddingVertical: 8 }} - ListHeaderComponent={ - handleSelectFile(null)} - > - All files - - {files.length} changed {files.length === 1 ? "file" : "files"} - - - } - renderItem={renderFile} - /> + {fileList} ); } @@ -287,8 +332,8 @@ type ReviewSheetProps = StaticScreenProps<{ export function ReviewSheet(props: ReviewSheetProps) { const { nativeReviewDiffStyle } = useAppearanceCodeSurface(); useAdaptiveWorkspacePaneRole("inspector"); - const { layout, panes, showAuxiliaryPane, toggleAuxiliaryPane, togglePrimarySidebar } = - useAdaptiveWorkspaceLayout(); + const { panes, showAuxiliaryPane, toggleAuxiliaryPane } = useAdaptiveWorkspaceLayout(); + const navigation = useNavigation(); const insets = useSafeAreaInsets(); const colorScheme = useColorScheme(); const headerIcon = String(useThemeColor("--color-icon")); @@ -298,6 +343,23 @@ export function ReviewSheet(props: ReviewSheetProps) { const isEnvironmentReady = environment.presentation?.connection.phase === "connected"; const { draftMessage } = useThreadDraftForThread({ environmentId, threadId }); const reviewCache = useReviewCacheForThread({ environmentId, threadId }); + /* ─── Git actions for the toolbar menu (commit/push without leaving review) ── */ + const { selectedThread } = useThreadSelection(); + const { selectedThreadCwd } = useSelectedThreadWorktree(); + const gitState = useSelectedThreadGitState(); + const gitActions = useSelectedThreadGitActions(); + const gitStatusQuery = useEnvironmentQuery( + selectedThread !== null && selectedThreadCwd !== null + ? vcsEnvironment.status({ + environmentId: selectedThread.environmentId, + input: { cwd: selectedThreadCwd }, + }) + : null, + ); + // The selection-based git hooks only apply when this review belongs to the + // selected thread (it always does when reached from the thread's toolbar). + const gitMenuAvailable = + selectedThread !== null && String(selectedThread.id) === String(threadId); const selectedTheme = colorScheme === "dark" ? "dark" : "light"; // With a solid (non-overlay) header the content lays out below the header // natively, so no manual top inset is needed. @@ -395,13 +457,14 @@ export function ReviewSheet(props: ReviewSheetProps) { ), - [handleSelectFile, nativeReviewDiffData.files, selectedSection?.id], + [handleSelectFile, insets.top, nativeReviewDiffData.files, selectedSection?.id], ); const handleNativeToggleFile = useCallback( @@ -438,6 +501,22 @@ export function ReviewSheet(props: ReviewSheetProps) { const handleRetryEnvironment = useCallback(() => { void retryEnvironment(environmentId); }, [environmentId, retryEnvironment]); + const handleReturnToThread = useCallback(() => { + if (navigation.canGoBack()) { + navigation.goBack(); + return; + } + navigation.navigate("Thread", { + environmentId: String(environmentId), + threadId: String(threadId), + }); + }, [environmentId, navigation, threadId]); + + // The changed-files navigator lives in the workspace inspector column — + // the single right-hand pane per route — instead of an in-screen panel. + const showChangedFilesPane = + !showConnectionNotice && selectedSection !== null && parsedDiff.kind === "files"; + useRegisterWorkspaceInspector(showChangedFilesPane ? renderInspector : undefined); const listHeader = useMemo(() => { const children: ReactElement[] = []; @@ -487,20 +566,15 @@ export function ReviewSheet(props: ReviewSheetProps) { }} /> - {layout.usesSplitView ? ( - - - - ) : null} + + + - {showSectionToolbar || panes.supportsAuxiliaryPane ? ( + {showSectionToolbar || panes.supportsAuxiliaryPane || gitMenuAvailable ? ( {panes.supportsAuxiliaryPane ? ( ) : null} + {gitMenuAvailable && selectedThread !== null ? ( + + ) : null} {showSectionToolbar ? ( @@ -591,43 +676,41 @@ export function ReviewSheet(props: ReviewSheetProps) { backgroundColor: nativeBridge.theme.background, }} > - - - {listHeader} - - void handlePullToRefresh()} - style={StyleSheet.absoluteFill} - appearanceScheme={selectedTheme} - collapsedFileIdsJson={nativeBridge.collapsedFileIdsJson} - collapsedCommentIdsJson={nativeBridge.collapsedCommentIdsJson} - contentResetKey={`${reviewCache.threadKey}:${selectedSection.id}`} - contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH} - nativeViewRef={nativeReviewDiffViewRef} - rowHeight={nativeReviewDiffStyle.rowHeight} - rowsJson={nativeBridge.rowsJson} - selectedRowIdsJson={nativeBridge.selectedRowIdsJson} - styleJson={nativeBridge.styleJson} - themeJson={nativeBridge.themeJson} - tokensPatchJson={nativeBridge.tokensPatchJson} - tokensResetKey={nativeBridge.tokensResetKey} - viewedFileIdsJson={nativeBridge.viewedFileIdsJson} - onDebug={nativeBridge.onDebug} - onPressLine={commentSelection.onPressLine} - onVisibleFileChange={handleVisibleFileChange} - onToggleComment={nativeBridge.onToggleComment} - onToggleFile={handleNativeToggleFile} - onToggleViewedFile={handleNativeToggleViewedFile} - /> - + + {listHeader} + + void handlePullToRefresh()} + style={StyleSheet.absoluteFill} + appearanceScheme={selectedTheme} + collapsedFileIdsJson={nativeBridge.collapsedFileIdsJson} + collapsedCommentIdsJson={nativeBridge.collapsedCommentIdsJson} + contentResetKey={`${reviewCache.threadKey}:${selectedSection.id}`} + contentWidth={NATIVE_REVIEW_DIFF_CONTENT_WIDTH} + nativeViewRef={nativeReviewDiffViewRef} + rowHeight={nativeReviewDiffStyle.rowHeight} + rowsJson={nativeBridge.rowsJson} + selectedRowIdsJson={nativeBridge.selectedRowIdsJson} + styleJson={nativeBridge.styleJson} + themeJson={nativeBridge.themeJson} + tokensPatchJson={nativeBridge.tokensPatchJson} + tokensResetKey={nativeBridge.tokensResetKey} + viewedFileIdsJson={nativeBridge.viewedFileIdsJson} + onDebug={nativeBridge.onDebug} + onPressLine={commentSelection.onPressLine} + onVisibleFileChange={handleVisibleFileChange} + onToggleComment={nativeBridge.onToggleComment} + onToggleFile={handleNativeToggleFile} + onToggleViewedFile={handleNativeToggleViewedFile} + /> - + ) : ( ; +} + +/** Live sample of body text rendered at the chosen base font size. */ +export function TextAppearancePreview(props: { readonly fontSize: number }) { + const sizes = resolveMarkdownFontSizes(props.fontSize); + + return ( + + + The quick brown fox jumps over the lazy dog. + + + Messages, labels, and headings scale with this size. + + + ); +} + +/** + * Live terminal sample using the real terminal theme's text colors and font, + * on the shared card background so it reads like the other previews. + */ +export function TerminalAppearancePreview(props: { readonly fontSize: number }) { + const scheme = useColorScheme() === "light" ? "light" : "dark"; + const theme = getPierreTerminalTheme(scheme); + const lineHeight = Math.round(props.fontSize * 1.6); + const lineStyle = { + fontFamily: "Menlo", + fontSize: props.fontSize, + lineHeight, + } as const; + + return ( + + $ npm run dev + ✓ Ready in 430ms + + Local: http://localhost:3000{" "} + + + + ); +} + +interface CodePreviewToken { + readonly text: string; + readonly keyword?: boolean; +} + +interface CodePreviewLine { + readonly id: string; + readonly tokens: ReadonlyArray; +} + +const CODE_PREVIEW_LINES: ReadonlyArray = [ + { + id: "signature", + tokens: [{ text: "function", keyword: true }, { text: " formatUser(user) {" }], + }, + { + id: "body", + tokens: [ + { text: " " }, + { text: "return", keyword: true }, + { text: " `${user.name} <${user.email}>` // demonstrates how long lines behave" }, + ], + }, + { id: "close", tokens: [{ text: "}" }] }, +]; + +/** + * Live code sample matching the code & diff surface metrics. Long lines wrap + * when word break is on and scroll horizontally when it is off, mirroring the + * real code surface. + */ +export function CodeAppearancePreview(props: { + readonly fontSize: number; + readonly wordBreak: boolean; +}) { + const surface = resolveMobileCodeSurface(props.fontSize); + const lineNumberColor = useThemeColor("--color-icon-subtle"); + const keywordColor = useThemeColor("--color-md-link"); + + const lineNumber = (line: CodePreviewLine, index: number) => ( + + {index + 1} + + ); + + const codeLine = (line: CodePreviewLine, wrap: boolean) => ( + + {line.tokens.map((token) => ( + + {token.text} + + ))} + + ); + + if (props.wordBreak) { + return ( + + {CODE_PREVIEW_LINES.map((line, index) => ( + + {lineNumber(line, index)} + {codeLine(line, true)} + + ))} + + ); + } + + return ( + + {CODE_PREVIEW_LINES.map((line, index) => lineNumber(line, index))} + + {CODE_PREVIEW_LINES.map((line) => codeLine(line, false))} + + + ); +} diff --git a/apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx b/apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx deleted file mode 100644 index bb0f749a50c..00000000000 --- a/apps/mobile/src/features/settings/appearance/components/FontSizeControlRow.tsx +++ /dev/null @@ -1,81 +0,0 @@ -import { SymbolView } from "expo-symbols"; -import type { ComponentProps } from "react"; -import { Pressable, View } from "react-native"; - -import { AppText as Text } from "../../../../components/AppText"; -import { useThemeColor } from "../../../../lib/useThemeColor"; - -type SymbolName = ComponentProps["name"]; - -export function FontSizeControlRow(props: { - readonly disabled?: boolean; - readonly icon: SymbolName; - readonly label: string; - readonly valueLabel: string; - readonly canDecrease: boolean; - readonly canIncrease: boolean; - readonly onDecrease: () => void; - readonly onIncrease: () => void; -}) { - const icon = useThemeColor("--color-icon"); - const buttonBackground = String(useThemeColor("--color-secondary")); - const buttonForeground = String(useThemeColor("--color-foreground")); - - return ( - - - {props.label} - - - - {props.valueLabel} - - - - - ); -} - -function FontSizeStepButton(props: { - readonly accessibilityLabel: string; - readonly backgroundColor: string; - readonly disabled: boolean; - readonly foregroundColor: string; - readonly label: string; - readonly onPress: () => void; -}) { - return ( - - - {props.label} - - - ); -} diff --git a/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx b/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx new file mode 100644 index 00000000000..5166ed11db7 --- /dev/null +++ b/apps/mobile/src/features/settings/appearance/components/FontSizeSliderRow.tsx @@ -0,0 +1,214 @@ +import * as Haptics from "expo-haptics"; +import { SymbolView } from "expo-symbols"; +import { useCallback, useEffect, useMemo, useRef } from "react"; +import { View, type AccessibilityActionEvent } from "react-native"; +import { Gesture, GestureDetector } from "react-native-gesture-handler"; +import Animated, { + runOnJS, + useAnimatedStyle, + useSharedValue, + withTiming, +} from "react-native-reanimated"; +import type { ComponentProps } from "react"; + +import { AppText as Text } from "../../../../components/AppText"; +import { useThemeColor } from "../../../../lib/useThemeColor"; + +type SymbolName = ComponentProps["name"]; + +const THUMB_SIZE = 26; +const TRACK_HEIGHT = 4; +const SNAP_ANIMATION = { duration: 120 } as const; + +function clampFraction(value: number): number { + "worklet"; + return Math.min(1, Math.max(0, value)); +} + +export function FontSizeSliderRow(props: { + readonly disabled?: boolean; + readonly icon: SymbolName; + readonly label: string; + readonly valueLabel: string; + readonly min: number; + readonly max: number; + readonly step: number; + readonly value: number; + readonly onChange: (value: number) => void; +}) { + const icon = useThemeColor("--color-icon"); + const iconMuted = String(useThemeColor("--color-icon-muted")); + const trackColor = String(useThemeColor("--color-secondary-border")); + const fillColor = String(useThemeColor("--color-primary")); + + const latest = useRef(props); + latest.current = props; + + const { min, max, step, value, disabled } = props; + const fraction = (value - min) / (max - min); + + const progress = useSharedValue(clampFraction(fraction)); + const trackWidth = useSharedValue(0); + const dragging = useSharedValue(false); + + useEffect(() => { + if (!dragging.value) { + progress.value = withTiming(clampFraction(fraction), SNAP_ANIMATION); + } + }, [dragging, fraction, progress]); + + const commit = useCallback((next: number) => { + if (next === latest.current.value) { + return; + } + Haptics.selectionAsync().catch(() => undefined); + latest.current.onChange(next); + }, []); + + const gesture = useMemo(() => { + const snapValue = (raw: number): number => { + "worklet"; + const stepped = Math.round((raw - min) / step) * step + min; + return Math.min(max, Math.max(min, stepped)); + }; + const fractionAt = (x: number): number => { + "worklet"; + const usable = trackWidth.value - THUMB_SIZE; + if (usable <= 0) { + return 0; + } + return clampFraction((x - THUMB_SIZE / 2) / usable); + }; + const valueAtFraction = (f: number): number => { + "worklet"; + return snapValue(min + f * (max - min)); + }; + const fractionOfValue = (v: number): number => { + "worklet"; + return clampFraction((v - min) / (max - min)); + }; + + const pan = Gesture.Pan() + .enabled(!disabled) + .activeOffsetX([-8, 8]) + .failOffsetY([-12, 12]) + .onUpdate((event) => { + dragging.value = true; + const f = fractionAt(event.x); + progress.value = f; + runOnJS(commit)(valueAtFraction(f)); + }) + .onFinalize(() => { + if (!dragging.value) { + return; + } + dragging.value = false; + progress.value = withTiming( + fractionOfValue(valueAtFraction(progress.value)), + SNAP_ANIMATION, + ); + }); + + const tap = Gesture.Tap() + .enabled(!disabled) + .onEnd((event) => { + const next = valueAtFraction(fractionAt(event.x)); + progress.value = withTiming(fractionOfValue(next), SNAP_ANIMATION); + runOnJS(commit)(next); + }); + + return Gesture.Race(pan, tap); + }, [commit, disabled, dragging, max, min, progress, step, trackWidth]); + + const fillStyle = useAnimatedStyle(() => ({ + width: THUMB_SIZE / 2 + progress.value * Math.max(0, trackWidth.value - THUMB_SIZE), + })); + const thumbStyle = useAnimatedStyle(() => ({ + transform: [{ translateX: progress.value * Math.max(0, trackWidth.value - THUMB_SIZE) }], + })); + + const handleAccessibilityAction = (event: AccessibilityActionEvent) => { + if (event.nativeEvent.actionName === "increment") { + commit(Math.min(max, value + step)); + } else if (event.nativeEvent.actionName === "decrement") { + commit(Math.max(min, value - step)); + } + }; + + return ( + + + + {props.label} + {props.valueLabel} + + + + + { + trackWidth.value = event.nativeEvent.layout.width; + }} + > + + + + + + + + + + ); +} diff --git a/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx index 378e6d88fbb..6760504bfa2 100644 --- a/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx +++ b/apps/mobile/src/features/settings/appearance/sections/CodeAppearanceSection.tsx @@ -1,14 +1,18 @@ import { useCallback } from "react"; import { + CODE_FONT_SIZE_STEP, MAX_CODE_FONT_SIZE, MIN_CODE_FONT_SIZE, - stepCodeFontSize, } from "../../../../lib/appearancePreferences"; import { SettingsSection } from "../../components/SettingsSection"; import { SettingsSwitchRow } from "../../components/SettingsSwitchRow"; import { useAppearancePreferences } from "../AppearancePreferencesProvider"; -import { FontSizeControlRow } from "../components/FontSizeControlRow"; +import { + AppearancePreviewSeparator, + CodeAppearancePreview, +} from "../components/AppearancePreviews"; +import { FontSizeSliderRow } from "../components/FontSizeSliderRow"; export function CodeAppearanceSection() { const { isReady, appearance, setCodeFontSize, setCodeWordBreak } = useAppearancePreferences(); @@ -21,16 +25,13 @@ export function CodeAppearanceSection() { [appearance.codeFontSize, setCodeFontSize], ); - const handleDecrease = useCallback(() => { - setCodeFontSize(stepCodeFontSize(appearance.codeFontSize, -1)); - }, [appearance.codeFontSize, setCodeFontSize]); - - const handleIncrease = useCallback(() => { - setCodeFontSize(stepCodeFontSize(appearance.codeFontSize, 1)); - }, [appearance.codeFontSize, setCodeFontSize]); - return ( + + - MIN_CODE_FONT_SIZE} - canIncrease={appearance.codeFontSize < MAX_CODE_FONT_SIZE} - disabled={!isReady || !custom} - icon="textformat.size" - label="Font size" - onDecrease={handleDecrease} - onIncrease={handleIncrease} - valueLabel={`${appearance.codeFontSize} pt`} - /> + {custom ? ( + + ) : null} { - setTerminalFontSize(stepTerminalFontSize(appearance.terminalFontSize, -1)); - }, [appearance.terminalFontSize, setTerminalFontSize]); - - const handleIncrease = useCallback(() => { - setTerminalFontSize(stepTerminalFontSize(appearance.terminalFontSize, 1)); - }, [appearance.terminalFontSize, setTerminalFontSize]); - return ( + + - MIN_TERMINAL_FONT_SIZE} - canIncrease={appearance.terminalFontSize < MAX_TERMINAL_FONT_SIZE} - disabled={!isReady || !custom} - icon="textformat.size" - label="Font size" - onDecrease={handleDecrease} - onIncrease={handleIncrease} - valueLabel={`${appearance.terminalFontSize.toFixed(1)} pt`} - /> + {custom ? ( + + ) : null} ); } diff --git a/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx b/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx index e7c85cce51a..c1424088f8d 100644 --- a/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx +++ b/apps/mobile/src/features/settings/appearance/sections/TextAppearanceSection.tsx @@ -1,35 +1,32 @@ -import { useCallback } from "react"; - import { + BASE_FONT_SIZE_STEP, MAX_BASE_FONT_SIZE, MIN_BASE_FONT_SIZE, - stepBaseFontSize, } from "../../../../lib/appearancePreferences"; import { SettingsSection } from "../../components/SettingsSection"; import { useAppearancePreferences } from "../AppearancePreferencesProvider"; -import { FontSizeControlRow } from "../components/FontSizeControlRow"; +import { + AppearancePreviewSeparator, + TextAppearancePreview, +} from "../components/AppearancePreviews"; +import { FontSizeSliderRow } from "../components/FontSizeSliderRow"; export function TextAppearanceSection() { const { isReady, appearance, setBaseFontSize } = useAppearancePreferences(); - const handleDecrease = useCallback(() => { - setBaseFontSize(stepBaseFontSize(appearance.baseFontSize, -1)); - }, [appearance.baseFontSize, setBaseFontSize]); - - const handleIncrease = useCallback(() => { - setBaseFontSize(stepBaseFontSize(appearance.baseFontSize, 1)); - }, [appearance.baseFontSize, setBaseFontSize]); - return ( - MIN_BASE_FONT_SIZE} - canIncrease={appearance.baseFontSize < MAX_BASE_FONT_SIZE} + + + diff --git a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx index aa89bcaf70c..ad174a2502d 100644 --- a/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx +++ b/apps/mobile/src/features/terminal/NativeTerminalSurface.tsx @@ -12,7 +12,10 @@ import { import { AppText as Text } from "../../components/AppText"; import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; -import { resolveNativeTerminalSurfaceView } from "./nativeTerminalModule"; +import { + getNativeTerminalHardwareKeyRevision, + resolveNativeTerminalSurfaceView, +} from "./nativeTerminalModule"; import { buildGhosttyThemeConfig, getPierreTerminalTheme, @@ -189,6 +192,8 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf terminalDebugLog("native:surface", { terminalKey: props.terminalKey, native: hasNativeSurface, + // null = installed binary predates native hardware-key handling (rebuild needed). + hardwareKeyRevision: getNativeTerminalHardwareKeyRevision(), bufferLen: props.buffer.length, isRunning: props.isRunning, }); @@ -198,6 +203,9 @@ export const TerminalSurface = memo(function TerminalSurface(props: TerminalSurf if (!props.isRunning) { return; } + terminalDebugLog("native:onInput", { + codes: Array.from(event.nativeEvent.data, (char) => char.codePointAt(0)), + }); onInput(event.nativeEvent.data); }, [onInput, props.isRunning], diff --git a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx index 5614b2c8895..6234c40a31f 100644 --- a/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx +++ b/apps/mobile/src/features/terminal/ThreadTerminalRouteScreen.tsx @@ -4,7 +4,7 @@ import { SymbolView } from "expo-symbols"; import { NativeHeaderToolbar, NativeStackScreenOptions } from "../../native/StackHeader"; import { StackActions, useNavigation, type StaticScreenProps } from "@react-navigation/native"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Platform, Pressable, Text as RNText, View, useColorScheme } from "react-native"; +import { Platform, Pressable, View, useColorScheme } from "react-native"; import { KeyboardController, KeyboardEvents, @@ -25,7 +25,6 @@ import { useEnvironmentPresentation } from "../../state/presentation"; import { terminalEnvironment } from "../../state/terminal"; import { useAtomCommand } from "../../state/use-atom-command"; import { useWorkspaceState } from "../../state/workspace"; -import { MOBILE_TYPOGRAPHY } from "../../lib/typography"; import { MAX_TERMINAL_FONT_SIZE, MIN_TERMINAL_FONT_SIZE, @@ -81,47 +80,6 @@ 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; @@ -447,38 +405,7 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) const usesNativeHeaderGlass = Platform.OS === "ios"; const pendingModifier = pendingModifierState.terminalId === terminalId ? pendingModifierState.value : null; - const headerTitle = useMemo(() => { - const topLineParts = [ - selectedEnvironmentConnection?.environmentLabel ?? null, - selectedThreadProject?.title ?? null, - ].filter((value): value is string => Boolean(value)); - - return { - topLine: topLineParts.join(" \u00b7 "), - bottomLine: cwd ?? selectedThreadProject?.workspaceRoot ?? "", - }; - }, [ - cwd, - selectedEnvironmentConnection?.environmentLabel, - selectedThreadProject?.title, - selectedThreadProject?.workspaceRoot, - ]); - const renderTerminalHeaderTitle = useCallback( - () => ( - - ), - [ - headerTitle.bottomLine, - headerTitle.topLine, - terminalTheme.foreground, - terminalTheme.mutedForeground, - ], - ); + const headerSubtitle = selectedThreadProject?.title ?? ""; const terminalToolbarActions = useMemo>(() => { const modifierActions: ReadonlyArray = hostPlatform === "mac" @@ -929,16 +856,11 @@ export function ThreadTerminalRouteScreen(props: ThreadTerminalRouteScreenProps) 0 - ? headerTitle.bottomLine - : undefined, + usesNativeHeaderGlass && headerSubtitle.length > 0 ? headerSubtitle : undefined, }} /> diff --git a/apps/mobile/src/features/terminal/nativeTerminalModule.ts b/apps/mobile/src/features/terminal/nativeTerminalModule.ts index 25797dcbac5..120b7962f2c 100644 --- a/apps/mobile/src/features/terminal/nativeTerminalModule.ts +++ b/apps/mobile/src/features/terminal/nativeTerminalModule.ts @@ -1,6 +1,6 @@ import type { ComponentType } from "react"; import type { NativeSyntheticEvent, ViewProps } from "react-native"; -import { requireNativeView } from "expo"; +import { requireNativeView, requireOptionalNativeModule } from "expo"; import { NativeViewResolutionError } from "../../native/nativeViewResolutionError"; @@ -75,6 +75,25 @@ export function resolveNativeTerminalSurfaceView(): ComponentType( + NATIVE_TERMINAL_MODULE_NAME, + ); + return module?.hardwareKeyRevision ?? null; + } catch { + return null; + } +} + export function hasNativeTerminalSurface() { return resolveNativeTerminalSurfaceView() !== null; } diff --git a/apps/mobile/src/features/threads/ThreadGitControls.tsx b/apps/mobile/src/features/threads/ThreadGitControls.tsx index a1f4190adb9..31b65f49353 100644 --- a/apps/mobile/src/features/threads/ThreadGitControls.tsx +++ b/apps/mobile/src/features/threads/ThreadGitControls.tsx @@ -77,32 +77,36 @@ type QuickActionIcon = | "checkmark.circle" | "arrow.up.circle"; -type ThreadGitControlsProps = { +/** The subset of git-control wiring the standalone git menu needs. */ +export type ThreadGitMenuProps = { readonly environmentId: EnvironmentId | string; readonly threadId: ThreadId | string; + readonly currentBranch: string | null; + readonly gitStatus: VcsStatusResult | null; + readonly gitOperationLabel: string | null; + readonly onOpenFilesInspector?: () => void; + readonly onOpenGitInspector?: () => void; + readonly onPull: () => Promise; + readonly onRunAction: (input: GitActionRequestInput) => Promise; +}; + +type ThreadGitControlsProps = ThreadGitMenuProps & { readonly auxiliaryPaneControl?: { readonly accessibilityLabel: string; readonly onPress: () => void; }; - readonly currentBranch: string | null; - readonly gitStatus: VcsStatusResult | null; - readonly gitOperationLabel: string | null; readonly canOpenTerminal: boolean; readonly canOpenFiles: boolean; readonly projectScripts: ReadonlyArray; readonly terminalSessions: ReadonlyArray; readonly showActionControls?: boolean; readonly showDirectFileControl?: boolean; - readonly onOpenFilesInspector?: () => void; - readonly onOpenGitInspector?: () => void; readonly onOpenTerminal: (terminalId?: string | null) => void; readonly onOpenNewTerminal: () => void; readonly onRunProjectScript: (script: ProjectScript) => Promise; - readonly onPull: () => Promise; - readonly onRunAction: (input: GitActionRequestInput) => Promise; }; -function useThreadGitControlModel(props: ThreadGitControlsProps) { +function useThreadGitControlModel(props: ThreadGitMenuProps) { const navigation = useNavigation(); const environmentId = props.environmentId; const threadId = props.threadId; @@ -485,43 +489,54 @@ export function ThreadGitControls(props: ThreadGitControlsProps) { separateBackground /> ) : null} - {showActionControls ? ( - - {}} - subtitle={compactMenuStatus(props.gitStatus)} - > - - {compactMenuBranchLabel(model.currentBranchLabel)} - - - void model.runQuickAction()} - subtitle={model.quickActionHint ?? undefined} - > - {model.quickAction.label} - - - Review changes - - - More - - - ) : null} + {showActionControls ? : null} ); } + +/** + * The standalone git actions menu (branch status, quick commit/push action, + * review, more). Rendered inside a NativeHeaderToolbar by both the thread + * chat header and the review screen's toolbar. + */ +export function ThreadGitMenu(props: ThreadGitMenuProps) { + const model = useThreadGitControlModel(props); + + return ( + + {}} + subtitle={compactMenuStatus(props.gitStatus)} + > + + {compactMenuBranchLabel(model.currentBranchLabel)} + + + void model.runQuickAction()} + subtitle={model.quickActionHint ?? undefined} + > + {model.quickAction.label} + + + Review changes + + + More + + + ); +} diff --git a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx index 368848ffba8..bf165715d01 100644 --- a/apps/mobile/src/features/threads/ThreadRouteScreen.tsx +++ b/apps/mobile/src/features/threads/ThreadRouteScreen.tsx @@ -10,6 +10,7 @@ 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 { useSafeAreaInsets } from "react-native-safe-area-context"; import { useWorkspaceState } from "../../state/workspace"; import { useThemeColor } from "../../lib/useThemeColor"; import { useEnvironmentQuery } from "../../state/query"; @@ -57,10 +58,10 @@ 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 { AdaptiveInspectorLayout } from "../layout/adaptive-inspector-layout"; import { useAdaptiveWorkspaceLayout, useAdaptiveWorkspacePaneRole, + useRegisterWorkspaceInspector, } from "../layout/AdaptiveWorkspaceLayout"; import { withNativeGlassHeaderItem } from "../layout/native-glass-header-items"; import { ThreadFileNavigatorPane } from "../files/thread-file-navigator-pane"; @@ -394,15 +395,20 @@ function ThreadRouteContent( }, [fileInspector.supported, navigation, selectedThread], ); + // The workspace inspector column spans the full window height. On iOS the + // panes bring their own nested native headers (which underlap the status + // bar); elsewhere the pane content pads itself below the top inset. + const safeAreaInsets = useSafeAreaInsets(); + const inspectorHeaderInset = Platform.OS === "ios" ? 0 : safeAreaInsets.top; const GitInspector = useCallback( () => ( ), - [props.route.params], + [inspectorHeaderInset, props.route.params], ); const FilesInspector = useCallback( () => @@ -410,15 +416,24 @@ function ThreadRouteContent( ) : null, - [handleSelectInspectorFile, selectedThread, selectedThreadCwd, selectedThreadProject?.title], + [ + handleSelectInspectorFile, + inspectorHeaderInset, + selectedThread, + selectedThreadCwd, + selectedThreadProject?.title, + ], + ); + const RouteInspector = useCallback( + () => props.renderInspector?.(inspectorHeaderInset), + [inspectorHeaderInset, props.renderInspector], ); - const RouteInspector = useCallback(() => props.renderInspector?.(0), [props.renderInspector]); const renderInspectorStack = useCallback( () => inspectorMode === null ? null : ( @@ -432,6 +447,10 @@ function ThreadRouteContent( [FilesInspector, GitInspector, RouteInspector, inspectorMode, props.renderInspector], ); const activeInspectorRenderer = inspectorMode === null ? undefined : renderInspectorStack; + // Hand the inspector to the workspace so it renders beside the navigator, + // outside this screen's native header — the terminal/git/files toolbar + // stays anchored to the chat pane instead of floating above the inspector. + useRegisterWorkspaceInspector(activeInspectorRenderer); const handleOpenConnectionEditor = useCallback(() => { void navigation.navigate("Connections"); @@ -659,70 +678,68 @@ function ThreadRouteContent( - - - + + - {layout.usesSplitView ? null : ( - setDrawerVisible(false)} - onSelectThread={(thread) => { - navigation.dispatch( - StackActions.replace("Thread", { - environmentId: String(thread.environmentId), - threadId: String(thread.id), - }), - ); - }} - onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} - /> - )} - - + {layout.usesSplitView ? null : ( + setDrawerVisible(false)} + onSelectThread={(thread) => { + navigation.dispatch( + StackActions.replace("Thread", { + environmentId: String(thread.environmentId), + threadId: String(thread.id), + }), + ); + }} + onStartNewTask={() => navigation.navigate("NewTaskSheet", { screen: "NewTask" })} + /> + )} + ); From 9f54b06227c39eacbbf9dd82c65ab43cf02e9c21 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 21:19:35 -0700 Subject: [PATCH 79/81] chore: sync lockfile libc metadata after rebase onto main Co-Authored-By: Claude Fable 5 --- pnpm-lock.yaml | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0f87561801c..e91e824770b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -935,21 +935,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==} @@ -1001,24 +1005,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==} @@ -1607,21 +1615,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==} @@ -2607,21 +2619,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==} @@ -2708,89 +2724,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==} @@ -3249,48 +3281,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==} @@ -3393,48 +3433,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==} @@ -3956,108 +4004,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==} @@ -4194,66 +4260,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==} @@ -4488,48 +4567,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==} @@ -5002,24 +5089,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==} @@ -5102,24 +5193,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==} @@ -7498,72 +7593,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==} From 8a0c8f8fe8f16fa643e396e6b837e9c528d0ebe2 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 22:27:30 -0700 Subject: [PATCH 80/81] Improve mobile header layout on iPad - Adjust react-native-screens patch for toolbar and header sizing - Update lockfile to use the new patch hash --- patches/react-native-screens@4.25.2.patch | 69 +++++++++++++---------- pnpm-lock.yaml | 20 +++---- 2 files changed, 49 insertions(+), 40 deletions(-) diff --git a/patches/react-native-screens@4.25.2.patch b/patches/react-native-screens@4.25.2.patch index 746954492ca..48457c10a0a 100644 --- a/patches/react-native-screens@4.25.2.patch +++ b/patches/react-native-screens@4.25.2.patch @@ -1,5 +1,5 @@ diff --git a/ios/RNSBarButtonItem.h b/ios/RNSBarButtonItem.h -index ea5325e..acd2fd7 100644 +index ea5325ea8d17b1ddfa790ff8dab48ce83142d4d3..acd2fd7ceb7162ed300abc4d3c9f0c24f4d63898 100644 --- a/ios/RNSBarButtonItem.h +++ b/ios/RNSBarButtonItem.h @@ -13,4 +13,8 @@ typedef void (^RNSBarButtonMenuItemAction)(NSString *menuId); @@ -12,7 +12,7 @@ index ea5325e..acd2fd7 100644 + @end diff --git a/ios/RNSBarButtonItem.mm b/ios/RNSBarButtonItem.mm -index 0eb1f09..73298f1 100644 +index 0eb1f09dee82edd99e3e614938233db5cdeaebfe..73298f10807520970f625e166d7f5dd1338206f1 100644 --- a/ios/RNSBarButtonItem.mm +++ b/ios/RNSBarButtonItem.mm @@ -110,6 +110,58 @@ - (instancetype)initWithConfig:(NSDictionary *)dict @@ -75,7 +75,7 @@ index 0eb1f09..73298f1 100644 #if !TARGET_OS_TV || __TV_OS_VERSION_MAX_ALLOWED >= 170000 if (@available(tvOS 17.0, *)) { diff --git a/ios/RNSScreenStackHeaderConfig.h b/ios/RNSScreenStackHeaderConfig.h -index 919b984..5bb0cd6 100644 +index 919b984edc9f91ee9ac26faf257d8a721e26457c..5bb0cd6736ed6bc51db57e2a9326f758f22c51d8 100644 --- a/ios/RNSScreenStackHeaderConfig.h +++ b/ios/RNSScreenStackHeaderConfig.h @@ -21,6 +21,8 @@ @@ -101,7 +101,7 @@ index 919b984..5bb0cd6 100644 NS_ASSUME_NONNULL_END diff --git a/ios/RNSScreenStackHeaderConfig.mm b/ios/RNSScreenStackHeaderConfig.mm -index 5970e3e..cb050f9 100644 +index 5970e3e3a624b9498b8dedfc16831df03a274d0c..073a72a68688369f39e54b61d1381b8d652cbb83 100644 --- a/ios/RNSScreenStackHeaderConfig.mm +++ b/ios/RNSScreenStackHeaderConfig.mm @@ -30,6 +30,20 @@ @@ -174,7 +174,7 @@ index 5970e3e..cb050f9 100644 // appearance does not apply to the tvOS so we need to use lagacy customization #if TARGET_OS_TV -@@ -637,10 +667,270 @@ + (void)updateViewController:(UIViewController *)vc +@@ -637,10 +667,286 @@ + (void)updateViewController:(UIViewController *)vc // 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; @@ -267,9 +267,25 @@ index 5970e3e..cb050f9 100644 + toolbarHost.tag = RNSMailSearchToolbarViewTag; + toolbarHost.translatesAutoresizingMaskIntoConstraints = NO; + [chromeHostView addSubview:toolbarHost]; ++ // Keyboard avoidance is best-effort: on the iOS 27 beta the keyboard ++ // layout guide no longer rests at the bottom safe-area edge while the ++ // keyboard is hidden, which pushed the toolbar offscreen. The required ++ // resting position is the safe area; the keyboard guide only pulls the ++ // toolbar up when it actually tracks a visible keyboard. ++ NSLayoutConstraint *keyboardAvoidConstraint = ++ [toolbarHost.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor ++ constant:-toolbarBottomSpacing]; ++ keyboardAvoidConstraint.priority = UILayoutPriorityDefaultHigh; ++ NSLayoutConstraint *restingBottomConstraint = ++ [toolbarHost.bottomAnchor constraintEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor ++ constant:-toolbarBottomSpacing]; ++ restingBottomConstraint.priority = UILayoutPriorityDefaultLow; + [NSLayoutConstraint activateConstraints:@[ + [toolbarHost.centerXAnchor constraintEqualToAnchor:chromeHostView.centerXAnchor], -+ [toolbarHost.bottomAnchor constraintEqualToAnchor:keyboardLayoutGuide.topAnchor constant:-toolbarBottomSpacing], ++ [toolbarHost.bottomAnchor constraintLessThanOrEqualToAnchor:chromeHostView.safeAreaLayoutGuide.bottomAnchor ++ constant:-toolbarBottomSpacing], ++ keyboardAvoidConstraint, ++ restingBottomConstraint, + [toolbarHost.widthAnchor constraintEqualToConstant:resolvedWidth], + [toolbarHost.heightAnchor constraintEqualToConstant:toolbarHeight], + ]]; @@ -449,7 +465,7 @@ index 5970e3e..cb050f9 100644 // 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 +1063,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * +@@ -773,6 +1079,7 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * - (NSArray *)barButtonItemsFromConfigs:(NSArray *> *)dicts withCurrentItems:(NSArray *)currentItems @@ -457,7 +473,7 @@ index 5970e3e..cb050f9 100644 { if (dicts.count == 0) { return currentItems; -@@ -781,7 +1072,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * +@@ -781,7 +1088,197 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * [items addObjectsFromArray:currentItems]; for (NSUInteger i = 0; i < dicts.count; i++) { NSDictionary *dict = dicts[i]; @@ -656,7 +672,7 @@ index 5970e3e..cb050f9 100644 RNSBarButtonItem *item = [[RNSBarButtonItem alloc] initWithConfig:dict action:^(NSString *buttonId) { auto eventEmitter = std::static_pointer_cast( -@@ -809,11 +1290,15 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * +@@ -809,11 +1306,15 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * [items addObject:item]; } } else if (dict[@"spacing"]) { @@ -676,7 +692,7 @@ index 5970e3e..cb050f9 100644 NSNumber *index = dict[@"index"]; if (index.integerValue < items.count) { [items insertObject:item atIndex:index.integerValue]; -@@ -825,6 +1310,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * +@@ -825,6 +1326,47 @@ - (void)applySemanticContentAttributeIfNeededToNavCtrl:(UINavigationController * return items; } @@ -724,7 +740,7 @@ index 5970e3e..cb050f9 100644 RNS_IGNORE_SUPER_CALL_BEGIN - (void)insertReactSubview:(RNSScreenStackHeaderSubview *)subview atIndex:(NSInteger)atIndex { -@@ -1013,6 +1539,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: +@@ -1013,6 +1555,8 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: } _title = RCTNSStringFromStringNilIfEmpty(newScreenProps.title); @@ -733,7 +749,7 @@ index 5970e3e..cb050f9 100644 if (newScreenProps.titleFontFamily != oldScreenProps.titleFontFamily) { _titleFontFamily = RCTNSStringFromStringNilIfEmpty(newScreenProps.titleFontFamily); } -@@ -1038,6 +1566,7 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: +@@ -1038,6 +1582,7 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: _disableBackButtonMenu = newScreenProps.disableBackButtonMenu; _backButtonDisplayMode = [RNSConvert UINavigationItemBackButtonDisplayModeFromCppEquivalent:newScreenProps.backButtonDisplayMode]; @@ -741,7 +757,7 @@ index 5970e3e..cb050f9 100644 if (newScreenProps.userInterfaceStyle != oldScreenProps.userInterfaceStyle) { _userInterfaceStyle = [RNSConvert UIUserInterfaceStyleFromCppEquivalent:newScreenProps.userInterfaceStyle]; -@@ -1084,6 +1613,30 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: +@@ -1084,6 +1629,30 @@ - (void)updateProps:(react::Props::Shared const &)props oldProps:(react::Props:: _headerRightBarButtonItems = array; } @@ -773,7 +789,7 @@ index 5970e3e..cb050f9 100644 if (needsNavigationControllerLayout) { diff --git a/lib/commonjs/components/ScreenStackHeaderConfig.js b/lib/commonjs/components/ScreenStackHeaderConfig.js -index 16b979b..01415d4 100644 +index 16b979bb3dfb41ff247403f1632c300a9a60d549..01415d4cba66086a8d8e5af728f809aee5190d91 100644 --- a/lib/commonjs/components/ScreenStackHeaderConfig.js +++ b/lib/commonjs/components/ScreenStackHeaderConfig.js @@ -23,18 +23,42 @@ const ScreenStackHeaderConfig = exports.ScreenStackHeaderConfig = /*#__PURE__*/_ @@ -857,7 +873,7 @@ index 16b979b..01415d4 100644 onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, ref: ref, diff --git a/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js b/lib/commonjs/components/helpers/prepareHeaderBarButtonItems.js -index ab93f62..b5ea122 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 = '') => { @@ -894,7 +910,7 @@ index ab93f62..b5ea122 100644 if (item.icon?.type === 'imageSource') { imageSource = _reactNative.Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/module/components/ScreenStackHeaderConfig.js b/lib/module/components/ScreenStackHeaderConfig.js -index cf15f36..dd66167 100644 +index cf15f36f25e51d95c896fbc3a31ccba90156a5b6..dd66167c4ab0e5640ad27400e08dcfb4d20dce2a 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 @@ -978,7 +994,7 @@ index cf15f36..dd66167 100644 onPressHeaderBarButtonMenuItem: onPressHeaderBarButtonMenuItem, ref: ref, diff --git a/lib/module/components/helpers/prepareHeaderBarButtonItems.js b/lib/module/components/helpers/prepareHeaderBarButtonItems.js -index 8a70ffd..8dbd0ba 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 = '') => { @@ -1015,7 +1031,7 @@ index 8a70ffd..8dbd0ba 100644 if (item.icon?.type === 'imageSource') { imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts -index b7568ec..41ca1f2 100644 +index b7568ecfebd4f4420f2a8d3fec5184d7fc728dd1..41ca1f273f0175929e73105faa463978323837e3 100644 --- a/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts +++ b/lib/typescript/fabric/ScreenStackHeaderConfigNativeComponent.d.ts @@ -9,6 +9,7 @@ type OnPressHeaderBarButtonMenuItemEvent = Readonly<{ @@ -1051,7 +1067,7 @@ index b7568ec..41ca1f2 100644 onPressHeaderBarButtonMenuItem?: CT.DirectEventHandler | undefined; synchronousShadowStateUpdatesEnabled?: CT.WithDefault; diff --git a/lib/typescript/types.d.ts b/lib/typescript/types.d.ts -index 3b384e0..1e3a966 100644 +index 3b384e03891e38e936f370372a682d73440e7ec2..1e3a966806c8b8b312f75fb367f758efe49dde9d 100644 --- a/lib/typescript/types.d.ts +++ b/lib/typescript/types.d.ts @@ -10,6 +10,7 @@ export type SearchBarCommands = { @@ -1176,15 +1192,8 @@ index 3b384e0..1e3a966 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 +index 421b3c2545426ae957271bd6515bcf827437541c..0ca6f1d7f324eaf257891a3bbb0aab210aa30f6f 100644 --- a/src/components/ScreenStackHeaderConfig.tsx +++ b/src/components/ScreenStackHeaderConfig.tsx @@ -39,7 +39,12 @@ export const ScreenStackHeaderConfig = React.forwardRef< @@ -1311,7 +1320,7 @@ index 421b3c2..0ca6f1d 100644 onPressHeaderBarButtonMenuItem={onPressHeaderBarButtonMenuItem} ref={ref} diff --git a/src/components/helpers/prepareHeaderBarButtonItems.ts b/src/components/helpers/prepareHeaderBarButtonItems.ts -index be2c24d..0b038d2 100644 +index be2c24dfee77f5883b5ab1d7473be80933b5dc6f..0b038d2005b2d51bbb2ab1d7dba7eaccda83d42f 100644 --- a/src/components/helpers/prepareHeaderBarButtonItems.ts +++ b/src/components/helpers/prepareHeaderBarButtonItems.ts @@ -50,13 +50,43 @@ const prepareMenu = ( @@ -1361,7 +1370,7 @@ index be2c24d..0b038d2 100644 if (item.icon?.type === 'imageSource') { imageSource = Image.resolveAssetSource(item.icon.imageSource); diff --git a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts -index ba2479d..8677583 100644 +index ba2479de0f49c112470f78c5084059ddd9e2f3e8..8677583a8f8daa4839340467466bfab8d73111ab 100644 --- a/src/fabric/ScreenStackHeaderConfigNativeComponent.ts +++ b/src/fabric/ScreenStackHeaderConfigNativeComponent.ts @@ -14,6 +14,7 @@ type OnPressHeaderBarButtonItemEvent = Readonly<{ buttonId: string }>; @@ -1398,7 +1407,7 @@ index ba2479d..8677583 100644 | CT.DirectEventHandler | undefined; diff --git a/src/types.tsx b/src/types.tsx -index 76a83f3..9e4499f 100644 +index 76a83f3acb6fd3f0af7f027798848b7124100286..9e4499f076f9988e3266df4be7201e131d21242b 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 e91e824770b..e54d6056d48 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -97,7 +97,7 @@ patchedDependencies: hash: 825622aae63a8fb5b904f3c77908a0e216261d727ea171709f2c0b6088422675 path: patches/react-native-nitro-modules@0.35.9.patch react-native-screens@4.25.2: - hash: 7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3 + hash: e1b49230abc20a663115cc3f34ca2f63764a873485ccb67f2bd43b85ce2a94dc path: patches/react-native-screens@4.25.2.patch importers: @@ -246,7 +246,7 @@ importers: 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))(@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(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(d5eb07f8f6ba8c11ae9315cbbd4a373a) + version: 7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(889fbbb381dfeb79ac64b7284ebf7fa4) '@shikijs/core': specifier: 4.2.0 version: 4.2.0 @@ -393,7 +393,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(patch_hash=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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=e1b49230abc20a663115cc3f34ca2f63764a873485ccb67f2bd43b85ce2a94dc)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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) @@ -12072,7 +12072,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(edfa63c4d244c7ff5b11fa13f7e1f1c9) + expo-router: 56.2.11(3af01b44e50aae5a8ae60852f3cf7e3f) react-native: 0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@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' @@ -12370,7 +12370,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(edfa63c4d244c7ff5b11fa13f7e1f1c9) + expo-router: 56.2.11(3af01b44e50aae5a8ae60852f3cf7e3f) react-dom: 19.2.3(react@19.2.3) transitivePeerDependencies: - supports-color @@ -13665,7 +13665,7 @@ snapshots: 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))(@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(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(d5eb07f8f6ba8c11ae9315cbbd4a373a)': + '@react-navigation/native-stack@7.17.6(patch_hash=2d7fec7933e324ccf082b1ae0df3907c35d16d48c117d628e27d9c3921c26bca)(889fbbb381dfeb79ac64b7284ebf7fa4)': dependencies: '@react-navigation/elements': 2.9.26(@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-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))(@types/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@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@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': 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))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) @@ -13673,7 +13673,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-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=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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=e1b49230abc20a663115cc3f34ca2f63764a873485ccb67f2bd43b85ce2a94dc)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@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: @@ -16347,7 +16347,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(edfa63c4d244c7ff5b11fa13f7e1f1c9): + expo-router@56.2.11(3af01b44e50aae5a8ae60852f3cf7e3f): 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) @@ -16378,7 +16378,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(patch_hash=808eb26f9e57cf4945efd3985af4d9c764da6f91f4c9764433cc868602bbf4d3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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=e1b49230abc20a663115cc3f34ca2f63764a873485ccb67f2bd43b85ce2a94dc)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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 @@ -18990,7 +18990,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(patch_hash=7648b38a581227ad4bb8ad7e1eefa5ac4032e114cf82004aa6339c8a40fd7ca3)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7))(@types/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=e1b49230abc20a663115cc3f34ca2f63764a873485ccb67f2bd43b85ce2a94dc)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.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) From d4c2133624fedac51c88980be5bcff0ed5a9cc17 Mon Sep 17 00:00:00 2001 From: Julius Marminge Date: Thu, 2 Jul 2026 22:28:54 -0700 Subject: [PATCH 81/81] Refine mobile thread layout for iPad responsiveness - Improve sidebar and home list spacing on larger mobile layouts - Clean up related formatting and workspace catalog quotes --- apps/mobile/src/features/home/HomeScreen.tsx | 87 +++++---- .../features/home/thread-swipe-actions.tsx | 8 +- .../SettingsEnvironmentsRouteScreen.tsx | 4 +- .../src/features/threads/ThreadFeed.tsx | 6 +- .../threads/ThreadNavigationSidebar.tsx | 140 +++++++-------- .../features/threads/thread-list-items.tsx | 166 +++++++++--------- .../src/features/threads/thread-work-log.tsx | 5 +- pnpm-workspace.yaml | 116 ++++++------ 8 files changed, 260 insertions(+), 272 deletions(-) diff --git a/apps/mobile/src/features/home/HomeScreen.tsx b/apps/mobile/src/features/home/HomeScreen.tsx index 1558980c88c..5418fc9596a 100644 --- a/apps/mobile/src/features/home/HomeScreen.tsx +++ b/apps/mobile/src/features/home/HomeScreen.tsx @@ -153,19 +153,16 @@ export function HomeScreen(props: HomeScreenProps) { const insets = useSafeAreaInsets(); const accentColor = useThemeColor("--color-icon-muted"); - const updateGroupDisplay = useCallback( - (key: string, action: HomeGroupDisplayAction) => { - setGroupDisplayStates((previous) => { - const next = new Map(previous); - next.set( - key, - nextGroupDisplayState(previous.get(key) ?? DEFAULT_GROUP_DISPLAY_STATE, action), - ); - return next; - }); - }, - [], - ); + const updateGroupDisplay = useCallback((key: string, action: HomeGroupDisplayAction) => { + setGroupDisplayStates((previous) => { + const next = new Map(previous); + next.set( + key, + nextGroupDisplayState(previous.get(key) ?? DEFAULT_GROUP_DISPLAY_STATE, action), + ); + return next; + }); + }, []); const handleSwipeableWillOpen = useCallback((methods: SwipeableMethods) => { if (openSwipeableRef.current !== methods) { @@ -386,38 +383,38 @@ export function HomeScreen(props: HomeScreenProps) { collapse/expand data changes. The flattened layout still exposes `stickyHeaderIndices` if this gets revisited. */} - + {connectionStatus} diff --git a/apps/mobile/src/features/home/thread-swipe-actions.tsx b/apps/mobile/src/features/home/thread-swipe-actions.tsx index 791bf906cb6..dd412301077 100644 --- a/apps/mobile/src/features/home/thread-swipe-actions.tsx +++ b/apps/mobile/src/features/home/thread-swipe-actions.tsx @@ -10,7 +10,13 @@ import { type ComponentProps, type ReactNode, } from "react"; -import type { ColorValue, NativeScrollEvent, NativeSyntheticEvent, StyleProp, ViewStyle } from "react-native"; +import type { + ColorValue, + NativeScrollEvent, + NativeSyntheticEvent, + StyleProp, + ViewStyle, +} from "react-native"; import { Pressable, View } from "react-native"; import ReanimatedSwipeable, { type SwipeableMethods, diff --git a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx index 621da8f34a7..c8861ec75a1 100644 --- a/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx +++ b/apps/mobile/src/features/settings/SettingsEnvironmentsRouteScreen.tsx @@ -211,9 +211,7 @@ function ConfiguredCloudEnvironmentRows(props: { Could not load T3 Cloud environments - - {controller.relayDiscovery.error} - + {controller.relayDiscovery.error} {controller.relayDiscovery.errorTraceId ? ( ) : null} diff --git a/apps/mobile/src/features/threads/ThreadFeed.tsx b/apps/mobile/src/features/threads/ThreadFeed.tsx index 0abfb94feb1..443a924bd2d 100644 --- a/apps/mobile/src/features/threads/ThreadFeed.tsx +++ b/apps/mobile/src/features/threads/ThreadFeed.tsx @@ -1068,11 +1068,7 @@ const ReviewCommentCard = memo(function ReviewCommentCard(props: { ) : null} {props.comment.text.length > 0 ? ( - + {props.comment.text} diff --git a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx index 8f3ef98bdaf..21a4851e440 100644 --- a/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx +++ b/apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx @@ -4,11 +4,7 @@ import { LegendList } from "@legendapp/list/react-native"; import type { MenuAction } from "@react-native-menu/menu"; import { SymbolView } from "expo-symbols"; import { useCallback, useMemo, useRef, useState, type ReactNode } from "react"; -import type { - LayoutChangeEvent, - NativeScrollEvent, - NativeSyntheticEvent, -} from "react-native"; +import type { LayoutChangeEvent, NativeScrollEvent, NativeSyntheticEvent } from "react-native"; import { Platform, StyleSheet, TextInput, View, useColorScheme } from "react-native"; import { Gesture, GestureDetector } from "react-native-gesture-handler"; import type { SwipeableMethods } from "react-native-gesture-handler/ReanimatedSwipeable"; @@ -51,11 +47,7 @@ import { SidebarHeaderActions } from "./sidebar-header-actions"; import { SidebarFilterButton } from "./sidebar-filter-button"; import { createSidebarHeaderItems } from "./sidebar-native-header-items"; import { SidebarNavigationShell } from "./sidebar-navigation-shell"; -import { - ThreadListGroupHeader, - ThreadListRow, - ThreadListShowMoreRow, -} from "./thread-list-items"; +import { ThreadListGroupHeader, ThreadListRow, ThreadListShowMoreRow } from "./thread-list-items"; /** * Shared capsule behind the sidebar header buttons — a native liquid-glass @@ -540,6 +532,67 @@ function ThreadNavigationSidebarPane( /> + + item.type} + itemsAreEqual={homeListItemsAreEqual} + keyExtractor={(item) => item.key} + renderItem={renderListItem} + automaticallyAdjustsScrollIndicatorInsets + contentInsetAdjustmentBehavior="automatic" + contentContainerStyle={[ + styles.threadListContent, + { + paddingBottom: Math.max(insets.bottom, 16) + 16, + paddingTop: 6, + }, + ]} + keyboardDismissMode="on-drag" + keyboardShouldPersistTaps="handled" + {...scrollGateHandlers} + recycleItems + scrollEventThrottle={16} + showsVerticalScrollIndicator={false} + style={styles.threadList} + ListHeaderComponent={ + showsConnectionStatus ? ( + + + + ) : null + } + ListEmptyComponent={listEmpty} + /> + + + + + ); + } + + return ( + + + item.key} renderItem={renderListItem} - automaticallyAdjustsScrollIndicatorInsets - contentInsetAdjustmentBehavior="automatic" contentContainerStyle={[ styles.threadListContent, { - paddingBottom: Math.max(insets.bottom, 16) + 16, - paddingTop: 6, + paddingBottom: 16 + insets.bottom, + paddingTop: topListInset, }, ]} keyboardDismissMode="on-drag" @@ -566,69 +617,10 @@ function ThreadNavigationSidebarPane( scrollEventThrottle={16} showsVerticalScrollIndicator={false} style={styles.threadList} - ListHeaderComponent={ - showsConnectionStatus ? ( - - - - ) : null - } ListEmptyComponent={listEmpty} /> - - - - ); - } - - return ( - - - - - item.type} - itemsAreEqual={homeListItemsAreEqual} - keyExtractor={(item) => item.key} - renderItem={renderListItem} - contentContainerStyle={[ - styles.threadListContent, - { - paddingBottom: 16 + insets.bottom, - paddingTop: topListInset, - }, - ]} - keyboardDismissMode="on-drag" - keyboardShouldPersistTaps="handled" - {...scrollGateHandlers} - recycleItems - scrollEventThrottle={16} - showsVerticalScrollIndicator={false} - style={styles.threadList} - ListEmptyComponent={listEmpty} - /> - - + void) => compact ? ( - { - close(); - onSelectThread(thread); - }} - style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} - > - { + close(); + onSelectThread(thread); }} + style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} > + + + + {thread.title} + + + {statusPill} + + {timestamp} + + + + + {subtitleRow} + + + + ) : ( + setHovered(true)} + onHoverOut={() => setHovered(false)} + onPress={() => { + close(); + onSelectThread(thread); + }} + style={({ pressed }) => ({ + backgroundColor: selected + ? selectedBackgroundColor + : pressed || hovered + ? effectivePressedBackground + : backgroundColor, + borderRadius: SIDEBAR_ROW_RADIUS, + cursor: "pointer", + minHeight: 64, + justifyContent: "center", + paddingHorizontal: 12, + paddingVertical: 10, + })} + > + - + {thread.title} {statusPill} {timestamp} - {subtitleRow} - - - ) : ( - setHovered(true)} - onHoverOut={() => setHovered(false)} - onPress={() => { - close(); - onSelectThread(thread); - }} - style={({ pressed }) => ({ - backgroundColor: selected - ? selectedBackgroundColor - : pressed || hovered - ? effectivePressedBackground - : backgroundColor, - borderRadius: SIDEBAR_ROW_RADIUS, - cursor: "pointer", - minHeight: 64, - justifyContent: "center", - paddingHorizontal: 12, - paddingVertical: 10, - })} - > - - - - {thread.title} - - - {statusPill} - - {timestamp} - - - - {subtitleRow} - - - ); + + ); return ( - + @base-org/account': '-' - '@clerk/clerk-js>@coinbase/wallet-sdk': '-' - '@clerk/clerk-js>@solana/wallet-adapter-base': '-' - '@clerk/clerk-js>@solana/wallet-adapter-react': '-' - '@clerk/clerk-js>@solana/wallet-standard': '-' - '@clerk/clerk-js>@wallet-standard/core': '-' - '@clerk/electron': 'catalog:' - '@clerk/electron-passkeys': 'catalog:' - '@clerk/expo': 'catalog:' - '@clerk/react': 'catalog:' - '@clerk/shared': 'catalog:' - '@effect/atom-react': 'catalog:' - '@effect/platform-bun': 'catalog:' - '@effect/platform-node': 'catalog:' - '@effect/platform-node-shared': 'catalog:' - '@effect/sql-pg': 'catalog:' - '@effect/sql-sqlite-bun': 'catalog:' - '@effect/vitest': 'catalog:' - '@effect/vitest>vitest': '-' - '@expo/metro-config': 56.0.14 - '@pierre/diffs>@shikijs/transformers': ^4.2.0 - '@types/node': 'catalog:' - effect: 'catalog:' - vite: 'catalog:' - yaml: 'catalog:' + "@clerk/backend": "catalog:" + "@clerk/clerk-js": "catalog:" + "@clerk/clerk-js>@base-org/account": "-" + "@clerk/clerk-js>@coinbase/wallet-sdk": "-" + "@clerk/clerk-js>@solana/wallet-adapter-base": "-" + "@clerk/clerk-js>@solana/wallet-adapter-react": "-" + "@clerk/clerk-js>@solana/wallet-standard": "-" + "@clerk/clerk-js>@wallet-standard/core": "-" + "@clerk/electron": "catalog:" + "@clerk/electron-passkeys": "catalog:" + "@clerk/expo": "catalog:" + "@clerk/react": "catalog:" + "@clerk/shared": "catalog:" + "@effect/atom-react": "catalog:" + "@effect/platform-bun": "catalog:" + "@effect/platform-node": "catalog:" + "@effect/platform-node-shared": "catalog:" + "@effect/sql-pg": "catalog:" + "@effect/sql-sqlite-bun": "catalog:" + "@effect/vitest": "catalog:" + "@effect/vitest>vitest": "-" + "@expo/metro-config": 56.0.14 + "@pierre/diffs>@shikijs/transformers": ^4.2.0 + "@types/node": "catalog:" + effect: "catalog:" + vite: "catalog:" + yaml: "catalog:" packageExtensions: - '@effect/vitest@*': + "@effect/vitest@*": dependencies: - vite-plus: 'catalog:' + vite-plus: "catalog:" peerDependenciesMeta: vitest: optional: true vite-plus@*: dependencies: - vite: 'catalog:' + vite: "catalog:" patchedDependencies: - '@effect/vitest@4.0.0-beta.78': patches/@effect__vitest@4.0.0-beta.78.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 - '@react-native-menu/menu@2.0.0': patches/@react-native-menu__menu@2.0.0.patch - '@react-navigation/native-stack@7.17.6': patches/@react-navigation%2Fnative-stack@7.17.6.patch + "@effect/vitest@4.0.0-beta.78": patches/@effect__vitest@4.0.0-beta.78.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 + "@react-native-menu/menu@2.0.0": patches/@react-native-menu__menu@2.0.0.patch + "@react-navigation/native-stack@7.17.6": patches/@react-navigation%2Fnative-stack@7.17.6.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-gesture-handler@2.31.2: patches/react-native-gesture-handler@2.31.2.patch @@ -115,4 +115,4 @@ peerDependencyRules: allowAny: - vite allowedVersions: - vite: '*' + vite: "*"