234 lines
6.5 KiB
Swift
234 lines
6.5 KiB
Swift
import SwiftUI
|
|
|
|
struct AppSectionTitle: View {
|
|
let title: String
|
|
var subtitle: String? = nil
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(title)
|
|
.font(.title3.weight(.semibold))
|
|
|
|
if let subtitle, !subtitle.isEmpty {
|
|
Text(subtitle)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
struct AppNotice: View {
|
|
let message: String
|
|
var tone: Color = AppTheme.accent
|
|
|
|
var body: some View {
|
|
HStack(spacing: 10) {
|
|
Image(systemName: "checkmark.circle.fill")
|
|
.font(.footnote.weight(.bold))
|
|
.foregroundStyle(tone)
|
|
Text(message)
|
|
.font(.subheadline.weight(.semibold))
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
.background(tone.opacity(0.08), in: Capsule())
|
|
.overlay(
|
|
Capsule()
|
|
.stroke(AppTheme.border, lineWidth: 1)
|
|
)
|
|
}
|
|
}
|
|
|
|
struct AppStatusTag: View {
|
|
let title: String
|
|
var tone: Color = AppTheme.accent
|
|
|
|
var body: some View {
|
|
Text(title)
|
|
.font(.caption.weight(.semibold))
|
|
.lineLimit(1)
|
|
.minimumScaleFactor(0.8)
|
|
.fixedSize(horizontal: true, vertical: false)
|
|
.padding(.horizontal, 10)
|
|
.padding(.vertical, 6)
|
|
.background(tone.opacity(0.12), in: Capsule())
|
|
.foregroundStyle(tone)
|
|
}
|
|
}
|
|
|
|
struct AppKeyValue: View {
|
|
let label: String
|
|
let value: String
|
|
var monospaced: Bool = false
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(label.uppercased())
|
|
.font(.caption2.weight(.bold))
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(value)
|
|
.font(monospaced ? .subheadline.monospaced() : .subheadline.weight(.semibold))
|
|
.lineLimit(2)
|
|
.minimumScaleFactor(0.8)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
struct AppMetric: View {
|
|
let title: String
|
|
let value: String
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 6) {
|
|
Text(title.uppercased())
|
|
.font(.caption.weight(.bold))
|
|
.foregroundStyle(.secondary)
|
|
|
|
Text(value)
|
|
.font(.title3.weight(.bold))
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
struct AppTextSurface: View {
|
|
let text: String
|
|
var monospaced: Bool = false
|
|
|
|
var body: some View {
|
|
content
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(16)
|
|
.background(AppTheme.mutedFill, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
|
.stroke(AppTheme.border, lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var content: some View {
|
|
#if os(watchOS)
|
|
Text(text)
|
|
.font(monospaced ? .body.monospaced() : .body)
|
|
#else
|
|
Text(text)
|
|
.font(monospaced ? .body.monospaced() : .body)
|
|
.textSelection(.enabled)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
struct AppTextEditorField: View {
|
|
@Binding var text: String
|
|
var minHeight: CGFloat = 120
|
|
var monospaced: Bool = true
|
|
|
|
var body: some View {
|
|
editor
|
|
.frame(minHeight: minHeight)
|
|
.background(AppTheme.mutedFill, in: RoundedRectangle(cornerRadius: 20, style: .continuous))
|
|
.overlay(
|
|
RoundedRectangle(cornerRadius: 20, style: .continuous)
|
|
.stroke(AppTheme.border, lineWidth: 1)
|
|
)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var editor: some View {
|
|
#if os(watchOS)
|
|
Text(text)
|
|
.font(monospaced ? .body.monospaced() : .body)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
.padding(14)
|
|
#else
|
|
TextEditor(text: $text)
|
|
.font(monospaced ? .body.monospaced() : .body)
|
|
.scrollContentBackground(.hidden)
|
|
.autocorrectionDisabled()
|
|
.padding(14)
|
|
#endif
|
|
}
|
|
}
|
|
|
|
struct AppActionRow: View {
|
|
let title: String
|
|
var subtitle: String? = nil
|
|
let systemImage: String
|
|
var tone: Color = AppTheme.accent
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
Image(systemName: systemImage)
|
|
.font(.subheadline.weight(.semibold))
|
|
.foregroundStyle(tone)
|
|
.frame(width: 28, height: 28)
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(title)
|
|
.font(.headline)
|
|
|
|
if let subtitle, !subtitle.isEmpty {
|
|
Text(subtitle)
|
|
.font(.subheadline)
|
|
.foregroundStyle(.secondary)
|
|
.multilineTextAlignment(.leading)
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
Image(systemName: "arrow.right")
|
|
.font(.footnote.weight(.bold))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
|
|
struct AppActionTile: View {
|
|
let title: String
|
|
let systemImage: String
|
|
var tone: Color = AppTheme.accent
|
|
var isBusy: Bool = false
|
|
|
|
var body: some View {
|
|
VStack(alignment: .leading, spacing: 14) {
|
|
HStack(alignment: .center) {
|
|
ZStack {
|
|
Circle()
|
|
.fill(tone.opacity(0.10))
|
|
.frame(width: 38, height: 38)
|
|
|
|
if isBusy {
|
|
ProgressView()
|
|
.tint(tone)
|
|
} else {
|
|
Image(systemName: systemImage)
|
|
.font(.headline.weight(.semibold))
|
|
.foregroundStyle(tone)
|
|
}
|
|
}
|
|
|
|
Spacer(minLength: 0)
|
|
|
|
Image(systemName: "arrow.up.right")
|
|
.font(.caption.weight(.bold))
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
|
|
Text(title)
|
|
.font(.headline)
|
|
.multilineTextAlignment(.leading)
|
|
.lineLimit(2)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.padding(16)
|
|
.frame(maxWidth: .infinity, minHeight: 92, alignment: .topLeading)
|
|
.appSurface(radius: 22)
|
|
}
|
|
}
|