import SwiftUI public struct AICard: View { private let content: Content private let padding: CGFloat public init(padding: CGFloat = 16, @ViewBuilder content: () -> Content) { self.padding = padding self.content = content() } public var body: some View { content .padding(padding) .background(AITheme.surface) .clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius)) .shadow(color: .black.opacity(0.045), radius: 8, x: 0, y: 3) .overlay { RoundedRectangle(cornerRadius: AITheme.cardRadius) .strokeBorder(AITheme.line) } } } public struct AITag: View { private let text: String private let tint: Color public init(_ text: String, tint: Color = AITheme.primary) { self.text = text self.tint = tint } public var body: some View { Text(text) .font(.system(size: 12, weight: .semibold)) .foregroundStyle(tint) .lineLimit(1) .truncationMode(.middle) .padding(.horizontal, 9) .frame(minHeight: 26) .background(tint.opacity(0.10)) .clipShape(Capsule()) } } public struct AISectionTitle: View { private let title: String private let systemImage: String private let detail: String? private let footer: Footer public init( _ title: String, systemImage: String, detail: String? = nil, actionTitle: String? = nil, actionSystemImage: String? = nil, actionKind: AIButtonStyle.Kind = .secondary, action: (() -> Void)? = nil ) { self.title = title self.systemImage = systemImage self.detail = detail self.footer = Footer( title: actionTitle, systemImage: actionSystemImage, kind: actionKind, action: action ) } private struct Footer: View { let title: String? let systemImage: String? let kind: AIButtonStyle.Kind let action: (() -> Void)? var body: some View { if let title, let action { Button { action() } label: { if let systemImage { Label(title, systemImage: systemImage) } else { Text(title) } } .buttonStyle(AIButtonStyle(kind: kind, size: .compact)) } } } public var body: some View { HStack(spacing: 8) { Image(systemName: systemImage) .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.primary) Text(title) .font(.headline.weight(.bold)) .foregroundStyle(AITheme.ink) .lineLimit(1) .frame(maxWidth: .infinity, alignment: .leading) if let detail { Text(detail) .font(.caption.weight(.medium)) .foregroundStyle(AITheme.muted) .lineLimit(1) } if Footer.self != EmptyView.self { footer } } } } public struct AIFieldLabel: View { private let title: String public init(_ title: String) { self.title = title } public var body: some View { Text(title) .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.ink) } } public struct AIStatCard: View { private let title: String private let value: String private let detail: String? private let tint: Color public init(title: String, value: String, detail: String? = nil, tint: Color) { self.title = title self.value = value self.detail = detail self.tint = tint } public var body: some View { AICard(padding: 14) { VStack(alignment: .leading, spacing: 5) { Text(title) .font(.caption.weight(.semibold)) .foregroundStyle(AITheme.muted) Text(value) .font(.title3.weight(.bold)) .foregroundStyle(tint) .lineLimit(1) .minimumScaleFactor(0.75) if let detail { Text(detail) .font(.caption) .foregroundStyle(AITheme.muted) .lineLimit(2) } } .frame(maxWidth: .infinity, alignment: .leading) } } } public struct AIStatusBanner: View { private let message: String private let tint: Color private let systemImage: String public init(_ message: String, tint: Color, systemImage: String) { self.message = message self.tint = tint self.systemImage = systemImage } public var body: some View { HStack(alignment: .top, spacing: 10) { Image(systemName: systemImage) .font(.subheadline.weight(.semibold)) .foregroundStyle(tint) Text(message) .font(.subheadline.weight(.medium)) .foregroundStyle(tint) .frame(maxWidth: .infinity, alignment: .leading) } .padding(14) .background(tint.opacity(0.10)) .clipShape(RoundedRectangle(cornerRadius: AITheme.cardRadius)) .overlay { RoundedRectangle(cornerRadius: AITheme.cardRadius) .strokeBorder(tint.opacity(0.22)) } .accessibilityElement(children: .combine) } } public struct AIEmptyState: View { private let systemImage: String private let title: String private let message: String public init(systemImage: String, title: String, message: String) { self.systemImage = systemImage self.title = title self.message = message } public var body: some View { VStack(spacing: 10) { Image(systemName: systemImage) .font(.system(size: 26)) .foregroundStyle(AITheme.primary) .frame(width: 54, height: 54) .background(AITheme.primary.opacity(0.10)) .clipShape(Circle()) Text(title) .font(.headline.weight(.bold)) .foregroundStyle(AITheme.ink) Text(message) .font(.subheadline) .foregroundStyle(AITheme.muted) .multilineTextAlignment(.center) .fixedSize(horizontal: false, vertical: true) } .frame(maxWidth: .infinity) .padding(.vertical, 18) .accessibilityElement(children: .combine) } } public struct AIListCard: View { private let title: String private let subtitle: String? private let status: String? private let statusTint: Color? private let details: [String] private let footer: Footer public init( title: String, subtitle: String? = nil, status: String? = nil, statusTint: Color? = nil, details: [String] = [], @ViewBuilder footer: () -> Footer = { EmptyView() } ) { self.title = title self.subtitle = subtitle self.status = status self.statusTint = statusTint self.details = details self.footer = footer() } 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) 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 Footer.self != EmptyView.self { footer } } .accessibilityElement(children: .combine) } } public struct AILoadingCard: View { private let message: String public init(_ message: String) { self.message = message } public var body: some View { VStack(spacing: 10) { ProgressView() .controlSize(.regular) Text(message) .font(.subheadline) .foregroundStyle(AITheme.muted) } .frame(maxWidth: .infinity) .padding(.vertical, 18) .accessibilityElement(children: .combine) } } public struct AIKeyValueRow: View { private let title: String private let value: String private let valueTint: Color public init(_ title: String, value: String, tint: Color = AITheme.ink) { self.title = title self.value = value self.valueTint = tint } public var body: some View { HStack(alignment: .firstTextBaseline, spacing: 12) { Text(title) .font(.subheadline) .foregroundStyle(AITheme.muted) .lineLimit(2) Spacer(minLength: 12) Text(value) .font(.subheadline.weight(.medium)) .foregroundStyle(valueTint) .multilineTextAlignment(.trailing) .lineLimit(2) .truncationMode(.middle) .minimumScaleFactor(0.85) .textSelection(.enabled) } .accessibilityElement(children: .combine) } } public struct AISegmentedControl: View { private let title: String private let options: [(value: Value, title: String)] @Binding private var selection: Value public init( _ title: String, selection: Binding, options: [(value: Value, title: String)] ) { self.title = title self._selection = selection self.options = options } public var body: some View { HStack(spacing: 4) { ForEach(options, id: \.value) { option in let isSelected = selection == option.value Button { selection = option.value } label: { Text(option.title) .font(.subheadline.weight(isSelected ? .semibold : .medium)) .lineLimit(1) .minimumScaleFactor(0.75) .foregroundStyle(isSelected ? .white : AITheme.muted) .frame(maxWidth: .infinity, minHeight: 34) .background { if isSelected { RoundedRectangle(cornerRadius: 9) .fill(AITheme.primary) } } } .buttonStyle(.plain) .accessibilityLabel(option.title) .accessibilityAddTraits(isSelected ? .isSelected : []) } } .padding(4) .background(AITheme.control) .clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius)) .overlay { RoundedRectangle(cornerRadius: AITheme.buttonRadius) .strokeBorder(AITheme.line) } .animation(.easeOut(duration: 0.16), value: selection) .accessibilityElement(children: .contain) .accessibilityLabel(title) } } public struct AIFieldStyle: TextFieldStyle { public init() {} public func _body(configuration: TextField) -> some View { configuration .aiInputSurface() } } public struct AISearchField: View { private let placeholder: String @Binding private var text: String private var onSubmit: (() -> Void)? public init( _ placeholder: String, text: Binding, onSubmit: (() -> Void)? = nil ) { self.placeholder = placeholder self._text = text self.onSubmit = onSubmit } public var body: some View { HStack(spacing: 6) { Image(systemName: "magnifyingglass") .foregroundStyle(AITheme.muted) TextField(placeholder, text: $text) .textFieldStyle(.plain) .submitLabel(.search) .onSubmit { onSubmit?() } if !text.isEmpty { Button { text = "" onSubmit?() } label: { Image(systemName: "xmark.circle.fill") .foregroundStyle(AITheme.muted) .font(.system(size: 16, weight: .medium)) } .buttonStyle(.plain) .frame(width: 28, height: 28) .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) } } extension View { func aiInputSurface(minHeight: CGFloat = AITheme.buttonHeight) -> some View { self .padding(.horizontal, 12) .frame(minHeight: minHeight, alignment: .leading) .background(AITheme.control) .clipShape(RoundedRectangle(cornerRadius: AITheme.buttonRadius)) .overlay { RoundedRectangle(cornerRadius: AITheme.buttonRadius) .strokeBorder(AITheme.line) } } func aiBottomActionBar(@ViewBuilder content: () -> Content) -> some View { safeAreaInset(edge: .bottom) { content() .padding(.horizontal, 16) .padding(.top, 10) .padding(.bottom, 8) .background(AITheme.canvas) .overlay(alignment: .top) { Rectangle() .fill(AITheme.line) .frame(height: 1) } } } func aiSelectSurface() -> some View { self .tint(AITheme.ink) .font(.subheadline.weight(.semibold)) .foregroundStyle(AITheme.ink) .labelsHidden() .frame(maxWidth: .infinity, alignment: .leading) .aiInputSurface() } } public struct AIToolbarButton: View { private let title: String private let systemImage: String? private let kind: AIButtonStyle.Kind private let action: () -> Void public init( _ title: String, systemImage: String? = nil, kind: AIButtonStyle.Kind = .secondary, action: @escaping () -> Void ) { self.title = title self.systemImage = systemImage self.kind = kind self.action = action } public var body: some View { Button { action() } label: { if let systemImage { Label(title, systemImage: systemImage) .labelStyle(.titleAndIcon) .font(.system(size: 15, weight: .semibold)) } else { Text(title) .font(.system(size: 15, weight: .semibold)) } } .buttonStyle(AIButtonStyle(kind: kind, size: .toolbar)) .accessibilityLabel(title) } }