From ee098b6b83adb44a276fc7fce2579b21693784cf Mon Sep 17 00:00:00 2001 From: feragusper Date: Sun, 11 Jan 2026 19:12:49 +0100 Subject: [PATCH 1/2] feat(design): implement SmokeWebTheme and associated styles for improved UI consistency --- .gitignore | 1 + apps/mobile/build.gradle.kts | 2 +- apps/web/build.gradle.kts | 6 +- .../feragusper/smokeanalytics/WebScaffold.kt | 98 +---- .../com/feragusper/smokeanalytics/main.kt | 8 +- .../home/presentation/web/build.gradle.kts | 1 + .../home/presentation/web/HomeWebScreen.kt | 157 ++++---- .../presentation/web/build.gradle.kts | 9 +- .../presentation/web/SettingsWebScreen.kt | 64 ++-- .../stats/presentation/web/build.gradle.kts | 3 +- .../stats/presentation/web/StatsWebScreen.kt | 208 ++++++----- libraries/design/build.gradle.kts | 29 -- libraries/design/{ => common}/.gitignore | 0 libraries/design/common/build.gradle.kts | 26 ++ libraries/design/mobile/.gitignore | 1 + libraries/design/mobile/build.gradle.kts | 22 ++ .../{ => mobile}/src/main/AndroidManifest.xml | 0 .../libraries/design/compose/Previews.kt | 0 .../design/compose/PreviewsForTheme.kt | 0 .../libraries/design/compose/theme/Color.kt | 0 .../libraries/design/compose/theme/Theme.kt | 0 .../design/compose/theme/Typography.kt | 0 .../src/main/res/drawable/ic_cigarette.xml | 0 .../src/main/res/values/strings.xml | 0 libraries/design/web/.gitignore | 1 + libraries/design/web/build.gradle.kts | 27 ++ .../libraries/design/SmokeWebStyles.kt | 343 ++++++++++++++++++ .../libraries/design/SmokeWebTheme.kt | 38 ++ .../libraries/design/SurfaceCard.kt | 98 +++++ settings.gradle.kts | 4 +- 30 files changed, 816 insertions(+), 330 deletions(-) delete mode 100644 libraries/design/build.gradle.kts rename libraries/design/{ => common}/.gitignore (100%) create mode 100644 libraries/design/common/build.gradle.kts create mode 100644 libraries/design/mobile/.gitignore create mode 100644 libraries/design/mobile/build.gradle.kts rename libraries/design/{ => mobile}/src/main/AndroidManifest.xml (100%) rename libraries/design/{ => mobile}/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/Previews.kt (100%) rename libraries/design/{ => mobile}/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/PreviewsForTheme.kt (100%) rename libraries/design/{ => mobile}/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Color.kt (100%) rename libraries/design/{ => mobile}/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Theme.kt (100%) rename libraries/design/{ => mobile}/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Typography.kt (100%) rename libraries/design/{ => mobile}/src/main/res/drawable/ic_cigarette.xml (100%) rename libraries/design/{ => mobile}/src/main/res/values/strings.xml (100%) create mode 100644 libraries/design/web/.gitignore create mode 100644 libraries/design/web/build.gradle.kts create mode 100644 libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebStyles.kt create mode 100644 libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebTheme.kt create mode 100644 libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SurfaceCard.kt diff --git a/.gitignore b/.gitignore index 76240d8b..6cd4e56b 100644 --- a/.gitignore +++ b/.gitignore @@ -37,3 +37,4 @@ compose-build/ # Firebase local cache .firebase/ +/firebase-debug.log diff --git a/apps/mobile/build.gradle.kts b/apps/mobile/build.gradle.kts index 74db12ad..1ced30e8 100644 --- a/apps/mobile/build.gradle.kts +++ b/apps/mobile/build.gradle.kts @@ -40,7 +40,7 @@ val gitCode: Int by lazy { } // Construct the version name using a major.minor.patch pattern with the git code. -val majorMinorPatchVersionName = "0.5.0.$gitCode" +val majorMinorPatchVersionName = "0.6.0.$gitCode" android { // Set the application namespace. diff --git a/apps/web/build.gradle.kts b/apps/web/build.gradle.kts index 1b28d9a8..4c7509e3 100644 --- a/apps/web/build.gradle.kts +++ b/apps/web/build.gradle.kts @@ -1,5 +1,4 @@ import com.codingfeline.buildkonfig.compiler.FieldSpec.Type.STRING -import org.gradle.api.tasks.Sync plugins { kotlin("multiplatform") @@ -77,9 +76,10 @@ kotlin { implementation(project(":libraries:smokes:data:web")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:authentication:data:web")) + implementation(project(":libraries:design:web")) - implementation("dev.gitlive:firebase-auth:1.13.0") - implementation("dev.gitlive:firebase-app:1.13.0") + implementation(libs.gitlive.firebase.auth) + implementation(libs.firebase.app) } } } diff --git a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt index 501ab6d9..33d51abb 100644 --- a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt +++ b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebScaffold.kt @@ -1,98 +1,38 @@ package com.feragusper.smokeanalytics import androidx.compose.runtime.Composable -import org.jetbrains.compose.web.css.DisplayStyle -import org.jetbrains.compose.web.css.FlexDirection -import org.jetbrains.compose.web.css.JustifyContent -import org.jetbrains.compose.web.css.cursor -import org.jetbrains.compose.web.css.display -import org.jetbrains.compose.web.css.flexDirection -import org.jetbrains.compose.web.css.flexGrow -import org.jetbrains.compose.web.css.fontWeight -import org.jetbrains.compose.web.css.height -import org.jetbrains.compose.web.css.justifyContent -import org.jetbrains.compose.web.css.padding -import org.jetbrains.compose.web.css.px -import org.jetbrains.compose.web.css.vh +import com.feragusper.smokeanalytics.libraries.design.SmokeWebStyles import org.jetbrains.compose.web.dom.Div import org.jetbrains.compose.web.dom.Text -/** - * The scaffold for the web application. - * - * @param tab The current tab. - * @param onTabSelected The callback for when a tab is selected. - * @param content The content to display. - */ @Composable fun WebScaffold( tab: WebTab, onTabSelected: (WebTab) -> Unit, content: @Composable () -> Unit, ) { - Div({ - style { - display(DisplayStyle.Flex) - flexDirection(FlexDirection.Column) - height(100.vh) - } - }) { - Div({ - style { - flexGrow(1) - padding(16.px) - } - }) { - content() - } - - WebBottomNav( - selected = tab, - onSelected = onTabSelected, - ) - } -} + Div(attrs = { classes(SmokeWebStyles.shell) }) { + Div(attrs = { classes(SmokeWebStyles.sidebar) }) { + Div(attrs = { classes(SmokeWebStyles.sidebarTitle) }) { Text("Smoke Analytics") } -@Composable -private fun WebBottomNav( - selected: WebTab, - onSelected: (WebTab) -> Unit, -) { - Div({ - style { - display(DisplayStyle.Flex) - justifyContent(JustifyContent.SpaceAround) - padding(12.px) - property( - "border-top", - "1px solid lightgray" - ) - } - }) { - WebTab.entries.forEach { tab -> - WebNavItem( - label = tab.label(), - selected = tab == selected, - onClick = { onSelected(tab) }, - ) + Div(attrs = { classes(SmokeWebStyles.navList) }) { + WebTab.entries.forEach { t -> + Div( + attrs = { + classes(SmokeWebStyles.navItem) + if (t == tab) classes(SmokeWebStyles.navItemActive) + onClick { onTabSelected(t) } + } + ) { Text(t.label()) } + } + } } - } -} -@Composable -private fun WebNavItem( - label: String, - selected: Boolean, - onClick: () -> Unit, -) { - Div({ - onClick { onClick() } - style { - cursor("pointer") - fontWeight(if (selected) "bold" else "normal") + Div(attrs = { classes(SmokeWebStyles.main) }) { + Div(attrs = { classes(SmokeWebStyles.mainInner) }) { + content() + } } - }) { - Text(label) } } diff --git a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/main.kt b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/main.kt index f96e04ad..9962fcf4 100644 --- a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/main.kt +++ b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/main.kt @@ -1,15 +1,15 @@ package com.feragusper.smokeanalytics +import com.feragusper.smokeanalytics.libraries.design.SmokeWebTheme import org.jetbrains.compose.web.renderComposable -/** - * The main entry point for the web application. - */ fun main() { FirebaseWebInit.init() val graph = WebAppGraph.create() renderComposable(rootElementId = "root") { - AppRoot(graph) + SmokeWebTheme { + AppRoot(graph) + } } } \ No newline at end of file diff --git a/features/home/presentation/web/build.gradle.kts b/features/home/presentation/web/build.gradle.kts index be294844..c1902d9b 100644 --- a/features/home/presentation/web/build.gradle.kts +++ b/features/home/presentation/web/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { implementation(project(":libraries:smokes:domain")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:logging")) + implementation(project(":libraries:design:web")) implementation(libs.kotlinx.coroutines.core) diff --git a/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt b/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt index 17e6f9ad..e035c32f 100644 --- a/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt +++ b/features/home/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/home/presentation/web/HomeWebScreen.kt @@ -4,13 +4,16 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue import com.feragusper.smokeanalytics.features.home.presentation.web.mvi.HomeIntent import com.feragusper.smokeanalytics.features.home.presentation.web.mvi.HomeResult import com.feragusper.smokeanalytics.features.home.presentation.web.mvi.HomeWebStore -import com.feragusper.smokeanalytics.libraries.smokes.domain.model.Smoke +import com.feragusper.smokeanalytics.libraries.design.GhostButton +import com.feragusper.smokeanalytics.libraries.design.PrimaryButton +import com.feragusper.smokeanalytics.libraries.design.SmokeRow +import com.feragusper.smokeanalytics.libraries.design.SmokeWebStyles +import com.feragusper.smokeanalytics.libraries.design.StatCard +import com.feragusper.smokeanalytics.libraries.design.SurfaceCard import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime @@ -18,12 +21,9 @@ import kotlinx.datetime.LocalTime import kotlinx.datetime.TimeZone import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime -import org.jetbrains.compose.web.attributes.disabled -import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H2 -import org.jetbrains.compose.web.dom.Hr import org.jetbrains.compose.web.dom.P +import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text /** @@ -63,101 +63,84 @@ fun HomeViewState.Render( onIntent: (HomeIntent) -> Unit, ) { Div { - H2 { Text("Home") } - - if (displayLoading) { - P { Text("Loading...") } + Div(attrs = { classes(SmokeWebStyles.statsRow) }) { + StatCard( + title = "Today", + value = smokesPerDay?.toString() ?: "--", + onClick = { onIntent(HomeIntent.OnClickHistory) } + ) + StatCard( + title = "This week", + value = smokesPerWeek?.toString() ?: "--", + onClick = { onIntent(HomeIntent.OnClickHistory) } + ) + StatCard( + title = "This month", + value = smokesPerMonth?.toString() ?: "--", + onClick = { onIntent(HomeIntent.OnClickHistory) } + ) } - Div { - Button( - attrs = { - if (displayLoading) disabled() - onClick { onIntent(HomeIntent.AddSmoke) } - } - ) { Text("Add smoke") } - - Text(" ") + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { Text("Since your last cigarette") } - Button( - attrs = { - if (displayLoading) disabled() - onClick { onIntent(HomeIntent.RefreshFetchSmokes) } + SurfaceCard { + val since = timeSinceLastCigarette?.let { (h, m) -> + buildString { + if (h > 0) append("${h}h, ") + append("${m}m") } - ) { Text("Refresh") } - } - - Hr() - - P { Text("Today: ${smokesPerDay ?: "--"}") } - P { Text("Week: ${smokesPerWeek ?: "--"}") } - P { Text("Month: ${smokesPerMonth ?: "--"}") } + } ?: "--" - val since = timeSinceLastCigarette?.let { (h, m) -> "${h}h ${m}m" } ?: "--" - P { Text("Since last: $since") } - - Hr() - - var editing by remember { mutableStateOf(null) } + Div(attrs = { classes(SmokeWebStyles.sinceValue) }) { Text(since) } + } - latestSmokes?.let { smokes -> - Hr() - H2 { Text("Smoked today") } + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { Text("Smoked today") } - if (smokes.isEmpty()) { - P { Text("No smokes yet") } - } else { - smokes.forEach { smoke -> + if (displayLoading) { + P { Text("Loading...") } + } else if (latestSmokes.isNullOrEmpty()) { + P { Text("No smokes") } + } else { + Div(attrs = { classes(SmokeWebStyles.list) }) { + latestSmokes.forEach { smoke -> val local = smoke.date.toLocalDateTime(TimeZone.currentSystemDefault()) - Div { - P { - val hh = local.hour.toString().padStart(2, '0') - val mm = local.minute.toString().padStart(2, '0') - - Text("$hh:$mm") - Text(" (id=${smoke.id})") - } - - Button( - attrs = { - if (displayLoading) disabled() - onClick { editing = smoke } - } - ) { Text("Edit") } - - Text(" ") - - Button( - attrs = { - if (displayLoading) disabled() - onClick { onIntent(HomeIntent.DeleteSmoke(smoke.id)) } - } - ) { Text("Delete") } - - Hr() + val hh = local.hour.toString().padStart(2, '0') + val mm = local.minute.toString().padStart(2, '0') + val subtitle = smoke.timeElapsedSincePreviousSmoke.let { (h, m) -> + if (h > 0) "After $h hours and $m minutes" else "After $m minutes" } + + SmokeRow( + time = "$hh:$mm", + subtitle = subtitle, + onEdit = { onIntent(HomeIntent.EditSmoke(smoke.id, smoke.date)) }, + onDelete = { onIntent(HomeIntent.DeleteSmoke(smoke.id)) } + ) } } } - editing?.let { smoke -> - EditSmokeDialogWeb( - initialInstant = smoke.date, - fullDateTimeEdit = false, // igual que mobile Home (solo hora) - onDismiss = { editing = null }, - onConfirm = { newInstant -> - editing = null - onIntent(HomeIntent.EditSmoke(smoke.id, newInstant)) - } + Div { + PrimaryButton( + text = "Add smoke", + onClick = { onIntent(HomeIntent.AddSmoke) }, + enabled = !displayLoading + ) + Span { Text(" ") } + GhostButton( + text = "Refresh", + onClick = { onIntent(HomeIntent.RefreshFetchSmokes) }, + enabled = !displayLoading + ) + Span { Text(" ") } + GhostButton( + text = "History", + onClick = { onIntent(HomeIntent.OnClickHistory) }, + enabled = !displayLoading ) - } - - Button(attrs = { onClick { onIntent(HomeIntent.OnClickHistory) } }) { - Text("History") } if (error != null) { - Hr() P { Text( when (error) { @@ -190,8 +173,8 @@ internal fun dateTimeInputsToInstant( timeValue: String, timeZone: TimeZone, ): Instant { - val date = LocalDate.parse(dateValue) // "YYYY-MM-DD" - val time = LocalTime.parse(timeValue) // "HH:MM" + val date = LocalDate.parse(dateValue) + val time = LocalTime.parse(timeValue) val ldt = LocalDateTime( year = date.year, monthNumber = date.monthNumber, diff --git a/features/settings/presentation/web/build.gradle.kts b/features/settings/presentation/web/build.gradle.kts index 31e7e0c5..9813df97 100644 --- a/features/settings/presentation/web/build.gradle.kts +++ b/features/settings/presentation/web/build.gradle.kts @@ -14,17 +14,12 @@ kotlin { val jsMain by getting { dependencies { implementation(compose.runtime) - implementation(compose.web.core) + implementation(compose.html.core) + implementation(project(":libraries:design:web")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:authentication:presentation:web")) } } - - val jsTest by getting { - dependencies { - implementation(kotlin("test")) - } - } } } \ No newline at end of file diff --git a/features/settings/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/settings/presentation/web/SettingsWebScreen.kt b/features/settings/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/settings/presentation/web/SettingsWebScreen.kt index aabb5e17..5d7c635b 100644 --- a/features/settings/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/settings/presentation/web/SettingsWebScreen.kt +++ b/features/settings/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/settings/presentation/web/SettingsWebScreen.kt @@ -8,11 +8,11 @@ import androidx.compose.runtime.remember import com.feragusper.smokeanalytics.features.settings.presentation.web.mvi.SettingsIntent import com.feragusper.smokeanalytics.features.settings.presentation.web.mvi.SettingsWebStore import com.feragusper.smokeanalytics.libraries.authentication.presentation.compose.GoogleSignInComponentWeb -import org.jetbrains.compose.web.attributes.disabled -import org.jetbrains.compose.web.dom.Button +import com.feragusper.smokeanalytics.libraries.design.GhostButton +import com.feragusper.smokeanalytics.libraries.design.PrimaryButton +import com.feragusper.smokeanalytics.libraries.design.SmokeWebStyles +import com.feragusper.smokeanalytics.libraries.design.SurfaceCard import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H2 -import org.jetbrains.compose.web.dom.Hr import org.jetbrains.compose.web.dom.P import org.jetbrains.compose.web.dom.Text @@ -40,44 +40,52 @@ fun SettingsWebScreen( private fun SettingsViewState.Render( onIntent: (SettingsIntent) -> Unit, ) { - Div { - H2 { Text("Settings") } + Div(attrs = { classes(SmokeWebStyles.mainInner) }) { + + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { Text("Settings") } if (displayLoading) { - P { Text("Loading...") } + SurfaceCard { Text("Loading...") } return@Div } if (currentEmail != null) { - P { Text("Signed in as: $currentEmail") } - - Hr() + SurfaceCard { + P { Text("Signed in as: $currentEmail") } - Button( - attrs = { - if (displayLoading) disabled() - onClick { onIntent(SettingsIntent.SignOut) } + Div { + PrimaryButton( + text = "Sign out", + onClick = { onIntent(SettingsIntent.SignOut) }, + enabled = !displayLoading + ) + org.jetbrains.compose.web.dom.Span { Text(" ") } + GhostButton( + text = "Refresh session", + onClick = { onIntent(SettingsIntent.FetchUser) }, + enabled = !displayLoading + ) } - ) { - Text("Sign out") } } else { - P { Text("You are not signed in") } + SurfaceCard { + P { Text("You are not signed in.") } - Hr() + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { Text("Sign in") } - GoogleSignInComponentWeb( - onSignInSuccess = { onIntent(SettingsIntent.FetchUser) }, - onSignInError = { t -> - // Keep it simple: show a generic message, or you can push it into Store if you want - console.error("Sign-in error", t) - } - ) + GoogleSignInComponentWeb( + onSignInSuccess = { onIntent(SettingsIntent.FetchUser) }, + onSignInError = { t -> + console.error("Sign-in error", t) + } + ) + } } - errorMessage?.let { - Hr() - P { Text(it) } + errorMessage?.let { msg -> + SurfaceCard { + Text(msg) + } } } } \ No newline at end of file diff --git a/features/stats/presentation/web/build.gradle.kts b/features/stats/presentation/web/build.gradle.kts index 22d34371..6ffc4617 100644 --- a/features/stats/presentation/web/build.gradle.kts +++ b/features/stats/presentation/web/build.gradle.kts @@ -14,6 +14,7 @@ kotlin { dependencies { // --- Architecture / state handling --- implementation(project(":libraries:architecture:domain")) + implementation(project(":libraries:design:web")) // --- Domain --- implementation(project(":libraries:smokes:domain")) @@ -28,7 +29,5 @@ kotlin { implementation(npm("chart.js", "4.4.1")) } } - - val jsTest by getting } } \ No newline at end of file diff --git a/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt b/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt index 61bd9132..663837db 100644 --- a/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt +++ b/features/stats/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/stats/presentation/web/StatsWebScreen.kt @@ -10,6 +10,10 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import com.feragusper.smokeanalytics.features.stats.presentation.web.mvi.StatsIntent import com.feragusper.smokeanalytics.features.stats.presentation.web.mvi.StatsWebStore +import com.feragusper.smokeanalytics.libraries.design.GhostButton +import com.feragusper.smokeanalytics.libraries.design.PrimaryButton +import com.feragusper.smokeanalytics.libraries.design.SmokeWebStyles +import com.feragusper.smokeanalytics.libraries.design.SurfaceCard import com.feragusper.smokeanalytics.libraries.smokes.domain.usecase.FetchSmokeStatsUseCase import kotlinx.datetime.Clock import kotlinx.datetime.DateTimeUnit @@ -19,10 +23,8 @@ import kotlinx.datetime.plus import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.web.attributes.InputType import org.jetbrains.compose.web.attributes.disabled -import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Canvas import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H2 import org.jetbrains.compose.web.dom.Input import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text @@ -32,7 +34,6 @@ fun StatsWebScreen( deps: StatsWebDependencies, ) { val store = remember(deps) { StatsWebStore(processHolder = deps.processHolder) } - LaunchedEffect(store) { store.start() } val state by store.state.collectAsState() @@ -91,113 +92,144 @@ private fun StatsWebContent( onDateChange: (LocalDate) -> Unit, onReload: () -> Unit, ) { - val tz = remember { TimeZone.currentSystemDefault() } - - Div { - H2 { Text("Stats") } - - // Tabs - Div { - StatsPeriod.entries.forEach { p -> - val isSelected = p == currentPeriod - Button(attrs = { - if (state.displayLoading) disabled() - onClick { onPeriodChange(p) } - }) { - Text(if (isSelected) "[$p]" else p.name) + Div(attrs = { classes(SmokeWebStyles.mainInner) }) { + + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { Text("Stats") } + + SurfaceCard { + Div(attrs = { classes(SmokeWebStyles.statsToolbar) }) { + + Div(attrs = { classes(SmokeWebStyles.periodPills) }) { + StatsPeriod.entries.forEach { p -> + val isSelected = p == currentPeriod + val label = p.label() + + if (isSelected) { + PrimaryButton( + text = label, + onClick = { /* no-op */ }, + enabled = !state.displayLoading + ) + } else { + GhostButton( + text = label, + onClick = { onPeriodChange(p) }, + enabled = !state.displayLoading + ) + } + + Span { Text(" ") } + } } - Span { Text(" ") } - } - } - - // Header navigation (← label →) + optional date input - Div { - Button(attrs = { - if (state.displayLoading) disabled() - onClick { onDateChange(selectedDate.shift(currentPeriod, -1, tz)) } - }) { Text("←") } - - Span { Text(" ") } - - Text(selectedDate.headerLabel(currentPeriod)) - - Span { Text(" ") } - Button(attrs = { - if (state.displayLoading) disabled() - onClick { onDateChange(selectedDate.shift(currentPeriod, +1, tz)) } - }) { Text("→") } + Div(attrs = { classes(SmokeWebStyles.dateControls) }) { - Span { Text(" ") } + GhostButton( + text = "←", + onClick = { onDateChange(selectedDate.shift(currentPeriod, -1)) }, + enabled = !state.displayLoading + ) - Input( - type = InputType.Date, - attrs = { - value(selectedDate.toHtmlDate()) - if (state.displayLoading) disabled() - onInput { e -> - val picked = (e.value ?: "").toLocalDateOrNull() ?: return@onInput - onDateChange(picked) + Div(attrs = { classes(SmokeWebStyles.dateLabel) }) { + Text(selectedDate.headerLabel(currentPeriod)) } - } - ) - Span { Text(" ") } - - Button(attrs = { - if (state.displayLoading) disabled() - onClick { onReload() } - }) { Text("Reload") } + GhostButton( + text = "→", + onClick = { onDateChange(selectedDate.shift(currentPeriod, +1)) }, + enabled = !state.displayLoading + ) + + Input( + type = InputType.Date, + attrs = { + value(selectedDate.toHtmlDate()) + if (state.displayLoading) disabled() + onInput { e -> + val picked = e.value.toLocalDateOrNull() ?: return@onInput + onDateChange(picked) + } + classes(SmokeWebStyles.dateInput) + } + ) + + GhostButton( + text = "Reload", + onClick = onReload, + enabled = !state.displayLoading + ) + } + } } if (state.displayLoading) { - Div { Text("Loading...") } + SurfaceCard { Text("Loading...") } } if (state.error != null) { - Div { Text("Something went wrong") } + SurfaceCard { Text("Something went wrong") } } state.stats?.let { stats -> val chartId = remember(currentPeriod) { "statsChart_${currentPeriod.name}" } - Div { - Canvas(attrs = { - id(chartId) - attr("width", "900") - attr("height", "360") - }) - } - - when (currentPeriod) { - StatsPeriod.DAY -> LineChartJs( - canvasId = chartId, - title = "Today", - data = stats.hourly - ) - - StatsPeriod.WEEK -> BarChartJs( - canvasId = chartId, - title = "Week", - data = stats.weekly - ) + SurfaceCard { + Div(attrs = { classes(SmokeWebStyles.chartHeader) }) { + Text(currentPeriod.chartTitle()) + } - StatsPeriod.MONTH -> BarChartJs( - canvasId = chartId, - title = "Month", - data = stats.monthly - ) + Div(attrs = { classes(SmokeWebStyles.chartWrap) }) { + Canvas(attrs = { + id(chartId) + attr("width", "1200") + attr("height", "420") + }) + } - StatsPeriod.YEAR -> BarChartJs( - canvasId = chartId, - title = "Year", - data = stats.yearly - ) + when (currentPeriod) { + StatsPeriod.DAY -> LineChartJs( + canvasId = chartId, + title = "Today", + data = stats.hourly + ) + + StatsPeriod.WEEK -> BarChartJs( + canvasId = chartId, + title = "Week", + data = stats.weekly + ) + + StatsPeriod.MONTH -> BarChartJs( + canvasId = chartId, + title = "Month", + data = stats.monthly + ) + + StatsPeriod.YEAR -> BarChartJs( + canvasId = chartId, + title = "Year", + data = stats.yearly + ) + } } } } } +private fun StatsPeriod.label(): String = when (this) { + StatsPeriod.DAY -> "Day" + StatsPeriod.WEEK -> "Week" + StatsPeriod.MONTH -> "Month" + StatsPeriod.YEAR -> "Year" +} + +private fun StatsPeriod.chartTitle(): String = when (this) { + StatsPeriod.DAY -> "Today (hourly)" + StatsPeriod.WEEK -> "This week" + StatsPeriod.MONTH -> "This month" + StatsPeriod.YEAR -> "This year" +} + @Composable private fun LineChartJs( canvasId: String, @@ -206,7 +238,6 @@ private fun LineChartJs( ) { val labels = remember(data) { data.keys.toList() } val values = remember(data) { data.values.map { it as Number } } - val chartHolder = remember { mutableStateOf(null) } DisposableEffect(canvasId, labels, values) { @@ -244,7 +275,6 @@ private fun BarChartJs( ) { val labels = remember(data) { data.keys.toList() } val values = remember(data) { data.values.map { it as Number } } - val chartHolder = remember { mutableStateOf(null) } DisposableEffect(canvasId, labels, values) { @@ -274,7 +304,7 @@ private fun BarChartJs( } } -private fun LocalDate.shift(period: StatsPeriod, amount: Int, tz: TimeZone): LocalDate { +private fun LocalDate.shift(period: StatsPeriod, amount: Int): LocalDate { val unit = when (period) { StatsPeriod.DAY -> DateTimeUnit.DAY StatsPeriod.WEEK -> DateTimeUnit.WEEK diff --git a/libraries/design/build.gradle.kts b/libraries/design/build.gradle.kts deleted file mode 100644 index ed009452..00000000 --- a/libraries/design/build.gradle.kts +++ /dev/null @@ -1,29 +0,0 @@ -plugins { - // Use the predefined android-lib plugin for Android library modules. - `android-lib` - // Apply the Android Library plugin for building reusable UI components. - id("com.android.library") - // Apply the Kotlin Android plugin for using Kotlin in Android modules. - id("org.jetbrains.kotlin.android") - // Apply the Compose Compiler plugin using the version catalog alias. - alias(libs.plugins.compose.compiler) -} - -android { - // Set the namespace for the Android library. - namespace = "com.feragusper.smokeanalytics.libraries.design" - - buildFeatures { - // Enable Jetpack Compose support. - compose = true - } -} - -dependencies { - // Core AndroidX libraries bundle for base functionality. - implementation(libs.bundles.androidx.base) - // Material 3 design components for modern UI design. - implementation(libs.material3) - // Include a bundle of Jetpack Compose libraries. - implementation(libs.bundles.compose) -} diff --git a/libraries/design/.gitignore b/libraries/design/common/.gitignore similarity index 100% rename from libraries/design/.gitignore rename to libraries/design/common/.gitignore diff --git a/libraries/design/common/build.gradle.kts b/libraries/design/common/build.gradle.kts new file mode 100644 index 00000000..bb230e3f --- /dev/null +++ b/libraries/design/common/build.gradle.kts @@ -0,0 +1,26 @@ +plugins { + kotlin("multiplatform") + id("com.android.library") +} + +kotlin { + androidTarget() + js(IR) { + browser() + } + + sourceSets { + commonMain.dependencies { + // nada android/web acá; solo kotlin común + } + } +} + +android { + namespace = "com.feragusper.smokeanalytics.libraries.design.common" + compileSdk = Android.COMPILE_SDK + + defaultConfig { + minSdk = Android.MIN_SDK + } +} \ No newline at end of file diff --git a/libraries/design/mobile/.gitignore b/libraries/design/mobile/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/libraries/design/mobile/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libraries/design/mobile/build.gradle.kts b/libraries/design/mobile/build.gradle.kts new file mode 100644 index 00000000..cd31a751 --- /dev/null +++ b/libraries/design/mobile/build.gradle.kts @@ -0,0 +1,22 @@ +plugins { + `android-lib` + id("com.android.library") + id("org.jetbrains.kotlin.android") + alias(libs.plugins.compose.compiler) +} + +android { + namespace = "com.feragusper.smokeanalytics.libraries.design.mobile" + + buildFeatures { + compose = true + } +} + +dependencies { + implementation(project(":libraries:design:common")) + + implementation(libs.bundles.androidx.base) + implementation(libs.material3) + implementation(libs.bundles.compose) +} \ No newline at end of file diff --git a/libraries/design/src/main/AndroidManifest.xml b/libraries/design/mobile/src/main/AndroidManifest.xml similarity index 100% rename from libraries/design/src/main/AndroidManifest.xml rename to libraries/design/mobile/src/main/AndroidManifest.xml diff --git a/libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/Previews.kt b/libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/Previews.kt similarity index 100% rename from libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/Previews.kt rename to libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/Previews.kt diff --git a/libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/PreviewsForTheme.kt b/libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/PreviewsForTheme.kt similarity index 100% rename from libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/PreviewsForTheme.kt rename to libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/PreviewsForTheme.kt diff --git a/libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Color.kt b/libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Color.kt similarity index 100% rename from libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Color.kt rename to libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Color.kt diff --git a/libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Theme.kt b/libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Theme.kt similarity index 100% rename from libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Theme.kt rename to libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Theme.kt diff --git a/libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Typography.kt b/libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Typography.kt similarity index 100% rename from libraries/design/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Typography.kt rename to libraries/design/mobile/src/main/java/com/feragusper/smokeanalytics/libraries/design/compose/theme/Typography.kt diff --git a/libraries/design/src/main/res/drawable/ic_cigarette.xml b/libraries/design/mobile/src/main/res/drawable/ic_cigarette.xml similarity index 100% rename from libraries/design/src/main/res/drawable/ic_cigarette.xml rename to libraries/design/mobile/src/main/res/drawable/ic_cigarette.xml diff --git a/libraries/design/src/main/res/values/strings.xml b/libraries/design/mobile/src/main/res/values/strings.xml similarity index 100% rename from libraries/design/src/main/res/values/strings.xml rename to libraries/design/mobile/src/main/res/values/strings.xml diff --git a/libraries/design/web/.gitignore b/libraries/design/web/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/libraries/design/web/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/libraries/design/web/build.gradle.kts b/libraries/design/web/build.gradle.kts new file mode 100644 index 00000000..b4b19f14 --- /dev/null +++ b/libraries/design/web/build.gradle.kts @@ -0,0 +1,27 @@ +plugins { + kotlin("multiplatform") + alias(libs.plugins.compose.multiplatform) + alias(libs.plugins.compose.compiler) +} + +kotlin { + js(IR) { + browser() + binaries.executable() + } + + sourceSets { + commonMain { + dependencies { + implementation(project(":libraries:design:common")) + } + } + jsMain { + dependencies { + implementation(compose.runtime) + implementation(compose.html.core) + implementation(compose.html.svg) + } + } + } +} \ No newline at end of file diff --git a/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebStyles.kt b/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebStyles.kt new file mode 100644 index 00000000..85f31293 --- /dev/null +++ b/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebStyles.kt @@ -0,0 +1,343 @@ +package com.feragusper.smokeanalytics.libraries.design + +import org.jetbrains.compose.web.css.AlignItems +import org.jetbrains.compose.web.css.Color +import org.jetbrains.compose.web.css.DisplayStyle +import org.jetbrains.compose.web.css.FlexDirection +import org.jetbrains.compose.web.css.JustifyContent +import org.jetbrains.compose.web.css.LineStyle +import org.jetbrains.compose.web.css.StyleSheet +import org.jetbrains.compose.web.css.alignItems +import org.jetbrains.compose.web.css.backgroundColor +import org.jetbrains.compose.web.css.border +import org.jetbrains.compose.web.css.color +import org.jetbrains.compose.web.css.cursor +import org.jetbrains.compose.web.css.display +import org.jetbrains.compose.web.css.flexDirection +import org.jetbrains.compose.web.css.fontFamily +import org.jetbrains.compose.web.css.fontSize +import org.jetbrains.compose.web.css.fontWeight +import org.jetbrains.compose.web.css.gap +import org.jetbrains.compose.web.css.gridTemplateColumns +import org.jetbrains.compose.web.css.height +import org.jetbrains.compose.web.css.justifyContent +import org.jetbrains.compose.web.css.lineHeight +import org.jetbrains.compose.web.css.marginBottom +import org.jetbrains.compose.web.css.marginTop +import org.jetbrains.compose.web.css.maxWidth +import org.jetbrains.compose.web.css.media +import org.jetbrains.compose.web.css.mediaMaxWidth +import org.jetbrains.compose.web.css.minHeight +import org.jetbrains.compose.web.css.opacity +import org.jetbrains.compose.web.css.padding +import org.jetbrains.compose.web.css.px +import org.jetbrains.compose.web.css.style +import org.jetbrains.compose.web.css.vh +import org.jetbrains.compose.web.css.width + +object SmokeWebStyles : StyleSheet() { + + val appRoot by style { + fontFamily( + "system-ui", + "-apple-system", + "Segoe UI", + "Roboto", + "Helvetica", + "Arial", + "sans-serif" + ) + property("text-rendering", "optimizeLegibility") + + // Tokens (light defaults) + property("--sa-color-primary", "#006A6A") + property("--sa-color-onPrimary", "#FFFFFF") + property("--sa-color-secondary", "#4A6363") + property("--sa-color-bg", "#FFFFFF") + property("--sa-color-onBg", "#000000") + property("--sa-color-surface", "#DDE4E3") + property("--sa-color-onSurface", "#161D1D") + property("--sa-color-outline", "rgba(0,0,0,0.10)") + + property("--sa-radius-md", "16px") + property("--sa-radius-sm", "12px") + + property("--sa-shadow-1", "0 6px 18px rgba(0,0,0,0.10)") + property("--sa-shadow-2", "0 10px 30px rgba(0,0,0,0.12)") + + backgroundColor(Color("var(--sa-color-bg)")) + color(Color("var(--sa-color-onBg)")) + + // Important: no padding here (web shell controls spacing) + padding(0.px) + property("width", "100%") + minHeight(100.vh) + } + + val appRootDarkTokens by style { + property("--sa-color-primary", "#80D5D4") + property("--sa-color-onPrimary", "#003737") + property("--sa-color-secondary", "#B0CCCB") + property("--sa-color-bg", "#000000") + property("--sa-color-onBg", "#FFFFFF") + property("--sa-color-surface", "#0E1514") + property("--sa-color-onSurface", "#B0CCCB") + property("--sa-color-outline", "rgba(255,255,255,0.12)") + } + + // ---- Web shell (sidebar + main) + val shell by style { + display(DisplayStyle.Flex) + flexDirection(FlexDirection.Row) + height(100.vh) + property("width", "100%") + backgroundColor(Color("var(--sa-color-bg)")) + color(Color("var(--sa-color-onBg)")) + property("overflow", "hidden") + } + + val sidebar by style { + property("width", "260px") + property("flex", "0 0 260px") + backgroundColor(Color("var(--sa-color-surface)")) + color(Color("var(--sa-color-onSurface)")) + property("border-right", "1px solid var(--sa-color-outline)") + padding(16.px) + property("box-sizing", "border-box") + } + + val sidebarTitle by style { + fontSize(14.px) + fontWeight(700) + marginBottom(12.px) + opacity(0.9) + } + + val navList by style { + display(DisplayStyle.Flex) + flexDirection(FlexDirection.Column) + gap(6.px) + } + + val navItem by style { + padding(10.px, 12.px) + property("border-radius", "12px") + cursor("pointer") + property("user-select", "none") + property("transition", "background-color 120ms ease") + self + hover style { + backgroundColor(Color("rgba(0,0,0,0.06)")) + } + } + + val navItemActive by style { + backgroundColor(Color("var(--sa-color-primary)")) + color(Color("var(--sa-color-onPrimary)")) + } + + val main by style { + property("flex", "1 1 auto") + property("min-width", "0") + property("overflow-y", "auto") + padding(24.px) + property("box-sizing", "border-box") + } + + val mainInner by style { + // Keep content readable but NOT centered with huge gutters + property("width", "100%") + maxWidth(1200.px) + property("margin", "0 auto") + } + + // ---- Existing pieces: keep them, but avoid forcing 720px container everywhere + val statsRow by style { + display(DisplayStyle.Grid) + gridTemplateColumns("repeat(3, minmax(0, 1fr))") + gap(12.px) + + media(mediaMaxWidth(1100.px)) { self { gridTemplateColumns("repeat(2, minmax(0, 1fr))") } } + media(mediaMaxWidth(680.px)) { self { gridTemplateColumns("1fr") } } + } + + val card by style { + backgroundColor(Color("var(--sa-color-surface)")) + color(Color("var(--sa-color-onSurface)")) + property("border-radius", "var(--sa-radius-md)") + padding(16.px) + property("box-shadow", "var(--sa-shadow-1)") + border { + width(1.px) + style(LineStyle.Solid) + color(Color("var(--sa-color-outline)")) + } + } + + val statCard by style { + property("user-select", "none") + cursor("pointer") + property("transition", "transform 120ms ease, box-shadow 120ms ease") + self + hover style { + property("transform", "translateY(-1px)") + property("box-shadow", "var(--sa-shadow-2)") + } + } + + val statTitle by style { + fontSize(12.px) + fontWeight(600) + opacity(0.75) + } + + val statValue by style { + fontSize(40.px) + fontWeight(700) + lineHeight("1") + marginTop(8.px) + } + + // Titles / sections + val sectionTitle by style { + fontSize(14.px) + fontWeight(700) + marginTop(12.px) + } + + // Since card value + val sinceValue by style { + fontSize(28.px) + fontWeight(700) + lineHeight("1.1") + } + + // List container + val list by style { + display(DisplayStyle.Flex) + flexDirection(FlexDirection.Column) + gap(8.px) + } + + // Buttons + val button by style { + property("border-radius", "999px") + padding(10.px, 14.px) + + border { + width(1.px) + style(LineStyle.Solid) + color(Color("var(--sa-color-outline)")) + } + + backgroundColor(Color("transparent")) + color(Color("var(--sa-color-onSurface)")) + cursor("pointer") + + property("user-select", "none") + property("transition", "transform 120ms ease, box-shadow 120ms ease") + + self + hover style { + property("transform", "translateY(-1px)") + property("box-shadow", "var(--sa-shadow-1)") + } + } + + val buttonPrimary by style { + backgroundColor(Color("var(--sa-color-primary)")) + color(Color("var(--sa-color-onPrimary)")) + border { + width(0.px) + style(LineStyle.None) + color(Color.transparent) + } + } + + // List rows + val listRow by style { + display(DisplayStyle.Flex) + justifyContent(JustifyContent.SpaceBetween) + alignItems(AlignItems.Center) + + padding(12.px) + property("border-radius", "var(--sa-radius-sm)") + + backgroundColor(Color("rgba(255,255,255,0.25)")) + + border { + width(1.px) + style(LineStyle.Solid) + color(Color("var(--sa-color-outline)")) + } + } + + val timeText by style { + fontSize(14.px) + fontWeight(700) + } + + val subText by style { + fontSize(12.px) + opacity(0.75) + marginTop(2.px) + } + + val statsToolbar by style { + display(DisplayStyle.Flex) + property("flex-wrap", "wrap") + gap(12.px) + justifyContent(JustifyContent.SpaceBetween) + alignItems(AlignItems.Center) + } + + val periodPills by style { + display(DisplayStyle.Flex) + property("flex-wrap", "wrap") + gap(8.px) + alignItems(AlignItems.Center) + } + + val dateControls by style { + display(DisplayStyle.Flex) + property("flex-wrap", "wrap") + gap(8.px) + alignItems(AlignItems.Center) + } + + val dateLabel by style { + fontWeight(700) + property("min-width", "160px") + property("text-align", "center") + } + + val dateInput by style { + padding(10.px, 12.px) + property("border-radius", "999px") + border { + width(1.px) + style(LineStyle.Solid) + color(Color("var(--sa-color-outline)")) + } + backgroundColor(Color("transparent")) + color(Color("var(--sa-color-onSurface)")) + } + + val chartHeader by style { + fontWeight(700) + marginTop(2.px) + property("margin-bottom", "8px") + } + + val chartWrap by style { + property("width", "100%") + property("height", "420px") + } + + init { + // Full page reset + "html, body, #root".style { + property("height", "100%") + property("width", "100%") + property("margin", "0") + property("padding", "0") + } + } +} \ No newline at end of file diff --git a/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebTheme.kt b/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebTheme.kt new file mode 100644 index 00000000..502cc268 --- /dev/null +++ b/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SmokeWebTheme.kt @@ -0,0 +1,38 @@ +package com.feragusper.smokeanalytics.libraries.design + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import kotlinx.browser.window +import org.jetbrains.compose.web.css.Style +import org.jetbrains.compose.web.dom.Div + +@Composable +fun SmokeWebTheme( + forceDarkTheme: Boolean? = null, + content: @Composable () -> Unit, +) { + val prefersDark = remember { mutableStateOf(false) } + + LaunchedEffect(Unit) { + val mql = window.matchMedia("(prefers-color-scheme: dark)") + prefersDark.value = mql.matches + mql.addEventListener("change", { _ -> prefersDark.value = mql.matches }) + } + + val darkTheme = forceDarkTheme ?: prefersDark.value + val themeAttr = if (darkTheme) "dark" else "light" + + Style(SmokeWebStyles) + + Div( + attrs = { + classes(SmokeWebStyles.appRoot) + attr("data-theme", themeAttr) + if (darkTheme) classes(SmokeWebStyles.appRootDarkTokens) + } + ) { + content() + } +} \ No newline at end of file diff --git a/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SurfaceCard.kt b/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SurfaceCard.kt new file mode 100644 index 00000000..79453b93 --- /dev/null +++ b/libraries/design/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/libraries/design/SurfaceCard.kt @@ -0,0 +1,98 @@ +package com.feragusper.smokeanalytics.libraries.design + +import androidx.compose.runtime.Composable +import org.jetbrains.compose.web.dom.Button +import org.jetbrains.compose.web.dom.Div +import org.jetbrains.compose.web.dom.Span +import org.jetbrains.compose.web.dom.Text + +@Composable +fun SurfaceCard( + vararg extraClasses: String, + content: @Composable () -> Unit, +) { + Div( + attrs = { + classes(SmokeWebStyles.card) + if (extraClasses.isNotEmpty()) { + classes(*extraClasses) + } + } + ) { + content() + } +} + +@Composable +fun StatCard( + title: String, + value: String, + onClick: (() -> Unit)? = null, +) { + SurfaceCard(SmokeWebStyles.statCard) { + Div( + attrs = { + if (onClick != null) { + onClick { onClick() } + } + } + ) { + Div(attrs = { classes(SmokeWebStyles.statTitle) }) { Text(title) } + Div(attrs = { classes(SmokeWebStyles.statValue) }) { Text(value) } + } + } +} + +@Composable +fun PrimaryButton( + text: String, + onClick: () -> Unit, + enabled: Boolean = true, +) { + Button( + attrs = { + classes(SmokeWebStyles.button, SmokeWebStyles.buttonPrimary) + if (!enabled) attr("disabled", "true") + onClick { if (enabled) onClick() } + } + ) { Text(text) } +} + +@Composable +fun GhostButton( + text: String, + onClick: () -> Unit, + enabled: Boolean = true, +) { + Button( + attrs = { + classes(SmokeWebStyles.button) + if (!enabled) attr("disabled", "true") + onClick { if (enabled) onClick() } + } + ) { Text(text) } +} + +@Composable +fun SmokeRow( + time: String, + subtitle: String, + onEdit: (() -> Unit)?, + onDelete: (() -> Unit)?, +) { + Div(attrs = { classes(SmokeWebStyles.listRow) }) { + Div { + Div(attrs = { classes(SmokeWebStyles.timeText) }) { Text(time) } + Div(attrs = { classes(SmokeWebStyles.subText) }) { Text(subtitle) } + } + Div { + if (onEdit != null) { + GhostButton(text = "Edit", onClick = onEdit) + Span { Text(" ") } + } + if (onDelete != null) { + GhostButton(text = "Delete", onClick = onDelete) + } + } + } +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index cd7069c8..ce4539d8 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -51,7 +51,9 @@ include(":libraries:authentication:data:web") include(":libraries:authentication:domain") include(":libraries:authentication:presentation:mobile") include(":libraries:authentication:presentation:web") -include(":libraries:design") +include(":libraries:design:common") +include(":libraries:design:mobile") +include(":libraries:design:web") include(":libraries:logging") include(":libraries:smokes:data:mobile") include(":libraries:smokes:data:web") From c8bcb64064cd8f38fc820c98bb5f9242136f716f Mon Sep 17 00:00:00 2001 From: feragusper Date: Mon, 12 Jan 2026 21:01:05 +0100 Subject: [PATCH 2/2] feat(dependencies): update design library references to use mobile variant and adjust Firebase auth implementation --- apps/mobile/build.gradle.kts | 2 +- apps/wear/build.gradle.kts | 18 +- .../smokeanalytics/tile/MainTileService.kt | 2 +- .../feragusper/smokeanalytics/WebAppGraph.kt | 6 +- .../presentation/mobile/build.gradle.kts | 2 +- .../mvi/compose/AuthenticationViewState.kt | 14 +- .../chatbot/presentation/build.gradle.kts | 2 +- .../devtools/presentation/build.gradle.kts | 2 +- .../presentation/mobile/build.gradle.kts | 2 +- .../history/presentation/web/build.gradle.kts | 1 + .../history/presentation/HistoryWebScreen.kt | 271 +++++++++--------- .../home/presentation/mobile/build.gradle.kts | 2 +- .../presentation/mobile/build.gradle.kts | 2 +- .../mvi/compose/SettingsViewState.kt | 7 +- .../presentation/mobile/build.gradle.kts | 2 +- gradle/libs.versions.toml | 30 +- .../presentation/mobile/build.gradle.kts | 2 +- .../compose/GoogleSignInComponentWeb.kt | 6 +- .../smokes/presentation/build.gradle.kts | 2 +- 19 files changed, 195 insertions(+), 180 deletions(-) diff --git a/apps/mobile/build.gradle.kts b/apps/mobile/build.gradle.kts index 1ced30e8..ee4b3e2e 100644 --- a/apps/mobile/build.gradle.kts +++ b/apps/mobile/build.gradle.kts @@ -176,7 +176,7 @@ dependencies { // Timber for logging. implementation(libs.timber) // Project modules. - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) implementation(project(":libraries:architecture:presentation:mobile")) implementation(project(":libraries:authentication:domain")) implementation(project(":libraries:smokes:domain")) diff --git a/apps/wear/build.gradle.kts b/apps/wear/build.gradle.kts index 2d00fe4e..56dc5326 100644 --- a/apps/wear/build.gradle.kts +++ b/apps/wear/build.gradle.kts @@ -1,4 +1,5 @@ import com.google.common.base.Charsets +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import java.io.ByteArrayOutputStream import java.io.FileInputStream import java.io.InputStreamReader @@ -82,12 +83,14 @@ android { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } - kotlinOptions { - jvmTarget = Java.JVM_TARGET - freeCompilerArgs = listOf( - "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", - "-opt-in=kotlin.RequiresOptIn", - ) + kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.fromTarget(Java.JVM_TARGET)) + freeCompilerArgs.addAll( + "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", + "-opt-in=kotlin.RequiresOptIn", + ) + } } buildFeatures { @@ -95,6 +98,7 @@ android { buildConfig = true } + @Suppress("UnstableApiUsage") composeOptions { kotlinCompilerExtensionVersion = Java.KOTLIN_COMPILER_EXTENSION_VERSION } @@ -140,7 +144,7 @@ dependencies { implementation(libs.bundles.compose) implementation(libs.hilt) implementation(libs.timber) - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) implementation(libs.androidx.tiles) implementation(libs.horologist.composables) implementation(libs.horologist.tiles) diff --git a/apps/wear/src/main/java/com/feragusper/smokeanalytics/tile/MainTileService.kt b/apps/wear/src/main/java/com/feragusper/smokeanalytics/tile/MainTileService.kt index 0a333040..163ab341 100644 --- a/apps/wear/src/main/java/com/feragusper/smokeanalytics/tile/MainTileService.kt +++ b/apps/wear/src/main/java/com/feragusper/smokeanalytics/tile/MainTileService.kt @@ -84,7 +84,7 @@ class MainTileService : SuspendingTileService() { ResourceBuilders.ImageResource.Builder() .setAndroidResourceByResId( ResourceBuilders.AndroidImageResourceByResId.Builder() - .setResourceId(com.feragusper.smokeanalytics.libraries.design.R.drawable.ic_cigarette) + .setResourceId(com.feragusper.smokeanalytics.libraries.design.mobile.R.drawable.ic_cigarette) .build() ).build() ) diff --git a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebAppGraph.kt b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebAppGraph.kt index 9d96376b..160cf9df 100644 --- a/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebAppGraph.kt +++ b/apps/web/src/jsMain/kotlin/com/feragusper/smokeanalytics/WebAppGraph.kt @@ -13,9 +13,8 @@ import com.feragusper.smokeanalytics.libraries.smokes.domain.usecase.DeleteSmoke import com.feragusper.smokeanalytics.libraries.smokes.domain.usecase.EditSmokeUseCase import com.feragusper.smokeanalytics.libraries.smokes.domain.usecase.FetchSmokeStatsUseCase import com.feragusper.smokeanalytics.libraries.smokes.domain.usecase.FetchSmokesUseCase -import dev.gitlive.firebase.Firebase -import dev.gitlive.firebase.auth.auth import dev.gitlive.firebase.auth.externals.GoogleAuthProvider +import dev.gitlive.firebase.auth.externals.getAuth import dev.gitlive.firebase.auth.externals.signInWithPopup import kotlinx.coroutines.await @@ -73,9 +72,8 @@ data class WebAppGraph( ) val signInWithGoogleWeb: suspend () -> Unit = { - val auth = Firebase.auth val provider = GoogleAuthProvider() - signInWithPopup(auth.js, provider).await() + signInWithPopup(getAuth(), provider).await() } return WebAppGraph( diff --git a/features/authentication/presentation/mobile/build.gradle.kts b/features/authentication/presentation/mobile/build.gradle.kts index 7d50286d..1554a24f 100644 --- a/features/authentication/presentation/mobile/build.gradle.kts +++ b/features/authentication/presentation/mobile/build.gradle.kts @@ -34,7 +34,7 @@ dependencies { implementation(project(":libraries:authentication:domain")) // Design system for consistent theming and UI components - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // AndroidX and Compose libraries implementation(libs.bundles.androidx.base) diff --git a/features/authentication/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/authentication/presentation/mvi/compose/AuthenticationViewState.kt b/features/authentication/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/authentication/presentation/mvi/compose/AuthenticationViewState.kt index 2cc28034..b7659f1a 100644 --- a/features/authentication/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/authentication/presentation/mvi/compose/AuthenticationViewState.kt +++ b/features/authentication/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/authentication/presentation/mvi/compose/AuthenticationViewState.kt @@ -18,7 +18,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp import com.feragusper.smokeanalytics.features.authentication.presentation.R @@ -72,14 +71,15 @@ data class AuthenticationViewState( ) { Text(text = stringResource(id = R.string.authentication_sign_in_to_continue)) val scope = rememberCoroutineScope() - val context = LocalContext.current + val message = + stringResource(com.feragusper.smokeanalytics.libraries.design.mobile.R.string.error_general) GoogleSignInComponent( modifier = Modifier.padding(top = 16.dp), onSignInSuccess = { intent(AuthenticationIntent.FetchUser) }, onSignInError = { scope.launch { snackbarHostState.showSnackbar( - context.getString(com.feragusper.smokeanalytics.libraries.design.R.string.error_general) + message ) } }, @@ -87,14 +87,14 @@ data class AuthenticationViewState( } // Display error messages as snackbars - val context = LocalContext.current + val message = stringResource( + R.string.error_generic + ) LaunchedEffect(error) { error?.let { when (it) { AuthenticationResult.Error.Generic -> snackbarHostState.showSnackbar( - context.getString( - R.string.error_generic - ) + message ) } } diff --git a/features/chatbot/presentation/build.gradle.kts b/features/chatbot/presentation/build.gradle.kts index 9763c7b6..7e2fa97d 100644 --- a/features/chatbot/presentation/build.gradle.kts +++ b/features/chatbot/presentation/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(project(":libraries:architecture:presentation:mobile")) // Design system for consistent theming and UI components - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) implementation(project(":features:chatbot:domain")) implementation(project(":features:chatbot:data")) diff --git a/features/devtools/presentation/build.gradle.kts b/features/devtools/presentation/build.gradle.kts index 07b06a3f..1778e9f8 100644 --- a/features/devtools/presentation/build.gradle.kts +++ b/features/devtools/presentation/build.gradle.kts @@ -27,7 +27,7 @@ android { dependencies { // Architecture and presentation layers implementation(project(":libraries:architecture:presentation:mobile")) - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Authentication modules for user session and sign-in management implementation(project(":libraries:authentication:presentation:mobile")) diff --git a/features/history/presentation/mobile/build.gradle.kts b/features/history/presentation/mobile/build.gradle.kts index e849bb19..601cd9a9 100644 --- a/features/history/presentation/mobile/build.gradle.kts +++ b/features/history/presentation/mobile/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation(project(":libraries:authentication:domain")) // Design system for consistent theming and UI components - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Smoke feature dependencies implementation(project(":libraries:smokes:data:mobile")) diff --git a/features/history/presentation/web/build.gradle.kts b/features/history/presentation/web/build.gradle.kts index 81cd575a..ae140533 100644 --- a/features/history/presentation/web/build.gradle.kts +++ b/features/history/presentation/web/build.gradle.kts @@ -15,6 +15,7 @@ kotlin { implementation(compose.runtime) implementation(compose.html.core) + implementation(project(":libraries:design:web")) implementation(project(":libraries:architecture:domain")) implementation(project(":libraries:smokes:domain")) implementation(project(":libraries:authentication:domain")) diff --git a/features/history/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/history/presentation/HistoryWebScreen.kt b/features/history/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/history/presentation/HistoryWebScreen.kt index 2a933c93..4be20e56 100644 --- a/features/history/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/history/presentation/HistoryWebScreen.kt +++ b/features/history/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/features/history/presentation/HistoryWebScreen.kt @@ -9,6 +9,11 @@ import androidx.compose.runtime.remember import com.feragusper.smokeanalytics.features.history.presentation.mvi.HistoryIntent import com.feragusper.smokeanalytics.features.history.presentation.mvi.HistoryResult import com.feragusper.smokeanalytics.features.history.presentation.mvi.HistoryWebStore +import com.feragusper.smokeanalytics.libraries.design.GhostButton +import com.feragusper.smokeanalytics.libraries.design.PrimaryButton +import com.feragusper.smokeanalytics.libraries.design.SmokeRow +import com.feragusper.smokeanalytics.libraries.design.SmokeWebStyles +import com.feragusper.smokeanalytics.libraries.design.SurfaceCard import kotlinx.datetime.DateTimeUnit import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate @@ -21,14 +26,11 @@ import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDateTime import org.jetbrains.compose.web.attributes.InputType import org.jetbrains.compose.web.attributes.disabled -import org.jetbrains.compose.web.dom.Button import org.jetbrains.compose.web.dom.Div -import org.jetbrains.compose.web.dom.H3 import org.jetbrains.compose.web.dom.Input -import org.jetbrains.compose.web.dom.Li +import org.jetbrains.compose.web.dom.P import org.jetbrains.compose.web.dom.Span import org.jetbrains.compose.web.dom.Text -import org.jetbrains.compose.web.dom.Ul @Composable fun HistoryWebScreen( @@ -42,180 +44,193 @@ fun HistoryWebScreen( val state by store.state.collectAsState() val tz = remember { TimeZone.currentSystemDefault() } - // Per-row edit state val editing = remember { mutableStateMapOf() } val draftDateTime = remember { mutableStateMapOf() } - // ✅ Normalize selected day to 00:00 so all "day actions" are consistent. val selectedDayStart = state.selectedDate.dayStart(tz) val selectedLocalDate = selectedDayStart.toLocalDateTime(tz).date val selectedDateLabel = selectedLocalDate.toUiDate() - Div { - H3 { Text("History • $selectedDateLabel") } + Div(attrs = { classes(SmokeWebStyles.mainInner) }) { - if (state.displayLoading) { - Div { Text("Loading...") } + // Title + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { + Text("History • $selectedDateLabel") } + // Errors state.error?.let { err -> - Div { + SurfaceCard { Text( when (err) { HistoryResult.Error.NotLoggedIn -> "Not logged in" HistoryResult.Error.Generic -> "Something went wrong" } ) - } - if (err == HistoryResult.Error.NotLoggedIn) { - Button(attrs = { onClick { onNavigateToAuth() } }) { Text("Go to sign in") } + if (err == HistoryResult.Error.NotLoggedIn) { + Div { + PrimaryButton( + text = "Go to sign in", + onClick = onNavigateToAuth, + enabled = !state.displayLoading + ) + } + } } } - // Controls - Div { - Button(attrs = { onClick { store.send(HistoryIntent.NavigateUp); onNavigateUp() } }) { - Text("Back") - } + // Top actions (Back / Add / Refresh) + Div(attrs = { classes(SmokeWebStyles.statsToolbar) }) { - Span { Text(" ") } - - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { store.send(HistoryIntent.AddSmoke(selectedDayStart)) } - } - ) { Text("Add smoke") } - - Span { Text(" ") } + Div { + GhostButton( + text = "Back", + onClick = { + store.send(HistoryIntent.NavigateUp) + onNavigateUp() + }, + enabled = !state.displayLoading + ) + } - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { store.send(HistoryIntent.FetchSmokes(selectedDayStart)) } - } - ) { Text("Refresh") } + Div { + PrimaryButton( + text = "Add smoke", + onClick = { store.send(HistoryIntent.AddSmoke(selectedDayStart)) }, + enabled = !state.displayLoading + ) + Span { Text(" ") } + GhostButton( + text = "Refresh", + onClick = { store.send(HistoryIntent.FetchSmokes(selectedDayStart)) }, + enabled = !state.displayLoading + ) + } } - // Day navigation + day picker - Div { - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { + // Day navigation + picker + Div(attrs = { classes(SmokeWebStyles.statsToolbar) }) { + Div(attrs = { classes(SmokeWebStyles.dateControls) }) { + + GhostButton( + text = "←", + onClick = { store.send( HistoryIntent.FetchSmokes( selectedDayStart.minusDays(1, tz) ) ) - } - } - ) { Text("←") } - - Span { Text(" ") } - - Input( - type = InputType.Date, - attrs = { - value(selectedLocalDate.toHtmlDate()) - if (state.displayLoading) disabled() - onInput { e -> - val picked = (e.value ?: "").toLocalDateOrNull() ?: return@onInput - store.send(HistoryIntent.FetchSmokes(picked.atStartOfDayIn(tz))) - } - } - ) + }, + enabled = !state.displayLoading + ) - Span { Text(" ") } + Div(attrs = { classes(SmokeWebStyles.dateLabel) }) { + Text(selectedDateLabel) + } - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { + GhostButton( + text = "→", + onClick = { store.send( HistoryIntent.FetchSmokes( selectedDayStart.plusDays(1, tz) ) ) - } - } - ) { Text("→") } - } - - Div { Text("Smokes: ${state.smokes.size}") } + }, + enabled = !state.displayLoading + ) - Ul { - state.smokes.forEach { smoke -> - val id = smoke.id - val isEditing = editing[id] == true + Input( + type = InputType.Date, + attrs = { + classes(SmokeWebStyles.dateInput) + value(selectedLocalDate.toHtmlDate()) + if (state.displayLoading) disabled() + onInput { e -> + val picked = e.value.toLocalDateOrNull() ?: return@onInput + store.send(HistoryIntent.FetchSmokes(picked.atStartOfDayIn(tz))) + } + } + ) + } - val local = smoke.date.toLocalDateTime(tz) - val label = "${local.date.toUiDate()} ${local.toUiTime()}" + Div { + Text("Smokes: ${state.smokes.size}") + } + } - Li { - Text(label) - Span { Text(" ") } + if (state.displayLoading) { + SurfaceCard { Text("Loading...") } + } else if (state.smokes.isEmpty()) { + SurfaceCard { Text("No smokes for this day.") } + } else { + Div(attrs = { classes(SmokeWebStyles.list) }) { + state.smokes.forEach { smoke -> + val id = smoke.id + val isEditing = editing[id] == true + + val local = smoke.date.toLocalDateTime(tz) + val hh = local.hour.toString().padStart(2, '0') + val mm = local.minute.toString().padStart(2, '0') + val timeLabel = "$hh:$mm" + val subtitle = local.date.toUiDate() if (!isEditing) { - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { - editing[id] = true - draftDateTime[id] = smoke.date.toHtmlDateTimeLocal(tz) - } - } - ) { Text("Edit") } - - Span { Text(" ") } - - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { store.send(HistoryIntent.DeleteSmoke(id)) } - } - ) { Text("Delete") } + SmokeRow( + time = timeLabel, + subtitle = subtitle, + onEdit = { + editing[id] = true + draftDateTime[id] = smoke.date.toHtmlDateTimeLocal(tz) + }, + onDelete = { store.send(HistoryIntent.DeleteSmoke(id)) } + ) } else { val draft = draftDateTime[id] ?: smoke.date.toHtmlDateTimeLocal(tz) - Input( - type = InputType.DateTimeLocal, - attrs = { - value(draft) - if (state.displayLoading) disabled() - onInput { ev -> - draftDateTime[id] = ev.value ?: draft - } + SurfaceCard { + Div(attrs = { classes(SmokeWebStyles.sectionTitle) }) { + Text("Edit smoke") } - ) - Span { Text(" ") } - - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { - val v = draftDateTime[id] ?: return@onClick - val newInstant = - v.toInstantFromHtmlDateTimeLocalOrNull(tz) ?: return@onClick - store.send(HistoryIntent.EditSmoke(id, newInstant)) - editing[id] = false + P { Text("Current: ${local.date.toUiDate()} ${local.toUiTime()}") } + + Input( + type = InputType.DateTimeLocal, + attrs = { + classes(SmokeWebStyles.dateInput) + value(draft) + if (state.displayLoading) disabled() + onInput { ev -> + draftDateTime[id] = ev.value + } } - } - ) { Text("Apply") } - - Span { Text(" ") } + ) - Button( - attrs = { - if (state.displayLoading) disabled() - onClick { - editing[id] = false - draftDateTime.remove(id) - } + Div { + PrimaryButton( + text = "Apply", + enabled = !state.displayLoading, + onClick = { + val v = draftDateTime[id] ?: return@PrimaryButton + val newInstant = + v.toInstantFromHtmlDateTimeLocalOrNull(tz) ?: return@PrimaryButton + store.send(HistoryIntent.EditSmoke(id, newInstant)) + editing[id] = false + } + ) + Span { Text(" ") } + GhostButton( + text = "Cancel", + enabled = !state.displayLoading, + onClick = { + editing[id] = false + draftDateTime.remove(id) + } + ) } - ) { Text("Cancel") } + } } } } diff --git a/features/home/presentation/mobile/build.gradle.kts b/features/home/presentation/mobile/build.gradle.kts index 762eed56..6ef6a93b 100644 --- a/features/home/presentation/mobile/build.gradle.kts +++ b/features/home/presentation/mobile/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { implementation(project(":libraries:architecture:domain")) // Design system for consistent theming and UI components - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Authentication modules for user session management implementation(project(":libraries:authentication:domain")) diff --git a/features/settings/presentation/mobile/build.gradle.kts b/features/settings/presentation/mobile/build.gradle.kts index 527cc2c7..763179ec 100644 --- a/features/settings/presentation/mobile/build.gradle.kts +++ b/features/settings/presentation/mobile/build.gradle.kts @@ -29,7 +29,7 @@ dependencies { implementation(project(":libraries:architecture:presentation:mobile")) // Design system for consistent theming and UI components - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Authentication modules for managing user sessions implementation(project(":libraries:authentication:presentation:mobile")) diff --git a/features/settings/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/settings/presentation/mvi/compose/SettingsViewState.kt b/features/settings/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/settings/presentation/mvi/compose/SettingsViewState.kt index 7666bd44..81adc73e 100644 --- a/features/settings/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/settings/presentation/mvi/compose/SettingsViewState.kt +++ b/features/settings/presentation/mobile/src/main/java/com/feragusper/smokeanalytics/features/settings/presentation/mvi/compose/SettingsViewState.kt @@ -170,15 +170,14 @@ private fun LoggedOutView( snackbarHostState: SnackbarHostState ) { val scope = rememberCoroutineScope() - val context = LocalContext.current + val message = + stringResource(com.feragusper.smokeanalytics.libraries.design.mobile.R.string.error_general) GoogleSignInComponent( modifier = Modifier.testTag(SettingsViewState.TestTags.BUTTON_SIGN_IN), onSignInSuccess = onSignInSuccess, onSignInError = { scope.launch { - snackbarHostState.showSnackbar( - context.getString(com.feragusper.smokeanalytics.libraries.design.R.string.error_general) - ) + snackbarHostState.showSnackbar(message) } }, ) diff --git a/features/stats/presentation/mobile/build.gradle.kts b/features/stats/presentation/mobile/build.gradle.kts index 35711bb7..8ef0ddca 100644 --- a/features/stats/presentation/mobile/build.gradle.kts +++ b/features/stats/presentation/mobile/build.gradle.kts @@ -32,7 +32,7 @@ dependencies { implementation(project(":libraries:architecture:presentation:mobile")) // Design system for consistent theming and UI components - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Domain layer for accessing smoke-related data implementation(project(":libraries:smokes:domain")) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d1ff761e..62316721 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,22 +2,22 @@ [versions] accompanist = "0.36.0" androidxAppcompat = "1.7.1" -androidxCompose = "1.9.3" -androidxComposeBOM = "2025.10.00" +androidxCompose = "1.10.0" +androidxComposeBOM = "2025.12.01" androidxCoreKtx = "1.17.0" -androidxNavigationCompose = "2.9.5" +androidxNavigationCompose = "2.9.6" animatedNavigationBar = "1.0.0" composeShimmer = "1.3.3" -composeMultiplatform = "1.8.0" +composeMultiplatform = "1.9.3" coroutinesTest = "1.10.2" credentials = "1.5.0" espressoCore = "3.7.0" -firebaseApp = "1.13.0" -firebaseAuth = "1.13.0" -firebaseBOM = "34.4.0" +firebaseApp = "2.4.0" +firebaseAuth = "2.4.0" +firebaseBOM = "34.7.0" generativeai = "0.9.0" googleid = "1.1.1" -gradle = "8.13.0" +gradle = "8.13.2" hilt = "2.57.2" hiltNavigationCompose = "1.3.0" horologistComposables = "0.7.15" @@ -26,17 +26,17 @@ horologistTiles = "0.7.15" javaxInject = "1" junit = "1.3.0" junit4 = "4.13.2" -junitBOM = "5.11.4" +junitBOM = "6.0.2" kermit = "2.0.8" kluent = "1.73" kotlin = "2.2.20" kotlinxDatetime = "0.6.1" -kotlinxCoroutines = "1.8.1" -kotlinx-serialization = "1.7.3" -kover = "0.9.2" +kotlinxCoroutines = "1.10.2" +kotlinx-serialization = "1.9.0" +kover = "0.9.4" material3 = "1.4.0" -mockk = "1.14.6" -navVersion = "2.9.5" +mockk = "1.14.7" +navVersion = "2.9.6" playServicesWearable = "19.0.0" protolayoutCore = "1.3.0" protolayoutMaterial = "1.3.0" @@ -123,7 +123,7 @@ vico-views = { group = "com.patrykandpatrick.vico", name = "views", version.ref # Plugins [plugins] -buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.15.2" } +buildkonfig = { id = "com.codingfeline.buildkonfig", version = "0.17.1" } compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } compose-multiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } diff --git a/libraries/authentication/presentation/mobile/build.gradle.kts b/libraries/authentication/presentation/mobile/build.gradle.kts index 8f055869..6edaf01b 100644 --- a/libraries/authentication/presentation/mobile/build.gradle.kts +++ b/libraries/authentication/presentation/mobile/build.gradle.kts @@ -34,7 +34,7 @@ android { dependencies { // Include the design library for consistent theming and UI components. - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Use the Compose BOM for consistent Compose library versions. implementation(platform(libs.androidx.compose.bom)) diff --git a/libraries/authentication/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/libraries/authentication/presentation/compose/GoogleSignInComponentWeb.kt b/libraries/authentication/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/libraries/authentication/presentation/compose/GoogleSignInComponentWeb.kt index c98f3717..2fb75745 100644 --- a/libraries/authentication/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/libraries/authentication/presentation/compose/GoogleSignInComponentWeb.kt +++ b/libraries/authentication/presentation/web/src/commonMain/kotlin/com/feragusper/smokeanalytics/libraries/authentication/presentation/compose/GoogleSignInComponentWeb.kt @@ -6,9 +6,8 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import dev.gitlive.firebase.Firebase -import dev.gitlive.firebase.auth.auth import dev.gitlive.firebase.auth.externals.GoogleAuthProvider +import dev.gitlive.firebase.auth.externals.getAuth import dev.gitlive.firebase.auth.externals.signInWithPopup import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.await @@ -39,9 +38,8 @@ fun GoogleSignInComponentWeb( loading = true try { runCatching { - val auth = Firebase.auth val provider = GoogleAuthProvider() - signInWithPopup(auth.js, provider).await() + signInWithPopup(getAuth(), provider).await() }.onSuccess { onSignInSuccess() }.onFailure { t -> diff --git a/libraries/smokes/presentation/build.gradle.kts b/libraries/smokes/presentation/build.gradle.kts index e1c44b01..99b75e76 100644 --- a/libraries/smokes/presentation/build.gradle.kts +++ b/libraries/smokes/presentation/build.gradle.kts @@ -21,7 +21,7 @@ android { dependencies { // Include the design library for consistent theming and UI components. - implementation(project(":libraries:design")) + implementation(project(":libraries:design:mobile")) // Include the architecture domain module for shared domain logic. implementation(project(":libraries:architecture:domain"))