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