feat(desktop): full-width search expansion with Cmd+F via CompositionLocal

- FeedTabsHeader expands to full-width search card when active
  - Feed tabs hidden, search input fills entire header width
  - History section (recent + saved) shown below input
  - "Open full search" link at bottom
- Scrim overlay dims feed content when search is active
- Cmd+F toggles search via LocalFeedSearchActive CompositionLocal
  - MutableState<Boolean> provided at Window level
  - FeedScreen reads it directly, no param threading needed
- SearchPill simplified back to clickable-only (no inline expansion)
- Escape or click scrim dismisses search

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
nrobi144
2026-05-29 07:02:50 +03:00
co-authored by Claude Opus 4.6
parent c69c4462b9
commit ec8fbd3980
4 changed files with 342 additions and 247 deletions
@@ -263,6 +263,7 @@ fun main() {
var showAppDrawer by remember { mutableStateOf(false) }
var showAddColumnDialog by remember { mutableStateOf(false) }
var showImportFollowListDialog by remember { mutableStateOf(false) }
val feedSearchActiveState = remember { mutableStateOf(false) }
// Tor state at Window level — survives key() app rebuild
var torSettings by remember {
@@ -451,7 +452,7 @@ fun main() {
} else {
KeyShortcut(Key.F, ctrl = true)
},
onClick = { navigateToScreen?.invoke(DeckColumnType.Search) },
onClick = { feedSearchActiveState.value = !feedSearchActiveState.value },
)
Item(
"App Drawer",
@@ -617,39 +618,43 @@ fun main() {
LocalIsImmersiveFullscreen provides immersiveFullscreenState,
) {
key(appRestartKey) {
App(
layoutMode = layoutMode,
onLayoutModeChange = { newMode ->
layoutMode = newMode
DesktopPreferences.layoutMode = newMode.name
},
deckState = deckState,
workspaceManager = workspaceManager,
accountManager = accountManager,
showComposeDialog = showComposeDialog,
showAppDrawer = showAppDrawer,
onShowComposeDialog = { showComposeDialog = true },
onShowReplyDialog = { event ->
replyToNote = event
showComposeDialog = true
},
onDismissComposeDialog = {
showComposeDialog = false
replyToNote = null
},
onDismissAppDrawer = { showAppDrawer = false },
onShowAppDrawer = { showAppDrawer = true },
replyToNote = replyToNote,
showImportFollowListDialog = showImportFollowListDialog,
onShowImportFollowListDialog = { showImportFollowListDialog = true },
onDismissImportFollowListDialog = { showImportFollowListDialog = false },
onRestartApp = { appRestartKey++ },
torManager = torManager,
torTypeFlow = torTypeFlow,
externalPortFlow = externalPortFlow,
initialTorSettings = torSettings,
onNavigateToScreen = { navigateToScreen = it },
)
CompositionLocalProvider(
com.vitorpamplona.amethyst.desktop.ui.theme.LocalFeedSearchActive provides feedSearchActiveState,
) {
App(
layoutMode = layoutMode,
onLayoutModeChange = { newMode ->
layoutMode = newMode
DesktopPreferences.layoutMode = newMode.name
},
deckState = deckState,
workspaceManager = workspaceManager,
accountManager = accountManager,
showComposeDialog = showComposeDialog,
showAppDrawer = showAppDrawer,
onShowComposeDialog = { showComposeDialog = true },
onShowReplyDialog = { event ->
replyToNote = event
showComposeDialog = true
},
onDismissComposeDialog = {
showComposeDialog = false
replyToNote = null
},
onDismissAppDrawer = { showAppDrawer = false },
onShowAppDrawer = { showAppDrawer = true },
replyToNote = replyToNote,
showImportFollowListDialog = showImportFollowListDialog,
onShowImportFollowListDialog = { showImportFollowListDialog = true },
onDismissImportFollowListDialog = { showImportFollowListDialog = false },
onRestartApp = { appRestartKey++ },
torManager = torManager,
torTypeFlow = torTypeFlow,
externalPortFlow = externalPortFlow,
initialTorSettings = torSettings,
onNavigateToScreen = { navigateToScreen = it },
)
}
}
}
}
@@ -23,6 +23,9 @@ 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.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
@@ -34,10 +37,13 @@ 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.foundation.text.BasicTextField
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.FilterChip
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
@@ -54,7 +60,18 @@ import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
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.graphics.Color
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.input.TextFieldValue
import androidx.compose.ui.unit.dp
import androidx.compose.ui.zIndex
import com.vitorpamplona.amethyst.commons.compose.elements.BoostedMark
import com.vitorpamplona.amethyst.commons.compose.layouts.GenericRepostLayout
import com.vitorpamplona.amethyst.commons.icons.symbols.Icon
@@ -66,12 +83,14 @@ import com.vitorpamplona.amethyst.commons.ui.components.LoadingState
import com.vitorpamplona.amethyst.commons.ui.components.UserAvatar
import com.vitorpamplona.amethyst.commons.ui.feeds.FeedState
import com.vitorpamplona.amethyst.desktop.DesktopPreferences
import com.vitorpamplona.amethyst.desktop.SearchHistoryStore
import com.vitorpamplona.amethyst.desktop.account.AccountState
import com.vitorpamplona.amethyst.desktop.cache.DesktopLocalCache
import com.vitorpamplona.amethyst.desktop.feeds.DesktopCustomFeedFilter
import com.vitorpamplona.amethyst.desktop.feeds.DesktopFollowingFeedFilter
import com.vitorpamplona.amethyst.desktop.feeds.DesktopGlobalFeedFilter
import com.vitorpamplona.amethyst.desktop.network.DesktopRelayConnectionManager
import com.vitorpamplona.amethyst.desktop.platform.PlatformInfo
import com.vitorpamplona.amethyst.desktop.subscriptions.DesktopRelaySubscriptionsCoordinator
import com.vitorpamplona.amethyst.desktop.subscriptions.FeedMode
import com.vitorpamplona.amethyst.desktop.subscriptions.FilterBuilders
@@ -300,6 +319,9 @@ fun FeedScreen(
onNavigateToRelays: () -> Unit = {},
onSearchClick: () -> Unit = {},
) {
val feedSearchActiveState = com.vitorpamplona.amethyst.desktop.ui.theme.LocalFeedSearchActive.current
var searchActive by feedSearchActiveState
val onSearchActiveChange: (Boolean) -> Unit = { searchActive = it }
val relayStatuses by relayManager.relayStatuses.collectAsState()
val connectedRelays by relayManager.connectedRelays.collectAsState()
val followedUsers by localCache.followedUsers.collectAsState()
@@ -560,10 +582,12 @@ fun FeedScreen(
Box(modifier = Modifier.fillMaxSize()) {
ReadingColumn {
// Header: pinned feed tabs + "Show More +"
// Header: pinned feed tabs + search pill (or expanded search card)
FeedTabsHeader(
feedMode = feedMode,
activeFeedId = activeFeedId,
searchExpanded = searchActive,
onSearchExpandedChange = onSearchActiveChange,
onFeedModeChange = { mode ->
feedMode = mode
activeFeedId = null
@@ -724,6 +748,22 @@ fun FeedScreen(
)
}
// Search scrim — dims feed content when search is expanded
if (searchActive) {
Box(
modifier =
Modifier
.fillMaxSize()
.background(Color.Black.copy(alpha = 0.3f))
.clickable(
interactionSource = remember { MutableInteractionSource() },
indication = null,
) {
onSearchActiveChange(false)
},
)
}
// Lightbox overlay
lightboxState?.let { state ->
LightboxOverlay(
@@ -750,6 +790,8 @@ fun FeedScreen(
private fun FeedTabsHeader(
feedMode: FeedMode,
activeFeedId: String? = null,
searchExpanded: Boolean = false,
onSearchExpandedChange: (Boolean) -> Unit = {},
onFeedModeChange: (FeedMode) -> Unit,
onNavigateToFeed: (com.vitorpamplona.amethyst.commons.feeds.custom.FeedDefinition) -> Unit = {},
onOpenFeedsDrawer: () -> Unit,
@@ -764,58 +806,265 @@ private fun FeedTabsHeader(
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),
modifier =
Modifier
.fillMaxWidth()
.padding(horizontal = sidePadding + 8.dp, vertical = 8.dp)
.zIndex(if (searchExpanded) 10f else 0f),
) {
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 ->
feedMode == FeedMode.FOLLOWING
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
feedMode == FeedMode.GLOBAL
else -> activeFeedId == feed.id
Column {
Row(
modifier = Modifier.fillMaxWidth().padding(horizontal = 8.dp, vertical = 6.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(6.dp),
) {
if (!searchExpanded) {
// Compact feed tabs
pinnedFeeds.forEach { feed ->
val isSelected =
when (feed.source) {
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
feedMode == FeedMode.FOLLOWING
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
feedMode == FeedMode.GLOBAL
else -> activeFeedId == feed.id
}
FilterChip(
selected = isSelected,
onClick = {
when (feed.source) {
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
onFeedModeChange(FeedMode.FOLLOWING)
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
onFeedModeChange(FeedMode.GLOBAL)
else -> onNavigateToFeed(feed)
}
},
label = {
Text(
"${feed.emoji} ${feed.name}",
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
)
},
)
}
FilterChip(
selected = isSelected,
onClick = {
when (feed.source) {
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Following ->
onFeedModeChange(FeedMode.FOLLOWING)
is com.vitorpamplona.amethyst.commons.feeds.custom.FeedSource.Global ->
onFeedModeChange(FeedMode.GLOBAL)
else -> onNavigateToFeed(feed)
}
// Search pill / expanded input — takes remaining center space
if (searchExpanded) {
SearchInput(
onDismiss = { onSearchExpandedChange(false) },
onOpenFullSearch = onSearchClick,
modifier = Modifier.weight(1f),
)
} else {
com.vitorpamplona.amethyst.desktop.ui.search.SearchPill(
onClick = { onSearchExpandedChange(true) },
modifier = Modifier.weight(1f),
)
}
if (!searchExpanded) {
// Compose button
IconButton(onClick = onCompose, modifier = Modifier.size(32.dp)) {
Icon(
MaterialSymbols.Edit,
contentDescription = "Compose",
modifier = Modifier.size(18.dp),
)
}
}
}
// Expanded search: history section below the input
if (searchExpanded) {
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
SearchHistorySection(
onOpenFullSearch = {
onSearchExpandedChange(false)
onSearchClick()
},
)
}
}
}
}
@Composable
private fun SearchInput(
onDismiss: () -> Unit,
onOpenFullSearch: () -> Unit,
modifier: Modifier = Modifier,
) {
val focusRequester = remember { FocusRequester() }
val focusManager = LocalFocusManager.current
var textFieldValue by remember { mutableStateOf(TextFieldValue("")) }
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = modifier,
) {
Icon(
MaterialSymbols.Search,
contentDescription = null,
modifier = Modifier.size(20.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.width(8.dp))
BasicTextField(
value = textFieldValue,
onValueChange = { textFieldValue = it },
modifier =
Modifier
.weight(1f)
.focusRequester(focusRequester)
.onPreviewKeyEvent { event ->
if (event.type == KeyEventType.KeyDown) {
when (event.key) {
Key.Escape -> {
onDismiss()
focusManager.clearFocus()
true
}
Key.Enter -> {
onOpenFullSearch()
true
}
else -> false
}
} else {
false
}
},
label = {
textStyle =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface,
),
singleLine = true,
decorationBox = { innerTextField ->
Box(contentAlignment = Alignment.CenterStart) {
if (textFieldValue.text.isEmpty()) {
Text(
"${feed.emoji} ${feed.name}",
maxLines = 1,
style = MaterialTheme.typography.labelMedium,
"Search notes, profiles, hashtags...",
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
)
},
)
}
}
innerTextField()
}
},
)
Spacer(Modifier.width(8.dp))
Text(
if (PlatformInfo.isMacOS) "\u2318F" else "Ctrl+F",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.5f),
)
}
// Search pill — takes remaining center space
com.vitorpamplona.amethyst.desktop.ui.search.SearchPill(
onClick = onSearchClick,
modifier = Modifier.weight(1f),
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
@Composable
private fun SearchHistorySection(onOpenFullSearch: () -> Unit) {
val history by SearchHistoryStore.history.collectAsState()
val savedSearches by SearchHistoryStore.savedSearches.collectAsState()
Column(
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
) {
if (history.isNotEmpty()) {
Text(
"Recent",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
)
history.take(5).forEach { query ->
val text =
com.vitorpamplona.amethyst.commons.search.QuerySerializer
.serialize(query)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.clickable { onOpenFullSearch() }
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Icon(
MaterialSymbols.History,
null,
Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.width(10.dp))
Text(text, style = MaterialTheme.typography.bodySmall, maxLines = 1)
}
}
}
// Compose button
IconButton(onClick = onCompose, modifier = Modifier.size(32.dp)) {
Icon(
MaterialSymbols.Edit,
contentDescription = "Compose",
modifier = Modifier.size(18.dp),
if (savedSearches.isNotEmpty()) {
if (history.isNotEmpty()) {
HorizontalDivider(
color = MaterialTheme.colorScheme.outlineVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
)
}
Text(
"Saved",
style = MaterialTheme.typography.labelSmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 6.dp),
)
savedSearches.take(5).forEach { saved ->
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.clickable { onOpenFullSearch() }
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Icon(
MaterialSymbols.Bookmark,
null,
Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.width(10.dp))
Text(saved.label, style = MaterialTheme.typography.bodySmall, maxLines = 1)
}
}
}
HorizontalDivider(
color = MaterialTheme.colorScheme.outlineVariant,
modifier = Modifier.padding(horizontal = 16.dp, vertical = 4.dp),
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier =
Modifier
.fillMaxWidth()
.clickable { onOpenFullSearch() }
.padding(horizontal = 16.dp, vertical = 8.dp),
) {
Icon(
MaterialSymbols.AutoMirrored.OpenInNew,
null,
Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.primary,
)
Spacer(Modifier.width(10.dp))
Text(
"Open full search",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary,
)
}
}
}
@@ -20,46 +20,22 @@
*/
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.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
@@ -69,16 +45,8 @@ fun SearchPill(
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 = { expanded = true },
onClick = onClick,
shape = RoundedCornerShape(999.dp),
color = MaterialTheme.colorScheme.surfaceContainerHigh,
modifier = modifier.height(36.dp).hoverHighlight(),
@@ -94,52 +62,12 @@ fun SearchPill(
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
Spacer(Modifier.width(8.dp))
if (expanded) {
BasicTextField(
value = textFieldValue,
onValueChange = { textFieldValue = it },
modifier =
Modifier
.weight(1f)
.focusRequester(focusRequester)
.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()
}
},
)
} else {
Text(
"Search...",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f),
)
}
Text(
"Search...",
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.weight(1f),
)
Spacer(Modifier.width(8.dp))
Text(
shortcutHint,
@@ -148,96 +76,4 @@ fun SearchPill(
)
}
}
// Dropdown below the pill when expanded
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))
}.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)
}
}
if (expanded) {
LaunchedEffect(Unit) {
focusRequester.requestFocus()
}
}
}
@@ -24,6 +24,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.ReadOnlyComposable
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp
@@ -48,6 +50,9 @@ val LocalSpacing = staticCompositionLocalOf { AmethystSpacing() }
val LocalIsDarkTheme = staticCompositionLocalOf { false }
/** Mutable state for Cmd+F feed search. Provided at Main.kt level. */
val LocalFeedSearchActive = compositionLocalOf { mutableStateOf(false) }
val MaterialTheme.spacing: AmethystSpacing
@Composable @ReadOnlyComposable
get() = LocalSpacing.current