mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-12 09:13:23 +00:00
fix(desktop): feed header card design, inline search, compact tabs
- FeedTabsHeader wrapped in Surface card (border + rounded corners) - Feed tabs compact with spacedBy(4dp), removed "+ More" (in sidebar) - SearchPill centered with weight(1f), expands inline to BasicTextField with DropdownMenu showing recent + saved searches - Removed Dialog-based SearchSpotlight (wrong UX — created separate AWT window) - Cmd+F now navigates to Search column via navigateToScreen callback - All feed tab types (Following/Global/Custom) properly handled in onClick Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c21b85c83e
commit
55e7fcb903
@@ -263,7 +263,6 @@ fun main() {
|
||||
var showAppDrawer by remember { mutableStateOf(false) }
|
||||
var showAddColumnDialog by remember { mutableStateOf(false) }
|
||||
var showImportFollowListDialog by remember { mutableStateOf(false) }
|
||||
var showSearchSpotlight by remember { mutableStateOf(false) }
|
||||
|
||||
// Tor state at Window level — survives key() app rebuild
|
||||
var torSettings by remember {
|
||||
@@ -452,7 +451,7 @@ fun main() {
|
||||
} else {
|
||||
KeyShortcut(Key.F, ctrl = true)
|
||||
},
|
||||
onClick = { showSearchSpotlight = true },
|
||||
onClick = { navigateToScreen?.invoke(DeckColumnType.Search) },
|
||||
)
|
||||
Item(
|
||||
"App Drawer",
|
||||
@@ -629,9 +628,6 @@ fun main() {
|
||||
accountManager = accountManager,
|
||||
showComposeDialog = showComposeDialog,
|
||||
showAppDrawer = showAppDrawer,
|
||||
showSearchSpotlight = showSearchSpotlight,
|
||||
onShowSearchSpotlight = { showSearchSpotlight = true },
|
||||
onDismissSearchSpotlight = { showSearchSpotlight = false },
|
||||
onShowComposeDialog = { showComposeDialog = true },
|
||||
onShowReplyDialog = { event ->
|
||||
replyToNote = event
|
||||
@@ -669,9 +665,6 @@ fun App(
|
||||
accountManager: AccountManager,
|
||||
showComposeDialog: Boolean,
|
||||
showAppDrawer: Boolean,
|
||||
showSearchSpotlight: Boolean,
|
||||
onShowSearchSpotlight: () -> Unit,
|
||||
onDismissSearchSpotlight: () -> Unit,
|
||||
onShowComposeDialog: () -> Unit,
|
||||
onShowReplyDialog: (com.vitorpamplona.quartz.nip01Core.core.Event) -> Unit,
|
||||
onDismissComposeDialog: () -> Unit,
|
||||
@@ -1076,61 +1069,6 @@ fun App(
|
||||
)
|
||||
}
|
||||
|
||||
// Search Spotlight overlay
|
||||
if (showSearchSpotlight) {
|
||||
com.vitorpamplona.amethyst.desktop.ui.search.SearchSpotlight(
|
||||
onSelectProfile = { pubkey ->
|
||||
onDismissSearchSpotlight()
|
||||
when (layoutMode) {
|
||||
LayoutMode.DECK -> {
|
||||
deckState.addColumn(DeckColumnType.Profile(pubkey))
|
||||
}
|
||||
LayoutMode.SINGLE_PANE -> {
|
||||
singlePaneState.navigate(DeckColumnType.MyProfile)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectNote = { noteId ->
|
||||
onDismissSearchSpotlight()
|
||||
when (layoutMode) {
|
||||
LayoutMode.DECK -> {
|
||||
deckState.addColumn(DeckColumnType.Thread(noteId))
|
||||
}
|
||||
LayoutMode.SINGLE_PANE -> {
|
||||
singlePaneState.navigate(DeckColumnType.HomeFeed)
|
||||
}
|
||||
}
|
||||
},
|
||||
onSelectHashtag = { tag ->
|
||||
onDismissSearchSpotlight()
|
||||
when (layoutMode) {
|
||||
LayoutMode.DECK -> {
|
||||
deckState.addColumn(DeckColumnType.Hashtag(tag))
|
||||
}
|
||||
LayoutMode.SINGLE_PANE -> {
|
||||
singlePaneState.navigate(DeckColumnType.Search)
|
||||
}
|
||||
}
|
||||
},
|
||||
onOpenFullSearch = { queryText ->
|
||||
onDismissSearchSpotlight()
|
||||
when (layoutMode) {
|
||||
LayoutMode.DECK -> {
|
||||
if (deckState.hasColumnOfType(DeckColumnType.Search)) {
|
||||
deckState.focusExistingColumn(DeckColumnType.Search)
|
||||
} else {
|
||||
deckState.addColumn(DeckColumnType.Search)
|
||||
}
|
||||
}
|
||||
LayoutMode.SINGLE_PANE -> {
|
||||
singlePaneState.navigate(DeckColumnType.Search)
|
||||
}
|
||||
}
|
||||
},
|
||||
onDismiss = { onDismissSearchSpotlight() },
|
||||
)
|
||||
}
|
||||
|
||||
// App Drawer overlay
|
||||
if (showAppDrawer) {
|
||||
val openColumns by deckState.columns.collectAsState()
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.TooltipArea
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
@@ -33,7 +34,6 @@ 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.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material3.AlertDialog
|
||||
@@ -760,69 +760,60 @@ private fun FeedTabsHeader(
|
||||
val pinnedFeeds by feedRepo.pinnedFeeds.collectAsState()
|
||||
val sidePadding = LocalReadingSidePadding.current
|
||||
|
||||
Row(
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = sidePadding + 12.dp, vertical = 8.dp),
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
Surface(
|
||||
shape = MaterialTheme.shapes.medium,
|
||||
color = MaterialTheme.colorScheme.surface,
|
||||
border = BorderStroke(1.dp, MaterialTheme.colorScheme.outlineVariant),
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = sidePadding + 8.dp, vertical = 8.dp),
|
||||
) {
|
||||
// Pinned feed tabs
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(6.dp),
|
||||
) {
|
||||
// Compact feed tabs
|
||||
pinnedFeeds.forEach { feed ->
|
||||
val isSelected =
|
||||
when (feed.source) {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following -> {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
|
||||
feedMode == FeedMode.FOLLOWING
|
||||
}
|
||||
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global -> {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
|
||||
feedMode == FeedMode.GLOBAL
|
||||
}
|
||||
|
||||
else -> {
|
||||
activeFeedId == feed.id
|
||||
}
|
||||
else -> activeFeedId == feed.id
|
||||
}
|
||||
FilterChip(
|
||||
selected = isSelected,
|
||||
onClick = {
|
||||
when (feed.source) {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following -> {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
|
||||
onFeedModeChange(FeedMode.FOLLOWING)
|
||||
}
|
||||
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global -> {
|
||||
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
|
||||
onFeedModeChange(FeedMode.GLOBAL)
|
||||
}
|
||||
|
||||
else -> {
|
||||
onNavigateToFeed(feed)
|
||||
}
|
||||
else -> onNavigateToFeed(feed)
|
||||
}
|
||||
},
|
||||
label = { Text("${feed.emoji} ${feed.name}") },
|
||||
label = {
|
||||
Text(
|
||||
"${feed.emoji} ${feed.name}",
|
||||
maxLines = 1,
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
// "Show More +" button
|
||||
FilterChip(
|
||||
selected = false,
|
||||
onClick = onOpenFeedsDrawer,
|
||||
label = { Text("+ More") },
|
||||
)
|
||||
}
|
||||
|
||||
Row(horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
// Search pill — takes remaining center space
|
||||
com.vitorpamplona.amethyst.desktop.ui.search.SearchPill(
|
||||
onClick = onSearchClick,
|
||||
modifier = Modifier.width(180.dp),
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
IconButton(onClick = onCompose) {
|
||||
|
||||
// Compose button
|
||||
IconButton(onClick = onCompose, modifier = Modifier.size(32.dp)) {
|
||||
Icon(
|
||||
MaterialSymbols.Edit,
|
||||
contentDescription = "Compose",
|
||||
modifier = Modifier.size(20.dp),
|
||||
modifier = Modifier.size(18.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+247
-24
@@ -20,60 +20,283 @@
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.desktop.ui.search
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.material3.DropdownMenu
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Surface
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.focus.FocusRequester
|
||||
import androidx.compose.ui.focus.focusRequester
|
||||
import androidx.compose.ui.focus.onFocusChanged
|
||||
import androidx.compose.ui.input.key.Key
|
||||
import androidx.compose.ui.input.key.KeyEventType
|
||||
import androidx.compose.ui.input.key.key
|
||||
import androidx.compose.ui.input.key.onPreviewKeyEvent
|
||||
import androidx.compose.ui.input.key.type
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.text.TextRange
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
|
||||
import com.vitorpamplona.amethyst.commons.icons.symbols.MaterialSymbols
|
||||
import com.vitorpamplona.amethyst.commons.search.QuerySerializer
|
||||
import com.vitorpamplona.amethyst.desktop.SearchHistoryStore
|
||||
import com.vitorpamplona.amethyst.desktop.platform.PlatformInfo
|
||||
import com.vitorpamplona.amethyst.desktop.ui.theme.hoverHighlight
|
||||
|
||||
/**
|
||||
* Search pill that expands inline into a text input with history dropdown.
|
||||
* Collapsed: clickable pill with search icon + shortcut hint.
|
||||
* Expanded: BasicTextField in same pill shape + dropdown below with recent/saved searches.
|
||||
*/
|
||||
@Composable
|
||||
fun SearchPill(
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val shortcutHint = if (PlatformInfo.isMacOS) "\u2318F" else "Ctrl+F"
|
||||
val focusRequester = remember { FocusRequester() }
|
||||
val focusManager = LocalFocusManager.current
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
|
||||
|
||||
val history by SearchHistoryStore.history.collectAsState()
|
||||
val savedSearches by SearchHistoryStore.savedSearches.collectAsState()
|
||||
|
||||
Surface(
|
||||
onClick = onClick,
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = modifier.height(36.dp).hoverHighlight(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Search...",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
shortcutHint,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
Box {
|
||||
if (expanded) {
|
||||
// Expanded: inline text input
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Search,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
BasicTextField(
|
||||
value = textFieldValue,
|
||||
onValueChange = { textFieldValue = it },
|
||||
modifier =
|
||||
Modifier
|
||||
.weight(1f)
|
||||
.focusRequester(focusRequester)
|
||||
.onFocusChanged { state ->
|
||||
if (!state.isFocused && expanded) {
|
||||
expanded = false
|
||||
textFieldValue = TextFieldValue("")
|
||||
}
|
||||
}.onPreviewKeyEvent { event ->
|
||||
if (event.type == KeyEventType.KeyDown && event.key == Key.Escape) {
|
||||
expanded = false
|
||||
textFieldValue = TextFieldValue("")
|
||||
focusManager.clearFocus()
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
},
|
||||
textStyle =
|
||||
MaterialTheme.typography.labelMedium.copy(
|
||||
color = MaterialTheme.colorScheme.onSurface,
|
||||
),
|
||||
singleLine = true,
|
||||
decorationBox = { innerTextField ->
|
||||
Box(contentAlignment = Alignment.CenterStart) {
|
||||
if (textFieldValue.text.isEmpty()) {
|
||||
Text(
|
||||
"Search notes, profiles...",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
shortcutHint,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
|
||||
// Dropdown with history
|
||||
DropdownMenu(
|
||||
expanded = expanded,
|
||||
onDismissRequest = {
|
||||
expanded = false
|
||||
textFieldValue = TextFieldValue("")
|
||||
},
|
||||
) {
|
||||
if (history.isNotEmpty()) {
|
||||
Text(
|
||||
"Recent",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
history.take(5).forEach { query ->
|
||||
val text = QuerySerializer.serialize(query)
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable {
|
||||
textFieldValue = TextFieldValue(text, TextRange(text.length))
|
||||
onClick()
|
||||
}.hoverHighlight()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.History,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(text, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (savedSearches.isNotEmpty()) {
|
||||
if (history.isNotEmpty()) {
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
}
|
||||
Text(
|
||||
"Saved",
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(horizontal = 12.dp, vertical = 4.dp),
|
||||
)
|
||||
savedSearches.take(5).forEach { saved ->
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.hoverHighlight()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Bookmark,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(saved.label, style = MaterialTheme.typography.bodySmall, maxLines = 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (history.isEmpty() && savedSearches.isEmpty()) {
|
||||
Text(
|
||||
"Type to search",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
modifier = Modifier.padding(12.dp),
|
||||
)
|
||||
}
|
||||
|
||||
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier =
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick() }
|
||||
.hoverHighlight()
|
||||
.padding(horizontal = 12.dp, vertical = 8.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.AutoMirrored.OpenInNew,
|
||||
null,
|
||||
Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Open full search",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.primary,
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Collapsed: clickable pill
|
||||
Surface(
|
||||
onClick = {
|
||||
expanded = true
|
||||
},
|
||||
shape = RoundedCornerShape(999.dp),
|
||||
color = MaterialTheme.colorScheme.surfaceContainerHigh,
|
||||
modifier = Modifier.matchParentSize(),
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
modifier = Modifier.padding(horizontal = 12.dp),
|
||||
) {
|
||||
Icon(
|
||||
MaterialSymbols.Search,
|
||||
contentDescription = "Search",
|
||||
modifier = Modifier.size(16.dp),
|
||||
tint = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
"Search...",
|
||||
style = MaterialTheme.typography.labelMedium,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||
)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Spacer(Modifier.width(8.dp))
|
||||
Text(
|
||||
shortcutHint,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.6f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (expanded) {
|
||||
LaunchedEffect(Unit) {
|
||||
focusRequester.requestFocus()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user