Quellcode durchsuchen

Redesign Comment Writer layout for clearer alignment and hierarchy.

Match the Post Generator and Title Optimizer patterns with a segmented tab control, two-column compose view, and consistent spacing so the tool feels more polished and easier to scan.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 vor 1 Monat
Ursprung
Commit
f2010c962a

+ 177 - 91
Reddit App/Views/CommentWriterView.swift

@@ -4,25 +4,35 @@ struct CommentWriterView: View {
     @Environment(\.requirePremiumAccess) private var requirePremiumAccess
     @Bindable var viewModel: CommentWriterViewModel
 
+    private enum Layout {
+        static let horizontalPadding: CGFloat = 24
+        static let columnWidth: CGFloat = 300
+        static let columnSpacing: CGFloat = 20
+        static let sectionSpacing: CGFloat = 12
+    }
+
     var body: some View {
         VStack(spacing: 0) {
             header
             messageBanners
-            tabBar
 
             ScrollView {
-                PostPageBoundary {
-                    switch viewModel.selectedTab {
-                    case .compose:
-                        composeContent
-                    case .preview:
-                        previewContent
-                    case .variants:
-                        variantsContent
+                VStack(alignment: .leading, spacing: 12) {
+                    tabBar
+
+                    PostPageBoundary {
+                        switch viewModel.selectedTab {
+                        case .compose:
+                            composeContent
+                        case .preview:
+                            previewContent
+                        case .variants:
+                            variantsContent
+                        }
                     }
                 }
-                .padding(.horizontal, 24)
-                .padding(.top, 8)
+                .padding(.horizontal, Layout.horizontalPadding)
+                .padding(.top, 12)
                 .padding(.bottom, 24)
             }
         }
@@ -74,7 +84,7 @@ struct CommentWriterView: View {
 
             headerActions
         }
-        .padding(.horizontal, 24)
+        .padding(.horizontal, Layout.horizontalPadding)
         .padding(.vertical, 16)
         .overlay(alignment: .bottom) {
             Rectangle()
@@ -87,11 +97,11 @@ struct CommentWriterView: View {
     private var messageBanners: some View {
         if let error = viewModel.errorMessage {
             PostGeneratorMessageBanner(message: error, isError: true)
-                .padding(.horizontal, 24)
+                .padding(.horizontal, Layout.horizontalPadding)
                 .padding(.top, 8)
         } else if let success = viewModel.successMessage {
             PostGeneratorMessageBanner(message: success, isError: false)
-                .padding(.horizontal, 24)
+                .padding(.horizontal, Layout.horizontalPadding)
                 .padding(.top, 8)
         }
     }
@@ -145,10 +155,12 @@ struct CommentWriterView: View {
     // MARK: - Tab Bar
 
     private var tabBar: some View {
-        HStack(spacing: 4) {
+        HStack(spacing: 0) {
             ForEach(CommentWriterTab.allCases) { tab in
                 Button {
-                    viewModel.selectedTab = tab
+                    withAnimation(.easeInOut(duration: 0.15)) {
+                        viewModel.selectedTab = tab
+                    }
                 } label: {
                     HStack(spacing: 4) {
                         Text(tab.title)
@@ -165,43 +177,90 @@ struct CommentWriterView: View {
                         }
                     }
                     .foregroundStyle(viewModel.selectedTab == tab ? AppTheme.textPrimary : AppTheme.textSecondary)
-                    .padding(.horizontal, 14)
-                    .padding(.vertical, 8)
+                    .frame(maxWidth: .infinity)
+                    .padding(.vertical, 7)
                     .background(
                         viewModel.selectedTab == tab
                             ? AppTheme.cardBackground
                             : Color.clear
                     )
-                    .clipShape(RoundedRectangle(cornerRadius: 8))
+                    .clipShape(RoundedRectangle(cornerRadius: 7))
                 }
                 .buttonStyle(.plain)
             }
-
-            Spacer()
         }
-        .padding(.horizontal, 24)
-        .padding(.top, 12)
-        .padding(.bottom, 4)
+        .padding(3)
+        .frame(width: 300)
+        .background(AppTheme.panelBackground)
+        .clipShape(RoundedRectangle(cornerRadius: 9))
+        .overlay(
+            RoundedRectangle(cornerRadius: 9)
+                .stroke(AppTheme.cardBorder, lineWidth: 1)
+        )
     }
 
     // MARK: - Compose
 
     private var composeContent: some View {
         ViewThatFits(in: .horizontal) {
-            HStack(alignment: .top, spacing: 16) {
-                configurationPanel
-                editorPanel
+            composeColumns
+
+            VStack(alignment: .leading, spacing: Layout.columnSpacing) {
+                composeColumn(title: "Configuration", icon: "slider.horizontal.3") {
+                    configurationPanel
+                }
+                composeColumn(title: "Your Comment", icon: "bubble.left") {
+                    editorPanel
+                }
             }
+        }
+    }
 
-            VStack(alignment: .leading, spacing: 16) {
+    private var composeColumns: some View {
+        HStack(alignment: .top, spacing: Layout.columnSpacing) {
+            composeColumn(title: "Configuration", icon: "slider.horizontal.3") {
                 configurationPanel
+            }
+            .frame(width: Layout.columnWidth)
+
+            Rectangle()
+                .fill(AppTheme.border)
+                .frame(width: 1)
+                .padding(.vertical, 2)
+
+            composeColumn(title: "Your Comment", icon: "bubble.left") {
                 editorPanel
             }
+            .frame(maxWidth: .infinity)
         }
     }
 
+    private func composeColumn<Content: View>(
+        title: String,
+        icon: String,
+        @ViewBuilder content: () -> Content
+    ) -> some View {
+        VStack(alignment: .leading, spacing: Layout.sectionSpacing) {
+            sectionHeader(title: title, icon: icon)
+            content()
+        }
+    }
+
+    private func sectionHeader(title: String, icon: String) -> some View {
+        HStack(spacing: 6) {
+            Image(systemName: icon)
+                .font(.system(size: 10, weight: .semibold))
+                .foregroundStyle(AppTheme.accentGreen)
+            Text(title)
+                .font(.system(size: 11, weight: .semibold))
+                .foregroundStyle(AppTheme.textSecondary)
+        }
+        .textCase(.uppercase)
+        .tracking(0.4)
+    }
+
     private var configurationPanel: some View {
-        VStack(spacing: 12) {
+        VStack(spacing: Layout.sectionSpacing) {
             PostFormCard(title: "Comment Type", subtitle: "Top-level or reply to a comment") {
                 CommentTypePicker(selectedType: Binding(
                     get: { viewModel.draft.commentType },
@@ -220,9 +279,7 @@ struct CommentWriterView: View {
 
                     if !viewModel.draft.subreddit.isEmpty,
                        !PostDraftValidator.isValidSubreddit(viewModel.draft.subreddit) {
-                        Text("Use 3–21 characters: letters, numbers, underscores only.")
-                            .font(.system(size: 9))
-                            .foregroundStyle(Color(hex: 0xF87171))
+                        validationHint("Use 3–21 characters: letters, numbers, underscores only.")
                     }
 
                     PostFormField(
@@ -255,9 +312,7 @@ struct CommentWriterView: View {
 
                     if !viewModel.draft.postURL.isEmpty,
                        !CommentDraftValidator.isValidRedditURL(viewModel.draft.postURL) {
-                        Text("Enter a valid reddit.com URL to open the thread.")
-                            .font(.system(size: 9))
-                            .foregroundStyle(Color(hex: 0xF87171))
+                        validationHint("Enter a valid reddit.com URL to open the thread.")
                     }
                 }
             }
@@ -313,11 +368,10 @@ struct CommentWriterView: View {
 
             RedditCommentEtiquetteCard()
         }
-        .frame(minWidth: 280, idealWidth: 300, maxWidth: 320)
     }
 
     private var editorPanel: some View {
-        VStack(spacing: 12) {
+        VStack(spacing: Layout.sectionSpacing) {
             PostFormCard(
                 title: "Your Comment",
                 subtitle: viewModel.draft.useMarkdown
@@ -342,13 +396,12 @@ struct CommentWriterView: View {
                 formattedSubreddit: viewModel.formattedSubreddit
             )
         }
-        .frame(maxWidth: .infinity)
     }
 
     private var characterCountFooter: some View {
-        HStack {
+        HStack(spacing: 12) {
             Text("\(viewModel.draft.body.count) / \(CommentDraftValidator.maxCommentLength)")
-                .font(.system(size: 10))
+                .font(.system(size: 10, weight: .medium, design: .monospaced))
                 .foregroundStyle(
                     viewModel.draft.body.count > CommentDraftValidator.maxCommentLength
                         ? Color(hex: 0xF87171)
@@ -361,45 +414,60 @@ struct CommentWriterView: View {
                 .font(.system(size: 10))
                 .foregroundStyle(AppTheme.textTertiary)
         }
+        .padding(.horizontal, 12)
+        .padding(.vertical, 9)
+        .background(
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.cardBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                        .stroke(AppTheme.cardBorder, lineWidth: 1)
+                )
+        )
     }
 
     // MARK: - Preview
 
     private var previewContent: some View {
-        HStack {
-            Spacer(minLength: 0)
-
-            VStack(spacing: 16) {
-                Text("Live Preview")
-                    .font(.system(size: 12, weight: .semibold))
-                    .foregroundStyle(AppTheme.textSecondary)
-                    .frame(maxWidth: .infinity, alignment: .leading)
+        VStack(alignment: .leading, spacing: Layout.columnSpacing) {
+            sectionHeader(title: "Live Preview", icon: "eye")
+
+            HStack {
+                Spacer(minLength: 0)
+
+                VStack(spacing: Layout.sectionSpacing) {
+                    RedditCommentPreviewCard(
+                        commentBody: viewModel.activeCommentBody,
+                        formattedSubreddit: viewModel.formattedSubreddit,
+                        commentType: viewModel.draft.commentType,
+                        parentComment: viewModel.draft.parentComment
+                    )
 
-                RedditCommentPreviewCard(
-                    commentBody: viewModel.activeCommentBody,
-                    formattedSubreddit: viewModel.formattedSubreddit,
-                    commentType: viewModel.draft.commentType,
-                    parentComment: viewModel.draft.parentComment
-                )
+                    previewMetadata
+                }
+                .frame(maxWidth: 560)
 
-                previewMetadata
+                Spacer(minLength: 0)
             }
-            .frame(maxWidth: 560)
-
-            Spacer(minLength: 0)
         }
     }
 
     private var previewMetadata: some View {
-        VStack(alignment: .leading, spacing: 8) {
+        VStack(alignment: .leading, spacing: 0) {
             metadataRow(label: "Type", value: viewModel.draft.commentType.title)
+            metadataDivider
             metadataRow(label: "Subreddit", value: viewModel.formattedSubreddit)
+            metadataDivider
             metadataRow(label: "Intent", value: viewModel.draft.intent.title)
+            metadataDivider
             metadataRow(label: "Tone", value: viewModel.draft.tone.title)
+            metadataDivider
             metadataRow(label: "Length", value: viewModel.draft.length.title)
+            metadataDivider
             metadataRow(label: "Engine", value: viewModel.usesLiveAI ? "OpenAI" : "Local templates")
 
             if let best = viewModel.bestVariant {
+                metadataDivider
                 metadataRow(label: "Best Score", value: "\(best.score)/100")
             }
         }
@@ -418,36 +486,43 @@ struct CommentWriterView: View {
     // MARK: - Variants
 
     private var variantsContent: some View {
-        VStack(spacing: 16) {
+        VStack(alignment: .leading, spacing: Layout.columnSpacing) {
+            variantsHeader
+
             if viewModel.variants.isEmpty {
                 CommentWriterEmptyState(
                     icon: "bubble.left.and.bubble.right",
                     title: "No variants yet",
                     subtitle: "Fill in thread context on the Compose tab,\nthen tap Generate to get comment variants."
                 )
+                .frame(maxWidth: .infinity)
             } else {
-                variantsHeader
-
-                VStack(spacing: 8) {
-                    ForEach(viewModel.variants) { variant in
-                        CommentVariantRow(
-                            variant: variant,
-                            isSelected: viewModel.selectedVariantID == variant.id,
-                            onSelect: { viewModel.selectVariant(variant) },
-                            onApply: { viewModel.applyVariant(variant) }
-                        )
-                    }
+                variantsBody
+            }
+        }
+    }
+
+    private var variantsBody: some View {
+        VStack(spacing: Layout.sectionSpacing) {
+            VStack(spacing: 8) {
+                ForEach(viewModel.variants) { variant in
+                    CommentVariantRow(
+                        variant: variant,
+                        isSelected: viewModel.selectedVariantID == variant.id,
+                        onSelect: { viewModel.selectVariant(variant) },
+                        onApply: { viewModel.applyVariant(variant) }
+                    )
                 }
+            }
 
-                if !viewModel.activeCommentBody.isEmpty {
-                    PostFormCard(title: "Selected Preview", subtitle: "How this variant looks in a thread") {
-                        RedditCommentPreviewCard(
-                            commentBody: viewModel.activeCommentBody,
-                            formattedSubreddit: viewModel.formattedSubreddit,
-                            commentType: viewModel.draft.commentType,
-                            parentComment: viewModel.draft.parentComment
-                        )
-                    }
+            if !viewModel.activeCommentBody.isEmpty {
+                PostFormCard(title: "Selected Preview", subtitle: "How this variant looks in a thread") {
+                    RedditCommentPreviewCard(
+                        commentBody: viewModel.activeCommentBody,
+                        formattedSubreddit: viewModel.formattedSubreddit,
+                        commentType: viewModel.draft.commentType,
+                        parentComment: viewModel.draft.parentComment
+                    )
                 }
             }
         }
@@ -456,15 +531,8 @@ struct CommentWriterView: View {
     }
 
     private var variantsHeader: some View {
-        HStack {
-            VStack(alignment: .leading, spacing: 2) {
-                Text("Comment Variants")
-                    .font(.system(size: 12, weight: .semibold))
-                    .foregroundStyle(AppTheme.textPrimary)
-                Text("Tap a variant to preview, or Apply to use it")
-                    .font(.system(size: 10))
-                    .foregroundStyle(AppTheme.textSecondary)
-            }
+        HStack(alignment: .top) {
+            sectionHeader(title: "Comment Variants", icon: "bubble.left.and.bubble.right")
 
             Spacer()
 
@@ -477,22 +545,40 @@ struct CommentWriterView: View {
                         .font(.system(size: 10, weight: .semibold))
                         .foregroundStyle(AppTheme.textSecondary)
                 }
+                .padding(.top, 1)
             }
         }
     }
 
+    private var metadataDivider: some View {
+        Rectangle()
+            .fill(AppTheme.border)
+            .frame(height: 1)
+            .padding(.vertical, 6)
+    }
+
     private func metadataRow(label: String, value: String) -> some View {
-        HStack(alignment: .top) {
+        HStack(alignment: .top, spacing: 12) {
             Text(label)
                 .font(.system(size: 10, weight: .medium))
                 .foregroundStyle(AppTheme.textTertiary)
-                .frame(width: 70, alignment: .leading)
+                .frame(width: 72, alignment: .leading)
             Text(value)
                 .font(.system(size: 10))
                 .foregroundStyle(AppTheme.textSecondary)
                 .fixedSize(horizontal: false, vertical: true)
         }
     }
+
+    private func validationHint(_ message: String) -> some View {
+        HStack(spacing: 4) {
+            Image(systemName: "exclamationmark.circle.fill")
+                .font(.system(size: 9))
+            Text(message)
+                .font(.system(size: 9))
+        }
+        .foregroundStyle(Color(hex: 0xF87171))
+    }
 }
 
 #Preview {

+ 7 - 6
Reddit App/Views/Components/CommentWriterComponents.swift

@@ -31,6 +31,7 @@ struct CommentTypePicker: View {
 
                         Spacer(minLength: 0)
                     }
+                    .frame(maxWidth: .infinity, alignment: .leading)
                     .padding(.horizontal, 10)
                     .padding(.vertical, 8)
                     .background(
@@ -119,7 +120,7 @@ struct CommentLengthPicker: View {
     @Binding var selectedLength: CommentLength
 
     var body: some View {
-        HStack(spacing: 6) {
+        HStack(spacing: 8) {
             ForEach(CommentLength.allCases) { length in
                 Button {
                     selectedLength = length
@@ -200,7 +201,7 @@ struct CommentVariantCountPicker: View {
     @Binding var selectedCount: CommentVariantCount
 
     var body: some View {
-        HStack(spacing: 6) {
+        HStack(spacing: 8) {
             ForEach(CommentVariantCount.allCases) { count in
                 Button {
                     selectedCount = count
@@ -208,10 +209,10 @@ struct CommentVariantCountPicker: View {
                     Text(count.label)
                         .font(.system(size: 10, weight: .semibold))
                         .foregroundStyle(selectedCount == count ? .white : AppTheme.textSecondary)
-                        .padding(.horizontal, 12)
-                        .padding(.vertical, 6)
+                        .frame(maxWidth: .infinity)
+                        .padding(.vertical, 7)
                         .background(
-                            Capsule()
+                            RoundedRectangle(cornerRadius: 7)
                                 .fill(
                                     selectedCount == count
                                         ? AppTheme.accentGreen
@@ -219,7 +220,7 @@ struct CommentVariantCountPicker: View {
                                 )
                         )
                         .overlay(
-                            Capsule()
+                            RoundedRectangle(cornerRadius: 7)
                                 .stroke(AppTheme.cardBorder, lineWidth: 1)
                         )
                 }