diff --git a/app/app/src/main/AndroidManifest.xml b/app/app/src/main/AndroidManifest.xml index 1949239..30459fc 100644 --- a/app/app/src/main/AndroidManifest.xml +++ b/app/app/src/main/AndroidManifest.xml @@ -20,10 +20,11 @@ android:theme="@style/Theme.Rachita" tools:targetApi="34"> + diff --git a/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/DashboardScreen.kt b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/DashboardScreen.kt index 96ed496..3271c12 100644 --- a/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/DashboardScreen.kt +++ b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/DashboardScreen.kt @@ -3,6 +3,7 @@ package mx.paputec.rachita.ui.dashboard import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.WindowInsets @@ -35,6 +36,7 @@ import mx.paputec.rachita.domain.model.BatteryStatus import mx.paputec.rachita.domain.model.HabitIcon import mx.paputec.rachita.domain.model.HabitPalette import mx.paputec.rachita.ui.dashboard.components.ConfettiOverlay +import mx.paputec.rachita.ui.dashboard.components.DashboardCompact import mx.paputec.rachita.ui.dashboard.components.HabitCard import mx.paputec.rachita.ui.dashboard.components.RightColumn import mx.paputec.rachita.ui.dashboard.components.Sidebar @@ -47,6 +49,10 @@ import mx.paputec.rachita.ui.theme.SuccessGreen import mx.paputec.rachita.ui.theme.WarmGreyDeep // Module 2 — dashboard root (PRD §5.2): sidebar 232 dp · center flex · right 296 dp. +// Width breakpoint: below this the single-column phone layout is used; at or +// above it, the PRD §5.2 three-column tablet layout. +private val COMPACT_MAX_WIDTH = 600.dp + @Composable fun DashboardScreen( uiState: DashboardUiState, @@ -58,36 +64,19 @@ fun DashboardScreen( .background(CreamBackground) .padding(WindowInsets.safeDrawing.asPaddingValues()), ) { - Row( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 18.dp, vertical = 14.dp), - horizontalArrangement = Arrangement.spacedBy(18.dp), - ) { - Sidebar( - avatar = uiState.avatar, - childName = uiState.childName, - level = uiState.level, - xpInLevel = uiState.xpInLevel, - xpTarget = uiState.xpTarget, - streak = uiState.streak, - ) - CenterColumn( - uiState = uiState, - onToggleHabit = onToggleHabit, - modifier = Modifier.weight(1f), - ) - RightColumn( - streak = uiState.streak, - week = uiState.week, - streakSecuredToday = uiState.allDone, - mascotMessage = if (uiState.allDone) { - stringResource(R.string.dashboard_mascot_celebrate, uiState.childName) - } else { - stringResource(R.string.dashboard_mascot_normal) - }, - avatar = uiState.avatar, - ) + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + if (maxWidth < COMPACT_MAX_WIDTH) { + DashboardCompact( + uiState = uiState, + onToggleHabit = onToggleHabit, + header = { DashboardHeader(uiState = uiState) }, + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp, vertical = 12.dp), + ) + } else { + DashboardTablet(uiState = uiState, onToggleHabit = onToggleHabit) + } } // Confetti above everything (DA-04); confettiOn toggle arrives with Settings (DA-05). @@ -96,15 +85,49 @@ fun DashboardScreen( } @Composable -private fun CenterColumn( +private fun DashboardTablet( uiState: DashboardUiState, onToggleHabit: (String) -> Unit, - modifier: Modifier = Modifier, ) { - // Header laid out as a plain top-anchored column with height-bounded rows. - // On the EMUI test device the previous Bottom-aligned SpaceBetween header - // measured ~270 dp tall (giant invisible text box), crushing the habit list - // to zero height — see STATUS.md session 2. + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 18.dp, vertical = 14.dp), + horizontalArrangement = Arrangement.spacedBy(18.dp), + ) { + Sidebar( + avatar = uiState.avatar, + childName = uiState.childName, + level = uiState.level, + xpInLevel = uiState.xpInLevel, + xpTarget = uiState.xpTarget, + streak = uiState.streak, + ) + CenterColumn( + uiState = uiState, + onToggleHabit = onToggleHabit, + modifier = Modifier.weight(1f), + ) + RightColumn( + streak = uiState.streak, + week = uiState.week, + streakSecuredToday = uiState.allDone, + mascotMessage = if (uiState.allDone) { + stringResource(R.string.dashboard_mascot_celebrate, uiState.childName) + } else { + stringResource(R.string.dashboard_mascot_normal) + }, + avatar = uiState.avatar, + ) + } +} + +// Header shared by both layouts. Laid out as a plain top-anchored column with +// height-bounded rows: on the EMUI test device a Bottom-aligned SpaceBetween +// header measured ~270 dp tall (giant invisible text box), crushing the habit +// list to zero height — see STATUS.md session 2. +@Composable +private fun DashboardHeader(uiState: DashboardUiState, modifier: Modifier = Modifier) { val pendingText = when (uiState.pendingCount) { 0 -> stringResource(R.string.dashboard_pending_zero) 1 -> stringResource(R.string.dashboard_pending_one) @@ -131,6 +154,17 @@ private fun CenterColumn( uiState.battery?.let { BatteryChip(it) } TodayCounterPill(done = uiState.doneCount, total = uiState.totalCount) } + } +} + +@Composable +private fun CenterColumn( + uiState: DashboardUiState, + onToggleHabit: (String) -> Unit, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(10.dp)) { + DashboardHeader(uiState = uiState) LazyColumn( modifier = Modifier diff --git a/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/DashboardCompact.kt b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/DashboardCompact.kt new file mode 100644 index 0000000..05a587b --- /dev/null +++ b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/DashboardCompact.kt @@ -0,0 +1,226 @@ +package mx.paputec.rachita.ui.dashboard.components + +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.fadeIn +import androidx.compose.animation.fadeOut +import androidx.compose.animation.slideInVertically +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import mx.paputec.rachita.R +import mx.paputec.rachita.domain.model.AvatarKind +import mx.paputec.rachita.domain.model.BatteryStatus +import mx.paputec.rachita.domain.model.HabitIcon +import mx.paputec.rachita.domain.model.HabitPalette +import mx.paputec.rachita.ui.dashboard.DashboardUiState +import mx.paputec.rachita.ui.dashboard.HabitCardUi +import mx.paputec.rachita.ui.dashboard.WeekDayUi +import mx.paputec.rachita.ui.onboarding.components.Mascot +import mx.paputec.rachita.ui.theme.CoralIntense +import mx.paputec.rachita.ui.theme.CreamCardSoft +import mx.paputec.rachita.ui.theme.RachitaTheme +import mx.paputec.rachita.ui.theme.SuccessGreen +import mx.paputec.rachita.ui.theme.Sun +import mx.paputec.rachita.ui.theme.WarmGreyDeep + +// Phone-portrait dashboard: everything in one scrollable column, habits first +// (the child's main job), profile/streak as compact strips. The tablet keeps +// the PRD §5.2 three-column layout — see DashboardScreen. +@Composable +fun DashboardCompact( + uiState: DashboardUiState, + onToggleHabit: (String) -> Unit, + header: @Composable () -> Unit, + modifier: Modifier = Modifier, +) { + LazyColumn( + modifier = modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + item(key = "header") { header() } + item(key = "profile") { + CompactProfileStrip( + avatar = uiState.avatar, + childName = uiState.childName, + level = uiState.level, + xpInLevel = uiState.xpInLevel, + xpTarget = uiState.xpTarget, + streak = uiState.streak, + ) + } + item(key = "week") { + CompactStreakStrip(week = uiState.week, securedToday = uiState.allDone) + } + items(count = uiState.habits.size, key = { uiState.habits[it].id }) { index -> + val habit = uiState.habits[index] + HabitCard(habit = habit, onToggle = { onToggleHabit(habit.id) }) + } + item(key = "mascot") { + MascotMessageCard( + avatar = uiState.avatar, + message = if (uiState.allDone) { + stringResource(R.string.dashboard_mascot_celebrate, uiState.childName) + } else { + stringResource(R.string.dashboard_mascot_normal) + }, + ) + } + item(key = "team") { TeamAccessCard() } + item(key = "museum") { MuseumMiniCard() } + } +} + +// Sidebar essentials condensed into one card: mascot + name/level + XP bar + +// streak flame with count. +@Composable +private fun CompactProfileStrip( + avatar: AvatarKind, + childName: String, + level: Int, + xpInLevel: Int, + xpTarget: Int, + streak: Int, +) { + Row( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(20.dp)) + .background(Color.White) + .padding(14.dp), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + Mascot(kind = avatar, modifier = Modifier.size(64.dp)) + Column(modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp)) { + Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { + Text(text = childName, style = MaterialTheme.typography.titleMedium) + Box( + modifier = Modifier + .clip(RoundedCornerShape(999.dp)) + .background(Sun.copy(alpha = 0.25f)) + .padding(horizontal = 8.dp, vertical = 2.dp), + ) { + Text( + text = stringResource(R.string.dashboard_level_badge, level), + style = MaterialTheme.typography.labelSmall.copy(color = CoralIntense), + ) + } + } + Box( + modifier = Modifier + .fillMaxWidth() + .height(8.dp) + .clip(RoundedCornerShape(999.dp)) + .background(CreamCardSoft), + ) { + Box( + modifier = Modifier + .fillMaxWidth(fraction = (xpInLevel.toFloat() / xpTarget).coerceIn(0f, 1f)) + .height(8.dp) + .clip(RoundedCornerShape(999.dp)) + .background(Sun), + ) + } + Text( + text = stringResource(R.string.dashboard_xp_progress, xpInLevel, xpTarget), + style = MaterialTheme.typography.labelSmall, + ) + } + Column(horizontalAlignment = Alignment.CenterHorizontally) { + StreakFlame(modifier = Modifier.size(28.dp, 34.dp)) + Text( + text = streak.toString(), + style = MaterialTheme.typography.titleLarge.copy(color = CoralIntense), + ) + } + } +} + +@Composable +private fun CompactStreakStrip(week: List, securedToday: Boolean) { + Column( + modifier = Modifier + .fillMaxWidth() + .clip(RoundedCornerShape(16.dp)) + .background(Color.White) + .padding(horizontal = 12.dp, vertical = 10.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + WeekRow(week = week) + AnimatedVisibility( + visible = securedToday, + enter = slideInVertically { it / 2 } + fadeIn(), + exit = fadeOut(), + ) { + Box( + modifier = Modifier + .clip(RoundedCornerShape(999.dp)) + .background(SuccessGreen.copy(alpha = 0.15f)) + .padding(horizontal = 14.dp, vertical = 6.dp), + ) { + Text( + text = stringResource(R.string.dashboard_streak_secured), + style = MaterialTheme.typography.labelMedium.copy(color = SuccessGreen), + ) + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFF6EE, widthDp = 412, heightDp = 915) +@Composable +private fun PreviewDashboardCompact() { + RachitaTheme { + DashboardCompact( + uiState = DashboardUiState( + childName = "Lucía", + avatar = AvatarKind.Fox, + habits = listOf( + HabitCardUi("banarse", "Bañarse", "Antes de dormir", HabitIcon.Drop, HabitPalette.Mint, true), + HabitCardUi("dormir", "Dormir temprano", "A las 9:00 pm", HabitIcon.Moon, HabitPalette.Lilac, false), + HabitCardUi("escuela", "Ir a la escuela", "Sin faltar nunca", HabitIcon.School, HabitPalette.Coral, false), + ), + streak = 12, + week = listOf( + WeekDayUi("L", true, false), + WeekDayUi("M", true, false), + WeekDayUi("M", true, false), + WeekDayUi("J", false, true), + WeekDayUi("V", false, false), + WeekDayUi("S", false, false), + WeekDayUi("D", false, false), + ), + level = 7, + xpInLevel = 320, + battery = BatteryStatus(86, isCharging = false), + dateLine = "Lunes 22 de junio", + isLoading = false, + ), + onToggleHabit = {}, + header = { + Text(text = "¡Hola, Lucía!", style = MaterialTheme.typography.headlineMedium) + }, + ) + } +} diff --git a/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/RightColumn.kt b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/RightColumn.kt index 71b0dce..962a187 100644 --- a/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/RightColumn.kt +++ b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/RightColumn.kt @@ -17,11 +17,9 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width 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 -import androidx.compose.material.icons.rounded.Check import androidx.compose.material.icons.rounded.Favorite import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -124,44 +122,8 @@ private fun StreakCard(streak: Int, week: List, securedToday: Boolean } } -// DA-07: L M M J V S D circles, checked on secured days. @Composable -private fun WeekRow(week: List) { - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceEvenly, - ) { - week.forEach { day -> - Box( - modifier = Modifier - .size(30.dp) - .clip(CircleShape) - .background(if (day.secured) Coral else CreamCardSoft) - .then( - if (day.isToday) Modifier.border(2.dp, CoralIntense, CircleShape) else Modifier, - ), - contentAlignment = Alignment.Center, - ) { - if (day.secured) { - Icon( - imageVector = Icons.Rounded.Check, - contentDescription = null, - tint = Color.White, - modifier = Modifier.size(16.dp), - ) - } else { - Text( - text = day.label, - style = MaterialTheme.typography.labelSmall.copy(color = WarmGreyDeep), - ) - } - } - } - } -} - -@Composable -private fun TeamAccessCard() { +internal fun TeamAccessCard() { Row( modifier = Modifier .fillMaxWidth() @@ -198,7 +160,7 @@ fun MascotMessageCard(avatar: AvatarKind, message: String, modifier: Modifier = } @Composable -private fun MuseumMiniCard() { +internal fun MuseumMiniCard() { Column( modifier = Modifier .fillMaxWidth() diff --git a/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/WeekRow.kt b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/WeekRow.kt new file mode 100644 index 0000000..7a737ba --- /dev/null +++ b/app/app/src/main/java/mx/paputec/rachita/ui/dashboard/components/WeekRow.kt @@ -0,0 +1,83 @@ +package mx.paputec.rachita.ui.dashboard.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Check +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import mx.paputec.rachita.ui.dashboard.WeekDayUi +import mx.paputec.rachita.ui.theme.Coral +import mx.paputec.rachita.ui.theme.CoralIntense +import mx.paputec.rachita.ui.theme.CreamCardSoft +import mx.paputec.rachita.ui.theme.RachitaTheme +import mx.paputec.rachita.ui.theme.WarmGreyDeep + +// DA-07: L M M J V S D circles with checks on secured days. Shared by the +// tablet right column and the compact (portrait) streak strip. +@Composable +fun WeekRow(week: List, modifier: Modifier = Modifier) { + Row( + modifier = modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + week.forEach { day -> + Box( + modifier = Modifier + .size(30.dp) + .clip(CircleShape) + .background(if (day.secured) Coral else CreamCardSoft) + .then( + if (day.isToday) Modifier.border(2.dp, CoralIntense, CircleShape) else Modifier, + ), + contentAlignment = Alignment.Center, + ) { + if (day.secured) { + Icon( + imageVector = Icons.Rounded.Check, + contentDescription = null, + tint = Color.White, + modifier = Modifier.size(16.dp), + ) + } else { + Text( + text = day.label, + style = MaterialTheme.typography.labelSmall.copy(color = WarmGreyDeep), + ) + } + } + } + } +} + +@Preview(showBackground = true, backgroundColor = 0xFFFFF6EE, widthDp = 320) +@Composable +private fun PreviewWeekRow() { + RachitaTheme { + WeekRow( + week = listOf( + WeekDayUi("L", true, false), + WeekDayUi("M", true, false), + WeekDayUi("M", false, false), + WeekDayUi("J", false, false), + WeekDayUi("V", false, true), + WeekDayUi("S", false, false), + WeekDayUi("D", false, false), + ), + ) + } +} diff --git a/app/app/src/main/java/mx/paputec/rachita/ui/onboarding/OnboardingScreen.kt b/app/app/src/main/java/mx/paputec/rachita/ui/onboarding/OnboardingScreen.kt index e90e0ef..e16d68b 100644 --- a/app/app/src/main/java/mx/paputec/rachita/ui/onboarding/OnboardingScreen.kt +++ b/app/app/src/main/java/mx/paputec/rachita/ui/onboarding/OnboardingScreen.kt @@ -7,6 +7,7 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.PaddingValues @@ -76,26 +77,28 @@ fun OnboardingScreen( val insets = WindowInsets.safeDrawing.asPaddingValues() - Box( + BoxWithConstraints( modifier = Modifier .fillMaxSize() .background(CreamBackground) .padding(insets), ) { + // 2×2 grid on phones/portrait, 4-across on the tablet target (PRD layout). + val isCompact = maxWidth < 600.dp Column( modifier = Modifier .fillMaxSize() - .padding(horizontal = 32.dp, vertical = 24.dp), + .padding(horizontal = if (isCompact) 20.dp else 32.dp, vertical = if (isCompact) 16.dp else 24.dp), verticalArrangement = Arrangement.spacedBy(16.dp), ) { OnboardingHeader(childName = uiState.childName) LazyVerticalGrid( - columns = GridCells.Fixed(4), + columns = GridCells.Fixed(if (isCompact) 2 else 4), modifier = Modifier.fillMaxWidth().weight(1f), contentPadding = PaddingValues(vertical = 8.dp), - horizontalArrangement = Arrangement.spacedBy(20.dp), - verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalArrangement = Arrangement.spacedBy(if (isCompact) 14.dp else 20.dp), + verticalArrangement = Arrangement.spacedBy(if (isCompact) 14.dp else 20.dp), ) { items(items = AvatarOptions, key = { it.kind }) { option -> AvatarCard(