Repo created

This commit is contained in:
Fr4nz D13trich 2025-11-24 18:55:42 +01:00
parent a629de6271
commit 3cef7c5092
2161 changed files with 246605 additions and 2 deletions

View file

@ -0,0 +1,38 @@
## Core - UI - Compose - Design system
Uses [`:core:ui:compose:theme`](../theme/README.md)
## Background
[Jetpack Compose](https://developer.android.com/jetpack/compose) is a declarative UI toolkit for Android that provides a modern and efficient way to build UIs for Android apps. In this context, design systems and atomic design can help designers and developers create more scalable, maintainable, and reusable UIs.
### Design system
A design system is a collection of guidelines, principles, and tools that help teams create consistent and cohesive visual designs and user experiences.
It typically includes a set of reusable components, such as icons, typography, color palettes, and layouts, that can be combined and customized to create new designs.
The design system also provides documentation and resources for designers and developers to ensure that the designs are implemented consistently and efficiently across all platforms and devices.
The goal of a design system is to streamline the design process, improve design quality, and maintain brand consistency.
An example is Google's [Material Design](https://m3.material.io/) that is used to develop cohesive apps.
### Atomic Design
![Atomic design](assets/images/atomic_design.svg)
Atomic design is a methodology for creating user interfaces (UI) in a design system by breaking them down into smaller, reusable components.
These components are classified into five categories based on their level of abstraction: **atoms**, **molecules**, **organisms**, **templates**, and **pages**.
- **Atoms** are the smallest building blocks, such as buttons, labels, and input fields and could be combined to create more complex components.
- **Molecules** are groups of atoms that work together, like search bars, forms or menus
- **Organisms** are more complex components that combine molecules and atoms, such as headers or cards.
- **Templates** are pages with placeholders for components
- **Pages** are the final UI
By using atomic design, designers and developers can create more consistent and reusable UIs.
This can save time and improve the overall quality, as well as facilitate collaboration between team members.
## Acknowledgement
- [Atomic Design Methodology | Atomic Design by Brad Frost](https://atomicdesign.bradfrost.com/chapter-2/)
- [Atomic Design: Getting Started | Blog | We Are Mobile First](https://www.wearemobilefirst.com/blog/atomic-design)

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 17 KiB

View file

@ -0,0 +1,16 @@
plugins {
id(ThunderbirdPlugins.Library.androidCompose)
}
android {
namespace = "app.k9mail.core.ui.compose.designsystem"
resourcePrefix = "designsystem_"
}
dependencies {
api(projects.core.ui.compose.theme)
implementation(libs.androidx.compose.material)
implementation(libs.androidx.compose.material.icons.extended)
testImplementation(projects.core.ui.compose.testing)
}

View file

@ -0,0 +1,32 @@
package app.k9mail.core.ui.compose.designsystem.atom
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Surface as MaterialSurface
@Composable
fun Background(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
MaterialSurface(
modifier = modifier,
content = content,
color = MainTheme.colors.background,
)
}
@Preview(showBackground = true)
@Composable
internal fun BackgroundPreview() {
PreviewWithThemes {
Background(
modifier = Modifier.fillMaxSize(),
content = {},
)
}
}

View file

@ -0,0 +1,45 @@
package app.k9mail.core.ui.compose.designsystem.atom
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Checkbox as MaterialCheckbox
@Composable
fun Checkbox(
checked: Boolean,
onCheckedChange: (Boolean) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
) {
MaterialCheckbox(
checked = checked,
onCheckedChange = onCheckedChange,
modifier = modifier,
enabled = enabled,
)
}
@Preview(showBackground = true)
@Composable
internal fun CheckboxPreview() {
PreviewWithThemes {
Checkbox(
checked = true,
onCheckedChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun CheckboxDisabledPreview() {
PreviewWithThemes {
Checkbox(
checked = true,
onCheckedChange = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,37 @@
package app.k9mail.core.ui.compose.designsystem.atom
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Dp
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Surface as MaterialSurface
@Composable
fun Surface(
modifier: Modifier = Modifier,
color: Color = MainTheme.colors.surface,
elevation: Dp = MainTheme.elevations.default,
content: @Composable () -> Unit,
) {
MaterialSurface(
modifier = modifier,
content = content,
elevation = elevation,
color = color,
)
}
@Preview(showBackground = true)
@Composable
internal fun SurfacePreview() {
PreviewWithThemes {
Surface(
modifier = Modifier.fillMaxSize(),
content = {},
)
}
}

View file

@ -0,0 +1,52 @@
package app.k9mail.core.ui.compose.designsystem.atom.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Button as MaterialButton
@Composable
fun Button(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
) {
MaterialButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.buttonColors(),
contentPadding = contentPadding,
) {
TextButton(text = text)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonPreview() {
PreviewWithThemes {
Button(
text = "Button",
onClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonDisabledPreview() {
PreviewWithThemes {
Button(
text = "ButtonDisabled",
onClick = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,65 @@
package app.k9mail.core.ui.compose.designsystem.atom.button
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedButton as MaterialOutlinedButton
@Composable
fun ButtonOutlined(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
) {
MaterialOutlinedButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.outlinedButtonColors(),
border = BorderStroke(
width = 1.dp,
color = if (enabled) {
MainTheme.colors.primary
} else {
MainTheme.colors.onSurface.copy(
alpha = 0.12f,
)
},
),
contentPadding = contentPadding,
) {
TextButton(text = text)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonOutlinedPreview() {
PreviewWithThemes {
ButtonOutlined(
text = "ButtonOutlined",
onClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonOutlinedDisabledPreview() {
PreviewWithThemes {
ButtonOutlined(
text = "ButtonOutlinedDisabled",
onClick = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,52 @@
package app.k9mail.core.ui.compose.designsystem.atom.button
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.material.ButtonDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.atom.text.TextButton
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.TextButton as MaterialTextButton
@Composable
fun ButtonText(
text: String,
onClick: () -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
contentPadding: PaddingValues = ButtonDefaults.ContentPadding,
) {
MaterialTextButton(
onClick = onClick,
modifier = modifier,
enabled = enabled,
colors = ButtonDefaults.textButtonColors(),
contentPadding = contentPadding,
) {
TextButton(text = text)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonTextPreview() {
PreviewWithThemes {
ButtonText(
text = "ButtonText",
onClick = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun ButtonTextDisabledPreview() {
PreviewWithThemes {
ButtonText(
text = "ButtonTextDisabled",
onClick = {},
enabled = false,
)
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextBody1(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.body1,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextBody1Preview() {
PreviewWithThemes {
TextBody1(text = "TextBody1")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextBody2(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.body2,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextBody2Preview() {
PreviewWithThemes {
TextBody2(text = "TextBody2")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextButton(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text.uppercase(),
style = MainTheme.typography.button,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextButtonPreview() {
PreviewWithThemes {
TextButton(text = "TextButton")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextCaption(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.caption,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextCaptionPreview() {
PreviewWithThemes {
TextCaption(text = "TextCaption")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline1(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h1,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline1Preview() {
PreviewWithThemes {
TextHeadline1(text = "TextHeadline1")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline2(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h2,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline2Preview() {
PreviewWithThemes {
TextHeadline2(text = "TextHeadline2")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline3(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h3,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline3Preview() {
PreviewWithThemes {
TextHeadline3(text = "TextHeadline3")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline4(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h4,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline4Preview() {
PreviewWithThemes {
TextHeadline4(text = "TextHeadline4")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline5(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h5,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline5Preview() {
PreviewWithThemes {
TextHeadline5(text = "TextHeadline5")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextHeadline6(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.h6,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextHeadline6Preview() {
PreviewWithThemes {
TextHeadline6(text = "TextHeadline6")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextOverline(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text.uppercase(),
style = MainTheme.typography.overline,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextOverlinePreview() {
PreviewWithThemes {
TextOverline(text = "TextOverline")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextSubtitle1(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.subtitle1,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextSubtitle1Preview() {
PreviewWithThemes {
TextSubtitle1(text = "TextSubtitle1")
}
}

View file

@ -0,0 +1,28 @@
package app.k9mail.core.ui.compose.designsystem.atom.text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.MainTheme
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.Text as MaterialText
@Composable
fun TextSubtitle2(
text: String,
modifier: Modifier = Modifier,
) {
MaterialText(
text = text,
style = MainTheme.typography.subtitle2,
modifier = modifier,
)
}
@Preview(showBackground = true)
@Composable
internal fun TextSubtitle2Preview() {
PreviewWithThemes {
TextSubtitle2(text = "TextSubtitle2")
}
}

View file

@ -0,0 +1,154 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Text
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Visibility
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.designsystem.R
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
@Composable
fun PasswordTextFieldOutlined(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: String? = null,
isError: Boolean = false,
) {
var passwordVisibilityState by rememberSaveable { mutableStateOf(false) }
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = selectLabel(label),
trailingIcon = selectTrailingIcon(
isEnabled = enabled,
isPasswordVisible = passwordVisibilityState,
onClick = { passwordVisibilityState = !passwordVisibilityState },
),
isError = isError,
visualTransformation = selectVisualTransformation(
isEnabled = enabled,
isPasswordVisible = passwordVisibilityState,
),
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Password),
singleLine = true,
)
}
private fun selectLabel(label: String?): @Composable (() -> Unit)? {
return if (label != null) {
{
Text(text = label)
}
} else {
null
}
}
private fun selectTrailingIcon(
isEnabled: Boolean,
isPasswordVisible: Boolean,
onClick: () -> Unit,
hasTrailingIcon: Boolean = true,
): @Composable (() -> Unit)? {
return if (hasTrailingIcon) {
{
val image = if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
Icons.Filled.Visibility
} else {
Icons.Filled.VisibilityOff
}
val description = if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
stringResource(id = R.string.designsystem_atom_password_textfield_hide_password)
} else {
stringResource(id = R.string.designsystem_atom_password_textfield_show_password)
}
IconButton(onClick = onClick) {
Icon(imageVector = image, contentDescription = description)
}
}
} else {
null
}
}
private fun selectVisualTransformation(
isEnabled: Boolean,
isPasswordVisible: Boolean,
): VisualTransformation {
return if (isShowPasswordAllowed(isEnabled, isPasswordVisible)) {
VisualTransformation.None
} else {
PasswordVisualTransformation()
}
}
private fun isShowPasswordAllowed(isEnabled: Boolean, isPasswordVisible: Boolean) = isEnabled && isPasswordVisible
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedWithLabelPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
label = "Label",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedDisabledPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
enabled = false,
)
}
}
@Preview(showBackground = true)
@Composable
internal fun PasswordTextFieldOutlinedErrorPreview() {
PreviewWithThemes {
PasswordTextFieldOutlined(
value = "Input text",
onValueChange = {},
isError = true,
)
}
}

View file

@ -0,0 +1,84 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import app.k9mail.core.ui.compose.theme.PreviewWithThemes
import androidx.compose.material.OutlinedTextField as MaterialOutlinedTextField
@Composable
fun TextFieldOutlined(
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier = Modifier,
enabled: Boolean = true,
label: String? = null,
isError: Boolean = false,
) {
MaterialOutlinedTextField(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = selectLabel(label),
isError = isError,
)
}
private fun selectLabel(label: String?): @Composable (() -> Unit)? {
return if (label != null) {
{
Text(text = label)
}
} else {
null
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedWithLabelPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
label = "Label",
onValueChange = {},
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedDisabledPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
enabled = false,
)
}
}
@Preview(showBackground = true)
@Composable
internal fun TextFieldOutlinedErrorPreview() {
PreviewWithThemes {
TextFieldOutlined(
value = "Input text",
onValueChange = {},
isError = true,
)
}
}

View file

@ -0,0 +1,86 @@
package app.k9mail.core.ui.compose.designsystem.template
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.Density
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme.K9Theme
/**
* The [LazyColumnWithFooter] composable creates a [LazyColumn] with a footer.
*
* @param modifier The modifier to be applied to the layout.
* @param verticalArrangement The vertical arrangement of the children.
* @param horizontalAlignment The horizontal alignment of the children.
* @param footer The footer to be displayed at the bottom of the [LazyColumn].
* @param content The content of the [LazyColumn].
*/
@Composable
fun LazyColumnWithFooter(
modifier: Modifier = Modifier,
verticalArrangement: Arrangement.Vertical = Arrangement.Top,
horizontalAlignment: Alignment.Horizontal = Alignment.Start,
footer: @Composable () -> Unit = {},
content: LazyListScope.() -> Unit,
) {
LazyColumn(
modifier = modifier,
verticalArrangement = verticalArrangementWithFooter(verticalArrangement),
horizontalAlignment = horizontalAlignment,
) {
content()
item { footer() }
}
}
@Composable
private fun verticalArrangementWithFooter(verticalArrangement: Arrangement.Vertical) = remember {
object : Arrangement.Vertical {
override fun Density.arrange(
totalSize: Int,
sizes: IntArray,
outPositions: IntArray,
) {
val innerSizes = sizes.dropLast(1).toIntArray()
val footerSize = sizes.last()
val innerTotalSize = totalSize - footerSize
with(verticalArrangement) {
arrange(
totalSize = innerTotalSize,
sizes = innerSizes,
outPositions = outPositions,
)
}
outPositions[outPositions.lastIndex] = totalSize - footerSize
}
}
}
@Composable
@Preview
internal fun LazyColumnWithFooterPreview() {
K9Theme {
Surface {
LazyColumnWithFooter(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.spacedBy(32.dp, Alignment.CenterVertically),
footer = { Text(text = "Footer") },
) {
items(10) {
Text(text = "Item $it")
}
}
}
}
}

View file

@ -0,0 +1,129 @@
package app.k9mail.core.ui.compose.designsystem.template
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.requiredHeight
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import app.k9mail.core.ui.compose.common.DevicePreviews
import app.k9mail.core.ui.compose.common.window.WindowSizeClass
import app.k9mail.core.ui.compose.common.window.getWindowSizeInfo
import app.k9mail.core.ui.compose.designsystem.atom.Surface
import app.k9mail.core.ui.compose.theme.K9Theme
import app.k9mail.core.ui.compose.theme.MainTheme
/**
* The [ResponsiveContent] composable automatically adapts its child content to different screen sizes and resolutions,
* providing a responsive layout for a better user experience.
*
* It uses the [WindowSizeClass] (Compact, Medium, or Expanded) to make appropriate layout adjustments.
*
* @param modifier The modifier to be applied to the layout.
* @param content The content to be displayed.
*/
@Composable
fun ResponsiveContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
val windowSizeClass = getWindowSizeInfo()
when (windowSizeClass.screenWidthSizeClass) {
WindowSizeClass.Compact -> CompactContent(modifier = modifier, content = content)
WindowSizeClass.Medium -> MediumContent(modifier = modifier, content = content)
WindowSizeClass.Expanded -> ExpandedContent(modifier = modifier, content = content)
}
}
@Composable
private fun CompactContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
) {
content()
}
}
@Composable
private fun MediumContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.TopCenter,
) {
Box(
modifier = Modifier.requiredWidth(WindowSizeClass.COMPACT_MAX_WIDTH.dp),
) {
content()
}
}
}
@Composable
private fun ExpandedContent(
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
when (getWindowSizeInfo().screenHeightSizeClass) {
WindowSizeClass.Compact -> MediumContent(modifier, content)
WindowSizeClass.Medium -> {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.TopCenter,
) {
Surface(
modifier = Modifier.requiredWidth(WindowSizeClass.MEDIUM_MAX_WIDTH.dp),
elevation = MainTheme.elevations.raised,
) {
content()
}
}
}
WindowSizeClass.Expanded -> {
Box(
modifier = Modifier
.fillMaxSize()
.then(modifier),
contentAlignment = Alignment.Center,
) {
Surface(
modifier = Modifier
.requiredWidth(WindowSizeClass.MEDIUM_MAX_WIDTH.dp)
.requiredHeight(WindowSizeClass.MEDIUM_MAX_HEIGHT.dp),
elevation = MainTheme.elevations.raised,
) {
content()
}
}
}
}
}
@Composable
@DevicePreviews
internal fun ResponsiveContentPreview() {
K9Theme {
Surface {
ResponsiveContent {
Surface(
color = MainTheme.colors.info,
modifier = Modifier.fillMaxSize(),
) {}
}
}
}
}

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="designsystem_atom_password_textfield_hide_password">Hide password</string>
<string name="designsystem_atom_password_textfield_show_password">Show password</string>
</resources>

View file

@ -0,0 +1,98 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.onNodeWithContentDescription
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import app.k9mail.core.ui.compose.designsystem.R
import app.k9mail.core.ui.compose.testing.ComposeTest
import org.junit.Test
private const val PASSWORD = "Password input"
class PasswordTextFieldOutlinedKtTest : ComposeTest() {
@Test
fun `should not display password by default`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onNodeWithText(PASSWORD).assertDoesNotExist()
}
@Test
fun `should display password when show password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onNodeWithText(PASSWORD).assertIsDisplayed()
}
@Test
fun `should not display password when hide password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().performClick()
onNodeWithText(PASSWORD).assertDoesNotExist()
}
@Test
fun `should display hide password icon when show password is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().assertIsDisplayed()
}
@Test
fun `should display show password icon when hide password icon is clicked`() = runComposeTest {
setContent {
PasswordTextFieldOutlined(
value = PASSWORD,
onValueChange = {},
)
}
onShowPasswordNode().performClick()
onHidePasswordNode().performClick()
onShowPasswordNode().assertIsDisplayed()
}
private fun SemanticsNodeInteractionsProvider.onShowPasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_show_password),
)
}
private fun SemanticsNodeInteractionsProvider.onHidePasswordNode(): SemanticsNodeInteraction {
return onNodeWithContentDescription(
getString(R.string.designsystem_atom_password_textfield_hide_password),
)
}
}

View file

@ -0,0 +1,154 @@
package app.k9mail.core.ui.compose.designsystem.atom.textfield
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.assertIsEnabled
import androidx.compose.ui.test.assertIsNotEnabled
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import androidx.compose.ui.test.performTextInput
import app.k9mail.core.ui.compose.testing.ComposeTest
import assertk.assertThat
import assertk.assertions.isEqualTo
import org.junit.Test
import org.junit.runner.RunWith
import org.robolectric.ParameterizedRobolectricTestRunner
private const val VALUE = "Input text"
private const val LABEL = "Label"
data class TextFieldTestData(
val name: String,
val content: @Composable (
value: String,
onValueChange: (String) -> Unit,
modifier: Modifier,
enabled: Boolean?,
label: String?,
) -> Unit,
)
@RunWith(ParameterizedRobolectricTestRunner::class)
class TextFieldKtTest(
data: TextFieldTestData,
) : ComposeTest() {
private val testSubjectName = data.name
private val testSubject = data.content
@Test
fun `should call onValueChange when value changes`() = runComposeTest {
var value = VALUE
setContent {
testSubject(
value = value,
onValueChange = { value = it },
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = null,
)
}
onNodeWithTag(testSubjectName).performClick()
onNodeWithTag(testSubjectName).performTextInput(" + added text")
assertThat(value).isEqualTo("$VALUE + added text")
}
@Test
fun `should be enabled by default`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = null,
)
}
onNodeWithTag(testSubjectName).assertIsEnabled()
}
@Test
fun `should be disabled when enabled is false`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = false,
label = null,
)
}
onNodeWithTag(testSubjectName).assertIsNotEnabled()
}
@Test
fun `should show label when label is not null`() = runComposeTest {
setContent {
testSubject(
value = VALUE,
onValueChange = {},
modifier = Modifier.testTag(testSubjectName),
enabled = null,
label = LABEL,
)
}
onNodeWithText(LABEL).assertIsDisplayed()
}
companion object {
@JvmStatic
@ParameterizedRobolectricTestRunner.Parameters(name = "{0}")
fun data(): List<TextFieldTestData> = listOf(
TextFieldTestData(
name = "TextFieldOutlined",
content = { value, onValueChange, modifier, enabled, label ->
if (enabled != null) {
TextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = label,
)
} else {
TextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label,
)
}
},
),
TextFieldTestData(
name = "PasswordTextFieldOutlined",
content = { value, onValueChange, modifier, enabled, label ->
if (enabled != null) {
PasswordTextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
enabled = enabled,
label = label,
)
} else {
PasswordTextFieldOutlined(
value = value,
onValueChange = onValueChange,
modifier = modifier,
label = label,
)
}
},
),
)
}
}