Privacy-Period-Tracker/app/src/main/kotlin/dev/privacyllc/period/navigation/PeriodApp.kt

131 lines
4.9 KiB
Kotlin
Raw Normal View History

chore: adopt the project template and add the Kotlin/Compose skeleton Period was a bare directory holding one 2,527-line specification, with no git repository, no tracker and no documentation convention. This is the adoption from Projects/Template/START-HERE-New-Project.md, plus a project that compiles so the hooks and future guards have something real to run against. Documents. scaffold.sh created 19 paths, 0 skipped. The specification moved to docs/planning/PRODUCT_PLAN.md unchanged in substance, with a status header; the capitalised Docs/ is gone. Every scaffolded document was filled in for Period. docs/OPERATIONS.md deleted — an offline app is not a deployed service. DOC_TRUST_MAP.md written last, describing what is actually here, including what this project deliberately does not have. Code. Four Gradle modules. domain/cycle and domain/prediction are kotlin("jvm") and cannot see the Android SDK, so the engine is testable without an emulator — 17 tests pass, 12 of them the acceptance cases from PRODUCT_PLAN.md §51. BaselinePredictionEngine is a robust-median prototype and explicitly not the product; it exists so Batch 02's replacement can be shown to be better rather than merely different. Versions verified against their official sources today rather than inherited from the specification's own numbers, which that document asks for: Kotlin 2.4.10, AGP 9.3.1, Gradle 9.7.0, Compose BOM 2026.08.00, Room 2.8.4, Hilt 2.60.1. AGP 9 ships Kotlin built in, so org.jetbrains.kotlin.android is no longer applied. compileSdk is 37 because current AndroidX requires it; targetSdk stays 36, Play's floor from 2026-08-31, and the difference is deliberate. Six scripts taken into scripts/; the rest declined and named in docs/TOOLS.md. Three hooks in .githooks/, with pre-commit adapted to Gradle. closes #1 closes #2
2026-08-18 02:16:47 -05:00
package dev.privacyllc.period.navigation
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.CalendarMonth
import androidx.compose.material.icons.filled.Insights
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material.icons.filled.Today
import androidx.compose.material.icons.filled.Circle
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.NavigationBar
import androidx.compose.material3.NavigationBarItem
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import dev.privacyllc.period.R
import dev.privacyllc.period.designsystem.PeriodTheme
/**
* The four tabs from docs/planning/PRODUCT_PLAN.md §20.
*
* Four and only four. Every proposal to add a fifth is answered by
* docs/planning/PROJECT_PLAN.md's "deliberately not" list.
*/
enum class PeriodDestination(
val route: String,
@param:StringRes val labelRes: Int,
val icon: ImageVector,
) {
TODAY("today", R.string.tab_today, Icons.Filled.Today),
CALENDAR("calendar", R.string.tab_calendar, Icons.Filled.CalendarMonth),
INSIGHTS("insights", R.string.tab_insights, Icons.Filled.Insights),
SETTINGS("settings", R.string.tab_settings, Icons.Filled.Settings),
}
@Composable
fun PeriodApp() {
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination
Scaffold(
bottomBar = {
NavigationBar {
PeriodDestination.entries.forEach { destination ->
val selected = currentDestination?.hierarchy?.any { it.route == destination.route } == true
NavigationBarItem(
selected = selected,
onClick = {
navController.navigate(destination.route) {
popUpTo(navController.graph.findStartDestination().id) { saveState = true }
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(destination.icon, contentDescription = null) },
label = { Text(stringResource(destination.labelRes)) },
)
}
}
},
) { innerPadding ->
NavHost(
navController = navController,
startDestination = PeriodDestination.TODAY.route,
modifier = Modifier.padding(innerPadding),
) {
PeriodDestination.entries.forEach { destination ->
composable(destination.route) {
PlaceholderScreen(stringResource(destination.labelRes))
}
}
}
}
}
/**
* Skeleton only. Each of these is replaced by its real screen in Batch 03, and
* saying "not built yet" on the screen is deliberate a convincing mock is how
* a screen comes to be believed finished.
*/
@Composable
private fun PlaceholderScreen(title: String) {
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally,
) {
Icon(
Icons.Filled.Circle,
contentDescription = null,
tint = MaterialTheme.colorScheme.primaryContainer,
)
Text(
text = title,
style = MaterialTheme.typography.headlineMedium,
color = MaterialTheme.colorScheme.onBackground,
)
Text(
text = "Not built yet",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
)
}
}
@Preview(showBackground = true)
@Composable
private fun PeriodAppPreview() {
PeriodTheme { PeriodApp() }
}