From ab615f0c28698c3a72f2568f45e3ef52e2be3f48 Mon Sep 17 00:00:00 2001 From: Alan Wizemann Date: Sun, 3 May 2026 13:14:09 +0200 Subject: [PATCH] feat(ios-chat): redesign composer with HIG touch targets and clear disabled state MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Send button is now a 44pt circular target with an explicit color swap (rust accent → background-tertiary) on disable, instead of relying on SwiftUI's default opacity dim — addresses the "first tap doesn't register" complaint by making the inactive state visibly different in both light and dark mode. Paperclip and text field both gain a 44pt minimum height so the row feels modern and roomy. The text field swaps `.roundedBorder` for a plain field with a ScarfRadius.xl rounded fill (ScarfColor.backgroundSecondary) and a borderStrong stroke. Outer paddings and HStack spacing migrate from magic numbers to ScarfSpace tokens. Preserves verbatim: the `.toolbar { ToolbarItemGroup(placement: .keyboard) }` keyboard-dismiss chevron (issue #51), draft persistence, .submitLabel, @FocusState, photo-picker wiring, attachment-strip rendering, and every .disabled() predicate. Closes #69 Co-Authored-By: Claude Opus 4.7 (1M context) --- scarf/Scarf iOS/Chat/ChatView.swift | 64 ++++++++++++++++++++++++----- 1 file changed, 53 insertions(+), 11 deletions(-) diff --git a/scarf/Scarf iOS/Chat/ChatView.swift b/scarf/Scarf iOS/Chat/ChatView.swift index 8a95f0a..938b52c 100644 --- a/scarf/Scarf iOS/Chat/ChatView.swift +++ b/scarf/Scarf iOS/Chat/ChatView.swift @@ -448,14 +448,15 @@ struct ChatView: View { } private var composer: some View { - VStack(alignment: .leading, spacing: 4) { + VStack(alignment: .leading, spacing: ScarfSpace.s2) { if !controller.attachments.isEmpty || isEncodingAttachment || attachmentError != nil { attachmentStrip } composerRow } - .padding(.horizontal, 12) - .padding(.vertical, 8) + .padding(.horizontal, ScarfSpace.s3) + .padding(.top, ScarfSpace.s2) + .padding(.bottom, ScarfSpace.s2) .background(.regularMaterial) #if canImport(PhotosUI) .photosPicker( @@ -536,18 +537,23 @@ struct ChatView: View { } private var composerRow: some View { - HStack(alignment: .bottom, spacing: 8) { + HStack(alignment: .bottom, spacing: ScarfSpace.s2) { if supportsImagePrompts { Button { showPhotoPicker = true } label: { Image(systemName: "paperclip") - .font(.system(size: 22)) - .foregroundStyle(.secondary) - .padding(.bottom, 4) + .font(.system(size: 20, weight: .regular)) + .foregroundStyle( + attachDisabled + ? ScarfColor.foregroundFaint + : ScarfColor.foregroundMuted + ) + .frame(width: 44, height: 44) + .contentShape(Rectangle()) } .buttonStyle(.plain) - .disabled(controller.state != .ready || controller.attachments.count >= Self.maxAttachments) + .disabled(attachDisabled) .accessibilityLabel("Attach image") } TextField( @@ -555,8 +561,19 @@ struct ChatView: View { text: $controller.draft, axis: .vertical ) - .textFieldStyle(.roundedBorder) + .textFieldStyle(.plain) .lineLimit(1...5) + .padding(.horizontal, ScarfSpace.s3) + .padding(.vertical, ScarfSpace.s2) + .frame(minHeight: 44) + .background( + RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous) + .fill(ScarfColor.backgroundSecondary) + ) + .overlay( + RoundedRectangle(cornerRadius: ScarfRadius.xl, style: .continuous) + .strokeBorder(ScarfColor.borderStrong, lineWidth: 1) + ) .disabled(controller.state != .ready) .submitLabel(.send) .focused($composerFocused) @@ -592,13 +609,32 @@ struct ChatView: View { } } + // Big circular send button. Filled with the brand accent when + // ready, swapped to a flat gray when disabled — opacity dims + // alone read as "not quite tappable" (issue #69), the explicit + // color swap makes the state unambiguous in both light and + // dark mode. Button { Task { await controller.send() } } label: { - Image(systemName: "arrow.up.circle.fill") - .font(.system(size: 28)) + ZStack { + Circle() + .fill(canSendComposer + ? ScarfColor.accent + : ScarfColor.backgroundTertiary) + Image(systemName: "arrow.up") + .font(.system(size: 18, weight: .semibold)) + .foregroundStyle(canSendComposer + ? ScarfColor.onAccent + : ScarfColor.foregroundFaint) + } + .frame(width: 44, height: 44) + .contentShape(Circle()) + .animation(ScarfAnimation.fast, value: canSendComposer) } + .buttonStyle(.plain) .disabled(!canSendComposer) + .accessibilityLabel("Send message") } } @@ -610,6 +646,12 @@ struct ChatView: View { return !controller.draft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty } + /// Mirror of the `.disabled(...)` predicate on the paperclip button. + /// Pulled out so the button's foreground branch reads cleanly. + private var attachDisabled: Bool { + controller.state != .ready || controller.attachments.count >= Self.maxAttachments + } + /// Pull JPEG/PNG bytes out of each PhotosPickerItem and feed them /// through ImageEncoder. Detached so the heavyweight resize + /// JPEG-encode work doesn't block MainActor; the resulting