feat(skills): redesign view mode switcher and fix status filter alignment & card padding (#704)

* feat(skills): redesign tab switcher and fix status filter alignment and card padding

* fix(skills): replace status filter chips with SingleChoiceSegmentedButtonRow for precise alignment

* fix(skills): left-align status, category, and source filter chip rows

* fix(skills): explicitly left-align status FilterChips with 16dp start margin

* fix(skills): match status filter chip row styling and All label to category row below it

* fix(skills): remove extra Row wrapper padding from status filter chips in SkillsScreen

* feat(skills): enhance card background contrast and reduce side padding for Installed and Hub cards

* feat(skills): reduce card/search padding to 8dp and pin top controls with weight(1f) on LazyColumn

* style(skills): fix ktlint import-ordering and remove unused imports (PR #704)

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-24 01:10:12 +03:00
1 parent 426f14550e
commit cf18c3a02e
3 files changed
+104 -118

No files matched your search

@@ -1,9 +1,11 @@
package com.m57.hermescontrol.ui.skills package com.m57.hermescontrol.ui.skills
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -33,11 +35,14 @@ import androidx.compose.material3.Button
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilterChip import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SegmentedButton
import androidx.compose.material3.SegmentedButtonDefaults
import androidx.compose.material3.SingleChoiceSegmentedButtonRow
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -76,12 +81,12 @@ import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.ToastEffect import com.m57.hermescontrol.ui.common.ToastEffect
import com.m57.hermescontrol.ui.common.listContentPadding
import com.m57.hermescontrol.ui.common.listItemSpacing import com.m57.hermescontrol.ui.common.listItemSpacing
import com.m57.hermescontrol.ui.common.toDetailRows import com.m57.hermescontrol.ui.common.toDetailRows
internal const val CATEGORY_ALL = "All" internal const val CATEGORY_ALL = "All"
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SkillsScreen( fun SkillsScreen(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
@@ -116,20 +121,26 @@ fun SkillsScreen(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
) { ) {
// ── View mode tabs ────────────────────────────────────── // ── View mode tabs ──────────────────────────────────────
Row( SingleChoiceSegmentedButtonRow(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp, vertical = 8.dp), modifier =
horizontalArrangement = Arrangement.spacedBy(8.dp), Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp, vertical = 6.dp),
) { ) {
FilterChip( SegmentedButton(
selected = state.viewMode == SkillsViewMode.INSTALLED, selected = state.viewMode == SkillsViewMode.INSTALLED,
onClick = { viewModel.setViewMode(SkillsViewMode.INSTALLED) }, onClick = { viewModel.setViewMode(SkillsViewMode.INSTALLED) },
label = { Text(stringResource(R.string.skills_mode_installed)) }, shape = SegmentedButtonDefaults.itemShape(index = 0, count = 2),
) ) {
FilterChip( Text(stringResource(R.string.skills_mode_installed))
}
SegmentedButton(
selected = state.viewMode == SkillsViewMode.HUB, selected = state.viewMode == SkillsViewMode.HUB,
onClick = { viewModel.setViewMode(SkillsViewMode.HUB) }, onClick = { viewModel.setViewMode(SkillsViewMode.HUB) },
label = { Text(stringResource(R.string.skills_mode_hub)) }, shape = SegmentedButtonDefaults.itemShape(index = 1, count = 2),
) ) {
Text(stringResource(R.string.skills_mode_hub))
}
} }
when (state.viewMode) { when (state.viewMode) {
@@ -261,27 +272,24 @@ private fun InstalledSkillsView(
query = query, query = query,
onQueryChange = onQueryChange, onQueryChange = onQueryChange,
placeholder = stringResource(R.string.skills_search_placeholder), placeholder = stringResource(R.string.skills_search_placeholder),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
) )
Row( FilterChipRow(
modifier = chips = SkillFilter.entries.toList(),
Modifier selectedChip = selectedStatus,
.fillMaxWidth() onChipSelected = onStatusChange,
.padding(horizontal = 16.dp, vertical = 4.dp), chipLabel = { chip ->
horizontalArrangement = Arrangement.spacedBy(8.dp), Text(
) { text =
FilterChipRow( if (chip == SkillFilter.ALL_STATUSES) {
chips = stringResource(R.string.skills_category_all)
listOf( } else {
SkillFilter.ALL_STATUSES, stringResource(chip.labelRes)
SkillFilter.ENABLED, },
SkillFilter.DISABLED, )
), },
selectedChip = selectedStatus, )
onChipSelected = onStatusChange,
chipLabel = { chip -> Text(stringResource(chip.labelRes)) },
)
}
if (categories.isNotEmpty() || sources.isNotEmpty()) { if (categories.isNotEmpty() || sources.isNotEmpty()) {
FilterChipRow( FilterChipRow(
@@ -290,31 +298,17 @@ private fun InstalledSkillsView(
onChipSelected = onCategoryChange, onChipSelected = onCategoryChange,
) )
if (sources.isNotEmpty()) { if (sources.isNotEmpty()) {
Row( FilterChipRow(
modifier = chips = listOf<String?>(null) + sources,
Modifier selectedChip = sourceFilter,
.fillMaxWidth() onChipSelected = onSetSourceFilter,
.padding(horizontal = 16.dp, vertical = 2.dp), chipLabel = { source ->
horizontalArrangement = Arrangement.spacedBy(6.dp), Text(
) { text = source ?: stringResource(R.string.skills_source_all),
FilterChip( style = MaterialTheme.typography.labelSmall,
selected = sourceFilter == null,
onClick = { onSetSourceFilter(null) },
label = {
Text(
stringResource(R.string.skills_source_all),
style = MaterialTheme.typography.labelSmall,
)
},
)
sources.forEach { source ->
FilterChip(
selected = sourceFilter == source,
onClick = { onSetSourceFilter(if (sourceFilter == source) null else source) },
label = { Text(source, style = MaterialTheme.typography.labelSmall) },
) )
} },
} )
} }
} }
@@ -341,7 +335,8 @@ private fun InstalledSkillsView(
else -> { else -> {
LazyColumn( LazyColumn(
modifier = Modifier.padding(listContentPadding), modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
verticalArrangement = listItemSpacing, verticalArrangement = listItemSpacing,
) { ) {
itemsIndexed( itemsIndexed(
@@ -388,18 +383,19 @@ private fun SkillCard(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).clickable(onClick = onClick),
colors = colors =
CardDefaults.cardColors( CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column( Column(
modifier = Modifier.weight(1f).padding(16.dp), modifier = Modifier.weight(1f).padding(horizontal = 8.dp, vertical = 8.dp),
) { ) {
// ── Top row: name + source badge + toggle ── // ── Top row: name + source badge + toggle ──
Row( Row(
@@ -549,7 +545,7 @@ private fun HubBrowseView(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(horizontal = 8.dp, vertical = 6.dp),
placeholder = { Text(stringResource(R.string.skills_hub_search_placeholder)) }, placeholder = { Text(stringResource(R.string.skills_hub_search_placeholder)) },
trailingIcon = { trailingIcon = {
Row( Row(
@@ -611,7 +607,8 @@ private fun HubBrowseView(
state.hubResults.isNotEmpty() -> { state.hubResults.isNotEmpty() -> {
LazyColumn( LazyColumn(
modifier = Modifier.weight(1f).padding(listContentPadding), modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
verticalArrangement = listItemSpacing, verticalArrangement = listItemSpacing,
) { ) {
itemsIndexed( itemsIndexed(
@@ -679,14 +676,15 @@ private fun HubSkillCard(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick), modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp).clickable(onClick = onClick),
colors = colors =
CardDefaults.cardColors( CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth().padding(16.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp),
) { ) {
// ── Top row: name ── // ── Top row: name ──
Text( Text(
@@ -1,8 +1,10 @@
package com.m57.hermescontrol.ui.skills.components package com.m57.hermescontrol.ui.skills.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -49,7 +51,6 @@ import com.m57.hermescontrol.ui.common.DetailRow
import com.m57.hermescontrol.ui.common.EmptyState import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.SkeletonListState import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.listContentPadding
import com.m57.hermescontrol.ui.common.listItemSpacing import com.m57.hermescontrol.ui.common.listItemSpacing
import com.m57.hermescontrol.ui.skills.SkillsUiState import com.m57.hermescontrol.ui.skills.SkillsUiState
@@ -74,7 +75,7 @@ internal fun HubBrowseView(
modifier = modifier =
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(16.dp), .padding(horizontal = 8.dp, vertical = 6.dp),
placeholder = { Text(stringResource(R.string.skills_hub_search_placeholder)) }, placeholder = { Text(stringResource(R.string.skills_hub_search_placeholder)) },
trailingIcon = { trailingIcon = {
Row( Row(
@@ -138,7 +139,8 @@ internal fun HubBrowseView(
state.hubResults.isNotEmpty() -> { state.hubResults.isNotEmpty() -> {
LazyColumn( LazyColumn(
modifier = Modifier.weight(1f).padding(listContentPadding), modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
verticalArrangement = listItemSpacing, verticalArrangement = listItemSpacing,
) { ) {
itemsIndexed( itemsIndexed(
@@ -204,14 +206,15 @@ private fun HubSkillCard(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick), modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
colors = colors =
CardDefaults.cardColors( CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Column( Column(
modifier = Modifier.fillMaxWidth().padding(16.dp), modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 8.dp),
) { ) {
// ── Top row: name + source badge ── // ── Top row: name + source badge ──
Row( Row(
@@ -1,9 +1,11 @@
package com.m57.hermescontrol.ui.skills.components package com.m57.hermescontrol.ui.skills.components
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
@@ -23,7 +25,6 @@ import androidx.compose.material.icons.filled.Extension
import androidx.compose.material3.Card import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@@ -55,7 +56,6 @@ import com.m57.hermescontrol.ui.common.LoadingState
import com.m57.hermescontrol.ui.common.NavIcon import com.m57.hermescontrol.ui.common.NavIcon
import com.m57.hermescontrol.ui.common.SearchBar import com.m57.hermescontrol.ui.common.SearchBar
import com.m57.hermescontrol.ui.common.SkeletonListState import com.m57.hermescontrol.ui.common.SkeletonListState
import com.m57.hermescontrol.ui.common.listContentPadding
import com.m57.hermescontrol.ui.common.listItemSpacing import com.m57.hermescontrol.ui.common.listItemSpacing
import com.m57.hermescontrol.ui.common.toDetailRows import com.m57.hermescontrol.ui.common.toDetailRows
import com.m57.hermescontrol.ui.skills.CATEGORY_ALL import com.m57.hermescontrol.ui.skills.CATEGORY_ALL
@@ -119,27 +119,24 @@ internal fun InstalledSkillsView(
query = query, query = query,
onQueryChange = onQueryChange, onQueryChange = onQueryChange,
placeholder = stringResource(R.string.skills_search_placeholder), placeholder = stringResource(R.string.skills_search_placeholder),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
) )
Row( FilterChipRow(
modifier = chips = SkillFilter.entries.toList(),
Modifier selectedChip = selectedStatus,
.fillMaxWidth() onChipSelected = onStatusChange,
.padding(horizontal = 16.dp, vertical = 4.dp), chipLabel = { chip ->
horizontalArrangement = Arrangement.spacedBy(8.dp), Text(
) { text =
FilterChipRow( if (chip == SkillFilter.ALL_STATUSES) {
chips = stringResource(R.string.skills_category_all)
listOf( } else {
SkillFilter.ALL_STATUSES, stringResource(chip.labelRes)
SkillFilter.ENABLED, },
SkillFilter.DISABLED, )
), },
selectedChip = selectedStatus, )
onChipSelected = onStatusChange,
chipLabel = { chip -> Text(stringResource(chip.labelRes)) },
)
}
if (categories.isNotEmpty() || sources.isNotEmpty()) { if (categories.isNotEmpty() || sources.isNotEmpty()) {
FilterChipRow( FilterChipRow(
@@ -148,31 +145,17 @@ internal fun InstalledSkillsView(
onChipSelected = onCategoryChange, onChipSelected = onCategoryChange,
) )
if (sources.isNotEmpty()) { if (sources.isNotEmpty()) {
Row( FilterChipRow(
modifier = chips = listOf<String?>(null) + sources,
Modifier selectedChip = sourceFilter,
.fillMaxWidth() onChipSelected = onSetSourceFilter,
.padding(horizontal = 16.dp, vertical = 2.dp), chipLabel = { source ->
horizontalArrangement = Arrangement.spacedBy(6.dp), Text(
) { text = source ?: stringResource(R.string.skills_source_all),
FilterChip( style = MaterialTheme.typography.labelSmall,
selected = sourceFilter == null,
onClick = { onSetSourceFilter(null) },
label = {
Text(
stringResource(R.string.skills_source_all),
style = MaterialTheme.typography.labelSmall,
)
},
)
sources.forEach { source ->
FilterChip(
selected = sourceFilter == source,
onClick = { onSetSourceFilter(if (sourceFilter == source) null else source) },
label = { Text(source, style = MaterialTheme.typography.labelSmall) },
) )
} },
} )
} }
} }
@@ -197,7 +180,8 @@ internal fun InstalledSkillsView(
else -> { else -> {
LazyColumn( LazyColumn(
modifier = Modifier.padding(listContentPadding), modifier = Modifier.weight(1f),
contentPadding = PaddingValues(horizontal = 8.dp, vertical = 8.dp),
verticalArrangement = listItemSpacing, verticalArrangement = listItemSpacing,
) { ) {
itemsIndexed( itemsIndexed(
@@ -244,18 +228,19 @@ private fun SkillCard(
onClick: () -> Unit, onClick: () -> Unit,
) { ) {
Card( Card(
modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).clickable(onClick = onClick), modifier = Modifier.fillMaxWidth().clickable(onClick = onClick),
colors = colors =
CardDefaults.cardColors( CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surface, containerColor = MaterialTheme.colorScheme.surfaceContainerHigh,
), ),
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant.copy(alpha = 0.5f)),
) { ) {
Row( Row(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
Column( Column(
modifier = Modifier.weight(1f).padding(16.dp), modifier = Modifier.weight(1f).padding(horizontal = 8.dp, vertical = 8.dp),
) { ) {
// ── Top row: name + source badge + toggle ── // ── Top row: name + source badge + toggle ──
Row( Row(