Skip to content

Commit 81990c9

Browse files
authored
Merge pull request #13 from vssinghh/claude/ui-redesign-0aqgdz
Reimagine the UI with the "Nocturne" design language
2 parents c3aa3fd + 15dd1d0 commit 81990c9

10 files changed

Lines changed: 884 additions & 811 deletions

File tree

app/src/main/java/com/hush/app/ui/components/HushComponents.kt

Lines changed: 48 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package com.hush.app.ui.components
22

3+
import androidx.compose.foundation.BorderStroke
34
import androidx.compose.foundation.background
5+
import androidx.compose.foundation.border
46
import androidx.compose.foundation.layout.Arrangement
57
import androidx.compose.foundation.layout.Box
68
import androidx.compose.foundation.layout.Column
@@ -13,6 +15,7 @@ import androidx.compose.foundation.layout.size
1315
import androidx.compose.foundation.layout.width
1416
import androidx.compose.foundation.shape.CircleShape
1517
import androidx.compose.foundation.shape.RoundedCornerShape
18+
import androidx.compose.material3.Icon
1619
import androidx.compose.material3.MaterialTheme
1720
import androidx.compose.material3.Surface
1821
import androidx.compose.material3.Text
@@ -24,57 +27,40 @@ import androidx.compose.ui.graphics.Brush
2427
import androidx.compose.ui.graphics.Color
2528
import androidx.compose.ui.graphics.vector.ImageVector
2629
import androidx.compose.ui.text.font.FontWeight
30+
import androidx.compose.ui.text.style.TextAlign
2731
import androidx.compose.ui.unit.dp
28-
import androidx.compose.material3.Icon
29-
import com.hush.app.ui.theme.AccentPurple
32+
import com.hush.app.ui.theme.PlumMist
3033

31-
/** Gradient used for brand moments (logo chip, hero icons, user bubbles). */
32-
val HushGradient = Brush.linearGradient(
33-
listOf(Color(0xFF7C5CFC), Color(0xFF9E7BFF), Color(0xFF5CA8FC))
34+
/** Dusk-sky gradient, reserved for rare brand moments (onboarding hero). */
35+
val DuskGradient = Brush.linearGradient(
36+
listOf(Color(0xFF3E3663), Color(0xFF6B5490), Color(0xFF96688F))
3437
)
3538

3639
/**
37-
* Large screen header used at the top of every tab: bold title, quiet
38-
* subtitle, and an optional trailing element (status pill, count badge).
40+
* Editorial screen header: a large serif title over a quiet one-line
41+
* subtitle, with an optional trailing element (status pill, action).
3942
*/
4043
@Composable
4144
fun HushHeader(
4245
title: String,
4346
subtitle: String,
4447
modifier: Modifier = Modifier,
45-
leadingIcon: ImageVector? = null,
4648
trailing: (@Composable () -> Unit)? = null
4749
) {
4850
Row(
4951
modifier = modifier
5052
.fillMaxWidth()
51-
.padding(horizontal = 20.dp, vertical = 14.dp),
53+
.padding(horizontal = 24.dp)
54+
.padding(top = 24.dp, bottom = 14.dp),
5255
verticalAlignment = Alignment.CenterVertically
5356
) {
54-
if (leadingIcon != null) {
55-
Box(
56-
modifier = Modifier
57-
.size(42.dp)
58-
.clip(RoundedCornerShape(14.dp))
59-
.background(HushGradient),
60-
contentAlignment = Alignment.Center
61-
) {
62-
Icon(
63-
imageVector = leadingIcon,
64-
contentDescription = null,
65-
tint = Color.White,
66-
modifier = Modifier.size(22.dp)
67-
)
68-
}
69-
Spacer(modifier = Modifier.width(12.dp))
70-
}
7157
Column(modifier = Modifier.weight(1f)) {
7258
Text(
7359
text = title,
7460
style = MaterialTheme.typography.headlineMedium,
7561
color = MaterialTheme.colorScheme.onBackground
7662
)
77-
Spacer(modifier = Modifier.height(1.dp))
63+
Spacer(modifier = Modifier.height(3.dp))
7864
Text(
7965
text = subtitle,
8066
style = MaterialTheme.typography.bodySmall,
@@ -88,17 +74,17 @@ fun HushHeader(
8874
}
8975
}
9076

91-
/** Small colored status pill, e.g. "● On-device AI". */
77+
/** Small status pill: hairline ring, soft wash, and a colored dot. */
9278
@Composable
9379
fun StatusPill(
9480
label: String,
9581
color: Color,
96-
background: Color,
9782
modifier: Modifier = Modifier
9883
) {
9984
Surface(
10085
shape = CircleShape,
101-
color = background,
86+
color = color.copy(alpha = 0.08f),
87+
border = BorderStroke(1.dp, color.copy(alpha = 0.28f)),
10288
modifier = modifier
10389
) {
10490
Row(
@@ -108,7 +94,7 @@ fun StatusPill(
10894
) {
10995
Box(
11096
modifier = Modifier
111-
.size(7.dp)
97+
.size(6.dp)
11298
.clip(CircleShape)
11399
.background(color)
114100
)
@@ -122,17 +108,18 @@ fun StatusPill(
122108
}
123109
}
124110

125-
/** Tiny labeled chip used inside cards to show rule attributes. */
111+
/** Tiny outlined chip used inside cards to show rule attributes. */
126112
@Composable
127113
fun AttributeChip(
128114
icon: ImageVector?,
129115
label: String,
130-
color: Color = AccentPurple,
116+
color: Color = PlumMist,
131117
modifier: Modifier = Modifier
132118
) {
133119
Surface(
134120
shape = RoundedCornerShape(8.dp),
135-
color = color.copy(alpha = 0.12f),
121+
color = Color.Transparent,
122+
border = BorderStroke(1.dp, color.copy(alpha = 0.35f)),
136123
modifier = modifier
137124
) {
138125
Row(
@@ -158,7 +145,25 @@ fun AttributeChip(
158145
}
159146
}
160147

161-
/** Shared empty-state layout: soft icon disc, title, and helper text. */
148+
/** Flat card with a hairline border — the Nocturne surface idiom. */
149+
@Composable
150+
fun QuietSurface(
151+
modifier: Modifier = Modifier,
152+
shape: androidx.compose.ui.graphics.Shape = MaterialTheme.shapes.large,
153+
color: Color = MaterialTheme.colorScheme.surfaceVariant,
154+
borderColor: Color = MaterialTheme.colorScheme.outline,
155+
content: @Composable () -> Unit
156+
) {
157+
Surface(
158+
modifier = modifier,
159+
shape = shape,
160+
color = color,
161+
border = BorderStroke(1.dp, borderColor),
162+
content = content
163+
)
164+
}
165+
166+
/** Shared empty-state layout: outlined icon ring, serif title, helper text. */
162167
@Composable
163168
fun EmptyState(
164169
icon: ImageVector,
@@ -174,28 +179,30 @@ fun EmptyState(
174179
modifier = Modifier
175180
.size(88.dp)
176181
.clip(CircleShape)
177-
.background(AccentPurple.copy(alpha = 0.10f)),
182+
.border(1.dp, MaterialTheme.colorScheme.outline, CircleShape)
183+
.background(MaterialTheme.colorScheme.surfaceVariant),
178184
contentAlignment = Alignment.Center
179185
) {
180186
Icon(
181187
imageVector = icon,
182188
contentDescription = null,
183-
modifier = Modifier.size(40.dp),
184-
tint = AccentPurple.copy(alpha = 0.8f)
189+
modifier = Modifier.size(36.dp),
190+
tint = MaterialTheme.colorScheme.onSurfaceVariant
185191
)
186192
}
187193
Spacer(modifier = Modifier.height(20.dp))
188194
Text(
189195
title,
190-
style = MaterialTheme.typography.titleMedium,
191-
color = MaterialTheme.colorScheme.onSurface
196+
style = MaterialTheme.typography.titleLarge,
197+
color = MaterialTheme.colorScheme.onSurface,
198+
textAlign = TextAlign.Center
192199
)
193200
Spacer(modifier = Modifier.height(6.dp))
194201
Text(
195202
message,
196203
style = MaterialTheme.typography.bodySmall,
197204
color = MaterialTheme.colorScheme.onSurfaceVariant,
198-
textAlign = androidx.compose.ui.text.style.TextAlign.Center
205+
textAlign = TextAlign.Center
199206
)
200207
}
201208
}
Lines changed: 88 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
package com.hush.app.ui.screens
22

3+
import androidx.compose.animation.animateContentSize
34
import androidx.compose.animation.core.tween
45
import androidx.compose.animation.fadeIn
56
import androidx.compose.animation.fadeOut
67
import androidx.compose.animation.scaleIn
8+
import androidx.compose.foundation.BorderStroke
9+
import androidx.compose.foundation.layout.Arrangement
10+
import androidx.compose.foundation.layout.Box
11+
import androidx.compose.foundation.layout.Row
12+
import androidx.compose.foundation.layout.Spacer
13+
import androidx.compose.foundation.layout.fillMaxWidth
14+
import androidx.compose.foundation.layout.navigationBarsPadding
715
import androidx.compose.foundation.layout.padding
16+
import androidx.compose.foundation.layout.size
17+
import androidx.compose.foundation.layout.width
18+
import androidx.compose.foundation.shape.CircleShape
819
import androidx.compose.material3.Icon
920
import androidx.compose.material3.MaterialTheme
10-
import androidx.compose.material3.NavigationBar
11-
import androidx.compose.material3.NavigationBarItem
12-
import androidx.compose.material3.NavigationBarItemDefaults
1321
import androidx.compose.material3.Scaffold
22+
import androidx.compose.material3.Surface
1423
import androidx.compose.material3.Text
1524
import androidx.compose.runtime.Composable
1625
import androidx.compose.runtime.getValue
26+
import androidx.compose.ui.Alignment
1727
import androidx.compose.ui.Modifier
28+
import androidx.compose.ui.graphics.Color
1829
import androidx.compose.ui.platform.testTag
19-
import androidx.compose.ui.text.font.FontWeight
2030
import androidx.compose.ui.unit.dp
2131
import androidx.navigation.NavGraph.Companion.findStartDestination
2232
import androidx.navigation.compose.NavHost
@@ -43,48 +53,47 @@ fun MainScreen(
4353
)
4454

4555
Scaffold(
56+
containerColor = MaterialTheme.colorScheme.background,
4657
bottomBar = {
47-
NavigationBar(
48-
containerColor = MaterialTheme.colorScheme.surfaceContainer,
49-
tonalElevation = 0.dp
58+
// Floating pill navigation — a single quiet island instead of a
59+
// full-width bar. Selected tab expands into a filled pill.
60+
Box(
61+
modifier = Modifier
62+
.fillMaxWidth()
63+
.navigationBarsPadding()
64+
.padding(horizontal = 24.dp)
65+
.padding(top = 8.dp, bottom = 14.dp),
66+
contentAlignment = Alignment.Center
5067
) {
51-
val navBackStackEntry by childNavController.currentBackStackEntryAsState()
52-
val currentRoute = navBackStackEntry?.destination?.route
68+
Surface(
69+
shape = CircleShape,
70+
color = MaterialTheme.colorScheme.surfaceVariant,
71+
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outline)
72+
) {
73+
val navBackStackEntry by childNavController.currentBackStackEntryAsState()
74+
val currentRoute = navBackStackEntry?.destination?.route
5375

54-
tabs.forEach { tab ->
55-
val selected = currentRoute == tab.route
56-
NavigationBarItem(
57-
icon = {
58-
Icon(
59-
if (selected) tab.selectedIcon else tab.icon,
60-
contentDescription = tab.title
61-
)
62-
},
63-
label = {
64-
Text(
65-
tab.title,
66-
fontWeight = if (selected) FontWeight.SemiBold else FontWeight.Medium
67-
)
68-
},
69-
selected = selected,
70-
colors = NavigationBarItemDefaults.colors(
71-
indicatorColor = MaterialTheme.colorScheme.secondaryContainer,
72-
selectedIconColor = MaterialTheme.colorScheme.onSecondaryContainer,
73-
selectedTextColor = MaterialTheme.colorScheme.onSurface,
74-
unselectedIconColor = MaterialTheme.colorScheme.onSurfaceVariant,
75-
unselectedTextColor = MaterialTheme.colorScheme.onSurfaceVariant
76-
),
77-
onClick = {
78-
childNavController.navigate(tab.route) {
79-
popUpTo(childNavController.graph.findStartDestination().id) {
80-
saveState = true
76+
Row(
77+
modifier = Modifier.padding(6.dp),
78+
horizontalArrangement = Arrangement.spacedBy(2.dp),
79+
verticalAlignment = Alignment.CenterVertically
80+
) {
81+
tabs.forEach { tab ->
82+
NavPill(
83+
tab = tab,
84+
selected = currentRoute == tab.route,
85+
onClick = {
86+
childNavController.navigate(tab.route) {
87+
popUpTo(childNavController.graph.findStartDestination().id) {
88+
saveState = true
89+
}
90+
launchSingleTop = true
91+
restoreState = true
92+
}
8193
}
82-
launchSingleTop = true
83-
restoreState = true
84-
}
85-
},
86-
modifier = Modifier.testTag("bottom_nav_${tab.route}")
87-
)
94+
)
95+
}
96+
}
8897
}
8998
}
9099
},
@@ -94,7 +103,7 @@ fun MainScreen(
94103
navController = childNavController,
95104
startDestination = BottomTabRoute.Chat.route,
96105
modifier = Modifier.padding(innerPadding),
97-
// Gentle fade + settle between tabs — modern, not distracting
106+
// Gentle fade + settle between tabs — calm, not distracting
98107
enterTransition = {
99108
fadeIn(tween(220)) + scaleIn(initialScale = 0.985f, animationSpec = tween(220))
100109
},
@@ -115,3 +124,39 @@ fun MainScreen(
115124
}
116125
}
117126
}
127+
128+
@Composable
129+
private fun NavPill(
130+
tab: BottomTabRoute,
131+
selected: Boolean,
132+
onClick: () -> Unit
133+
) {
134+
Surface(
135+
onClick = onClick,
136+
shape = CircleShape,
137+
color = if (selected) MaterialTheme.colorScheme.primary else Color.Transparent,
138+
contentColor = if (selected) MaterialTheme.colorScheme.onPrimary
139+
else MaterialTheme.colorScheme.onSurfaceVariant,
140+
modifier = Modifier.testTag("bottom_nav_${tab.route}")
141+
) {
142+
Row(
143+
verticalAlignment = Alignment.CenterVertically,
144+
modifier = Modifier
145+
.animateContentSize(animationSpec = tween(200))
146+
.padding(horizontal = if (selected) 16.dp else 12.dp, vertical = 10.dp)
147+
) {
148+
Icon(
149+
imageVector = if (selected) tab.selectedIcon else tab.icon,
150+
contentDescription = tab.title,
151+
modifier = Modifier.size(20.dp)
152+
)
153+
if (selected) {
154+
Spacer(modifier = Modifier.width(7.dp))
155+
Text(
156+
text = tab.title,
157+
style = MaterialTheme.typography.labelLarge
158+
)
159+
}
160+
}
161+
}
162+
}

0 commit comments

Comments
 (0)