From e1480fbb574cd4bfd527849761df39cca4cbf92d Mon Sep 17 00:00:00 2001 From: Bruno Deanoz Date: Sun, 21 Jun 2026 18:50:58 +0200 Subject: [PATCH] feat: modern attachment menu with round colored icon buttons Replaced the generic DropdownMenu with a row of three large (56dp) circular icon buttons: Camera (accent), Gallery (indigo), File (emerald). Slides in/out with animation above the chat input. Toggle on +. --- .../dev/kaizen/app/chat/ChatComponents.kt | 65 +++++++++++++++++++ .../java/dev/kaizen/app/chat/ChatScreen.kt | 52 +++++---------- 2 files changed, 83 insertions(+), 34 deletions(-) diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt index f1bb534..009a361 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatComponents.kt @@ -36,6 +36,7 @@ import androidx.compose.foundation.text.BasicTextField import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Add +import androidx.compose.material.icons.rounded.CameraAlt import androidx.compose.material.icons.rounded.Call import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme @@ -467,6 +468,70 @@ private fun FileChip(attachment: Attachment) { } } +@Composable +fun AttachmentMenu( + onCamera: () -> Unit, + onGallery: () -> Unit, + onFile: () -> Unit, + modifier: Modifier = Modifier, +) { + val cs = MaterialTheme.colorScheme + val accent = LocalKaizenAccent.current + val isDark = isSystemInDarkTheme() + + Row( + modifier = modifier.fillMaxWidth().padding(horizontal = 32.dp, vertical = 8.dp), + horizontalArrangement = Arrangement.SpaceEvenly, + ) { + AttachOption( + icon = Icons.Rounded.CameraAlt, + label = stringResource(R.string.attach_camera), + tint = accent.primary, + bg = accent.primary.copy(alpha = if (isDark) 0.15f else 0.10f), + onClick = onCamera, + ) + AttachOption( + icon = Icons.Rounded.Image, + label = stringResource(R.string.attach_gallery), + tint = Color(0xFF6366F1), + bg = Color(0xFF6366F1).copy(alpha = if (isDark) 0.15f else 0.10f), + onClick = onGallery, + ) + AttachOption( + icon = Icons.AutoMirrored.Rounded.InsertDriveFile, + label = stringResource(R.string.attach_file), + tint = Color(0xFF10B981), + bg = Color(0xFF10B981).copy(alpha = if (isDark) 0.15f else 0.10f), + onClick = onFile, + ) + } +} + +@Composable +private fun AttachOption( + icon: ImageVector, + label: String, + tint: Color, + bg: Color, + onClick: () -> Unit, +) { + val cs = MaterialTheme.colorScheme + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Box( + Modifier + .size(56.dp) + .clip(KaizenShapes.circle) + .background(bg) + .clickable(onClick = onClick), + contentAlignment = Alignment.Center, + ) { + Icon(icon, null, tint = tint, modifier = Modifier.size(24.dp)) + } + Spacer(Modifier.height(6.dp)) + Text(label, color = cs.onSurfaceVariant, fontSize = 12.sp, fontWeight = FontWeight.Medium) + } +} + @Composable private fun SendArrow(color: Color) { Canvas(Modifier.size(18.dp)) { diff --git a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt index c24b24f..fada69f 100644 --- a/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt +++ b/app/src/main/java/dev/kaizen/app/chat/ChatScreen.kt @@ -72,11 +72,6 @@ import dev.kaizen.app.settings.SettingsScreen import dev.kaizen.app.settings.SettingsViewModel import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.result.contract.ActivityResultContracts -import androidx.compose.material.icons.rounded.CameraAlt -import androidx.compose.material.icons.rounded.Image -import androidx.compose.material.icons.automirrored.rounded.InsertDriveFile -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.LifecycleResumeEffect @@ -545,37 +540,26 @@ fun ChatScreen( }, ) Spacer(Modifier.height(10.dp)) - Box { - ChatInput( - value = input, - onValueChange = { input = it }, - onSend = { send(input) }, - enabled = !isStreaming, - pendingFiles = pendingFiles, - onAddClick = { showAttachMenu = true }, - onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, + androidx.compose.animation.AnimatedVisibility( + visible = showAttachMenu, + enter = androidx.compose.animation.fadeIn() + androidx.compose.animation.slideInVertically { it / 2 }, + exit = androidx.compose.animation.fadeOut() + androidx.compose.animation.slideOutVertically { it / 2 }, + ) { + AttachmentMenu( + onCamera = { showAttachMenu = false; cameraPicker.launch(null) }, + onGallery = { showAttachMenu = false; filePicker.launch("image/*") }, + onFile = { showAttachMenu = false; filePicker.launch("*/*") }, ) - DropdownMenu( - expanded = showAttachMenu, - onDismissRequest = { showAttachMenu = false }, - ) { - DropdownMenuItem( - text = { Text(stringResource(R.string.attach_camera)) }, - onClick = { showAttachMenu = false; cameraPicker.launch(null) }, - leadingIcon = { Icon(Icons.Rounded.CameraAlt, null, modifier = Modifier.size(20.dp)) }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.attach_gallery)) }, - onClick = { showAttachMenu = false; filePicker.launch("image/*") }, - leadingIcon = { Icon(Icons.Rounded.Image, null, modifier = Modifier.size(20.dp)) }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.attach_file)) }, - onClick = { showAttachMenu = false; filePicker.launch("*/*") }, - leadingIcon = { Icon(Icons.AutoMirrored.Rounded.InsertDriveFile, null, modifier = Modifier.size(20.dp)) }, - ) - } } + ChatInput( + value = input, + onValueChange = { input = it }, + onSend = { send(input) }, + enabled = !isStreaming, + pendingFiles = pendingFiles, + onAddClick = { showAttachMenu = !showAttachMenu }, + onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, + ) Spacer(Modifier.height(16.dp)) }