feat: align native UI with Happy Engineering style

This commit is contained in:
Hermes Agent
2026-07-09 11:27:01 +00:00
parent d8eb84c219
commit 68a1884ea5
@@ -46,9 +46,7 @@ import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChatBubble import androidx.compose.material.icons.filled.ChatBubble
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Favorite
import androidx.compose.material.icons.filled.Folder import androidx.compose.material.icons.filled.Folder
import androidx.compose.material.icons.filled.Key
import androidx.compose.material.icons.filled.PlayArrow import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.filled.Refresh import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save import androidx.compose.material.icons.filled.Save
@@ -58,9 +56,6 @@ import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.filled.WifiOff import androidx.compose.material.icons.filled.WifiOff
import androidx.compose.material.icons.outlined.Article import androidx.compose.material.icons.outlined.Article
import androidx.compose.material.icons.outlined.Bolt import androidx.compose.material.icons.outlined.Bolt
import androidx.compose.material.icons.outlined.Diamond
import androidx.compose.material.icons.outlined.LocalFireDepartment
import androidx.compose.material.icons.outlined.EmojiEvents
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.Card import androidx.compose.material3.Card
@@ -110,11 +105,11 @@ import java.net.URLEncoder
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
enum class Screen(val label: String, val icon: ImageVector, val title: String, val kicker: String) { enum class Screen(val label: String, val icon: ImageVector, val title: String, val kicker: String) {
Quest("Quest", Icons.Filled.ChatBubble, "Quest", "Hermes training path"), Quest("Chat", Icons.Filled.ChatBubble, "Agent", "Claude Code mobile client"),
Files("Files", Icons.Filled.Folder, "Backpack", "Workspace loot"), Files("Files", Icons.Filled.Folder, "Workspace", "Project files"),
Terminal("Terminal", Icons.Filled.Terminal, "Power move", "Run a command"), Terminal("Terminal", Icons.Filled.Terminal, "Terminal", "Run commands"),
Status("Status", Icons.Outlined.EmojiEvents, "League", "Companion stats"), Status("Status", Icons.Outlined.Bolt, "Sessions", "Companion status"),
Settings("Settings", Icons.Filled.Settings, "Nest", "Connection setup") Settings("Settings", Icons.Filled.Settings, "Settings", "Pair companion")
} }
data class FileEntry(val name: String, val path: String, val type: String, val size: Long) data class FileEntry(val name: String, val path: String, val type: String, val size: Long)
@@ -247,8 +242,8 @@ fun HermesNativeApp() {
colorScheme = darkColorScheme( colorScheme = darkColorScheme(
background = Cream, background = Cream,
surface = Paper, surface = Paper,
primary = Green, primary = Teal,
onPrimary = Color.White, onPrimary = Color.Black,
onBackground = Ink, onBackground = Ink,
onSurface = Ink onSurface = Ink
) )
@@ -317,25 +312,23 @@ fun NativeTopBar(screen: Screen, online: Boolean) {
.padding(horizontal = 16.dp, vertical = 10.dp), .padding(horizontal = 16.dp, vertical = 10.dp),
verticalArrangement = Arrangement.spacedBy(10.dp) verticalArrangement = Arrangement.spacedBy(10.dp)
) { ) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(12.dp)) {
RaisedCircle(color = Green, shadow = GreenDark, content = "") RaisedCircle(color = Purple, shadow = PurpleDark, content = "H")
Column(Modifier.weight(1f)) { Column(Modifier.weight(1f)) {
TinyLabel(screen.kicker) TinyLabel(screen.kicker)
Text(screen.title, fontSize = 31.sp, fontWeight = FontWeight.Black, color = Ink, lineHeight = 31.sp) Text(screen.title, fontSize = 30.sp, fontWeight = FontWeight.Black, color = Ink, lineHeight = 31.sp)
} }
StatChip(Icons.Outlined.LocalFireDepartment, "7", Orange) StatusDot(online)
StatChip(Icons.Outlined.Diamond, "550", Blue)
StatChip(Icons.Filled.Favorite, "3", Red)
} }
RaisedCard( RaisedCard(
color = if (online) PaleGreen else PaleOrange, color = if (online) TealPanel else WarningPanel,
shadow = if (online) GreenShadow else OrangeShadow, shadow = PanelShadow,
border = if (online) GreenBorder else OrangeBorder, border = if (online) TealBorder else WarningBorder,
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth()
) { ) {
Row(Modifier.padding(12.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { 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) GreenDark else Orange) Icon(if (online) Icons.Filled.Wifi else Icons.Filled.WifiOff, contentDescription = null, tint = if (online) Teal else Orange)
Text(if (online) "Companion online — ready for quests" else "Companion offline — check Settings", color = if (online) GreenDark else Orange, fontWeight = FontWeight.Black) Text(if (online) "Companion connected" else "Set companion URL and access key", color = if (online) Teal else Orange, fontWeight = FontWeight.Black)
} }
} }
} }
@@ -359,9 +352,9 @@ fun NativeBottomBar(current: Screen, onSelect: (Screen) -> Unit) {
icon = { Icon(item.icon, contentDescription = item.label) }, icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) }, label = { Text(item.label, fontWeight = FontWeight.Black, fontSize = 11.sp) },
colors = NavigationBarItemDefaults.colors( colors = NavigationBarItemDefaults.colors(
selectedIconColor = GreenDark, selectedIconColor = Teal,
selectedTextColor = GreenDark, selectedTextColor = Teal,
indicatorColor = PaleGreen, indicatorColor = TealPanel,
unselectedIconColor = Muted, unselectedIconColor = Muted,
unselectedTextColor = Muted unselectedTextColor = Muted
) )
@@ -380,10 +373,10 @@ fun QuestScreen(api: CompanionApi) {
val messages = remember { mutableStateListOf<ChatMessage>() } val messages = remember { mutableStateListOf<ChatMessage>() }
ScreenList { ScreenList {
HeroCard(icon = Icons.Filled.ChatBubble, title = "Give Hermes one clean mission.", label = "Daily quest", text = "Short, direct prompts work best. The native app calls the local companion server.") 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) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween) {
PathPill(Icons.Filled.CheckCircle, "Check", true) PathPill(Icons.Filled.CheckCircle, "Connect", true)
PathPill(Icons.Outlined.Bolt, "Ask", true, current = true) PathPill(Icons.Outlined.Bolt, "Prompt", true, current = true)
PathPill(Icons.Filled.ChatBubble, "Review", false) PathPill(Icons.Filled.ChatBubble, "Review", false)
} }
if (messages.isEmpty()) EmptyLesson() if (messages.isEmpty()) EmptyLesson()
@@ -391,8 +384,8 @@ fun QuestScreen(api: CompanionApi) {
if (error.isNotBlank()) ErrorCard(error) if (error.isNotBlank()) ErrorCard(error)
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
AppTextField(value = prompt, onValue = { prompt = it }, placeholder = "What should Hermes do?", minLines = 3) AppTextField(value = prompt, onValue = { prompt = it }, placeholder = "Message Hermes", minLines = 3)
PrimaryButton(text = if (busy) "Working…" else "Start quest", icon = Icons.Filled.PlayArrow, enabled = !busy && prompt.isNotBlank()) { PrimaryButton(text = if (busy) "Working…" else "Send", icon = Icons.Filled.PlayArrow, enabled = !busy && prompt.isNotBlank()) {
val p = prompt.trim() val p = prompt.trim()
prompt = "" prompt = ""
messages.add(ChatMessage("You", p)) messages.add(ChatMessage("You", p))
@@ -427,7 +420,7 @@ fun FilesScreen(api: CompanionApi) {
LaunchedEffect(Unit) { load(".") } LaunchedEffect(Unit) { load(".") }
ScreenList { ScreenList {
CompactQuest(title = "Workspace files", label = "Backpack", text = "Tap folders to go deeper. Files open in a native editor.", icon = Icons.Filled.Folder) { load() } CompactQuest(title = "Project files", label = "Workspace", text = "Browse and edit files through the companion server.", icon = Icons.Filled.Folder) { load() }
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(8.dp)) {
@@ -468,7 +461,7 @@ fun TerminalScreen(api: CompanionApi) {
var busy by remember { mutableStateOf(false) } var busy by remember { mutableStateOf(false) }
ScreenList { ScreenList {
HeroCard(icon = Icons.Filled.Terminal, title = "Run one safe command.", label = "Power move", text = "Commands execute through the companion server inside the configured workspace.", orbColor = Blue, orbShadow = BlueDark) 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)
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
FieldLabel("Working directory") FieldLabel("Working directory")
@@ -495,7 +488,7 @@ fun StatusScreen(api: CompanionApi) {
fun refresh() = scope.launch { health = runCatching { api.health() }.getOrElse { HealthState(false, it.message ?: "Health check failed") } } fun refresh() = scope.launch { health = runCatching { api.health() }.getOrElse { HealthState(false, it.message ?: "Health check failed") } }
ScreenList { ScreenList {
CompactQuest(title = "Companion status", label = "League board", text = health.status, icon = Icons.Outlined.EmojiEvents, orbColor = Gold, orbShadow = GoldDark) { refresh() } CompactQuest(title = "Companion status", label = "Sessions", text = health.status, icon = Icons.Outlined.Bolt, orbColor = Teal, orbShadow = TealDark) { refresh() }
Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
Metric("Hermes", if (health.hermesCli.startsWith("Found")) "Ready" else "", Modifier.weight(1f)) Metric("Hermes", if (health.hermesCli.startsWith("Found")) "Ready" else "", Modifier.weight(1f))
Metric("Uptime", health.uptime, Modifier.weight(1f)) Metric("Uptime", health.uptime, Modifier.weight(1f))
@@ -522,7 +515,7 @@ fun SettingsScreen(api: CompanionApi) {
var status by remember { mutableStateOf("Paste the companion access key, then test the connection.") } var status by remember { mutableStateOf("Paste the companion access key, then test the connection.") }
ScreenList { ScreenList {
CompactQuest(title = "Pair your companion", label = "Nest setup", text = status, icon = Icons.Filled.Settings, orbColor = Green, orbShadow = GreenDark) {} CompactQuest(title = "Pair companion", label = "Settings", text = status, icon = Icons.Filled.Settings, orbColor = Purple, orbShadow = PurpleDark) {}
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) { Column(Modifier.padding(12.dp), verticalArrangement = Arrangement.spacedBy(10.dp)) {
FieldLabel("Companion server URL") FieldLabel("Companion server URL")
@@ -562,7 +555,7 @@ fun RaisedCard(modifier: Modifier = Modifier, color: Color = Paper, shadow: Colo
} }
@Composable @Composable
fun HeroCard(icon: ImageVector, title: String, label: String, text: String, orbColor: Color = Green, orbShadow: Color = GreenDark) { fun HeroCard(icon: ImageVector, title: String, label: String, text: String, orbColor: Color = Teal, orbShadow: Color = TealDark) {
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Row(Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.Top) { Row(Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(14.dp), verticalAlignment = Alignment.Top) {
RaisedIcon(icon, orbColor, orbShadow) RaisedIcon(icon, orbColor, orbShadow)
@@ -572,7 +565,7 @@ fun HeroCard(icon: ImageVector, title: String, label: String, text: String, orbC
} }
@Composable @Composable
fun CompactQuest(title: String, label: String, text: String, icon: ImageVector, orbColor: Color = Green, orbShadow: Color = GreenDark, onRefresh: () -> Unit) { fun CompactQuest(title: String, label: String, text: String, icon: ImageVector, orbColor: Color = Teal, orbShadow: Color = TealDark, onRefresh: () -> Unit) {
RaisedCard(Modifier.fillMaxWidth()) { RaisedCard(Modifier.fillMaxWidth()) {
Row(Modifier.padding(14.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) { Row(Modifier.padding(14.dp), horizontalArrangement = Arrangement.spacedBy(12.dp), verticalAlignment = Alignment.CenterVertically) {
RaisedIcon(icon, orbColor, orbShadow, size = 58) RaisedIcon(icon, orbColor, orbShadow, size = 58)
@@ -583,45 +576,46 @@ fun CompactQuest(title: String, label: String, text: String, icon: ImageVector,
} }
@Composable fun RaisedIcon(icon: ImageVector, color: Color, shadow: Color, size: Int = 66) { Box(Modifier.size(size.dp), contentAlignment = Alignment.Center) { Box(Modifier.matchParentSize().padding(top = 5.dp).clip(CircleShape).background(shadow)); Box(Modifier.matchParentSize().padding(bottom = 5.dp).clip(CircleShape).background(color), contentAlignment = Alignment.Center) { Icon(icon, null, tint = Color.White, modifier = Modifier.size((size / 2).dp)) } } } @Composable fun RaisedIcon(icon: ImageVector, color: Color, shadow: Color, size: Int = 66) { Box(Modifier.size(size.dp), contentAlignment = Alignment.Center) { Box(Modifier.matchParentSize().padding(top = 5.dp).clip(CircleShape).background(shadow)); Box(Modifier.matchParentSize().padding(bottom = 5.dp).clip(CircleShape).background(color), contentAlignment = Alignment.Center) { Icon(icon, null, tint = Color.White, modifier = Modifier.size((size / 2).dp)) } } }
@Composable fun RaisedCircle(color: Color, shadow: Color, content: String) { Box(Modifier.size(54.dp), contentAlignment = Alignment.Center) { Box(Modifier.matchParentSize().padding(top = 5.dp).clip(RoundedCornerShape(18.dp)).background(shadow)); Box(Modifier.matchParentSize().padding(bottom = 5.dp).clip(RoundedCornerShape(18.dp)).background(color), contentAlignment = Alignment.Center) { Text(content, fontSize = 24.sp) } } } @Composable fun RaisedCircle(color: Color, shadow: Color, content: String) { Box(Modifier.size(54.dp), contentAlignment = Alignment.Center) { Box(Modifier.matchParentSize().padding(top = 5.dp).clip(RoundedCornerShape(18.dp)).background(shadow)); Box(Modifier.matchParentSize().padding(bottom = 5.dp).clip(RoundedCornerShape(18.dp)).background(color), contentAlignment = Alignment.Center) { Text(content, fontSize = 24.sp, fontWeight = FontWeight.Black, color = Color.White) } } }
@Composable fun StatChip(icon: ImageVector, value: String, color: Color) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(2.dp), modifier = Modifier.clip(RoundedCornerShape(99.dp)).background(Paper).padding(horizontal = 7.dp, vertical = 3.dp)) { Icon(icon, null, tint = color, modifier = Modifier.size(15.dp)); Text(value, color = color, fontWeight = FontWeight.Black, fontSize = 12.sp) } } @Composable fun StatusDot(online: Boolean) { Row(verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(6.dp), modifier = Modifier.clip(RoundedCornerShape(99.dp)).background(if (online) TealPanel else WarningPanel).padding(horizontal = 10.dp, vertical = 7.dp)) { Box(Modifier.size(8.dp).clip(CircleShape).background(if (online) Teal else Orange)); Text(if (online) "LIVE" else "SETUP", color = if (online) Teal else Orange, fontWeight = FontWeight.Black, fontSize = 11.sp) } }
@Composable fun TinyLabel(text: String) { Text(text.uppercase(), color = Muted, fontSize = 11.sp, fontWeight = FontWeight.Black, letterSpacing = 0.7.sp) } @Composable fun TinyLabel(text: String) { Text(text.uppercase(), color = Muted, fontSize = 11.sp, fontWeight = FontWeight.Black, letterSpacing = 0.7.sp) }
@Composable fun FieldLabel(text: String) { Text(text.uppercase(), color = Muted, fontSize = 12.sp, fontWeight = FontWeight.Black) } @Composable fun FieldLabel(text: String) { Text(text.uppercase(), color = Muted, fontSize = 12.sp, fontWeight = FontWeight.Black) }
@Composable fun PathPill(icon: ImageVector, text: String, done: Boolean, current: Boolean = false) { Row(Modifier.clip(RoundedCornerShape(99.dp)).background(if (current) Green else if (done) PaleGreen else Surface).padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Icon(icon, null, tint = if (current) Color.White else GreenDark, modifier = Modifier.size(18.dp)); Text(text, color = if (current) Color.White else GreenDark, fontWeight = FontWeight.Black, fontSize = 13.sp) } } @Composable fun PathPill(icon: ImageVector, text: String, done: Boolean, current: Boolean = false) { Row(Modifier.clip(RoundedCornerShape(99.dp)).background(if (current) Teal else if (done) TealPanel else Surface).padding(horizontal = 12.dp, vertical = 10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(5.dp)) { Icon(icon, null, tint = if (current) Color.Black else Teal, modifier = Modifier.size(18.dp)); Text(text, color = if (current) Color.Black else Teal, fontWeight = FontWeight.Black, fontSize = 13.sp) } }
@Composable fun EmptyLesson() { RaisedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(18.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text("🪽", fontSize = 42.sp); Text("No mission yet", color = Ink, fontWeight = FontWeight.Black, fontSize = 20.sp); Text("Try: check PM2 status or summarize logs.", color = Muted, fontWeight = FontWeight.ExtraBold) } } } @Composable fun EmptyLesson() { RaisedCard(Modifier.fillMaxWidth()) { Column(Modifier.padding(18.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text("\$ happy", color = Teal, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, fontWeight = FontWeight.Black, fontSize = 24.sp); Text("No messages yet", color = Ink, fontWeight = FontWeight.Black, fontSize = 20.sp); Text("Try: check PM2 status or summarize logs.", color = Muted, fontWeight = FontWeight.ExtraBold) } } }
@Composable fun Bubble(msg: ChatMessage) { RaisedCard(Modifier.fillMaxWidth().padding(start = if (msg.role == "You") 28.dp else 0.dp, end = if (msg.role == "You") 0.dp else 28.dp), color = if (msg.role == "You") PaleGreen else PaleGold) { Column(Modifier.padding(12.dp)) { TinyLabel(msg.role); Text(msg.content, color = Ink, fontWeight = FontWeight.ExtraBold, lineHeight = 19.sp) } } } @Composable fun Bubble(msg: ChatMessage) { RaisedCard(Modifier.fillMaxWidth().padding(start = if (msg.role == "You") 28.dp else 0.dp, end = if (msg.role == "You") 0.dp else 28.dp), color = if (msg.role == "You") TealPanel else Surface) { Column(Modifier.padding(12.dp)) { TinyLabel(msg.role); Text(msg.content, color = Ink, fontWeight = FontWeight.ExtraBold, lineHeight = 19.sp) } } }
@Composable fun ErrorCard(text: String) { RaisedCard(Modifier.fillMaxWidth(), color = PaleRed, shadow = RedShadow, border = RedBorder) { Text(text, Modifier.padding(12.dp), color = Red, fontWeight = FontWeight.Black) } } @Composable fun ErrorCard(text: String) { RaisedCard(Modifier.fillMaxWidth(), color = PaleRed, shadow = RedShadow, border = RedBorder) { Text(text, Modifier.padding(12.dp), color = Red, fontWeight = FontWeight.Black) } }
@Composable fun CodeCard(text: String) { RaisedCard(Modifier.fillMaxWidth()) { Text(text, Modifier.padding(14.dp), color = BlueDark, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, fontSize = 13.sp, lineHeight = 18.sp) } } @Composable fun CodeCard(text: String) { RaisedCard(Modifier.fillMaxWidth()) { Text(text, Modifier.padding(14.dp), color = BlueDark, fontFamily = androidx.compose.ui.text.font.FontFamily.Monospace, fontSize = 13.sp, lineHeight = 18.sp) } }
@Composable fun FileRow(entry: FileEntry, onClick: () -> Unit) { RaisedCard(Modifier.fillMaxWidth()) { Row(Modifier.clickable(onClick = onClick).padding(10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Box(Modifier.size(42.dp).clip(RoundedCornerShape(14.dp)).background(if (entry.type == "directory") PaleBlue else PaleGold), contentAlignment = Alignment.Center) { Icon(if (entry.type == "directory") Icons.Filled.Folder else Icons.Outlined.Article, null, tint = if (entry.type == "directory") Blue else GoldDark) }; Text(entry.name, Modifier.weight(1f), color = Ink, fontWeight = FontWeight.Black, maxLines = 1, overflow = TextOverflow.Ellipsis); Text(if (entry.type == "file") "${entry.size} B" else "dir", color = Muted, fontWeight = FontWeight.Black, fontSize = 12.sp) } } } @Composable fun FileRow(entry: FileEntry, onClick: () -> Unit) { RaisedCard(Modifier.fillMaxWidth()) { Row(Modifier.clickable(onClick = onClick).padding(10.dp), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.spacedBy(10.dp)) { Box(Modifier.size(42.dp).clip(RoundedCornerShape(14.dp)).background(if (entry.type == "directory") PaleBlue else PaleGold), contentAlignment = Alignment.Center) { Icon(if (entry.type == "directory") Icons.Filled.Folder else Icons.Outlined.Article, null, tint = if (entry.type == "directory") Blue else GoldDark) }; Text(entry.name, Modifier.weight(1f), color = Ink, fontWeight = FontWeight.Black, maxLines = 1, overflow = TextOverflow.Ellipsis); Text(if (entry.type == "file") "${entry.size} B" else "dir", color = Muted, fontWeight = FontWeight.Black, fontSize = 12.sp) } } }
@Composable fun Metric(label: String, value: String, modifier: Modifier = Modifier) { RaisedCard(modifier) { Column(Modifier.padding(10.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text(value, color = GreenDark, fontWeight = FontWeight.Black, fontSize = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis); Text(label.uppercase(), color = Muted, fontWeight = FontWeight.Black, fontSize = 10.sp) } } } @Composable fun Metric(label: String, value: String, modifier: Modifier = Modifier) { RaisedCard(modifier) { Column(Modifier.padding(10.dp), horizontalAlignment = Alignment.CenterHorizontally) { Text(value, color = Teal, fontWeight = FontWeight.Black, fontSize = 18.sp, maxLines = 1, overflow = TextOverflow.Ellipsis); Text(label.uppercase(), color = Muted, fontWeight = FontWeight.Black, fontSize = 10.sp) } } }
@Composable fun AppTextField(value: String, onValue: (String) -> Unit, placeholder: String, minLines: Int = 1, keyboardType: KeyboardType = KeyboardType.Text, password: Boolean = false) { OutlinedTextField(value = value, onValueChange = onValue, modifier = Modifier.fillMaxWidth(), placeholder = { Text(placeholder, color = Muted) }, minLines = minLines, keyboardOptions = KeyboardOptions(keyboardType = keyboardType), visualTransformation = if (password) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None, shape = RoundedCornerShape(16.dp), colors = TextFieldDefaults.colors(focusedContainerColor = Surface, unfocusedContainerColor = Surface, focusedIndicatorColor = Green, unfocusedIndicatorColor = NeutralBorder, cursorColor = GreenDark)) } @Composable fun AppTextField(value: String, onValue: (String) -> Unit, placeholder: String, minLines: Int = 1, keyboardType: KeyboardType = KeyboardType.Text, password: Boolean = false) { OutlinedTextField(value = value, onValueChange = onValue, modifier = Modifier.fillMaxWidth(), placeholder = { Text(placeholder, color = Muted) }, minLines = minLines, keyboardOptions = KeyboardOptions(keyboardType = keyboardType), visualTransformation = if (password) PasswordVisualTransformation() else androidx.compose.ui.text.input.VisualTransformation.None, shape = RoundedCornerShape(16.dp), colors = TextFieldDefaults.colors(focusedContainerColor = Surface, unfocusedContainerColor = Surface, focusedIndicatorColor = Teal, unfocusedIndicatorColor = NeutralBorder, cursorColor = Teal)) }
@Composable fun PrimaryButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { Button(onClick = onClick, enabled = enabled, modifier = modifier.height(54.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Green, contentColor = Color.White, disabledContainerColor = NeutralBorder), elevation = ButtonDefaults.buttonElevation(defaultElevation = 6.dp, pressedElevation = 1.dp)) { Text(text, fontWeight = FontWeight.Black); Spacer(Modifier.width(6.dp)); Icon(icon, null) } } @Composable fun PrimaryButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, enabled: Boolean = true, onClick: () -> Unit) { Button(onClick = onClick, enabled = enabled, modifier = modifier.height(54.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Teal, contentColor = Color.Black, disabledContainerColor = NeutralBorder), elevation = ButtonDefaults.buttonElevation(defaultElevation = 6.dp, pressedElevation = 1.dp)) { Text(text, fontWeight = FontWeight.Black); Spacer(Modifier.width(6.dp)); Icon(icon, null) } }
@Composable fun SecondaryButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, onClick: () -> Unit) { Button(onClick = onClick, modifier = modifier.height(54.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Surface, contentColor = Ink), elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp, pressedElevation = 1.dp)) { Text(text, fontWeight = FontWeight.Black); Spacer(Modifier.width(6.dp)); Icon(icon, null) } } @Composable fun SecondaryButton(text: String, icon: ImageVector, modifier: Modifier = Modifier, onClick: () -> Unit) { Button(onClick = onClick, modifier = modifier.height(54.dp), shape = RoundedCornerShape(16.dp), colors = ButtonDefaults.buttonColors(containerColor = Surface, contentColor = Ink), elevation = ButtonDefaults.buttonElevation(defaultElevation = 4.dp, pressedElevation = 1.dp)) { Text(text, fontWeight = FontWeight.Black); Spacer(Modifier.width(6.dp)); Icon(icon, null) } }
@Composable fun RoundButton(icon: ImageVector, onClick: () -> Unit) { Button(onClick = onClick, modifier = Modifier.size(50.dp), shape = RoundedCornerShape(16.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(0.dp), colors = ButtonDefaults.buttonColors(containerColor = Blue, contentColor = Color.White), elevation = ButtonDefaults.buttonElevation(defaultElevation = 5.dp, pressedElevation = 1.dp)) { Icon(icon, null) } } @Composable fun RoundButton(icon: ImageVector, onClick: () -> Unit) { Button(onClick = onClick, modifier = Modifier.size(50.dp), shape = RoundedCornerShape(16.dp), contentPadding = androidx.compose.foundation.layout.PaddingValues(0.dp), colors = ButtonDefaults.buttonColors(containerColor = Purple, contentColor = Color.White), elevation = ButtonDefaults.buttonElevation(defaultElevation = 5.dp, pressedElevation = 1.dp)) { Icon(icon, null) } }
val Green = Color(0xFF58CC02) val Teal = Color(0xFF28DBC9)
val GreenDark = Color(0xFF7FE83B) val TealDark = Color(0xFF0F8F84)
val GreenShadow = Color(0xFF244B19) val TealPanel = Color(0xFF0E2B2A)
val GreenBorder = Color(0xFF3D7F28) val TealBorder = Color(0xFF176C66)
val PaleGreen = Color(0xFF17301A) val Purple = Color(0xFF6701E6)
val Blue = Color(0xFF1CB0F6) val PurpleDark = Color(0xFF3B0087)
val BlueDark = Color(0xFF73D4FF) val PurplePanel = Color(0xFF211133)
val PaleBlue = Color(0xFF112A3A) val Blue = Color(0xFF3B82F6)
val Gold = Color(0xFFFFC800) val BlueDark = Color(0xFF8AB7FF)
val GoldDark = Color(0xFFFFD95A) val PaleBlue = Color(0xFF111B2B)
val PaleGold = Color(0xFF332A10) val GoldDark = Color(0xFFFBBF24)
val Orange = Color(0xFFFFA033) val PaleGold = Color(0xFF211B10)
val PaleOrange = Color(0xFF332312) val Orange = Color(0xFFFBBF24)
val OrangeBorder = Color(0xFF7B4D1C) val WarningPanel = Color(0xFF2B2111)
val OrangeShadow = Color(0xFF26180C) val WarningBorder = Color(0xFF7A5A1B)
val Red = Color(0xFFFF6B6B) val Red = Color(0xFFF87171)
val PaleRed = Color(0xFF3A171B) val PaleRed = Color(0xFF311719)
val RedBorder = Color(0xFF8B3D45) val RedBorder = Color(0xFF7A363A)
val RedShadow = Color(0xFF271013) val RedShadow = Color(0xFF0A0304)
val Cream = Color(0xFF08100E) val Cream = Color(0xFF000000)
val Paper = Color(0xFF111B18) val Paper = Color(0xFF0B0F12)
val Surface = Color(0xFF17231F) val Surface = Color(0xFF12181D)
val Ink = Color(0xFFEAF7EE) val Ink = Color(0xFFF4F4F5)
val Muted = Color(0xFF9FB4A7) val Muted = Color(0xFF9CA3AF)
val NeutralBorder = Color(0xFF294138) val NeutralBorder = Color(0xFF2A3138)
val NeutralShadow = Color(0xFF060B0A) val NeutralShadow = Color(0xFF030506)
val PanelShadow = Color(0xFF040707)