11package to.bitkit.ui.components
22
33import androidx.annotation.DrawableRes
4+ import androidx.compose.animation.core.LinearEasing
5+ import androidx.compose.animation.core.RepeatMode
6+ import androidx.compose.animation.core.animateFloat
7+ import androidx.compose.animation.core.infiniteRepeatable
8+ import androidx.compose.animation.core.rememberInfiniteTransition
9+ import androidx.compose.animation.core.tween
410import androidx.compose.foundation.Image
11+ import androidx.compose.foundation.background
12+ import androidx.compose.foundation.border
513import androidx.compose.foundation.layout.Arrangement
614import androidx.compose.foundation.layout.Box
715import androidx.compose.foundation.layout.Column
@@ -13,14 +21,21 @@ import androidx.compose.foundation.layout.size
1321import androidx.compose.foundation.lazy.grid.GridCells
1422import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
1523import androidx.compose.foundation.lazy.grid.items
24+ import androidx.compose.foundation.shape.RoundedCornerShape
1625import androidx.compose.material3.Icon
1726import androidx.compose.material3.IconButton
18- import androidx.compose.material3.ShapeDefaults
1927import androidx.compose.runtime.Composable
2028import androidx.compose.runtime.LaunchedEffect
29+ import androidx.compose.runtime.getValue
2130import androidx.compose.ui.Modifier
31+ import androidx.compose.ui.draw.alpha
2232import androidx.compose.ui.draw.clip
33+ import androidx.compose.ui.draw.drawBehind
34+ import androidx.compose.ui.graphics.Brush
2335import androidx.compose.ui.graphics.Color
36+ import androidx.compose.ui.graphics.drawscope.drawIntoCanvas
37+ import androidx.compose.ui.graphics.nativeCanvas
38+ import androidx.compose.ui.graphics.toArgb
2439import androidx.compose.ui.layout.ContentScale
2540import androidx.compose.ui.platform.testTag
2641import androidx.compose.ui.res.painterResource
@@ -48,6 +63,7 @@ fun SuggestionCard(
4863 duration : Duration ? = null,
4964 size : Int = 152,
5065 captionColor : Color = Colors .White64 ,
66+ dismissable : Boolean = true,
5167 onClick : () -> Unit ,
5268) {
5369 LaunchedEffect (Unit ) {
@@ -57,57 +73,195 @@ fun SuggestionCard(
5773 }
5874 }
5975
60- Box (
61- modifier = modifier
62- .size(size.dp)
63- .clip(ShapeDefaults .Large )
64- .gradientBackground(gradientColor)
65- .clickableAlpha { onClick() }
66- ) {
67- Column (
76+ Box (modifier = modifier) {
77+ if (! dismissable) {
78+ GlowEffect (
79+ size = size,
80+ color = gradientColor,
81+ modifier = Modifier .size(size.dp)
82+ )
83+ }
84+
85+ Box (
6886 modifier = Modifier
69- .fillMaxWidth()
70- .padding(horizontal = 16 .dp, vertical = 12 .dp),
71- verticalArrangement = Arrangement .spacedBy(8 .dp)
87+ .size(size.dp)
88+ .clip(RoundedCornerShape (16 .dp))
89+ .then(
90+ if (dismissable) {
91+ Modifier .gradientBackground(gradientColor)
92+ } else {
93+ Modifier
94+ .border(
95+ width = 1 .dp,
96+ color = getBorderColorForGradient(gradientColor),
97+ shape = RoundedCornerShape (16 .dp)
98+ )
99+ .gradientBackground(gradientColor)
100+ }
101+ )
102+ .clickableAlpha { onClick() }
72103 ) {
73- Row (
104+ // Shade effect for dismissable cards (similar to the Shade component in RN)
105+ if (dismissable) {
106+ Box (
107+ modifier = Modifier
108+ .fillMaxSize()
109+ .background(
110+ brush = Brush .verticalGradient(
111+ colors = listOf (
112+ Color .Transparent ,
113+ Color .Black .copy(alpha = 0.6f )
114+ ),
115+ startY = size * 0.4f ,
116+ endY = size.toFloat()
117+ )
118+ )
119+ )
120+ }
121+
122+ Column (
74123 modifier = Modifier
75124 .fillMaxWidth()
76- .weight(1f )
125+ .padding(horizontal = 16 .dp, vertical = 12 .dp),
126+ verticalArrangement = Arrangement .spacedBy(8 .dp)
77127 ) {
78- Image (
79- painter = painterResource(icon),
80- contentDescription = null ,
81- contentScale = ContentScale .FillHeight ,
82- modifier = Modifier .weight(1f )
128+ Row (
129+ modifier = Modifier
130+ .fillMaxWidth()
131+ .weight(1f )
132+ ) {
133+ Image (
134+ painter = painterResource(icon),
135+ contentDescription = null ,
136+ contentScale = ContentScale .FillHeight ,
137+ modifier = Modifier .weight(1f )
138+ )
139+
140+ if (duration == null && onClose != null && dismissable) {
141+ IconButton (
142+ onClick = onClose,
143+ modifier = Modifier
144+ .size(16 .dp)
145+ .testTag(" SuggestionDismiss" )
146+ ) {
147+ Icon (
148+ painter = painterResource(R .drawable.ic_x),
149+ contentDescription = null ,
150+ tint = Colors .White ,
151+ )
152+ }
153+ }
154+ }
155+
156+ Headline20 (
157+ text = AnnotatedString (title),
158+ color = Colors .White ,
159+ )
160+
161+ CaptionB (
162+ text = description,
163+ color = captionColor,
83164 )
165+ }
166+ }
167+ }
168+ }
84169
85- if (duration == null && onClose != null ) {
86- IconButton (
87- onClick = onClose,
88- modifier = Modifier
89- .size(16 .dp)
90- .testTag(" SuggestionDismiss" )
91- ) {
92- Icon (
93- painter = painterResource(R .drawable.ic_x),
94- contentDescription = null ,
95- tint = Colors .White ,
170+ @Composable
171+ private fun GlowEffect (
172+ size : Int ,
173+ color : Color ,
174+ modifier : Modifier = Modifier ,
175+ ) {
176+ val infiniteTransition = rememberInfiniteTransition(label = " glowTransition" )
177+ val glowAlpha by infiniteTransition.animateFloat(
178+ initialValue = 0f ,
179+ targetValue = 1f ,
180+ animationSpec = infiniteRepeatable(
181+ animation = tween(1100 , easing = LinearEasing ),
182+ repeatMode = RepeatMode .Reverse
183+ ),
184+ label = " glowAlpha"
185+ )
186+
187+ val (shadowColor, _, radialGradientColor) = getGlowColors(color)
188+
189+ Box (modifier = modifier) {
190+ // Outer glow with animated opacity
191+ Box (
192+ modifier = Modifier
193+ .fillMaxSize()
194+ .alpha(glowAlpha)
195+ .drawBehind {
196+ drawIntoCanvas { canvas ->
197+ val paint = android.graphics.Paint ().apply {
198+ this .color = shadowColor.toArgb()
199+ setShadowLayer(15f , 0f , 0f , shadowColor.toArgb())
200+ isAntiAlias = true
201+ }
202+
203+ val rect = android.graphics.RectF (
204+ 5f ,
205+ 5f ,
206+ size.toFloat() - 5f ,
207+ size.toFloat() - 5f
208+ )
209+
210+ canvas.nativeCanvas.drawRoundRect(
211+ rect,
212+ 16f ,
213+ 16f ,
214+ paint
96215 )
97216 }
98217 }
99- }
218+ )
100219
101- Headline20 (
102- text = AnnotatedString (title),
103- color = Colors .White ,
104- )
220+ // Static radial gradient overlay
221+ Box (
222+ modifier = Modifier
223+ .fillMaxSize()
224+ .alpha(0.4f )
225+ .background(
226+ brush = Brush .radialGradient(
227+ colors = listOf (radialGradientColor, color),
228+ center = androidx.compose.ui.geometry.Offset (
229+ size / 2f ,
230+ size / 2f
231+ ),
232+ radius = size / 2f
233+ ),
234+ shape = RoundedCornerShape (16 .dp)
235+ )
236+ )
237+ }
238+ }
105239
106- CaptionB (
107- text = description,
108- color = captionColor,
109- )
110- }
240+ private fun getGlowColors (color : Color ): Triple <Color , Color , Color > {
241+ return when (color) {
242+ Colors .Brand24 -> Triple (
243+ Color (200 , 48 , 0 ), // shadowColor
244+ Color (255 , 68 , 0 ), // borderColor
245+ Color (100 , 24 , 0 ) // radialGradientColor
246+ )
247+
248+ else -> Triple (
249+ Color (130 , 65 , 175 ), // shadowColor (default purple)
250+ Color (185 , 92 , 232 ), // borderColor
251+ Color (65 , 32 , 80 ) // radialGradientColor
252+ )
253+ }
254+ }
255+
256+ private fun getBorderColorForGradient (color : Color ): Color {
257+ return when (color) {
258+ Colors .Brand24 -> Color (255 , 68 , 0 )
259+ Colors .Purple24 -> Color (185 , 92 , 232 )
260+ Colors .Blue24 -> Color (92 , 185 , 232 )
261+ Colors .Green24 -> Color (92 , 232 , 185 )
262+ Colors .Yellow24 -> Color (232 , 185 , 92 )
263+ Colors .Red24 -> Color (232 , 92 , 92 )
264+ else -> Color (185 , 92 , 232 )
111265 }
112266}
113267
@@ -127,6 +281,7 @@ private fun Preview() {
127281 icon = item.icon,
128282 onClose = {},
129283 onClick = {},
284+ dismissable = item != Suggestion .LIGHTNING_READY ,
130285 duration = 5 .seconds.takeIf { item == Suggestion .LIGHTNING_READY }
131286 )
132287 }
0 commit comments