Bläddra i källkod

Make grammar checker and paywall sizing responsive while keeping paywall vertically pinned.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 månad sedan
förälder
incheckning
554f1b10e8
2 ändrade filer med 43 tillägg och 33 borttagningar
  1. 22 14
      gramora/Views/GrammarCheckerView.swift
  2. 21 19
      gramora/Views/PaywallView.swift

+ 22 - 14
gramora/Views/GrammarCheckerView.swift

@@ -4,22 +4,30 @@ struct GrammarCheckerView: View {
     @StateObject private var viewModel = GrammarCheckerViewModel()
 
     var body: some View {
-        ScrollView(.vertical, showsIndicators: false) {
-            VStack(alignment: .leading, spacing: 0) {
-                header
-                    .padding(.bottom, 28)
+        GeometryReader { proxy in
+            let dynamicHorizontalPadding = min(max(proxy.size.width * 0.045, AppTheme.contentPadding), 52)
+            let dynamicTopPadding = min(max(proxy.size.height * 0.05, 24), 44)
+            let dynamicBottomPadding = min(max(proxy.size.height * 0.06, 28), 56)
+            let dynamicEditorHeight = min(max(proxy.size.height * 0.43, 260), 560)
 
-                contentCard
-                    .padding(.bottom, 24)
+            ScrollView(.vertical, showsIndicators: false) {
+                VStack(alignment: .leading, spacing: 0) {
+                    header
+                        .padding(.bottom, 28)
 
-                GradientButton(title: "Check Grammar") {
-                    viewModel.checkGrammar()
+                    contentCard(editorHeight: dynamicEditorHeight)
+                        .padding(.bottom, 24)
+
+                    GradientButton(title: "Check Grammar") {
+                        viewModel.checkGrammar()
+                    }
                 }
+                .frame(maxWidth: 1200, alignment: .topLeading)
+                .frame(maxWidth: .infinity, alignment: .topLeading)
+                .padding(.horizontal, dynamicHorizontalPadding)
+                .padding(.top, dynamicTopPadding)
+                .padding(.bottom, dynamicBottomPadding)
             }
-            .frame(maxWidth: .infinity, alignment: .topLeading)
-            .padding(.horizontal, AppTheme.contentPadding)
-            .padding(.top, 32)
-            .padding(.bottom, 40)
         }
         .onChange(of: viewModel.text) { newValue in
             if newValue.count > GrammarCheckerViewModel.maxCharacters {
@@ -61,7 +69,7 @@ struct GrammarCheckerView: View {
         }
     }
 
-    private var contentCard: some View {
+    private func contentCard(editorHeight: CGFloat) -> some View {
         VStack(alignment: .leading, spacing: 16) {
             Image("YourContentLogo")
                 .resizable()
@@ -75,7 +83,7 @@ struct GrammarCheckerView: View {
                 ThinCaretTextEditor(text: $viewModel.text)
                     .font(.system(size: 14))
                     .foregroundStyle(AppTheme.textPrimary)
-                    .frame(minHeight: 320)
+                    .frame(minHeight: editorHeight, idealHeight: editorHeight)
                     .background(Color.white)
                     .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
                     .overlay(

+ 21 - 19
gramora/Views/PaywallView.swift

@@ -7,28 +7,27 @@ struct PaywallView: View {
 
     var body: some View {
         GeometryReader { proxy in
-            let availableWidth = max(320, proxy.size.width - 24)
+            let horizontalInset = min(max(proxy.size.width * 0.03, 12), 34)
+            let availableWidth = max(320, proxy.size.width - (horizontalInset * 2))
             let availableHeight = max(420, proxy.size.height - 24)
             let baseWidth: CGFloat = 860
-            let baseContentHeight: CGFloat = 610
-            let topChromeHeight: CGFloat = 44
-            let totalBaseHeight = baseContentHeight + topChromeHeight
-            let scale = min(1, availableWidth / baseWidth, availableHeight / totalBaseHeight)
+            let contentWidth = min(baseWidth, availableWidth)
+            let heightScale = min(max(availableHeight / 680, 0.82), 1.2)
+            let verticalSpacing: CGFloat = 18
+            let topPadding: CGFloat = 10
+            let featureHeight = max(72, min(108, 84 * heightScale))
+            let planHeight = max(150, min(210, 170 * heightScale))
 
             VStack(spacing: 0) {
-                VStack(spacing: 18) {
+                VStack(spacing: verticalSpacing) {
                     titleSection
-                    featuresSection
-                    plansSection
+                    featuresSection(featureHeight: featureHeight)
+                    plansSection(planHeight: planHeight)
                     ctaSection
-                    Spacer(minLength: 0)
                     legalSection
                 }
-                .frame(width: baseWidth, height: baseContentHeight, alignment: .top)
-                .scaleEffect(scale, anchor: .top)
-                .padding(.top, 10)
-
-                Spacer(minLength: 0)
+                .frame(width: contentWidth, alignment: .top)
+                .padding(.top, topPadding)
             }
             .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .top)
             .background(Color.white.ignoresSafeArea())
@@ -57,20 +56,21 @@ struct PaywallView: View {
         }
     }
 
-    private var featuresSection: some View {
+    private func featuresSection(featureHeight: CGFloat) -> some View {
         LazyVGrid(columns: Array(repeating: GridItem(.flexible(), spacing: 14), count: 3), spacing: 14) {
             ForEach(viewModel.features) { feature in
-                PaywallFeatureBox(feature: feature)
+                PaywallFeatureBox(feature: feature, minHeight: featureHeight)
             }
         }
     }
 
-    private var plansSection: some View {
+    private func plansSection(planHeight: CGFloat) -> some View {
         HStack(spacing: 14) {
             ForEach(PaywallPlan.allCases) { plan in
                 PaywallPlanCardView(
                     plan: plan,
                     isSelected: viewModel.selectedPlan == plan,
+                    minHeight: planHeight,
                     onSelect: { viewModel.selectPlan(plan) }
                 )
             }
@@ -146,6 +146,7 @@ struct PaywallView: View {
 
 private struct PaywallFeatureBox: View {
     let feature: PaywallFeature
+    let minHeight: CGFloat
     @State private var isHovered = false
 
     var body: some View {
@@ -175,7 +176,7 @@ private struct PaywallFeatureBox: View {
         }
         .padding(.horizontal, 12)
         .padding(.vertical, 12)
-        .frame(maxWidth: .infinity, minHeight: 84, maxHeight: 84, alignment: .topLeading)
+        .frame(maxWidth: .infinity, minHeight: minHeight, alignment: .topLeading)
         .background(isHovered ? AppTheme.tealLight.opacity(0.55) : AppTheme.cardBackground)
         .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
         .overlay(
@@ -192,6 +193,7 @@ private struct PaywallFeatureBox: View {
 private struct PaywallPlanCardView: View {
     let plan: PaywallPlan
     let isSelected: Bool
+    let minHeight: CGFloat
     let onSelect: () -> Void
     @State private var isHovered = false
 
@@ -265,7 +267,7 @@ private struct PaywallPlanCardView: View {
                     RoundedRectangle(cornerRadius: 0)
                 )
             }
-            .frame(maxWidth: .infinity, minHeight: 170, alignment: .topLeading)
+            .frame(maxWidth: .infinity, minHeight: minHeight, alignment: .topLeading)
             .background((isSelected || isHovered) ? AppTheme.tealLight.opacity(isSelected ? 0.28 : 0.16) : AppTheme.cardBackground)
             .clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
             .overlay(