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 +.
This commit is contained in:
Bruno Deanoz 2026-06-21 18:50:58 +02:00
parent 2e323f3609
commit e1480fbb57
2 changed files with 83 additions and 34 deletions

View file

@ -36,6 +36,7 @@ import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.rounded.Add import androidx.compose.material.icons.rounded.Add
import androidx.compose.material.icons.rounded.CameraAlt
import androidx.compose.material.icons.rounded.Call import androidx.compose.material.icons.rounded.Call
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme 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 @Composable
private fun SendArrow(color: Color) { private fun SendArrow(color: Color) {
Canvas(Modifier.size(18.dp)) { Canvas(Modifier.size(18.dp)) {

View file

@ -72,11 +72,6 @@ import dev.kaizen.app.settings.SettingsScreen
import dev.kaizen.app.settings.SettingsViewModel import dev.kaizen.app.settings.SettingsViewModel
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts 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.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.LifecycleResumeEffect import androidx.lifecycle.compose.LifecycleResumeEffect
@ -545,37 +540,26 @@ fun ChatScreen(
}, },
) )
Spacer(Modifier.height(10.dp)) Spacer(Modifier.height(10.dp))
Box { 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("*/*") },
)
}
ChatInput( ChatInput(
value = input, value = input,
onValueChange = { input = it }, onValueChange = { input = it },
onSend = { send(input) }, onSend = { send(input) },
enabled = !isStreaming, enabled = !isStreaming,
pendingFiles = pendingFiles, pendingFiles = pendingFiles,
onAddClick = { showAttachMenu = true }, onAddClick = { showAttachMenu = !showAttachMenu },
onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } }, onRemoveFile = { id -> pendingFiles.removeAll { it.id == id } },
) )
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)) },
)
}
}
Spacer(Modifier.height(16.dp)) Spacer(Modifier.height(16.dp))
} }