Some checks failed
CI / test (push) Has been cancelled
Move the app payload under swift/ while keeping git, package.json, and .smartconfig.json at the repo root. This standardizes the Swift app setup so build, test, run, and watch workflows match the other repos.
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)
|
|
}
|
|
}
|