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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,10 +13,10 @@ public struct AICard<Content: View>: View {
|
||||
content
|
||||
.padding(padding)
|
||||
.background(AITheme.surface)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius))
|
||||
.shadow(color: .black.opacity(0.045), radius: 8, x: 0, y: 3)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius, style: .continuous))
|
||||
.shadow(color: .black.opacity(0.07), radius: 18, x: 0, y: 8)
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.cardRadius)
|
||||
RoundedRectangle(cornerRadius: AITheme.cardRadius, style: .continuous)
|
||||
.strokeBorder(AITheme.line)
|
||||
}
|
||||
}
|
||||
@@ -37,10 +37,14 @@ public struct AITag: View {
|
||||
.foregroundStyle(tint)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.padding(.horizontal, 9)
|
||||
.frame(minHeight: 26)
|
||||
.padding(.horizontal, 10)
|
||||
.frame(minHeight: 30)
|
||||
.background(tint.opacity(0.10))
|
||||
.clipShape(Capsule())
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 10, style: .continuous)
|
||||
.strokeBorder(tint.opacity(0.16))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,8 +100,10 @@ public struct AISectionTitle: View {
|
||||
Image(systemName: systemImage)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(AITheme.primary)
|
||||
.accessibilityHidden(true)
|
||||
Text(title)
|
||||
.font(.headline.weight(.bold))
|
||||
.font(.headline.weight(.semibold))
|
||||
.kerning(-0.1)
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(1)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
@@ -149,7 +155,7 @@ public struct AIStatCard: View {
|
||||
.foregroundStyle(AITheme.muted)
|
||||
Text(value)
|
||||
.font(.title3.weight(.bold))
|
||||
.foregroundStyle(tint)
|
||||
.foregroundStyle(value == "0" || value.hasPrefix("0/") ? AITheme.muted : tint)
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
if let detail {
|
||||
@@ -180,16 +186,17 @@ public struct AIStatusBanner: View {
|
||||
Image(systemName: systemImage)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(tint)
|
||||
.accessibilityHidden(true)
|
||||
Text(message)
|
||||
.font(.subheadline.weight(.medium))
|
||||
.foregroundStyle(tint)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(14)
|
||||
.padding(15)
|
||||
.background(tint.opacity(0.10))
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 18, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.cardRadius)
|
||||
RoundedRectangle(cornerRadius: 18, style: .continuous)
|
||||
.strokeBorder(tint.opacity(0.22))
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
@@ -210,13 +217,14 @@ public struct AIEmptyState: View {
|
||||
public var body: some View {
|
||||
VStack(spacing: 10) {
|
||||
Image(systemName: systemImage)
|
||||
.font(.system(size: 26))
|
||||
.font(.system(size: 27, weight: .medium))
|
||||
.foregroundStyle(AITheme.primary)
|
||||
.frame(width: 54, height: 54)
|
||||
.background(AITheme.primary.opacity(0.10))
|
||||
.clipShape(Circle())
|
||||
.accessibilityHidden(true)
|
||||
Text(title)
|
||||
.font(.headline.weight(.bold))
|
||||
.font(.headline.weight(.semibold))
|
||||
.foregroundStyle(AITheme.ink)
|
||||
Text(message)
|
||||
.font(.subheadline)
|
||||
@@ -225,7 +233,7 @@ public struct AIEmptyState: View {
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding(.vertical, 18)
|
||||
.padding(.vertical, 16)
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
@@ -255,46 +263,47 @@ public struct AIListCard<Footer: View>: View {
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
AICard {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 10) {
|
||||
Text(title)
|
||||
.font(.subheadline.weight(.semibold))
|
||||
.foregroundStyle(AITheme.ink)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
if let status {
|
||||
AITag(status, tint: statusTint ?? AITheme.muted)
|
||||
}
|
||||
}
|
||||
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if !details.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(details, id: \.self) { detail in
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
if let status {
|
||||
AITag(status, tint: statusTint ?? AITheme.muted)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Footer.self != EmptyView.self {
|
||||
footer
|
||||
if let subtitle {
|
||||
Text(subtitle)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.middle)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
|
||||
if !details.isEmpty {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
ForEach(details, id: \.self) { detail in
|
||||
Text(detail)
|
||||
.font(.caption)
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.lineLimit(2)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if Footer.self != EmptyView.self {
|
||||
footer
|
||||
}
|
||||
}
|
||||
}
|
||||
.accessibilityElement(children: .combine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -319,6 +328,77 @@ public struct AILoadingCard: View {
|
||||
}
|
||||
}
|
||||
|
||||
public struct AIActionRow<Content: View>: View {
|
||||
private let spacing: CGFloat
|
||||
private let content: Content
|
||||
|
||||
public init(
|
||||
spacing: CGFloat = 10,
|
||||
@ViewBuilder content: () -> Content
|
||||
) {
|
||||
self.spacing = spacing
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
AIEqualWidthLayout(spacing: spacing) {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct AIEqualWidthLayout: Layout {
|
||||
let spacing: CGFloat
|
||||
|
||||
func sizeThatFits(
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) -> CGSize {
|
||||
guard !subviews.isEmpty else { return .zero }
|
||||
|
||||
let count = subviews.count
|
||||
let width: CGFloat
|
||||
if let proposedWidth = proposal.width {
|
||||
width = proposedWidth
|
||||
} else {
|
||||
let idealWidths = subviews.map { $0.sizeThatFits(.unspecified).width }
|
||||
width = idealWidths.reduce(0, +) + spacing * CGFloat(count - 1)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
return CGSize(width: width, height: height)
|
||||
}
|
||||
|
||||
func placeSubviews(
|
||||
in bounds: CGRect,
|
||||
proposal: ProposedViewSize,
|
||||
subviews: Subviews,
|
||||
cache: inout ()
|
||||
) {
|
||||
guard !subviews.isEmpty else { return }
|
||||
|
||||
let equalWidth = max(
|
||||
0,
|
||||
(bounds.width - spacing * CGFloat(subviews.count - 1)) / CGFloat(subviews.count)
|
||||
)
|
||||
var x = bounds.minX
|
||||
|
||||
for subview in subviews {
|
||||
let size = subview.sizeThatFits(ProposedViewSize(width: equalWidth, height: nil))
|
||||
subview.place(
|
||||
at: CGPoint(x: x, y: bounds.midY - size.height / 2),
|
||||
proposal: ProposedViewSize(width: equalWidth, height: size.height)
|
||||
)
|
||||
x += equalWidth + spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public struct AIKeyValueRow: View {
|
||||
private let title: String
|
||||
private let value: String
|
||||
@@ -377,11 +457,11 @@ public struct AISegmentedControl<Value: Hashable>: View {
|
||||
.font(.subheadline.weight(isSelected ? .semibold : .medium))
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.75)
|
||||
.foregroundStyle(isSelected ? .white : AITheme.muted)
|
||||
.frame(maxWidth: .infinity, minHeight: 34)
|
||||
.foregroundStyle(isSelected ? AITheme.onPrimary : AITheme.muted)
|
||||
.frame(maxWidth: .infinity, minHeight: AITheme.segmentedControlHeight)
|
||||
.background {
|
||||
if isSelected {
|
||||
RoundedRectangle(cornerRadius: 9)
|
||||
RoundedRectangle(cornerRadius: 12, style: .continuous)
|
||||
.fill(AITheme.primary)
|
||||
}
|
||||
}
|
||||
@@ -393,9 +473,9 @@ public struct AISegmentedControl<Value: Hashable>: View {
|
||||
}
|
||||
.padding(4)
|
||||
.background(AITheme.control)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 16, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.buttonRadius)
|
||||
RoundedRectangle(cornerRadius: 16, style: .continuous)
|
||||
.strokeBorder(AITheme.line)
|
||||
}
|
||||
.animation(.easeOut(duration: 0.16), value: selection)
|
||||
@@ -405,10 +485,14 @@ public struct AISegmentedControl<Value: Hashable>: View {
|
||||
}
|
||||
|
||||
public struct AIFieldStyle: TextFieldStyle {
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
public init() {}
|
||||
|
||||
public func _body(configuration: TextField<Self._Label>) -> some View {
|
||||
configuration
|
||||
.textFieldStyle(.plain)
|
||||
.focused($isFocused)
|
||||
.aiInputSurface()
|
||||
}
|
||||
}
|
||||
@@ -417,6 +501,7 @@ public struct AISearchField: View {
|
||||
private let placeholder: String
|
||||
@Binding private var text: String
|
||||
private var onSubmit: (() -> Void)?
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
public init(
|
||||
_ placeholder: String,
|
||||
@@ -432,8 +517,10 @@ public struct AISearchField: View {
|
||||
HStack(spacing: 6) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundStyle(AITheme.muted)
|
||||
.accessibilityHidden(true)
|
||||
TextField(placeholder, text: $text)
|
||||
.textFieldStyle(.plain)
|
||||
.focused($isFocused)
|
||||
.submitLabel(.search)
|
||||
.onSubmit {
|
||||
onSubmit?()
|
||||
@@ -448,52 +535,48 @@ public struct AISearchField: View {
|
||||
.font(.system(size: 16, weight: .medium))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.frame(width: 28, height: 28)
|
||||
.frame(width: AITheme.iconButtonSize, height: AITheme.iconButtonSize)
|
||||
.contentShape(Rectangle())
|
||||
.accessibilityLabel("清除搜索")
|
||||
}
|
||||
}
|
||||
.aiInputSurface(minHeight: AITheme.buttonHeight)
|
||||
}
|
||||
}
|
||||
|
||||
public struct AIBrandBar: View {
|
||||
public init() {}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: 10) {
|
||||
Image(systemName: "antenna.radiowaves.left.and.right")
|
||||
Text(AppMetadata.name)
|
||||
.font(.headline)
|
||||
Spacer()
|
||||
}
|
||||
.foregroundStyle(.white)
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(AITheme.brand)
|
||||
.aiInputSurface(minHeight: AITheme.buttonHeight, isFocused: isFocused)
|
||||
}
|
||||
}
|
||||
|
||||
extension View {
|
||||
func aiInputSurface(minHeight: CGFloat = AITheme.buttonHeight) -> some View {
|
||||
func aiInputSurface(
|
||||
minHeight: CGFloat = AITheme.inputHeight,
|
||||
isFocused: Bool = false
|
||||
) -> some View {
|
||||
self
|
||||
.padding(.horizontal, 12)
|
||||
.frame(minHeight: minHeight, alignment: .leading)
|
||||
.background(AITheme.control)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius))
|
||||
.background(isFocused ? AITheme.surfaceRaised : AITheme.control)
|
||||
.clipShape(RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: AITheme.buttonRadius)
|
||||
.strokeBorder(AITheme.line)
|
||||
RoundedRectangle(cornerRadius: AITheme.inputRadius, style: .continuous)
|
||||
.strokeBorder(isFocused ? AITheme.primary.opacity(0.55) : AITheme.line)
|
||||
}
|
||||
.animation(.easeOut(duration: 0.15), value: isFocused)
|
||||
}
|
||||
|
||||
func aiBottomActionBar<Content: View>(@ViewBuilder content: () -> Content) -> some View {
|
||||
safeAreaInset(edge: .bottom) {
|
||||
VStack(spacing: 0) {
|
||||
self
|
||||
content()
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.top, 10)
|
||||
.padding(.bottom, 8)
|
||||
.background(AITheme.canvas)
|
||||
.padding(.top, 12)
|
||||
.padding(.bottom, 10)
|
||||
.background {
|
||||
ZStack {
|
||||
Rectangle()
|
||||
.fill(.ultraThinMaterial)
|
||||
Rectangle()
|
||||
.fill(AITheme.surface.opacity(0.98))
|
||||
}
|
||||
.ignoresSafeArea(edges: .bottom)
|
||||
}
|
||||
.overlay(alignment: .top) {
|
||||
Rectangle()
|
||||
.fill(AITheme.line)
|
||||
|
||||
@@ -1,44 +1,49 @@
|
||||
import SwiftUI
|
||||
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
#if canImport(UIKit)
|
||||
import UIKit
|
||||
#elseif canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
private enum AIPalette {
|
||||
#if canImport(UIKit)
|
||||
#if canImport(UIKit)
|
||||
private static func dynamicColor(light: UIColor, dark: UIColor) -> Color {
|
||||
Color(UIColor { traits in
|
||||
traits.userInterfaceStyle == .dark ? dark : light
|
||||
Color(
|
||||
UIColor { traits in
|
||||
traits.userInterfaceStyle == .dark ? dark : light
|
||||
})
|
||||
}
|
||||
|
||||
private static func rgba(
|
||||
_ red: CGFloat,
|
||||
_ green: CGFloat,
|
||||
_ blue: CGFloat,
|
||||
_ alpha: CGFloat
|
||||
_ red: CGFloat,
|
||||
_ green: CGFloat,
|
||||
_ blue: CGFloat,
|
||||
_ alpha: CGFloat
|
||||
) -> UIColor {
|
||||
UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha)
|
||||
UIColor(red: red / 255, green: green / 255, blue: blue / 255, alpha: alpha)
|
||||
}
|
||||
|
||||
private static func color(light: UIColor, dark: UIColor) -> Color {
|
||||
dynamicColor(light: light, dark: dark)
|
||||
dynamicColor(light: light, dark: dark)
|
||||
}
|
||||
|
||||
static let canvas = color(light: rgba(243, 245, 248, 1), dark: rgba(19, 22, 28, 1))
|
||||
static let surface = color(light: rgba(255, 255, 255, 1), dark: rgba(27, 31, 39, 1))
|
||||
static let control = color(light: rgba(247, 249, 251, 1), dark: rgba(37, 44, 56, 1))
|
||||
static let ink = color(light: rgba(28, 39, 56, 1), dark: rgba(235, 242, 247, 1))
|
||||
static let muted = color(light: rgba(111, 124, 143, 1), dark: rgba(173, 183, 199, 1))
|
||||
static let line = color(light: rgba(224, 229, 235, 1), dark: rgba(56, 63, 74, 1))
|
||||
static let brand = color(light: rgba(19, 30, 48, 1), dark: rgba(14, 22, 35, 1))
|
||||
static let primary = color(light: rgba(17, 119, 110, 1), dark: rgba(20, 142, 126, 1))
|
||||
static let danger = color(light: rgba(182, 77, 72, 1), dark: rgba(193, 79, 71, 1))
|
||||
static let warning = color(light: rgba(184, 120, 17, 1), dark: rgba(209, 141, 39, 1))
|
||||
static let success = color(light: rgba(21, 149, 112, 1), dark: rgba(32, 161, 119, 1))
|
||||
#elseif canImport(AppKit)
|
||||
static let canvas = color(light: rgba(245, 246, 248, 1), dark: rgba(6, 7, 9, 1))
|
||||
static let surface = color(light: rgba(255, 255, 255, 1), dark: rgba(18, 20, 24, 1))
|
||||
static let surfaceRaised = color(light: rgba(251, 252, 253, 1), dark: rgba(27, 30, 35, 1))
|
||||
static let control = color(light: rgba(243, 245, 247, 1), dark: rgba(26, 29, 34, 1))
|
||||
static let chrome = color(light: rgba(255, 255, 255, 0.82), dark: rgba(11, 12, 15, 0.82))
|
||||
static let ink = color(light: rgba(15, 18, 22, 1), dark: rgba(243, 245, 247, 1))
|
||||
static let muted = color(light: rgba(98, 107, 119, 1), dark: rgba(151, 160, 172, 1))
|
||||
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 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))
|
||||
static let warning = color(light: rgba(170, 111, 23, 1), dark: rgba(226, 166, 65, 1))
|
||||
static let success = color(light: rgba(50, 150, 101, 1), dark: rgba(103, 198, 148, 1))
|
||||
#elseif canImport(AppKit)
|
||||
static let canvas = Color(red: 0.953, green: 0.961, blue: 0.973)
|
||||
static let surface = Color.white
|
||||
static let control = Color(red: 0.969, green: 0.976, blue: 0.984)
|
||||
@@ -46,13 +51,17 @@ private enum AIPalette {
|
||||
static let muted = Color(red: 0.435, green: 0.486, blue: 0.561)
|
||||
static let line = Color(red: 0.878, green: 0.898, blue: 0.922)
|
||||
static let brand = Color(red: 0.075, green: 0.118, blue: 0.188)
|
||||
static let primary = Color(red: 0.067, green: 0.467, blue: 0.431)
|
||||
static let danger = Color(red: 0.714, green: 0.302, blue: 0.282)
|
||||
static let warning = Color(red: 0.722, green: 0.471, blue: 0.067)
|
||||
static let success = Color(red: 0.082, green: 0.584, blue: 0.439)
|
||||
#else
|
||||
static let surfaceRaised = Color(red: 0.984, green: 0.988, blue: 0.992)
|
||||
static let chrome = Color.white.opacity(0.82)
|
||||
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 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)
|
||||
#else
|
||||
private static func color(_ red: CGFloat, _ green: CGFloat, _ blue: CGFloat) -> Color {
|
||||
Color(red: red / 255, green: green / 255, blue: blue / 255)
|
||||
Color(red: red / 255, green: green / 255, blue: blue / 255)
|
||||
}
|
||||
|
||||
static let canvas = color(243, 245, 248)
|
||||
@@ -62,37 +71,48 @@ private enum AIPalette {
|
||||
static let muted = color(111, 124, 143)
|
||||
static let line = color(224, 229, 235)
|
||||
static let brand = color(19, 30, 48)
|
||||
static let primary = color(17, 119, 110)
|
||||
static let danger = color(182, 77, 72)
|
||||
static let warning = color(184, 120, 17)
|
||||
static let success = color(21, 149, 112)
|
||||
#endif
|
||||
static let surfaceRaised = color(251, 252, 253)
|
||||
static let chrome = color(255, 255, 255, 0.82)
|
||||
static let onPrimary = color(255, 255, 255)
|
||||
static let onDanger = color(255, 255, 255)
|
||||
static let primary = color(17, 20, 25)
|
||||
static let danger = color(190, 65, 57)
|
||||
static let warning = color(170, 111, 23)
|
||||
static let success = color(50, 150, 101)
|
||||
#endif
|
||||
}
|
||||
|
||||
public enum AITheme {
|
||||
public static let canvas = AIPalette.canvas
|
||||
public static let surface = AIPalette.surface
|
||||
public static let control = AIPalette.control
|
||||
public static let ink = AIPalette.ink
|
||||
public static let muted = AIPalette.muted
|
||||
public static let line = AIPalette.line
|
||||
public static let brand = AIPalette.brand
|
||||
public static let primary = AIPalette.primary
|
||||
public static let danger = AIPalette.danger
|
||||
public static let warning = AIPalette.warning
|
||||
public static let success = AIPalette.success
|
||||
public static let canvas = AIPalette.canvas
|
||||
public static let surface = AIPalette.surface
|
||||
public static let surfaceRaised = AIPalette.surfaceRaised
|
||||
public static let control = AIPalette.control
|
||||
public static let chrome = AIPalette.chrome
|
||||
public static let ink = AIPalette.ink
|
||||
public static let muted = AIPalette.muted
|
||||
public static let line = AIPalette.line
|
||||
public static let brand = AIPalette.brand
|
||||
public static let primary = AIPalette.primary
|
||||
public static let onPrimary = AIPalette.onPrimary
|
||||
public static let danger = AIPalette.danger
|
||||
public static let onDanger = AIPalette.onDanger
|
||||
public static let warning = AIPalette.warning
|
||||
public static let success = AIPalette.success
|
||||
|
||||
public static let buttonHeight: CGFloat = 44
|
||||
public static let compactButtonHeight: CGFloat = 40
|
||||
public static let largeButtonHeight: CGFloat = 50
|
||||
public static let toolbarButtonHeight: CGFloat = 38
|
||||
public static let iconButtonSize: CGFloat = 44
|
||||
public static let buttonRadius: CGFloat = 12
|
||||
public static let cardRadius: CGFloat = 16
|
||||
public static let compactButtonMinWidth: CGFloat = 58
|
||||
public static let regularButtonMinWidth: CGFloat = 76
|
||||
public static let toolbarButtonMinWidth: CGFloat = 88
|
||||
public static let toolbarButtonWidth: CGFloat = 100
|
||||
public static let footerSecondaryButtonWidth: CGFloat = 100
|
||||
public static let listActionButtonWidth: CGFloat = 72
|
||||
public static let buttonHeight: CGFloat = 48
|
||||
public static let compactButtonHeight: CGFloat = 44
|
||||
public static let largeButtonHeight: CGFloat = 56
|
||||
public static let toolbarButtonHeight: CGFloat = 44
|
||||
public static let iconButtonSize: CGFloat = 44
|
||||
public static let buttonRadius: CGFloat = 16
|
||||
public static let cardRadius: CGFloat = 22
|
||||
public static let inputRadius: CGFloat = 16
|
||||
public static let inputHeight: CGFloat = 52
|
||||
public static let segmentedControlHeight: CGFloat = 44
|
||||
public static let compactButtonMinWidth: CGFloat = 72
|
||||
public static let regularButtonMinWidth: CGFloat = 88
|
||||
public static let toolbarButtonMinWidth: CGFloat = 96
|
||||
public static let toolbarButtonWidth: CGFloat = 104
|
||||
public static let footerSecondaryButtonWidth: CGFloat = 104
|
||||
public static let listActionButtonWidth: CGFloat = 84
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user