feat(ios): refine native interface and app icon
Unify bottom actions, selection controls, navigation structure, spacing, and theme colors across iOS pages. Replace the app icon with a higher-contrast native-style asset and include its source SVG.
This commit is contained in:
@@ -1,98 +1,116 @@
|
||||
import SwiftUI
|
||||
|
||||
public struct AIButtonStyle: ButtonStyle {
|
||||
public static let pressedScale: CGFloat = 0.97
|
||||
public static let pressedScale: CGFloat = 0.97
|
||||
|
||||
public enum Kind: Hashable {
|
||||
case primary
|
||||
case secondary
|
||||
case danger
|
||||
public enum Kind: Hashable {
|
||||
case primary
|
||||
case secondary
|
||||
case danger
|
||||
|
||||
var foregroundColor: Color { foregroundColor(isEnabled: true) }
|
||||
var foregroundColor: Color { foregroundColor(isEnabled: true) }
|
||||
|
||||
var backgroundColor: Color { backgroundColor(isEnabled: true) }
|
||||
var backgroundColor: Color { backgroundColor(isEnabled: true) }
|
||||
|
||||
func foregroundColor(isEnabled: Bool) -> Color {
|
||||
switch self {
|
||||
case .primary, .danger: return .white.opacity(isEnabled ? 1 : 0.72)
|
||||
case .secondary: return isEnabled ? AITheme.ink : AITheme.muted
|
||||
}
|
||||
}
|
||||
|
||||
func backgroundColor(isEnabled: Bool) -> Color {
|
||||
switch self {
|
||||
case .primary: return isEnabled ? AITheme.primary : AITheme.primary.opacity(0.35)
|
||||
case .secondary: return isEnabled ? AITheme.control : AITheme.canvas
|
||||
case .danger: return isEnabled ? AITheme.danger : AITheme.danger.opacity(0.30)
|
||||
}
|
||||
}
|
||||
func foregroundColor(isEnabled: Bool) -> Color {
|
||||
switch self {
|
||||
case .primary: return isEnabled ? AITheme.onPrimary : AITheme.muted
|
||||
case .danger: return isEnabled ? AITheme.onDanger : AITheme.muted
|
||||
case .secondary: return isEnabled ? AITheme.ink : AITheme.muted
|
||||
}
|
||||
}
|
||||
|
||||
public enum Size: Hashable {
|
||||
case compact
|
||||
case regular
|
||||
case large
|
||||
case toolbar
|
||||
func backgroundColor(isEnabled: Bool) -> Color {
|
||||
switch self {
|
||||
case .primary, .secondary, .danger:
|
||||
return isEnabled ? kindBackground : AITheme.control
|
||||
}
|
||||
}
|
||||
|
||||
private let kind: Kind
|
||||
private let size: Size
|
||||
private let fixedWidthRequest: CGFloat?
|
||||
let fillsWidth: Bool
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
public init(
|
||||
kind: Kind = .secondary,
|
||||
size: Size = .regular,
|
||||
fixedWidth: CGFloat? = nil,
|
||||
fillsWidth: Bool = false
|
||||
) {
|
||||
self.kind = kind
|
||||
self.size = size
|
||||
self.fixedWidthRequest = fixedWidth
|
||||
self.fillsWidth = fillsWidth
|
||||
private var kindBackground: Color {
|
||||
switch self {
|
||||
case .primary: return AITheme.primary
|
||||
case .secondary: return AITheme.control
|
||||
case .danger: return AITheme.danger
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func makeBody(configuration: Configuration) -> some View {
|
||||
let height: CGFloat = switch size {
|
||||
case .compact: AITheme.compactButtonHeight
|
||||
case .large: AITheme.largeButtonHeight
|
||||
case .regular: AITheme.buttonHeight
|
||||
case .toolbar: AITheme.toolbarButtonHeight
|
||||
}
|
||||
public enum Size: Hashable {
|
||||
case compact
|
||||
case regular
|
||||
case large
|
||||
case toolbar
|
||||
}
|
||||
|
||||
configuration.label
|
||||
.labelStyle(.titleAndIcon)
|
||||
.font(.system(size: size == .toolbar ? 15 : 16, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(kind.foregroundColor(isEnabled: isEnabled))
|
||||
.frame(
|
||||
minWidth: (!fillsWidth && fixedWidth == nil) ? minimumWidth : 0,
|
||||
minHeight: height
|
||||
)
|
||||
.frame(maxWidth: fillsWidth ? .infinity : nil)
|
||||
.padding(.horizontal, fixedWidth == nil ? (kind == .secondary ? 12 : 14) : 0)
|
||||
.frame(width: fixedWidth)
|
||||
.background(kind.backgroundColor(isEnabled: isEnabled))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.buttonRadius)
|
||||
.strokeBorder(kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? Self.pressedScale : 1)
|
||||
.opacity(configuration.isPressed ? 0.82 : 1)
|
||||
private let kind: Kind
|
||||
private let size: Size
|
||||
private let fixedWidthRequest: CGFloat?
|
||||
let fillsWidth: Bool
|
||||
@Environment(\.isEnabled) private var isEnabled
|
||||
|
||||
public init(
|
||||
kind: Kind = .secondary,
|
||||
size: Size = .regular,
|
||||
fixedWidth: CGFloat? = nil,
|
||||
fillsWidth: Bool = false
|
||||
) {
|
||||
self.kind = kind
|
||||
self.size = size
|
||||
self.fixedWidthRequest = fixedWidth
|
||||
self.fillsWidth = fillsWidth
|
||||
}
|
||||
|
||||
public func makeBody(configuration: Configuration) -> some View {
|
||||
let height: CGFloat =
|
||||
switch size {
|
||||
case .compact: AITheme.compactButtonHeight
|
||||
case .large: AITheme.largeButtonHeight
|
||||
case .regular: AITheme.buttonHeight
|
||||
case .toolbar: AITheme.toolbarButtonHeight
|
||||
}
|
||||
|
||||
configuration.label
|
||||
.labelStyle(.titleAndIcon)
|
||||
.font(.system(size: size == .toolbar ? 15 : 16, weight: .semibold))
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(kind.foregroundColor(isEnabled: isEnabled))
|
||||
.padding(.horizontal, fixedWidth == nil ? (kind == .secondary ? 12 : 14) : 0)
|
||||
.frame(
|
||||
minWidth: (!fillsWidth && fixedWidth == nil) ? minimumWidth : 0,
|
||||
minHeight: height
|
||||
)
|
||||
.frame(maxWidth: fillsWidth ? .infinity : nil)
|
||||
.frame(width: fixedWidth)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: AITheme.buttonRadius, style: .continuous)
|
||||
.fill(kind.backgroundColor(isEnabled: isEnabled))
|
||||
.shadow(
|
||||
color: (kind == .secondary || !isEnabled) ? .clear : .black.opacity(0.10),
|
||||
radius: 12,
|
||||
x: 0,
|
||||
y: 5
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.buttonRadius, style: .continuous)
|
||||
.strokeBorder(kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? Self.pressedScale : 1)
|
||||
.opacity(configuration.isPressed ? 0.82 : 1)
|
||||
}
|
||||
|
||||
private var minimumWidth: CGFloat {
|
||||
switch size {
|
||||
case .compact: return AITheme.compactButtonMinWidth
|
||||
case .regular: return AITheme.regularButtonMinWidth
|
||||
case .large: return 92
|
||||
case .toolbar: return AITheme.toolbarButtonMinWidth
|
||||
}
|
||||
}
|
||||
|
||||
private var minimumWidth: CGFloat {
|
||||
switch size {
|
||||
case .compact: return AITheme.compactButtonMinWidth
|
||||
case .regular: return AITheme.regularButtonMinWidth
|
||||
case .large: return 92
|
||||
case .toolbar: return AITheme.toolbarButtonMinWidth
|
||||
}
|
||||
}
|
||||
|
||||
private var fixedWidth: CGFloat? {
|
||||
fixedWidthRequest ?? (size == .toolbar ? AITheme.toolbarButtonWidth : nil)
|
||||
}
|
||||
private var fixedWidth: CGFloat? {
|
||||
fixedWidthRequest ?? (size == .toolbar ? AITheme.toolbarButtonWidth : nil)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user