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
|
package com.fnregistry.gallery_kt
|
||||||
|
|
||||||
|
import androidx.compose.ui.test.assertCountEquals
|
||||||
import androidx.compose.ui.test.assertIsDisplayed
|
import androidx.compose.ui.test.assertIsDisplayed
|
||||||
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
import androidx.compose.ui.test.junit4.createAndroidComposeRule
|
||||||
|
import androidx.compose.ui.test.onAllNodesWithText
|
||||||
import androidx.compose.ui.test.onNodeWithText
|
import androidx.compose.ui.test.onNodeWithText
|
||||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||||
import org.junit.Rule
|
import org.junit.Rule
|
||||||
@@ -15,9 +17,35 @@ class MainActivityTest {
|
|||||||
val composeTestRule = createAndroidComposeRule<MainActivity>()
|
val composeTestRule = createAndroidComposeRule<MainActivity>()
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
fun appLaunchesAndShowsReadyText() {
|
fun appLaunchesAndShowsTitle() {
|
||||||
composeTestRule
|
composeTestRule.onNodeWithText("@fn_compose Gallery").assertIsDisplayed()
|
||||||
.onNodeWithText("gallery_kt ready")
|
}
|
||||||
.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 android.os.Bundle
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
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.Surface
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
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.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import fn.compose.theme.FnSpacing
|
||||||
import fn.compose.theme.FnTheme
|
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() {
|
class MainActivity : ComponentActivity() {
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
setContent {
|
setContent {
|
||||||
FnTheme {
|
FnTheme {
|
||||||
Surface(modifier = Modifier.fillMaxSize()) {
|
Surface(Modifier.fillMaxSize()) { GalleryScreen() }
|
||||||
Text("gallery_kt ready")
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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