Implement modern design system for iOS app

Created a comprehensive, modern design system and applied it to key views for a sleek, contemporary look.

Design System Features:
- Modern color palette with blue primary and purple accent colors
- Semantic colors for success, warning, error, and info states
- Typography scale with SF Rounded for headlines
- Spacing and radius systems for consistent layout
- Shadow utilities for depth and elevation
- Reusable button and card styles

Modernized Views:
- LoginView: Card-based layout with gradient app icon, animated focus states, gradient buttons
- HomeScreenView: Personalized greeting, modern navigation cards
- OverviewCard: Stats grid with circular icon badges and dividers
- StatView: Circular backgrounds with modern typography
- HomeNavigationCard: Gradient icon backgrounds with clean hierarchy

Key Improvements:
- Replaced hardcoded colors with design system tokens
- Added gradient accents for visual interest
- Improved spacing and typography hierarchy
- Added subtle shadows for depth
- Implemented animated focus states
- Better disabled and loading states for buttons

Documentation:
- Added DESIGN_SYSTEM.md with comprehensive usage guide
- Documented colors, typography, spacing, and component patterns
- Included migration guide for updating remaining views

All views remain separate files as requested.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Trey t
2025-11-10 11:45:45 -06:00
parent 77a118a6f7
commit 4c9dc56e64
7 changed files with 737 additions and 162 deletions

205
iosApp/DESIGN_SYSTEM.md Normal file
View File

@@ -0,0 +1,205 @@
# MyCrib iOS Design System
## Overview
This document outlines the modern, sleek design system implemented for the MyCrib iOS app.
## Design Philosophy
- **Modern & Clean**: Minimalist approach with ample white space
- **Consistent**: Reusable components with unified styling
- **Accessible**: High contrast ratios and readable typography
- **Delightful**: Subtle animations and gradient accents
## Color Palette
### Primary Colors
- **Primary**: `#2563EB` - Modern blue for primary actions
- **Primary Light**: `#3B82F6` - Lighter variant for gradients
- **Primary Dark**: `#1E40AF` - Darker variant for pressed states
### Accent Colors
- **Accent**: `#8B5CF6` - Purple for special highlights
- **Accent Light**: `#A78BFA` - Lighter purple for gradients
### Semantic Colors
- **Success**: `#10B981` - Green for completed/success states
- **Warning**: `#F59E0B` - Orange for in-progress/warning states
- **Error**: `#EF4444` - Red for errors and destructive actions
- **Info**: `#3B82F6` - Blue for informational content
### Neutral Colors
- **Background**: `#F9FAFB` - Light gray for app background
- **Surface**: `#FFFFFF` - White for cards and surfaces
- **Surface Secondary**: `#F3F4F6` - Light gray for secondary surfaces
- **Text Primary**: `#111827` - Near black for primary text
- **Text Secondary**: `#6B7280` - Medium gray for secondary text
- **Text Tertiary**: `#9CA3AF` - Light gray for tertiary text
- **Border**: `#E5E7EB` - Light gray for borders
- **Border Light**: `#F3F4F6` - Very light gray for subtle borders
## Typography
### Display (Hero Sections)
- **Display Large**: 57pt, Bold, Rounded
- **Display Medium**: 45pt, Bold, Rounded
- **Display Small**: 36pt, Bold, Rounded
### Headline (Section Headers)
- **Headline Large**: 32pt, Bold, Rounded
- **Headline Medium**: 28pt, Semibold, Rounded
- **Headline Small**: 24pt, Semibold, Rounded
### Title (Card Titles)
- **Title Large**: 22pt, Semibold
- **Title Medium**: 18pt, Semibold
- **Title Small**: 16pt, Semibold
### Body (Main Content)
- **Body Large**: 17pt, Regular
- **Body Medium**: 15pt, Regular
- **Body Small**: 13pt, Regular
### Label (Labels & Captions)
- **Label Large**: 14pt, Medium
- **Label Medium**: 12pt, Medium
- **Label Small**: 11pt, Medium
### Caption
- **Caption**: 12pt, Regular
## Spacing Scale
- **XXS**: 4pt
- **XS**: 8pt
- **SM**: 12pt
- **MD**: 16pt
- **LG**: 24pt
- **XL**: 32pt
- **XXL**: 48pt
- **XXXL**: 64pt
## Border Radius
- **XS**: 4pt
- **SM**: 8pt
- **MD**: 12pt
- **LG**: 16pt
- **XL**: 20pt
- **XXL**: 24pt
- **Full**: 9999pt (Circular)
## Shadows
- **SM**: rgba(0,0,0,0.05), radius: 2, y: 1
- **MD**: rgba(0,0,0,0.1), radius: 4, y: 2
- **LG**: rgba(0,0,0,0.1), radius: 8, y: 4
- **XL**: rgba(0,0,0,0.15), radius: 16, y: 8
## Component Patterns
### Cards
- White background (`AppColors.surface`)
- Rounded corners (16pt or 20pt)
- Subtle shadow (`AppShadow.md` or `AppShadow.lg`)
- Padding: 16-32pt depending on content
### Buttons
- **Primary**: Gradient background, white text, 56pt height
- **Secondary**: Light gray background, primary color text, 56pt height
- Rounded corners: 12pt
- Pressed state: Scale to 0.98
### Text Fields
- Light gray background (`AppColors.surfaceSecondary`)
- 16pt padding
- 12pt rounded corners
- Focused state: Primary color border + subtle shadow
- Icon prefix for visual context
### Navigation Cards
- Gradient icon background in rounded rectangle
- Clear hierarchy: Title (semibold) + Subtitle (secondary color)
- Chevron indicator
- Hover/tap feedback
## Modernization Highlights
### Login Screen
- Circular gradient app icon with shadow
- Card-based form layout
- Animated focus states on input fields
- Gradient button with disabled states
- Clean error messaging
### Home Screen
- Personalized greeting header
- Stats overview card with icon badges
- Modern navigation cards with gradient icons
- Smooth scroll experience
### Components
- **OverviewCard**: Stats grid with dividers and icon badges
- **StatView**: Circular icon backgrounds with modern typography
- **HomeNavigationCard**: Gradient icons with clean layout
- **Design System**: Centralized colors, typography, and spacing
## Usage
### Importing the Design System
```swift
import SwiftUI
// Colors
.foregroundColor(AppColors.primary)
.background(AppColors.surface)
// Typography
.font(AppTypography.headlineSmall)
// Spacing
.padding(AppSpacing.md)
// Radius
.cornerRadius(AppRadius.lg)
// Shadows
.shadow(color: AppShadow.lg.color, radius: AppShadow.lg.radius, y: AppShadow.lg.y)
// Gradients
.fill(AppColors.primaryGradient)
```
### Button Styles
```swift
Button("Action") {
// action
}
.buttonStyle(PrimaryButtonStyle())
Button("Cancel") {
// action
}
.buttonStyle(SecondaryButtonStyle())
```
### Card Style
```swift
VStack {
// content
}
.cardStyle() // Applies background, corners, and shadow
```
## Future Enhancements
- Dark mode support with adaptive colors
- Additional component styles (chips, badges, alerts)
- Animation utilities for transitions
- Accessibility utilities (dynamic type, VoiceOver)
- Custom SF Symbols integration
## Migration Guide
When updating existing views:
1. Replace hardcoded colors with `AppColors.*`
2. Replace hardcoded fonts with `AppTypography.*`
3. Replace hardcoded spacing with `AppSpacing.*`
4. Use `cardStyle()` modifier for cards
5. Apply button styles for consistency
6. Add gradients to prominent UI elements
7. Ensure proper spacing hierarchy

View File

@@ -0,0 +1,225 @@
import SwiftUI
// MARK: - Design System
// Modern, sleek design system for MyCrib
struct AppColors {
// Primary Colors - Modern blue gradient
static let primary = Color(hex: "2563EB") ?? .blue
static let primaryLight = Color(hex: "3B82F6") ?? .blue
static let primaryDark = Color(hex: "1E40AF") ?? .blue
// Accent Colors
static let accent = Color(hex: "8B5CF6") ?? .purple
static let accentLight = Color(hex: "A78BFA") ?? .purple
// Semantic Colors
static let success = Color(hex: "10B981") ?? .green
static let warning = Color(hex: "F59E0B") ?? .orange
static let error = Color(hex: "EF4444") ?? .red
static let info = Color(hex: "3B82F6") ?? .blue
// Neutral Colors - Modern grays
static let background = Color(hex: "F9FAFB") ?? Color(.systemGroupedBackground)
static let surface = Color.white
static let surfaceSecondary = Color(hex: "F3F4F6") ?? Color(.secondarySystemGroupedBackground)
static let textPrimary = Color(hex: "111827") ?? Color(.label)
static let textSecondary = Color(hex: "6B7280") ?? Color(.secondaryLabel)
static let textTertiary = Color(hex: "9CA3AF") ?? Color(.tertiaryLabel)
static let border = Color(hex: "E5E7EB") ?? Color(.separator)
static let borderLight = Color(hex: "F3F4F6") ?? Color(.separator)
// Task Status Colors
static let taskUpcoming = Color(hex: "3B82F6") ?? .blue
static let taskInProgress = Color(hex: "F59E0B") ?? .orange
static let taskCompleted = Color(hex: "10B981") ?? .green
static let taskCanceled = Color(hex: "6B7280") ?? .gray
static let taskArchived = Color(hex: "9CA3AF") ?? .gray
// Gradient
static let primaryGradient = LinearGradient(
colors: [primary, primaryLight],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
static let accentGradient = LinearGradient(
colors: [accent, accentLight],
startPoint: .topLeading,
endPoint: .bottomTrailing
)
}
struct AppTypography {
// Display - For hero sections
static let displayLarge = Font.system(size: 57, weight: .bold, design: .rounded)
static let displayMedium = Font.system(size: 45, weight: .bold, design: .rounded)
static let displaySmall = Font.system(size: 36, weight: .bold, design: .rounded)
// Headline - For section headers
static let headlineLarge = Font.system(size: 32, weight: .bold, design: .rounded)
static let headlineMedium = Font.system(size: 28, weight: .semibold, design: .rounded)
static let headlineSmall = Font.system(size: 24, weight: .semibold, design: .rounded)
// Title - For card titles
static let titleLarge = Font.system(size: 22, weight: .semibold, design: .default)
static let titleMedium = Font.system(size: 18, weight: .semibold, design: .default)
static let titleSmall = Font.system(size: 16, weight: .semibold, design: .default)
// Body - For main content
static let bodyLarge = Font.system(size: 17, weight: .regular, design: .default)
static let bodyMedium = Font.system(size: 15, weight: .regular, design: .default)
static let bodySmall = Font.system(size: 13, weight: .regular, design: .default)
// Label - For labels and captions
static let labelLarge = Font.system(size: 14, weight: .medium, design: .default)
static let labelMedium = Font.system(size: 12, weight: .medium, design: .default)
static let labelSmall = Font.system(size: 11, weight: .medium, design: .default)
// Caption
static let caption = Font.system(size: 12, weight: .regular, design: .default)
}
struct AppSpacing {
static let xxs: CGFloat = 4
static let xs: CGFloat = 8
static let sm: CGFloat = 12
static let md: CGFloat = 16
static let lg: CGFloat = 24
static let xl: CGFloat = 32
static let xxl: CGFloat = 48
static let xxxl: CGFloat = 64
}
struct AppRadius {
static let xs: CGFloat = 4
static let sm: CGFloat = 8
static let md: CGFloat = 12
static let lg: CGFloat = 16
static let xl: CGFloat = 20
static let xxl: CGFloat = 24
static let full: CGFloat = 9999
}
struct AppShadow {
static let sm = Shadow(color: .black.opacity(0.05), radius: 2, y: 1)
static let md = Shadow(color: .black.opacity(0.1), radius: 4, y: 2)
static let lg = Shadow(color: .black.opacity(0.1), radius: 8, y: 4)
static let xl = Shadow(color: .black.opacity(0.15), radius: 16, y: 8)
struct Shadow {
let color: Color
let radius: CGFloat
let x: CGFloat
let y: CGFloat
init(color: Color, radius: CGFloat, x: CGFloat = 0, y: CGFloat) {
self.color = color
self.radius = radius
self.x = x
self.y = y
}
}
}
// MARK: - View Modifiers
struct CardStyle: ViewModifier {
var shadow: AppShadow.Shadow = AppShadow.md
var padding: CGFloat = AppSpacing.md
func body(content: Content) -> some View {
content
.background(AppColors.surface)
.cornerRadius(AppRadius.lg)
.shadow(color: shadow.color, radius: shadow.radius, x: shadow.x, y: shadow.y)
}
}
struct PrimaryButtonStyle: ButtonStyle {
var isLoading: Bool = false
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(AppTypography.titleSmall)
.foregroundColor(.white)
.frame(maxWidth: .infinity)
.frame(height: 56)
.background(
configuration.isPressed ? AppColors.primaryDark : AppColors.primary
)
.cornerRadius(AppRadius.md)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
struct SecondaryButtonStyle: ButtonStyle {
func makeBody(configuration: Configuration) -> some View {
configuration.label
.font(AppTypography.titleSmall)
.foregroundColor(AppColors.primary)
.frame(maxWidth: .infinity)
.frame(height: 56)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.md)
.scaleEffect(configuration.isPressed ? 0.98 : 1.0)
.animation(.easeInOut(duration: 0.1), value: configuration.isPressed)
}
}
struct TextFieldStyle: ViewModifier {
func body(content: Content) -> some View {
content
.padding(AppSpacing.md)
.background(AppColors.surfaceSecondary)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(AppColors.borderLight, lineWidth: 1)
)
}
}
// MARK: - View Extensions
extension View {
func cardStyle(shadow: AppShadow.Shadow = AppShadow.md) -> some View {
modifier(CardStyle(shadow: shadow))
}
func textFieldStyle() -> some View {
modifier(TextFieldStyle())
}
}
// MARK: - Color Extension for Hex Support
extension Color {
init?(hex: String) {
let hex = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)
var int: UInt64 = 0
Scanner(string: hex).scanHexInt64(&int)
let a, r, g, b: UInt64
switch hex.count {
case 3: // RGB (12-bit)
(a, r, g, b) = (255, (int >> 8) * 17, (int >> 4 & 0xF) * 17, (int & 0xF) * 17)
case 6: // RGB (24-bit)
(a, r, g, b) = (255, int >> 16, int >> 8 & 0xFF, int & 0xFF)
case 8: // ARGB (32-bit)
(a, r, g, b) = (int >> 24, int >> 16 & 0xFF, int >> 8 & 0xFF, int & 0xFF)
default:
return nil
}
self.init(
.sRGB,
red: Double(r) / 255,
green: Double(g) / 255,
blue: Double(b) / 255,
opacity: Double(a) / 255
)
}
}

View File

@@ -8,21 +8,43 @@ struct HomeScreenView: View {
var body: some View {
NavigationView {
ZStack {
Color(.systemGroupedBackground)
AppColors.background
.ignoresSafeArea()
if viewModel.isLoading {
ProgressView()
VStack(spacing: AppSpacing.lg) {
ProgressView()
.scaleEffect(1.2)
Text("Loading...")
.font(AppTypography.bodyMedium)
.foregroundColor(AppColors.textSecondary)
}
} else {
ScrollView {
VStack(spacing: 20) {
ScrollView(showsIndicators: false) {
VStack(spacing: AppSpacing.xl) {
// Greeting Header
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text("Hello!")
.font(AppTypography.headlineLarge)
.fontWeight(.bold)
.foregroundColor(AppColors.textPrimary)
Text("Welcome to MyCrib")
.font(AppTypography.bodyLarge)
.foregroundColor(AppColors.textSecondary)
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(.horizontal, AppSpacing.md)
.padding(.top, AppSpacing.md)
// Overview Card
if let summary = viewModel.residenceSummary {
OverviewCard(summary: summary.summary)
.transition(.scale.combined(with: .opacity))
}
// Navigation Cards
VStack(spacing: 16) {
VStack(spacing: AppSpacing.md) {
NavigationLink(destination: ResidencesListView()) {
HomeNavigationCard(
icon: "house.fill",
@@ -30,6 +52,7 @@ struct HomeScreenView: View {
subtitle: "Manage your properties"
)
}
.buttonStyle(PlainButtonStyle())
NavigationLink(destination: AllTasksView()) {
HomeNavigationCard(
@@ -38,20 +61,26 @@ struct HomeScreenView: View {
subtitle: "View and manage all tasks"
)
}
.buttonStyle(PlainButtonStyle())
}
.padding(.horizontal)
.padding(.horizontal, AppSpacing.md)
}
.padding(.vertical)
.padding(.vertical, AppSpacing.md)
}
}
}
.navigationTitle("MyCrib")
.navigationBarTitleDisplayMode(.large)
.toolbar {
ToolbarItem(placement: .navigationBarTrailing) {
Button(action: {
loginViewModel.logout()
}) {
Image(systemName: "rectangle.portrait.and.arrow.right")
HStack(spacing: AppSpacing.xs) {
Image(systemName: "rectangle.portrait.and.arrow.right")
.font(.system(size: 18, weight: .semibold))
}
.foregroundColor(AppColors.error)
}
}
}

View File

@@ -20,131 +20,209 @@ struct LoginView: View {
var body: some View {
NavigationView {
Form {
Section {
VStack(spacing: 16) {
Image(systemName: "house.fill")
.font(.system(size: 60))
.foregroundStyle(.blue.gradient)
ZStack {
// Background gradient
AppColors.background
.ignoresSafeArea()
Text("MyCrib")
.font(.largeTitle)
.fontWeight(.bold)
ScrollView {
VStack(spacing: AppSpacing.xl) {
Spacer()
.frame(height: AppSpacing.xxxl)
Text("Manage your properties with ease")
.font(.subheadline)
.foregroundColor(.secondary)
}
.frame(maxWidth: .infinity)
.padding(.vertical)
}
.listRowBackground(Color.clear)
// Hero Section
VStack(spacing: AppSpacing.lg) {
// App Icon with gradient
ZStack {
Circle()
.fill(AppColors.primaryGradient)
.frame(width: 100, height: 100)
.shadow(color: AppColors.primary.opacity(0.3), radius: 20, y: 10)
Section {
TextField("Username or Email", text: $viewModel.username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
.onChange(of: viewModel.username) { _, _ in
viewModel.clearError()
}
HStack {
if isPasswordVisible {
TextField("Password", text: $viewModel.password)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
} else {
SecureField("Password", text: $viewModel.password)
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(.secondary)
}
.buttonStyle(.plain)
}
.onChange(of: viewModel.password) { _, _ in
viewModel.clearError()
}
} header: {
Text("Account Information")
}
if let errorMessage = viewModel.errorMessage {
Section {
HStack {
Image(systemName: "exclamationmark.triangle.fill")
.foregroundColor(.red)
Text(errorMessage)
.foregroundColor(.red)
.font(.subheadline)
}
}
}
Section {
Button(action: viewModel.login) {
HStack {
Spacer()
if viewModel.isLoading {
ProgressView()
} else {
Text("Login")
.fontWeight(.semibold)
Image(systemName: "house.fill")
.font(.system(size: 50, weight: .semibold))
.foregroundStyle(.white)
}
Spacer()
}
}
.disabled(viewModel.isLoading)
}
Section {
HStack {
Spacer()
Button("Forgot Password?") {
showPasswordReset = true
}
.font(.subheadline)
.fontWeight(.semibold)
Spacer()
}
}
.listRowBackground(Color.clear)
VStack(spacing: AppSpacing.xs) {
Text("Welcome Back")
.font(AppTypography.displaySmall)
.foregroundColor(AppColors.textPrimary)
Section {
HStack {
Spacer()
Text("Don't have an account?")
.font(.subheadline)
.foregroundColor(.secondary)
Button("Sign Up") {
showingRegister = true
Text("Sign in to manage your properties")
.font(AppTypography.bodyMedium)
.foregroundColor(AppColors.textSecondary)
}
}
.font(.subheadline)
.fontWeight(.semibold)
// Login Card
VStack(spacing: AppSpacing.lg) {
// Username Field
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text("Email or Username")
.font(AppTypography.labelLarge)
.foregroundColor(AppColors.textSecondary)
HStack(spacing: AppSpacing.sm) {
Image(systemName: "envelope.fill")
.foregroundColor(AppColors.textTertiary)
.frame(width: 20)
TextField("Enter your email", text: $viewModel.username)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.keyboardType(.emailAddress)
.focused($focusedField, equals: .username)
.submitLabel(.next)
.onSubmit {
focusedField = .password
}
.onChange(of: viewModel.username) { _, _ in
viewModel.clearError()
}
}
.padding(AppSpacing.md)
.background(AppColors.surface)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(focusedField == .username ? AppColors.primary : AppColors.border, lineWidth: 1.5)
)
.shadow(color: focusedField == .username ? AppColors.primary.opacity(0.1) : .clear, radius: 8)
.animation(.easeInOut(duration: 0.2), value: focusedField)
}
// Password Field
VStack(alignment: .leading, spacing: AppSpacing.xs) {
Text("Password")
.font(AppTypography.labelLarge)
.foregroundColor(AppColors.textSecondary)
HStack(spacing: AppSpacing.sm) {
Image(systemName: "lock.fill")
.foregroundColor(AppColors.textTertiary)
.frame(width: 20)
Group {
if isPasswordVisible {
TextField("Enter your password", text: $viewModel.password)
.textInputAutocapitalization(.never)
.autocorrectionDisabled()
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
} else {
SecureField("Enter your password", text: $viewModel.password)
.focused($focusedField, equals: .password)
.submitLabel(.go)
.onSubmit {
viewModel.login()
}
}
}
Button(action: {
isPasswordVisible.toggle()
}) {
Image(systemName: isPasswordVisible ? "eye.slash.fill" : "eye.fill")
.foregroundColor(AppColors.textTertiary)
.frame(width: 20)
}
}
.padding(AppSpacing.md)
.background(AppColors.surface)
.cornerRadius(AppRadius.md)
.overlay(
RoundedRectangle(cornerRadius: AppRadius.md)
.stroke(focusedField == .password ? AppColors.primary : AppColors.border, lineWidth: 1.5)
)
.shadow(color: focusedField == .password ? AppColors.primary.opacity(0.1) : .clear, radius: 8)
.animation(.easeInOut(duration: 0.2), value: focusedField)
.onChange(of: viewModel.password) { _, _ in
viewModel.clearError()
}
}
// Forgot Password
HStack {
Spacer()
Button("Forgot Password?") {
showPasswordReset = true
}
.font(AppTypography.labelLarge)
.foregroundColor(AppColors.primary)
}
// Error Message
if let errorMessage = viewModel.errorMessage {
HStack(spacing: AppSpacing.sm) {
Image(systemName: "exclamationmark.circle.fill")
.foregroundColor(AppColors.error)
Text(errorMessage)
.font(AppTypography.bodySmall)
.foregroundColor(AppColors.error)
Spacer()
}
.padding(AppSpacing.md)
.background(AppColors.error.opacity(0.1))
.cornerRadius(AppRadius.md)
}
// Login Button
Button(action: viewModel.login) {
HStack(spacing: AppSpacing.sm) {
if viewModel.isLoading {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
Text(viewModel.isLoading ? "Signing In..." : "Sign In")
.font(AppTypography.titleSmall)
.fontWeight(.semibold)
}
.frame(maxWidth: .infinity)
.frame(height: 56)
.foregroundColor(.white)
.background(
viewModel.isLoading || viewModel.username.isEmpty || viewModel.password.isEmpty
? AppColors.textTertiary
: AppColors.primaryGradient
)
.cornerRadius(AppRadius.md)
.shadow(
color: viewModel.username.isEmpty || viewModel.password.isEmpty ? .clear : AppColors.primary.opacity(0.3),
radius: 10,
y: 5
)
}
.disabled(viewModel.isLoading || viewModel.username.isEmpty || viewModel.password.isEmpty)
// Sign Up Link
HStack(spacing: AppSpacing.xs) {
Text("Don't have an account?")
.font(AppTypography.bodyMedium)
.foregroundColor(AppColors.textSecondary)
Button("Sign Up") {
showingRegister = true
}
.font(AppTypography.bodyMedium)
.fontWeight(.semibold)
.foregroundColor(AppColors.primary)
}
}
.padding(AppSpacing.xl)
.background(AppColors.surface)
.cornerRadius(AppRadius.xxl)
.shadow(color: .black.opacity(0.08), radius: 20, y: 10)
.padding(.horizontal, AppSpacing.lg)
Spacer()
}
}
.listRowBackground(Color.clear)
}
.navigationBarHidden(true)
.onChange(of: viewModel.isAuthenticated) { _, isAuth in
if isAuth {
print("isAuthenticated changed to true, isVerified = \(viewModel.isVerified)")
@@ -154,7 +232,6 @@ struct LoginView: View {
showVerification = true
}
} else {
// User logged out, dismiss main tab
print("isAuthenticated changed to false, dismissing main tab")
showMainTab = false
showVerification = false
@@ -174,11 +251,9 @@ struct LoginView: View {
.fullScreenCover(isPresented: $showVerification) {
VerifyEmailView(
onVerifySuccess: {
// After verification, show main tab view
viewModel.isVerified = true
},
onLogout: {
// Logout and dismiss verification screen
viewModel.logout()
showVerification = false
showMainTab = false
@@ -192,7 +267,6 @@ struct LoginView: View {
PasswordResetFlow(resetToken: resetToken)
}
.onChange(of: resetToken) { _, token in
// When deep link token arrives, show password reset
if token != nil {
showPasswordReset = true
}

View File

@@ -6,31 +6,41 @@ struct HomeNavigationCard: View {
let subtitle: String
var body: some View {
HStack(spacing: 16) {
Image(systemName: icon)
.font(.system(size: 36))
.foregroundColor(.blue)
.frame(width: 60)
HStack(spacing: AppSpacing.md) {
// Icon with gradient background
ZStack {
RoundedRectangle(cornerRadius: AppRadius.md)
.fill(AppColors.primaryGradient)
.frame(width: 60, height: 60)
.shadow(color: AppColors.primary.opacity(0.3), radius: 8, y: 4)
VStack(alignment: .leading, spacing: 4) {
Image(systemName: icon)
.font(.system(size: 28, weight: .semibold))
.foregroundColor(.white)
}
// Text Content
VStack(alignment: .leading, spacing: AppSpacing.xxs) {
Text(title)
.font(.title3)
.font(AppTypography.titleMedium)
.fontWeight(.semibold)
.foregroundColor(.primary)
.foregroundColor(AppColors.textPrimary)
Text(subtitle)
.font(.subheadline)
.foregroundColor(.secondary)
.font(AppTypography.bodySmall)
.foregroundColor(AppColors.textSecondary)
}
Spacer()
// Chevron
Image(systemName: "chevron.right")
.foregroundColor(.secondary)
.font(.system(size: 16, weight: .semibold))
.foregroundColor(AppColors.textTertiary)
}
.padding(20)
.background(Color(.systemBackground))
.cornerRadius(12)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2)
.padding(AppSpacing.lg)
.background(AppColors.surface)
.cornerRadius(AppRadius.lg)
.shadow(color: AppShadow.md.color, radius: AppShadow.md.radius, x: AppShadow.md.x, y: AppShadow.md.y)
}
}

View File

@@ -5,39 +5,61 @@ struct OverviewCard: View {
let summary: OverallSummary
var body: some View {
VStack(spacing: 16) {
VStack(spacing: AppSpacing.lg) {
// Header
HStack {
Image(systemName: "chart.bar.fill")
.font(.title3)
Text("Overview")
.font(.title2)
.fontWeight(.bold)
HStack(spacing: AppSpacing.sm) {
ZStack {
Circle()
.fill(AppColors.primaryGradient)
.frame(width: 44, height: 44)
Image(systemName: "chart.bar.fill")
.font(.system(size: 20, weight: .semibold))
.foregroundColor(.white)
}
Text("Overview")
.font(AppTypography.headlineSmall)
.foregroundColor(AppColors.textPrimary)
}
Spacer()
}
HStack(spacing: 40) {
// Stats Grid
HStack(spacing: AppSpacing.md) {
StatView(
icon: "house.fill",
value: "\(summary.totalResidences)",
label: "Properties"
label: "Properties",
color: AppColors.primary
)
Divider()
.frame(height: 60)
StatView(
icon: "list.bullet",
value: "\(summary.totalTasks)",
label: "Total Tasks"
label: "Total Tasks",
color: AppColors.info
)
Divider()
.frame(height: 60)
StatView(
icon: "clock.fill",
value: "\(summary.totalPending)",
label: "Pending"
label: "Pending",
color: AppColors.warning
)
}
}
.padding(20)
.background(Color.blue.opacity(0.1))
.cornerRadius(16)
.padding(.horizontal)
.padding(AppSpacing.xl)
.background(AppColors.surface)
.cornerRadius(AppRadius.xl)
.shadow(color: AppShadow.lg.color, radius: AppShadow.lg.radius, x: AppShadow.lg.x, y: AppShadow.lg.y)
.padding(.horizontal, AppSpacing.md)
}
}

View File

@@ -4,20 +4,30 @@ struct StatView: View {
let icon: String
let value: String
let label: String
var color: Color = AppColors.primary
var body: some View {
VStack(spacing: 8) {
Image(systemName: icon)
.font(.title2)
.foregroundColor(.blue)
VStack(spacing: AppSpacing.sm) {
ZStack {
Circle()
.fill(color.opacity(0.1))
.frame(width: 48, height: 48)
Image(systemName: icon)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(color)
}
Text(value)
.font(.title)
.font(AppTypography.headlineMedium)
.fontWeight(.bold)
.foregroundColor(AppColors.textPrimary)
Text(label)
.font(.caption)
.foregroundColor(.secondary)
.font(AppTypography.labelMedium)
.foregroundColor(AppColors.textSecondary)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity)
}
}