android ui

This commit is contained in:
Trey t
2025-12-18 12:18:33 -06:00
parent b39d37a6e8
commit 59cbc60668
34 changed files with 6112 additions and 5767 deletions

View File

@@ -23,6 +23,7 @@ import com.example.casera.viewmodel.TaskCompletionViewModel
import com.example.casera.viewmodel.TaskViewModel
import com.example.casera.models.TaskDetail
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
@OptIn(ExperimentalMaterial3Api::class)
@Composable
@@ -105,7 +106,9 @@ fun AllTasksScreen(
}
}
WarmGradientBackground {
Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = {
TopAppBar(
title = {
@@ -135,7 +138,7 @@ fun AllTasksScreen(
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
containerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
}
@@ -156,46 +159,35 @@ fun AllTasksScreen(
) {
Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
modifier = Modifier.padding(OrganicSpacing.comfortable)
) {
Icon(
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
OrganicIconContainer(
icon = Icons.Default.Assignment,
size = 80.dp,
iconScale = 0.6f,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
iconColor = MaterialTheme.colorScheme.primary
)
Text(
"No tasks yet",
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
)
Text(
"Create your first task to get started",
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.textSecondary
)
Spacer(modifier = Modifier.height(8.dp))
Button(
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
OrganicPrimaryButton(
text = "Add Task",
onClick = { showNewTaskDialog = true },
modifier = Modifier
.fillMaxWidth(0.7f)
.height(56.dp),
modifier = Modifier.fillMaxWidth(0.7f),
enabled = myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isNotEmpty()
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Icon(Icons.Default.Add, contentDescription = null)
Text(
"Add Task",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
}
}
if (myResidencesState is ApiResult.Success &&
(myResidencesState as ApiResult.Success).data.residences.isEmpty()) {
Text(
@@ -266,6 +258,7 @@ fun AllTasksScreen(
}
}
}
}
if (showCompleteDialog && selectedTask != null) {
CompleteTaskDialog(

View File

@@ -9,7 +9,6 @@ import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -30,8 +29,7 @@ import com.example.casera.models.TaskCompletionCreateRequest
import com.example.casera.models.ContractorSummary
import com.example.casera.network.ApiResult
import com.example.casera.platform.*
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.ContractorViewModel
import org.jetbrains.compose.resources.stringResource
@@ -74,7 +72,9 @@ fun CompleteTaskScreen(
}
}
WarmGradientBackground {
Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = {
TopAppBar(
title = {
@@ -90,7 +90,7 @@ fun CompleteTaskScreen(
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
containerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
}
@@ -102,17 +102,13 @@ fun CompleteTaskScreen(
.verticalScroll(rememberScrollState())
) {
// Task Info Section
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
) {
Column(
modifier = Modifier.padding(AppSpacing.lg)
modifier = Modifier.padding(OrganicSpacing.lg)
) {
Text(
text = taskTitle,
@@ -120,7 +116,7 @@ fun CompleteTaskScreen(
fontWeight = FontWeight.SemiBold
)
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Row(
modifier = Modifier.fillMaxWidth(),
@@ -130,7 +126,7 @@ fun CompleteTaskScreen(
if (residenceName.isNotEmpty()) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Icon(
Icons.Default.Home,
@@ -155,31 +151,26 @@ fun CompleteTaskScreen(
subtitle = stringResource(Res.string.completions_contractor_helper)
)
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg)
.clickable { showContractorPicker = true },
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
.padding(horizontal = OrganicSpacing.lg)
.clickable { showContractorPicker = true }
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Icon(
Icons.Default.Build,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.Build,
size = 24.dp
)
Column {
Text(
@@ -204,7 +195,7 @@ fun CompleteTaskScreen(
}
}
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Completion Details Section
SectionHeader(
@@ -213,8 +204,8 @@ fun CompleteTaskScreen(
)
Column(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
// Completed By Name
OutlinedTextField(
@@ -226,7 +217,7 @@ fun CompleteTaskScreen(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
enabled = selectedContractor == null,
shape = RoundedCornerShape(AppRadius.md)
shape = OrganicShapes.medium
)
// Actual Cost
@@ -239,11 +230,11 @@ fun CompleteTaskScreen(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Decimal),
shape = RoundedCornerShape(AppRadius.md)
shape = OrganicShapes.medium
)
}
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Notes Section
SectionHeader(
@@ -257,12 +248,12 @@ fun CompleteTaskScreen(
placeholder = { Text(stringResource(Res.string.completions_notes_placeholder)) },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg)
.padding(horizontal = OrganicSpacing.lg)
.height(120.dp),
shape = RoundedCornerShape(AppRadius.md)
shape = OrganicShapes.medium
)
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Rating Section
SectionHeader(
@@ -270,19 +261,15 @@ fun CompleteTaskScreen(
subtitle = stringResource(Res.string.completions_rate_quality)
)
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
.padding(horizontal = OrganicSpacing.lg)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
@@ -291,7 +278,7 @@ fun CompleteTaskScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Row(
horizontalArrangement = Arrangement.Center,
@@ -325,7 +312,7 @@ fun CompleteTaskScreen(
}
}
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Photos Section
SectionHeader(
@@ -334,12 +321,12 @@ fun CompleteTaskScreen(
)
Column(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
OutlinedButton(
onClick = {
@@ -350,7 +337,7 @@ fun CompleteTaskScreen(
enabled = selectedImages.size < MAX_IMAGES
) {
Icon(Icons.Default.CameraAlt, null, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.completions_camera))
}
@@ -363,7 +350,7 @@ fun CompleteTaskScreen(
enabled = selectedImages.size < MAX_IMAGES
) {
Icon(Icons.Default.PhotoLibrary, null, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.completions_library))
}
}
@@ -373,7 +360,7 @@ fun CompleteTaskScreen(
modifier = Modifier
.fillMaxWidth()
.horizontalScroll(rememberScrollState()),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
selectedImages.forEachIndexed { index, imageData ->
ImageThumbnailCard(
@@ -390,10 +377,11 @@ fun CompleteTaskScreen(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Complete Button
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.completions_complete_button),
onClick = {
isSubmitting = true
val notesWithContractor = buildString {
@@ -424,28 +412,14 @@ fun CompleteTaskScreen(
},
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg)
.height(56.dp),
.padding(horizontal = OrganicSpacing.lg),
enabled = !isSubmitting,
shape = RoundedCornerShape(AppRadius.md)
) {
if (isSubmitting) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
isLoading = isSubmitting,
icon = Icons.Default.CheckCircle
)
} else {
Icon(Icons.Default.CheckCircle, null)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Text(
stringResource(Res.string.completions_complete_button),
fontWeight = FontWeight.SemiBold
)
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
}
}
@@ -475,7 +449,7 @@ private fun SectionHeader(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm)
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm)
) {
Text(
text = title,
@@ -503,7 +477,7 @@ private fun ImageThumbnailCard(
Box(
modifier = Modifier
.size(100.dp)
.clip(RoundedCornerShape(AppRadius.md))
.clip(OrganicShapes.medium)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
if (imageBitmap != null) {
@@ -530,7 +504,7 @@ private fun ImageThumbnailCard(
Box(
modifier = Modifier
.align(Alignment.TopEnd)
.padding(AppSpacing.xs)
.padding(OrganicSpacing.xs)
.size(24.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.error)
@@ -565,16 +539,16 @@ private fun ContractorPickerSheet(
Column(
modifier = Modifier
.fillMaxWidth()
.padding(bottom = AppSpacing.xl)
.padding(bottom = OrganicSpacing.xl)
) {
Text(
text = stringResource(Res.string.completions_select_contractor),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md)
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
)
HorizontalDivider()
OrganicDivider()
// None option
ListItem(
@@ -592,13 +566,13 @@ private fun ContractorPickerSheet(
modifier = Modifier.clickable { onSelect(null) }
)
HorizontalDivider()
OrganicDivider()
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.xl),
.padding(OrganicSpacing.xl),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()

View File

@@ -1,19 +1,15 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalUriHandler
@@ -31,6 +27,7 @@ import com.example.casera.network.ApiResult
import com.example.casera.platform.rememberShareContractor
import com.example.casera.utils.SubscriptionHelper
import com.example.casera.ui.subscription.UpgradePromptDialog
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -123,16 +120,16 @@ fun ContractorDetailScreen(
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = Color(0xFFF9FAFB)
containerColor = MaterialTheme.colorScheme.surface
)
)
}
) { padding ->
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(Color(0xFFF9FAFB))
) {
val uriHandler = LocalUriHandler.current
val residences = DataManager.residences.value
@@ -147,40 +144,28 @@ fun ContractorDetailScreen(
) { contractor ->
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
contentPadding = PaddingValues(OrganicSpacing.medium),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) {
// Header Card
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
.padding(OrganicSpacing.large),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Avatar
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.Person,
size = 80.dp,
iconSize = 48.dp,
containerColor = MaterialTheme.colorScheme.primaryContainer,
iconTint = MaterialTheme.colorScheme.primary
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.medium))
Text(
text = contractor.name,
@@ -198,18 +183,18 @@ fun ContractorDetailScreen(
}
if (contractor.specialties.isNotEmpty()) {
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.medium))
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small, Alignment.CenterHorizontally),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
) {
contractor.specialties.forEach { specialty ->
Surface(
shape = RoundedCornerShape(20.dp),
shape = OrganicShapes.large,
color = MaterialTheme.colorScheme.primaryContainer
) {
Row(
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -218,7 +203,7 @@ fun ContractorDetailScreen(
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text(
text = specialty.name,
style = MaterialTheme.typography.bodyMedium,
@@ -232,7 +217,7 @@ fun ContractorDetailScreen(
}
if (contractor.rating != null && contractor.rating > 0) {
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.medium))
Row(verticalAlignment = Alignment.CenterVertically) {
repeat(5) { index ->
Icon(
@@ -242,7 +227,7 @@ fun ContractorDetailScreen(
tint = Color(0xFFF59E0B)
)
}
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.small))
Text(
text = ((contractor.rating * 10).toInt() / 10.0).toString(),
style = MaterialTheme.typography.titleMedium,
@@ -253,7 +238,7 @@ fun ContractorDetailScreen(
}
if (contractor.taskCount > 0) {
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.extraSmall))
Text(
text = stringResource(Res.string.contractors_completed_tasks, contractor.taskCount),
style = MaterialTheme.typography.bodySmall,
@@ -269,7 +254,7 @@ fun ContractorDetailScreen(
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) {
contractor.phone?.let { phone ->
QuickActionButton(
@@ -388,7 +373,7 @@ fun ContractorDetailScreen(
text = stringResource(Res.string.contractors_no_contact_info),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(16.dp)
modifier = Modifier.padding(OrganicSpacing.medium)
)
}
}
@@ -459,8 +444,8 @@ fun ContractorDetailScreen(
item {
DetailSection(title = stringResource(Res.string.contractors_notes)) {
Row(
modifier = Modifier.padding(horizontal = 16.dp, vertical = 12.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) {
Icon(
Icons.Default.Notes,
@@ -484,7 +469,7 @@ fun ContractorDetailScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(OrganicSpacing.medium),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StatCard(
@@ -516,7 +501,7 @@ fun ContractorDetailScreen(
value = createdBy.username,
iconTint = MaterialTheme.colorScheme.onSurfaceVariant
)
HorizontalDivider(modifier = Modifier.padding(horizontal = 16.dp))
OrganicDivider(modifier = Modifier.padding(horizontal = OrganicSpacing.medium))
}
DetailRow(
@@ -531,6 +516,7 @@ fun ContractorDetailScreen(
}
}
}
}
if (showEditDialog) {
AddContractorDialog(
@@ -565,8 +551,8 @@ fun ContractorDetailScreen(
Text(stringResource(Res.string.common_cancel))
}
},
containerColor = Color.White,
shape = RoundedCornerShape(16.dp)
containerColor = MaterialTheme.colorScheme.surface,
shape = OrganicShapes.large
)
}
@@ -591,19 +577,14 @@ fun DetailSection(
title: String,
content: @Composable ColumnScope.() -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(modifier = Modifier.fillMaxWidth()) {
Text(
text = title,
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.padding(16.dp).padding(bottom = 0.dp)
modifier = Modifier.padding(OrganicSpacing.medium).padding(bottom = 0.dp)
)
content()
}
@@ -620,7 +601,7 @@ fun DetailRow(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
verticalAlignment = Alignment.Top
) {
Icon(
@@ -629,7 +610,7 @@ fun DetailRow(
modifier = Modifier.size(20.dp),
tint = iconTint
)
Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.medium))
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
@@ -658,7 +639,7 @@ fun ClickableDetailRow(
modifier = Modifier
.fillMaxWidth()
.clickable(onClick = onClick)
.padding(horizontal = 16.dp, vertical = 12.dp),
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.medium),
verticalAlignment = Alignment.Top
) {
Icon(
@@ -667,7 +648,7 @@ fun ClickableDetailRow(
modifier = Modifier.size(20.dp),
tint = iconTint
)
Spacer(modifier = Modifier.width(12.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.medium))
Column(modifier = Modifier.weight(1f)) {
Text(
text = label,
@@ -698,33 +679,23 @@ fun QuickActionButton(
modifier: Modifier = Modifier,
onClick: () -> Unit
) {
Card(
modifier = modifier.clickable(onClick = onClick),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surface),
elevation = CardDefaults.cardElevation(defaultElevation = 1.dp)
OrganicCard(
modifier = modifier.clickable(onClick = onClick)
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 12.dp),
.padding(vertical = OrganicSpacing.medium),
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(22.dp),
tint = color
OrganicIconContainer(
icon = icon,
size = 44.dp,
iconSize = 22.dp,
containerColor = color.copy(alpha = 0.1f),
iconTint = color
)
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.small))
Text(
text = label,
style = MaterialTheme.typography.labelMedium,
@@ -745,21 +716,14 @@ fun StatCard(
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = null,
modifier = Modifier.size(22.dp),
tint = color
OrganicIconContainer(
icon = icon,
size = 44.dp,
iconSize = 22.dp,
containerColor = color.copy(alpha = 0.1f),
iconTint = color
)
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.small))
Text(
text = value,
style = MaterialTheme.typography.titleLarge,

View File

@@ -1,12 +1,9 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@@ -14,8 +11,6 @@ import androidx.compose.material3.pulltorefresh.PullToRefreshBox
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
@@ -31,6 +26,7 @@ import com.example.casera.ui.subscription.UpgradeFeatureScreen
import com.example.casera.utils.SubscriptionHelper
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -215,11 +211,11 @@ fun ContractorsScreen(
)
}
} else {
WarmGradientBackground {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.background(MaterialTheme.colorScheme.background)
) {
// Search bar
OutlinedTextField(
@@ -227,7 +223,7 @@ fun ContractorsScreen(
onValueChange = { searchQuery = it },
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 8.dp),
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.small),
placeholder = { Text(stringResource(Res.string.contractors_search)) },
leadingIcon = { Icon(Icons.Default.Search, stringResource(Res.string.common_search)) },
trailingIcon = {
@@ -238,7 +234,7 @@ fun ContractorsScreen(
}
},
singleLine = true,
shape = RoundedCornerShape(12.dp),
shape = OrganicShapes.medium,
colors = OutlinedTextFieldDefaults.colors(
focusedContainerColor = MaterialTheme.colorScheme.surface,
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
@@ -252,8 +248,8 @@ fun ContractorsScreen(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 16.dp, vertical = 4.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp)
.padding(horizontal = OrganicSpacing.medium, vertical = OrganicSpacing.extraSmall),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
) {
if (showFavoritesOnly) {
FilterChip(
@@ -288,33 +284,37 @@ fun ContractorsScreen(
) { _ ->
// Use filteredContractors for client-side filtering
if (filteredContractors.isEmpty()) {
// Empty state
// Empty state with organic styling
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.small)
) {
Icon(
Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
OrganicIconContainer(
icon = Icons.Default.PersonAdd,
size = 80.dp,
iconSize = 40.dp,
containerColor = MaterialTheme.colorScheme.primaryContainer,
iconTint = MaterialTheme.colorScheme.onPrimaryContainer
)
Spacer(modifier = Modifier.height(OrganicSpacing.small))
Text(
if (searchQuery.isNotEmpty() || selectedFilter != null || showFavoritesOnly)
stringResource(Res.string.contractors_no_results)
else
stringResource(Res.string.contractors_empty_title),
color = MaterialTheme.colorScheme.onSurfaceVariant
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.onSurface
)
if (searchQuery.isEmpty() && selectedFilter == null && !showFavoritesOnly) {
Text(
stringResource(Res.string.contractors_empty_subtitle_first),
color = MaterialTheme.colorScheme.onSurfaceVariant,
style = MaterialTheme.typography.bodySmall
style = MaterialTheme.typography.bodyMedium
)
}
}
@@ -330,8 +330,8 @@ fun ContractorsScreen(
) {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
contentPadding = PaddingValues(OrganicSpacing.medium),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.medium)
) {
items(filteredContractors, key = { it.id }) { contractor ->
ContractorCard(
@@ -347,6 +347,7 @@ fun ContractorsScreen(
}
}
}
}
if (showAddDialog) {
AddContractorDialog(
@@ -381,41 +382,27 @@ fun ContractorCard(
onToggleFavorite: (Int) -> Unit,
onClick: (Int) -> Unit
) {
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick(contractor.id) },
shape = RoundedCornerShape(16.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(
defaultElevation = 1.dp
)
.clickable { onClick(contractor.id) }
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(OrganicSpacing.medium),
verticalAlignment = Alignment.CenterVertically
) {
// Avatar/Icon
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primaryContainer),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Person,
contentDescription = null,
modifier = Modifier.size(32.dp),
tint = MaterialTheme.colorScheme.onPrimaryContainer
OrganicIconContainer(
icon = Icons.Default.Person,
size = 56.dp,
iconSize = 32.dp,
containerColor = MaterialTheme.colorScheme.primaryContainer,
iconTint = MaterialTheme.colorScheme.onPrimaryContainer
)
}
Spacer(modifier = Modifier.width(16.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.medium))
Column(modifier = Modifier.weight(1f)) {
Row(verticalAlignment = Alignment.CenterVertically) {
@@ -428,7 +415,7 @@ fun ContractorCard(
overflow = TextOverflow.Ellipsis
)
if (contractor.isFavorite) {
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Icon(
Icons.Default.Star,
contentDescription = stringResource(Res.string.contractors_favorite),
@@ -448,10 +435,10 @@ fun ContractorCard(
)
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.small))
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.medium),
verticalAlignment = Alignment.CenterVertically
) {
if (contractor.specialties.isNotEmpty()) {
@@ -462,7 +449,7 @@ fun ContractorCard(
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text(
text = contractor.specialties.first().name,
style = MaterialTheme.typography.bodySmall,
@@ -479,7 +466,7 @@ fun ContractorCard(
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.tertiary
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text(
text = "${(contractor.rating * 10).toInt() / 10.0}",
style = MaterialTheme.typography.bodySmall,
@@ -497,7 +484,7 @@ fun ContractorCard(
modifier = Modifier.size(14.dp),
tint = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.width(4.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.extraSmall))
Text(
text = stringResource(Res.string.contractors_tasks_count, contractor.taskCount),
style = MaterialTheme.typography.bodySmall,
@@ -528,4 +515,3 @@ fun ContractorCard(
}
}
}

View File

@@ -36,6 +36,7 @@ import coil3.compose.SubcomposeAsyncImageContent
import coil3.compose.AsyncImagePainter
import com.example.casera.ui.components.AuthenticatedImage
import com.example.casera.util.DateUtils
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -96,6 +97,7 @@ fun DocumentDetailScreen(
)
}
) { padding ->
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
@@ -110,8 +112,8 @@ fun DocumentDetailScreen(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(start = OrganicSpacing.lg, end = OrganicSpacing.lg, top = OrganicSpacing.lg, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
// Status badge (for warranties)
if (document.documentType == "warranty") {
@@ -124,16 +126,14 @@ fun DocumentDetailScreen(
else -> Color(0xFF10B981)
}
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = statusColor.copy(alpha = 0.1f)
)
accentColor = statusColor.copy(alpha = 0.1f)
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -175,17 +175,17 @@ fun DocumentDetailScreen(
}
// Basic Information
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_basic_info),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
DetailRow(stringResource(Res.string.documents_title_label), document.title)
DetailRow(stringResource(Res.string.documents_type_label), DocumentType.fromValue(document.documentType).displayName)
@@ -202,17 +202,17 @@ fun DocumentDetailScreen(
if (document.documentType == "warranty" &&
(document.itemName != null || document.modelNumber != null ||
document.serialNumber != null || document.provider != null)) {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_item_details),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.itemName?.let { DetailRow(stringResource(Res.string.documents_item_name), it) }
document.modelNumber?.let { DetailRow(stringResource(Res.string.documents_model_number), it) }
@@ -227,17 +227,17 @@ fun DocumentDetailScreen(
if (document.documentType == "warranty" &&
(document.claimPhone != null || document.claimEmail != null ||
document.claimWebsite != null)) {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_claim_info),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.claimPhone?.let { DetailRow(stringResource(Res.string.documents_claim_phone), it) }
document.claimEmail?.let { DetailRow(stringResource(Res.string.documents_claim_email), it) }
@@ -249,17 +249,17 @@ fun DocumentDetailScreen(
// Dates
if (document.purchaseDate != null || document.startDate != null ||
document.endDate != null) {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_important_dates),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.purchaseDate?.let { DetailRow(stringResource(Res.string.documents_purchase_date), DateUtils.formatDateMedium(it)) }
document.startDate?.let { DetailRow(stringResource(Res.string.documents_start_date), DateUtils.formatDateMedium(it)) }
@@ -269,17 +269,17 @@ fun DocumentDetailScreen(
}
// Residence & Contractor
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_associations),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.residenceAddress?.let { DetailRow(stringResource(Res.string.documents_residence), it) }
document.contractorName?.let { DetailRow(stringResource(Res.string.documents_contractor), it) }
@@ -289,17 +289,17 @@ fun DocumentDetailScreen(
// Additional Information
if (document.tags != null || document.notes != null) {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_additional_info),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.tags?.let { DetailRow(stringResource(Res.string.documents_tags), it) }
document.notes?.let { DetailRow(stringResource(Res.string.documents_notes), it) }
@@ -309,22 +309,22 @@ fun DocumentDetailScreen(
// Images
if (document.images.isNotEmpty()) {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_images, document.images.size),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
// Image grid
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
document.images.take(4).forEachIndexed { index, image ->
Box(
@@ -366,17 +366,17 @@ fun DocumentDetailScreen(
// File Information
if (document.fileUrl != null) {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_attached_file),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.fileType?.let { DetailRow(stringResource(Res.string.documents_file_type), it) }
document.fileSize?.let {
@@ -388,7 +388,7 @@ fun DocumentDetailScreen(
modifier = Modifier.fillMaxWidth()
) {
Icon(Icons.Default.Download, null)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.documents_download_file))
}
}
@@ -396,17 +396,17 @@ fun DocumentDetailScreen(
}
// Metadata
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
stringResource(Res.string.documents_metadata),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Divider()
OrganicDivider()
document.uploadedByUsername?.let { DetailRow(stringResource(Res.string.documents_uploaded_by), it) }
document.createdAt?.let { DetailRow(stringResource(Res.string.documents_created), DateUtils.formatDateMedium(it)) }
@@ -417,6 +417,7 @@ fun DocumentDetailScreen(
}
}
}
}
// Delete confirmation dialog
if (showDeleteDialog) {
@@ -463,7 +464,7 @@ fun DetailRow(label: String, value: String) {
style = MaterialTheme.typography.labelMedium,
color = Color.Gray
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.xs))
Text(
value,
style = MaterialTheme.typography.bodyLarge
@@ -498,7 +499,7 @@ fun DocumentImageViewer(
modifier = Modifier
.fillMaxWidth(0.95f)
.fillMaxHeight(0.9f),
shape = RoundedCornerShape(16.dp),
shape = RoundedCornerShape(OrganicSpacing.lg),
color = MaterialTheme.colorScheme.background
) {
Column(
@@ -508,7 +509,7 @@ fun DocumentImageViewer(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -531,7 +532,7 @@ fun DocumentImageViewer(
}
}
HorizontalDivider()
OrganicDivider()
// Content
if (showFullImage) {
@@ -539,7 +540,7 @@ fun DocumentImageViewer(
Column(
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
.padding(OrganicSpacing.lg),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -553,16 +554,13 @@ fun DocumentImageViewer(
)
images[selectedIndex].caption?.let { caption ->
Spacer(modifier = Modifier.height(16.dp))
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
),
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Text(
text = caption,
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(OrganicSpacing.lg),
style = MaterialTheme.typography.bodyMedium
)
}
@@ -570,7 +568,7 @@ fun DocumentImageViewer(
// Navigation buttons
if (images.size > 1) {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween
@@ -580,7 +578,7 @@ fun DocumentImageViewer(
enabled = selectedIndex > 0
) {
Icon(Icons.Default.ArrowBack, "Previous")
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text("Previous")
}
Button(
@@ -588,7 +586,7 @@ fun DocumentImageViewer(
enabled = selectedIndex < images.size - 1
) {
Text("Next")
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Icon(Icons.Default.ArrowForward, "Next")
}
}
@@ -600,19 +598,19 @@ fun DocumentImageViewer(
columns = GridCells.Fixed(2),
modifier = Modifier
.fillMaxSize()
.padding(16.dp),
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalArrangement = Arrangement.spacedBy(12.dp)
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
items(images.size) { index ->
val image = images[index]
Card(
onClick = {
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
selectedIndex = index
showFullImage = true
},
shape = RoundedCornerShape(12.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
}
) {
Column {
AuthenticatedImage(
@@ -627,7 +625,7 @@ fun DocumentImageViewer(
image.caption?.let { caption ->
Text(
text = caption,
modifier = Modifier.padding(8.dp),
modifier = Modifier.padding(OrganicSpacing.sm),
style = MaterialTheme.typography.bodySmall,
maxLines = 2
)

View File

@@ -28,6 +28,7 @@ import com.example.casera.platform.rememberImagePicker
import com.example.casera.platform.rememberCameraPicker
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -201,13 +202,14 @@ fun DocumentFormScreen(
)
}
) { padding ->
WarmGradientBackground {
Column(
modifier = Modifier
.fillMaxSize()
.padding(padding)
.verticalScroll(rememberScrollState())
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(start = OrganicSpacing.cozy, end = OrganicSpacing.cozy, top = OrganicSpacing.cozy, bottom = 96.dp),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
// Loading state for edit mode
if (isEditMode && documentDetailState is ApiResult.Loading) {
@@ -497,14 +499,12 @@ fun DocumentFormScreen(
// Existing images (edit mode only)
if (isEditMode && existingImages.isNotEmpty()) {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
showBlob = false
) {
Column(
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
@@ -529,14 +529,12 @@ fun DocumentFormScreen(
}
// Image upload section
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
showBlob = false
) {
Column(
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
Text(
@@ -549,7 +547,7 @@ fun DocumentFormScreen(
)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Button(
onClick = { cameraPicker() },
@@ -575,7 +573,7 @@ fun DocumentFormScreen(
// Display selected images
if (selectedImages.isNotEmpty()) {
Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
selectedImages.forEachIndexed { index, image ->
Row(
@@ -584,7 +582,7 @@ fun DocumentFormScreen(
verticalAlignment = Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -617,16 +615,14 @@ fun DocumentFormScreen(
// Error message
if (operationState is ApiResult.Error) {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
)
showBlob = false
) {
Text(
com.example.casera.util.ErrorMessageParser.parse((operationState as ApiResult.Error).message),
modifier = Modifier.padding(12.dp),
color = MaterialTheme.colorScheme.onErrorContainer
color = MaterialTheme.colorScheme.error
)
}
}
@@ -638,7 +634,13 @@ fun DocumentFormScreen(
val providerRequiredError = stringResource(Res.string.documents_form_provider_error)
// Save Button
Button(
OrganicPrimaryButton(
text = when {
isEditMode && isWarranty -> stringResource(Res.string.documents_form_update_warranty)
isEditMode -> stringResource(Res.string.documents_form_update_document)
isWarranty -> stringResource(Res.string.documents_form_add_warranty)
else -> stringResource(Res.string.documents_form_add_document)
},
onClick = {
// Validate
var hasError = false
@@ -723,24 +725,9 @@ fun DocumentFormScreen(
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = operationState !is ApiResult.Loading
) {
if (operationState is ApiResult.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
enabled = operationState !is ApiResult.Loading,
isLoading = operationState is ApiResult.Loading
)
} else {
Text(
when {
isEditMode && isWarranty -> stringResource(Res.string.documents_form_update_warranty)
isEditMode -> stringResource(Res.string.documents_form_update_document)
isWarranty -> stringResource(Res.string.documents_form_add_warranty)
else -> stringResource(Res.string.documents_form_add_document)
}
)
}
}
}
}

View File

@@ -18,6 +18,7 @@ import com.example.casera.viewmodel.DocumentViewModel
import com.example.casera.models.*
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -120,7 +121,7 @@ fun DocumentsScreen(
showFiltersMenu = false
}
)
Divider()
OrganicDivider()
DocumentCategory.values().forEach { category ->
DropdownMenuItem(
text = { Text(category.displayName) },
@@ -138,7 +139,7 @@ fun DocumentsScreen(
showFiltersMenu = false
}
)
Divider()
OrganicDivider()
DocumentType.values().forEach { type ->
DropdownMenuItem(
text = { Text(type.displayName) },
@@ -199,6 +200,7 @@ fun DocumentsScreen(
}
}
) { padding ->
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
@@ -227,6 +229,7 @@ fun DocumentsScreen(
}
}
}
}
// Show upgrade dialog when user hits limit
if (showUpgradeDialog) {

View File

@@ -17,6 +17,7 @@ import com.example.casera.viewmodel.ResidenceViewModel
import com.example.casera.repository.LookupsRepository
import com.example.casera.models.*
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -103,13 +104,14 @@ fun EditTaskScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.padding(OrganicSpacing.cozy)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
// Required fields section
Text(
@@ -298,7 +300,8 @@ fun EditTaskScreen(
}
// Submit button
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.tasks_update),
onClick = {
if (validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null) {
@@ -321,21 +324,13 @@ fun EditTaskScreen(
)
}
},
modifier = Modifier.fillMaxWidth(),
enabled = validateForm() && selectedCategory != null &&
selectedFrequency != null && selectedPriority != null
) {
if (updateTaskState is ApiResult.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
selectedFrequency != null && selectedPriority != null,
isLoading = updateTaskState is ApiResult.Loading
)
} else {
Text(stringResource(Res.string.tasks_update))
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
}
}
}
}

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@@ -18,6 +16,7 @@ import com.example.casera.ui.components.auth.AuthHeader
import com.example.casera.ui.components.common.ErrorCard
import com.example.casera.viewmodel.PasswordResetViewModel
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -74,37 +73,51 @@ fun ForgotPasswordScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
showBlob = true,
blobVariation = 0
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
.padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
AuthHeader(
OrganicIconContainer(
icon = Icons.Default.Key,
title = stringResource(Res.string.auth_forgot_title),
subtitle = stringResource(Res.string.auth_forgot_subtitle)
size = 80.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.height(8.dp))
Text(
text = stringResource(Res.string.auth_forgot_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Text(
text = stringResource(Res.string.auth_forgot_subtitle),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
OutlinedTextField(
value = email,
@@ -118,74 +131,58 @@ fun ForgotPasswordScreen(
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp),
enabled = !isLoading
)
Text(
"We'll send a 6-digit verification code to this address",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
ErrorCard(message = errorMessage)
if (isSuccess) {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
accentColor = MaterialTheme.colorScheme.primary,
showBlob = false
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
.padding(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(12.dp))
Text(
"Check your email for a 6-digit verification code",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
color = MaterialTheme.colorScheme.textPrimary
)
}
}
}
Button(
OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = stringResource(Res.string.auth_forgot_button),
onClick = {
viewModel.setEmail(email)
viewModel.requestPasswordReset(email)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = email.isNotEmpty() && !isLoading,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
isLoading = isLoading
)
} else {
Icon(Icons.Default.Send, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
stringResource(Res.string.auth_forgot_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
TextButton(
onClick = onNavigateBack,
@@ -200,4 +197,5 @@ fun ForgotPasswordScreen(
}
}
}
}
}

View File

@@ -1,22 +1,18 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.components.HandleErrors
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.ResidenceViewModel
import com.example.casera.viewmodel.TaskViewModel
import com.example.casera.network.ApiResult
@@ -67,17 +63,17 @@ fun HomeScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues)
.padding(horizontal = 16.dp, vertical = 12.dp),
verticalArrangement = Arrangement.spacedBy(20.dp)
.padding(horizontal = OrganicSpacing.comfortable, vertical = OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.generous)
) {
// Personalized Greeting
Column(
modifier = Modifier.padding(vertical = 8.dp)
modifier = Modifier.padding(vertical = OrganicSpacing.cozy)
) {
Text(
text = stringResource(Res.string.home_welcome),
@@ -94,45 +90,23 @@ fun HomeScreen(
when (summaryState) {
is ApiResult.Success -> {
val summary = (summaryState as ApiResult.Success).data
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
showBlob = true,
blobVariation = 0
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
modifier = Modifier.fillMaxWidth()
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(12.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
// Gradient circular icon
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(
listOf(
Color(0xFF2563EB),
Color(0xFF8B5CF6)
OrganicIconContainer(
icon = Icons.Default.Home,
size = 44.dp
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
Icons.Default.Home,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
)
}
Column {
Text(
text = stringResource(Res.string.home_overview),
@@ -147,35 +121,38 @@ fun HomeScreen(
}
}
Spacer(modifier = Modifier.height(20.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.generous))
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
) {
StatItem(
OrganicStatPill(
icon = Icons.Default.Home,
value = "${summary.residences.size}",
label = stringResource(Res.string.home_properties),
color = Color(0xFF3B82F6)
)
Divider(
OrganicDivider(
modifier = Modifier
.height(48.dp)
.width(1.dp),
color = MaterialTheme.colorScheme.outlineVariant
vertical = true
)
StatItem(
OrganicStatPill(
icon = Icons.Default.Task,
value = "${totalSummary?.totalTasks ?: 0}",
label = stringResource(Res.string.home_total_tasks),
color = Color(0xFF8B5CF6)
)
Divider(
OrganicDivider(
modifier = Modifier
.height(48.dp)
.width(1.dp),
color = MaterialTheme.colorScheme.outlineVariant
vertical = true
)
StatItem(
OrganicStatPill(
icon = Icons.Default.Schedule,
value = "${totalSummary?.totalPending ?: 0}",
label = stringResource(Res.string.home_pending),
color = Color(0xFFF59E0B)
@@ -185,7 +162,7 @@ fun HomeScreen(
}
}
is ApiResult.Idle, is ApiResult.Loading -> {
Card(modifier = Modifier.fillMaxWidth()) {
OrganicCard(modifier = Modifier.fillMaxWidth()) {
Box(
modifier = Modifier
.fillMaxWidth()
@@ -222,36 +199,6 @@ fun HomeScreen(
)
}
}
}
@Composable
private fun StatItem(
value: String,
label: String,
color: Color
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Box(
modifier = Modifier
.size(36.dp)
.clip(CircleShape)
.background(color.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Text(
text = value,
style = MaterialTheme.typography.titleLarge,
color = color
)
}
Text(
text = label,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
@@ -263,45 +210,24 @@ private fun NavigationCard(
iconColor: Color,
onClick: () -> Unit
) {
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = MaterialTheme.shapes.large,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp)
showBlob = true,
blobVariation = 1
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(16.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.comfortable)
) {
// Gradient circular icon
Box(
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(
listOf(
iconColor,
iconColor.copy(alpha = 0.7f)
OrganicIconContainer(
icon = icon,
size = 56.dp,
iconColor = iconColor
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(28.dp)
)
}
Column(
modifier = Modifier.weight(1f)
@@ -311,7 +237,7 @@ private fun NavigationCard(
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface
)
Spacer(modifier = Modifier.height(4.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.minimal))
Text(
text = subtitle,
style = MaterialTheme.typography.bodyMedium,

View File

@@ -29,6 +29,7 @@ import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -93,28 +94,25 @@ fun LoginScreen(
val isLoading = loginState is ApiResult.Loading || googleSignInState is ApiResult.Loading
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background),
.fillMaxSize(),
contentAlignment = Alignment.Center
) {
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = MaterialTheme.shapes.extraLarge,
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
showBlob = true,
blobVariation = 0
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
.padding(OrganicSpacing.xxl),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
AuthHeader(
icon = Icons.Default.Home,
@@ -122,7 +120,7 @@ fun LoginScreen(
subtitle = stringResource(Res.string.app_tagline)
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
OutlinedTextField(
value = username,
@@ -133,7 +131,7 @@ fun LoginScreen(
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp),
shape = RoundedCornerShape(OrganicRadius.md),
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Email,
imeAction = ImeAction.Next
@@ -158,7 +156,7 @@ fun LoginScreen(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
shape = RoundedCornerShape(OrganicRadius.md)
)
ErrorCard(message = errorMessage)
@@ -168,75 +166,33 @@ fun LoginScreen(
googleSignInError = null
}
// Gradient button
Box(
modifier = Modifier
.fillMaxWidth()
.height(56.dp)
.clip(MaterialTheme.shapes.medium)
.then(
if (username.isNotEmpty() && password.isNotEmpty() && !isLoading) {
Modifier.background(
Brush.linearGradient(
listOf(
Color(0xFF2563EB),
Color(0xFF8B5CF6)
)
)
)
} else {
Modifier.background(MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f))
}
),
contentAlignment = Alignment.Center
) {
Button(
// Organic Primary Button
OrganicPrimaryButton(
text = stringResource(Res.string.auth_login_button),
onClick = {
viewModel.login(username, password)
},
modifier = Modifier.fillMaxSize(),
modifier = Modifier.fillMaxWidth(),
enabled = username.isNotEmpty() && password.isNotEmpty(),
shape = MaterialTheme.shapes.medium,
colors = ButtonDefaults.buttonColors(
containerColor = Color.Transparent,
disabledContainerColor = Color.Transparent
isLoading = isLoading
)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = Color.White,
strokeWidth = 2.dp
)
} else {
Text(
stringResource(Res.string.auth_login_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
color = Color.White
)
}
}
}
// Divider with "or"
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.weight(1f)
)
Text(
text = "or",
modifier = Modifier.padding(horizontal = 16.dp),
modifier = Modifier.padding(horizontal = OrganicSpacing.lg),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
HorizontalDivider(
modifier = Modifier.weight(1f),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.weight(1f)
)
}
@@ -277,4 +233,5 @@ fun LoginScreen(
}
}
}
}
}

View File

@@ -17,6 +17,7 @@ import com.example.casera.models.Residence
import com.example.casera.models.TaskDetail
import com.example.casera.storage.TokenStorage
import com.example.casera.ui.subscription.UpgradeScreen
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import kotlinx.serialization.json.Json
import org.jetbrains.compose.resources.stringResource
@@ -45,7 +46,9 @@ fun MainScreen(
}
}
WarmGradientBackground {
Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
bottomBar = {
NavigationBar(
containerColor = MaterialTheme.colorScheme.surfaceContainer,
@@ -123,24 +126,6 @@ fun MainScreen(
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
)
)
// NavigationBarItem(
// icon = { Icon(Icons.Default.Person, contentDescription = "Profile") },
// label = { Text("Profile") },
// selected = selectedTab == 4,
// onClick = {
// selectedTab = 4
// navController.navigate(MainTabProfileRoute) {
// popUpTo(MainTabResidencesRoute) { inclusive = false }
// }
// },
// colors = NavigationBarItemDefaults.colors(
// selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
// selectedTextColor = MaterialTheme.colorScheme.onSecondaryContainer,
// indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
// unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
// unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
// )
// )
}
}
) { paddingValues ->
@@ -346,4 +331,5 @@ fun MainScreen(
}
}
}
}
}

View File

@@ -3,7 +3,6 @@ package com.example.casera.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@@ -22,8 +21,7 @@ import com.example.casera.models.ResidenceShareCode
import com.example.casera.network.ApiResult
import com.example.casera.network.ResidenceApi
import com.example.casera.storage.TokenStorage
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@@ -68,7 +66,9 @@ fun ManageUsersScreen(
}
}
WarmGradientBackground {
Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = {
TopAppBar(
title = {
@@ -90,7 +90,7 @@ fun ManageUsersScreen(
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
containerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
},
@@ -114,7 +114,7 @@ fun ManageUsersScreen(
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Icon(
Icons.Default.Error,
@@ -133,26 +133,23 @@ fun ManageUsersScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
contentPadding = PaddingValues(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
// Share sections (primary owner only)
if (isPrimaryOwner) {
// Easy Share Section
item {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.lg),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
accentColor = MaterialTheme.colorScheme.primaryContainer
) {
Column(
modifier = Modifier.padding(AppSpacing.lg)
modifier = Modifier.padding(OrganicSpacing.lg)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Icon(
Icons.Default.Share,
@@ -167,24 +164,16 @@ fun ManageUsersScreen(
)
}
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.manage_users_send_invite),
onClick = onSharePackage,
modifier = Modifier.fillMaxWidth(),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
icon = Icons.Default.Send
)
) {
Icon(Icons.Default.Send, null, modifier = Modifier.size(20.dp))
Spacer(modifier = Modifier.width(AppSpacing.sm))
Text(
stringResource(Res.string.manage_users_send_invite),
fontWeight = FontWeight.SemiBold
)
}
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.manage_users_easy_share_desc),
@@ -201,37 +190,32 @@ fun ManageUsersScreen(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
HorizontalDivider(modifier = Modifier.weight(1f))
OrganicDivider(modifier = Modifier.weight(1f))
Text(
text = stringResource(Res.string.manage_users_or),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = AppSpacing.lg)
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
)
HorizontalDivider(modifier = Modifier.weight(1f))
OrganicDivider(modifier = Modifier.weight(1f))
}
}
// Share Code Section
item {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.lg),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(AppSpacing.lg)
modifier = Modifier.padding(OrganicSpacing.lg)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Icon(
Icons.Default.Key,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.Key,
size = 24.dp
)
Text(
text = stringResource(Res.string.manage_users_share_code),
@@ -240,20 +224,17 @@ fun ManageUsersScreen(
)
}
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Share code display
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
accentColor = MaterialTheme.colorScheme.surface
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
@@ -291,9 +272,11 @@ fun ManageUsersScreen(
}
}
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Button(
OrganicPrimaryButton(
text = if (shareCode != null) stringResource(Res.string.manage_users_generate_new)
else stringResource(Res.string.manage_users_generate),
onClick = {
scope.launch {
isGeneratingCode = true
@@ -312,27 +295,14 @@ fun ManageUsersScreen(
isGeneratingCode = false
}
},
modifier = Modifier.fillMaxWidth(),
enabled = !isGeneratingCode,
modifier = Modifier.fillMaxWidth()
) {
if (isGeneratingCode) {
CircularProgressIndicator(
modifier = Modifier.size(20.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
isLoading = isGeneratingCode,
icon = Icons.Default.Refresh
)
} else {
Icon(Icons.Default.Refresh, null, modifier = Modifier.size(20.dp))
}
Spacer(modifier = Modifier.width(AppSpacing.sm))
Text(
if (shareCode != null) stringResource(Res.string.manage_users_generate_new)
else stringResource(Res.string.manage_users_generate)
)
}
if (shareCode != null) {
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.manage_users_code_desc),
style = MaterialTheme.typography.bodySmall,
@@ -344,7 +314,7 @@ fun ManageUsersScreen(
}
item {
HorizontalDivider()
OrganicDivider()
}
}
@@ -369,7 +339,8 @@ fun ManageUsersScreen(
// Bottom spacing
item {
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
}
}
}
@@ -424,27 +395,23 @@ private fun UserCard(
canRemove: Boolean,
onRemove: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
// Avatar
Surface(
shape = RoundedCornerShape(AppRadius.md),
shape = OrganicShapes.medium,
color = MaterialTheme.colorScheme.primaryContainer,
modifier = Modifier.size(48.dp)
) {
@@ -461,7 +428,7 @@ private fun UserCard(
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Text(
text = user.username,
@@ -471,13 +438,13 @@ private fun UserCard(
if (isOwner) {
Surface(
color = MaterialTheme.colorScheme.primaryContainer,
shape = RoundedCornerShape(AppRadius.xs)
shape = OrganicShapes.extraSmall
) {
Text(
text = stringResource(Res.string.manage_users_owner_badge),
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp)
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp)
)
}
}

View File

@@ -3,7 +3,6 @@ package com.example.casera.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@@ -15,8 +14,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.util.DateUtils
import com.example.casera.viewmodel.NotificationPreferencesViewModel
import com.example.casera.analytics.PostHogAnalytics
@@ -93,7 +91,9 @@ fun NotificationPreferencesScreen(
}
}
WarmGradientBackground {
Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = {
TopAppBar(
title = { Text(stringResource(Res.string.notifications_title), fontWeight = FontWeight.SemiBold) },
@@ -103,7 +103,7 @@ fun NotificationPreferencesScreen(
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
containerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
}
@@ -113,29 +113,23 @@ fun NotificationPreferencesScreen(
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
// Header
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.lg),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.xl),
.padding(OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Icon(
imageVector = Icons.Default.Notifications,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.Notifications,
size = 60.dp
)
Text(
@@ -157,7 +151,7 @@ fun NotificationPreferencesScreen(
Box(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.xl),
.padding(OrganicSpacing.xl),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
@@ -165,21 +159,18 @@ fun NotificationPreferencesScreen(
}
is ApiResult.Error -> {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(AppRadius.md)
accentColor = MaterialTheme.colorScheme.errorContainer
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -194,12 +185,11 @@ fun NotificationPreferencesScreen(
fontWeight = FontWeight.SemiBold
)
}
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.common_retry),
onClick = { viewModel.loadPreferences() },
modifier = Modifier.fillMaxWidth()
) {
Text(stringResource(Res.string.common_retry))
}
)
}
}
}
@@ -210,15 +200,11 @@ fun NotificationPreferencesScreen(
stringResource(Res.string.notifications_task_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md)
modifier = Modifier.padding(top = OrganicSpacing.md)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column {
NotificationToggle(
@@ -247,9 +233,8 @@ fun NotificationPreferencesScreen(
)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
)
NotificationToggle(
@@ -278,9 +263,8 @@ fun NotificationPreferencesScreen(
)
}
HorizontalDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
)
NotificationToggle(
@@ -295,9 +279,8 @@ fun NotificationPreferencesScreen(
}
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
)
NotificationToggle(
@@ -346,15 +329,11 @@ fun NotificationPreferencesScreen(
stringResource(Res.string.notifications_other_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md)
modifier = Modifier.padding(top = OrganicSpacing.md)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column {
NotificationToggle(
@@ -369,9 +348,8 @@ fun NotificationPreferencesScreen(
}
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
)
NotificationToggle(
@@ -386,9 +364,8 @@ fun NotificationPreferencesScreen(
}
)
HorizontalDivider(
modifier = Modifier.padding(horizontal = AppSpacing.lg),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f)
OrganicDivider(
modifier = Modifier.padding(horizontal = OrganicSpacing.lg)
)
NotificationToggle(
@@ -438,15 +415,11 @@ fun NotificationPreferencesScreen(
stringResource(Res.string.notifications_email_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(top = AppSpacing.md)
modifier = Modifier.padding(top = OrganicSpacing.md)
)
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column {
NotificationToggle(
@@ -463,7 +436,8 @@ fun NotificationPreferencesScreen(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
}
}
}
@@ -482,8 +456,8 @@ private fun NotificationToggle(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -528,8 +502,8 @@ private fun NotificationTimePickerRow(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(start = AppSpacing.lg + 24.dp + AppSpacing.md, end = AppSpacing.lg, bottom = AppSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
.padding(start = OrganicSpacing.lg + 24.dp + OrganicSpacing.md, end = OrganicSpacing.lg, bottom = OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -584,7 +558,7 @@ private fun HourPickerDialog(
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Text(
text = DateUtils.formatHour(selectedHour),
@@ -601,7 +575,7 @@ private fun HourPickerDialog(
// AM hours (6 AM - 11 AM)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
"AM",
@@ -620,7 +594,7 @@ private fun HourPickerDialog(
// PM hours (12 PM - 5 PM)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
"PM",
@@ -639,7 +613,7 @@ private fun HourPickerDialog(
// Evening hours (6 PM - 11 PM)
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
"EVE",
@@ -687,7 +661,7 @@ private fun HourChip(
modifier = Modifier
.width(56.dp)
.clickable { onClick() },
shape = RoundedCornerShape(AppRadius.sm),
shape = OrganicShapes.small,
color = if (isSelected) MaterialTheme.colorScheme.primary else MaterialTheme.colorScheme.surfaceVariant
) {
Text(
@@ -695,7 +669,7 @@ private fun HourChip(
style = MaterialTheme.typography.bodySmall,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) MaterialTheme.colorScheme.onPrimary else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = AppSpacing.xs),
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = OrganicSpacing.xs),
textAlign = androidx.compose.ui.text.style.TextAlign.Center
)
}

View File

@@ -23,6 +23,7 @@ import com.example.casera.utils.SubscriptionHelper
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.ThemeManager
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult
import com.example.casera.storage.TokenStorage
@@ -140,6 +141,7 @@ fun ProfileScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
if (isLoadingUser) {
Box(
modifier = Modifier
@@ -157,16 +159,15 @@ fun ProfileScreen(
.verticalScroll(rememberScrollState())
.padding(start = 24.dp, end = 24.dp, top = 24.dp, bottom = 96.dp),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Profile Icon
Icon(
Icons.Default.AccountCircle,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.AccountCircle,
size = 80.dp,
iconSize = 48.dp
)
Text(
@@ -175,27 +176,24 @@ fun ProfileScreen(
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Edit Profile Section (scrolls down to profile fields)
Card(
// Edit Profile Section
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { /* Profile fields are below - could add scroll behavior */ },
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
.clickable { /* Profile fields are below - could add scroll behavior */ }
.naturalShadow()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = stringResource(Res.string.profile_edit_profile),
@@ -219,24 +217,21 @@ fun ProfileScreen(
}
// Theme Selector Section
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { showThemePicker = true },
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
.clickable { showThemePicker = true }
.naturalShadow()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = stringResource(Res.string.profile_appearance),
@@ -258,24 +253,21 @@ fun ProfileScreen(
}
// Notification Preferences Section
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onNavigateToNotificationPreferences() },
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
.clickable { onNavigateToNotificationPreferences() }
.naturalShadow()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = stringResource(Res.string.profile_notifications),
@@ -298,26 +290,23 @@ fun ProfileScreen(
// Contact Support Section
val uriHandler = LocalUriHandler.current
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri("mailto:caseraSupport@treymail.com?subject=Casera%20Support%20Request")
},
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
}
.naturalShadow()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = stringResource(Res.string.profile_contact_support),
@@ -339,26 +328,23 @@ fun ProfileScreen(
}
// Privacy Policy Section
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
uriHandler.openUri("https://mycrib.treytartt.com/privacy")
},
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
}
.naturalShadow()
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
.padding(OrganicSpacing.lg),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = stringResource(Res.string.profile_privacy),
@@ -381,24 +367,22 @@ fun ProfileScreen(
// Subscription Section - Only show if limitations are enabled
if (currentSubscription?.limitationsEnabled == true) {
Divider(modifier = Modifier.padding(vertical = AppSpacing.sm))
OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.sm))
Card(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceVariant
)
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.naturalShadow()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(AppSpacing.lg),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
.padding(OrganicSpacing.lg),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.md)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
Icon(
imageVector = Icons.Default.Star,
@@ -407,7 +391,7 @@ fun ProfileScreen(
)
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = if (SubscriptionHelper.currentTier == "pro") stringResource(Res.string.profile_pro_plan) else stringResource(Res.string.profile_free_plan),
@@ -430,8 +414,8 @@ fun ProfileScreen(
if (SubscriptionHelper.currentTier != "pro") {
// Upgrade Benefits List
Column(
modifier = Modifier.padding(vertical = AppSpacing.sm),
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
modifier = Modifier.padding(vertical = OrganicSpacing.sm),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Text(
text = stringResource(Res.string.profile_upgrade_benefits_title),
@@ -483,7 +467,7 @@ fun ProfileScreen(
imageVector = Icons.Default.KeyboardArrowUp,
contentDescription = null
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(stringResource(Res.string.profile_upgrade_to_pro), fontWeight = FontWeight.SemiBold)
}
} else {
@@ -491,13 +475,13 @@ fun ProfileScreen(
text = stringResource(Res.string.profile_manage_subscription),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(top = AppSpacing.xs)
modifier = Modifier.padding(top = OrganicSpacing.xs)
)
}
}
}
Divider(modifier = Modifier.padding(vertical = AppSpacing.sm))
OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.sm))
}
Text(
@@ -610,7 +594,7 @@ fun ProfileScreen(
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Button(
onClick = {
@@ -648,14 +632,14 @@ fun ProfileScreen(
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// App Version Section
HorizontalDivider(modifier = Modifier.padding(vertical = AppSpacing.md))
OrganicDivider(modifier = Modifier.padding(vertical = OrganicSpacing.md))
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xs)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xs)
) {
Text(
text = stringResource(Res.string.profile_app_name),
@@ -670,7 +654,8 @@ fun ProfileScreen(
)
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
}
}
}
@@ -713,7 +698,7 @@ private fun UpgradeBenefitRow(
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Icon(
imageVector = icon,

View File

@@ -17,6 +17,7 @@ import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.components.HandleErrors
import com.example.casera.ui.components.auth.AuthHeader
import com.example.casera.ui.components.common.ErrorCard
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult
import com.example.casera.analytics.PostHogAnalytics
@@ -65,6 +66,7 @@ fun RegisterScreen(
}
}
WarmGradientBackground {
Scaffold(
topBar = {
TopAppBar(
@@ -75,21 +77,22 @@ fun RegisterScreen(
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = MaterialTheme.colorScheme.surface
containerColor = MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
)
)
}
},
containerColor = androidx.compose.ui.graphics.Color.Transparent
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(24.dp),
.padding(OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
AuthHeader(
icon = Icons.Default.PersonAdd,
@@ -97,8 +100,14 @@ fun RegisterScreen(
subtitle = stringResource(Res.string.auth_register_subtitle)
)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
OrganicCard(
modifier = Modifier.fillMaxWidth()
) {
Column(
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
OutlinedTextField(
value = username,
onValueChange = { username = it },
@@ -123,6 +132,8 @@ fun RegisterScreen(
shape = RoundedCornerShape(12.dp)
)
OrganicDivider()
OutlinedTextField(
value = password,
onValueChange = { password = it },
@@ -148,13 +159,16 @@ fun RegisterScreen(
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp)
)
}
}
ErrorCard(message = errorMessage)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
val passwordsDontMatchMessage = stringResource(Res.string.auth_passwords_dont_match)
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.auth_register_button),
onClick = {
when {
password != confirmPassword -> {
@@ -167,29 +181,14 @@ fun RegisterScreen(
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
modifier = Modifier.fillMaxWidth(),
enabled = username.isNotEmpty() && email.isNotEmpty() &&
password.isNotEmpty() && !isLoading,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
isLoading = isLoading
)
} else {
Text(
stringResource(Res.string.auth_register_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
}
}
}

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
@@ -20,6 +18,7 @@ import com.example.casera.ui.components.auth.RequirementItem
import com.example.casera.ui.components.common.ErrorCard
import com.example.casera.viewmodel.PasswordResetViewModel
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -80,90 +79,116 @@ fun ResetPasswordScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
showBlob = true,
blobVariation = 2
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
.padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
if (isSuccess) {
// Success State
AuthHeader(
OrganicIconContainer(
icon = Icons.Default.CheckCircle,
title = "Success!",
subtitle = "Your password has been reset successfully"
size = 80.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Card(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
Text(
text = "Success!",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Text(
text = "Your password has been reset successfully",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = MaterialTheme.colorScheme.primary,
showBlob = false
) {
Text(
"You can now log in with your new password",
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(OrganicSpacing.cozy),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
}
Button(
onClick = onPasswordResetSuccess,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(12.dp)
) {
Text(
"Return to Login",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = "Return to Login",
onClick = onPasswordResetSuccess
)
}
} else {
// Reset Password Form
AuthHeader(
OrganicIconContainer(
icon = Icons.Default.LockReset,
title = "Set New Password",
subtitle = "Create a strong password to secure your account"
size = 80.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = "Set New Password",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Text(
text = "Create a strong password to secure your account",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
// Password Requirements
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = MaterialTheme.colorScheme.secondary,
showBlob = false
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Text(
"Password Requirements",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
)
RequirementItem(
@@ -206,7 +231,6 @@ fun ResetPasswordScreen(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (newPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading
)
@@ -231,43 +255,23 @@ fun ResetPasswordScreen(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = if (confirmPasswordVisible) VisualTransformation.None else PasswordVisualTransformation(),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading
)
ErrorCard(message = errorMessage)
Button(
OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = if (isLoggingIn) "Logging in..." else stringResource(Res.string.auth_reset_button),
onClick = {
viewModel.resetPassword(newPassword, confirmPassword)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = isFormValid && !isLoading && !isLoggingIn,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading || isLoggingIn) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
isLoading = isLoading || isLoggingIn
)
Spacer(modifier = Modifier.width(8.dp))
Text(
if (isLoggingIn) "Logging in..." else "Resetting...",
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
} else {
Icon(Icons.Default.LockReset, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
stringResource(Res.string.auth_reset_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
}

View File

@@ -39,6 +39,7 @@ import com.example.casera.util.DateUtils
import com.example.casera.platform.rememberShareResidence
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -539,6 +540,7 @@ fun ResidenceDetailScreen(
}
}
) { paddingValues ->
WarmGradientBackground {
ApiResultHandler(
state = residenceState,
onRetry = {
@@ -551,7 +553,7 @@ fun ResidenceDetailScreen(
loadingContent = {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
CircularProgressIndicator()
Text(
@@ -566,23 +568,21 @@ fun ResidenceDetailScreen(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues),
contentPadding = PaddingValues(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
contentPadding = PaddingValues(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
// Property Header Card
item {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
accentColor = MaterialTheme.colorScheme.primary,
showBlob = true,
blobVariation = 0
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp)
.padding(OrganicSpacing.comfortable)
) {
Row(
modifier = Modifier.fillMaxWidth(),
@@ -594,7 +594,7 @@ fun ResidenceDetailScreen(
text = residence.name,
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
color = MaterialTheme.colorScheme.textPrimary
)
}
}
@@ -607,21 +607,60 @@ fun ResidenceDetailScreen(
residence.stateProvince != null || residence.postalCode != null ||
residence.country != null) {
item {
InfoCard(
icon = Icons.Default.LocationOn,
title = stringResource(Res.string.properties_address_section)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
showBlob = true,
blobVariation = 1
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
OrganicIconContainer(
icon = Icons.Default.LocationOn,
size = 40.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = stringResource(Res.string.properties_address_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary
)
}
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
if (residence.streetAddress != null) {
Text(text = residence.streetAddress)
Text(
text = residence.streetAddress,
color = MaterialTheme.colorScheme.textSecondary
)
}
if (residence.apartmentUnit != null) {
Text(text = "Unit: ${residence.apartmentUnit}")
Text(
text = "Unit: ${residence.apartmentUnit}",
color = MaterialTheme.colorScheme.textSecondary
)
}
if (residence.city != null || residence.stateProvince != null || residence.postalCode != null) {
Text(text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}")
Text(
text = "${residence.city ?: ""}, ${residence.stateProvince ?: ""} ${residence.postalCode ?: ""}",
color = MaterialTheme.colorScheme.textSecondary
)
}
if (residence.country != null) {
Text(text = residence.country)
Text(
text = residence.country,
color = MaterialTheme.colorScheme.textSecondary
)
}
}
}
}
@@ -631,10 +670,36 @@ fun ResidenceDetailScreen(
if (residence.bedrooms != null || residence.bathrooms != null ||
residence.squareFootage != null || residence.yearBuilt != null) {
item {
InfoCard(
icon = Icons.Default.Info,
title = stringResource(Res.string.properties_property_details_section)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
showBlob = true,
blobVariation = 2
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
OrganicIconContainer(
icon = Icons.Default.Info,
size = 40.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = stringResource(Res.string.properties_property_details_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary
)
}
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceEvenly
@@ -646,7 +711,7 @@ fun ResidenceDetailScreen(
PropertyDetailItem(Icons.Default.Bathroom, "$it", "Bathrooms")
}
}
Spacer(modifier = Modifier.height(12.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
residence.squareFootage?.let {
DetailRow(Icons.Default.SquareFoot, "Square Footage", "$it sq ft")
}
@@ -659,30 +724,84 @@ fun ResidenceDetailScreen(
}
}
}
}
// Description Card
if (residence.description != null && !residence.description.isEmpty()) {
item {
InfoCard(
icon = Icons.Default.Description,
title = stringResource(Res.string.properties_description_section)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
showBlob = true,
blobVariation = 0
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
OrganicIconContainer(
icon = Icons.Default.Description,
size = 40.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = stringResource(Res.string.properties_description_section),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary
)
}
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
Text(
text = residence.description,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.textSecondary
)
}
}
}
}
// Purchase Information
if (residence.purchaseDate != null || residence.purchasePrice != null) {
item {
InfoCard(
icon = Icons.Default.AttachMoney,
title = stringResource(Res.string.properties_purchase_info)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
showBlob = true,
blobVariation = 1
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
OrganicIconContainer(
icon = Icons.Default.AttachMoney,
size = 40.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = stringResource(Res.string.properties_purchase_info),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary
)
}
OrganicDivider(horizontalPadding = OrganicSpacing.compact)
residence.purchaseDate?.let {
DetailRow(Icons.Default.Event, "Purchase Date", DateUtils.formatDateMedium(it))
}
@@ -692,27 +811,29 @@ fun ResidenceDetailScreen(
}
}
}
}
// Tasks Section Header
item {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
.padding(vertical = OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.Assignment,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
OrganicIconContainer(
icon = Icons.Default.Assignment,
size = 36.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(Res.string.tasks_title),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
color = MaterialTheme.colorScheme.textPrimary
)
}
}
@@ -732,16 +853,15 @@ fun ResidenceDetailScreen(
}
is ApiResult.Error -> {
item {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(12.dp)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = MaterialTheme.colorScheme.error,
showBlob = false
) {
Text(
text = "Error loading tasks: ${com.example.casera.util.ErrorMessageParser.parse((tasksState as ApiResult.Error).message)}",
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp)
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(OrganicSpacing.cozy)
)
}
}
@@ -751,32 +871,35 @@ fun ResidenceDetailScreen(
val allTasksEmpty = taskData.columns.all { it.tasks.isEmpty() }
if (allTasksEmpty) {
item {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp)
showBlob = true,
blobVariation = 2
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
horizontalAlignment = Alignment.CenterHorizontally
.padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
Icon(
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(64.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
OrganicIconContainer(
icon = Icons.Default.Assignment,
size = 64.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
iconColor = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
stringResource(Res.string.properties_no_tasks),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
)
Text(
stringResource(Res.string.properties_add_task_start),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.textSecondary
)
}
}
@@ -833,25 +956,26 @@ fun ResidenceDetailScreen(
// Contractors Section Header
item {
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
Row(
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically
.padding(vertical = OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.People,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(28.dp)
OrganicIconContainer(
icon = Icons.Default.People,
size = 36.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(Res.string.contractors_title),
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.primary
color = MaterialTheme.colorScheme.textPrimary
)
}
}
@@ -871,16 +995,15 @@ fun ResidenceDetailScreen(
}
is ApiResult.Error -> {
item {
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(12.dp)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = MaterialTheme.colorScheme.error,
showBlob = false
) {
Text(
text = "Error loading contractors: ${com.example.casera.util.ErrorMessageParser.parse((contractorsState as ApiResult.Error).message)}",
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp)
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(OrganicSpacing.cozy)
)
}
}
@@ -889,32 +1012,35 @@ fun ResidenceDetailScreen(
val contractors = (contractorsState as ApiResult.Success<List<ContractorSummary>>).data
if (contractors.isEmpty()) {
item {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(16.dp)
showBlob = true,
blobVariation = 1
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(24.dp),
horizontalAlignment = Alignment.CenterHorizontally
.padding(OrganicSpacing.comfortable),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(48.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f)
OrganicIconContainer(
icon = Icons.Default.PersonAdd,
size = 56.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f),
iconColor = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.height(12.dp))
Text(
stringResource(Res.string.properties_no_contractors),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
)
Text(
stringResource(Res.string.properties_add_contractors_hint),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.textSecondary
)
}
}
@@ -939,4 +1065,5 @@ fun ResidenceDetailScreen(
}
}
}
}
}

View File

@@ -26,6 +26,7 @@ import com.example.casera.network.ResidenceApi
import com.example.casera.storage.TokenStorage
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import kotlinx.coroutines.launch
import org.jetbrains.compose.resources.stringResource
@@ -170,13 +171,14 @@ fun ResidenceFormScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(16.dp)
.padding(OrganicSpacing.cozy)
.verticalScroll(rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
// Basic Information section
Text(
@@ -279,7 +281,7 @@ fun ResidenceFormScreen(
)
// Optional fields section
Divider()
OrganicDivider()
Text(
text = stringResource(Res.string.properties_form_optional),
style = MaterialTheme.typography.titleMedium,
@@ -288,7 +290,7 @@ fun ResidenceFormScreen(
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
OutlinedTextField(
value = bedrooms,
@@ -353,7 +355,7 @@ fun ResidenceFormScreen(
// Users section (edit mode only, owner only)
if (isEditMode && isCurrentUserOwner) {
Divider()
OrganicDivider()
Text(
text = "Shared Users (${users.size})",
style = MaterialTheme.typography.titleMedium,
@@ -362,7 +364,7 @@ fun ResidenceFormScreen(
if (isLoadingUsers) {
Box(
modifier = Modifier.fillMaxWidth().padding(16.dp),
modifier = Modifier.fillMaxWidth().padding(OrganicSpacing.cozy),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(modifier = Modifier.size(24.dp))
@@ -372,7 +374,7 @@ fun ResidenceFormScreen(
text = "No shared users",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(vertical = 8.dp)
modifier = Modifier.padding(vertical = OrganicSpacing.compact)
)
} else {
users.forEach { user ->
@@ -404,7 +406,8 @@ fun ResidenceFormScreen(
}
// Submit button
Button(
OrganicPrimaryButton(
text = if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create),
onClick = {
if (validateForm()) {
val request = ResidenceCreateRequest(
@@ -432,20 +435,12 @@ fun ResidenceFormScreen(
}
}
},
modifier = Modifier.fillMaxWidth(),
enabled = validateForm()
) {
if (operationState is ApiResult.Loading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
enabled = validateForm(),
isLoading = operationState is ApiResult.Loading
)
} else {
Text(if (isEditMode) stringResource(Res.string.properties_form_update) else stringResource(Res.string.properties_form_create))
}
}
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
}
}
}
@@ -508,8 +503,9 @@ private fun UserListItem(
user: ResidenceUser,
onRemove: () -> Unit
) {
Card(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp)
OrganicCard(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
showBlob = false
) {
Row(
modifier = Modifier.fillMaxWidth().padding(12.dp),

View File

@@ -37,6 +37,7 @@ import com.example.casera.cache.SubscriptionCache
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.data.DataManager
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -225,14 +226,14 @@ fun ResidencesScreen(
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
modifier = Modifier.padding(OrganicSpacing.comfortable)
) {
Icon(
Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
OrganicIconContainer(
icon = Icons.Default.Home,
size = 80.dp,
iconScale = 0.6f,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
)
Text(
stringResource(Res.string.properties_empty_title),
@@ -244,7 +245,7 @@ fun ResidencesScreen(
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
// Only show Add Property button if not blocked (limit>0)
if (!isBlocked.allowed) {
Button(
@@ -263,7 +264,7 @@ fun ResidencesScreen(
shape = RoundedCornerShape(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Add, contentDescription = null)
@@ -274,7 +275,7 @@ fun ResidencesScreen(
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
OutlinedButton(
onClick = {
val (allowed, triggerKey) = canAddProperty()
@@ -291,7 +292,7 @@ fun ResidencesScreen(
shape = RoundedCornerShape(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.GroupAdd, contentDescription = null)
@@ -315,7 +316,7 @@ fun ResidencesScreen(
shape = RoundedCornerShape(12.dp)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.Star, contentDescription = null)
@@ -344,28 +345,27 @@ fun ResidencesScreen(
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(
start = 16.dp,
end = 16.dp,
top = 16.dp,
start = OrganicSpacing.cozy,
end = OrganicSpacing.cozy,
top = OrganicSpacing.cozy,
bottom = 96.dp
),
verticalArrangement = Arrangement.spacedBy(16.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
// Summary Card
item {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
shape = RoundedCornerShape(20.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
accentColor = MaterialTheme.colorScheme.primary,
showBlob = true,
blobVariation = 0,
shadowIntensity = ShadowIntensity.Medium
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
.padding(OrganicSpacing.cozy),
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
Row(
verticalAlignment = Alignment.CenterVertically
@@ -373,15 +373,15 @@ fun ResidencesScreen(
Icon(
Icons.Default.Dashboard,
contentDescription = null,
tint = MaterialTheme.colorScheme.onPrimaryContainer,
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(8.dp))
Spacer(modifier = Modifier.width(OrganicSpacing.compact))
Text(
text = stringResource(Res.string.home_overview),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onPrimaryContainer
color = MaterialTheme.colorScheme.textPrimary
)
}
@@ -401,8 +401,8 @@ fun ResidencesScreen(
)
}
HorizontalDivider(
color = MaterialTheme.colorScheme.onPrimaryContainer.copy(alpha = 0.2f)
OrganicDivider(
color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.2f)
)
Row(
@@ -436,7 +436,7 @@ fun ResidencesScreen(
text = stringResource(Res.string.home_your_properties),
style = MaterialTheme.typography.titleLarge,
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 8.dp)
modifier = Modifier.padding(top = OrganicSpacing.compact)
)
}
@@ -456,46 +456,41 @@ fun ResidencesScreen(
label = "pulseScale"
)
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onResidenceClick(residence.id) },
shape = MaterialTheme.shapes.large,
elevation = CardDefaults.cardElevation(defaultElevation = 0.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
)
accentColor = if (hasOverdue) MaterialTheme.colorScheme.error else MaterialTheme.colorScheme.primary,
showBlob = true,
blobVariation = residence.id % 3,
shadowIntensity = ShadowIntensity.Subtle
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(20.dp)
.padding(OrganicSpacing.cozy)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(16.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically
) {
// Pulsing circular house icon when overdue
// Pulsing organic icon container when overdue
Box(
modifier = Modifier
.size(56.dp)
.then(
if (hasOverdue) Modifier.scale(pulseScale) else Modifier
)
.clip(CircleShape)
.background(
if (hasOverdue) MaterialTheme.colorScheme.errorContainer
else MaterialTheme.colorScheme.primaryContainer
),
contentAlignment = Alignment.Center
modifier = if (hasOverdue) Modifier.scale(pulseScale) else Modifier
) {
Icon(
Icons.Default.Home,
contentDescription = null,
tint = if (hasOverdue) MaterialTheme.colorScheme.onErrorContainer
else MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.size(28.dp)
OrganicIconContainer(
icon = Icons.Default.Home,
size = 56.dp,
iconScale = 0.5f,
backgroundColor = if (hasOverdue)
MaterialTheme.colorScheme.errorContainer
else
MaterialTheme.colorScheme.primaryContainer,
iconColor = if (hasOverdue)
MaterialTheme.colorScheme.onErrorContainer
else
MaterialTheme.colorScheme.onPrimaryContainer
)
}
@@ -582,9 +577,9 @@ fun ResidencesScreen(
)
}
Spacer(modifier = Modifier.height(16.dp))
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(16.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
OrganicDivider(color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f))
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
// Fully dynamic task summary from API - show first 3 categories
val displayCategories = residence.taskSummary.categories.take(3)

View File

@@ -1,5 +1,6 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
@@ -21,6 +22,7 @@ import com.example.casera.viewmodel.TaskViewModel
import com.example.casera.network.ApiResult
import com.example.casera.analytics.PostHogAnalytics
import com.example.casera.analytics.AnalyticsEvents
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -66,7 +68,9 @@ fun TasksScreen(
}
}
WarmGradientBackground {
Scaffold(
containerColor = androidx.compose.ui.graphics.Color.Transparent,
topBar = {
TopAppBar(
title = { Text(stringResource(Res.string.tasks_title)) },
@@ -74,7 +78,10 @@ fun TasksScreen(
IconButton(onClick = onNavigateBack) {
Icon(Icons.Default.ArrowBack, contentDescription = stringResource(Res.string.common_back))
}
}
},
colors = TopAppBarDefaults.topAppBarColors(
containerColor = androidx.compose.ui.graphics.Color.Transparent
)
)
},
// No FAB on Tasks screen - tasks are added from within residences
@@ -87,7 +94,9 @@ fun TasksScreen(
.padding(paddingValues),
contentAlignment = androidx.compose.ui.Alignment.Center
) {
CircularProgressIndicator()
CircularProgressIndicator(
color = MaterialTheme.colorScheme.primary
)
}
}
is ApiResult.Success -> {
@@ -103,24 +112,26 @@ fun TasksScreen(
) {
Column(
horizontalAlignment = androidx.compose.ui.Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier.padding(24.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy),
modifier = Modifier.padding(OrganicSpacing.comfortable)
) {
Icon(
Icons.Default.Assignment,
contentDescription = null,
modifier = Modifier.size(80.dp),
tint = MaterialTheme.colorScheme.primary.copy(alpha = 0.6f)
OrganicIconContainer(
icon = Icons.Default.Assignment,
size = 80.dp,
iconScale = 0.6f,
backgroundColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.2f),
iconColor = MaterialTheme.colorScheme.primary
)
Text(
stringResource(Res.string.tasks_empty_title),
style = MaterialTheme.typography.headlineSmall,
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
)
Text(
stringResource(Res.string.tasks_empty_subtitle),
style = MaterialTheme.typography.bodyLarge,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.textSecondary
)
}
}
@@ -129,18 +140,18 @@ fun TasksScreen(
modifier = Modifier
.fillMaxSize(),
contentPadding = PaddingValues(
top = paddingValues.calculateTopPadding() + 16.dp,
bottom = paddingValues.calculateBottomPadding() + 16.dp,
start = 16.dp,
end = 16.dp
top = paddingValues.calculateTopPadding() + OrganicSpacing.cozy,
bottom = paddingValues.calculateBottomPadding() + OrganicSpacing.cozy,
start = OrganicSpacing.cozy,
end = OrganicSpacing.cozy
),
verticalArrangement = Arrangement.spacedBy(12.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
// Task summary pills - dynamically generated from all columns
item {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp)
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
taskData.columns.forEach { column ->
TaskPill(
@@ -162,7 +173,8 @@ fun TasksScreen(
Text(
text = "${column.displayName} (${column.tasks.size})",
style = MaterialTheme.typography.titleMedium,
modifier = Modifier.padding(top = 8.dp)
color = MaterialTheme.colorScheme.textPrimary,
modifier = Modifier.padding(top = OrganicSpacing.compact)
)
}
@@ -183,40 +195,47 @@ fun TasksScreen(
val isExpanded = expandedColumns.contains(column.name)
item {
Card(
modifier = Modifier.fillMaxWidth(),
onClick = {
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable {
expandedColumns = if (isExpanded) {
expandedColumns - column.name
} else {
expandedColumns + column.name
}
}
},
showBlob = false,
shadowIntensity = ShadowIntensity.Subtle
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
.padding(OrganicSpacing.cozy),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact),
verticalAlignment = androidx.compose.ui.Alignment.CenterVertically
) {
Icon(
getIconFromName(column.icons["android"] ?: "List"),
contentDescription = null,
tint = hexToColor(column.color)
OrganicIconContainer(
icon = getIconFromName(column.icons["android"] ?: "List"),
size = 40.dp,
iconScale = 0.5f,
backgroundColor = hexToColor(column.color).copy(alpha = 0.2f),
iconColor = hexToColor(column.color)
)
Text(
text = "${column.displayName} (${column.tasks.size})",
style = MaterialTheme.typography.titleMedium
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.textPrimary
)
}
Icon(
if (isExpanded) Icons.Default.KeyboardArrowUp else Icons.Default.KeyboardArrowDown,
contentDescription = if (isExpanded) "Collapse" else "Expand"
contentDescription = if (isExpanded) "Collapse" else "Expand",
tint = MaterialTheme.colorScheme.textSecondary
)
}
}
@@ -246,6 +265,7 @@ fun TasksScreen(
else -> {}
}
}
}
if (showCompleteDialog && selectedTask != null) {
CompleteTaskDialog(

View File

@@ -2,7 +2,6 @@ package com.example.casera.ui.screens
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -21,6 +20,7 @@ import com.example.casera.ui.components.auth.AuthHeader
import com.example.casera.ui.components.common.ErrorCard
import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -89,46 +89,62 @@ fun VerifyEmailScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.verticalScroll(rememberScrollState())
.padding(24.dp),
.padding(OrganicSpacing.comfortable),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
AuthHeader(
OrganicIconContainer(
icon = Icons.Default.MarkEmailRead,
title = stringResource(Res.string.auth_verify_title),
subtitle = stringResource(Res.string.auth_verify_subtitle)
size = 80.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = stringResource(Res.string.auth_verify_title),
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Card(
Text(
text = stringResource(Res.string.auth_verify_subtitle),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(OrganicSpacing.cozy))
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(12.dp)
accentColor = MaterialTheme.colorScheme.error,
showBlob = false
) {
Column(
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(OrganicSpacing.cozy),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.Info,
contentDescription = null,
tint = MaterialTheme.colorScheme.onErrorContainer
tint = MaterialTheme.colorScheme.error
)
Text(
text = "Email verification is required. Check your inbox for a 6-digit code.",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onErrorContainer,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center,
fontWeight = FontWeight.SemiBold
)
@@ -148,7 +164,6 @@ fun VerifyEmailScreen(
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(12.dp),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
placeholder = { Text("000000") }
)
@@ -159,7 +174,12 @@ fun VerifyEmailScreen(
)
}
Button(
OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = stringResource(Res.string.auth_verify_button),
onClick = {
if (code.length == 6) {
isLoading = true
@@ -168,40 +188,19 @@ fun VerifyEmailScreen(
errorMessage = "Please enter a valid 6-digit code"
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading && code.length == 6
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary
enabled = !isLoading && code.length == 6,
isLoading = isLoading
)
} else {
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Icon(Icons.Default.CheckCircle, contentDescription = null)
Text(
stringResource(Res.string.auth_verify_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
Text(
text = "Didn't receive the code? Check your spam folder or contact support.",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
}
}
}
}

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
@@ -19,6 +17,7 @@ import com.example.casera.ui.components.auth.AuthHeader
import com.example.casera.ui.components.common.ErrorCard
import com.example.casera.viewmodel.PasswordResetViewModel
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -70,69 +69,85 @@ fun VerifyResetCodeScreen(
)
}
) { paddingValues ->
WarmGradientBackground {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.padding(paddingValues),
contentAlignment = Alignment.Center
) {
Card(
OrganicCard(
modifier = Modifier
.fillMaxWidth(0.9f)
.wrapContentHeight(),
shape = RoundedCornerShape(24.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface
),
elevation = CardDefaults.cardElevation(defaultElevation = 8.dp)
showBlob = true,
blobVariation = 1
) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(32.dp),
.padding(OrganicSpacing.spacious),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(20.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.cozy)
) {
AuthHeader(
OrganicIconContainer(
icon = Icons.Default.MarkEmailRead,
title = "Check Your Email",
subtitle = "We sent a 6-digit code to"
size = 80.dp,
iconScale = 0.5f,
backgroundColor = MaterialTheme.colorScheme.primary,
iconColor = MaterialTheme.colorScheme.onPrimary
)
Text(
text = "Check Your Email",
style = MaterialTheme.typography.headlineMedium,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Text(
text = "We sent a 6-digit code to",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
Text(
email,
style = MaterialTheme.typography.bodyLarge,
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary,
textAlign = TextAlign.Center
)
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = MaterialTheme.colorScheme.secondary,
showBlob = false
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
.padding(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.Timer,
contentDescription = null,
tint = MaterialTheme.colorScheme.secondary
)
Spacer(modifier = Modifier.width(12.dp))
Text(
"Code expires in 15 minutes",
style = MaterialTheme.typography.bodyMedium,
fontWeight = FontWeight.SemiBold
fontWeight = FontWeight.SemiBold,
color = MaterialTheme.colorScheme.textPrimary
)
}
}
Spacer(modifier = Modifier.height(8.dp))
Spacer(modifier = Modifier.height(OrganicSpacing.compact))
OutlinedTextField(
value = code,
@@ -147,7 +162,6 @@ fun VerifyResetCodeScreen(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
shape = RoundedCornerShape(12.dp),
enabled = !isLoading,
textStyle = MaterialTheme.typography.headlineMedium.copy(
fontWeight = FontWeight.Bold,
@@ -158,75 +172,60 @@ fun VerifyResetCodeScreen(
Text(
"Enter the 6-digit code from your email",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
ErrorCard(message = errorMessage)
if (isSuccess) {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
)
accentColor = MaterialTheme.colorScheme.primary,
showBlob = false
) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
verticalAlignment = Alignment.CenterVertically
.padding(OrganicSpacing.cozy),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Icon(
Icons.Default.CheckCircle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primary
)
Spacer(modifier = Modifier.width(12.dp))
Text(
"Code verified! Now set your new password",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onPrimaryContainer
color = MaterialTheme.colorScheme.textPrimary
)
}
}
}
Button(
OrganicDivider(
modifier = Modifier.fillMaxWidth()
)
OrganicPrimaryButton(
text = stringResource(Res.string.auth_verify_button),
onClick = {
viewModel.verifyResetCode(email, code)
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
enabled = code.length == 6 && !isLoading,
shape = RoundedCornerShape(12.dp)
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
isLoading = isLoading
)
} else {
Icon(Icons.Default.CheckCircle, contentDescription = null)
Spacer(modifier = Modifier.width(8.dp))
Text(
stringResource(Res.string.auth_verify_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.compact)
) {
Text(
"Didn't receive the code?",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
color = MaterialTheme.colorScheme.textSecondary
)
TextButton(onClick = {
@@ -245,7 +244,7 @@ fun VerifyResetCodeScreen(
Text(
"Check your spam folder if you don't see it",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
color = MaterialTheme.colorScheme.textSecondary,
textAlign = TextAlign.Center
)
}
@@ -253,4 +252,5 @@ fun VerifyResetCodeScreen(
}
}
}
}
}

View File

@@ -5,10 +5,8 @@ import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
@@ -17,15 +15,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -65,35 +60,30 @@ fun OnboardingCreateAccountContent(
password.isNotBlank() &&
password == confirmPassword
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
.padding(horizontal = AppSpacing.xl)
.padding(horizontal = OrganicSpacing.xl)
) {
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Header
Column(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
// Icon
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.PersonAdd,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.PersonAdd,
size = 80.dp,
iconSize = 40.dp,
contentDescription = null
)
}
Text(
text = stringResource(Res.string.onboarding_create_account_title),
@@ -111,7 +101,7 @@ fun OnboardingCreateAccountContent(
)
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Create with Email section
if (!isFormExpanded) {
@@ -121,14 +111,14 @@ fun OnboardingCreateAccountContent(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.1f),
contentColor = MaterialTheme.colorScheme.primary
)
) {
Icon(Icons.Default.Email, contentDescription = null)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_create_with_email),
fontWeight = FontWeight.Medium
@@ -143,7 +133,7 @@ fun OnboardingCreateAccountContent(
exit = fadeOut() + shrinkVertically()
) {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
// Username
OutlinedTextField(
@@ -158,7 +148,7 @@ fun OnboardingCreateAccountContent(
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading
)
@@ -175,7 +165,7 @@ fun OnboardingCreateAccountContent(
},
modifier = Modifier.fillMaxWidth(),
singleLine = true,
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading
)
@@ -193,7 +183,7 @@ fun OnboardingCreateAccountContent(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading
)
@@ -211,7 +201,7 @@ fun OnboardingCreateAccountContent(
modifier = Modifier.fillMaxWidth(),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading,
isError = confirmPassword.isNotEmpty() && password != confirmPassword,
supportingText = if (confirmPassword.isNotEmpty() && password != confirmPassword) {
@@ -221,16 +211,14 @@ fun OnboardingCreateAccountContent(
// Error message
if (localErrorMessage != null) {
Card(
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(AppRadius.md)
accentColor = MaterialTheme.colorScheme.error,
showBlob = false
) {
Row(
modifier = Modifier.padding(AppSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
modifier = Modifier.padding(OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -247,10 +235,11 @@ fun OnboardingCreateAccountContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Create Account button
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.auth_register_button),
onClick = {
if (password == confirmPassword) {
viewModel.register(username, email, password)
@@ -258,30 +247,14 @@ fun OnboardingCreateAccountContent(
localErrorMessage = "Passwords don't match"
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
enabled = isFormValid && !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
modifier = Modifier.fillMaxWidth(),
enabled = isFormValid && !isLoading,
isLoading = isLoading
)
} else {
Text(
text = stringResource(Res.string.auth_register_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
)
}
}
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Already have an account
Row(
@@ -303,7 +276,8 @@ fun OnboardingCreateAccountContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
// Login dialog
@@ -356,7 +330,7 @@ private fun OnboardingLoginDialog(
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
OutlinedTextField(
value = username,
@@ -364,7 +338,7 @@ private fun OnboardingLoginDialog(
label = { Text(stringResource(Res.string.auth_login_username_label)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading
)
@@ -374,7 +348,7 @@ private fun OnboardingLoginDialog(
label = { Text(stringResource(Res.string.auth_login_password_label)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
visualTransformation = PasswordVisualTransformation(),
enabled = !isLoading
)

View File

@@ -24,8 +24,7 @@ import androidx.compose.ui.unit.dp
import com.example.casera.data.DataManager
import com.example.casera.models.TaskCreateRequest
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.*
import com.example.casera.util.DateUtils
@@ -163,7 +162,7 @@ fun OnboardingFirstTaskContent(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentPadding = PaddingValues(horizontal = AppSpacing.lg, vertical = AppSpacing.md)
contentPadding = PaddingValues(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md)
) {
// Header
item {
@@ -171,30 +170,19 @@ fun OnboardingFirstTaskContent(
modifier = Modifier.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally
) {
// Celebration icon
Box(
modifier = Modifier
.size(80.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(
colors = listOf(
// Celebration icon using OrganicIconContainer
OrganicIconContainer(
icon = Icons.Default.Celebration,
size = 80.dp,
iconSize = 40.dp,
gradientColors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.secondary
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Celebration,
contentDescription = null,
modifier = Modifier.size(40.dp),
tint = Color.White
contentDescription = null
)
}
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
Text(
text = stringResource(Res.string.onboarding_tasks_title),
@@ -203,7 +191,7 @@ fun OnboardingFirstTaskContent(
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_tasks_subtitle),
@@ -212,11 +200,11 @@ fun OnboardingFirstTaskContent(
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Selection counter
Surface(
shape = RoundedCornerShape(AppRadius.xl),
shape = RoundedCornerShape(OrganicRadius.xl),
color = if (isAtMaxSelection) {
MaterialTheme.colorScheme.tertiary.copy(alpha = 0.1f)
} else {
@@ -224,8 +212,8 @@ fun OnboardingFirstTaskContent(
}
) {
Row(
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -243,7 +231,7 @@ fun OnboardingFirstTaskContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
}
}
@@ -267,7 +255,7 @@ fun OnboardingFirstTaskContent(
}
}
)
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
}
// Add popular tasks button
@@ -291,7 +279,7 @@ fun OnboardingFirstTaskContent(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.lg),
shape = RoundedCornerShape(OrganicRadius.lg),
border = ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.linearGradient(
colors = listOf(
@@ -302,7 +290,7 @@ fun OnboardingFirstTaskContent(
)
) {
Icon(Icons.Default.AutoAwesome, contentDescription = null)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_tasks_add_popular),
fontWeight = FontWeight.Medium
@@ -318,9 +306,14 @@ fun OnboardingFirstTaskContent(
shadowElevation = 8.dp
) {
Column(
modifier = Modifier.padding(AppSpacing.lg)
modifier = Modifier.padding(OrganicSpacing.lg)
) {
Button(
OrganicPrimaryButton(
text = if (selectedCount > 0) {
"Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue"
} else {
stringResource(Res.string.onboarding_tasks_skip)
},
onClick = {
if (selectedTaskIds.isEmpty()) {
onTasksAdded()
@@ -360,32 +353,11 @@ fun OnboardingFirstTaskContent(
}
}
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.lg),
enabled = !isCreatingTasks
) {
if (isCreatingTasks) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
modifier = Modifier.fillMaxWidth(),
enabled = !isCreatingTasks,
isLoading = isCreatingTasks,
icon = Icons.Default.ArrowForward
)
} else {
Text(
text = if (selectedCount > 0) {
"Add $selectedCount Task${if (selectedCount == 1) "" else "s"} & Continue"
} else {
stringResource(Res.string.onboarding_tasks_skip)
},
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Icon(Icons.Default.ArrowForward, contentDescription = null)
}
}
}
}
}
@@ -402,39 +374,32 @@ private fun TaskCategorySection(
) {
val selectedInCategory = category.tasks.count { it.id in selectedTaskIds }
OrganicCard(
modifier = Modifier.fillMaxWidth(),
accentColor = category.color,
showBlob = false
) {
Column(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(AppRadius.lg))
modifier = Modifier.fillMaxWidth()
) {
// Header
Surface(
Row(
modifier = Modifier
.fillMaxWidth()
.clickable { onToggleExpand() },
color = MaterialTheme.colorScheme.surfaceVariant
) {
Row(
modifier = Modifier.padding(AppSpacing.md),
.clickable { onToggleExpand() }
.padding(OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically
) {
// Category icon
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(category.color),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = category.icon,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(24.dp)
OrganicIconContainer(
icon = category.icon,
size = 44.dp,
iconSize = 24.dp,
gradientColors = listOf(category.color),
contentDescription = null
)
}
Spacer(modifier = Modifier.width(AppSpacing.md))
Spacer(modifier = Modifier.width(OrganicSpacing.md))
Text(
text = category.name,
@@ -459,7 +424,7 @@ private fun TaskCategorySection(
color = Color.White
)
}
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
}
Icon(
@@ -468,7 +433,6 @@ private fun TaskCategorySection(
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
// Expanded content
AnimatedVisibility(
@@ -477,9 +441,7 @@ private fun TaskCategorySection(
exit = fadeOut() + shrinkVertically()
) {
Column(
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.5f))
modifier = Modifier.fillMaxWidth()
) {
category.tasks.forEachIndexed { index, task ->
val isSelected = task.id in selectedTaskIds
@@ -494,7 +456,7 @@ private fun TaskCategorySection(
)
if (index < category.tasks.lastIndex) {
Divider(
OrganicDivider(
modifier = Modifier.padding(start = 60.dp),
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.2f)
)
@@ -503,6 +465,7 @@ private fun TaskCategorySection(
}
}
}
}
}
@Composable
@@ -517,7 +480,7 @@ private fun TaskTemplateRow(
modifier = Modifier
.fillMaxWidth()
.clickable(enabled = !isDisabled) { onClick() }
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm),
.padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
// Checkbox
@@ -541,7 +504,7 @@ private fun TaskTemplateRow(
}
}
Spacer(modifier = Modifier.width(AppSpacing.md))
Spacer(modifier = Modifier.width(OrganicSpacing.md))
Column(modifier = Modifier.weight(1f)) {
Text(

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -11,14 +9,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -48,10 +44,13 @@ fun OnboardingJoinResidenceContent(
val isLoading = joinState is ApiResult.Loading
val isCodeValid = shareCode.length == 6
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = AppSpacing.xl),
.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
@@ -59,28 +58,20 @@ fun OnboardingJoinResidenceContent(
// Header
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
// Icon
Box(
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.GroupAdd,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = MaterialTheme.colorScheme.primary
OrganicIconContainer(
icon = Icons.Default.GroupAdd,
size = 100.dp,
iconSize = 50.dp,
contentDescription = null
)
}
// Title and subtitle
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Text(
text = stringResource(Res.string.onboarding_join_title),
@@ -98,7 +89,7 @@ fun OnboardingJoinResidenceContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Share code input
OutlinedTextField(
@@ -123,7 +114,7 @@ fun OnboardingJoinResidenceContent(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
singleLine = true,
keyboardOptions = KeyboardOptions(
capitalization = KeyboardCapitalization.Characters
@@ -133,17 +124,15 @@ fun OnboardingJoinResidenceContent(
// Error message
if (localErrorMessage != null) {
Spacer(modifier = Modifier.height(AppSpacing.md))
Card(
Spacer(modifier = Modifier.height(OrganicSpacing.md))
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(AppRadius.md)
accentColor = MaterialTheme.colorScheme.error,
showBlob = false
) {
Row(
modifier = Modifier.padding(AppSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
modifier = Modifier.padding(OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -162,9 +151,9 @@ fun OnboardingJoinResidenceContent(
// Loading indicator
if (isLoading) {
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
@@ -182,29 +171,15 @@ fun OnboardingJoinResidenceContent(
Spacer(modifier = Modifier.weight(1f))
// Join button
Button(
onClick = { viewModel.joinResidence(shareCode) },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
enabled = isCodeValid && !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Text(
OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_join_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
onClick = { viewModel.joinResidence(shareCode) },
modifier = Modifier.fillMaxWidth(),
enabled = isCodeValid && !isLoading,
isLoading = isLoading
)
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
}

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowForward
@@ -11,14 +9,10 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -31,10 +25,13 @@ fun OnboardingNameResidenceContent(
val residenceName by viewModel.residenceName.collectAsState()
var localName by remember { mutableStateOf(residenceName) }
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = AppSpacing.xl),
.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
@@ -42,40 +39,20 @@ fun OnboardingNameResidenceContent(
// Header with icon
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
// Icon with gradient background
Box(
modifier = Modifier
.size(100.dp)
.shadow(
elevation = 16.dp,
shape = CircleShape,
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.4f)
// Icon with OrganicIconContainer
OrganicIconContainer(
icon = Icons.Default.Home,
size = 100.dp,
iconSize = 50.dp,
contentDescription = null
)
.clip(CircleShape)
.background(
Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.primary,
MaterialTheme.colorScheme.primary.copy(alpha = 0.8f)
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = MaterialTheme.colorScheme.onPrimary
)
}
// Title and subtitle
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Text(
text = stringResource(Res.string.onboarding_name_residence_title),
@@ -93,7 +70,7 @@ fun OnboardingNameResidenceContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Name input
OutlinedTextField(
@@ -106,7 +83,7 @@ fun OnboardingNameResidenceContent(
)
},
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
singleLine = true,
colors = OutlinedTextFieldDefaults.colors(
focusedBorderColor = MaterialTheme.colorScheme.primary,
@@ -114,7 +91,7 @@ fun OnboardingNameResidenceContent(
)
)
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_name_residence_hint),
@@ -125,26 +102,18 @@ fun OnboardingNameResidenceContent(
Spacer(modifier = Modifier.weight(1f))
// Continue button
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_continue),
onClick = {
viewModel.setResidenceName(localName)
onContinue()
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
enabled = localName.isNotBlank()
) {
Text(
text = stringResource(Res.string.onboarding_continue),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
modifier = Modifier.fillMaxWidth(),
enabled = localName.isNotBlank(),
icon = Icons.Default.ArrowForward
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Icon(Icons.Default.ArrowForward, contentDescription = null)
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
}

View File

@@ -14,7 +14,7 @@ import androidx.compose.ui.draw.clip
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.OnboardingStep
import com.example.casera.viewmodel.OnboardingViewModel
import com.example.casera.viewmodel.OnboardingIntent
@@ -189,7 +189,7 @@ private fun OnboardingNavigationBar(
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.md),
.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.md),
verticalAlignment = Alignment.CenterVertically
) {
// Back button
@@ -240,7 +240,7 @@ fun OnboardingProgressIndicator(
totalSteps: Int
) {
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalAlignment = Alignment.CenterVertically
) {
repeat(totalSteps) { index ->

View File

@@ -23,8 +23,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -117,16 +116,19 @@ fun OnboardingSubscriptionContent(
)
)
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.verticalScroll(rememberScrollState())
) {
Column(
modifier = Modifier.padding(horizontal = AppSpacing.xl),
modifier = Modifier.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Crown header with animation
Box(
@@ -149,38 +151,27 @@ fun OnboardingSubscriptionContent(
)
)
// Crown icon
Box(
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(
colors = listOf(
// Crown icon using OrganicIconContainer
OrganicIconContainer(
icon = Icons.Default.EmojiEvents,
size = 100.dp,
iconSize = 50.dp,
gradientColors = listOf(
MaterialTheme.colorScheme.tertiary,
Color(0xFFFF9500)
)
)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.EmojiEvents,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = Color.White
contentDescription = null
)
}
}
// PRO badge
Surface(
shape = RoundedCornerShape(AppRadius.full),
shape = RoundedCornerShape(OrganicRadius.full),
color = MaterialTheme.colorScheme.tertiary.copy(alpha = 0.15f)
) {
Row(
modifier = Modifier.padding(horizontal = AppSpacing.lg, vertical = AppSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs),
modifier = Modifier.padding(horizontal = OrganicSpacing.lg, vertical = OrganicSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -204,7 +195,7 @@ fun OnboardingSubscriptionContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Text(
text = stringResource(Res.string.onboarding_subscription_subtitle),
@@ -214,11 +205,11 @@ fun OnboardingSubscriptionContent(
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Social proof
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.xs),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.xs),
verticalAlignment = Alignment.CenterVertically
) {
repeat(5) {
@@ -236,18 +227,18 @@ fun OnboardingSubscriptionContent(
)
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Benefits list
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
benefits.forEach { benefit ->
BenefitRow(benefit = benefit)
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Plan selection
Text(
@@ -257,7 +248,7 @@ fun OnboardingSubscriptionContent(
color = MaterialTheme.colorScheme.onBackground
)
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
// Yearly plan
PlanCard(
@@ -266,7 +257,7 @@ fun OnboardingSubscriptionContent(
onClick = { selectedPlan = SubscriptionPlan.YEARLY }
)
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Monthly plan
PlanCard(
@@ -275,42 +266,23 @@ fun OnboardingSubscriptionContent(
onClick = { selectedPlan = SubscriptionPlan.MONTHLY }
)
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Start trial button
Button(
OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_subscription_start_trial),
onClick = {
isLoading = true
// Simulate subscription flow
onSubscribe()
},
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.lg),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.tertiary
),
enabled = !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onTertiary,
strokeWidth = 2.dp
modifier = Modifier.fillMaxWidth(),
enabled = !isLoading,
isLoading = isLoading,
icon = Icons.Default.ArrowForward
)
} else {
Text(
text = stringResource(Res.string.onboarding_subscription_start_trial),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Icon(Icons.Default.ArrowForward, contentDescription = null)
}
}
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
// Continue free button
TextButton(onClick = onSkip) {
@@ -322,7 +294,7 @@ fun OnboardingSubscriptionContent(
)
}
Spacer(modifier = Modifier.height(AppSpacing.sm))
Spacer(modifier = Modifier.height(OrganicSpacing.sm))
// Legal text
Text(
@@ -337,7 +309,8 @@ fun OnboardingSubscriptionContent(
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.7f)
)
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
}
}
@@ -347,28 +320,19 @@ private fun BenefitRow(benefit: SubscriptionBenefit) {
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.md, vertical = AppSpacing.sm),
.padding(horizontal = OrganicSpacing.md, vertical = OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
// Gradient icon
Box(
modifier = Modifier
.size(44.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(benefit.gradientColors)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = benefit.icon,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = Color.White
// Gradient icon using OrganicIconContainer
OrganicIconContainer(
icon = benefit.icon,
size = 44.dp,
iconSize = 24.dp,
gradientColors = benefit.gradientColors,
contentDescription = null
)
}
Spacer(modifier = Modifier.width(AppSpacing.md))
Spacer(modifier = Modifier.width(OrganicSpacing.md))
Column(modifier = Modifier.weight(1f)) {
Text(
@@ -402,26 +366,15 @@ private fun PlanCard(
) {
val isYearly = plan == SubscriptionPlan.YEARLY
Surface(
OrganicCard(
modifier = Modifier
.fillMaxWidth()
.clickable { onClick() },
shape = RoundedCornerShape(AppRadius.lg),
color = MaterialTheme.colorScheme.surfaceVariant,
border = if (isSelected) {
ButtonDefaults.outlinedButtonBorder.copy(
brush = Brush.linearGradient(
colors = listOf(
MaterialTheme.colorScheme.tertiary,
Color(0xFFFF9500)
)
),
width = 2.dp
)
} else null
accentColor = if (isSelected) MaterialTheme.colorScheme.tertiary else MaterialTheme.colorScheme.surfaceVariant,
showBlob = isSelected
) {
Row(
modifier = Modifier.padding(AppSpacing.lg),
modifier = Modifier.padding(OrganicSpacing.lg),
verticalAlignment = Alignment.CenterVertically
) {
// Selection indicator
@@ -446,11 +399,11 @@ private fun PlanCard(
}
}
Spacer(modifier = Modifier.width(AppSpacing.md))
Spacer(modifier = Modifier.width(OrganicSpacing.md))
Column(modifier = Modifier.weight(1f)) {
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Text(
@@ -466,7 +419,7 @@ private fun PlanCard(
if (isYearly) {
Surface(
shape = RoundedCornerShape(AppRadius.full),
shape = RoundedCornerShape(OrganicRadius.full),
color = Color(0xFF34C759)
) {
Text(
@@ -474,7 +427,7 @@ private fun PlanCard(
style = MaterialTheme.typography.labelSmall,
fontWeight = FontWeight.Bold,
color = Color.White,
modifier = Modifier.padding(horizontal = AppSpacing.sm, vertical = 2.dp)
modifier = Modifier.padding(horizontal = OrganicSpacing.sm, vertical = 2.dp)
)
}
}

View File

@@ -21,8 +21,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import casera.composeapp.generated.resources.*
import kotlinx.coroutines.delay
import org.jetbrains.compose.resources.stringResource
@@ -111,10 +110,13 @@ fun OnboardingValuePropsContent(
}
}
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = AppSpacing.xl),
.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(0.5f))
@@ -129,11 +131,11 @@ fun OnboardingValuePropsContent(
FeatureCard(feature = features[page])
}
Spacer(modifier = Modifier.height(AppSpacing.xl))
Spacer(modifier = Modifier.height(OrganicSpacing.xl))
// Page indicators
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
repeat(features.size) { index ->
@@ -155,23 +157,15 @@ fun OnboardingValuePropsContent(
Spacer(modifier = Modifier.weight(1f))
// Continue button
Button(
onClick = onContinue,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md)
) {
Text(
OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_continue),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
onClick = onContinue,
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.ArrowForward
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Icon(Icons.Default.ArrowForward, contentDescription = null)
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
}
@@ -180,29 +174,20 @@ private fun FeatureCard(feature: FeatureItem) {
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = AppSpacing.md),
.padding(horizontal = OrganicSpacing.md),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
// Icon with gradient background
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(
Brush.linearGradient(feature.gradientColors)
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = feature.icon,
contentDescription = null,
modifier = Modifier.size(60.dp),
tint = Color.White
// Icon with gradient background using OrganicIconContainer
OrganicIconContainer(
icon = feature.icon,
size = 120.dp,
iconSize = 60.dp,
gradientColors = feature.gradientColors,
contentDescription = null
)
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Title
Text(
@@ -213,7 +198,7 @@ private fun FeatureCard(feature: FeatureItem) {
textAlign = TextAlign.Center
)
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
// Description
Text(

View File

@@ -1,8 +1,6 @@
package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
@@ -11,15 +9,12 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.example.casera.network.ApiResult
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.OnboardingViewModel
import casera.composeapp.generated.resources.*
import org.jetbrains.compose.resources.stringResource
@@ -56,10 +51,13 @@ fun OnboardingVerifyEmailContent(
val isLoading = verifyState is ApiResult.Loading
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = AppSpacing.xl),
.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally
) {
Spacer(modifier = Modifier.weight(1f))
@@ -67,28 +65,20 @@ fun OnboardingVerifyEmailContent(
// Header
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.lg)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.lg)
) {
// Icon with gradient background
Box(
modifier = Modifier
.size(100.dp)
.clip(CircleShape)
.background(MaterialTheme.colorScheme.primary.copy(alpha = 0.1f)),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = Icons.Default.MarkEmailRead,
contentDescription = null,
modifier = Modifier.size(50.dp),
tint = MaterialTheme.colorScheme.primary
// Icon with OrganicIconContainer
OrganicIconContainer(
icon = Icons.Default.MarkEmailRead,
size = 100.dp,
iconSize = 50.dp,
contentDescription = null
)
}
// Title and subtitle
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Text(
text = stringResource(Res.string.onboarding_verify_email_title),
@@ -106,7 +96,7 @@ fun OnboardingVerifyEmailContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
// Code input
OutlinedTextField(
@@ -133,7 +123,7 @@ fun OnboardingVerifyEmailContent(
textAlign = TextAlign.Center,
fontWeight = FontWeight.Bold
),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
enabled = !isLoading
@@ -141,17 +131,15 @@ fun OnboardingVerifyEmailContent(
// Error message
if (localErrorMessage != null) {
Spacer(modifier = Modifier.height(AppSpacing.md))
Card(
Spacer(modifier = Modifier.height(OrganicSpacing.md))
OrganicCard(
modifier = Modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
shape = RoundedCornerShape(AppRadius.md)
accentColor = MaterialTheme.colorScheme.error,
showBlob = false
) {
Row(
modifier = Modifier.padding(AppSpacing.md),
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
modifier = Modifier.padding(OrganicSpacing.md),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
Icon(
@@ -170,9 +158,9 @@ fun OnboardingVerifyEmailContent(
// Loading indicator
if (isLoading) {
Spacer(modifier = Modifier.height(AppSpacing.md))
Spacer(modifier = Modifier.height(OrganicSpacing.md))
Row(
horizontalArrangement = Arrangement.spacedBy(AppSpacing.sm),
horizontalArrangement = Arrangement.spacedBy(OrganicSpacing.sm),
verticalAlignment = Alignment.CenterVertically
) {
CircularProgressIndicator(
@@ -187,7 +175,7 @@ fun OnboardingVerifyEmailContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.lg))
Spacer(modifier = Modifier.height(OrganicSpacing.lg))
// Hint text
Text(
@@ -200,29 +188,15 @@ fun OnboardingVerifyEmailContent(
Spacer(modifier = Modifier.weight(1f))
// Verify button
Button(
onClick = { viewModel.verifyEmail(code) },
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
enabled = code.length == 6 && !isLoading
) {
if (isLoading) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Text(
OrganicPrimaryButton(
text = stringResource(Res.string.auth_verify_button),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
onClick = { viewModel.verifyEmail(code) },
modifier = Modifier.fillMaxWidth(),
enabled = code.length == 6 && !isLoading,
isLoading = isLoading
)
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
}

View File

@@ -1,7 +1,6 @@
package com.example.casera.ui.screens.onboarding
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
@@ -11,16 +10,11 @@ import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.example.casera.ui.theme.AppRadius
import com.example.casera.ui.theme.AppSpacing
import com.example.casera.ui.theme.*
import com.example.casera.viewmodel.AuthViewModel
import com.example.casera.network.ApiResult
import casera.composeapp.generated.resources.*
@@ -35,10 +29,13 @@ fun OnboardingWelcomeContent(
) {
var showLoginDialog by remember { mutableStateOf(false) }
WarmGradientBackground(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = AppSpacing.xl),
.padding(horizontal = OrganicSpacing.xl),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.Center
) {
@@ -47,34 +44,20 @@ fun OnboardingWelcomeContent(
// Hero section
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.xl)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.xl)
) {
// App icon with shadow
Box(
modifier = Modifier
.size(120.dp)
.shadow(
elevation = 20.dp,
shape = RoundedCornerShape(AppRadius.xxl),
spotColor = MaterialTheme.colorScheme.primary.copy(alpha = 0.3f)
// App icon with OrganicIconContainer
OrganicIconContainer(
icon = Icons.Default.Home,
size = 120.dp,
iconSize = 80.dp,
contentDescription = null
)
.clip(RoundedCornerShape(AppRadius.xxl))
.background(MaterialTheme.colorScheme.surface)
) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null,
modifier = Modifier
.size(80.dp)
.align(Alignment.Center),
tint = MaterialTheme.colorScheme.primary
)
}
// Welcome text
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(AppSpacing.sm)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.sm)
) {
Text(
text = stringResource(Res.string.onboarding_welcome_title),
@@ -97,31 +80,15 @@ fun OnboardingWelcomeContent(
// Action buttons
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
// Primary CTA - Start Fresh
Button(
onClick = onStartFresh,
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primary
)
) {
Icon(
imageVector = Icons.Default.Home,
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Text(
OrganicPrimaryButton(
text = stringResource(Res.string.onboarding_start_fresh),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold
onClick = onStartFresh,
modifier = Modifier.fillMaxWidth(),
icon = Icons.Default.Home
)
}
// Secondary CTA - Join Existing
OutlinedButton(
@@ -129,7 +96,7 @@ fun OnboardingWelcomeContent(
modifier = Modifier
.fillMaxWidth()
.height(56.dp),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
colors = ButtonDefaults.outlinedButtonColors(
contentColor = MaterialTheme.colorScheme.primary
)
@@ -139,7 +106,7 @@ fun OnboardingWelcomeContent(
contentDescription = null,
modifier = Modifier.size(24.dp)
)
Spacer(modifier = Modifier.width(AppSpacing.sm))
Spacer(modifier = Modifier.width(OrganicSpacing.sm))
Text(
text = stringResource(Res.string.onboarding_join_existing),
style = MaterialTheme.typography.titleMedium,
@@ -160,7 +127,8 @@ fun OnboardingWelcomeContent(
}
}
Spacer(modifier = Modifier.height(AppSpacing.xl * 2))
Spacer(modifier = Modifier.height(OrganicSpacing.xl * 2))
}
}
// Login dialog
@@ -212,7 +180,7 @@ private fun LoginDialog(
},
text = {
Column(
verticalArrangement = Arrangement.spacedBy(AppSpacing.md)
verticalArrangement = Arrangement.spacedBy(OrganicSpacing.md)
) {
OutlinedTextField(
value = username,
@@ -220,7 +188,7 @@ private fun LoginDialog(
label = { Text(stringResource(Res.string.auth_login_username_label)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
enabled = !isLoading
)
@@ -230,7 +198,7 @@ private fun LoginDialog(
label = { Text(stringResource(Res.string.auth_login_password_label)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
shape = RoundedCornerShape(AppRadius.md),
shape = RoundedCornerShape(OrganicRadius.md),
visualTransformation = androidx.compose.ui.text.input.PasswordVisualTransformation(),
enabled = !isLoading
)

View File

@@ -0,0 +1,745 @@
package com.example.casera.ui.theme
import androidx.compose.animation.core.*
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Eco
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.blur
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.draw.shadow
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.*
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.clipRect
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.random.Random
// MARK: - Organic Design System
// Warm, natural aesthetic with soft shapes, subtle textures, and flowing layouts
// MARK: - Organic Shapes
/**
* Soft organic blob shape for backgrounds
* Matches iOS OrganicBlobShape
*/
class OrganicBlobShape(private val variation: Int = 0) : Shape {
override fun createOutline(
size: Size,
layoutDirection: androidx.compose.ui.unit.LayoutDirection,
density: androidx.compose.ui.unit.Density
): Outline {
val path = Path()
val w = size.width
val h = size.height
when (variation % 3) {
0 -> {
// Soft cloud-like blob
path.moveTo(w * 0.1f, h * 0.5f)
path.cubicTo(
w * 0.0f, h * 0.1f,
w * 0.25f, h * 0.0f,
w * 0.5f, h * 0.05f
)
path.cubicTo(
w * 0.75f, h * 0.1f,
w * 1.0f, h * 0.25f,
w * 0.95f, h * 0.45f
)
path.cubicTo(
w * 0.9f, h * 0.7f,
w * 0.8f, h * 0.95f,
w * 0.55f, h * 0.95f
)
path.cubicTo(
w * 0.25f, h * 0.95f,
w * 0.05f, h * 0.75f,
w * 0.1f, h * 0.5f
)
}
1 -> {
// Pebble shape
path.moveTo(w * 0.15f, h * 0.4f)
path.cubicTo(
w * 0.1f, h * 0.15f,
w * 0.35f, h * 0.05f,
w * 0.6f, h * 0.08f
)
path.cubicTo(
w * 0.85f, h * 0.12f,
w * 0.95f, h * 0.35f,
w * 0.9f, h * 0.55f
)
path.cubicTo(
w * 0.85f, h * 0.8f,
w * 0.65f, h * 0.95f,
w * 0.45f, h * 0.92f
)
path.cubicTo(
w * 0.2f, h * 0.88f,
w * 0.08f, h * 0.65f,
w * 0.15f, h * 0.4f
)
}
else -> {
// Leaf-like shape
path.moveTo(w * 0.05f, h * 0.5f)
path.cubicTo(
w * 0.05f, h * 0.2f,
w * 0.25f, h * 0.02f,
w * 0.5f, h * 0.02f
)
path.cubicTo(
w * 0.75f, h * 0.02f,
w * 0.95f, h * 0.2f,
w * 0.95f, h * 0.5f
)
path.cubicTo(
w * 0.95f, h * 0.8f,
w * 0.75f, h * 0.98f,
w * 0.5f, h * 0.98f
)
path.cubicTo(
w * 0.25f, h * 0.98f,
w * 0.05f, h * 0.8f,
w * 0.05f, h * 0.5f
)
}
}
path.close()
return Outline.Generic(path)
}
}
/**
* Super soft rounded rectangle for organic cards
* Uses continuous corner radius matching iOS .continuous style
*/
val OrganicRoundedShape = RoundedCornerShape(28.dp)
// MARK: - Grain Texture Overlay
/**
* Grain texture overlay for natural feel
* Matches iOS GrainTexture
*/
@Composable
fun GrainTexture(
modifier: Modifier = Modifier,
opacity: Float = 0.03f
) {
val grainColor = Color.Black.copy(alpha = opacity)
Canvas(modifier = modifier.fillMaxSize()) {
val density = size.width * size.height / 50
val random = Random(42) // Fixed seed for consistent pattern
repeat(density.toInt().coerceAtMost(5000)) {
val x = random.nextFloat() * size.width
val y = random.nextFloat() * size.height
val grainOpacity = random.nextFloat() * 0.7f + 0.3f
drawCircle(
color = grainColor.copy(alpha = grainColor.alpha * grainOpacity),
radius = 0.5f,
center = Offset(x, y)
)
}
}
}
// MARK: - Organic Card Background
/**
* Organic card background with subtle accent blob and grain texture
* Matches iOS OrganicCardBackground
*/
@Composable
fun OrganicCardBackground(
modifier: Modifier = Modifier,
accentColor: Color = MaterialTheme.colorScheme.primary,
showBlob: Boolean = true,
blobVariation: Int = 0
) {
Box(modifier = modifier) {
// Main card fill
Box(
modifier = Modifier
.fillMaxSize()
.clip(OrganicRoundedShape)
.background(MaterialTheme.colorScheme.backgroundSecondary)
)
// Subtle accent blob in corner
if (showBlob) {
Box(
modifier = Modifier
.fillMaxSize()
.clip(OrganicRoundedShape)
) {
Box(
modifier = Modifier
.fillMaxWidth(0.6f)
.fillMaxHeight(0.7f)
.offset(x = 80.dp, y = (-20).dp)
.align(Alignment.TopEnd)
.clip(OrganicBlobShape(blobVariation))
.background(
brush = Brush.linearGradient(
colors = listOf(
accentColor.copy(alpha = 0.08f),
accentColor.copy(alpha = 0.02f)
)
)
)
.blur(20.dp)
)
}
}
// Grain texture
Box(
modifier = Modifier
.fillMaxSize()
.clip(OrganicRoundedShape)
) {
GrainTexture(opacity = 0.015f)
}
}
}
// MARK: - Natural Shadow
/**
* Shadow intensity levels matching iOS NaturalShadow
*/
enum class ShadowIntensity {
Subtle,
Medium,
Pronounced;
val elevation: Dp
get() = when (this) {
Subtle -> 4.dp
Medium -> 8.dp
Pronounced -> 12.dp
}
val ambientAlpha: Float
get() = when (this) {
Subtle -> 0.04f
Medium -> 0.08f
Pronounced -> 0.12f
}
}
/**
* Natural shadow modifier matching iOS NaturalShadow
*/
fun Modifier.naturalShadow(
intensity: ShadowIntensity = ShadowIntensity.Medium,
shape: Shape = OrganicRoundedShape
): Modifier = this
.shadow(
elevation = intensity.elevation,
shape = shape,
ambientColor = Color.Black.copy(alpha = intensity.ambientAlpha),
spotColor = Color.Black.copy(alpha = intensity.ambientAlpha * 0.5f)
)
// MARK: - Organic Icon Container
/**
* Icon with soft organic background and inner glow
* Matches iOS OrganicIconContainer
*
* @param icon The icon to display (ImageVector)
* @param modifier Modifier for the container
* @param size The size of the container (default 48.dp)
* @param iconScale Scale of the icon relative to container (default 0.5f)
* @param iconSize Optional explicit icon size (overrides iconScale if provided)
* @param backgroundColor Background color for the container
* @param iconColor Tint color for the icon
* @param containerColor Alias for backgroundColor (for compatibility)
* @param iconTint Alias for iconColor (for compatibility)
* @param gradientColors Optional custom gradient colors (overrides backgroundColor)
* @param contentDescription Accessibility description
*/
@Composable
fun OrganicIconContainer(
icon: ImageVector,
modifier: Modifier = Modifier,
size: Dp = 48.dp,
iconScale: Float = 0.5f,
iconSize: Dp? = null,
backgroundColor: Color? = null,
iconColor: Color? = null,
containerColor: Color? = null,
iconTint: Color? = null,
gradientColors: List<Color>? = null,
contentDescription: String? = null
) {
// Resolve colors with fallback chain: explicit param -> alias -> default
val resolvedBackgroundColor = backgroundColor ?: containerColor ?: MaterialTheme.colorScheme.primary
val resolvedIconColor = iconColor ?: iconTint ?: MaterialTheme.colorScheme.onPrimary
val actualGradientColors = gradientColors ?: listOf(
resolvedBackgroundColor,
resolvedBackgroundColor.copy(alpha = 0.85f)
)
val actualIconSize = iconSize ?: (size * iconScale)
Box(
modifier = modifier
.size(size)
.naturalShadow(ShadowIntensity.Subtle, CircleShape),
contentAlignment = Alignment.Center
) {
// Soft organic background with radial gradient
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
brush = Brush.radialGradient(
colors = actualGradientColors,
center = Offset(this.size.width * 0.3f, this.size.height * 0.3f),
radius = this.size.maxDimension
)
)
}
// Inner glow
Canvas(modifier = Modifier.fillMaxSize()) {
drawCircle(
brush = Brush.radialGradient(
colors = listOf(
Color.White.copy(alpha = 0.2f),
Color.Transparent
),
center = Offset(this.size.width * 0.3f, this.size.height * 0.3f),
radius = this.size.maxDimension * 0.5f
)
)
}
// Icon
Icon(
imageVector = icon,
contentDescription = contentDescription,
modifier = Modifier.size(actualIconSize),
tint = resolvedIconColor
)
}
}
// MARK: - Organic Stat Pill
/**
* Stat display pill with icon and label
* Matches iOS OrganicStatPill
*/
@Composable
fun OrganicStatPill(
icon: ImageVector,
value: String,
label: String,
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.primary
) {
Row(
modifier = modifier
.background(
color = MaterialTheme.colorScheme.backgroundSecondary,
shape = RoundedCornerShape(50)
)
.drawBehind {
// Border
drawRoundRect(
color = color.copy(alpha = 0.15f),
style = androidx.compose.ui.graphics.drawscope.Stroke(width = 1.dp.toPx()),
cornerRadius = androidx.compose.ui.geometry.CornerRadius(50.dp.toPx())
)
}
.padding(horizontal = 14.dp, vertical = 10.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
// Icon with soft background
Box(
modifier = Modifier
.size(32.dp)
.background(
color = color.copy(alpha = 0.12f),
shape = CircleShape
),
contentAlignment = Alignment.Center
) {
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(13.dp),
tint = color
)
}
Column(verticalArrangement = Arrangement.spacedBy(1.dp)) {
Text(
text = value,
style = MaterialTheme.typography.labelLarge.copy(
fontSize = 15.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Bold
),
color = MaterialTheme.colorScheme.textPrimary
)
Text(
text = label,
style = MaterialTheme.typography.labelSmall.copy(
fontSize = 11.sp,
fontWeight = androidx.compose.ui.text.font.FontWeight.Medium
),
color = MaterialTheme.colorScheme.textSecondary
)
}
}
}
// MARK: - Organic Divider
/**
* Gradient divider that fades at edges
* Matches iOS OrganicDivider
*/
@Composable
fun OrganicDivider(
modifier: Modifier = Modifier,
color: Color = MaterialTheme.colorScheme.textSecondary.copy(alpha = 0.15f),
height: Dp = 1.dp,
horizontalPadding: Dp = 0.dp,
vertical: Boolean = false
) {
if (vertical) {
Box(
modifier = modifier
.width(1.dp)
.background(
brush = Brush.verticalGradient(
colors = listOf(
color.copy(alpha = 0f),
color,
color,
color.copy(alpha = 0f)
)
)
)
)
} else {
Box(
modifier = modifier
.padding(horizontal = horizontalPadding)
.fillMaxWidth()
.height(height)
.background(
brush = Brush.horizontalGradient(
colors = listOf(
color.copy(alpha = 0f),
color,
color,
color.copy(alpha = 0f)
)
)
)
)
}
}
// MARK: - Warm Gradient Background
/**
* Screen background with subtle warm gradient and grain
* Matches iOS WarmGradientBackground
*/
@Composable
fun WarmGradientBackground(
modifier: Modifier = Modifier,
content: @Composable BoxScope.() -> Unit = {}
) {
val isDark = isSystemInDarkTheme()
val primaryColor = MaterialTheme.colorScheme.primary
val backgroundColor = MaterialTheme.colorScheme.backgroundPrimary
Box(modifier = modifier.fillMaxSize()) {
// Base background
Box(
modifier = Modifier
.fillMaxSize()
.background(backgroundColor)
)
// Subtle warm gradient overlay
Box(
modifier = Modifier
.fillMaxSize()
.background(
brush = Brush.linearGradient(
colors = listOf(
primaryColor.copy(alpha = if (isDark) 0.05f else 0.03f),
Color.Transparent
),
start = Offset.Zero,
end = Offset.Infinite
)
)
)
// Grain for natural feel
GrainTexture(opacity = 0.02f)
// Content
content()
}
}
// MARK: - Organic Card Modifier
/**
* Organic card styling modifier
* Matches iOS organicCard view modifier
*/
@Composable
fun Modifier.organicCard(
accentColor: Color = MaterialTheme.colorScheme.primary,
showBlob: Boolean = true,
shadowIntensity: ShadowIntensity = ShadowIntensity.Medium,
blobVariation: Int = 0
): Modifier = this
.naturalShadow(shadowIntensity)
.clip(OrganicRoundedShape)
.drawBehind {
// Main fill
drawRoundRect(
color = Color.Transparent, // Background handled by OrganicCardBackground
cornerRadius = androidx.compose.ui.geometry.CornerRadius(28.dp.toPx())
)
}
/**
* Composable wrapper for organic card styling
*/
@Composable
fun OrganicCard(
modifier: Modifier = Modifier,
accentColor: Color = MaterialTheme.colorScheme.primary,
showBlob: Boolean = true,
shadowIntensity: ShadowIntensity = ShadowIntensity.Medium,
blobVariation: Int = 0,
content: @Composable BoxScope.() -> Unit
) {
Box(
modifier = modifier.naturalShadow(shadowIntensity, OrganicRoundedShape)
) {
OrganicCardBackground(
accentColor = accentColor,
showBlob = showBlob,
blobVariation = blobVariation
)
Box(
modifier = Modifier
.fillMaxWidth()
.clip(OrganicRoundedShape),
content = content
)
}
}
// MARK: - Organic Spacing
/**
* Organic spacing constants matching iOS OrganicSpacing
* Provides both semantic names (compact, cozy, etc.) and standard names (xs, sm, etc.)
*/
object OrganicSpacing {
// Semantic names (original)
val compact = 8.dp
val cozy = 20.dp
val comfortable = 24.dp
val spacious = 32.dp
val airy = 40.dp
// Standard size names (for compatibility)
val xs = 4.dp
val sm = 8.dp
val md = 12.dp
val lg = 16.dp
val xl = 24.dp
val xxl = 32.dp
// Additional aliases
val extraSmall = xs
val small = sm
val medium = md
val large = lg
val extraLarge = xl
// Extended aliases (matching iOS naming)
val minimal = 2.dp
val generous = 48.dp
}
/**
* Organic radius constants for rounded corners
* Alias for AppRadius with organic naming
*/
object OrganicRadius {
val xs = 4.dp
val sm = 8.dp
val md = 12.dp
val lg = 16.dp
val xl = 20.dp
val xxl = 24.dp
val full = 50.dp
}
/**
* Organic shapes for consistency
*/
object OrganicShapes {
val extraSmall = RoundedCornerShape(OrganicRadius.xs)
val small = RoundedCornerShape(OrganicRadius.sm)
val medium = RoundedCornerShape(OrganicRadius.md)
val large = RoundedCornerShape(OrganicRadius.lg)
val extraLarge = RoundedCornerShape(OrganicRadius.xl)
}
// MARK: - Floating Leaf Decoration
/**
* Animated floating leaf decoration
* Matches iOS FloatingLeaf
*/
@Composable
fun FloatingLeaf(
modifier: Modifier = Modifier,
delay: Int = 0,
size: Dp = 20.dp,
color: Color = MaterialTheme.colorScheme.primary
) {
val infiniteTransition = rememberInfiniteTransition(label = "leaf")
val rotation by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 15f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 4000,
delayMillis = delay,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
),
label = "leafRotation"
)
val offsetY by infiniteTransition.animateFloat(
initialValue = 0f,
targetValue = 8f,
animationSpec = infiniteRepeatable(
animation = tween(
durationMillis = 4000,
delayMillis = delay,
easing = FastOutSlowInEasing
),
repeatMode = RepeatMode.Reverse
),
label = "leafOffset"
)
Icon(
imageVector = Icons.Default.Eco,
contentDescription = null,
modifier = modifier
.size(size)
.rotate(rotation)
.offset(y = offsetY.dp),
tint = color.copy(alpha = 0.15f)
)
}
// MARK: - Organic Button
/**
* Primary button with organic gradient styling
*/
@Composable
fun OrganicPrimaryButton(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
isLoading: Boolean = false,
icon: ImageVector? = null
) {
val primaryColor = MaterialTheme.colorScheme.primary
androidx.compose.material3.Button(
onClick = onClick,
modifier = modifier
.fillMaxWidth()
.height(56.dp),
enabled = enabled && !isLoading,
shape = RoundedCornerShape(28.dp),
colors = androidx.compose.material3.ButtonDefaults.buttonColors(
containerColor = primaryColor,
contentColor = MaterialTheme.colorScheme.onPrimary,
disabledContainerColor = primaryColor.copy(alpha = 0.5f),
disabledContentColor = MaterialTheme.colorScheme.onPrimary.copy(alpha = 0.7f)
)
) {
if (isLoading) {
androidx.compose.material3.CircularProgressIndicator(
modifier = Modifier.size(24.dp),
color = MaterialTheme.colorScheme.onPrimary,
strokeWidth = 2.dp
)
} else {
Row(
horizontalArrangement = Arrangement.Center,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = text,
style = MaterialTheme.typography.labelLarge.copy(
fontWeight = androidx.compose.ui.text.font.FontWeight.SemiBold
)
)
if (icon != null) {
Spacer(modifier = Modifier.width(8.dp))
Icon(
imageVector = icon,
contentDescription = null,
modifier = Modifier.size(20.dp)
)
}
}
}
}
}

View File

@@ -0,0 +1,6 @@
{
"enabledMcpjsonServers": [
"ios-simulator"
],
"enableAllProjectMcpServers": true
}