|
|
@@ -164,7 +164,15 @@ struct IconButton: View {
|
|
|
Button(action: action) {
|
|
|
iconContent
|
|
|
.foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
|
|
|
- .padding(4)
|
|
|
+ .padding(6)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 6)
|
|
|
+ .fill(isHovered ? AppTheme.toolbarBackgroundHover : Color.clear)
|
|
|
+ )
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 6)
|
|
|
+ .stroke(isHovered ? AppTheme.toolbarBorderHover : Color.clear, lineWidth: 1)
|
|
|
+ )
|
|
|
.contentShape(Rectangle())
|
|
|
.scaleEffect(isHovered ? 1.05 : 1.0)
|
|
|
.animation(.easeOut(duration: 0.15), value: isHovered)
|
|
|
@@ -174,6 +182,165 @@ struct IconButton: View {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+struct CompactPrimaryButton: View {
|
|
|
+ let title: String
|
|
|
+ let iconName: String
|
|
|
+ var isEnabled: Bool = true
|
|
|
+ let action: () -> Void
|
|
|
+
|
|
|
+ @State private var isHovered = false
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: action) {
|
|
|
+ HStack(spacing: 8) {
|
|
|
+ Image(systemName: iconName)
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
+
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 14, weight: .semibold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(.white)
|
|
|
+ .padding(.horizontal, 20)
|
|
|
+ .padding(.vertical, 14)
|
|
|
+ .background(backgroundGradient)
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: AppTheme.inputCornerRadius))
|
|
|
+ .shadow(
|
|
|
+ color: isEnabled && isHovered ? AppTheme.primaryShadowHover : AppTheme.primaryShadow.opacity(isEnabled ? 1 : 0),
|
|
|
+ radius: isHovered ? 12 : 8,
|
|
|
+ y: isHovered ? 5 : 3
|
|
|
+ )
|
|
|
+ .scaleEffect(isEnabled && isHovered ? 1.02 : 1.0)
|
|
|
+ .animation(.easeOut(duration: 0.15), value: isHovered)
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .disabled(!isEnabled)
|
|
|
+ .onHover { isHovered = $0 }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var backgroundGradient: LinearGradient {
|
|
|
+ guard isEnabled else {
|
|
|
+ return LinearGradient(
|
|
|
+ colors: [AppTheme.border, AppTheme.border],
|
|
|
+ startPoint: .leading,
|
|
|
+ endPoint: .trailing
|
|
|
+ )
|
|
|
+ }
|
|
|
+ return isHovered ? AppTheme.primaryGradientHover : AppTheme.primaryGradient
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct TealPillButton: View {
|
|
|
+ let title: String
|
|
|
+ let iconName: String
|
|
|
+ let action: () -> Void
|
|
|
+
|
|
|
+ @State private var isHovered = false
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: action) {
|
|
|
+ HStack(spacing: 6) {
|
|
|
+ Image(systemName: iconName)
|
|
|
+ .font(.system(size: 12, weight: .semibold))
|
|
|
+
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 13, weight: .semibold))
|
|
|
+ }
|
|
|
+ .foregroundStyle(isHovered ? AppTheme.tealDark : AppTheme.teal)
|
|
|
+ .padding(.horizontal, 14)
|
|
|
+ .padding(.vertical, 9)
|
|
|
+ .background(isHovered ? AppTheme.tealLight.opacity(0.85) : AppTheme.tealLight)
|
|
|
+ .clipShape(RoundedRectangle(cornerRadius: 10))
|
|
|
+ .overlay(
|
|
|
+ RoundedRectangle(cornerRadius: 10)
|
|
|
+ .stroke(isHovered ? AppTheme.toolbarBorderHover : Color.clear, lineWidth: 1)
|
|
|
+ )
|
|
|
+ .shadow(
|
|
|
+ color: isHovered ? AppTheme.toolbarShadowHover : .clear,
|
|
|
+ radius: isHovered ? 4 : 0,
|
|
|
+ y: isHovered ? 2 : 0
|
|
|
+ )
|
|
|
+ .scaleEffect(isHovered ? 1.02 : 1.0)
|
|
|
+ .animation(.easeOut(duration: 0.15), value: isHovered)
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .onHover { isHovered = $0 }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct ChipButton: View {
|
|
|
+ let title: String
|
|
|
+ var isSelected: Bool = false
|
|
|
+ let action: () -> Void
|
|
|
+
|
|
|
+ @State private var isHovered = false
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: action) {
|
|
|
+ Text(title)
|
|
|
+ .font(.system(size: 12, weight: .medium))
|
|
|
+ .foregroundStyle(isSelected ? AppTheme.teal : (isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary))
|
|
|
+ .padding(.horizontal, isSelected ? 12 : 10)
|
|
|
+ .padding(.vertical, isSelected ? 7 : 6)
|
|
|
+ .background(chipBackground)
|
|
|
+ .clipShape(Capsule())
|
|
|
+ .overlay(
|
|
|
+ Capsule()
|
|
|
+ .stroke(chipBorder, lineWidth: 1)
|
|
|
+ )
|
|
|
+ .scaleEffect(isHovered ? 1.02 : 1.0)
|
|
|
+ .animation(.easeOut(duration: 0.15), value: isHovered)
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .onHover { isHovered = $0 }
|
|
|
+ }
|
|
|
+
|
|
|
+ private var chipBackground: Color {
|
|
|
+ if isSelected {
|
|
|
+ return AppTheme.tealLight
|
|
|
+ }
|
|
|
+ return isHovered ? AppTheme.toolbarBackgroundHover : Color.white
|
|
|
+ }
|
|
|
+
|
|
|
+ private var chipBorder: Color {
|
|
|
+ if isSelected {
|
|
|
+ return AppTheme.teal.opacity(0.4)
|
|
|
+ }
|
|
|
+ return isHovered ? AppTheme.toolbarBorderHover : AppTheme.border.opacity(0.7)
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+struct PopoverOptionRow: View {
|
|
|
+ let title: String
|
|
|
+ let isSelected: Bool
|
|
|
+ let action: () -> Void
|
|
|
+
|
|
|
+ @State private var isHovered = false
|
|
|
+
|
|
|
+ var body: some View {
|
|
|
+ Button(action: action) {
|
|
|
+ HStack {
|
|
|
+ Text(title)
|
|
|
+ if isSelected {
|
|
|
+ Spacer()
|
|
|
+ Image(systemName: "checkmark")
|
|
|
+ }
|
|
|
+ }
|
|
|
+ .font(.system(size: 13, weight: isSelected ? .semibold : .regular))
|
|
|
+ .foregroundStyle(isHovered || isSelected ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
|
|
|
+ .frame(minWidth: 180, alignment: .leading)
|
|
|
+ .padding(.horizontal, 10)
|
|
|
+ .padding(.vertical, 7)
|
|
|
+ .background(
|
|
|
+ RoundedRectangle(cornerRadius: 6)
|
|
|
+ .fill(isHovered ? AppTheme.toolbarBackgroundHover : Color.clear)
|
|
|
+ )
|
|
|
+ .contentShape(Rectangle())
|
|
|
+ }
|
|
|
+ .buttonStyle(.plain)
|
|
|
+ .onHover { isHovered = $0 }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
struct ClearButton: View {
|
|
|
let action: () -> Void
|
|
|
|