Files
honeyDueKMP/iosApp/iosApp/Subviews/Residence/SummaryCard.swift
Trey T af73f8861b iOS VoiceOver accessibility overhaul — 67 files
New framework:
- AccessibilityLabels.swift: centralized A11y struct with VoiceOver strings
- AccessibilityModifiers.swift: reusable .a11yHeader, .a11yDecorative,
  .a11yButton, .a11yCard, .a11yStatValue View extensions

Shared components: decorative elements hidden, stat views combined,
status/priority badges labeled, error views announced, empty states grouped

Cards: ResidenceCard, TaskCard, DynamicTaskCard, ContractorCard,
DocumentCard, WarrantyCard — all grouped with combined labels,
chevrons hidden, action buttons labeled

Main screens: Login, Register, Residences, Tasks, Contractors, Documents —
toolbar buttons labeled, section headers marked, form field hints added

Onboarding: all 10 views — header traits, button hints, task selection
state, progress indicator, decorative backgrounds hidden

Profile/Subscription: toggle hints, theme selection state, feature
comparison table accessibility, subscription button labels

iOS build verified: BUILD SUCCEEDED
2026-03-26 14:51:29 -05:00

261 lines
8.4 KiB
Swift

import SwiftUI
import ComposeApp
struct SummaryCard: View {
let summary: TotalSummary
@Environment(\.colorScheme) var colorScheme
var body: some View {
VStack(spacing: 0) {
// Header
Text("Your Home Dashboard")
.font(.system(size: 22, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, OrganicSpacing.cozy)
.padding(.top, OrganicSpacing.cozy)
.padding(.bottom, 20)
.accessibilityAddTraits(.isHeader)
// Main Stats Row
HStack(spacing: 0) {
OrganicStatItem(
icon: "outline",
value: "\(summary.totalResidences)",
label: "Properties",
accentColor: Color.appPrimary
)
// Vertical divider
Rectangle()
.fill(Color.appTextSecondary.opacity(0.12))
.frame(width: 1, height: 50)
OrganicStatItem(
icon: "checklist",
value: "\(summary.totalTasks)",
label: "Total Tasks",
accentColor: Color.appSecondary
)
}
.padding(.horizontal, 12)
.padding(.bottom, 16)
// Organic divider
OrganicDivider()
.padding(.horizontal, 24)
// Timeline Stats
HStack(spacing: 8) {
TimelineStatPill(
icon: "exclamationmark.circle.fill",
value: "\(summary.totalOverdue)",
label: "Overdue",
color: summary.totalOverdue > 0 ? Color.appError : Color.appTextSecondary,
isAlert: summary.totalOverdue > 0
)
TimelineStatPill(
icon: "clock.fill",
value: "\(summary.tasksDueNextWeek)",
label: "Next 7 Days",
color: Color.appAccent
)
TimelineStatPill(
icon: "arrow.forward.circle.fill",
value: "\(summary.tasksDueNextMonth)",
label: "Next 30 Days",
color: Color.appPrimary.opacity(0.7)
)
}
.padding(.horizontal, OrganicSpacing.cozy)
.padding(.top, 16)
.padding(.bottom, OrganicSpacing.cozy)
}
.background(SummaryCardBackground())
.clipShape(RoundedRectangle(cornerRadius: 32, style: .continuous))
.naturalShadow(.pronounced)
}
}
// MARK: - Organic Stat Item
private struct OrganicStatItem: View {
let icon: String
let value: String
let label: String
var accentColor: Color = Color.appPrimary
var body: some View {
VStack(spacing: 8) {
// Icon with soft background
ZStack {
Circle()
.fill(accentColor.opacity(0.12))
.frame(width: 40, height: 40)
if icon == "outline" {
Image("outline")
.renderingMode(.template)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20)
.foregroundColor(accentColor)
} else {
Image(systemName: icon)
.font(.system(size: 18, weight: .semibold))
.foregroundColor(accentColor)
}
}
// Value
Text(value)
.font(.system(size: 28, weight: .bold, design: .rounded))
.foregroundColor(Color.appTextPrimary)
// Label
Text(label)
.font(.system(size: 12, weight: .medium))
.foregroundColor(Color.appTextSecondary)
}
.frame(maxWidth: .infinity)
.accessibilityElement(children: .combine)
}
}
// MARK: - Timeline Stat Pill
private struct TimelineStatPill: View {
let icon: String
let value: String
let label: String
var color: Color = Color.appPrimary
var isAlert: Bool = false
var body: some View {
VStack(spacing: 8) {
HStack(spacing: 6) {
Image(systemName: icon)
.font(.system(size: 17, weight: .semibold))
.foregroundColor(color)
Text(value)
.font(.system(size: 24, weight: .bold, design: .rounded))
.foregroundColor(isAlert ? color : Color.appTextPrimary)
}
Text(label)
.font(.system(size: 13, weight: .medium))
.foregroundColor(Color.appTextSecondary)
}
.frame(maxWidth: .infinity)
.accessibilityElement(children: .combine)
.padding(.vertical, 18)
.background(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.fill(
isAlert
? color.opacity(0.08)
: Color.appBackgroundPrimary.opacity(0.5)
)
.overlay(
RoundedRectangle(cornerRadius: 20, style: .continuous)
.stroke(
isAlert ? color.opacity(0.2) : Color.clear,
lineWidth: 1
)
)
)
}
}
// MARK: - Summary Card Background
private struct SummaryCardBackground: View {
@Environment(\.colorScheme) var colorScheme
var body: some View {
ZStack {
// Base gradient
LinearGradient(
colors: [
Color.appBackgroundSecondary,
Color.appBackgroundSecondary.opacity(0.95)
],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
// Decorative blob in top-right
GeometryReader { geo in
OrganicBlobShape(variation: 0)
.fill(
RadialGradient(
colors: [
Color.appPrimary.opacity(colorScheme == .dark ? 0.08 : 0.05),
Color.appPrimary.opacity(0.01)
],
center: .center,
startRadius: 0,
endRadius: geo.size.width * 0.4
)
)
.frame(width: geo.size.width * 0.6, height: geo.size.height * 0.8)
.offset(x: geo.size.width * 0.5, y: -geo.size.height * 0.15)
.blur(radius: 25)
}
// Secondary blob in bottom-left
GeometryReader { geo in
OrganicBlobShape(variation: 2)
.fill(
RadialGradient(
colors: [
Color.appAccent.opacity(colorScheme == .dark ? 0.06 : 0.04),
Color.appAccent.opacity(0.01)
],
center: .center,
startRadius: 0,
endRadius: geo.size.width * 0.3
)
)
.frame(width: geo.size.width * 0.4, height: geo.size.height * 0.5)
.offset(x: -geo.size.width * 0.15, y: geo.size.height * 0.6)
.blur(radius: 20)
}
// Grain texture
GrainTexture(opacity: 0.015)
}
}
}
// MARK: - Preview
#Preview("Summary Card") {
VStack(spacing: 24) {
SummaryCard(summary: TotalSummary(
totalResidences: 3,
totalTasks: 24,
totalPending: 8,
totalOverdue: 2,
tasksDueNextWeek: 5,
tasksDueNextMonth: 12
))
SummaryCard(summary: TotalSummary(
totalResidences: 1,
totalTasks: 8,
totalPending: 3,
totalOverdue: 0,
tasksDueNextWeek: 2,
tasksDueNextMonth: 4
))
}
.padding(.horizontal, 16)
.padding(.vertical, 24)
.background(WarmGradientBackground())
}