GradientButton.swift 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. import SwiftUI
  2. struct GradientButton: View {
  3. let title: String
  4. let action: () -> Void
  5. @State private var isHovered = false
  6. var body: some View {
  7. Button(action: action) {
  8. HStack(spacing: 10) {
  9. Image(systemName: "sparkles")
  10. .font(.system(size: 15, weight: .semibold))
  11. Text(title)
  12. .font(.system(size: 16, weight: .semibold))
  13. .textSelection(.disabled)
  14. Image(systemName: "arrow.right")
  15. .font(.system(size: 14, weight: .semibold))
  16. }
  17. .foregroundStyle(.white)
  18. .padding(.horizontal, 28)
  19. .padding(.vertical, 15)
  20. .frame(maxWidth: .infinity)
  21. .background(isHovered ? AppTheme.primaryGradientHover : AppTheme.primaryGradient)
  22. .clipShape(RoundedRectangle(cornerRadius: AppTheme.buttonCornerRadius))
  23. .shadow(
  24. color: isHovered ? AppTheme.primaryShadowHover : AppTheme.primaryShadow,
  25. radius: isHovered ? 14 : 10,
  26. y: isHovered ? 6 : 4
  27. )
  28. .scaleEffect(isHovered ? 1.02 : 1.0)
  29. .animation(.easeOut(duration: 0.15), value: isHovered)
  30. }
  31. .buttonStyle(.plain)
  32. .onHover { isHovered = $0 }
  33. }
  34. }
  35. struct ToolbarButton: View {
  36. let title: String
  37. let iconName: String
  38. let action: () -> Void
  39. @State private var isHovered = false
  40. var body: some View {
  41. Button(action: action) {
  42. HStack(spacing: 6) {
  43. Image(systemName: iconName)
  44. .font(.system(size: 12))
  45. Text(title)
  46. .font(.system(size: 12, weight: .medium))
  47. }
  48. .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
  49. .padding(.horizontal, 24)
  50. .padding(.vertical, 7)
  51. .background(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
  52. .clipShape(RoundedRectangle(cornerRadius: 8))
  53. .overlay(
  54. RoundedRectangle(cornerRadius: 8)
  55. .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
  56. )
  57. .shadow(
  58. color: isHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
  59. radius: isHovered ? 4 : 2,
  60. y: isHovered ? 2 : 1
  61. )
  62. .scaleEffect(isHovered ? 1.02 : 1.0)
  63. .animation(.easeOut(duration: 0.15), value: isHovered)
  64. }
  65. .buttonStyle(.plain)
  66. .onHover { isHovered = $0 }
  67. }
  68. }
  69. struct ToolbarMenuButton<MenuContent: View>: View {
  70. let title: String
  71. let iconName: String
  72. @ViewBuilder let menuContent: () -> MenuContent
  73. @State private var isHovered = false
  74. var body: some View {
  75. Menu {
  76. menuContent()
  77. } label: {
  78. HStack(spacing: 6) {
  79. Image(systemName: iconName)
  80. .font(.system(size: 12))
  81. Text(title)
  82. .font(.system(size: 12, weight: .medium))
  83. Image(systemName: "chevron.down")
  84. .font(.system(size: 10, weight: .semibold))
  85. }
  86. .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
  87. .padding(.horizontal, 24)
  88. .padding(.vertical, 7)
  89. .background(
  90. RoundedRectangle(cornerRadius: 8, style: .continuous)
  91. .fill(isHovered ? AppTheme.toolbarBackgroundHover : Color.white)
  92. )
  93. .overlay(
  94. RoundedRectangle(cornerRadius: 8, style: .continuous)
  95. .stroke(isHovered ? AppTheme.toolbarBorderHover : AppTheme.border, lineWidth: 1)
  96. )
  97. .shadow(
  98. color: isHovered ? AppTheme.toolbarShadowHover : AppTheme.toolbarShadow,
  99. radius: isHovered ? 4 : 2,
  100. y: isHovered ? 2 : 1
  101. )
  102. .scaleEffect(isHovered ? 1.02 : 1.0)
  103. .animation(.easeOut(duration: 0.15), value: isHovered)
  104. .compositingGroup()
  105. }
  106. .fixedSize(horizontal: true, vertical: false)
  107. .menuStyle(.borderlessButton)
  108. .buttonStyle(.plain)
  109. .onHover { isHovered = $0 }
  110. }
  111. }
  112. struct IconButton: View {
  113. let action: () -> Void
  114. private let iconName: String?
  115. private let customIcon: (() -> AnyView)?
  116. @State private var isHovered = false
  117. init(iconName: String, action: @escaping () -> Void) {
  118. self.iconName = iconName
  119. self.customIcon = nil
  120. self.action = action
  121. }
  122. init<Content: View>(@ViewBuilder icon: @escaping () -> Content, action: @escaping () -> Void) {
  123. self.iconName = nil
  124. self.customIcon = { AnyView(icon()) }
  125. self.action = action
  126. }
  127. @ViewBuilder
  128. private var iconContent: some View {
  129. if let iconName {
  130. Image(systemName: iconName)
  131. .font(.system(size: 14))
  132. } else if let customIcon {
  133. customIcon()
  134. }
  135. }
  136. var body: some View {
  137. Button(action: action) {
  138. iconContent
  139. .foregroundStyle(isHovered ? AppTheme.toolbarForegroundHover : AppTheme.textSecondary)
  140. .padding(4)
  141. .contentShape(Rectangle())
  142. .scaleEffect(isHovered ? 1.05 : 1.0)
  143. .animation(.easeOut(duration: 0.15), value: isHovered)
  144. }
  145. .buttonStyle(.plain)
  146. .onHover { isHovered = $0 }
  147. }
  148. }
  149. struct ClearButton: View {
  150. let action: () -> Void
  151. @State private var isHovered = false
  152. var body: some View {
  153. Button(action: action) {
  154. HStack(spacing: 6) {
  155. Image(systemName: "eraser.fill")
  156. .font(.system(size: 12, weight: .semibold))
  157. .symbolRenderingMode(.hierarchical)
  158. Text("Clear")
  159. .font(.system(size: 12, weight: .semibold))
  160. }
  161. .foregroundStyle(isHovered ? AppTheme.clearForegroundHover : AppTheme.clearForeground)
  162. .padding(.horizontal, 12)
  163. .padding(.vertical, 7)
  164. .background(isHovered ? AppTheme.clearBackgroundHover : AppTheme.clearBackground)
  165. .clipShape(RoundedRectangle(cornerRadius: 8))
  166. .overlay(
  167. RoundedRectangle(cornerRadius: 8)
  168. .stroke(isHovered ? AppTheme.clearForeground.opacity(0.35) : AppTheme.clearBorder, lineWidth: 1)
  169. )
  170. .shadow(
  171. color: isHovered ? AppTheme.clearShadow : AppTheme.clearShadow.opacity(0.5),
  172. radius: isHovered ? 6 : 3,
  173. y: isHovered ? 3 : 1
  174. )
  175. .scaleEffect(isHovered ? 1.02 : 1.0)
  176. .animation(.easeOut(duration: 0.15), value: isHovered)
  177. }
  178. .buttonStyle(.plain)
  179. .onHover { isHovered = $0 }
  180. .accessibilityLabel("Clear text")
  181. }
  182. }