feat(ios): unify action hierarchy and surfaces
Refine primary and toolbar actions, align row buttons, normalize status surfaces, and split storage from destructive cleanup. Verified with Swift tests and the simulator build.
This commit is contained in:
@@ -41,6 +41,7 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
case regular
|
||||
case large
|
||||
case toolbar
|
||||
case toolbarInline
|
||||
}
|
||||
|
||||
private let kind: Kind
|
||||
@@ -66,13 +67,16 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
switch size {
|
||||
case .compact: AITheme.compactButtonHeight
|
||||
case .large: AITheme.largeButtonHeight
|
||||
case .regular: AITheme.buttonHeight
|
||||
case .toolbar: AITheme.toolbarButtonHeight
|
||||
case .regular: AITheme.buttonHeight
|
||||
case .toolbar: AITheme.toolbarButtonHeight
|
||||
case .toolbarInline: AITheme.toolbarButtonHeight
|
||||
}
|
||||
|
||||
configuration.label
|
||||
configuration.label
|
||||
.labelStyle(.titleAndIcon)
|
||||
.font(.system(size: size == .toolbar ? 15 : 16, weight: .semibold))
|
||||
.font(
|
||||
.system(size: size == .compact || size == .toolbarInline ? 15 : 16, weight: .semibold)
|
||||
)
|
||||
.lineLimit(1)
|
||||
.foregroundStyle(kind.foregroundColor(isEnabled: isEnabled))
|
||||
.padding(.horizontal, fixedWidth == nil ? (kind == .secondary ? 12 : 14) : 0)
|
||||
@@ -83,19 +87,38 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
.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
|
||||
kind == .primary && isEnabled && size != .toolbarInline
|
||||
? AnyShapeStyle(
|
||||
LinearGradient(
|
||||
colors: [AITheme.primaryTop, AITheme.primaryBottom],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
: AnyShapeStyle(kind.backgroundColor(isEnabled: isEnabled))
|
||||
)
|
||||
.shadow(
|
||||
color: (kind == .secondary || !isEnabled) ? .clear : .black.opacity(0.10),
|
||||
radius: 12,
|
||||
x: 0,
|
||||
y: 5
|
||||
)
|
||||
.clipShape(
|
||||
RoundedRectangle(
|
||||
cornerRadius: size == .toolbarInline ? AITheme.toolbarButtonHeight / 2 : AITheme.buttonRadius,
|
||||
style: .continuous
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.buttonRadius, style: .continuous)
|
||||
.strokeBorder(kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||
RoundedRectangle(
|
||||
cornerRadius: size == .toolbarInline ? AITheme.toolbarButtonHeight / 2 : AITheme.buttonRadius,
|
||||
style: .continuous
|
||||
)
|
||||
.strokeBorder(
|
||||
size == .toolbarInline && kind == .primary
|
||||
? AITheme.primary.opacity(0.28)
|
||||
: (kind == .secondary || !isEnabled ? AITheme.line : .clear)
|
||||
)
|
||||
}
|
||||
.scaleEffect(configuration.isPressed ? Self.pressedScale : 1)
|
||||
.opacity(configuration.isPressed ? 0.82 : 1)
|
||||
@@ -106,7 +129,7 @@ public struct AIButtonStyle: ButtonStyle {
|
||||
case .compact: return AITheme.compactButtonMinWidth
|
||||
case .regular: return AITheme.regularButtonMinWidth
|
||||
case .large: return 92
|
||||
case .toolbar: return AITheme.toolbarButtonMinWidth
|
||||
case .toolbar, .toolbarInline: return AITheme.toolbarButtonMinWidth
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ public struct AICard<Content: View>: View {
|
||||
|
||||
public var body: some View {
|
||||
content
|
||||
.padding(padding)
|
||||
.background(AITheme.surface)
|
||||
.padding(padding)
|
||||
.background(AICard.backgroundGradient)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.07), radius: 18, x: 0, y: 8)
|
||||
.overlay {
|
||||
@@ -22,6 +22,16 @@ public struct AICard<Content: View>: View {
|
||||
}
|
||||
}
|
||||
|
||||
private extension AICard {
|
||||
static var backgroundGradient: LinearGradient {
|
||||
LinearGradient(
|
||||
colors: [AITheme.surfaceRaised, AITheme.surface],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public struct AITag: View {
|
||||
private let text: String
|
||||
private let tint: Color
|
||||
@@ -33,7 +43,7 @@ public struct AITag: View {
|
||||
|
||||
public var body: some View {
|
||||
Text(text)
|
||||
.font(.system(size: 12, weight: .semibold))
|
||||
.font(.caption2.weight(.semibold))
|
||||
.foregroundStyle(tint)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
@@ -100,6 +110,7 @@ public struct AISectionTitle: View {
|
||||
Image(systemName: systemImage)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(AITheme.primary)
|
||||
.frame(width: 22, height: 22)
|
||||
.accessibilityHidden(true)
|
||||
Text(title)
|
||||
.font(.headline.weight(.semibold))
|
||||
@@ -152,6 +163,8 @@ public struct AIStatCard: View {
|
||||
VStack(alignment: .leading, spacing: 5) {
|
||||
Text(title)
|
||||
.font(.caption.weight(.semibold))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.85)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
Text(value)
|
||||
.font(.title3.weight(.bold))
|
||||
@@ -194,9 +207,9 @@ public struct AIStatusBanner: View {
|
||||
}
|
||||
.padding(15)
|
||||
.background(tint.opacity(0.10))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius - 6, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
RoundedRectangle(cornerRadius: AITheme.cardRadius - 6, style: .continuous)
|
||||
.strokeBorder(tint.opacity(0.22))
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
@@ -282,7 +295,7 @@ public struct AIListCard<Footer: View>: View {
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(1)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
@@ -318,12 +331,28 @@ public struct AILoadingCard: View {
|
||||
VStack(spacing: 10) {
|
||||
ProgressView()
|
||||
.controlSize(.regular)
|
||||
.frame(width: 44, height: 44)
|
||||
.background(AITheme.control)
|
||||
.clipShape(Circle())
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 18)
|
||||
.padding(16)
|
||||
.background(
|
||||
LinearGradient(
|
||||
colors: [AITheme.surfaceRaised, AITheme.surface],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.07), radius: 18, x: 0, y: 8)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.cardRadius, style: .continuous)
|
||||
.strokeBorder(AITheme.line)
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
@@ -367,9 +396,10 @@ private struct AIEqualWidthLayout: Layout {
|
||||
}
|
||||
|
||||
let equalWidth = max(0, (width - spacing * CGFloat(count - 1)) / CGFloat(count))
|
||||
let height = subviews.map {
|
||||
$0.sizeThatFits(ProposedViewSize(width: equalWidth, height: nil)).height
|
||||
}.max() ?? 0
|
||||
let height =
|
||||
subviews.map {
|
||||
$0.sizeThatFits(ProposedViewSize(width: equalWidth, height: nil)).height
|
||||
}.max() ?? 0
|
||||
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
@@ -454,7 +484,7 @@ public struct AISegmentedControl<Value: Hashable>: View {
|
||||
selection = option.value
|
||||
} label: {
|
||||
Text(option.title)
|
||||
.font(.subheadline.weight(isSelected ? .semibold : .medium))
|
||||
.font(.body.weight(isSelected ? .semibold : .medium))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
.foregroundStyle(isSelected ? AITheme.onPrimary : AITheme.muted)
|
||||
@@ -462,11 +492,18 @@ public struct AISegmentedControl<Value: Hashable>: View {
|
||||
.background {
|
||||
if isSelected {
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(AITheme.primary)
|
||||
.fill(
|
||||
LinearGradient(
|
||||
colors: [AITheme.primaryTop, AITheme.primaryBottom],
|
||||
startPoint: .top,
|
||||
endPoint: .bottom
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityLabel(option.title)
|
||||
.accessibilityAddTraits(isSelected ? .isSelected : [])
|
||||
}
|
||||
@@ -516,6 +553,7 @@ public struct AISearchField: View {
|
||||
public var body: some View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.font(.system(size: 15, weight: .medium))
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.accessibilityHidden(true)
|
||||
TextField(placeholder, text: $text)
|
||||
@@ -627,7 +665,7 @@ public struct AIToolbarButton: View {
|
||||
.font(.system(size: 15, weight: .semibold))
|
||||
}
|
||||
}
|
||||
.buttonStyle(AIButtonStyle(kind: kind, size: .toolbar))
|
||||
.buttonStyle(AIButtonStyle(kind: kind, size: .toolbarInline))
|
||||
.accessibilityLabel(title)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,6 +38,8 @@ private enum AIPalette {
|
||||
static let line = color(light: rgba(227, 230, 234, 1), dark: rgba(255, 255, 255, 0.09))
|
||||
static let brand = color(light: rgba(13, 15, 18, 1), dark: rgba(8, 9, 11, 1))
|
||||
static let primary = color(light: rgba(17, 20, 25, 1), dark: rgba(220, 178, 111, 1))
|
||||
static let primaryTop = color(light: rgba(43, 49, 58, 1), dark: rgba(240, 209, 154, 1))
|
||||
static let primaryBottom = color(light: rgba(15, 18, 22, 1), dark: rgba(198, 154, 92, 1))
|
||||
static let onPrimary = color(light: rgba(255, 255, 255, 1), dark: rgba(12, 14, 17, 1))
|
||||
static let danger = color(light: rgba(190, 65, 57, 1), dark: rgba(242, 124, 113, 1))
|
||||
static let onDanger = color(light: rgba(255, 255, 255, 1), dark: rgba(22, 9, 8, 1))
|
||||
@@ -56,6 +58,8 @@ private enum AIPalette {
|
||||
static let onPrimary = Color.white
|
||||
static let onDanger = Color.white
|
||||
static let primary = Color(red: 0.067, green: 0.078, blue: 0.098)
|
||||
static let primaryTop = Color(red: 0.17, green: 0.19, blue: 0.23)
|
||||
static let primaryBottom = Color(red: 0.06, green: 0.07, blue: 0.09)
|
||||
static let danger = Color(red: 0.745, green: 0.255, blue: 0.224)
|
||||
static let warning = Color(red: 0.667, green: 0.435, blue: 0.09)
|
||||
static let success = Color(red: 0.196, green: 0.588, blue: 0.396)
|
||||
@@ -76,6 +80,8 @@ private enum AIPalette {
|
||||
static let onPrimary = color(255, 255, 255)
|
||||
static let onDanger = color(255, 255, 255)
|
||||
static let primary = color(17, 20, 25)
|
||||
static let primaryTop = color(43, 49, 58)
|
||||
static let primaryBottom = color(15, 18, 22)
|
||||
static let danger = color(190, 65, 57)
|
||||
static let warning = color(170, 111, 23)
|
||||
static let success = color(50, 150, 101)
|
||||
@@ -93,6 +99,8 @@ public enum AITheme {
|
||||
public static let line = AIPalette.line
|
||||
public static let brand = AIPalette.brand
|
||||
public static let primary = AIPalette.primary
|
||||
public static let primaryTop = AIPalette.primaryTop
|
||||
public static let primaryBottom = AIPalette.primaryBottom
|
||||
public static let onPrimary = AIPalette.onPrimary
|
||||
public static let danger = AIPalette.danger
|
||||
public static let onDanger = AIPalette.onDanger
|
||||
|
||||
Reference in New Issue
Block a user