feat(kotlin-compose): design system + 33 components + gallery_kt + e2e android emulator + scaffolder fixes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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<MainActivity>()
|
||||
|
||||
@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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String?>("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<Triple<String, String, String>>(
|
||||
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) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user