@@ -43,12 +43,10 @@ import androidx.compose.material3.OutlinedTextField
43import androidx.compose.material3.Scaffold
43import androidx.compose.material3.Scaffold
44import androidx.compose.material3.SnackbarHost
44import androidx.compose.material3.SnackbarHost
45import androidx.compose.material3.SnackbarHostState
45import androidx.compose.material3.SnackbarHostState
46−import androidx.compose.material3.SwipeToDismissBox
47import androidx.compose.material3.Text
46import androidx.compose.material3.Text
48import androidx.compose.material3.TextButton
47import androidx.compose.material3.TextButton
49import androidx.compose.material3.TopAppBar
48import androidx.compose.material3.TopAppBar
50import androidx.compose.material3.pulltorefresh.PullToRefreshBox
49import androidx.compose.material3.pulltorefresh.PullToRefreshBox
51−import androidx.compose.material3.rememberSwipeToDismissBoxState
52import androidx.compose.runtime.Composable
50import androidx.compose.runtime.Composable
53import androidx.compose.runtime.CompositionLocalProvider
51import androidx.compose.runtime.CompositionLocalProvider
54import androidx.compose.runtime.LaunchedEffect
52import androidx.compose.runtime.LaunchedEffect
@@ -261,7 +259,7 @@ fun SessionListScreen(
261 onClick = { onOpenSession(session.id) },
259 onClick = { onOpenSession(session.id) },
262 onRename = { pendingRenameId = session.id },
260 onRename = { pendingRenameId = session.id },
263 onDelete = { pendingDeleteId = session.id },
261 onDelete = { pendingDeleteId = session.id },
264− modifier = Modifier.animateItem().testTag("session_card"),
262+ modifier = Modifier.testTag("session_card"),
@@ -348,110 +346,74 @@ private fun SessionCard(
348 onDelete: () -> Unit,
346 onDelete: () -> Unit,
349 modifier: Modifier = Modifier,
347 modifier: Modifier = Modifier,
351− // Swipe toward the start reveals a delete affordance; releasing snaps back and opens
349+ Card(
352− // the same confirmation dialog the trash icon uses, so the destructive action is
350+ modifier = modifier.fillMaxWidth(),
353− // always confirmed.
354− val dismissState = rememberSwipeToDismissBoxState(
355− confirmValueChange = {
357− false // snap back; the dialog owns the actual deletion
361− state = dismissState,
362− backgroundContent = { SwipeDeleteBackground() },
363− enableDismissFromStartToEnd = false,
364− modifier = modifier,
367− modifier = Modifier.fillMaxWidth(),
369− Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
352+ Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
371− modifier = Modifier.fillMaxWidth(),
354+ modifier = Modifier.fillMaxWidth(),
372− verticalAlignment = Alignment.CenterVertically,
355+ verticalAlignment = Alignment.CenterVertically,
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.
363+ .clickable(role = Role.Button) { onClick() },
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.
380− .clickable(role = Role.Button) { onClick() },
382− Row(verticalAlignment = Alignment.CenterVertically) {
365+ Row(verticalAlignment = Alignment.CenterVertically) {
383− if (unread) {
366+ if (unread) {
384− val unreadLabel = stringResource(R.string.unread)
367+ val unreadLabel = stringResource(R.string.unread)
386− modifier = Modifier
369+ modifier = Modifier
387− .padding(end = 8.dp)
370+ .padding(end = 8.dp)
388− .size(8.dp)
371+ .size(8.dp)
389− .clip(androidx.compose.foundation.shape.CircleShape)
372+ .clip(androidx.compose.foundation.shape.CircleShape)
390− .background(MaterialTheme.colorScheme.primary)
373+ .background(MaterialTheme.colorScheme.primary)
391− .semantics { contentDescription = unreadLabel },
374+ .semantics { contentDescription = unreadLabel },
395− session.displayTitle,
396− style = MaterialTheme.typography.titleMedium,
397− color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
399− overflow = TextOverflow.Ellipsis,
402− val time = rememberRelativeTime(session.time?.updated ?: session.time?.created)
403− if (time.isNotEmpty()) {
406− style = MaterialTheme.typography.labelSmall,
407− color = MaterialTheme.colorScheme.onSurfaceVariant,
408− modifier = Modifier.padding(top = 2.dp),
378+ session.displayTitle,
379+ style = MaterialTheme.typography.titleMedium,
380+ color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
382+ overflow = TextOverflow.Ellipsis,
412− IconButton(onClick = onRename) {
385+ val time = rememberRelativeTime(session.time?.updated ?: session.time?.created)
413− Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.rename))
386+ if (time.isNotEmpty()) {
415− IconButton(onClick = onDelete) {
388+ time,
416− Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
389+ style = MaterialTheme.typography.labelSmall,
390+ color = MaterialTheme.colorScheme.onSurfaceVariant,
391+ modifier = Modifier.padding(top = 2.dp),
419− if (!preview.isNullOrBlank()) {
395+ IconButton(onClick = onRename) {
420− Spacer(Modifier.size(4.dp))
396+ Icon(Icons.Filled.Edit, contentDescription = stringResource(R.string.rename))
422− preview,
398+ IconButton(onClick = onDelete) {
423− style = MaterialTheme.typography.bodyMedium,
399+ Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.delete))
424− color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
426− overflow = TextOverflow.Ellipsis,
427− modifier = Modifier.clickable(role = Role.Button) { onClick() },
402+ if (!preview.isNullOrBlank()) {
403+ Spacer(Modifier.size(4.dp))
406+ style = MaterialTheme.typography.bodyMedium,
407+ color = if (unread) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant,
409+ overflow = TextOverflow.Ellipsis,
410+ modifier = Modifier.clickable(role = Role.Button) { onClick() },
435−/** Red background with a trash icon shown behind a session card as it is swiped away. */
437−private fun SwipeDeleteBackground() {
441− .clip(androidx.compose.foundation.shape.RoundedCornerShape(12.dp))
442− .background(MaterialTheme.colorScheme.errorContainer)
443− .padding(horizontal = 20.dp),
444− contentAlignment = Alignment.CenterEnd,
446− val deleteLabel = stringResource(R.string.delete)
448− Icons.Filled.Delete,
449− contentDescription = deleteLabel,
450− tint = MaterialTheme.colorScheme.onErrorContainer,
455@Composable
417@Composable
456private fun EmptySessions(onCreate: () -> Unit, modifier: Modifier = Modifier) {
418private fun EmptySessions(onCreate: () -> Unit, modifier: Modifier = Modifier) {