feat: match Happy Engineering mobile screenshots
This commit is contained in:
@@ -105,11 +105,9 @@ import java.net.URLEncoder
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
enum class Screen(val label: String, val icon: ImageVector, val title: String, val kicker: String) {
|
||||
Quest("Chat", Icons.Filled.ChatBubble, "Agent", "Claude Code mobile client"),
|
||||
Files("Files", Icons.Filled.Folder, "Workspace", "Project files"),
|
||||
Terminal("Terminal", Icons.Filled.Terminal, "Terminal", "Run commands"),
|
||||
Status("Status", Icons.Outlined.Bolt, "Sessions", "Companion status"),
|
||||
Settings("Settings", Icons.Filled.Settings, "Settings", "Pair companion")
|
||||
Inbox("Inbox", Icons.Filled.ChatBubble, "Inbox", "connected"),
|
||||
Terminals("Terminals", Icons.Filled.Terminal, "Terminals", "connected"),
|
||||
Settings("Settings", Icons.Filled.Settings, "Settings", "connected")
|
||||
}
|
||||
|
||||
data class FileEntry(val name: String, val path: String, val type: String, val size: Long)
|
||||
@@ -231,7 +229,7 @@ class MainActivity : ComponentActivity() {
|
||||
fun HermesNativeApp() {
|
||||
val context = LocalContext.current
|
||||
val api = remember { CompanionApi(context.applicationContext) }
|
||||
var screen by remember { mutableStateOf(Screen.Quest) }
|
||||
var screen by remember { mutableStateOf(Screen.Inbox) }
|
||||
var online by remember { mutableStateOf(false) }
|
||||
|
||||
LaunchedEffect(screen) {
|
||||
@@ -240,8 +238,8 @@ fun HermesNativeApp() {
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = darkColorScheme(
|
||||
background = Cream,
|
||||
surface = Paper,
|
||||
background = AppBackground,
|
||||
surface = RowSurface,
|
||||
primary = Teal,
|
||||
onPrimary = Color.Black,
|
||||
onBackground = Ink,
|
||||
@@ -250,7 +248,7 @@ fun HermesNativeApp() {
|
||||
) {
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
containerColor = Cream,
|
||||
containerColor = AppBackground,
|
||||
topBar = { NativeTopBar(screen, online) },
|
||||
bottomBar = {
|
||||
NativeBottomBar(
|
||||
@@ -272,10 +270,8 @@ fun HermesNativeApp() {
|
||||
label = "screen-slide"
|
||||
) { active ->
|
||||
when (active) {
|
||||
Screen.Quest -> QuestScreen(api)
|
||||
Screen.Files -> FilesScreen(api)
|
||||
Screen.Terminal -> TerminalScreen(api)
|
||||
Screen.Status -> StatusScreen(api)
|
||||
Screen.Inbox -> InboxScreen(api)
|
||||
Screen.Terminals -> TerminalsScreen(api)
|
||||
Screen.Settings -> SettingsScreen(api)
|
||||
}
|
||||
}
|
||||
@@ -312,37 +308,26 @@ fun NativeTopBar(screen: Screen, online: Boolean) {
|
||||
.padding(horizontal = 16.dp, vertical = 10.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp)
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
RaisedCircle(color = Purple, shadow = PurpleDark, content = "H")
|
||||
Column(Modifier.weight(1f)) {
|
||||
TinyLabel(screen.kicker)
|
||||
Text(screen.title, fontSize = 30.sp, fontWeight = FontWeight.Black, color = Ink, lineHeight = 31.sp)
|
||||
}
|
||||
StatusDot(online)
|
||||
}
|
||||
RaisedCard(
|
||||
color = if (online) TealPanel else WarningPanel,
|
||||
shadow = PanelShadow,
|
||||
border = if (online) TealBorder else WarningBorder,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Icon(if (online) Icons.Filled.Wifi else Icons.Filled.WifiOff, contentDescription = null, tint = if (online) Teal else Orange)
|
||||
Text(if (online) "Companion connected" else "Set companion URL and access key", color = if (online) Teal else Orange, fontWeight = FontWeight.Black)
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
Text(screen.title, Modifier.weight(1f), fontSize = 24.sp, fontWeight = FontWeight.SemiBold, color = Ink, lineHeight = 28.sp)
|
||||
if (screen == Screen.Terminals) Text("+", color = Ink, fontSize = 26.sp, fontWeight = FontWeight.Light)
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp)) {
|
||||
Box(Modifier.size(8.dp).clip(CircleShape).background(if (online) Teal else Muted))
|
||||
Text(if (online) "connected" else "offline", color = if (online) Teal else Muted, fontWeight = FontWeight.SemiBold, fontSize = 13.sp)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
|
||||
Surface(shadowElevation = 14.dp, color = Paper, tonalElevation = 0.dp) {
|
||||
Surface(shadowElevation = 0.dp, color = AppBackground, tonalElevation = 0.dp) {
|
||||
NavigationBar(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.navigationBarsPadding()
|
||||
.height(82.dp),
|
||||
containerColor = Paper,
|
||||
.height(76.dp),
|
||||
containerColor = AppBackground,
|
||||
tonalElevation = 0.dp
|
||||
) {
|
||||
Screen.entries.forEach { item ->
|
||||
@@ -352,9 +337,9 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
|
||||
icon = { Icon(item.icon, contentDescription = item.label) },
|
||||
label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) },
|
||||
colors = NavigationBarItemDefaults.colors(
|
||||
selectedIconColor = Teal,
|
||||
selectedTextColor = Teal,
|
||||
indicatorColor = TealPanel,
|
||||
selectedIconColor = Ink,
|
||||
selectedTextColor = Ink,
|
||||
indicatorColor = Color.Transparent,
|
||||
unselectedIconColor = Muted,
|
||||
unselectedTextColor = Muted
|
||||
)
|
||||
@@ -365,7 +350,7 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun QuestScreen(api: CompanionApi) {
|
||||
fun InboxScreen(api: CompanionApi) {
|
||||
val scope = remember { CoroutineScope(Dispatchers.Main) }
|
||||
var prompt by remember { mutableStateOf("") }
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
@@ -373,12 +358,7 @@ fun QuestScreen(api: CompanionApi) {
|
||||
val messages = remember { mutableStateListOf<ChatMessage>() }
|
||||
|
||||
ScreenList {
|
||||
HeroCard(icon = Icons.Filled.ChatBubble, title = "Claude Code from anywhere.", label = "Agent control", text = "Send prompts to your local Hermes companion and continue work from your phone.")
|
||||
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
|
||||
PathPill(Icons.Filled.CheckCircle, "Connect", true)
|
||||
PathPill(Icons.Outlined.Bolt, "Prompt", true, current = true)
|
||||
PathPill(Icons.Filled.ChatBubble, "Review", false)
|
||||
}
|
||||
Text("Hello!", color = Ink, fontSize = 32.sp, fontWeight = FontWeight.Normal, modifier = Modifier.padding(top = 18.dp, bottom = 24.dp))
|
||||
if (messages.isEmpty()) EmptyLesson()
|
||||
messages.forEach { Bubble(it) }
|
||||
if (error.isNotBlank()) ErrorCard(error)
|
||||
@@ -453,7 +433,7 @@ fun FilesScreen(api: CompanionApi) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun TerminalScreen(api: CompanionApi) {
|
||||
fun TerminalsScreen(api: CompanionApi) {
|
||||
val scope = remember { CoroutineScope(Dispatchers.Main) }
|
||||
var cwd by remember { mutableStateOf(".") }
|
||||
var command by remember { mutableStateOf("pwd && ls") }
|
||||
@@ -461,7 +441,11 @@ fun TerminalScreen(api: CompanionApi) {
|
||||
var busy by remember { mutableStateOf(false) }
|
||||
|
||||
ScreenList {
|
||||
HeroCard(icon = Icons.Filled.Terminal, title = "Run workspace commands.", label = "Terminal", text = "Execute commands on the paired machine without leaving the app.", orbColor = Purple, orbShadow = PurpleDark)
|
||||
SectionLabel("MACHINES")
|
||||
ListRow(icon = Icons.Filled.Terminal, title = "Hermes", subtitle = "hermes-mobile • online") {}
|
||||
TextButton(onClick = {}, modifier = Modifier.fillMaxWidth()) { Text("Show archived", color = Muted, fontWeight = FontWeight.SemiBold) }
|
||||
SectionLabel("NEW CHAT")
|
||||
ListRow(icon = Icons.Filled.ChatBubble, title = "New chat", subtitle = "Start a session on the paired machine") {}
|
||||
RaisedCard(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
FieldLabel("Working directory")
|
||||
@@ -515,7 +499,12 @@ fun SettingsScreen(api: CompanionApi) {
|
||||
var status by remember { mutableStateOf("Paste the companion access key, then test the connection.") }
|
||||
|
||||
ScreenList {
|
||||
CompactQuest(title = "Pair companion", label = "Settings", text = status, icon = Icons.Filled.Settings, orbColor = Purple, orbShadow = PurpleDark) {}
|
||||
SectionLabel("MACHINES")
|
||||
ListRow(icon = Icons.Filled.Terminal, title = "Hermes", subtitle = "linux • online") {}
|
||||
SectionLabel("FEATURES")
|
||||
ListRow(icon = Icons.Filled.Settings, title = "Appearance", subtitle = "Customize how the app looks") {}
|
||||
ListRow(icon = Icons.Outlined.Bolt, title = "Agent Defaults", subtitle = "Default model, effort, and permissions") {}
|
||||
SectionLabel("COMPANION")
|
||||
RaisedCard(Modifier.fillMaxWidth()) {
|
||||
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
|
||||
FieldLabel("Companion server URL")
|
||||
@@ -547,11 +536,29 @@ fun ScreenList(content: @Composable ColumnScope.() -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RaisedCard(modifier: Modifier = Modifier, color: Color = Paper, shadow: Color = NeutralShadow, border: Color = NeutralBorder, content: @Composable () -> Unit) {
|
||||
Box(modifier.padding(bottom = 5.dp)) {
|
||||
Box(Modifier.matchParentSize().padding(top = 5.dp).clip(RoundedCornerShape(22.dp)).background(shadow))
|
||||
Card(modifier = Modifier.fillMaxWidth(), shape = RoundedCornerShape(22.dp), colors = CardDefaults.cardColors(containerColor = color), border = BorderStroke(2.dp, border), elevation = CardDefaults.cardElevation(0.dp)) { content() }
|
||||
fun SectionLabel(text: String) {
|
||||
Text(text, color = Muted, fontSize = 12.sp, fontWeight = FontWeight.SemiBold, letterSpacing = 0.6.sp, modifier = Modifier.padding(top = 10.dp, start = 2.dp))
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ListRow(icon: ImageVector, title: String, subtitle: String, onClick: () -> Unit) {
|
||||
RaisedCard(Modifier.fillMaxWidth()) {
|
||||
Row(Modifier.clickable(onClick = onClick).padding(horizontal = 14.dp, vertical = 13.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
|
||||
Box(Modifier.size(32.dp).clip(RoundedCornerShape(8.dp)).background(Surface), contentAlignment = Alignment.Center) {
|
||||
Icon(icon, null, tint = Ink, modifier = Modifier.size(19.dp))
|
||||
}
|
||||
Column(Modifier.weight(1f)) {
|
||||
Text(title, color = Ink, fontWeight = FontWeight.SemiBold, fontSize = 16.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
Text(subtitle, color = Muted, fontWeight = FontWeight.Normal, fontSize = 13.sp, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
Text("›", color = Muted, fontSize = 22.sp, fontWeight = FontWeight.Light)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RaisedCard(modifier: Modifier = Modifier, color: Color = RowSurface, shadow: Color = Color.Transparent, border: Color = Color.Transparent, content: @Composable () -> Unit) {
|
||||
Card(modifier = modifier.fillMaxWidth(), shape = RoundedCornerShape(14.dp), colors = CardDefaults.cardColors(containerColor = color), border = BorderStroke(0.dp, border), elevation = CardDefaults.cardElevation(0.dp)) { content() }
|
||||
}
|
||||
|
||||
@Composable
|
||||
@@ -611,9 +618,9 @@ val Red = Color(0xFFF87171)
|
||||
val PaleRed = Color(0xFF311719)
|
||||
val RedBorder = Color(0xFF7A363A)
|
||||
val RedShadow = Color(0xFF0A0304)
|
||||
val Cream = Color(0xFF000000)
|
||||
val Paper = Color(0xFF0B0F12)
|
||||
val Surface = Color(0xFF12181D)
|
||||
val AppBackground = Color(0xFF212121)
|
||||
val RowSurface = Color(0xFF303030)
|
||||
val Surface = Color(0xFF2B2B2B)
|
||||
val Ink = Color(0xFFF4F4F5)
|
||||
val Muted = Color(0xFF9CA3AF)
|
||||
val NeutralBorder = Color(0xFF2A3138)
|
||||
|
||||
Reference in New Issue
Block a user