@@ -43,12 +43,10 @@ import androidx.compose.material3.OutlinedTextField
4343import androidx.compose.material3.Scaffold
4444import androidx.compose.material3.SnackbarHost
4545import androidx.compose.material3.SnackbarHostState
46−import androidx.compose.material3.SwipeToDismissBox
4746import androidx.compose.material3.Text
4847import androidx.compose.material3.TextButton
4948import androidx.compose.material3.TopAppBar
5049import androidx.compose.material3.pulltorefresh.PullToRefreshBox
51−import androidx.compose.material3.rememberSwipeToDismissBoxState
5250import androidx.compose.runtime.Composable
5351import androidx.compose.runtime.CompositionLocalProvider
5452import androidx.compose.runtime.LaunchedEffect
@@ -261,7 +259,7 @@ fun SessionListScreen(
261259 onClick = { onOpenSession(session.id) },
262260 onRename = { pendingRenameId = session.id },
263261 onDelete = { pendingDeleteId = session.id },
264− modifier = Modifier.animateItem().testTag("session_card"),
262+ modifier = Modifier.testTag("session_card"),
265263 )
266264 }
267265 }
@@ -348,110 +346,74 @@ private fun SessionCard(
348346 onDelete: () -> Unit,
349347 modifier: Modifier = Modifier,
350348) {
351− // Swipe toward the start reveals a delete affordance; releasing snaps back and opens
352− // the same confirmation dialog the trash icon uses, so the destructive action is
353− // always confirmed.
354− val dismissState = rememberSwipeToDismissBoxState(
355− confirmValueChange = {
356− onDelete()
357− false // snap back; the dialog owns the actual deletion
358− },
359− )
360− SwipeToDismissBox(
361− state = dismissState,
362− backgroundContent = { SwipeDeleteBackground() },
363− enableDismissFromStartToEnd = false,
364− modifier = modifier,
349+ Card(
350+ modifier = modifier.fillMaxWidth(),
365351 ) {
366− Card(
367− modifier = Modifier.fillMaxWidth(),
368− ) {
369− Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
370− Row(
371− modifier = Modifier.fillMaxWidth(),
372− verticalAlignment = Alignment.CenterVertically,
352+ Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
353+ Row(
354+ modifier = Modifier.fillMaxWidth(),
355+ verticalAlignment = Alignment.CenterVertically,
356+ ) {
357+ // Title area is the primary click target; the action buttons below
358+ // remain separate accessibility nodes so screen readers can activate
359+ // them independently.
360+ Column(
361+ modifier = Modifier
362+ .weight(1f)
363+ .clickable(role = Role.Button) { onClick() },
373364 ) {
374− // Title area is the primary click target; the action buttons below
375− // remain separate accessibility nodes so screen readers can activate
376− // them independently.
377− Column(
378− modifier = Modifier
379− .weight(1f)
380− .clickable(role = Role.Button) { onClick() },
381− ) {
382− Row(verticalAlignment = Alignment.CenterVertically) {
383− if (unread) {
384− val unreadLabel = stringResource(R.string.unread)
385− Box(
386− modifier = Modifier
387− .padding(end = 8.dp)
388− .size(8.dp)
389− .clip(androidx.compose.foundation.shape.CircleShape)
390− .background(MaterialTheme.colorScheme.primary)
391− .semantics { contentDescription = unreadLabel },
392− )
393− }
394− Text(
395− session.displayTitle,
396− style = MaterialTheme.typography.titleMedium,
397− color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
398− maxLines = 1,
399− overflow = TextOverflow.Ellipsis,
400− )
401− }
402− val time = rememberRelativeTime(session.time?.updated ?: session.time?.created)
403− if (time.isNotEmpty()) {
404− Text(
405− time,
406− style = MaterialTheme.typography.labelSmall,
407− color = MaterialTheme.colorScheme.onSurfaceVariant,
408− modifier = Modifier.padding(top = 2.dp),
365+ Row(verticalAlignment = Alignment.CenterVertically) {
366+ if (unread) {
367+ val unreadLabel = stringResource(R.string.unread)
368+ Box(
369+ modifier = Modifier
370+ .padding(end = 8.dp)
371+ .size(8.dp)
372+ .clip(androidx.compose.foundation.shape.CircleShape)
373+ .background(MaterialTheme.colorScheme.primary)
374+ .semantics { contentDescription = unreadLabel },
409375 )
410376 }
377+ Text(
378+ session.displayTitle,
379+ style = MaterialTheme.typography.titleMedium,
380+ color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
381+ maxLines = 1,
382+ overflow = TextOverflow.Ellipsis,
383+ )
411384 }
412− IconButton(onClick = onRename) {
413− Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.rename))
414− }
415− IconButton(onClick = onDelete) {
416− Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
385+ val time = rememberRelativeTime(session.time?.updated ?: session.time?.created)
386+ if (time.isNotEmpty()) {
387+ Text(
388+ time,
389+ style = MaterialTheme.typography.labelSmall,
390+ color = MaterialTheme.colorScheme.onSurfaceVariant,
391+ modifier = Modifier.padding(top = 2.dp),
392+ )
417393 }
418394 }
419− if (!preview.isNullOrBlank()) {
420− Spacer(Modifier.size(4.dp))
421− Text(
422− preview,
423− style = MaterialTheme.typography.bodyMedium,
424− color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
425− maxLines = 2,
426− overflow = TextOverflow.Ellipsis,
427− modifier = Modifier.clickable(role = Role.Button) { onClick() },
428− )
395+ IconButton(onClick = onRename) {
396+ Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.rename))
397+ }
398+ IconButton(onClick = onDelete) {
399+ Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
429400 }
430401 }
402+ if (!preview.isNullOrBlank()) {
403+ Spacer(Modifier.size(4.dp))
404+ Text(
405+ preview,
406+ style = MaterialTheme.typography.bodyMedium,
407+ color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
408+ maxLines = 2,
409+ overflow = TextOverflow.Ellipsis,
410+ modifier = Modifier.clickable(role = Role.Button) { onClick() },
411+ )
412+ }
431413 }
432414 }
433415}
434416
435−/** Red background with a trash icon shown behind a session card as it is swiped away. */
436−@Composable
437−private fun SwipeDeleteBackground() {
438− Box(
439− modifier = Modifier
440− .fillMaxSize()
441− .clip(androidx.compose.foundation.shape.RoundedCornerShape(12.dp))
442− .background(MaterialTheme.colorScheme.errorContainer)
443− .padding(horizontal = 20.dp),
444− contentAlignment = Alignment.CenterEnd,
445− ) {
446− val deleteLabel = stringResource(R.string.delete)
447− Icon(
448− Icons.Filled.Delete,
449− contentDescription = deleteLabel,
450− tint = MaterialTheme.colorScheme.onErrorContainer,
451− )
452− }
453−}
454−
455417@Composable
456418private fun EmptySessions(onCreate: () -> Unit, modifier: Modifier = Modifier) {
457419 Column(