Apply Emil Kowalski design across all screens

Near-monochrome palette (ink tokens), black primary buttons, spring
animations, 0.5pt hairline dividers, shadow cards, squircle corners.
Replaces all blue/coloured accents. Adds ButtonStyles component.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Robin Kutesa
2026-05-16 00:43:31 +03:00
parent 848c1da820
commit 04e7a77c1a
14 changed files with 889 additions and 625 deletions

View File

@@ -8,20 +8,20 @@ struct HistoryView: View {
Color.white.ignoresSafeArea()
if store.historyEntries.isEmpty {
VStack(spacing: 16) {
VStack(spacing: 12) {
Image(systemName: "clock.arrow.circlepath")
.font(.system(size: 48))
.foregroundColor(AppTheme.textTertiary)
.font(.system(size: 36, weight: .light))
.foregroundStyle(AppTheme.inkQuaternary)
Text("No backup history yet")
.font(AppTheme.bodyFont)
.foregroundColor(AppTheme.textSecondary)
.font(AppTheme.body())
.foregroundStyle(AppTheme.inkTertiary)
}
} else {
List {
ForEach(store.historyEntries) { entry in
HistoryRowView(entry: entry)
.listRowSeparator(.visible)
.listRowInsets(EdgeInsets(top: 0, leading: 0, bottom: 0, trailing: 0))
.listRowSeparator(.hidden)
.listRowInsets(EdgeInsets())
}
.onDelete { offsets in
store.removeHistoryEntries(at: offsets)
@@ -35,7 +35,8 @@ struct HistoryView: View {
if !store.historyEntries.isEmpty {
ToolbarItem(placement: .navigationBarTrailing) {
Button("Clear") { store.clearHistory() }
.foregroundColor(AppTheme.red)
.font(AppTheme.caption())
.foregroundStyle(AppTheme.destructive)
}
}
}
@@ -45,53 +46,79 @@ struct HistoryView: View {
struct HistoryRowView: View {
let entry: BackupHistoryEntry
private var hasErrors: Bool { entry.failedCount > 0 }
var body: some View {
HStack(spacing: 14) {
Circle()
.fill(entry.failedCount == 0 ? AppTheme.green : AppTheme.orange)
.frame(width: 10, height: 10)
VStack(spacing: 0) {
HStack(alignment: .top, spacing: 14) {
Circle()
.fill(hasErrors ? AppTheme.destructive : AppTheme.positive)
.frame(width: 7, height: 7)
.padding(.top, 5)
VStack(alignment: .leading, spacing: 4) {
HStack {
Text(entry.date, style: .date)
.font(.system(size: 15, weight: .semibold))
.foregroundColor(AppTheme.textPrimary)
if entry.triggeredByLAN {
Text("AUTO")
.font(.system(size: 10, weight: .bold))
.foregroundColor(AppTheme.blue)
.padding(.horizontal, 5)
.padding(.vertical, 2)
.overlay(RoundedRectangle(cornerRadius: 4).stroke(AppTheme.blue, lineWidth: 1))
VStack(alignment: .leading, spacing: 5) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(entry.date, style: .date)
.font(.system(size: 15, weight: .medium))
.foregroundStyle(AppTheme.ink)
if entry.triggeredByLAN {
Text("AUTO")
.font(.system(size: 9, weight: .semibold))
.foregroundStyle(AppTheme.inkSecondary)
.padding(.horizontal, 5)
.padding(.vertical, 2)
.overlay(
RoundedRectangle(cornerRadius: 3, style: .continuous)
.stroke(AppTheme.inkQuaternary, lineWidth: 0.75)
)
}
Spacer()
Text(entry.date, style: .time)
.font(AppTheme.micro(11))
.foregroundStyle(AppTheme.inkTertiary)
}
Spacer()
Text(entry.date, style: .time)
.font(AppTheme.captionFont)
.foregroundColor(AppTheme.textTertiary)
HStack(spacing: 8) {
Text("\(entry.uploadedCount) uploaded")
.font(AppTheme.caption())
.foregroundStyle(AppTheme.inkSecondary)
if entry.skippedCount > 0 {
dot
Text("\(entry.skippedCount) skipped")
.font(AppTheme.caption())
.foregroundStyle(AppTheme.inkTertiary)
}
if entry.failedCount > 0 {
dot
Text("\(entry.failedCount) errors")
.font(AppTheme.caption())
.foregroundStyle(AppTheme.destructive)
}
}
Text(entry.nasHost)
.font(AppTheme.micro(11))
.foregroundStyle(AppTheme.inkQuaternary)
}
HStack(spacing: 12) {
statChip("\(entry.uploadedCount) uploaded", color: AppTheme.textSecondary)
if entry.skippedCount > 0 {
statChip("\(entry.skippedCount) skipped", color: AppTheme.textTertiary)
}
if entry.failedCount > 0 {
statChip("\(entry.failedCount) errors", color: AppTheme.red)
}
}
Text(entry.nasHost)
.font(AppTheme.captionFont)
.foregroundColor(AppTheme.textTertiary)
}
.padding(.horizontal, 16)
.padding(.vertical, 14)
Rectangle()
.fill(AppTheme.separator)
.frame(height: 0.5)
.padding(.leading, 37)
}
.padding(.horizontal, 16)
.padding(.vertical, 12)
}
private func statChip(_ text: String, color: Color) -> some View {
Text(text)
.font(AppTheme.captionFont)
.foregroundColor(color)
private var dot: some View {
Circle()
.fill(AppTheme.inkQuaternary)
.frame(width: 3, height: 3)
}
}