소스 검색

Redesign the home chat input to match the reference app layout.

Use a compact bordered input card with a native text field so the placeholder and cursor stay left-aligned at the same position.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 3 주 전
부모
커밋
796746e9b9

+ 6 - 0
clone _of_clarus_ai_chat_bot/Utilities/AppTheme.swift

@@ -24,4 +24,10 @@ enum AppTheme {
     static let cornerRadiusLarge: CGFloat = 20
     static let cornerRadiusMedium: CGFloat = 12
     static let cornerRadiusPill: CGFloat = 22
+
+    static let chatInputCornerRadius: CGFloat = 22
+    static let chatInputBorder = Color(red: 0.90, green: 0.90, blue: 0.90)
+    static let chatInputBackground = Color.white
+    static let chatInputHeight: CGFloat = 84
+    static let chatInputToolbarHeight: CGFloat = 36
 }

+ 79 - 0
clone _of_clarus_ai_chat_bot/Views/CenteredChatTextField.swift

@@ -0,0 +1,79 @@
+import AppKit
+import SwiftUI
+
+struct CenteredChatTextField: NSViewRepresentable {
+    @Binding var text: String
+    let placeholder: String
+    let fontSize: CGFloat
+
+    func makeNSView(context: Context) -> NSView {
+        let container = NSView()
+        let field = NSTextField()
+        field.delegate = context.coordinator
+        context.coordinator.field = field
+        applyStyle(to: field)
+
+        field.translatesAutoresizingMaskIntoConstraints = false
+        container.addSubview(field)
+
+        NSLayoutConstraint.activate([
+            field.leadingAnchor.constraint(equalTo: container.leadingAnchor),
+            field.trailingAnchor.constraint(equalTo: container.trailingAnchor),
+            field.centerYAnchor.constraint(equalTo: container.centerYAnchor),
+        ])
+
+        return container
+    }
+
+    func updateNSView(_ nsView: NSView, context: Context) {
+        guard let field = context.coordinator.field else { return }
+        applyStyle(to: field)
+        if field.stringValue != text {
+            field.stringValue = text
+        }
+    }
+
+    func makeCoordinator() -> Coordinator {
+        Coordinator(text: $text)
+    }
+
+    private func applyStyle(to field: NSTextField) {
+        field.isBezeled = false
+        field.isBordered = false
+        field.drawsBackground = false
+        field.focusRingType = .none
+        field.alignment = .left
+        field.font = .systemFont(ofSize: fontSize)
+        field.textColor = NSColor(AppTheme.textPrimary)
+
+        let paragraphStyle = NSMutableParagraphStyle()
+        paragraphStyle.alignment = .left
+        field.placeholderAttributedString = NSAttributedString(
+            string: placeholder,
+            attributes: [
+                .foregroundColor: NSColor(AppTheme.textPlaceholder),
+                .font: NSFont.systemFont(ofSize: fontSize),
+                .paragraphStyle: paragraphStyle,
+            ]
+        )
+        field.cell?.usesSingleLineMode = false
+        field.cell?.wraps = true
+        field.cell?.isScrollable = false
+        field.maximumNumberOfLines = 3
+        field.lineBreakMode = .byWordWrapping
+    }
+
+    final class Coordinator: NSObject, NSTextFieldDelegate {
+        @Binding var text: String
+        weak var field: NSTextField?
+
+        init(text: Binding<String>) {
+            _text = text
+        }
+
+        func controlTextDidChange(_ obj: Notification) {
+            guard let field = obj.object as? NSTextField else { return }
+            text = field.stringValue
+        }
+    }
+}

+ 53 - 90
clone _of_clarus_ai_chat_bot/Views/ChatInputCard.swift

@@ -3,46 +3,50 @@ import SwiftUI
 struct ChatInputCard: View {
     @ObservedObject var viewModel: HomeViewModel
 
-    var body: some View {
-        VStack(alignment: .leading, spacing: 0) {
-            ZStack(alignment: .topLeading) {
-                if viewModel.promptText.isEmpty {
-                    Text("How can I help you today?")
-                        .font(.system(size: 22, weight: .regular))
-                        .foregroundStyle(AppTheme.textPlaceholder)
-                        .padding(.top, 2)
-                        .allowsHitTesting(false)
-                }
-
-                TextEditor(text: $viewModel.promptText)
-                    .font(.system(size: 22, weight: .regular))
-                    .foregroundStyle(AppTheme.textPrimary)
-                    .scrollContentBackground(.hidden)
-                    .background(Color.clear)
-                    .frame(minHeight: 88, maxHeight: 120)
-            }
-            .padding(.bottom, 16)
+    private let inputSectionHeight: CGFloat = AppTheme.chatInputHeight - AppTheme.chatInputToolbarHeight
+    private let actionButtonSize: CGFloat = 30
 
-            HStack(spacing: 10) {
-                CircleIconButton(symbol: "plus", accessibilityLabel: "Add attachment")
+    var body: some View {
+        VStack(spacing: 0) {
+            CenteredChatTextField(
+                text: $viewModel.promptText,
+                placeholder: "How can I help you",
+                fontSize: 16
+            )
+            .padding(.horizontal, 16)
+            .frame(height: inputSectionHeight)
 
-                PillButton(title: "Web search", symbol: "globe")
+            HStack(spacing: 6) {
+                Button(action: {}) {
+                    Image(systemName: "plus")
+                        .font(.system(size: 17, weight: .regular))
+                        .foregroundStyle(AppTheme.textSecondary)
+                        .frame(width: actionButtonSize, height: actionButtonSize)
+                }
+                .buttonStyle(.plain)
+                .accessibilityLabel("Add attachment")
 
-                Spacer(minLength: 8)
+                Spacer(minLength: 0)
 
                 modelMenu
 
-                CircleIconButton(symbol: "mic", accessibilityLabel: "Voice input")
+                voiceButton
 
                 sendButton
             }
+            .padding(.horizontal, 12)
+            .frame(height: AppTheme.chatInputToolbarHeight)
         }
-        .padding(.horizontal, 22)
-        .padding(.vertical, 20)
+        .frame(maxWidth: .infinity)
+        .frame(height: AppTheme.chatInputHeight)
         .background(
-            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusLarge, style: .continuous)
-                .fill(Color.white)
-                .shadow(color: AppTheme.cardShadow, radius: 14, y: 4)
+            RoundedRectangle(cornerRadius: AppTheme.chatInputCornerRadius, style: .continuous)
+                .fill(AppTheme.chatInputBackground)
+        )
+        .clipShape(RoundedRectangle(cornerRadius: AppTheme.chatInputCornerRadius, style: .continuous))
+        .overlay(
+            RoundedRectangle(cornerRadius: AppTheme.chatInputCornerRadius, style: .continuous)
+                .stroke(AppTheme.chatInputBorder, lineWidth: 1)
         )
     }
 
@@ -54,87 +58,46 @@ struct ChatInputCard: View {
                 }
             }
         } label: {
-            HStack(spacing: 6) {
+            HStack(spacing: 4) {
                 Text(viewModel.selectedModel)
                     .font(.system(size: 13, weight: .medium))
                     .foregroundStyle(AppTheme.textPrimary)
                 Image(systemName: "chevron.down")
-                    .font(.system(size: 10, weight: .semibold))
+                    .font(.system(size: 9, weight: .semibold))
                     .foregroundStyle(AppTheme.textSecondary)
             }
-            .padding(.horizontal, 12)
-            .padding(.vertical, 8)
+            .padding(.horizontal, 10)
+            .padding(.vertical, 6)
             .background(
-                RoundedRectangle(cornerRadius: 10, style: .continuous)
-                    .fill(AppTheme.controlBackground)
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(Color.white)
             )
         }
         .menuStyle(.borderlessButton)
         .fixedSize()
     }
 
-    private var sendButton: some View {
+    private var voiceButton: some View {
         Button(action: {}) {
-            HStack(spacing: 7) {
-                Image(systemName: "paperplane.fill")
-                    .font(.system(size: 12, weight: .bold))
-                Text("Send")
-                    .font(.system(size: 14, weight: .bold))
-            }
-            .foregroundStyle(.white)
-            .padding(.horizontal, 18)
-            .padding(.vertical, 10)
-            .background(
-                RoundedRectangle(cornerRadius: 12, style: .continuous)
-                    .fill(AppTheme.accent)
-            )
+            Image(systemName: "waveform")
+                .font(.system(size: 12, weight: .medium))
+                .foregroundStyle(AppTheme.textSecondary)
+                .frame(width: actionButtonSize, height: actionButtonSize)
+                .background(Circle().fill(Color.white))
         }
         .buttonStyle(.plain)
-        .accessibilityLabel("Send message")
+        .accessibilityLabel("Voice input")
     }
-}
 
-private struct CircleIconButton: View {
-    let symbol: String
-    let accessibilityLabel: String
-
-    var body: some View {
-        Button(action: {}) {
-            Image(systemName: symbol)
-                .font(.system(size: 13, weight: .semibold))
-                .foregroundStyle(AppTheme.accent)
-                .frame(width: 34, height: 34)
-                .background(
-                    Circle()
-                        .fill(AppTheme.controlBackground)
-                )
-        }
-        .buttonStyle(.plain)
-        .accessibilityLabel(accessibilityLabel)
-    }
-}
-
-private struct PillButton: View {
-    let title: String
-    let symbol: String
-
-    var body: some View {
+    private var sendButton: some View {
         Button(action: {}) {
-            HStack(spacing: 6) {
-                Image(systemName: symbol)
-                    .font(.system(size: 11, weight: .semibold))
-                Text(title)
-                    .font(.system(size: 13, weight: .medium))
-            }
-            .foregroundStyle(AppTheme.textPrimary)
-            .padding(.horizontal, 12)
-            .padding(.vertical, 8)
-            .background(
-                RoundedRectangle(cornerRadius: 10, style: .continuous)
-                    .fill(AppTheme.controlBackground)
-            )
+            Image(systemName: "arrow.up")
+                .font(.system(size: 15, weight: .bold))
+                .foregroundStyle(.white)
+                .frame(width: actionButtonSize, height: actionButtonSize)
+                .background(Circle().fill(AppTheme.accent))
         }
         .buttonStyle(.plain)
-        .accessibilityLabel(title)
+        .accessibilityLabel("Send message")
     }
 }

+ 1 - 1
clone _of_clarus_ai_chat_bot/Views/MainContentView.swift

@@ -12,7 +12,7 @@ struct MainContentView: View {
 
             ChatInputCard(viewModel: viewModel)
                 .padding(.horizontal, 48)
-                .frame(maxWidth: 760)
+                .frame(maxWidth: .infinity)
 
             quickActionRow
                 .padding(.top, 22)