diff --git a/NucleicRemote/NucleicRemote/Views/Composer.swift b/NucleicRemote/NucleicRemote/Views/Composer.swift index 7d9cadd..7560555 100644 --- a/NucleicRemote/NucleicRemote/Views/Composer.swift +++ b/NucleicRemote/NucleicRemote/Views/Composer.swift @@ -144,6 +144,10 @@ struct NewChatAffordance: ViewModifier { var project: WireProject? = nil /// Owned by the caller so it can also open the composer programmatically (iPad ⌘N on Home). @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 /// 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) } + private var isEmpty: Bool { draft.trimmingCharacters(in: .whitespaces).isEmpty } + func body(content: Content) -> some View { content // 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, - // no nav chrome, just the entry box. Tapping the scrim dismisses it. + // no nav chrome, just the entry box. .overlay { if isPresented { ZStack(alignment: .bottom) { @@ -183,8 +189,14 @@ struct NewChatAffordance: ViewModifier { .fill(.black.opacity(0.2)) .ignoresSafeArea() .transition(.opacity) - .onTapGesture { isPresented = false } - StartChatComposer(onStart: { isPresented = false }, lockedProject: project) + // Tapping away drops the keyboard; with nothing typed there's nothing to + // 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(.bottom, 8) .transition(.move(edge: .bottom).combined(with: .opacity)) @@ -192,6 +204,20 @@ struct NewChatAffordance: ViewModifier { } } .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 = "" + } + } } }