Bläddra i källkod

Align Grammar Checker UI with the design mockup.

Update theme tokens, sidebar navigation, background decorations, and content card layout to match the target design, including a taller text input area.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 månad sedan
förälder
incheckning
cee8ffc5b1

+ 15 - 11
gramora/Models/NavigationDestination.swift

@@ -32,18 +32,22 @@ enum NavigationDestination: String, CaseIterable, Identifiable {
     }
 
     var iconName: String {
+        iconName(isSelected: false)
+    }
+
+    func iconName(isSelected: Bool) -> String {
         switch self {
-        case .grammarChecker: "text.badge.checkmark"
-        case .summarizer: "doc.text"
-        case .paraphrasing: "arrow.triangle.2.circlepath"
-        case .languageTranslator: "globe"
-        case .punctuationChecker: "text.quote"
-        case .wordsCount: "textformat.123"
-        case .aiEmailWriter: "envelope"
-        case .spellingChecker: "checklist"
-        case .dictionary: "book"
-        case .journalFinder: "magnifyingglass"
-        case .history: "clock"
+        case .grammarChecker: isSelected ? "checkmark.square.fill" : "checkmark.square"
+        case .summarizer: isSelected ? "doc.text.fill" : "doc.text"
+        case .paraphrasing: isSelected ? "arrow.triangle.2.circlepath" : "arrow.triangle.2.circlepath"
+        case .languageTranslator: isSelected ? "globe" : "globe"
+        case .punctuationChecker: isSelected ? "text.quote" : "text.quote"
+        case .wordsCount: isSelected ? "textformat.123" : "textformat.123"
+        case .aiEmailWriter: isSelected ? "envelope.fill" : "envelope"
+        case .spellingChecker: isSelected ? "checklist" : "checklist"
+        case .dictionary: isSelected ? "book.fill" : "book"
+        case .journalFinder: isSelected ? "magnifyingglass" : "magnifyingglass"
+        case .history: isSelected ? "clock.fill" : "clock"
         }
     }
 }

+ 13 - 12
gramora/Utilities/AppTheme.swift

@@ -6,13 +6,14 @@ enum AppTheme {
     static let windowMinWidth: CGFloat = 900
     static let windowMinHeight: CGFloat = 600
 
-    static let sidebarWidth: CGFloat = 230
-    static let contentPadding: CGFloat = 32
+    static let sidebarWidth: CGFloat = 240
+    static let contentPadding: CGFloat = 40
     static let cardCornerRadius: CGFloat = 16
-    static let buttonCornerRadius: CGFloat = 10
+    static let buttonCornerRadius: CGFloat = 14
     static let navItemCornerRadius: CGFloat = 10
+    static let inputCornerRadius: CGFloat = 12
 
-    static let background = Color(red: 0.96, green: 0.97, blue: 0.98)
+    static let background = Color(red: 0.973, green: 0.980, blue: 0.984)
     static let sidebarBackground = Color.white
     static let cardBackground = Color.white
     static let textPrimary = Color(red: 0.10, green: 0.14, blue: 0.22)
@@ -21,18 +22,18 @@ enum AppTheme {
     static let border = Color(red: 0.90, green: 0.92, blue: 0.94)
     static let inputBorder = Color(red: 0.88, green: 0.90, blue: 0.93)
 
-    static let teal = Color(red: 0.18, green: 0.71, blue: 0.49)
-    static let tealLight = Color(red: 0.91, green: 0.97, blue: 0.94)
-    static let tealDark = Color(red: 0.13, green: 0.60, blue: 0.42)
-    static let gradientStart = Color(red: 0.18, green: 0.71, blue: 0.49)
-    static let gradientEnd = Color(red: 0.34, green: 0.85, blue: 0.63)
+    static let teal = Color(red: 0.0, green: 0.651, blue: 0.494)
+    static let tealLight = Color(red: 0.90, green: 0.97, blue: 0.94)
+    static let tealDark = Color(red: 0.0, green: 0.55, blue: 0.42)
+    static let gradientStart = Color(red: 0.0, green: 0.62, blue: 0.47)
+    static let gradientEnd = Color(red: 0.20, green: 0.78, blue: 0.60)
 
     static let premiumBackground = Color(red: 0.91, green: 0.97, blue: 0.95)
     static let premiumBorder = Color(red: 0.82, green: 0.93, blue: 0.88)
-    static let blobColor = Color(red: 0.18, green: 0.71, blue: 0.49).opacity(0.06)
-    static let dotColor = Color(red: 0.18, green: 0.71, blue: 0.49).opacity(0.12)
+    static let blobColor = Color(red: 0.0, green: 0.651, blue: 0.494).opacity(0.07)
+    static let dotColor = Color(red: 0.0, green: 0.651, blue: 0.494).opacity(0.14)
 
-    static let cardShadow = Color.black.opacity(0.04)
+    static let cardShadow = Color.black.opacity(0.06)
 
     static var primaryGradient: LinearGradient {
         LinearGradient(

+ 40 - 19
gramora/Views/Components/BackgroundDecorations.swift

@@ -5,33 +5,54 @@ struct BackgroundDecorations: View {
         ZStack {
             AppTheme.background
 
-            Circle()
+            OrganicWave()
                 .fill(AppTheme.blobColor)
-                .frame(width: 320, height: 320)
-                .blur(radius: 40)
-                .offset(x: 380, y: -220)
+                .frame(width: 420, height: 320)
+                .offset(x: 340, y: -200)
 
-            Circle()
-                .fill(AppTheme.blobColor)
-                .frame(width: 260, height: 260)
-                .blur(radius: 50)
-                .offset(x: -420, y: 280)
-
-            Ellipse()
-                .fill(AppTheme.blobColor.opacity(0.8))
-                .frame(width: 200, height: 140)
-                .blur(radius: 30)
-                .offset(x: 300, y: 200)
+            OrganicWave()
+                .fill(AppTheme.blobColor.opacity(0.85))
+                .frame(width: 380, height: 280)
+                .scaleEffect(x: -1, y: 1)
+                .offset(x: -360, y: 260)
 
             DotGrid()
-                .offset(x: -380, y: 260)
-                .opacity(0.6)
+                .offset(x: -400, y: 280)
+                .opacity(0.55)
 
             DotGrid()
-                .offset(x: 420, y: -240)
-                .opacity(0.5)
+                .offset(x: 440, y: -220)
+                .opacity(0.45)
         }
         .ignoresSafeArea()
+        .clipped()
+    }
+}
+
+private struct OrganicWave: Shape {
+    func path(in rect: CGRect) -> Path {
+        var path = Path()
+        let w = rect.width
+        let h = rect.height
+
+        path.move(to: CGPoint(x: w * 0.1, y: h * 0.85))
+        path.addCurve(
+            to: CGPoint(x: w * 0.9, y: h * 0.15),
+            control1: CGPoint(x: w * 0.05, y: h * 0.35),
+            control2: CGPoint(x: w * 0.55, y: h * 0.0)
+        )
+        path.addCurve(
+            to: CGPoint(x: w * 0.65, y: h * 0.95),
+            control1: CGPoint(x: w * 1.1, y: h * 0.45),
+            control2: CGPoint(x: w * 0.85, y: h * 1.05)
+        )
+        path.addCurve(
+            to: CGPoint(x: w * 0.1, y: h * 0.85),
+            control1: CGPoint(x: w * 0.35, y: h * 0.75),
+            control2: CGPoint(x: w * 0.0, y: h * 1.0)
+        )
+        path.closeSubpath()
+        return path
     }
 }
 

+ 8 - 6
gramora/Views/Components/GradientButton.swift

@@ -18,12 +18,12 @@ struct GradientButton: View {
                     .font(.system(size: 14, weight: .semibold))
             }
             .foregroundStyle(.white)
-            .padding(.horizontal, 24)
-            .padding(.vertical, 14)
+            .padding(.horizontal, 28)
+            .padding(.vertical, 15)
             .frame(maxWidth: .infinity)
             .background(AppTheme.primaryGradient)
-            .clipShape(RoundedRectangle(cornerRadius: AppTheme.buttonCornerRadius))
-            .shadow(color: AppTheme.teal.opacity(0.25), radius: 8, y: 4)
+            .clipShape(Capsule())
+            .shadow(color: AppTheme.teal.opacity(0.30), radius: 10, y: 4)
         }
         .buttonStyle(.plain)
     }
@@ -52,6 +52,7 @@ struct ToolbarButton: View {
                 RoundedRectangle(cornerRadius: 8)
                     .stroke(AppTheme.border, lineWidth: 1)
             )
+            .shadow(color: Color.black.opacity(0.03), radius: 2, y: 1)
         }
         .buttonStyle(.plain)
     }
@@ -64,15 +65,16 @@ struct IconButton: View {
     var body: some View {
         Button(action: action) {
             Image(systemName: iconName)
-                .font(.system(size: 13))
+                .font(.system(size: 14))
                 .foregroundStyle(AppTheme.textSecondary)
-                .frame(width: 34, height: 34)
+                .frame(width: 36, height: 36)
                 .background(Color.white)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .overlay(
                     RoundedRectangle(cornerRadius: 8)
                         .stroke(AppTheme.border, lineWidth: 1)
                 )
+                .shadow(color: Color.black.opacity(0.04), radius: 3, y: 1)
         }
         .buttonStyle(.plain)
     }

+ 3 - 3
gramora/Views/Components/PremiumCardView.swift

@@ -27,14 +27,14 @@ struct PremiumCardView: View {
                     Image(systemName: "arrow.up.right")
                         .font(.system(size: 10, weight: .semibold))
                 }
-                .foregroundStyle(AppTheme.teal)
+                .foregroundStyle(AppTheme.textPrimary)
                 .frame(maxWidth: .infinity)
-                .padding(.vertical, 8)
+                .padding(.vertical, 9)
                 .background(Color.white)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
                 .overlay(
                     RoundedRectangle(cornerRadius: 8)
-                        .stroke(AppTheme.premiumBorder, lineWidth: 1)
+                        .stroke(AppTheme.border, lineWidth: 1)
                 )
             }
             .buttonStyle(.plain)

+ 32 - 30
gramora/Views/GrammarCheckerView.swift

@@ -4,21 +4,23 @@ struct GrammarCheckerView: View {
     @StateObject private var viewModel = GrammarCheckerViewModel()
 
     var body: some View {
-        VStack(alignment: .leading, spacing: 0) {
-            header
-                .padding(.bottom, 24)
+        ScrollView(.vertical, showsIndicators: false) {
+            VStack(alignment: .leading, spacing: 0) {
+                header
+                    .padding(.bottom, 28)
 
-            contentCard
-                .padding(.bottom, 20)
+                contentCard
+                    .padding(.bottom, 24)
 
-            GradientButton(title: "Check Grammar") {
-                viewModel.checkGrammar()
+                GradientButton(title: "Check Grammar") {
+                    viewModel.checkGrammar()
+                }
             }
+            .frame(maxWidth: .infinity, alignment: .topLeading)
+            .padding(.horizontal, AppTheme.contentPadding)
+            .padding(.top, 32)
+            .padding(.bottom, 40)
         }
-        .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
-        .padding(.horizontal, AppTheme.contentPadding)
-        .padding(.top, 28)
-        .padding(.bottom, 32)
         .onChange(of: viewModel.text) { newValue in
             if newValue.count > GrammarCheckerViewModel.maxCharacters {
                 viewModel.text = String(newValue.prefix(GrammarCheckerViewModel.maxCharacters))
@@ -28,26 +30,26 @@ struct GrammarCheckerView: View {
 
     private var header: some View {
         HStack(alignment: .top) {
-            VStack(alignment: .leading, spacing: 6) {
+            VStack(alignment: .leading, spacing: 8) {
                 Text("Grammar Checker")
-                    .font(.system(size: 28, weight: .bold))
+                    .font(.system(size: 30, weight: .bold))
                     .foregroundStyle(AppTheme.textPrimary)
 
                 HStack(spacing: 0) {
                     Text("Write ")
                         .foregroundStyle(AppTheme.textSecondary)
                     Text("better")
-                        .font(.system(size: 14, weight: .semibold))
+                        .fontWeight(.semibold)
                         .foregroundStyle(AppTheme.teal)
                     Text(". Sound ")
                         .foregroundStyle(AppTheme.textSecondary)
                     Text("smarter")
-                        .font(.system(size: 14, weight: .semibold))
+                        .fontWeight(.semibold)
                         .foregroundStyle(AppTheme.teal)
                     Text(".")
                         .foregroundStyle(AppTheme.textSecondary)
                 }
-                .font(.system(size: 14))
+                .font(.system(size: 15))
             }
 
             Spacer()
@@ -57,10 +59,10 @@ struct GrammarCheckerView: View {
     }
 
     private var contentCard: some View {
-        VStack(alignment: .leading, spacing: 14) {
+        VStack(alignment: .leading, spacing: 16) {
             HStack(spacing: 8) {
-                Image(systemName: "pencil.line")
-                    .font(.system(size: 13))
+                Image(systemName: "doc.text.magnifyingglass")
+                    .font(.system(size: 14))
                     .foregroundStyle(AppTheme.teal)
 
                 Text("Your Content")
@@ -72,12 +74,12 @@ struct GrammarCheckerView: View {
                 ThinCaretTextEditor(text: $viewModel.text)
                     .font(.system(size: 14))
                     .foregroundStyle(AppTheme.textPrimary)
-                    .padding(12)
-                    .frame(minHeight: 220)
+                    .padding(14)
+                    .frame(minHeight: 320)
                     .background(Color.white)
-                    .clipShape(RoundedRectangle(cornerRadius: 10))
+                    .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
                     .overlay(
-                        RoundedRectangle(cornerRadius: 10)
+                        RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius)
                             .stroke(AppTheme.inputBorder, lineWidth: 1)
                     )
                     .overlay(alignment: .topLeading) {
@@ -85,8 +87,8 @@ struct GrammarCheckerView: View {
                             Text("Type or paste your text here and press the check button...")
                                 .font(.system(size: 14))
                                 .foregroundStyle(AppTheme.textMuted)
-                                .padding(.horizontal, 16)
-                                .padding(.vertical, 20)
+                                .padding(.horizontal, 18)
+                                .padding(.vertical, 22)
                                 .allowsHitTesting(false)
                         }
                     }
@@ -94,8 +96,8 @@ struct GrammarCheckerView: View {
                 Text(viewModel.characterCountLabel)
                     .font(.system(size: 11))
                     .foregroundStyle(AppTheme.textMuted)
-                    .padding(.trailing, 14)
-                    .padding(.bottom, 10)
+                    .padding(.trailing, 16)
+                    .padding(.bottom, 12)
             }
 
             HStack(spacing: 8) {
@@ -103,7 +105,7 @@ struct GrammarCheckerView: View {
                     viewModel.pasteText()
                 }
 
-                ToolbarButton(title: "Upload File", iconName: "arrow.up.doc") {}
+                ToolbarButton(title: "Upload File", iconName: "square.and.arrow.up") {}
 
                 HStack(spacing: 6) {
                     Image(systemName: "textformat")
@@ -129,9 +131,9 @@ struct GrammarCheckerView: View {
                 }
             }
         }
-        .padding(20)
+        .padding(22)
         .background(AppTheme.cardBackground)
         .clipShape(RoundedRectangle(cornerRadius: AppTheme.cardCornerRadius))
-        .shadow(color: AppTheme.cardShadow, radius: 12, y: 4)
+        .shadow(color: AppTheme.cardShadow, radius: 16, y: 6)
     }
 }

+ 14 - 24
gramora/Views/SidebarView.swift

@@ -7,24 +7,22 @@ struct SidebarView: View {
         VStack(alignment: .leading, spacing: 0) {
             logoHeader
                 .padding(.horizontal, 20)
-                .padding(.top, 28)
-                .padding(.bottom, 24)
+                .padding(.top, 24)
+                .padding(.bottom, 20)
 
-            ScrollView(.vertical, showsIndicators: false) {
-                VStack(spacing: 2) {
-                    ForEach(NavigationDestination.allCases) { destination in
-                        SidebarNavItem(
-                            destination: destination,
-                            isSelected: selectedDestination == destination
-                        ) {
-                            selectedDestination = destination
-                        }
+            VStack(spacing: 2) {
+                ForEach(NavigationDestination.allCases) { destination in
+                    SidebarNavItem(
+                        destination: destination,
+                        isSelected: selectedDestination == destination
+                    ) {
+                        selectedDestination = destination
                     }
                 }
-                .padding(.horizontal, 12)
             }
+            .padding(.horizontal, 12)
 
-            Spacer(minLength: 16)
+            Spacer(minLength: 12)
 
             PremiumCardView()
                 .padding(.horizontal, 16)
@@ -66,9 +64,9 @@ private struct SidebarNavItem: View {
     var body: some View {
         Button(action: action) {
             HStack(spacing: 10) {
-                Image(systemName: destination.iconName)
-                    .font(.system(size: 13))
-                    .frame(width: 18)
+                Image(systemName: destination.iconName(isSelected: isSelected))
+                    .font(.system(size: 14))
+                    .frame(width: 20)
 
                 Text(destination.title)
                     .font(.system(size: 13, weight: isSelected ? .semibold : .regular))
@@ -83,14 +81,6 @@ private struct SidebarNavItem: View {
                 RoundedRectangle(cornerRadius: AppTheme.navItemCornerRadius)
                     .fill(isSelected ? AppTheme.tealLight : Color.clear)
             )
-            .overlay(alignment: .leading) {
-                if isSelected {
-                    RoundedRectangle(cornerRadius: 2)
-                        .fill(AppTheme.teal)
-                        .frame(width: 3, height: 20)
-                        .offset(x: -4)
-                }
-            }
         }
         .buttonStyle(.plain)
     }