feat: Material You design system rewrite — Phases 1–4

Complete UI/UX rewrite of hermes-mobile with Material You design system.

Phase 1: Color/Theme/Type/Shapes/Spacing/Motion tokens
Phase 2: HermesScaffold, StateViews (skeleton/empty/error), SearchBar, StatusBadge, ToggleRow
Phase 3: Bottom nav + drawer navigation with NavigationController
Phase 4: All 17 screens rewritten with HermesScaffold and Material You patterns

Closes PR #3.
This commit is contained in:
M57 authored and GitHub committed 2026-06-18 16:34:44 +03:00
1 parent f833005bec
commit d90e36974c
29 files changed
+2669 -2377

No files matched your search

@@ -2,7 +2,6 @@ package com.m57.hermescontrol.ui.logs
import android.widget.Toast
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
@@ -11,30 +10,25 @@ import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material.icons.filled.HistoryEdu
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun LogsScreen(
modifier: Modifier = Modifier,
@@ -43,6 +37,7 @@ fun LogsScreen(
) {
val state by viewModel.uiState.collectAsStateWithLifecycle()
val context = LocalContext.current
val spacing = LocalSpacing.current
val listState = rememberLazyListState()
LaunchedEffect(Unit) {
@@ -63,73 +58,43 @@ fun LogsScreen(
}
}
Scaffold(
topBar = {
TopAppBar(
navigationIcon = {
if (onOpenDrawer != null) {
IconButton(onClick = onOpenDrawer) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "Open Drawer",
)
}
}
},
title = { Text("System Logs") },
actions = {
IconButton(onClick = { viewModel.loadLogs() }) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = "Refresh",
)
}
},
)
},
modifier = modifier,
) { paddingValues ->
Box(
modifier =
Modifier
.fillMaxSize()
.padding(paddingValues)
.background(Color.Black),
) {
if (state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center), color = Color.Green)
} else if (state.errorMessage != null) {
Text(
text = state.errorMessage ?: "",
color = MaterialTheme.colorScheme.error,
modifier =
Modifier
.align(Alignment.Center)
.padding(16.dp),
HermesScaffold(
title = "Logs",
onOpenDrawer = onOpenDrawer,
onRefresh = { viewModel.loadLogs() },
) {
when {
state.isLoading -> LoadingState()
state.errorMessage != null ->
ErrorState(
message = state.errorMessage ?: "Unknown error",
onRetry = { viewModel.loadLogs() },
)
} else if (state.logs.isEmpty()) {
// B5 (Jun 18 2026, kanban t_2322818d): explicit empty-state UX.
// Sibling screens (PairingScreen.kt, WebhooksScreen.kt) did this;
// LogsScreen didn't — when logs were empty and not loading, the
// black Box rendered with NO content visible to the user.
Text(
text = "No logs available",
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier =
Modifier
.align(Alignment.Center)
.padding(16.dp),
state.logs.isEmpty() ->
// B5 (Jun 18 2026, kanban t_2322818d): proper empty-state using
// the shared EmptyState component instead of a bare Text.
EmptyState(
title = "No logs available",
subtitle = "Log output from Hermes will appear here.",
icon = Icons.Filled.HistoryEdu,
)
} else {
else ->
LazyColumn(
state = listState,
modifier = Modifier.fillMaxSize(),
contentPadding = PaddingValues(16.dp),
modifier =
Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.surfaceContainerLowest),
contentPadding =
PaddingValues(
horizontal = spacing.md,
vertical = spacing.sm,
),
) {
items(state.logs) { logLine ->
Text(
text = logLine,
color = Color(0xFF00FF00),
color = MaterialTheme.colorScheme.onSurface,
fontFamily = FontFamily.Monospace,
fontSize = 12.sp,
modifier =
@@ -139,7 +104,6 @@ fun LogsScreen(
)
}
}
}
}
}
}