Browse Source

Add consistent hover effects across all interactive UI components.

Introduce a shared hover style system so buttons, sidebar items, tabs, pickers, and cards give clear visual feedback on macOS.

Co-authored-by: Cursor <cursoragent@cursor.com>
AhtashamShahzad1 1 month ago
parent
commit
8166497969

+ 248 - 0
Reddit App/Utilities/InteractiveHoverStyle.swift

@@ -0,0 +1,248 @@
+import AppKit
+import SwiftUI
+
+// MARK: - Theme Tokens
+
+extension AppTheme {
+    static let hoverAnimation: Animation = .easeOut(duration: 0.15)
+    static let hoverFillOpacity: Double = 0.06
+    static let hoverBorderOpacity: Double = 0.14
+    static let hoverPressScale: CGFloat = 0.98
+    static let hoverLiftScale: CGFloat = 1.012
+
+    static var hoverFill: Color { Color.white.opacity(hoverFillOpacity) }
+    static var hoverBorder: Color { Color.white.opacity(hoverBorderOpacity) }
+}
+
+// MARK: - Button Styles
+
+struct AppPlainButtonStyle: ButtonStyle {
+    @State private var isHovered = false
+
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .scaleEffect(
+                configuration.isPressed ? AppTheme.hoverPressScale : (isHovered ? AppTheme.hoverLiftScale : 1)
+            )
+            .brightness(configuration.isPressed ? -0.04 : (isHovered ? 0.05 : 0))
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .animation(AppTheme.hoverAnimation, value: configuration.isPressed)
+            .onHover { isHovered = $0 }
+            .hoverCursor()
+    }
+}
+
+struct AppSecondaryButtonStyle: ButtonStyle {
+    @State private var isHovered = false
+
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .foregroundStyle(AppTheme.textSecondary)
+            .padding(.horizontal, 12)
+            .padding(.vertical, 8)
+            .background(secondaryBackground(isPressed: configuration.isPressed))
+            .clipShape(RoundedRectangle(cornerRadius: 8))
+            .overlay(
+                RoundedRectangle(cornerRadius: 8)
+                    .stroke(
+                        isHovered || configuration.isPressed
+                            ? AppTheme.hoverBorder
+                            : AppTheme.cardBorder,
+                        lineWidth: 1
+                    )
+            )
+            .scaleEffect(configuration.isPressed ? AppTheme.hoverPressScale : (isHovered ? AppTheme.hoverLiftScale : 1))
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .animation(AppTheme.hoverAnimation, value: configuration.isPressed)
+            .onHover { isHovered = $0 }
+            .hoverCursor()
+    }
+
+    private func secondaryBackground(isPressed: Bool) -> Color {
+        if isPressed {
+            return AppTheme.cardBackground.opacity(0.55)
+        }
+        if isHovered {
+            return AppTheme.cardBackground.opacity(0.85)
+        }
+        return AppTheme.cardBackground
+    }
+}
+
+struct AppPrimaryButtonStyle: ButtonStyle {
+    @State private var isHovered = false
+
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .brightness(configuration.isPressed ? -0.08 : (isHovered ? 0.08 : 0))
+            .scaleEffect(configuration.isPressed ? AppTheme.hoverPressScale : (isHovered ? AppTheme.hoverLiftScale : 1))
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .animation(AppTheme.hoverAnimation, value: configuration.isPressed)
+            .onHover { isHovered = $0 }
+            .hoverCursor()
+    }
+}
+
+struct AppTextLinkButtonStyle: ButtonStyle {
+    @State private var isHovered = false
+
+    func makeBody(configuration: Configuration) -> some View {
+        configuration.label
+            .foregroundStyle(isHovered ? AppTheme.textPrimary : AppTheme.textSecondary)
+            .underline(isHovered || configuration.isPressed)
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .animation(AppTheme.hoverAnimation, value: configuration.isPressed)
+            .onHover { isHovered = $0 }
+            .hoverCursor()
+    }
+}
+
+// MARK: - View Modifiers
+
+private struct HoverCursorModifier: ViewModifier {
+    @State private var isHovered = false
+
+    func body(content: Content) -> some View {
+        content
+            .onHover { hovering in
+                isHovered = hovering
+                if hovering {
+                    NSCursor.pointingHand.push()
+                } else {
+                    NSCursor.pop()
+                }
+            }
+    }
+}
+
+private struct HoverOverlayModifier: ViewModifier {
+    let cornerRadius: CGFloat
+    var isEnabled: Bool = true
+    @State private var isHovered = false
+
+    func body(content: Content) -> some View {
+        content
+            .overlay {
+                if isEnabled && isHovered {
+                    RoundedRectangle(cornerRadius: cornerRadius)
+                        .fill(AppTheme.hoverFill)
+                        .allowsHitTesting(false)
+                }
+            }
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .onHover { isHovered = $0 }
+    }
+}
+
+private struct HoverCardModifier: ViewModifier {
+    let cornerRadius: CGFloat
+    var isSelected: Bool = false
+    var isEnabled: Bool = true
+    @State private var isHovered = false
+
+    func body(content: Content) -> some View {
+        content
+            .overlay {
+                if isEnabled && isHovered {
+                    RoundedRectangle(cornerRadius: cornerRadius)
+                        .fill(isSelected ? Color.white.opacity(0.04) : AppTheme.hoverFill)
+                        .allowsHitTesting(false)
+                }
+            }
+            .overlay {
+                if isEnabled && isHovered {
+                    RoundedRectangle(cornerRadius: cornerRadius)
+                        .stroke(AppTheme.hoverBorder, lineWidth: 1)
+                        .allowsHitTesting(false)
+                }
+            }
+            .scaleEffect(isEnabled && isHovered ? AppTheme.hoverLiftScale : 1)
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .onHover { isHovered = $0 }
+    }
+}
+
+private struct SidebarRowHoverModifier: ViewModifier {
+    let isSelected: Bool
+    let showsCardBackground: Bool
+    let selectedBorderColor: Color
+    @State private var isHovered = false
+
+    func body(content: Content) -> some View {
+        content
+            .background(sidebarRowBackground)
+            .animation(AppTheme.hoverAnimation, value: isHovered)
+            .onHover { isHovered = $0 }
+    }
+
+    @ViewBuilder
+    private var sidebarRowBackground: some View {
+        if showsCardBackground {
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.quickAccessBackground)
+                .overlay(
+                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                        .stroke(
+                            isSelected
+                                ? selectedBorderColor
+                                : (isHovered ? AppTheme.hoverBorder : AppTheme.cardBorder),
+                            lineWidth: 1
+                        )
+                )
+                .overlay {
+                    if isHovered && !isSelected {
+                        RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                            .fill(AppTheme.hoverFill)
+                    }
+                }
+        } else if isSelected {
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(Color.white.opacity(0.05))
+        } else if isHovered {
+            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
+                .fill(AppTheme.hoverFill)
+        }
+    }
+}
+
+// MARK: - View Extensions
+
+extension View {
+    func hoverCursor(_ enabled: Bool = true) -> some View {
+        modifier(HoverCursorEnabledModifier(enabled: enabled))
+    }
+
+    func hoverOverlay(cornerRadius: CGFloat, isEnabled: Bool = true) -> some View {
+        modifier(HoverOverlayModifier(cornerRadius: cornerRadius, isEnabled: isEnabled))
+    }
+
+    func hoverCard(cornerRadius: CGFloat, isSelected: Bool = false, isEnabled: Bool = true) -> some View {
+        modifier(HoverCardModifier(cornerRadius: cornerRadius, isSelected: isSelected, isEnabled: isEnabled))
+    }
+
+    func sidebarRowHover(
+        isSelected: Bool,
+        showsCardBackground: Bool = false,
+        selectedBorderColor: Color = .clear
+    ) -> some View {
+        modifier(
+            SidebarRowHoverModifier(
+                isSelected: isSelected,
+                showsCardBackground: showsCardBackground,
+                selectedBorderColor: selectedBorderColor
+            )
+        )
+    }
+}
+
+private struct HoverCursorEnabledModifier: ViewModifier {
+    let enabled: Bool
+
+    func body(content: Content) -> some View {
+        if enabled {
+            content.modifier(HoverCursorModifier())
+        } else {
+            content
+        }
+    }
+}

+ 5 - 4
Reddit App/Views/CommentWriterView.swift

@@ -114,7 +114,7 @@ struct CommentWriterView: View {
                 Label("Reset", systemImage: "arrow.counterclockwise")
                     .font(.system(size: 11, weight: .medium))
             }
-            .buttonStyle(CommentSecondaryButtonStyle())
+            .buttonStyle(AppSecondaryButtonStyle())
 
             Button {
                 viewModel.copyToClipboard()
@@ -122,7 +122,7 @@ struct CommentWriterView: View {
                 Label("Copy", systemImage: "doc.on.doc")
                     .font(.system(size: 11, weight: .medium))
             }
-            .buttonStyle(CommentSecondaryButtonStyle())
+            .buttonStyle(AppSecondaryButtonStyle())
             .disabled(!viewModel.canExport)
 
             Button {
@@ -147,7 +147,7 @@ struct CommentWriterView: View {
                 .background(viewModel.canGenerate ? AppTheme.accentGreen : AppTheme.accentGreen.opacity(0.4))
                 .clipShape(RoundedRectangle(cornerRadius: 8))
             }
-            .buttonStyle(.plain)
+            .buttonStyle(AppPrimaryButtonStyle())
             .disabled(!viewModel.canGenerate)
         }
     }
@@ -186,7 +186,8 @@ struct CommentWriterView: View {
                     )
                     .clipShape(RoundedRectangle(cornerRadius: 7))
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+                .hoverOverlay(cornerRadius: 7, isEnabled: viewModel.selectedTab != tab)
             }
         }
         .padding(3)

+ 21 - 17
Reddit App/Views/Components/CommentWriterComponents.swift

@@ -52,7 +52,8 @@ struct CommentTypePicker: View {
                             )
                     )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             }
         }
     }
@@ -110,7 +111,8 @@ struct CommentIntentPicker: View {
                             )
                     )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             }
         }
     }
@@ -151,7 +153,8 @@ struct CommentLengthPicker: View {
                             .stroke(AppTheme.cardBorder, lineWidth: 1)
                     )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             }
         }
     }
@@ -190,7 +193,8 @@ struct CommentTonePicker: View {
                                     )
                             )
                     }
-                    .buttonStyle(.plain)
+                    .buttonStyle(AppPlainButtonStyle())
+                    .hoverOverlay(cornerRadius: 50)
                 }
             }
         }
@@ -224,7 +228,8 @@ struct CommentVariantCountPicker: View {
                                 .stroke(AppTheme.cardBorder, lineWidth: 1)
                         )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             }
         }
     }
@@ -248,6 +253,10 @@ struct CommentToggleRow: View {
         }
         .toggleStyle(.switch)
         .tint(AppTheme.accentGreen)
+        .padding(.horizontal, 10)
+        .padding(.vertical, 8)
+        .hoverOverlay(cornerRadius: 8)
+        .hoverCursor()
     }
 }
 
@@ -537,7 +546,8 @@ struct CommentVariantRow: View {
                     Button("Select", action: onSelect)
                         .font(.system(size: 9, weight: .semibold))
                         .foregroundStyle(isSelected ? AppTheme.accentGreen : AppTheme.textSecondary)
-                        .buttonStyle(.plain)
+                        .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
 
                     Button("Apply", action: onApply)
                         .font(.system(size: 9, weight: .semibold))
@@ -546,7 +556,8 @@ struct CommentVariantRow: View {
                         .padding(.vertical, 4)
                         .background(AppTheme.accentGreen.opacity(0.8))
                         .clipShape(RoundedRectangle(cornerRadius: 5))
-                        .buttonStyle(.plain)
+                        .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
                 }
             }
 
@@ -569,6 +580,8 @@ struct CommentVariantRow: View {
                     lineWidth: 1
                 )
         )
+        .hoverCard(cornerRadius: 8, isSelected: isSelected)
+        .hoverCursor()
         .onTapGesture(perform: onSelect)
     }
 }
@@ -620,15 +633,6 @@ struct CommentWriterEmptyState: View {
 
 struct CommentSecondaryButtonStyle: ButtonStyle {
     func makeBody(configuration: Configuration) -> some View {
-        configuration.label
-            .foregroundStyle(AppTheme.textSecondary)
-            .padding(.horizontal, 12)
-            .padding(.vertical, 8)
-            .background(AppTheme.cardBackground.opacity(configuration.isPressed ? 0.6 : 1))
-            .clipShape(RoundedRectangle(cornerRadius: 8))
-            .overlay(
-                RoundedRectangle(cornerRadius: 8)
-                    .stroke(AppTheme.cardBorder, lineWidth: 1)
-            )
+        AppSecondaryButtonStyle().makeBody(configuration: configuration)
     }
 }

+ 6 - 3
Reddit App/Views/Components/PostGeneratorComponents.swift

@@ -176,7 +176,8 @@ private struct PostTypeButton: View {
                     )
             )
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
     }
 }
 
@@ -209,7 +210,8 @@ struct PostTagToggle: View {
                     .stroke(isOn ? activeColor.opacity(0.4) : AppTheme.cardBorder, lineWidth: 1)
             )
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
     }
 }
 
@@ -240,7 +242,8 @@ struct TonePicker: View {
                                     )
                             )
                     }
-                    .buttonStyle(.plain)
+                    .buttonStyle(AppPlainButtonStyle())
+                    .hoverOverlay(cornerRadius: 50)
                 }
             }
         }

+ 4 - 2
Reddit App/Views/Components/SettingsComponents.swift

@@ -41,7 +41,8 @@ struct SettingsShareAppButton: View {
                     )
             )
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
+        .hoverCard(cornerRadius: AppTheme.cornerRadiusSmall)
         .frame(maxWidth: .infinity, alignment: .leading)
     }
 }
@@ -90,7 +91,8 @@ struct SettingsLinkRow: View {
                     .stroke(AppTheme.cardBorder, lineWidth: 1)
             )
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
+        .hoverCard(cornerRadius: 8)
         .frame(maxWidth: .infinity, alignment: .leading)
     }
 }

+ 7 - 21
Reddit App/Views/Components/SidebarNavItemView.swift

@@ -15,7 +15,7 @@ struct SidebarNavItemView: View {
                 ModernSidebarIconView(kind: item.iconKind)
             }
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
     }
 }
 
@@ -36,7 +36,7 @@ struct SidebarQuickAccessItemView: View {
                 ModernRedditIconView()
             }
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
     }
 }
 
@@ -77,25 +77,11 @@ private struct SidebarItemRow<Icon: View>: View {
         .padding(.horizontal, AppTheme.sidebarRowPaddingH)
         .padding(.vertical, AppTheme.sidebarRowPaddingV)
         .contentShape(RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall))
-        .background(rowBackground)
-    }
-
-    @ViewBuilder
-    private var rowBackground: some View {
-        if showsCardBackground {
-            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
-                .fill(AppTheme.quickAccessBackground)
-                .overlay(
-                    RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
-                        .stroke(
-                            isSelected ? selectedBorderColor : AppTheme.cardBorder,
-                            lineWidth: 1
-                        )
-                )
-        } else if isSelected {
-            RoundedRectangle(cornerRadius: AppTheme.cornerRadiusSmall)
-                .fill(Color.white.opacity(0.05))
-        }
+        .sidebarRowHover(
+            isSelected: isSelected,
+            showsCardBackground: showsCardBackground,
+            selectedBorderColor: selectedBorderColor
+        )
     }
 }
 

+ 11 - 4
Reddit App/Views/Components/TitleOptimizerComponents.swift

@@ -44,7 +44,8 @@ struct TitleGoalPicker: View {
                             )
                     )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             }
         }
     }
@@ -79,7 +80,8 @@ struct TitleVariantCountPicker: View {
                                 .stroke(AppTheme.cardBorder, lineWidth: 1)
                         )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             }
         }
     }
@@ -229,7 +231,8 @@ struct TitleVariantRow: View {
                         }
                     }
             }
-            .buttonStyle(.plain)
+            .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
             .padding(.top, 2)
 
             VStack(alignment: .leading, spacing: 6) {
@@ -262,7 +265,8 @@ struct TitleVariantRow: View {
                             .font(.system(size: 10, weight: .semibold))
                             .foregroundStyle(AppTheme.accentBlue)
                     }
-                    .buttonStyle(.plain)
+                    .buttonStyle(AppPlainButtonStyle())
+        .hoverOverlay(cornerRadius: 8)
                 }
             }
         }
@@ -278,6 +282,7 @@ struct TitleVariantRow: View {
                     lineWidth: 1
                 )
         )
+        .hoverCard(cornerRadius: 8, isSelected: isSelected)
     }
 }
 
@@ -348,6 +353,8 @@ struct TitleCompareCard: View {
                         )
                 )
         )
+        .hoverCard(cornerRadius: AppTheme.cornerRadiusSmall, isSelected: isSelected)
+        .hoverCursor()
     }
 }
 

+ 5 - 4
Reddit App/Views/PaywallView.swift

@@ -90,7 +90,7 @@ struct PaywallView: View {
                                 )
                         )
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
                 .help("Close")
             }
             Spacer()
@@ -183,7 +183,7 @@ struct PaywallView: View {
                 )
                 .clipShape(RoundedRectangle(cornerRadius: metrics.cornerRadius))
             }
-            .buttonStyle(.plain)
+            .buttonStyle(AppPrimaryButtonStyle())
             .disabled(buttonsBusy || subscriptions.isLoadingProducts)
             .opacity((buttonsBusy || subscriptions.isLoadingProducts) ? 0.7 : 1)
 
@@ -242,7 +242,7 @@ struct PaywallView: View {
 
     private func footerLink(_ title: String, metrics: PaywallLayoutMetrics, action: @escaping () -> Void) -> some View {
         Button(title, action: action)
-            .buttonStyle(.plain)
+            .buttonStyle(AppTextLinkButtonStyle())
             .font(.system(size: metrics.footerLinkFontSize))
             .foregroundStyle(AppTheme.textSecondary)
             .lineLimit(1)
@@ -404,7 +404,8 @@ private struct PaywallPricingCard: View {
                 }
             }
         }
-        .buttonStyle(.plain)
+        .buttonStyle(AppPlainButtonStyle())
+        .hoverCard(cornerRadius: metrics.cornerRadius, isSelected: isSelected)
     }
 }
 

+ 16 - 19
Reddit App/Views/PostGeneratorView.swift

@@ -130,7 +130,7 @@ struct PostGeneratorView: View {
                 Label("Reset", systemImage: "arrow.counterclockwise")
                     .font(.system(size: 11, weight: .medium))
             }
-            .buttonStyle(PostSecondaryButtonStyle())
+            .buttonStyle(AppSecondaryButtonStyle())
 
             Button {
                 viewModel.copyToClipboard()
@@ -138,7 +138,7 @@ struct PostGeneratorView: View {
                 Label("Copy", systemImage: "doc.on.doc")
                     .font(.system(size: 11, weight: .medium))
             }
-            .buttonStyle(PostSecondaryButtonStyle())
+            .buttonStyle(AppSecondaryButtonStyle())
             .disabled(!viewModel.canExport)
 
             Button {
@@ -163,7 +163,7 @@ struct PostGeneratorView: View {
                 .background(viewModel.canGenerate ? AppTheme.accentPurple : AppTheme.accentPurple.opacity(0.4))
                 .clipShape(RoundedRectangle(cornerRadius: 8))
             }
-            .buttonStyle(.plain)
+            .buttonStyle(AppPrimaryButtonStyle())
             .disabled(!viewModel.canGenerate)
         }
     }
@@ -190,7 +190,8 @@ struct PostGeneratorView: View {
                         )
                         .clipShape(RoundedRectangle(cornerRadius: 7))
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+                .hoverOverlay(cornerRadius: 7, isEnabled: viewModel.selectedTab != tab)
             }
         }
         .padding(3)
@@ -471,7 +472,8 @@ struct PostGeneratorView: View {
                                     .font(.system(size: 14))
                                     .foregroundStyle(Color(hex: 0xEF4444).opacity(0.8))
                             }
-                            .buttonStyle(.plain)
+                            .buttonStyle(AppPlainButtonStyle())
+                            .hoverOverlay(cornerRadius: 7)
                             .padding(.top, 18)
                         }
                     }
@@ -485,7 +487,8 @@ struct PostGeneratorView: View {
                             .font(.system(size: 11, weight: .medium))
                             .foregroundStyle(AppTheme.accentPurpleLight)
                     }
-                    .buttonStyle(.plain)
+                    .buttonStyle(AppPlainButtonStyle())
+                    .hoverOverlay(cornerRadius: 6)
                 }
 
                 VStack(alignment: .leading, spacing: 6) {
@@ -519,7 +522,8 @@ struct PostGeneratorView: View {
                                             .stroke(AppTheme.cardBorder, lineWidth: 1)
                                     )
                             }
-                            .buttonStyle(.plain)
+                            .buttonStyle(AppPlainButtonStyle())
+                            .hoverOverlay(cornerRadius: 7)
                         }
                     }
                 }
@@ -553,7 +557,8 @@ struct PostGeneratorView: View {
                         .foregroundStyle(.white)
                         .shadow(radius: 2)
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+                .hoverOverlay(cornerRadius: 8)
                 .padding(8)
             }
         } else {
@@ -583,7 +588,8 @@ struct PostGeneratorView: View {
                         )
                 )
             }
-            .buttonStyle(.plain)
+            .buttonStyle(AppPlainButtonStyle())
+            .hoverCard(cornerRadius: 8)
         }
     }
 
@@ -729,16 +735,7 @@ struct PostGeneratorView: View {
 
 private struct PostSecondaryButtonStyle: ButtonStyle {
     func makeBody(configuration: Configuration) -> some View {
-        configuration.label
-            .foregroundStyle(AppTheme.textSecondary)
-            .padding(.horizontal, 12)
-            .padding(.vertical, 8)
-            .background(AppTheme.cardBackground.opacity(configuration.isPressed ? 0.6 : 1))
-            .clipShape(RoundedRectangle(cornerRadius: 8))
-            .overlay(
-                RoundedRectangle(cornerRadius: 8)
-                    .stroke(AppTheme.cardBorder, lineWidth: 1)
-            )
+        AppSecondaryButtonStyle().makeBody(configuration: configuration)
     }
 }
 

+ 1 - 1
Reddit App/Views/SidebarView.swift

@@ -128,7 +128,7 @@ struct SidebarView: View {
                 .padding(.vertical, 6)
                 .background(AppTheme.accentPurple)
                 .clipShape(RoundedRectangle(cornerRadius: 8))
-                .buttonStyle(.plain)
+                .buttonStyle(AppPrimaryButtonStyle())
         }
         .frame(maxWidth: .infinity, minHeight: 105)
         .padding(.horizontal, 12)

+ 6 - 14
Reddit App/Views/TitleOptimizerView.swift

@@ -88,7 +88,7 @@ struct TitleOptimizerView: View {
                 Label("Reset", systemImage: "arrow.counterclockwise")
                     .font(.system(size: 11, weight: .medium))
             }
-            .buttonStyle(TitleSecondaryButtonStyle())
+            .buttonStyle(AppSecondaryButtonStyle())
 
             Button {
                 viewModel.copyToClipboard()
@@ -96,7 +96,7 @@ struct TitleOptimizerView: View {
                 Label("Copy", systemImage: "doc.on.doc")
                     .font(.system(size: 11, weight: .medium))
             }
-            .buttonStyle(TitleSecondaryButtonStyle())
+            .buttonStyle(AppSecondaryButtonStyle())
             .disabled(viewModel.activeTitle.trimmingCharacters(in: .whitespaces).isEmpty)
 
             Button {
@@ -121,7 +121,7 @@ struct TitleOptimizerView: View {
                 .background(viewModel.canOptimize ? AppTheme.accentBlue : AppTheme.accentBlue.opacity(0.4))
                 .clipShape(RoundedRectangle(cornerRadius: 8))
             }
-            .buttonStyle(.plain)
+            .buttonStyle(AppPrimaryButtonStyle())
             .disabled(!viewModel.canOptimize)
         }
     }
@@ -148,7 +148,8 @@ struct TitleOptimizerView: View {
                         )
                         .clipShape(RoundedRectangle(cornerRadius: 7))
                 }
-                .buttonStyle(.plain)
+                .buttonStyle(AppPlainButtonStyle())
+                .hoverOverlay(cornerRadius: 7, isEnabled: viewModel.selectedTab != tab)
             }
         }
         .padding(3)
@@ -592,16 +593,7 @@ struct TitleOptimizerView: View {
 
 private struct TitleSecondaryButtonStyle: ButtonStyle {
     func makeBody(configuration: Configuration) -> some View {
-        configuration.label
-            .foregroundStyle(AppTheme.textSecondary)
-            .padding(.horizontal, 12)
-            .padding(.vertical, 8)
-            .background(AppTheme.cardBackground.opacity(configuration.isPressed ? 0.6 : 1))
-            .clipShape(RoundedRectangle(cornerRadius: 8))
-            .overlay(
-                RoundedRectangle(cornerRadius: 8)
-                    .stroke(AppTheme.cardBorder, lineWidth: 1)
-            )
+        AppSecondaryButtonStyle().makeBody(configuration: configuration)
     }
 }