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 { var body: some View {
NavigationView { NavigationView {
ZStack { ZStack {
Color(.systemGroupedBackground) AppColors.background
.ignoresSafeArea() .ignoresSafeArea()
if viewModel.isLoading { if viewModel.isLoading {
ProgressView() VStack(spacing: AppSpacing.lg) {
ProgressView()
.scaleEffect(1.2)
Text("Loading...")
.font(AppTypography.bodyMedium)
.foregroundColor(AppColors.textSecondary)
}
} else { } else {
ScrollView { ScrollView(showsIndicators: false) {
VStack(spacing: 20) { 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 // Overview Card
if let summary = viewModel.residenceSummary { if let summary = viewModel.residenceSummary {
OverviewCard(summary: summary.summary) OverviewCard(summary: summary.summary)
.transition(.scale.combined(with: .opacity))
} }
// Navigation Cards // Navigation Cards
VStack(spacing: 16) { VStack(spacing: AppSpacing.md) {
NavigationLink(destination: ResidencesListView()) { NavigationLink(destination: ResidencesListView()) {
HomeNavigationCard( HomeNavigationCard(
icon: "house.fill", icon: "house.fill",
@@ -30,6 +52,7 @@ struct HomeScreenView: View {
subtitle: "Manage your properties" subtitle: "Manage your properties"
) )
} }
.buttonStyle(PlainButtonStyle())
NavigationLink(destination: AllTasksView()) { NavigationLink(destination: AllTasksView()) {
HomeNavigationCard( HomeNavigationCard(
@@ -38,20 +61,26 @@ struct HomeScreenView: View {
subtitle: "View and manage all tasks" subtitle: "View and manage all tasks"
) )
} }
.buttonStyle(PlainButtonStyle())
} }
.padding(.horizontal) .padding(.horizontal, AppSpacing.md)
} }
.padding(.vertical) .padding(.vertical, AppSpacing.md)
} }
} }
} }
.navigationTitle("MyCrib") .navigationTitle("MyCrib")
.navigationBarTitleDisplayMode(.large)
.toolbar { .toolbar {
ToolbarItem(placement: .navigationBarTrailing) { ToolbarItem(placement: .navigationBarTrailing) {
Button(action: { Button(action: {
loginViewModel.logout() 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 { var body: some View {
NavigationView { NavigationView {
Form { ZStack {
Section { // Background gradient
VStack(spacing: 16) { AppColors.background
Image(systemName: "house.fill") .ignoresSafeArea()
.font(.system(size: 60))
.foregroundStyle(.blue.gradient)
Text("MyCrib") ScrollView {
.font(.largeTitle) VStack(spacing: AppSpacing.xl) {
.fontWeight(.bold) Spacer()
.frame(height: AppSpacing.xxxl)
Text("Manage your properties with ease") // Hero Section
.font(.subheadline) VStack(spacing: AppSpacing.lg) {
.foregroundColor(.secondary) // App Icon with gradient
} ZStack {
.frame(maxWidth: .infinity) Circle()
.padding(.vertical) .fill(AppColors.primaryGradient)
} .frame(width: 100, height: 100)
.listRowBackground(Color.clear) .shadow(color: AppColors.primary.opacity(0.3), radius: 20, y: 10)
Section { Image(systemName: "house.fill")
TextField("Username or Email", text: $viewModel.username) .font(.system(size: 50, weight: .semibold))
.textInputAutocapitalization(.never) .foregroundStyle(.white)
.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)
} }
Spacer()
}
}
.disabled(viewModel.isLoading)
}
Section { VStack(spacing: AppSpacing.xs) {
HStack { Text("Welcome Back")
Spacer() .font(AppTypography.displaySmall)
Button("Forgot Password?") { .foregroundColor(AppColors.textPrimary)
showPasswordReset = true
}
.font(.subheadline)
.fontWeight(.semibold)
Spacer()
}
}
.listRowBackground(Color.clear)
Section { Text("Sign in to manage your properties")
HStack { .font(AppTypography.bodyMedium)
Spacer() .foregroundColor(AppColors.textSecondary)
Text("Don't have an account?") }
.font(.subheadline)
.foregroundColor(.secondary)
Button("Sign Up") {
showingRegister = true
} }
.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() Spacer()
} }
} }
.listRowBackground(Color.clear)
} }
.navigationBarHidden(true)
.onChange(of: viewModel.isAuthenticated) { _, isAuth in .onChange(of: viewModel.isAuthenticated) { _, isAuth in
if isAuth { if isAuth {
print("isAuthenticated changed to true, isVerified = \(viewModel.isVerified)") print("isAuthenticated changed to true, isVerified = \(viewModel.isVerified)")
@@ -154,7 +232,6 @@ struct LoginView: View {
showVerification = true showVerification = true
} }
} else { } else {
// User logged out, dismiss main tab
print("isAuthenticated changed to false, dismissing main tab") print("isAuthenticated changed to false, dismissing main tab")
showMainTab = false showMainTab = false
showVerification = false showVerification = false
@@ -174,11 +251,9 @@ struct LoginView: View {
.fullScreenCover(isPresented: $showVerification) { .fullScreenCover(isPresented: $showVerification) {
VerifyEmailView( VerifyEmailView(
onVerifySuccess: { onVerifySuccess: {
// After verification, show main tab view
viewModel.isVerified = true viewModel.isVerified = true
}, },
onLogout: { onLogout: {
// Logout and dismiss verification screen
viewModel.logout() viewModel.logout()
showVerification = false showVerification = false
showMainTab = false showMainTab = false
@@ -192,7 +267,6 @@ struct LoginView: View {
PasswordResetFlow(resetToken: resetToken) PasswordResetFlow(resetToken: resetToken)
} }
.onChange(of: resetToken) { _, token in .onChange(of: resetToken) { _, token in
// When deep link token arrives, show password reset
if token != nil { if token != nil {
showPasswordReset = true showPasswordReset = true
} }

View File

@@ -6,31 +6,41 @@ struct HomeNavigationCard: View {
let subtitle: String let subtitle: String
var body: some View { var body: some View {
HStack(spacing: 16) { HStack(spacing: AppSpacing.md) {
Image(systemName: icon) // Icon with gradient background
.font(.system(size: 36)) ZStack {
.foregroundColor(.blue) RoundedRectangle(cornerRadius: AppRadius.md)
.frame(width: 60) .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) Text(title)
.font(.title3) .font(AppTypography.titleMedium)
.fontWeight(.semibold) .fontWeight(.semibold)
.foregroundColor(.primary) .foregroundColor(AppColors.textPrimary)
Text(subtitle) Text(subtitle)
.font(.subheadline) .font(AppTypography.bodySmall)
.foregroundColor(.secondary) .foregroundColor(AppColors.textSecondary)
} }
Spacer() Spacer()
// Chevron
Image(systemName: "chevron.right") Image(systemName: "chevron.right")
.foregroundColor(.secondary) .font(.system(size: 16, weight: .semibold))
.foregroundColor(AppColors.textTertiary)
} }
.padding(20) .padding(AppSpacing.lg)
.background(Color(.systemBackground)) .background(AppColors.surface)
.cornerRadius(12) .cornerRadius(AppRadius.lg)
.shadow(color: Color.black.opacity(0.05), radius: 5, x: 0, y: 2) .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 let summary: OverallSummary
var body: some View { var body: some View {
VStack(spacing: 16) { VStack(spacing: AppSpacing.lg) {
// Header
HStack { HStack {
Image(systemName: "chart.bar.fill") HStack(spacing: AppSpacing.sm) {
.font(.title3) ZStack {
Text("Overview") Circle()
.font(.title2) .fill(AppColors.primaryGradient)
.fontWeight(.bold) .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() Spacer()
} }
HStack(spacing: 40) { // Stats Grid
HStack(spacing: AppSpacing.md) {
StatView( StatView(
icon: "house.fill", icon: "house.fill",
value: "\(summary.totalResidences)", value: "\(summary.totalResidences)",
label: "Properties" label: "Properties",
color: AppColors.primary
) )
Divider()
.frame(height: 60)
StatView( StatView(
icon: "list.bullet", icon: "list.bullet",
value: "\(summary.totalTasks)", value: "\(summary.totalTasks)",
label: "Total Tasks" label: "Total Tasks",
color: AppColors.info
) )
Divider()
.frame(height: 60)
StatView( StatView(
icon: "clock.fill", icon: "clock.fill",
value: "\(summary.totalPending)", value: "\(summary.totalPending)",
label: "Pending" label: "Pending",
color: AppColors.warning
) )
} }
} }
.padding(20) .padding(AppSpacing.xl)
.background(Color.blue.opacity(0.1)) .background(AppColors.surface)
.cornerRadius(16) .cornerRadius(AppRadius.xl)
.padding(.horizontal) .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 icon: String
let value: String let value: String
let label: String let label: String
var color: Color = AppColors.primary
var body: some View { var body: some View {
VStack(spacing: 8) { VStack(spacing: AppSpacing.sm) {
Image(systemName: icon) ZStack {
.font(.title2) Circle()
.foregroundColor(.blue) .fill(color.opacity(0.1))
.frame(width: 48, height: 48)
Image(systemName: icon)
.font(.system(size: 22, weight: .semibold))
.foregroundColor(color)
}
Text(value) Text(value)
.font(.title) .font(AppTypography.headlineMedium)
.fontWeight(.bold) .fontWeight(.bold)
.foregroundColor(AppColors.textPrimary)
Text(label) Text(label)
.font(.caption) .font(AppTypography.labelMedium)
.foregroundColor(.secondary) .foregroundColor(AppColors.textSecondary)
.multilineTextAlignment(.center)
} }
.frame(maxWidth: .infinity)
} }
} }