diff --git a/app/src/androidTest/kotlin/com/fnregistry/gallery_kt/MainActivityTest.kt b/app/src/androidTest/kotlin/com/fnregistry/gallery_kt/MainActivityTest.kt index 6f19dd6..09cd218 100644 --- a/app/src/androidTest/kotlin/com/fnregistry/gallery_kt/MainActivityTest.kt +++ b/app/src/androidTest/kotlin/com/fnregistry/gallery_kt/MainActivityTest.kt @@ -1,7 +1,9 @@ package com.fnregistry.gallery_kt +import androidx.compose.ui.test.assertCountEquals import androidx.compose.ui.test.assertIsDisplayed import androidx.compose.ui.test.junit4.createAndroidComposeRule +import androidx.compose.ui.test.onAllNodesWithText import androidx.compose.ui.test.onNodeWithText import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Rule @@ -15,9 +17,35 @@ class MainActivityTest { val composeTestRule = createAndroidComposeRule() @Test - fun appLaunchesAndShowsReadyText() { - composeTestRule - .onNodeWithText("gallery_kt ready") - .assertIsDisplayed() + fun appLaunchesAndShowsTitle() { + composeTestRule.onNodeWithText("@fn_compose Gallery").assertIsDisplayed() + } + + @Test + fun typographySectionRenders() { + composeTestRule.onNodeWithText("Typography").assertIsDisplayed() + composeTestRule.onNodeWithText("Title order 1").assertIsDisplayed() + } + + @Test + fun buttonsSectionShowsAllVariants() { + composeTestRule.onNodeWithText("Filled").assertIsDisplayed() + composeTestRule.onNodeWithText("Outlined").assertIsDisplayed() + composeTestRule.onNodeWithText("Secondary").assertIsDisplayed() + composeTestRule.onNodeWithText("Ghost").assertIsDisplayed() + composeTestRule.onNodeWithText("Destructive").assertIsDisplayed() + composeTestRule.onNodeWithText("Link").assertIsDisplayed() + // "Disabled" appears twice (button + input label) — assert >=1 displayed. + composeTestRule.onAllNodesWithText("Disabled")[0].assertIsDisplayed() + } + + @Test + fun alertsAndBadgesPresent() { + // Badges live below the fold; assertExists checks tree, not viewport. + composeTestRule.onNodeWithText("BRAND").assertExists() + composeTestRule.onNodeWithText("GREEN").assertExists() + composeTestRule.onNodeWithText("RED").assertExists() + composeTestRule.onNodeWithText("Info").assertExists() + composeTestRule.onNodeWithText("Success").assertExists() } } diff --git a/app/src/main/kotlin/com/fnregistry/gallery_kt/MainActivity.kt b/app/src/main/kotlin/com/fnregistry/gallery_kt/MainActivity.kt index 4c4a187..eb0431e 100644 --- a/app/src/main/kotlin/com/fnregistry/gallery_kt/MainActivity.kt +++ b/app/src/main/kotlin/com/fnregistry/gallery_kt/MainActivity.kt @@ -3,22 +3,357 @@ package com.fnregistry.gallery_kt import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Surface -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.Modifier +import androidx.compose.ui.unit.dp +import fn.compose.theme.FnSpacing import fn.compose.theme.FnTheme +import fn.compose.ui.FnAlert +import fn.compose.ui.FnAlertVariant +import fn.compose.ui.FnAppShell +import fn.compose.ui.FnAvatar +import fn.compose.ui.FnAvatarSize +import fn.compose.ui.FnBadge +import fn.compose.ui.FnBadgeColor +import fn.compose.ui.FnBarChart +import fn.compose.ui.FnBarItem +import fn.compose.ui.FnButton +import fn.compose.ui.FnButtonVariant +import fn.compose.ui.FnCard +import fn.compose.ui.FnCardVariant +import fn.compose.ui.FnCheckbox +import fn.compose.ui.FnDataTable +import fn.compose.ui.FnDialog +import fn.compose.ui.FnEmptyState +import fn.compose.ui.FnGroup +import fn.compose.ui.FnKpiCard +import fn.compose.ui.FnLineChart +import fn.compose.ui.FnLoader +import fn.compose.ui.FnPageHeader +import fn.compose.ui.FnPaper +import fn.compose.ui.FnSelect +import fn.compose.ui.FnSkeleton +import fn.compose.ui.FnSparkline +import fn.compose.ui.FnStack +import fn.compose.ui.FnSwitch +import fn.compose.ui.FnTableColumn +import fn.compose.ui.FnTabs +import fn.compose.ui.FnText +import fn.compose.ui.FnTextInput +import fn.compose.ui.FnTextSize +import fn.compose.ui.FnTitle class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { FnTheme { - Surface(modifier = Modifier.fillMaxSize()) { - Text("gallery_kt ready") + Surface(Modifier.fillMaxSize()) { GalleryScreen() } + } + } + } +} + +private val CATEGORIES = listOf("Typography", "Inputs", "Display", "Data", "Charts", "Feedback", "Layout") + +@Composable +fun GalleryScreen() { + var tab by remember { mutableStateOf(0) } + FnAppShell(title = "@fn_compose Gallery") { padding -> + FnStack( + modifier = Modifier.padding(padding).fillMaxSize(), + gap = FnSpacing.xs, + ) { + FnTabs(tabs = CATEGORIES, selectedIndex = tab, onTabSelected = { tab = it }, scrollable = true) + FnStack( + modifier = Modifier + .padding(horizontal = FnSpacing.md, vertical = FnSpacing.sm) + .fillMaxSize() + .verticalScroll(rememberScrollState()), + gap = FnSpacing.lg, + ) { + when (tab) { + 0 -> TypographyCategory() + 1 -> InputsCategory() + 2 -> DisplayCategory() + 3 -> DataCategory() + 4 -> ChartsCategory() + 5 -> FeedbackCategory() + 6 -> LayoutCategory() } } } } } + +@Composable +private fun TypographyCategory() { + FnPageHeader("Typography", subtitle = "Titles + body scale, weights via FnTypography tokens.") + FnCard(modifier = Modifier.fillMaxWidth()) { + FnStack(gap = FnSpacing.xs) { + FnTitle("Title order 1", order = 1) + FnTitle("Title order 2", order = 2) + FnTitle("Title order 3", order = 3) + FnTitle("Title order 4", order = 4) + FnTitle("Title order 5", order = 5) + FnTitle("Title order 6", order = 6) + } + } + FnCard(modifier = Modifier.fillMaxWidth()) { + FnStack(gap = FnSpacing.xs) { + FnText("Body Xl size", size = FnTextSize.Xl) + FnText("Body Lg size", size = FnTextSize.Lg) + FnText("Body Md (default)", size = FnTextSize.Md) + FnText("Body Sm size", size = FnTextSize.Sm) + FnText("Body Xs size", size = FnTextSize.Xs) + } + } +} + +@Composable +private fun InputsCategory() { + FnPageHeader("Inputs", subtitle = "Buttons + text + select + toggles.") + + var input by remember { mutableStateOf("") } + var selected by remember { mutableStateOf("Opcion 1") } + var switchOn by remember { mutableStateOf(true) } + var checked by remember { mutableStateOf(false) } + var dialogOpen by remember { mutableStateOf(false) } + + FnTitle("Buttons", order = 4) + FnGroup { FnButton("Filled", onClick = {}); FnButton("Outlined", onClick = {}, variant = FnButtonVariant.Outlined); FnButton("Secondary", onClick = {}, variant = FnButtonVariant.Secondary) } + FnGroup { FnButton("Ghost", onClick = {}, variant = FnButtonVariant.Ghost); FnButton("Destructive", onClick = {}, variant = FnButtonVariant.Destructive); FnButton("Link", onClick = {}, variant = FnButtonVariant.Link) } + FnButton("Open dialog", onClick = { dialogOpen = true }, variant = FnButtonVariant.Outlined) + + FnTitle("Text input", order = 4) + FnTextInput(value = input, onValueChange = { input = it }, label = "Tu nombre", placeholder = "Ej. Lucas", modifier = Modifier.fillMaxWidth()) + FnTextInput(value = "valor con error", onValueChange = {}, label = "Email", error = "Email invalido", modifier = Modifier.fillMaxWidth()) + + FnTitle("Select", order = 4) + FnSelect(options = listOf("Opcion 1", "Opcion 2", "Opcion 3"), selected = selected, onSelected = { selected = it }, label = "Tipo", modifier = Modifier.fillMaxWidth()) + + FnTitle("Toggles", order = 4) + FnSwitch(checked = switchOn, onCheckedChange = { switchOn = it }, label = "Switch activado") + FnCheckbox(checked = checked, onCheckedChange = { checked = it }, label = "Acepto los terminos") + + FnDialog( + open = dialogOpen, + onDismiss = { dialogOpen = false }, + title = "Confirmar accion", + description = "Esto es un dialog modal de ejemplo. Confirmar?", + onConfirm = { dialogOpen = false }, + ) +} + +@Composable +private fun DisplayCategory() { + FnPageHeader("Display", subtitle = "Avatars, badges, cards, paper.") + + FnTitle("Avatars", order = 4) + FnGroup { + FnAvatar("LM", size = FnAvatarSize.Sm) + FnAvatar("AM", size = FnAvatarSize.Md) + FnAvatar("EG", size = FnAvatarSize.Lg) + } + + FnTitle("Badges", order = 4) + FnGroup { + FnBadge("BRAND") + FnBadge("GRAY", color = FnBadgeColor.Gray) + FnBadge("GREEN", color = FnBadgeColor.Green) + FnBadge("RED", color = FnBadgeColor.Red) + FnBadge("YELLOW", color = FnBadgeColor.Yellow) + FnBadge("BLUE", color = FnBadgeColor.Blue) + } + + FnTitle("Cards", order = 4) + FnCard(modifier = Modifier.fillMaxWidth(), variant = FnCardVariant.Default) { + FnText("Default — border + shadow", size = FnTextSize.Sm) + } + FnCard(modifier = Modifier.fillMaxWidth(), variant = FnCardVariant.Borderless) { + FnText("Borderless — solo bg", size = FnTextSize.Sm) + } + FnCard(modifier = Modifier.fillMaxWidth(), variant = FnCardVariant.Ghost) { + FnText("Ghost — transparente", size = FnTextSize.Sm) + } +} + +@Composable +private fun DataCategory() { + FnPageHeader("Data", subtitle = "Tables + KPI cards + page header.") + + FnTitle("KPI cards", order = 4) + FnGroup(gap = FnSpacing.sm) { + FnKpiCard( + label = "Revenue", + value = "€42.3k", + delta = "+12.4%", + deltaPositive = true, + sparklineData = listOf(10f, 14f, 12f, 18f, 22f, 28f, 32f), + modifier = Modifier.weight(1f), + ) + FnKpiCard( + label = "Churn", + value = "3.1%", + delta = "-0.5pp", + deltaPositive = true, + sparklineData = listOf(5f, 4.8f, 4.2f, 4f, 3.5f, 3.2f, 3.1f), + modifier = Modifier.weight(1f), + ) + } + + FnTitle("Data table", order = 4) + val rows = listOf( + Triple("Lucas", "Madrid", "active"), + Triple("Ana", "Barcelona", "active"), + Triple("Marta", "Valencia", "paused"), + Triple("Carlos", "Sevilla", "active"), + Triple("Sara", "Bilbao", "paused"), + ) + FnDataTable( + rows = rows, + modifier = Modifier.fillMaxWidth(), + columns = listOf( + FnTableColumn>( + header = "Nombre", weight = 1.2f, + cell = { FnText(it.first, size = FnTextSize.Sm) }, + ), + FnTableColumn( + header = "Ciudad", weight = 1.5f, + cell = { FnText(it.second, size = FnTextSize.Sm) }, + ), + FnTableColumn( + header = "Estado", weight = 1f, + cell = { + val (txt, color) = if (it.third == "active") "ACTIVE" to FnBadgeColor.Green + else "PAUSED" to FnBadgeColor.Yellow + FnBadge(txt, color = color) + }, + ), + ), + ) +} + +@Composable +private fun ChartsCategory() { + FnPageHeader("Charts", subtitle = "Canvas-based, sin deps externas.") + + FnTitle("Line chart", order = 4) + FnCard(modifier = Modifier.fillMaxWidth()) { + FnLineChart( + data = listOf(10f, 14f, 12f, 18f, 22f, 28f, 32f, 30f, 36f, 42f, 40f, 48f), + modifier = Modifier.fillMaxWidth(), + ) + } + + FnTitle("Bar chart", order = 4) + FnCard(modifier = Modifier.fillMaxWidth()) { + FnBarChart( + data = listOf( + FnBarItem("Lun", 12f), + FnBarItem("Mar", 19f), + FnBarItem("Mie", 8f), + FnBarItem("Jue", 22f), + FnBarItem("Vie", 28f), + FnBarItem("Sab", 18f), + FnBarItem("Dom", 10f), + ), + modifier = Modifier.fillMaxWidth(), + ) + } + + FnTitle("Sparklines (inline)", order = 4) + FnPaper(modifier = Modifier.fillMaxWidth()) { + FnStack(gap = FnSpacing.sm) { + FnGroup { FnText("CPU", size = FnTextSize.Sm); FnSparkline(listOf(20f, 35f, 28f, 42f, 38f, 55f, 60f)) } + FnGroup { FnText("RAM", size = FnTextSize.Sm); FnSparkline(listOf(50f, 52f, 48f, 51f, 49f, 50f, 53f)) } + FnGroup { FnText("Net", size = FnTextSize.Sm); FnSparkline(listOf(5f, 12f, 8f, 25f, 18f, 30f, 22f)) } + } + } +} + +@Composable +private fun FeedbackCategory() { + FnPageHeader("Feedback", subtitle = "Alerts + loaders + skeletons + empty state.") + + FnTitle("Alerts", order = 4) + FnAlert("Mensaje informativo neutro", title = "Info", variant = FnAlertVariant.Info) + FnAlert("Accion completada con exito", title = "Success", variant = FnAlertVariant.Success) + FnAlert("Algo requiere atencion", title = "Warning", variant = FnAlertVariant.Warning) + FnAlert("Operacion fallida", title = "Error", variant = FnAlertVariant.Error) + + FnTitle("Loaders", order = 4) + FnGroup { + FnLoader(size = fn.compose.ui.FnLoaderSize.Sm) + FnLoader(size = fn.compose.ui.FnLoaderSize.Md) + FnLoader(size = fn.compose.ui.FnLoaderSize.Lg) + } + + FnTitle("Skeleton placeholders", order = 4) + FnCard(modifier = Modifier.fillMaxWidth()) { + FnStack(gap = FnSpacing.sm) { + FnSkeleton(height = 20.dp) + FnSkeleton(height = 16.dp, modifier = Modifier.fillMaxWidth(0.7f)) + FnSkeleton(height = 16.dp) + } + } + + FnTitle("Empty state", order = 4) + FnCard(modifier = Modifier.fillMaxWidth(), variant = FnCardVariant.Borderless) { + FnEmptyState( + title = "Sin resultados", + description = "Prueba a cambiar los filtros.", + icon = "🔍", + action = { FnButton("Limpiar filtros", onClick = {}, variant = FnButtonVariant.Outlined) }, + ) + } +} + +@Composable +private fun LayoutCategory() { + FnPageHeader("Layout primitives", subtitle = "Stack, Group, Paper, Card.") + + FnTitle("FnStack (column)", order = 4) + FnCard(modifier = Modifier.fillMaxWidth()) { + FnStack(gap = FnSpacing.xs) { + FnText("Item 1", size = FnTextSize.Sm) + FnText("Item 2", size = FnTextSize.Sm) + FnText("Item 3", size = FnTextSize.Sm) + } + } + + FnTitle("FnGroup (row)", order = 4) + FnCard(modifier = Modifier.fillMaxWidth()) { + FnGroup(gap = FnSpacing.sm) { + FnBadge("A") + FnBadge("B", color = FnBadgeColor.Gray) + FnBadge("C", color = FnBadgeColor.Green) + } + } + + FnTitle("FnPaper", order = 4) + FnPaper(modifier = Modifier.fillMaxWidth()) { + FnText("Container minimal: radius + shadow leves.", size = FnTextSize.Sm) + } + + FnTitle("FnPageHeader", order = 4) + FnPaper(modifier = Modifier.fillMaxWidth()) { + FnPageHeader( + title = "Titulo de seccion", + subtitle = "Subtitulo descriptivo", + actions = { FnButton("Action", onClick = {}, variant = FnButtonVariant.Outlined) }, + ) + } +}