diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt index 1c78e6f5c..0d370ce63 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/Main.kt @@ -32,7 +32,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.rememberScrollState @@ -1324,18 +1323,17 @@ fun RelaySettingsScreen( accountManager.loadNwcConnection() } - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.TopCenter, - ) { + com.vitorpamplona.amethyst.desktop.ui.ReadingColumn { + val sidePadding = + com.vitorpamplona.amethyst.desktop.ui + .readingHorizontalPadding() Column( modifier = Modifier - .widthIn(max = 720.dp) .fillMaxWidth() .fillMaxHeight() .verticalScroll(rememberScrollState()) - .padding(horizontal = 12.dp), + .padding(horizontal = sidePadding), ) { Row( modifier = diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt index 8c418e309..54a2b1626 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/BookmarksScreen.kt @@ -249,13 +249,14 @@ fun BookmarksScreen( val currentBookmarkIds = if (selectedTab == BookmarkTab.PUBLIC) publicBookmarkIds else privateBookmarkIds ReadingColumn { + val sidePadding = readingHorizontalPadding() // Header with tabs Row( modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(horizontal = sidePadding, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -306,7 +307,7 @@ fun BookmarksScreen( else -> { LazyColumn( modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = sidePadding), ) { items(currentEvents, key = { it.id }) { event -> Column( diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt index d5d9c062f..70f904c32 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/DraftsScreen.kt @@ -67,12 +67,13 @@ fun DraftsScreen( var deleteTarget by remember { mutableStateOf(null) } ReadingColumn { + val sidePadding = readingHorizontalPadding() Row( modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(horizontal = sidePadding, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -103,7 +104,7 @@ fun DraftsScreen( ) } else { LazyColumn( - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = sidePadding), verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(drafts, key = { it.slug }) { entry -> diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedHeader.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedHeader.kt index 802d7eb12..c1dd65a69 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedHeader.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedHeader.kt @@ -55,7 +55,10 @@ fun FeedHeader( modifier: Modifier = Modifier, ) { Row( - modifier = modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + modifier = + modifier + .fillMaxWidth() + .padding(horizontal = readingHorizontalPadding(), vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt index f8542c338..35b786398 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/FeedScreen.kt @@ -546,8 +546,9 @@ fun FeedScreen( is FeedState.Loaded -> { val loadedState by state.feed.collectAsState() + val sidePadding = LocalReadingSidePadding.current LazyColumn( - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = sidePadding + 12.dp), verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(loadedState.list, key = { it.idHex }) { note -> @@ -649,8 +650,12 @@ private fun FeedHeader( onNavigateToRelays: () -> Unit = {}, onOpenRelayPicker: () -> Unit = {}, ) { + val sidePadding = LocalReadingSidePadding.current Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = sidePadding + 12.dp, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt index 6a7395e89..1429565d7 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/NotificationsScreen.kt @@ -256,7 +256,7 @@ fun NotificationsScreen( ) } else { LazyColumn( - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = readingHorizontalPadding()), verticalArrangement = Arrangement.spacedBy(8.dp), ) { items(notifications.distinctBy { it.event.id }, key = { it.event.id }) { notification -> diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt index 645b6bd15..040e385a1 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadingColumn.kt @@ -20,15 +20,13 @@ */ package com.vitorpamplona.amethyst.desktop.ui -import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.BoxWithConstraints import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.ColumnScope -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.widthIn import androidx.compose.runtime.Composable -import androidx.compose.ui.Alignment +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.ui.Modifier import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp @@ -42,35 +40,50 @@ import androidx.compose.ui.unit.dp val DefaultReadingWidth: Dp = 720.dp /** - * A top-level content scaffold that caps width and centers its column on wide - * displays. Each feed / list / profile screen wraps its contents in this so - * cards maintain a consistent proportion across the whole app. + * Side padding the current screen should apply to its scrollable list / + * headers to keep content centered at [DefaultReadingWidth] within the + * window. `0.dp` outside of a [ReadingColumn]. + * + * Screens use this to widen the gutter on wide displays (`horizontal = + * readingSidePadding + 12.dp`) while keeping the scrollable area itself at + * full window width — so the mouse wheel scrolls the feed wherever it + * hovers, not only inside the 720 dp column. + */ +val LocalReadingSidePadding = compositionLocalOf { 0.dp } + +/** + * Convenience: reads the current reading-column side padding and adds the + * standard 12.dp screen-edge gutter. Use this inside composable bodies when + * applying horizontal padding to header rows or `contentPadding` on + * LazyColumns so items stay centered at [DefaultReadingWidth] while the + * scrollable surface still spans the full window width. + */ +@Composable +fun readingHorizontalPadding(): Dp = LocalReadingSidePadding.current + 12.dp + +/** + * Top-level scaffold for single-pane content screens. Measures the window + * width and computes the side padding that centers a [maxWidth]-wide column + * within it. The actual content (header + LazyColumn) fills the window — the + * centering is done via [LocalReadingSidePadding] applied to inner modifiers + * (`horizontal` padding on a header Row, `contentPadding` on a LazyColumn). + * This keeps scroll events live across the full window, not just the center + * column. * * Not used by: * - Messages (two-pane layout with its own sizing) * - Article Reader (has its own narrower reading-width logic) - * - Editor / Chess / Relay Dashboard (rely on full width for tools / boards) + * - Editor / Chess / Relay Dashboard (tools that want full width) */ @Composable fun ReadingColumn( - modifier: Modifier = Modifier, maxWidth: Dp = DefaultReadingWidth, content: @Composable ColumnScope.() -> Unit, ) { - Box( - modifier = Modifier.fillMaxSize(), - contentAlignment = Alignment.TopCenter, - ) { - // Order matters: widthIn must come BEFORE fillMaxWidth, otherwise - // fillMaxWidth locks the Column to parent.width and the cap is ignored. - // fillMaxHeight is safe (it only constrains the other axis). - Column( - modifier = - modifier - .widthIn(max = maxWidth) - .fillMaxWidth() - .fillMaxHeight(), - content = content, - ) + BoxWithConstraints(modifier = Modifier.fillMaxSize()) { + val sidePadding = ((this.maxWidth - maxWidth) / 2).coerceAtLeast(0.dp) + CompositionLocalProvider(LocalReadingSidePadding provides sidePadding) { + Column(modifier = Modifier.fillMaxSize(), content = content) + } } } diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt index b286fc35e..fa3a2135f 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ReadsScreen.kt @@ -286,13 +286,14 @@ fun ReadsScreen( } ReadingColumn { + val sidePadding = readingHorizontalPadding() // Header — Messages-style: tabs left, refresh right. The selected tab // (Following / Global) acts as the screen title, so no separate label. Row( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(horizontal = sidePadding, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -357,7 +358,7 @@ fun ReadsScreen( else -> { LazyColumn( - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = sidePadding), verticalArrangement = Arrangement.spacedBy(12.dp), ) { items(events, key = { it.id }) { event -> diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt index 77774c165..b895a80ed 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/SearchScreen.kt @@ -32,14 +32,12 @@ import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxHeight -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape @@ -292,16 +290,10 @@ fun SearchScreen( focusRequester.requestFocus() } - androidx.compose.foundation.layout.Box( - modifier = - androidx.compose.ui.Modifier - .fillMaxSize(), - contentAlignment = androidx.compose.ui.Alignment.TopCenter, - ) { + ReadingColumn { Column( modifier = modifier - .widthIn(max = DefaultReadingWidth) .fillMaxWidth() .fillMaxHeight() .onPreviewKeyEvent { event -> @@ -342,12 +334,13 @@ fun SearchScreen( ) // Title row + val sidePadding = readingHorizontalPadding() Row( modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(horizontal = sidePadding, vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -384,7 +377,7 @@ fun SearchScreen( textStyle = MaterialTheme.typography.bodyMedium, placeholder = { Text( - "Search notes, people, tags... or use operators", + "Search people, tags, notes…", style = MaterialTheme.typography.bodyMedium, ) }, @@ -548,7 +541,7 @@ private fun SearchEmptyState( ) { LazyColumn( modifier = Modifier.fillMaxWidth(), - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = readingHorizontalPadding()), verticalArrangement = Arrangement.spacedBy(4.dp), ) { // Saved searches diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt index b57685ec3..4bc4b2a36 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/ThreadScreen.kt @@ -199,9 +199,10 @@ fun ThreadScreen( Box(modifier = Modifier.fillMaxSize()) { ReadingColumn { + val sidePadding = readingHorizontalPadding() // Header — Messages-style: compact row with back + titleMedium Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { IconButton(onClick = onBack, modifier = Modifier.size(32.dp)) { @@ -240,7 +241,7 @@ fun ThreadScreen( else -> { LazyColumn( - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = sidePadding), verticalArrangement = Arrangement.spacedBy(0.dp), ) { // Root note diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt index 7a501d60d..c80bddd02 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/UserProfileScreen.kt @@ -30,14 +30,12 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width -import androidx.compose.foundation.layout.widthIn import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.rememberLazyListState @@ -439,20 +437,14 @@ fun UserProfileScreen( previousFirstVisibleItemScrollOffset = currentOffset } - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.TopCenter) { - Box( - modifier = - Modifier - .widthIn(max = DefaultReadingWidth) - .fillMaxWidth() - .fillMaxHeight(), - ) { + ReadingColumn { + Box(modifier = Modifier.fillMaxSize()) { if (connectedRelays.isEmpty()) { LoadingState("Connecting to relays...") } else { LazyColumn( state = listState, - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = readingHorizontalPadding()), verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier.fillMaxSize(), ) { @@ -470,10 +462,11 @@ fun UserProfileScreen( ) } - // Header — Messages-style: compact row, titleMedium title + // Header — Messages-style: compact row, titleMedium title. + // Horizontal gutter already supplied by LazyColumn.contentPadding. item(key = "header") { Row( - modifier = Modifier.fillMaxWidth().padding(horizontal = 12.dp, vertical = 8.dp), + modifier = Modifier.fillMaxWidth().padding(vertical = 8.dp), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { @@ -902,8 +895,11 @@ fun UserProfileScreen( } } - // Floating header — appears on scroll up when profile header is out of view - AnimatedVisibility( + // Floating header — appears on scroll up when profile header is out of view. + // Fully-qualified call to force the non-scoped overload; ReadingColumn + // provides a ColumnScope in the outer lambda which would otherwise win + // overload resolution and break the BoxScope Modifier.align call below. + androidx.compose.animation.AnimatedVisibility( visible = showFloatingHeader, enter = slideInVertically { -it }, exit = slideOutVertically { -it }, diff --git a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt index f6fc2d797..67c8baa34 100644 --- a/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt +++ b/desktopApp/src/jvmMain/kotlin/com/vitorpamplona/amethyst/desktop/ui/highlights/MyHighlightsScreen.kt @@ -73,12 +73,15 @@ fun MyHighlightsScreen( var deleteTarget by remember { mutableStateOf(null) } com.vitorpamplona.amethyst.desktop.ui.ReadingColumn { + val sidePadding = + com.vitorpamplona.amethyst.desktop.ui + .readingHorizontalPadding() Row( modifier = Modifier .fillMaxWidth() .heightIn(min = 48.dp) - .padding(horizontal = 12.dp, vertical = 8.dp), + .padding(horizontal = sidePadding, vertical = 8.dp), verticalAlignment = Alignment.CenterVertically, ) { Text( @@ -95,7 +98,7 @@ fun MyHighlightsScreen( ) } else { LazyColumn( - contentPadding = PaddingValues(horizontal = 12.dp), + contentPadding = PaddingValues(horizontal = sidePadding), verticalArrangement = Arrangement.spacedBy(8.dp), ) { allHighlights.forEach { (addressTag, highlights) ->