소스 검색

Align Email Writer controls with the rest of the app UI.

Match the generate button size and layout to other tools, and give Type/Tone selectors proper bordered button styling on macOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 개월 전
부모
커밋
275299324e
2개의 변경된 파일72개의 추가작업 그리고 66개의 파일을 삭제
  1. 12 49
      gramora/Views/Components/GradientButton.swift
  2. 60 17
      gramora/Views/EmailWriterView.swift

+ 12 - 49
gramora/Views/Components/GradientButton.swift

@@ -1,48 +1,7 @@
 import SwiftUI
 
 struct GradientButton: View {
-    enum Size {
-        case standard
-        case large
-
-        var verticalPadding: CGFloat {
-            switch self {
-            case .standard: 15
-            case .large: 22
-            }
-        }
-
-        var horizontalPadding: CGFloat {
-            switch self {
-            case .standard: 28
-            case .large: 32
-            }
-        }
-
-        var titleFont: Font {
-            switch self {
-            case .standard: .system(size: 16, weight: .semibold)
-            case .large: .system(size: 18, weight: .semibold)
-            }
-        }
-
-        var leadingIconFont: Font {
-            switch self {
-            case .standard: .system(size: 15, weight: .semibold)
-            case .large: .system(size: 17, weight: .semibold)
-            }
-        }
-
-        var trailingIconFont: Font {
-            switch self {
-            case .standard: .system(size: 14, weight: .semibold)
-            case .large: .system(size: 16, weight: .semibold)
-            }
-        }
-    }
-
     let title: String
-    var size: Size = .standard
     let action: () -> Void
 
     @State private var isHovered = false
@@ -51,18 +10,18 @@ struct GradientButton: View {
         Button(action: action) {
             HStack(spacing: 10) {
                 Image(systemName: "sparkles")
-                    .font(size.leadingIconFont)
+                    .font(.system(size: 15, weight: .semibold))
 
                 Text(title)
-                    .font(size.titleFont)
+                    .font(.system(size: 16, weight: .semibold))
                     .textSelection(.disabled)
 
                 Image(systemName: "arrow.right")
-                    .font(size.trailingIconFont)
+                    .font(.system(size: 14, weight: .semibold))
             }
             .foregroundStyle(.white)
-            .padding(.horizontal, size.horizontalPadding)
-            .padding(.vertical, size.verticalPadding)
+            .padding(.horizontal, 28)
+            .padding(.vertical, 15)
             .frame(maxWidth: .infinity)
             .background(isHovered ? AppTheme.primaryGradientHover : AppTheme.primaryGradient)
             .clipShape(RoundedRectangle(cornerRadius: AppTheme.buttonCornerRadius))
@@ -141,10 +100,12 @@ struct ToolbarMenuButton<MenuContent: View>: View {
             .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
             .padding(.horizontal, 24)
             .padding(.vertical, 7)
-            .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
-            .clipShape(RoundedRectangle(cornerRadius: 8))
+            .background(
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
+                    .fill(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
+            )
             .overlay(
-                RoundedRectangle(cornerRadius: 8)
+                RoundedRectangle(cornerRadius: 8, style: .continuous)
                     .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
             )
             .shadow(
@@ -154,7 +115,9 @@ struct ToolbarMenuButton<MenuContent: View>: View {
             )
             .scaleEffect(isHovered ? 1.02 : 1.0)
             .animation(.easeOut(duration: 0.15), value: isHovered)
+            .compositingGroup()
         }
+        .fixedSize(horizontal: true, vertical: false)
         .menuStyle(.borderlessButton)
         .buttonStyle(.plain)
         .onHover { isHovered = $0 }

+ 60 - 17
gramora/Views/EmailWriterView.swift

@@ -14,16 +14,16 @@ struct EmailWriterView: View {
                     .padding(.bottom, 16)
 
                 contentCard
-                    .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
-                    .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
+                    .frame(maxHeight: .infinity, alignment: .topLeading)
+                    .padding(.bottom, 24)
 
-                GradientButton(title: "Generate Email", size: .large) {
+                GradientButton(title: "Generate Email") {
                     viewModel.generateEmail()
                 }
                 .disabled(!viewModel.canGenerate)
                 .opacity(viewModel.canGenerate ? 1 : 0.55)
-                .padding(.top, 16)
             }
+            .frame(maxWidth: 1200, maxHeight: .infinity, alignment: .topLeading)
             .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
             .padding(.horizontal, dynamicHorizontalPadding)
             .padding(.top, dynamicTopPadding)
@@ -252,24 +252,67 @@ private struct EmailOptionMenu<Option: Identifiable & Equatable>: View {
     let options: [Option]
     let label: KeyPath<Option, String>
 
+    @State private var isPresented = false
+    @State private var isHovered = false
+
+    private var iconName: String {
+        title == "Type" ? "envelope" : "textformat"
+    }
+
     var body: some View {
-        ToolbarMenuButton(
-            title: "\(title): \(selection[keyPath: label])",
-            iconName: title == "Type" ? "envelope" : "textformat"
-        ) {
-            ForEach(options) { option in
-                Button {
-                    selection = option
-                } label: {
-                    HStack {
-                        Text(option[keyPath: label])
-                        if selection == option {
-                            Spacer()
-                            Image(systemName: "checkmark")
+        Button {
+            isPresented.toggle()
+        } label: {
+            HStack(spacing: 6) {
+                Image(systemName: iconName)
+                    .font(.system(size: 12))
+
+                Text("\(title): \(selection[keyPath: label])")
+                    .font(.system(size: 12, weight: .medium))
+
+                Image(systemName: "chevron.down")
+                    .font(.system(size: 10, weight: .semibold))
+            }
+            .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
+            .padding(.horizontal, 24)
+            .padding(.vertical, 7)
+            .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
+            .clipShape(RoundedRectangle(cornerRadius: 8))
+            .overlay(
+                RoundedRectangle(cornerRadius: 8)
+                    .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
+            )
+            .shadow(
+                color: isHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
+                radius: isHovered ? 4 : 2,
+                y: isHovered ? 2 : 1
+            )
+            .scaleEffect(isHovered ? 1.02 : 1.0)
+            .animation(.easeOut(duration: 0.15), value: isHovered)
+        }
+        .buttonStyle(.plain)
+        .onHover { isHovered = $0 }
+        .popover(isPresented: $isPresented, arrowEdge: .bottom) {
+            VStack(alignment: .leading, spacing: 4) {
+                ForEach(options) { option in
+                    Button {
+                        selection = option
+                        isPresented = false
+                    } label: {
+                        HStack {
+                            Text(option[keyPath: label])
+                            if selection == option {
+                                Spacer()
+                                Image(systemName: "checkmark")
+                            }
                         }
+                        .frame(minWidth: 180, alignment: .leading)
+                        .contentShape(Rectangle())
                     }
+                    .buttonStyle(.plain)
                 }
             }
+            .padding(8)
         }
         .accessibilityLabel("\(title): \(selection[keyPath: label])")
     }