From ea9117b7b17222d668b48ad4fc58695d10163a2a Mon Sep 17 00:00:00 2001 From: Andrew Blakeslee Moore Date: Sun, 5 Jul 2026 21:39:33 -0700 Subject: [PATCH] =?UTF-8?q?nvrsion:=20Add:=20Resolve=20conflict,=20reapply?= =?UTF-8?q?=20tab=20bar=20load=20animation=20fixes,=20refactor=20chat=20to?= =?UTF-8?q?ol=20call=20card=20alignment,=20fix=20historical=20transcript?= =?UTF-8?q?=20data=20fetching,=20fix=20freezing=20during=20exit,=20refine?= =?UTF-8?q?=20modal=20design,=20adjust=20spacing,=20hide=20bottom=20tab=20?= =?UTF-8?q?bar=20on=20sessions,=20adjust=20=E2=80=9CNew=20Chat=E2=80=9D=20?= =?UTF-8?q?button,=20fix=20LAN=20routing=20issues,=20implement=20C=20fix,?= =?UTF-8?q?=20remove=20incorrect=20backgrounds,=20remove=20full=20modal,?= =?UTF-8?q?=20refine=20swiping=20behavior,=20adjust=20Model=20Picker=20Lab?= =?UTF-8?q?el,=20refine=20tab=20bar=20animation,=20adjust=20settings,=20re?= =?UTF-8?q?fine=20latency,=20fix=20keyboard=20delay,=20remove=20Trusted=20?= =?UTF-8?q?Tester=20Section,=20remove=20Setting=20Descriptions,=20fix=20co?= =?UTF-8?q?ncurrent=20session=20issues=20in=20tab=20bar=20load=20animation?= =?UTF-8?q?,=20fix=20tab=20bar=20load=20animation=20overlap=20at=20bottom.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nucleic-Promote: 1 Co-authored-by: Nucleic --- .../Views/AdaptiveRootView.swift | 55 ++++++++++++------- .../NucleicRemote/Views/HomeView.swift | 1 + .../NucleicRemote/Views/ProjectsView.swift | 2 + .../NucleicRemote/Views/SessionsView.swift | 1 + .../NucleicRemote/Views/SettingsView.swift | 1 + 5 files changed, 41 insertions(+), 19 deletions(-) diff --git a/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift b/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift index a2240d4..eeaab5e 100644 --- a/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift +++ b/NucleicRemote/NucleicRemote/Views/AdaptiveRootView.swift @@ -61,15 +61,16 @@ struct CompactRootView: View { var body: some View { TabView(selection: $tab) { - // The inset is applied per tab (not to the `TabView`): a bottom `safeAreaInset` on the - // container does NOT propagate into the tab pages' scroll content, so the list would run - // under the bar. On each tab's `NavigationStack` it reaches the scroll content and pushes - // it up above the bar. - HomeView().modifier(TabBarInset(height: contentInset)).tag(0) - SessionsView().modifier(TabBarInset(height: contentInset)).tag(1) - ProjectsView().modifier(TabBarInset(height: contentInset)).tag(2) - SettingsView().modifier(TabBarInset(height: contentInset)).tag(3) + HomeView().toolbar(.hidden, for: .tabBar).tag(0) + SessionsView().toolbar(.hidden, for: .tabBar).tag(1) + ProjectsView().toolbar(.hidden, for: .tabBar).tag(2) + SettingsView().toolbar(.hidden, for: .tabBar).tag(3) } + // Each tab's scroll view reads this and reserves room for the floating bar directly on + // itself (via `.compactTabBarInset()`). Neither a `safeAreaInset` nor `contentMargins` on + // the tab root reaches the pages' scroll content through the `TabView`, so it must be applied + // to the `List`/`ScrollView` itself. Zero (its default) on iPad, which has no bar. + .environment(\.compactTabBarInset, contentInset) // The bar itself lives in an always-mounted overlay and slides on `offset`, so it travels // continuously off the bottom of the screen (a fixed distance past the home-indicator curve) // instead of a transition that removes it mid-descent and makes it pop. @@ -126,18 +127,34 @@ private struct BarHeightKey: PreferenceKey { static func reduce(value: inout CGFloat, nextValue: () -> CGFloat) { value = nextValue() } } -/// Per-tab chrome for the compact shell: suppress the system tab bar (we draw our own) and reserve -/// `height` at the bottom of this tab's content so its scroll view lays out above the floating bar. -/// Applied to each tab rather than the `TabView`, whose container-level inset doesn't reach the tab -/// pages' scroll content. -private struct TabBarInset: ViewModifier { - let height: CGFloat +/// Height the compact shell's floating tab bar wants reserved at the bottom of a tab's scroll +/// content, published down the environment by `CompactRootView`. Zero on iPad (no bar). +private struct CompactTabBarInsetKey: EnvironmentKey { + static let defaultValue: CGFloat = 0 +} + +extension EnvironmentValues { + var compactTabBarInset: CGFloat { + get { self[CompactTabBarInsetKey.self] } + set { self[CompactTabBarInsetKey.self] = newValue } + } +} + +extension View { + /// Reserve room for the compact shell's floating tab bar at the bottom of a scroll view, so its + /// content clears the bar when scrolled to the end. Apply this directly to the `List`/`ScrollView` + /// inside a tab — a bottom `safeAreaInset` on the scroll view itself reliably insets its content, + /// unlike one on the tab root, which the `TabView` doesn't forward. A no-op at regular width + /// (iPad), where the environment inset is 0. + func compactTabBarInset() -> some View { modifier(CompactTabBarInsetReader()) } +} + +private struct CompactTabBarInsetReader: ViewModifier { + @Environment(\.compactTabBarInset) private var inset func body(content: Content) -> some View { - content - .toolbar(.hidden, for: .tabBar) - .safeAreaInset(edge: .bottom, spacing: 0) { - Color.clear.frame(height: height) - } + content.safeAreaInset(edge: .bottom, spacing: 0) { + Color.clear.frame(height: inset) + } } } diff --git a/NucleicRemote/NucleicRemote/Views/HomeView.swift b/NucleicRemote/NucleicRemote/Views/HomeView.swift index 61f1022..2ed6bd6 100644 --- a/NucleicRemote/NucleicRemote/Views/HomeView.swift +++ b/NucleicRemote/NucleicRemote/Views/HomeView.swift @@ -105,6 +105,7 @@ struct HomeView: View { } .navigationTitle("Home") .navigationBarTitleDisplayMode(.inline) + .compactTabBarInset() .scrollDismissesKeyboard(.interactively) .refreshable { store.refreshSessions() } .onChange(of: focusNewChat.wrappedValue) { _, want in diff --git a/NucleicRemote/NucleicRemote/Views/ProjectsView.swift b/NucleicRemote/NucleicRemote/Views/ProjectsView.swift index 1cb4845..23f85e9 100644 --- a/NucleicRemote/NucleicRemote/Views/ProjectsView.swift +++ b/NucleicRemote/NucleicRemote/Views/ProjectsView.swift @@ -35,6 +35,7 @@ struct ProjectsView: View { } } } + .compactTabBarInset() } } .navigationTitle("Projects") @@ -71,6 +72,7 @@ struct ProjectDetailView: View { .navigationTitle(project.name) .navigationBarTitleDisplayMode(.inline) .scrollDismissesKeyboard(.interactively) + .compactTabBarInset() // A floating "+" scoped to this project — the glass composer opens pre-locked to it. Only // at regular width; on compact the shared tab-bar "+" (project picker) covers new chats. .newChatAffordance(project: project, isPresented: $showNewChat, enabled: sizeClass == .regular) diff --git a/NucleicRemote/NucleicRemote/Views/SessionsView.swift b/NucleicRemote/NucleicRemote/Views/SessionsView.swift index 05101a8..5fc3328 100644 --- a/NucleicRemote/NucleicRemote/Views/SessionsView.swift +++ b/NucleicRemote/NucleicRemote/Views/SessionsView.swift @@ -59,6 +59,7 @@ struct SessionsView: View { } .listStyle(.insetGrouped) .refreshable { store.refreshSessions() } + .compactTabBarInset() } } .navigationTitle("Sessions") diff --git a/NucleicRemote/NucleicRemote/Views/SettingsView.swift b/NucleicRemote/NucleicRemote/Views/SettingsView.swift index 1c68dcf..1278510 100644 --- a/NucleicRemote/NucleicRemote/Views/SettingsView.swift +++ b/NucleicRemote/NucleicRemote/Views/SettingsView.swift @@ -158,6 +158,7 @@ struct SettingsView: View { } } .navigationTitle("Settings") + .compactTabBarInset() .sheet(isPresented: $showScanner) { PairingScannerView { payload in showScanner = false