Bottom Tab Bar Visibility Adjustment

Nucleic-Session: 747527BF-AEE9-4939-BE70-A14F1F3BBBA8
Co-authored-by: Nucleic <[email protected]>
This commit is contained in:
2026-07-05 18:23:10 -07:00
co-authored by Nucleic
parent 04eb9dbc10
commit e7b0dff5db
@@ -144,6 +144,10 @@ struct NewChatAffordance: ViewModifier {
var project: WireProject? = nil var project: WireProject? = nil
/// Owned by the caller so it can also open the composer programmatically (iPad ⌘N on Home). /// Owned by the caller so it can also open the composer programmatically (iPad ⌘N on Home).
@Binding var isPresented: Bool @Binding var isPresented: Bool
/// Prompt text and its focus live here so the scrim tap and auto-focus can coordinate with the
/// composer (whether it's empty, whether the keyboard is up).
@State private var draft = ""
@FocusState private var draftFocused: Bool
/// The button only makes sense when this device can control a Mac and there's a project to /// The button only makes sense when this device can control a Mac and there's a project to
/// start a chat in; otherwise the composer it opens would be inert. /// start a chat in; otherwise the composer it opens would be inert.
@@ -151,6 +155,8 @@ struct NewChatAffordance: ViewModifier {
store.canControl && (project != nil || !store.dashboard.projects.isEmpty) store.canControl && (project != nil || !store.dashboard.projects.isEmpty)
} }
private var isEmpty: Bool { draft.trimmingCharacters(in: .whitespaces).isEmpty }
func body(content: Content) -> some View { func body(content: Content) -> some View {
content content
// The "+" disc — hidden while the composer is up so it doesn't sit over the bar. // The "+" disc — hidden while the composer is up so it doesn't sit over the bar.
@@ -175,7 +181,7 @@ struct NewChatAffordance: ViewModifier {
} }
} }
// The composer itself, floating up from the bottom over a light scrim — no modal pane, // The composer itself, floating up from the bottom over a light scrim — no modal pane,
// no nav chrome, just the entry box. Tapping the scrim dismisses it. // no nav chrome, just the entry box.
.overlay { .overlay {
if isPresented { if isPresented {
ZStack(alignment: .bottom) { ZStack(alignment: .bottom) {
@@ -183,8 +189,14 @@ struct NewChatAffordance: ViewModifier {
.fill(.black.opacity(0.2)) .fill(.black.opacity(0.2))
.ignoresSafeArea() .ignoresSafeArea()
.transition(.opacity) .transition(.opacity)
.onTapGesture { isPresented = false } // Tapping away drops the keyboard; with nothing typed there's nothing to
StartChatComposer(onStart: { isPresented = false }, lockedProject: project) // keep the bar around for, so close it. A draft in progress stays open
// (keyboard down) so a stray tap doesn't discard it.
.onTapGesture {
if isEmpty { isPresented = false } else { draftFocused = false }
}
StartChatComposer(draft: $draft, focus: $draftFocused,
onStart: { isPresented = false }, lockedProject: project)
.padding(.horizontal, 12) .padding(.horizontal, 12)
.padding(.bottom, 8) .padding(.bottom, 8)
.transition(.move(edge: .bottom).combined(with: .opacity)) .transition(.move(edge: .bottom).combined(with: .opacity))
@@ -192,6 +204,20 @@ struct NewChatAffordance: ViewModifier {
} }
} }
.animation(.spring(response: 0.35, dampingFraction: 0.86), value: isPresented) .animation(.spring(response: 0.35, dampingFraction: 0.86), value: isPresented)
.onChange(of: isPresented) { _, open in
if open {
draft = ""
// Focus once the bar has sprung into place, so the keyboard rises against a
// settled layout — otherwise its avoidance inset is computed mid-animation and
// the keyboard lands overlapping the bar until the next re-layout.
DispatchQueue.main.asyncAfter(deadline: .now() + 0.35) {
if isPresented { draftFocused = true }
}
} else {
draftFocused = false
draft = ""
}
}
} }
} }