Updated on 2026-08-14
This commit is contained in:
parent
80c206e13b
commit
df4186cca0
7 changed files with 672 additions and 2 deletions
|
|
@ -214,12 +214,15 @@ private inline fun ProvideButtonRippleConfiguration(crossinline content: @Compos
|
|||
|
||||
@Composable
|
||||
private fun TangemButtonIcon(tangemIconUM: TangemIconUM?, isVisible: Boolean, size: TangemButtonSize) {
|
||||
val wrappedIconUM = rememberLastNonNull(tangemIconUM)
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = isVisible,
|
||||
modifier = Modifier.size(size = size.toContentSize()),
|
||||
) {
|
||||
val wrappedIconUM = remember(tangemIconUM) { requireNotNull(tangemIconUM) }
|
||||
TangemIcon(tangemIconUM = wrappedIconUM)
|
||||
wrappedIconUM?.let {
|
||||
TangemIcon(tangemIconUM = it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,325 @@
|
|||
package com.tangem.core.ui.ds2.search
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.core.CubicBezierEasing
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.animation.expandHorizontally
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.animation.scaleIn
|
||||
import androidx.compose.animation.scaleOut
|
||||
import androidx.compose.animation.shrinkHorizontally
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.heightIn
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
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.graphics.SolidColor
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.platform.LocalSoftwareKeyboardController
|
||||
import androidx.compose.ui.semantics.contentDescription
|
||||
import androidx.compose.ui.semantics.semantics
|
||||
import androidx.compose.ui.text.PlatformTextStyle
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.KeyboardType
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.tooling.preview.PreviewParameter
|
||||
import androidx.compose.ui.tooling.preview.datasource.CollectionPreviewParameterProvider
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds.image.TangemIconUM
|
||||
import com.tangem.core.ui.ds2.button.TangemButton
|
||||
import com.tangem.core.ui.ds2.surface.TangemSurface
|
||||
import com.tangem.core.ui.extensions.TextReference
|
||||
import com.tangem.core.ui.extensions.resolveReference
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.core.ui.res.TangemThemePreviewRedesign
|
||||
import com.tangem.core.ui.res.generated.icons.Icons
|
||||
import com.tangem.core.ui.res.generated.icons.ic_cross_20
|
||||
import com.tangem.core.ui.res.generated.icons.ic_cross_circle_20_filled
|
||||
import com.tangem.core.ui.res.generated.icons.ic_search_20
|
||||
|
||||
/**
|
||||
* Search component (DS V3)
|
||||
*
|
||||
* [Figma](https://www.figma.com/design/AsnJ5CPHib4Qxw12gszjMS/%F0%9F%92%A0-DS-Components?node-id=3953-249&m=dev)
|
||||
*
|
||||
* @param state Hoisted state holding the query, active flag, placeholder, and callbacks.
|
||||
* When [TangemSearch.State.onCloseClick] is `null`, the trailing close button is omitted.
|
||||
* @param modifier Modifier applied to the outer row container.
|
||||
* @param focusRequester Focus requester wired to the text field. Pass a hoisted instance to
|
||||
* programmatically focus the field from the caller (e.g., on screen entry). Tapping anywhere on
|
||||
* the surface also invokes [FocusRequester.requestFocus] on this instance.
|
||||
*/
|
||||
@Composable
|
||||
fun TangemSearch(
|
||||
state: TangemSearch.State,
|
||||
modifier: Modifier = Modifier,
|
||||
focusRequester: FocusRequester = remember { FocusRequester() },
|
||||
) {
|
||||
val openingEasing = CubicBezierEasing(a = 0.8f, b = 0f, c = 0.2f, d = 1f)
|
||||
val closingEasing = CubicBezierEasing(a = 0.8f, b = 0f, c = 0.8f, d = 1f)
|
||||
|
||||
Row(
|
||||
modifier = modifier,
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
TangemSurface(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.heightIn(min = TangemTheme.dimens3.size.s550),
|
||||
isMaterial = true,
|
||||
shape = CircleShape,
|
||||
onClick = focusRequester::requestFocus,
|
||||
) {
|
||||
SearchField(state = state, focusRequester = focusRequester)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.onCloseClick != null && (state.isActive || state.query.isNotEmpty()),
|
||||
enter = expandHorizontally(animationSpec = tween(durationMillis = 150, easing = openingEasing)) +
|
||||
fadeIn(animationSpec = tween(durationMillis = 150, delayMillis = 150, easing = openingEasing)) +
|
||||
scaleIn(animationSpec = tween(durationMillis = 150, delayMillis = 150, easing = openingEasing)),
|
||||
exit = fadeOut(animationSpec = tween(durationMillis = 150, easing = closingEasing)) +
|
||||
scaleOut(animationSpec = tween(durationMillis = 150, easing = closingEasing)) +
|
||||
shrinkHorizontally(animationSpec = tween(durationMillis = 150, easing = closingEasing)),
|
||||
) {
|
||||
CloseButton(onClick = state.onCloseClick ?: {})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
object TangemSearch {
|
||||
|
||||
@Immutable
|
||||
data class State(
|
||||
val placeholderText: TextReference,
|
||||
val query: String,
|
||||
val onQueryChange: (String) -> Unit,
|
||||
val isActive: Boolean,
|
||||
val onActiveChange: (Boolean) -> Unit,
|
||||
val onClearClick: () -> Unit = { },
|
||||
val onCloseClick: (() -> Unit)? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SearchField(state: TangemSearch.State, focusRequester: FocusRequester) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.weight(1f)
|
||||
.padding(
|
||||
start = TangemTheme.dimens3.spacing.s150,
|
||||
top = TangemTheme.dimens3.spacing.s150,
|
||||
bottom = TangemTheme.dimens3.spacing.s150,
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens3.spacing.s100),
|
||||
imageVector = Icons.ic_search_20,
|
||||
tint = TangemTheme.colors3.icon.primary,
|
||||
contentDescription = null,
|
||||
)
|
||||
QueryTextField(state = state, focusRequester = focusRequester)
|
||||
}
|
||||
|
||||
AnimatedVisibility(
|
||||
visible = state.query.isNotEmpty(),
|
||||
enter = fadeIn(),
|
||||
exit = fadeOut(),
|
||||
) {
|
||||
ClearButton(
|
||||
onClick = {
|
||||
state.onQueryChange("")
|
||||
state.onClearClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun QueryTextField(state: TangemSearch.State, focusRequester: FocusRequester) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
// Initial composition reports isFocused = false. Without this guard the field would
|
||||
// clobber a parent-supplied `state.isActive = true` on the first frame.
|
||||
var isInitialComposition by remember { mutableStateOf(true) }
|
||||
LaunchedEffect(Unit) { isInitialComposition = false }
|
||||
|
||||
val placeholder = state.placeholderText.resolveReference()
|
||||
val sharedTextStyle = TangemTheme.typography3.body.medium.copy(
|
||||
platformStyle = PlatformTextStyle(includeFontPadding = false),
|
||||
)
|
||||
BasicTextField(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.focusRequester(focusRequester)
|
||||
.semantics { contentDescription = placeholder }
|
||||
.onFocusChanged { focusState ->
|
||||
if (!isInitialComposition) {
|
||||
state.onActiveChange(focusState.isFocused)
|
||||
}
|
||||
},
|
||||
value = state.query,
|
||||
onValueChange = state.onQueryChange,
|
||||
singleLine = true,
|
||||
textStyle = sharedTextStyle.copy(color = TangemTheme.colors3.text.primary),
|
||||
cursorBrush = SolidColor(TangemTheme.colors3.icon.brand),
|
||||
keyboardOptions = KeyboardOptions(
|
||||
keyboardType = KeyboardType.Text,
|
||||
imeAction = ImeAction.Search,
|
||||
),
|
||||
keyboardActions = KeyboardActions(
|
||||
onSearch = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
},
|
||||
),
|
||||
decorationBox = { innerTextField ->
|
||||
Box {
|
||||
if (state.query.isEmpty()) {
|
||||
Text(
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens3.spacing.s250),
|
||||
text = placeholder,
|
||||
style = sharedTextStyle,
|
||||
color = TangemTheme.colors3.text.secondary,
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
)
|
||||
}
|
||||
innerTextField()
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ClearButton(onClick: () -> Unit) {
|
||||
TangemButton(
|
||||
modifier = Modifier.padding(end = TangemTheme.dimens3.spacing.s050),
|
||||
size = TangemButton.Size.X9,
|
||||
variant = TangemButton.Variant.Ghost,
|
||||
iconStart = TangemIconUM.Icon(Icons.ic_cross_circle_20_filled),
|
||||
onClick = onClick,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun CloseButton(onClick: () -> Unit) {
|
||||
val keyboardController = LocalSoftwareKeyboardController.current
|
||||
val focusManager = LocalFocusManager.current
|
||||
TangemButton(
|
||||
modifier = Modifier.padding(start = TangemTheme.dimens3.spacing.s100),
|
||||
size = TangemButton.Size.X11,
|
||||
variant = TangemButton.Variant.Material,
|
||||
iconStart = TangemIconUM.Icon(Icons.ic_cross_20),
|
||||
onClick = {
|
||||
keyboardController?.hide()
|
||||
focusManager.clearFocus()
|
||||
onClick()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@Preview(showBackground = true)
|
||||
@Preview(showBackground = true, uiMode = Configuration.UI_MODE_NIGHT_YES)
|
||||
@Composable
|
||||
private fun Preview(@PreviewParameter(TangemSearchStateProvider::class) state: TangemSearch.State) {
|
||||
TangemThemePreviewRedesign {
|
||||
Box(modifier = Modifier.background(TangemTheme.colors3.bg.secondary)) {
|
||||
TangemSearch(
|
||||
state = state,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private class TangemSearchStateProvider : CollectionPreviewParameterProvider<TangemSearch.State>(
|
||||
collection = listOf(
|
||||
// Idle: empty + inactive — no clear, no close.
|
||||
TangemSearch.State(
|
||||
placeholderText = stringReference("Search"),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
// Focused empty: active + empty — close visible, clear hidden.
|
||||
TangemSearch.State(
|
||||
placeholderText = stringReference("Search"),
|
||||
query = "",
|
||||
onQueryChange = {},
|
||||
isActive = true,
|
||||
onActiveChange = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
// Typing: active + query — clear and close both visible.
|
||||
TangemSearch.State(
|
||||
placeholderText = stringReference("Search"),
|
||||
query = "Bitcoin",
|
||||
onQueryChange = {},
|
||||
isActive = true,
|
||||
onActiveChange = {},
|
||||
onClearClick = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
// Filled but blurred: query persists without focus — close stays visible.
|
||||
TangemSearch.State(
|
||||
placeholderText = stringReference("Search"),
|
||||
query = "Ethereum",
|
||||
onQueryChange = {},
|
||||
isActive = false,
|
||||
onActiveChange = {},
|
||||
onClearClick = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
// Embedded variant: no close affordance at all (onCloseClick = null).
|
||||
TangemSearch.State(
|
||||
placeholderText = stringReference("Filter tokens"),
|
||||
query = "USDT",
|
||||
onQueryChange = {},
|
||||
isActive = true,
|
||||
onActiveChange = {},
|
||||
onClearClick = {},
|
||||
onCloseClick = null,
|
||||
),
|
||||
// Long query that exercises clipping inside the surface.
|
||||
TangemSearch.State(
|
||||
placeholderText = stringReference("Search"),
|
||||
query = "A very long search query that should clip nicely",
|
||||
onQueryChange = {},
|
||||
isActive = true,
|
||||
onActiveChange = {},
|
||||
onClearClick = {},
|
||||
onCloseClick = {},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
|
@ -291,6 +291,33 @@ internal data class TangemTopNavigationStory(
|
|||
}
|
||||
}
|
||||
|
||||
@Suppress("BooleanPropertyNaming")
|
||||
internal data class TangemSearchStory(
|
||||
val background: Background,
|
||||
val placeholder: Placeholder,
|
||||
val hasCloseButton: Boolean,
|
||||
val onBackgroundChange: (Background) -> Unit,
|
||||
val onPlaceholderChange: (Placeholder) -> Unit,
|
||||
val onCloseButtonToggle: () -> Unit,
|
||||
) : DsStoryBookPage {
|
||||
|
||||
/** Backdrop the search preview is rendered on top of. */
|
||||
enum class Background(val label: String) {
|
||||
Rainbow("rainbow"),
|
||||
BgPrimary("bg.primary"),
|
||||
BgSecondary("bg.secondary"),
|
||||
BgBrand("bg.brand"),
|
||||
BgInverse("bg.inverse"),
|
||||
}
|
||||
|
||||
/** Placeholder length variants — short typical label vs. long string to test layout. */
|
||||
enum class Placeholder(val label: String, val text: String) {
|
||||
Short("short", "Search"),
|
||||
Medium("medium", "Filter tokens"),
|
||||
Long("long", "Search by name, symbol or contract address"),
|
||||
}
|
||||
}
|
||||
|
||||
internal data class TangemBadgeV2Story(
|
||||
val variant: TangemBadge.Variant,
|
||||
val status: TangemBadge.Status,
|
||||
|
|
|
|||
|
|
@ -20,6 +20,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.button.tangemBut
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.tangemFadeStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.tangemLoaderStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.tangemRowStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.tangemSearchStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.tangemShimmerStoryFactory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.tangemTopNavigationStoryFactory
|
||||
|
||||
|
|
@ -30,6 +31,7 @@ private fun buildDsStories() = listOf(
|
|||
DsStoryItem(title = "🔘 TangemButton", factory = tangemButtonStoryFactory),
|
||||
DsStoryItem(title = "🏷️ TangemBadge", factory = tangemBadgeV2StoryFactory),
|
||||
DsStoryItem(title = "📋 TangemRow", factory = tangemRowStoryFactory),
|
||||
DsStoryItem(title = "🔎 TangemSearch", factory = tangemSearchStoryFactory),
|
||||
DsStoryItem(title = "✨ TangemShimmer", factory = tangemShimmerStoryFactory),
|
||||
DsStoryItem(title = "🌫️ TangemFade", factory = tangemFadeStoryFactory),
|
||||
DsStoryItem(title = "🧭 TangemTopNavigation", factory = tangemTopNavigationStoryFactory),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,25 @@
|
|||
package com.tangem.feature.tester.presentation.storybook.page.ds.search
|
||||
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchStory
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.StateUpdater
|
||||
import com.tangem.feature.tester.presentation.storybook.viewmodel.storyPageFactory
|
||||
|
||||
internal fun StateUpdater<TangemSearchStory>.build(): TangemSearchStory {
|
||||
return TangemSearchStory(
|
||||
background = TangemSearchStory.Background.BgSecondary,
|
||||
placeholder = TangemSearchStory.Placeholder.Short,
|
||||
hasCloseButton = true,
|
||||
onBackgroundChange = { background ->
|
||||
updateStory { it.copy(background = background) }
|
||||
},
|
||||
onPlaceholderChange = { placeholder ->
|
||||
updateStory { it.copy(placeholder = placeholder) }
|
||||
},
|
||||
onCloseButtonToggle = {
|
||||
updateStory { it.copy(hasCloseButton = !it.hasCloseButton) }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
internal val tangemSearchStoryFactory
|
||||
get() = storyPageFactory(StateUpdater<TangemSearchStory>::build)
|
||||
|
|
@ -0,0 +1,286 @@
|
|||
@file:Suppress("MagicNumber")
|
||||
|
||||
package com.tangem.feature.tester.presentation.storybook.page.ds.search
|
||||
|
||||
import androidx.compose.animation.core.LinearEasing
|
||||
import androidx.compose.animation.core.RepeatMode
|
||||
import androidx.compose.animation.core.animateFloat
|
||||
import androidx.compose.animation.core.infiniteRepeatable
|
||||
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.TileMode
|
||||
import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.tangem.core.ui.ds2.search.TangemSearch
|
||||
import com.tangem.core.ui.extensions.stringReference
|
||||
import com.tangem.core.ui.res.TangemTheme
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchStory
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchStory.Background
|
||||
import com.tangem.feature.tester.presentation.storybook.entity.TangemSearchStory.Placeholder
|
||||
|
||||
@Composable
|
||||
internal fun TangemSearchStory(state: TangemSearchStory, modifier: Modifier = Modifier) {
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxSize()
|
||||
.background(TangemTheme.colors.background.primary)
|
||||
.padding(vertical = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
ComponentPreview(state = state)
|
||||
Column(
|
||||
modifier = Modifier.verticalScroll(rememberScrollState()),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||
) {
|
||||
BackgroundSelector(selected = state.background, onSelect = state.onBackgroundChange)
|
||||
PlaceholderSelector(selected = state.placeholder, onSelect = state.onPlaceholderChange)
|
||||
Toggles(state = state)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ComponentPreview(state: TangemSearchStory) {
|
||||
var query by remember { mutableStateOf("") }
|
||||
var isActive by remember { mutableStateOf(false) }
|
||||
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(RoundedCornerShape(16.dp)),
|
||||
) {
|
||||
PreviewBackground(background = state.background, modifier = Modifier.matchParentSize())
|
||||
TangemSearch(
|
||||
state = TangemSearch.State(
|
||||
placeholderText = stringReference(state.placeholder.text),
|
||||
query = query,
|
||||
onQueryChange = { query = it },
|
||||
isActive = isActive,
|
||||
onActiveChange = { isActive = it },
|
||||
onClearClick = { query = "" },
|
||||
// Close handles keyboard + focus internally; parent just clears the text.
|
||||
onCloseClick = if (state.hasCloseButton) {
|
||||
{ query = "" }
|
||||
} else {
|
||||
null
|
||||
},
|
||||
),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(16.dp),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PreviewBackground(background: Background, modifier: Modifier = Modifier) {
|
||||
when (background) {
|
||||
Background.Rainbow -> BlurTestBackground(modifier = modifier)
|
||||
Background.BgPrimary -> Box(modifier.background(TangemTheme.colors3.bg.primary))
|
||||
Background.BgSecondary -> Box(modifier.background(TangemTheme.colors3.bg.secondary))
|
||||
Background.BgBrand -> Box(modifier.background(TangemTheme.colors3.bg.brand))
|
||||
Background.BgInverse -> Box(modifier.background(TangemTheme.colors3.bg.inverse))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BlurTestBackground(modifier: Modifier = Modifier) {
|
||||
val bands = remember {
|
||||
listOf(
|
||||
Color(0xFFFF1744), // red
|
||||
Color(0xFFFF9100), // orange
|
||||
Color(0xFFFFEA00), // yellow
|
||||
Color(0xFF00E676), // green
|
||||
Color(0xFF00B8D4), // cyan
|
||||
Color(0xFF2962FF), // blue
|
||||
Color(0xFFD500F9), // magenta
|
||||
)
|
||||
}
|
||||
val stops = remember(bands) {
|
||||
buildList {
|
||||
bands.forEachIndexed { index, color ->
|
||||
val start = index.toFloat() / bands.size
|
||||
val end = (index + 1).toFloat() / bands.size
|
||||
add(start to color)
|
||||
add(end to color)
|
||||
}
|
||||
}.toTypedArray()
|
||||
}
|
||||
val tilePx = with(LocalDensity.current) { 320.dp.toPx() }
|
||||
val transition = rememberInfiniteTransition(label = "blur-bg")
|
||||
val offset by transition.animateFloat(
|
||||
initialValue = 0f,
|
||||
targetValue = tilePx,
|
||||
animationSpec = infiniteRepeatable(
|
||||
animation = tween(durationMillis = 4_000, easing = LinearEasing),
|
||||
repeatMode = RepeatMode.Restart,
|
||||
),
|
||||
label = "blur-bg-offset",
|
||||
)
|
||||
Box(
|
||||
modifier = modifier.background(
|
||||
brush = Brush.linearGradient(
|
||||
colorStops = stops,
|
||||
start = Offset(offset, 0f),
|
||||
end = Offset(offset + tilePx, 0f),
|
||||
tileMode = TileMode.Repeated,
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BackgroundSelector(selected: Background, onSelect: (Background) -> Unit) {
|
||||
Section(label = "Background") {
|
||||
ChipGrid(
|
||||
items = Background.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PlaceholderSelector(selected: Placeholder, onSelect: (Placeholder) -> Unit) {
|
||||
Section(label = "Placeholder") {
|
||||
ChipGrid(
|
||||
items = Placeholder.entries,
|
||||
label = { it.label },
|
||||
isSelected = { it == selected },
|
||||
onSelect = onSelect,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Toggles(state: TangemSearchStory) {
|
||||
Section(label = "Flags") {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
ToggleRow(
|
||||
label = "close button (onCloseClick)",
|
||||
checked = state.hasCloseButton,
|
||||
onToggle = state.onCloseButtonToggle,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Section(label: String, content: @Composable () -> Unit) {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Text(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
text = label,
|
||||
style = TangemTheme.typography.subtitle1,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
content()
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun <T> ChipGrid(items: List<T>, label: (T) -> String, isSelected: (T) -> Boolean, onSelect: (T) -> Unit) {
|
||||
val shape = RoundedCornerShape(50)
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.padding(horizontal = 16.dp)
|
||||
.clip(shape)
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.border(
|
||||
width = 1.dp,
|
||||
color = TangemTheme.colors2.border.neutral.secondary,
|
||||
shape = shape,
|
||||
)
|
||||
.padding(4.dp),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
) {
|
||||
items.forEach { item ->
|
||||
Chip(
|
||||
label = label(item),
|
||||
selected = isSelected(item),
|
||||
onClick = { onSelect(item) },
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun Chip(label: String, selected: Boolean, onClick: () -> Unit, modifier: Modifier = Modifier) {
|
||||
val chipShape = RoundedCornerShape(50)
|
||||
Box(
|
||||
contentAlignment = Alignment.Center,
|
||||
modifier = modifier
|
||||
.clip(chipShape)
|
||||
.background(
|
||||
if (selected) TangemTheme.colors2.surface.level3 else TangemTheme.colors2.surface.level2,
|
||||
)
|
||||
.clickable(onClick = onClick)
|
||||
.padding(vertical = 8.dp, horizontal = 4.dp),
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = if (selected) TangemTheme.colors.text.primary1 else TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ToggleRow(label: String, checked: Boolean, onToggle: () -> Unit) {
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.clip(RoundedCornerShape(12.dp))
|
||||
.background(TangemTheme.colors2.surface.level2)
|
||||
.clickable(onClick = onToggle)
|
||||
.padding(horizontal = 12.dp, vertical = 12.dp),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween,
|
||||
) {
|
||||
Text(
|
||||
text = label,
|
||||
style = TangemTheme.typography.body2,
|
||||
color = TangemTheme.colors.text.primary1,
|
||||
)
|
||||
Text(
|
||||
text = if (checked) "ON" else "OFF",
|
||||
style = TangemTheme.typography.caption2,
|
||||
color = if (checked) TangemTheme.colors.text.accent else TangemTheme.colors.text.secondary,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -42,6 +42,7 @@ import com.tangem.feature.tester.presentation.storybook.page.ds.button.TangemBut
|
|||
import com.tangem.feature.tester.presentation.storybook.page.ds.fade.TangemFadeStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.loader.TangemLoaderStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.row.TangemRowStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.search.TangemSearchStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.shimmer.TangemShimmerStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.ds.topnavigation.TangemTopNavigationStory
|
||||
import com.tangem.feature.tester.presentation.storybook.page.headerrow.TangemHeaderRowStory
|
||||
|
|
@ -92,6 +93,7 @@ internal fun StoryBookScreen(state: StoryBookUM, modifier: Modifier = Modifier)
|
|||
is TangemButtonStory -> TangemButtonStory(state = storyState)
|
||||
is TangemBadgeV2Story -> TangemBadgeV2Story(state = storyState)
|
||||
is TangemRowStory -> TangemRowStory(state = storyState)
|
||||
is TangemSearchStory -> TangemSearchStory(state = storyState)
|
||||
is TangemShimmerStory -> TangemShimmerStory(state = storyState)
|
||||
is TangemFadeStory -> TangemFadeStory(state = storyState)
|
||||
is TangemTopNavigationStory -> TangemTopNavigationStory(state = storyState)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue