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:
205
iosApp/DESIGN_SYSTEM.md
Normal file
205
iosApp/DESIGN_SYSTEM.md
Normal 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
|
||||
225
iosApp/iosApp/Design/DesignSystem.swift
Normal file
225
iosApp/iosApp/Design/DesignSystem.swift
Normal 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
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user